aiwf 0.3.0 → 0.3.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +38 -36
  2. package/CLAUDE.md +8 -209
  3. package/README.md +25 -5
  4. package/claude-code/aiwf/en/.claude/commands/aiwf/aiwf_docs.md +225 -0
  5. package/claude-code/aiwf/en/.claude/commands/aiwf/aiwf_language_manager.md +287 -0
  6. package/claude-code/aiwf/en/.claude/commands/aiwf/aiwf_language_status.md +246 -0
  7. package/claude-code/aiwf/en/.claude/commands/aiwf/aiwf_switch_language.md +160 -0
  8. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_code_review.md +62 -62
  9. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_commit.md +111 -111
  10. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_create_general_task.md +106 -106
  11. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_create_milestone_plan.md +127 -127
  12. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_create_sprint_tasks.md +130 -130
  13. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_create_sprints_from_milestone.md +87 -87
  14. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_do_task.md +72 -72
  15. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_docs.md +225 -0
  16. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_initialize.md +86 -86
  17. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_issue_create.md +3 -3
  18. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_language_manager.md +287 -0
  19. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_language_status.md +246 -0
  20. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_pr_create.md +14 -14
  21. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_prime.md +5 -5
  22. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_project_review.md +39 -39
  23. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_switch_language.md +160 -0
  24. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_test.md +76 -77
  25. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_testing_review.md +120 -120
  26. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_ultrathink_general.md +209 -266
  27. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_yolo.md +226 -226
  28. package/docs/AI-WORKFLOW.ko.md +9 -2
  29. package/docs/AI-WORKFLOW.md +9 -2
  30. package/docs/COMMANDS_GUIDE.ko.md +93 -3
  31. package/docs/COMMANDS_GUIDE.md +94 -4
  32. package/docs/Enhanced_Installation_Flow_Design.md +498 -0
  33. package/docs/PRD.ko.md +110 -58
  34. package/docs/PRD.md +110 -58
  35. package/index.js +1240 -20
  36. package/jest.config.js +4 -0
  37. package/language-cli.js +279 -0
  38. package/language-utils.js +330 -0
  39. package/package.json +12 -4
  40. package/scripts/validate-commands.cjs +338 -0
  41. package/scripts/validate-commands.js +254 -0
  42. package/tests/basic.test.js +62 -0
  43. package/tests/commands.test.js +127 -0
  44. package/tests/installer.test.js +129 -0
  45. package/tests/language-utils.test.js +271 -0
  46. package/claude-code/aiwf/en/.claude/commands/aiwf/aiwf_prime_context.md +0 -5
  47. package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_prime_context.md +0 -8
package/index.js CHANGED
@@ -10,13 +10,21 @@ import { fileURLToPath } from 'url';
10
10
  import https from 'https';
11
11
  import { createWriteStream } from 'fs';
12
12
  import { pipeline } from 'stream/promises';
13
+ import {
14
+ detectLanguage,
15
+ loadUserLanguageConfig,
16
+ saveUserLanguageConfig,
17
+ getInstallationLanguagePath,
18
+ SUPPORTED_LANGUAGES,
19
+ DEFAULT_LANGUAGE
20
+ } from './language-utils.js';
13
21
 
14
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
23
 
16
24
  const program = new Command();
17
25
 
18
26
  const GITHUB_API_URL = 'https://api.github.com/repos/moonklabs/aiwf';
19
- const GITHUB_RAW_URL = 'https://raw.githubusercontent.com/moonklabs/aiwf/master';
27
+ const GITHUB_RAW_URL = 'https://raw.githubusercontent.com/moonklabs/aiwf/main';
20
28
  const GITHUB_CONTENT_PREFIX = 'claude-code/aiwf';
21
29
 
22
30
  async function fetchGitHubContent(url) {
@@ -99,15 +107,34 @@ async function backupFile(filePath, backupDir) {
99
107
  await fs.mkdir(backupDir, { recursive: true });
100
108
  const backupPath = `${filePath}.bak`;
101
109
  await fs.copyFile(filePath, backupPath);
102
- // 상대경로 보존: .claude/commands/aiwf/foo.md -> backupDir/claude-commands-aiwf-foo.md.bak
110
+
111
+ // 도구별 백업 파일 명명 규칙 개선
103
112
  let bakFileName;
104
- if (filePath.startsWith(COMMANDS_DIR + '/')) {
105
- bakFileName = filePath.replace(/\//g, '-').replace(/^\./, '') + '.bak';
113
+ if (filePath.startsWith('.claude/commands/aiwf/')) {
114
+ // Claude Code 명령어 파일
115
+ const relativePath = filePath.substring('.claude/commands/aiwf/'.length);
116
+ bakFileName = `claude-commands-aiwf-${relativePath.replace(/\//g, '-')}.bak`;
117
+ } else if (filePath.startsWith('.gemini/prompts/aiwf/')) {
118
+ // Gemini CLI 프롬프트 파일
119
+ const relativePath = filePath.substring('.gemini/prompts/aiwf/'.length);
120
+ bakFileName = `gemini-prompts-aiwf-${relativePath.replace(/\//g, '-')}.bak`;
121
+ } else if (filePath.startsWith('.cursor/rules/')) {
122
+ // Cursor 규칙 파일
123
+ const relativePath = filePath.substring('.cursor/rules/'.length);
124
+ bakFileName = `cursor-rules-${relativePath.replace(/\//g, '-')}.bak`;
125
+ } else if (filePath.startsWith('.windsurf/rules/')) {
126
+ // Windsurf 규칙 파일
127
+ const relativePath = filePath.substring('.windsurf/rules/'.length);
128
+ bakFileName = `windsurf-rules-${relativePath.replace(/\//g, '-')}.bak`;
106
129
  } else if (filePath.startsWith('.aiwf/')) {
107
- bakFileName = filePath.replace(/\//g, '-').replace(/^\./, '') + '.bak';
130
+ // AIWF 핵심 파일
131
+ const relativePath = filePath.substring('.aiwf/'.length);
132
+ bakFileName = `aiwf-${relativePath.replace(/\//g, '-')}.bak`;
108
133
  } else {
134
+ // 기타 파일
109
135
  bakFileName = path.basename(filePath) + '.bak';
110
136
  }
137
+
111
138
  const destPath = path.join(backupDir, bakFileName);
112
139
  await fs.rename(backupPath, destPath);
113
140
  return destPath;
@@ -131,6 +158,7 @@ async function backupCommandsAndDocs(msg) {
131
158
  '.aiwf/03_SPRINTS/CLAUDE.md',
132
159
  '.aiwf/04_GENERAL_TASKS/CLAUDE.md'
133
160
  ];
161
+
134
162
  // Backup CLAUDE.md files
135
163
  for (const file of filesToBackup) {
136
164
  const backupPath = await backupFile(file, BACKUP_DIR);
@@ -138,6 +166,7 @@ async function backupCommandsAndDocs(msg) {
138
166
  backedUpFiles.push(backupPath);
139
167
  }
140
168
  }
169
+
141
170
  // Backup all command files
142
171
  const commandsExist = await fs.access(COMMANDS_DIR).then(() => true).catch(() => false);
143
172
  if (commandsExist) {
@@ -157,6 +186,45 @@ async function backupCommandsAndDocs(msg) {
157
186
  // Commands directory might be empty or have issues
158
187
  }
159
188
  }
189
+
190
+ // Backup IDE rule files (Cursor)
191
+ const cursorRulesExist = await fs.access('.cursor/rules').then(() => true).catch(() => false);
192
+ if (cursorRulesExist) {
193
+ try {
194
+ const cursorFiles = await fs.readdir('.cursor/rules');
195
+ for (const file of cursorFiles) {
196
+ if (file.endsWith('.mdc')) {
197
+ const filePath = path.join('.cursor/rules', file);
198
+ const backupPath = await backupFile(filePath, BACKUP_DIR);
199
+ if (backupPath) {
200
+ backedUpFiles.push(backupPath);
201
+ }
202
+ }
203
+ }
204
+ } catch (error) {
205
+ // Cursor rules directory might have issues
206
+ }
207
+ }
208
+
209
+ // Backup IDE rule files (Windsurf)
210
+ const windsurfRulesExist = await fs.access('.windsurf/rules').then(() => true).catch(() => false);
211
+ if (windsurfRulesExist) {
212
+ try {
213
+ const windsurfFiles = await fs.readdir('.windsurf/rules');
214
+ for (const file of windsurfFiles) {
215
+ if (file.endsWith('.md')) {
216
+ const filePath = path.join('.windsurf/rules', file);
217
+ const backupPath = await backupFile(filePath, BACKUP_DIR);
218
+ if (backupPath) {
219
+ backedUpFiles.push(backupPath);
220
+ }
221
+ }
222
+ }
223
+ } catch (error) {
224
+ // Windsurf rules directory might have issues
225
+ }
226
+ }
227
+
160
228
  if (backedUpFiles.length > 0) {
161
229
  spinner.succeed(chalk.green(msg.backupComplete.replace('{count}', backedUpFiles.length)));
162
230
  } else {
@@ -310,24 +378,1128 @@ const messages = {
310
378
  }
311
379
  };
312
380
 
381
+ // =============================================================================
382
+ // INSTALLATION VALIDATION SYSTEM
383
+ // =============================================================================
384
+
385
+ // 각 도구별 필수 파일 목록 정의
386
+ const ESSENTIAL_FILES = {
387
+ claudeCode: {
388
+ baseDir: '.claude/commands/aiwf',
389
+ requiredFiles: [
390
+ 'aiwf_initialize.md',
391
+ 'aiwf_do_task.md',
392
+ 'aiwf_commit.md',
393
+ 'aiwf_create_sprint_tasks.md',
394
+ 'aiwf_code_review.md'
395
+ ],
396
+ minFileCount: 5,
397
+ description: 'Claude Code Commands'
398
+ },
399
+ geminiCLI: {
400
+ baseDir: '.gemini/prompts/aiwf',
401
+ requiredFiles: [
402
+ 'README.md'
403
+ ],
404
+ minFileCount: 1,
405
+ description: 'Gemini-CLI Prompts'
406
+ },
407
+ cursor: {
408
+ baseDir: '.cursor/rules',
409
+ requiredFiles: [],
410
+ minFileCount: 2,
411
+ fileExtension: '.mdc',
412
+ description: 'Cursor IDE Rules'
413
+ },
414
+ windsurf: {
415
+ baseDir: '.windsurf/rules',
416
+ requiredFiles: [],
417
+ minFileCount: 2,
418
+ fileExtension: '.md',
419
+ description: 'Windsurf IDE Rules'
420
+ },
421
+ aiwf: {
422
+ baseDir: '.aiwf',
423
+ requiredFiles: [
424
+ 'CLAUDE.md',
425
+ '00_PROJECT_MANIFEST.md'
426
+ ],
427
+ minFileCount: 2,
428
+ description: 'AIWF Core Structure'
429
+ }
430
+ };
431
+
432
+ // 범용 검증 함수 (모든 도구에서 사용)
433
+ async function validateToolInstallation(config) {
434
+ const results = {
435
+ success: true,
436
+ description: config.description,
437
+ baseDir: config.baseDir,
438
+ checks: {
439
+ directoryExists: false,
440
+ fileCount: 0,
441
+ requiredFiles: [],
442
+ totalSize: 0,
443
+ issues: []
444
+ }
445
+ };
446
+
447
+ try {
448
+ // 1. 디렉토리 존재 확인
449
+ await fs.access(config.baseDir);
450
+ const dirStat = await fs.stat(config.baseDir);
451
+ if (dirStat.isDirectory()) {
452
+ results.checks.directoryExists = true;
453
+ } else {
454
+ results.checks.issues.push(`${config.baseDir} is not a directory`);
455
+ results.success = false;
456
+ return results;
457
+ }
458
+
459
+ // 2. 파일 목록 가져오기
460
+ const actualFiles = await fs.readdir(config.baseDir);
461
+ results.checks.fileCount = actualFiles.length;
462
+
463
+ // 3. 필수 파일 존재 확인
464
+ for (const requiredFile of config.requiredFiles || []) {
465
+ const filePath = path.join(config.baseDir, requiredFile);
466
+ const fileCheck = {
467
+ file: requiredFile,
468
+ exists: false,
469
+ size: 0,
470
+ valid: false
471
+ };
472
+
473
+ if (actualFiles.includes(requiredFile)) {
474
+ fileCheck.exists = true;
475
+ try {
476
+ const stats = await fs.stat(filePath);
477
+ fileCheck.size = stats.size;
478
+ results.checks.totalSize += stats.size;
479
+
480
+ // 파일 크기 검증 (최소 10바이트)
481
+ if (stats.size < 10) {
482
+ results.checks.issues.push(`File ${requiredFile} is too small (${stats.size} bytes)`);
483
+ results.success = false;
484
+ } else {
485
+ fileCheck.valid = true;
486
+ }
487
+ } catch (error) {
488
+ results.checks.issues.push(`Cannot read stats for ${requiredFile}: ${error.message}`);
489
+ results.success = false;
490
+ }
491
+ } else {
492
+ results.checks.issues.push(`Required file ${requiredFile} is missing`);
493
+ results.success = false;
494
+ }
495
+
496
+ results.checks.requiredFiles.push(fileCheck);
497
+ }
498
+
499
+ // 4. 파일 확장자 기반 검증 (Cursor/Windsurf)
500
+ if (config.fileExtension) {
501
+ const matchingFiles = actualFiles.filter(f => f.endsWith(config.fileExtension));
502
+ if (matchingFiles.length < config.minFileCount) {
503
+ results.checks.issues.push(`Expected at least ${config.minFileCount} ${config.fileExtension} files, found ${matchingFiles.length}`);
504
+ results.success = false;
505
+ }
506
+
507
+ // 확장자별 파일 크기 검증
508
+ for (const file of matchingFiles) {
509
+ const filePath = path.join(config.baseDir, file);
510
+ try {
511
+ const stats = await fs.stat(filePath);
512
+ results.checks.totalSize += stats.size;
513
+
514
+ if (stats.size < 50) { // Rules 파일은 최소 50바이트
515
+ results.checks.issues.push(`Rules file ${file} is too small (${stats.size} bytes)`);
516
+ results.success = false;
517
+ }
518
+ } catch (error) {
519
+ results.checks.issues.push(`Cannot read ${file}: ${error.message}`);
520
+ results.success = false;
521
+ }
522
+ }
523
+ }
524
+
525
+ // 5. 최소 파일 개수 검증
526
+ if (results.checks.fileCount < config.minFileCount) {
527
+ results.checks.issues.push(`Expected at least ${config.minFileCount} files, found ${results.checks.fileCount}`);
528
+ results.success = false;
529
+ }
530
+
531
+ } catch (error) {
532
+ results.success = false;
533
+ results.checks.issues.push(`Validation error: ${error.message}`);
534
+ }
535
+
536
+ return results;
537
+ }
538
+
539
+ // 개별 도구 검증 함수들
540
+ async function validateClaudeInstall() {
541
+ return await validateToolInstallation(ESSENTIAL_FILES.claudeCode);
542
+ }
543
+
544
+ async function validateGeminiInstall() {
545
+ return await validateToolInstallation(ESSENTIAL_FILES.geminiCLI);
546
+ }
547
+
548
+ async function validateCursorInstall() {
549
+ return await validateToolInstallation(ESSENTIAL_FILES.cursor);
550
+ }
551
+
552
+ async function validateWindsurfInstall() {
553
+ return await validateToolInstallation(ESSENTIAL_FILES.windsurf);
554
+ }
555
+
556
+ async function validateAIWFCore() {
557
+ return await validateToolInstallation(ESSENTIAL_FILES.aiwf);
558
+ }
559
+
560
+ // Legacy detailed validation (for backward compatibility)
561
+ async function validateInstallationDetailed() {
562
+ const validationResults = {
563
+ overall: {
564
+ success: true,
565
+ toolsChecked: 0,
566
+ toolsPassed: 0,
567
+ toolsFailed: 0,
568
+ totalIssues: 0
569
+ },
570
+ tools: {}
571
+ };
572
+
573
+ // AIWF 코어는 항상 검증
574
+ const aiwfResult = await validateAIWFCore();
575
+ validationResults.tools.aiwf = aiwfResult;
576
+ validationResults.overall.toolsChecked++;
577
+
578
+ if (aiwfResult.success) {
579
+ validationResults.overall.toolsPassed++;
580
+ } else {
581
+ validationResults.overall.toolsFailed++;
582
+ validationResults.overall.success = false;
583
+ validationResults.overall.totalIssues += aiwfResult.checks.issues.length;
584
+ }
585
+
586
+ // 모든 도구 검증 (이 간단한 버전에서는 항상 모든 도구 설치)
587
+ const toolValidators = {
588
+ claudeCode: validateClaudeInstall,
589
+ geminiCLI: validateGeminiInstall,
590
+ cursor: validateCursorInstall,
591
+ windsurf: validateWindsurfInstall
592
+ };
593
+
594
+ for (const [toolName, validator] of Object.entries(toolValidators)) {
595
+ const result = await validator();
596
+ validationResults.tools[toolName] = result;
597
+ validationResults.overall.toolsChecked++;
598
+
599
+ if (result.success) {
600
+ validationResults.overall.toolsPassed++;
601
+ } else {
602
+ validationResults.overall.toolsFailed++;
603
+ validationResults.overall.success = false;
604
+ validationResults.overall.totalIssues += result.checks.issues.length;
605
+ }
606
+ }
607
+
608
+ return validationResults;
609
+ }
610
+
611
+ // 향상된 설치 검증 함수 (선택적 도구 지원)
612
+ async function validateInstallationEnhanced(selectedTools = null, language = 'en') {
613
+ const validationResults = {
614
+ overall: {
615
+ success: true,
616
+ toolsChecked: 0,
617
+ toolsPassed: 0,
618
+ toolsFailed: 0,
619
+ totalIssues: 0
620
+ },
621
+ tools: {}
622
+ };
623
+
624
+ // AIWF 코어는 항상 검증
625
+ const aiwfResult = await validateAIWFCore();
626
+ validationResults.tools.aiwf = aiwfResult;
627
+ validationResults.overall.toolsChecked++;
628
+
629
+ if (aiwfResult.success) {
630
+ validationResults.overall.toolsPassed++;
631
+ } else {
632
+ validationResults.overall.toolsFailed++;
633
+ validationResults.overall.success = false;
634
+ validationResults.overall.totalIssues += aiwfResult.checks.issues.length;
635
+ }
636
+
637
+ // 도구 목록 결정 (선택된 도구 또는 모든 도구)
638
+ const toolsToValidate = selectedTools || ['claudeCode', 'geminiCLI', 'cursor', 'windsurf'];
639
+
640
+ const toolValidators = {
641
+ claudeCode: validateClaudeInstall,
642
+ geminiCLI: validateGeminiInstall,
643
+ cursor: validateCursorInstall,
644
+ windsurf: validateWindsurfInstall
645
+ };
646
+
647
+ for (const toolName of toolsToValidate) {
648
+ if (toolValidators[toolName]) {
649
+ const result = await toolValidators[toolName]();
650
+ validationResults.tools[toolName] = result;
651
+ validationResults.overall.toolsChecked++;
652
+
653
+ if (result.success) {
654
+ validationResults.overall.toolsPassed++;
655
+ } else {
656
+ validationResults.overall.toolsFailed++;
657
+ validationResults.overall.success = false;
658
+ validationResults.overall.totalIssues += result.checks.issues.length;
659
+ }
660
+ }
661
+ }
662
+
663
+ return validationResults;
664
+ }
665
+
666
+ // Display validation results for specification-compliant format
667
+ function displaySpecCompliantValidationResults(validationResults, language) {
668
+ const msg = messages[language];
669
+
670
+ console.log(chalk.blue('\n=== Installation Validation Results ==='));
671
+
672
+ // Success summary
673
+ if (validationResults.success.length > 0) {
674
+ console.log(chalk.green(`\n✅ Successfully Validated Tools (${validationResults.success.length}):`));
675
+ validationResults.success.forEach(tool => {
676
+ console.log(chalk.green(` ✓ ${tool}`));
677
+ });
678
+ }
679
+
680
+ // Failed summary
681
+ if (validationResults.failed.length > 0) {
682
+ console.log(chalk.red(`\n❌ Failed Validations (${validationResults.failed.length}):`));
683
+ validationResults.failed.forEach(({tool, reason}) => {
684
+ console.log(chalk.red(` ✗ ${tool}: ${reason}`));
685
+ });
686
+ }
687
+
688
+ // Warnings summary
689
+ if (validationResults.warnings.length > 0) {
690
+ console.log(chalk.yellow(`\n⚠️ Warnings (${validationResults.warnings.length}):`));
691
+ validationResults.warnings.forEach(warning => {
692
+ console.log(chalk.yellow(` ⚠ ${warning}`));
693
+ });
694
+ }
695
+
696
+ // Overall status
697
+ const isSuccess = validationResults.failed.length === 0;
698
+ const statusIcon = isSuccess ? '✅' : '❌';
699
+ const statusColor = isSuccess ? chalk.green : chalk.red;
700
+ const statusText = isSuccess ? 'PASSED' : 'FAILED';
701
+
702
+ console.log(statusColor(`\n${statusIcon} Overall Validation: ${statusText}`));
703
+
704
+ if (isSuccess) {
705
+ console.log(chalk.green('🎉 All selected tools are properly installed and validated!'));
706
+ } else {
707
+ console.log(chalk.yellow('⚠️ Some installations failed. See details above.'));
708
+ }
709
+ }
710
+
711
+ // Enhanced validation function with simplified interface for T05_S02
712
+ async function validateInstallation(selectedTools, language) {
713
+ const results = {
714
+ success: [],
715
+ failed: [],
716
+ warnings: []
717
+ };
718
+
719
+ // Always validate common files (AIWF core)
720
+ const commonValid = await validateCommonFiles();
721
+ if (!commonValid.success) {
722
+ results.failed.push({ tool: 'aiwf', reason: commonValid.reason });
723
+ } else {
724
+ results.success.push('aiwf');
725
+ }
726
+
727
+ // Validate selected tools
728
+ if (selectedTools && selectedTools.length > 0) {
729
+ for (const tool of selectedTools) {
730
+ const validation = await validateTool(tool);
731
+ if (validation.success) {
732
+ results.success.push(tool);
733
+ } else {
734
+ results.failed.push({ tool, reason: validation.reason });
735
+ }
736
+ }
737
+ } else {
738
+ // Default: validate all tools if no selection provided
739
+ const allTools = ['claudeCode', 'geminiCLI', 'cursor', 'windsurf'];
740
+ for (const tool of allTools) {
741
+ const validation = await validateTool(tool);
742
+ if (validation.success) {
743
+ results.success.push(tool);
744
+ } else {
745
+ results.failed.push({ tool, reason: validation.reason });
746
+ }
747
+ }
748
+ }
749
+
750
+ return results;
751
+ }
752
+
753
+ // Helper function to validate common/core files
754
+ async function validateCommonFiles() {
755
+ try {
756
+ const requiredFiles = [
757
+ '.aiwf/CLAUDE.md',
758
+ '.aiwf/00_PROJECT_MANIFEST.md'
759
+ ];
760
+
761
+ for (const file of requiredFiles) {
762
+ const exists = await fs.access(file).then(() => true).catch(() => false);
763
+ if (!exists) {
764
+ return { success: false, reason: `Missing core file: ${file}` };
765
+ }
766
+ }
767
+
768
+ return { success: true };
769
+ } catch (error) {
770
+ return { success: false, reason: `Core validation error: ${error.message}` };
771
+ }
772
+ }
773
+
774
+ // Helper function to validate specific tools with simplified interface
775
+ async function validateTool(tool) {
776
+ switch (tool) {
777
+ case 'claudeCode':
778
+ case 'claude-code':
779
+ return validateClaudeCode();
780
+ case 'geminiCLI':
781
+ case 'gemini-cli':
782
+ return validateGeminiCLI();
783
+ case 'cursor':
784
+ return validateCursorTool();
785
+ case 'windsurf':
786
+ return validateWindsurfTool();
787
+ case 'aiwf':
788
+ return validateCommonFiles();
789
+ default:
790
+ return { success: false, reason: `Unknown tool: ${tool}` };
791
+ }
792
+ }
793
+
794
+ // Simplified tool validation functions
795
+ async function validateClaudeCode() {
796
+ try {
797
+ const requiredFiles = [
798
+ '.claude/commands/aiwf/aiwf_initialize.md',
799
+ '.claude/commands/aiwf/aiwf_do_task.md',
800
+ '.claude/commands/aiwf/aiwf_commit.md',
801
+ '.claude/commands/aiwf/aiwf_code_review.md'
802
+ ];
803
+
804
+ for (const file of requiredFiles) {
805
+ const exists = await fs.access(file).then(() => true).catch(() => false);
806
+ if (!exists) {
807
+ return { success: false, reason: `Missing file: ${file}` };
808
+ }
809
+ }
810
+
811
+ return { success: true };
812
+ } catch (error) {
813
+ return { success: false, reason: `Claude Code validation error: ${error.message}` };
814
+ }
815
+ }
816
+
817
+ async function validateGeminiCLI() {
818
+ try {
819
+ // Check if .gemini/prompts/aiwf directory exists
820
+ const dirExists = await fs.access('.gemini/prompts/aiwf').then(() => true).catch(() => false);
821
+ if (!dirExists) {
822
+ return { success: false, reason: 'Missing Gemini-CLI prompts directory: .gemini/prompts/aiwf' };
823
+ }
824
+
825
+ return { success: true };
826
+ } catch (error) {
827
+ return { success: false, reason: `Gemini-CLI validation error: ${error.message}` };
828
+ }
829
+ }
830
+
831
+ async function validateCursorTool() {
832
+ try {
833
+ const dirExists = await fs.access('.cursor/rules').then(() => true).catch(() => false);
834
+ if (!dirExists) {
835
+ return { success: false, reason: 'Missing Cursor rules directory: .cursor/rules' };
836
+ }
837
+
838
+ return { success: true };
839
+ } catch (error) {
840
+ return { success: false, reason: `Cursor validation error: ${error.message}` };
841
+ }
842
+ }
843
+
844
+ async function validateWindsurfTool() {
845
+ try {
846
+ const dirExists = await fs.access('.windsurf/rules').then(() => true).catch(() => false);
847
+ if (!dirExists) {
848
+ return { success: false, reason: 'Missing Windsurf rules directory: .windsurf/rules' };
849
+ }
850
+
851
+ return { success: true };
852
+ } catch (error) {
853
+ return { success: false, reason: `Windsurf validation error: ${error.message}` };
854
+ }
855
+ }
856
+
857
+ // Enhanced rollback function for T05_S02
858
+ async function rollbackToolEnhanced(tool, backupDir, language = 'ko') {
859
+ const spinner = ora(`Rolling back ${tool}...`).start();
860
+
861
+ try {
862
+ // Get tool directory mapping
863
+ const toolDir = getToolDirectory(tool);
864
+ if (!toolDir) {
865
+ spinner.fail(`Unknown tool: ${tool}`);
866
+ return false;
867
+ }
868
+
869
+ // Remove installed directory
870
+ try {
871
+ await fs.access(toolDir);
872
+ await fs.rm(toolDir, { recursive: true, force: true });
873
+ spinner.text = `Removed ${toolDir}`;
874
+ } catch (error) {
875
+ // Directory doesn't exist, skip
876
+ }
877
+
878
+ // Restore from backup if available
879
+ if (backupDir) {
880
+ const backupFiles = await getBackupFilesForTool(tool, backupDir);
881
+ for (const backup of backupFiles) {
882
+ await restoreBackupFile(backup);
883
+ spinner.text = `Restored ${backup.originalPath}`;
884
+ }
885
+ }
886
+
887
+ spinner.succeed(`${tool} rolled back successfully`);
888
+ return true;
889
+ } catch (error) {
890
+ spinner.fail(`Failed to rollback ${tool}: ${error.message}`);
891
+ return false;
892
+ }
893
+ }
894
+
895
+ // Helper function to get tool directories
896
+ function getToolDirectory(tool) {
897
+ const toolDirs = {
898
+ claudeCode: '.claude/commands/aiwf',
899
+ 'claude-code': '.claude/commands/aiwf',
900
+ geminiCLI: '.gemini/prompts/aiwf',
901
+ 'gemini-cli': '.gemini/prompts/aiwf',
902
+ cursor: '.cursor/rules',
903
+ windsurf: '.windsurf/rules',
904
+ aiwf: '.aiwf'
905
+ };
906
+
907
+ return toolDirs[tool] || null;
908
+ }
909
+
910
+ // Helper function to restore backup file
911
+ async function restoreBackupFile(backup) {
912
+ try {
913
+ // Ensure target directory exists
914
+ const targetDir = path.dirname(backup.originalPath);
915
+ await fs.mkdir(targetDir, { recursive: true });
916
+
917
+ // Copy backup file to original location
918
+ await fs.copyFile(backup.backupFile, backup.originalPath);
919
+ } catch (error) {
920
+ throw new Error(`Failed to restore ${backup.originalPath}: ${error.message}`);
921
+ }
922
+ }
923
+
924
+ // Enhanced installation report generation function for T05_S02
925
+ function generateInstallationReportEnhanced(results, language) {
926
+ const msg = messages[language];
927
+
928
+ console.log('\n' + chalk.bold(language === 'ko' ? '📋 설치 리포트' : '📋 Installation Report'));
929
+ console.log('─'.repeat(50));
930
+
931
+ if (results.success.length > 0) {
932
+ console.log(chalk.green(language === 'ko' ? '✅ 성공적으로 설치된 도구:' : '✅ Successfully Installed Tools:'));
933
+ results.success.forEach(tool => {
934
+ console.log(chalk.green(` ✓ ${getToolName(tool, language)}`));
935
+ });
936
+ }
937
+
938
+ if (results.failed.length > 0) {
939
+ console.log(chalk.red(language === 'ko' ? '\n❌ 설치 실패한 도구:' : '\n❌ Failed to Install:'));
940
+ results.failed.forEach(({ tool, reason }) => {
941
+ console.log(chalk.red(` ✗ ${getToolName(tool, language)}: ${reason}`));
942
+ });
943
+ }
944
+
945
+ if (results.warnings.length > 0) {
946
+ console.log(chalk.yellow(language === 'ko' ? '\n⚠️ 경고:' : '\n⚠️ Warnings:'));
947
+ results.warnings.forEach(warning => {
948
+ console.log(chalk.yellow(` ⚠ ${warning}`));
949
+ });
950
+ }
951
+
952
+ console.log('─'.repeat(50));
953
+ }
954
+
955
+ // Helper function to get localized tool names
956
+ function getToolName(tool, language) {
957
+ const toolNames = {
958
+ ko: {
959
+ aiwf: 'AIWF 코어 구조',
960
+ claudeCode: 'Claude Code 명령어',
961
+ 'claude-code': 'Claude Code 명령어',
962
+ geminiCLI: 'Gemini-CLI 프롬프트',
963
+ 'gemini-cli': 'Gemini-CLI 프롬프트',
964
+ cursor: 'Cursor IDE 규칙',
965
+ windsurf: 'Windsurf IDE 규칙'
966
+ },
967
+ en: {
968
+ aiwf: 'AIWF Core Structure',
969
+ claudeCode: 'Claude Code Commands',
970
+ 'claude-code': 'Claude Code Commands',
971
+ geminiCLI: 'Gemini-CLI Prompts',
972
+ 'gemini-cli': 'Gemini-CLI Prompts',
973
+ cursor: 'Cursor IDE Rules',
974
+ windsurf: 'Windsurf IDE Rules'
975
+ }
976
+ };
977
+
978
+ return toolNames[language]?.[tool] || tool;
979
+ }
980
+
981
+ // Enhanced installation flow with validation and rollback for T05_S02
982
+ async function performInstallationStepEnhanced(language, selectedTools) {
983
+ try {
984
+ // ... installation performed by existing code ...
985
+ // This function would be integrated into the main installation flow
986
+
987
+ // After installation, perform validation
988
+ const validation = await validateInstallation(selectedTools, language);
989
+
990
+ // Handle failed tools with rollback
991
+ if (validation.failed.length > 0) {
992
+ console.log(chalk.yellow('\n⚠️ Some installations failed. Initiating rollback...'));
993
+
994
+ for (const { tool } of validation.failed) {
995
+ console.log(chalk.yellow(`Rolling back ${tool}...`));
996
+ await rollbackToolEnhanced(tool, BACKUP_DIR, language);
997
+ }
998
+ }
999
+
1000
+ // Generate installation report
1001
+ generateInstallationReportEnhanced(validation, language);
1002
+
1003
+ // Throw error if complete failure
1004
+ if (validation.success.length === 0) {
1005
+ const errorMsg = language === 'ko' ?
1006
+ '모든 설치가 실패했습니다.' :
1007
+ 'All installations failed.';
1008
+ throw new Error(errorMsg);
1009
+ }
1010
+
1011
+ return validation;
1012
+ } catch (error) {
1013
+ console.error(chalk.red(`Installation step failed: ${error.message}`));
1014
+ throw error;
1015
+ }
1016
+ }
1017
+
1018
+ // 설치 리포트 생성 함수 (통합된 버전)
1019
+ function generateInstallationReport(validationResults, language, installationType = 'install') {
1020
+ const msg = messages[language];
1021
+
1022
+ // 상단 헤더
1023
+ console.log(chalk.blue.bold('\n' + '='.repeat(60)));
1024
+ console.log(chalk.blue.bold(
1025
+ language === 'ko' ?
1026
+ '🔍 AIWF 설치 검증 및 리포트' :
1027
+ '🔍 AIWF Installation Validation & Report'
1028
+ ));
1029
+ console.log(chalk.blue.bold('='.repeat(60)));
1030
+
1031
+ // 설치 유형 표시
1032
+ const installTypeText = {
1033
+ install: language === 'ko' ? '새 설치' : 'Fresh Installation',
1034
+ update: language === 'ko' ? '업데이트' : 'Update',
1035
+ reinstall: language === 'ko' ? '재설치' : 'Reinstallation'
1036
+ };
1037
+ console.log(chalk.gray(`설치 유형 / Installation Type: ${installTypeText[installationType] || installTypeText.install}`));
1038
+ console.log(chalk.gray(`검증 시간 / Validation Time: ${new Date().toLocaleString()}`));
1039
+
1040
+ // 전체 요약
1041
+ const { overall } = validationResults;
1042
+ const statusIcon = overall.success ? '✅' : '❌';
1043
+ const statusColor = overall.success ? chalk.green : chalk.red;
1044
+
1045
+ console.log(statusColor(`\n${statusIcon} 전체 상태 / Overall Status: ${overall.success ? 'PASSED' : 'FAILED'}`));
1046
+ console.log(chalk.gray(` 검증한 도구 / Tools Checked: ${overall.toolsChecked}`));
1047
+ console.log(chalk.gray(` 성공한 도구 / Tools Passed: ${overall.toolsPassed}`));
1048
+
1049
+ if (overall.toolsFailed > 0) {
1050
+ console.log(chalk.red(` 실패한 도구 / Tools Failed: ${overall.toolsFailed}`));
1051
+ console.log(chalk.red(` 총 이슈 수 / Total Issues: ${overall.totalIssues}`));
1052
+ }
1053
+
1054
+ // 개별 도구 결과
1055
+ console.log(chalk.blue('\n=== 개별 도구 결과 / Individual Tool Results ==='));
1056
+
1057
+ for (const [toolName, result] of Object.entries(validationResults.tools)) {
1058
+ const toolIcon = result.success ? '✅' : '❌';
1059
+ const toolColor = result.success ? chalk.green : chalk.red;
1060
+
1061
+ console.log(toolColor(`\n${toolIcon} ${result.description} (${toolName})`));
1062
+ console.log(chalk.gray(` 디렉토리 / Directory: ${result.baseDir}`));
1063
+ console.log(chalk.gray(` 파일 수 / Files Found: ${result.checks.fileCount}`));
1064
+ console.log(chalk.gray(` 총 크기 / Total Size: ${(result.checks.totalSize / 1024).toFixed(2)} KB`));
1065
+
1066
+ if (result.checks.issues.length > 0) {
1067
+ console.log(chalk.red(` 이슈 / Issues:`));
1068
+ for (const issue of result.checks.issues) {
1069
+ console.log(chalk.red(` • ${issue}`));
1070
+ }
1071
+ }
1072
+
1073
+ if (result.checks.requiredFiles.length > 0) {
1074
+ console.log(chalk.gray(` 필수 파일 / Required Files:`));
1075
+ for (const fileCheck of result.checks.requiredFiles) {
1076
+ const fileIcon = fileCheck.valid ? '✅' : '❌';
1077
+ const fileColor = fileCheck.valid ? chalk.gray : chalk.red;
1078
+ console.log(fileColor(` ${fileIcon} ${fileCheck.file} (${(fileCheck.size / 1024).toFixed(2)} KB)`));
1079
+ }
1080
+ }
1081
+ }
1082
+
1083
+ // 권장사항 섹션
1084
+ console.log(chalk.blue('\n=== 권장사항 / Recommendations ==='));
1085
+ if (overall.success) {
1086
+ console.log(chalk.green('🎉 모든 도구가 올바르게 설치되어 검증되었습니다!'));
1087
+ console.log(chalk.green('🎉 All tools are properly installed and validated!'));
1088
+ console.log(chalk.blue('\n다음 단계 / Next Steps:'));
1089
+ console.log(chalk.white(' 1. Claude Code에서 프로젝트를 열어보세요'));
1090
+ console.log(chalk.white(' 2. /project:aiwf:initialize 명령어로 프로젝트를 초기화하세요'));
1091
+ console.log(chalk.white(' 3. 프로젝트 관리를 시작하세요!'));
1092
+ } else {
1093
+ console.log(chalk.yellow('⚠️ 일부 이슈가 발견되었습니다. 위의 세부사항을 확인하세요.'));
1094
+ console.log(chalk.yellow('⚠️ Some issues were found. See details above.'));
1095
+
1096
+ // 실패한 도구별 해결 방안 제시
1097
+ const failedTools = Object.entries(validationResults.tools)
1098
+ .filter(([_, result]) => !result.success)
1099
+ .map(([tool, _]) => tool);
1100
+
1101
+ if (failedTools.length > 0) {
1102
+ console.log(chalk.blue('\n해결 방안 / Solutions:'));
1103
+ console.log(chalk.yellow(' • 전체 재설치를 시도해보세요'));
1104
+ console.log(chalk.yellow(' • 실패한 도구만 롤백 후 다시 설치하세요'));
1105
+ console.log(chalk.yellow(' • 네트워크 연결을 확인하고 다시 시도하세요'));
1106
+ }
1107
+ }
1108
+
1109
+ console.log(chalk.blue.bold('\n' + '='.repeat(60)));
1110
+ }
1111
+
1112
+ // 검증 결과 표시 함수 (기존 함수는 유지, 새로운 리포트 함수와 함께 사용)
1113
+ function displayValidationResults(validationResults, language) {
1114
+ return generateInstallationReport(validationResults, language, 'install');
1115
+ }
1116
+
1117
+ // 도구별 디렉토리 매핑 함수 (중복 제거됨 - 위의 함수 사용)
1118
+
1119
+ // 특정 도구의 백업 파일 목록 가져오기
1120
+ async function getBackupFilesForTool(tool, backupDir) {
1121
+ const toolBackupFiles = [];
1122
+
1123
+ try {
1124
+ const backupFiles = await fs.readdir(backupDir);
1125
+ const toolDir = getToolDirectory(tool);
1126
+
1127
+ if (!toolDir) return toolBackupFiles;
1128
+
1129
+ // 도구별 백업 파일 패턴 매칭
1130
+ const patterns = {
1131
+ claudeCode: /^claude-commands-aiwf-.*\.bak$/,
1132
+ geminiCLI: /^gemini-prompts-aiwf-.*\.bak$/,
1133
+ cursor: /^cursor-rules-.*\.bak$/,
1134
+ windsurf: /^windsurf-rules-.*\.bak$/,
1135
+ aiwf: /^aiwf-.*\.bak$/
1136
+ };
1137
+
1138
+ const pattern = patterns[tool];
1139
+ if (pattern) {
1140
+ const matchingFiles = backupFiles.filter(file => pattern.test(file));
1141
+ for (const file of matchingFiles) {
1142
+ toolBackupFiles.push({
1143
+ backupFile: path.join(backupDir, file),
1144
+ originalPath: restoreOriginalPath(file, tool)
1145
+ });
1146
+ }
1147
+ }
1148
+
1149
+ } catch (error) {
1150
+ // 백업 디렉토리가 없거나 읽을 수 없는 경우
1151
+ }
1152
+
1153
+ return toolBackupFiles;
1154
+ }
1155
+
1156
+ // 백업 파일명에서 원본 경로 복원
1157
+ function restoreOriginalPath(backupFileName, tool) {
1158
+ const toolDirs = {
1159
+ claudeCode: '.claude/commands/aiwf',
1160
+ geminiCLI: '.gemini/prompts/aiwf',
1161
+ cursor: '.cursor/rules',
1162
+ windsurf: '.windsurf/rules',
1163
+ aiwf: '.aiwf'
1164
+ };
1165
+
1166
+ const baseDir = toolDirs[tool];
1167
+ if (!baseDir) return null;
1168
+
1169
+ // 백업 파일명에서 원본 파일명 추출
1170
+ let originalFileName = backupFileName;
1171
+
1172
+ // 도구별 프리픽스 제거
1173
+ const prefixes = {
1174
+ claudeCode: 'claude-commands-aiwf-',
1175
+ geminiCLI: 'gemini-prompts-aiwf-',
1176
+ cursor: 'cursor-rules-',
1177
+ windsurf: 'windsurf-rules-',
1178
+ aiwf: 'aiwf-'
1179
+ };
1180
+
1181
+ const prefix = prefixes[tool];
1182
+ if (prefix && originalFileName.startsWith(prefix)) {
1183
+ originalFileName = originalFileName.substring(prefix.length);
1184
+ }
1185
+
1186
+ // .bak 확장자 제거
1187
+ if (originalFileName.endsWith('.bak')) {
1188
+ originalFileName = originalFileName.substring(0, originalFileName.length - 4);
1189
+ }
1190
+
1191
+ // 하이픈을 슬래시로 복원 (하위 디렉토리 구조)
1192
+ originalFileName = originalFileName.replace(/-/g, '/');
1193
+
1194
+ return path.join(baseDir, originalFileName);
1195
+ }
1196
+
1197
+ // 개별 백업 파일 복원 (중복 제거됨 - 위의 함수 사용)
1198
+
1199
+ // 특정 도구 롤백 함수
1200
+ async function rollbackTool(tool, backupDir, language = 'ko') {
1201
+ const msg = messages[language];
1202
+ const spinner = ora(
1203
+ language === 'ko' ?
1204
+ `${tool} 도구를 롤백하는 중...` :
1205
+ `Rolling back ${tool}...`
1206
+ ).start();
1207
+
1208
+ try {
1209
+ // 1. 해당 도구의 디렉토리 삭제
1210
+ const toolDir = getToolDirectory(tool);
1211
+ if (toolDir) {
1212
+ const dirExists = await fs.access(toolDir).then(() => true).catch(() => false);
1213
+ if (dirExists) {
1214
+ await fs.rm(toolDir, { recursive: true, force: true });
1215
+ spinner.text = language === 'ko' ?
1216
+ `${tool} 디렉토리 삭제 완료` :
1217
+ `${tool} directory removed`;
1218
+ }
1219
+ }
1220
+
1221
+ // 2. 백업에서 복원
1222
+ const backupFiles = await getBackupFilesForTool(tool, backupDir);
1223
+ let restoredCount = 0;
1224
+
1225
+ for (const backup of backupFiles) {
1226
+ const restored = await restoreBackupFile(backup);
1227
+ if (restored) {
1228
+ restoredCount++;
1229
+ }
1230
+ }
1231
+
1232
+ if (restoredCount > 0) {
1233
+ spinner.succeed(chalk.green(
1234
+ language === 'ko' ?
1235
+ `${tool} 롤백 완료 (${restoredCount}개 파일 복원)` :
1236
+ `${tool} rolled back successfully (${restoredCount} files restored)`
1237
+ ));
1238
+ return { success: true, restoredCount };
1239
+ } else {
1240
+ spinner.warn(chalk.yellow(
1241
+ language === 'ko' ?
1242
+ `${tool} 롤백: 복원할 백업 파일이 없음` :
1243
+ `${tool} rollback: No backup files to restore`
1244
+ ));
1245
+ return { success: true, restoredCount: 0 };
1246
+ }
1247
+
1248
+ } catch (error) {
1249
+ spinner.fail(chalk.red(
1250
+ language === 'ko' ?
1251
+ `${tool} 롤백 실패: ${error.message}` :
1252
+ `Failed to rollback ${tool}: ${error.message}`
1253
+ ));
1254
+ return { success: false, error: error.message };
1255
+ }
1256
+ }
1257
+
1258
+ // =============================================================================
1259
+ // ROLLBACK SYSTEM IMPLEMENTATION
1260
+ // =============================================================================
1261
+
1262
+ // 도구별 디렉토리 경로 가져오기 (중복 제거됨 - 위의 getToolDirectory 함수 사용)
1263
+
1264
+ // 백업에서 특정 도구의 파일들 찾기 (중복 제거됨 - 위의 함수 사용)
1265
+
1266
+ // 백업 파일명에서 원래 경로 복원
1267
+ function restoreBackupPath(backupFileName, tool) {
1268
+ const toolDir = getToolDirectory(tool);
1269
+ if (!toolDir) return null;
1270
+
1271
+ // 백업 파일명 패턴: claude-commands-aiwf-foo.md.bak -> .claude/commands/aiwf/foo.md
1272
+ if (backupFileName.startsWith('claude-commands-aiwf-')) {
1273
+ const relativePath = backupFileName
1274
+ .replace('claude-commands-aiwf-', '')
1275
+ .replace('.bak', '')
1276
+ .replace(/-/g, '/');
1277
+ return path.join('.claude/commands/aiwf', relativePath);
1278
+ }
1279
+
1280
+ // 백업 파일명 패턴: aiwf-foo-bar.md.bak -> .aiwf/foo/bar.md
1281
+ if (backupFileName.startsWith('aiwf-')) {
1282
+ const relativePath = backupFileName
1283
+ .replace('aiwf-', '')
1284
+ .replace('.bak', '')
1285
+ .replace(/-/g, '/');
1286
+ return path.join('.aiwf', relativePath);
1287
+ }
1288
+
1289
+ // 기타 패턴
1290
+ return backupFileName.replace('.bak', '');
1291
+ }
1292
+
1293
+ // 개별 백업 파일 복원 (중복 제거됨 - 위의 함수 사용)
1294
+
1295
+ // 특정 도구 롤백 함수
1296
+
1297
+ // 다중 도구 롤백 함수
1298
+ async function rollbackMultipleTools(failedTools, backupDir, language = 'en') {
1299
+ const results = {
1300
+ success: [],
1301
+ failed: [],
1302
+ partial: []
1303
+ };
1304
+
1305
+ console.log(chalk.blue('\n🔄 Starting rollback process...'));
1306
+
1307
+ for (const toolInfo of failedTools) {
1308
+ const toolName = typeof toolInfo === 'string' ? toolInfo : toolInfo.tool;
1309
+ const rollbackResult = await rollbackTool(toolName, backupDir, language);
1310
+
1311
+ if (rollbackResult === true) {
1312
+ results.success.push(toolName);
1313
+ } else if (rollbackResult === false) {
1314
+ results.failed.push(toolName);
1315
+ } else {
1316
+ results.partial.push(toolName);
1317
+ }
1318
+ }
1319
+
1320
+ // 롤백 결과 요약
1321
+ console.log(chalk.blue('\n📋 Rollback Summary:'));
1322
+ if (results.success.length > 0) {
1323
+ console.log(chalk.green(`✅ Successfully rolled back: ${results.success.join(', ')}`));
1324
+ }
1325
+ if (results.partial.length > 0) {
1326
+ console.log(chalk.yellow(`⚠️ Partially rolled back: ${results.partial.join(', ')}`));
1327
+ }
1328
+ if (results.failed.length > 0) {
1329
+ console.log(chalk.red(`❌ Failed to rollback: ${results.failed.join(', ')}`));
1330
+ }
1331
+
1332
+ return results;
1333
+ }
1334
+
1335
+ // 개선된 재설치 옵션 제공 함수
1336
+ async function offerReinstallationOptions(validationResults, language) {
1337
+ console.log(chalk.yellow(
1338
+ language === 'ko' ?
1339
+ '\n⚠️ 일부 설치가 실패했습니다. 무엇을 하시겠습니까?' :
1340
+ '\n⚠️ Some installations failed. What would you like to do?'
1341
+ ));
1342
+
1343
+ // 실패한 도구들 나열 - 새로운 형식에 맞게 수정
1344
+ const failedTools = validationResults.failed.map(({tool}) => tool);
1345
+
1346
+ if (failedTools.length > 0) {
1347
+ console.log(chalk.red(
1348
+ language === 'ko' ?
1349
+ `실패한 도구: ${failedTools.join(', ')}` :
1350
+ `Failed tools: ${failedTools.join(', ')}`
1351
+ ));
1352
+ }
1353
+
1354
+ const response = await prompts({
1355
+ type: 'select',
1356
+ name: 'action',
1357
+ message: language === 'ko' ? '어떤 작업을 수행하시겠습니까?' : 'What would you like to do?',
1358
+ choices: [
1359
+ {
1360
+ title: language === 'ko' ? '전체 재설치' : 'Full reinstallation',
1361
+ value: 'reinstall'
1362
+ },
1363
+ {
1364
+ title: language === 'ko' ? '실패한 도구만 롤백' : 'Rollback failed tools only',
1365
+ value: 'rollback'
1366
+ },
1367
+ {
1368
+ title: language === 'ko' ? '현재 상태 유지' : 'Keep current state',
1369
+ value: 'keep'
1370
+ }
1371
+ ]
1372
+ });
1373
+
1374
+ return response.action;
1375
+ }
1376
+
1377
+ // =============================================================================
1378
+ // ROLLBACK SYSTEM
1379
+ // =============================================================================
1380
+
1381
+ // 도구별 롤백 함수
1382
+ // 도구별 백업 복원 함수
1383
+ async function restoreToolFromBackup(tool, backupDir) {
1384
+ try {
1385
+ const backupFiles = await fs.readdir(backupDir);
1386
+ const toolBackups = backupFiles.filter(f => f.endsWith('.bak'));
1387
+
1388
+ for (const backup of toolBackups) {
1389
+ let shouldRestore = false;
1390
+ let originalPath = '';
1391
+
1392
+ // 도구별 백업 파일 패턴 매칭
1393
+ switch (tool) {
1394
+ case 'claudeCode':
1395
+ if (backup.startsWith('claude-commands-aiwf-')) {
1396
+ shouldRestore = true;
1397
+ const fileName = backup.replace('claude-commands-aiwf-', '').replace('.bak', '');
1398
+ originalPath = path.join('.claude/commands/aiwf', fileName);
1399
+ }
1400
+ break;
1401
+ case 'aiwf':
1402
+ if (backup.startsWith('aiwf-')) {
1403
+ shouldRestore = true;
1404
+ const relativePath = backup.replace('aiwf-', '').replace(/-/g, '/').replace('.bak', '');
1405
+ originalPath = '.aiwf/' + relativePath;
1406
+ }
1407
+ break;
1408
+ // cursor와 windsurf는 현재 백업하지 않음 (rules 파일은 동적 생성)
1409
+ }
1410
+
1411
+ if (shouldRestore) {
1412
+ const backupPath = path.join(backupDir, backup);
1413
+ await fs.mkdir(path.dirname(originalPath), { recursive: true });
1414
+ await fs.copyFile(backupPath, originalPath);
1415
+ }
1416
+ }
1417
+ } catch (error) {
1418
+ // 백업 복원 실패는 심각하지 않음
1419
+ console.warn(`Warning: Could not restore ${tool} from backup: ${error.message}`);
1420
+ }
1421
+ }
1422
+
1423
+ // 실패한 도구들에 대한 일괄 롤백
1424
+ async function rollbackFailedTools(failedTools, language) {
1425
+ const results = {
1426
+ success: [],
1427
+ failed: []
1428
+ };
1429
+
1430
+ for (const toolInfo of failedTools) {
1431
+ const tool = typeof toolInfo === 'object' ? toolInfo.tool : toolInfo;
1432
+ const rollbackSuccess = await rollbackTool(tool, language);
1433
+
1434
+ if (rollbackSuccess) {
1435
+ results.success.push(tool);
1436
+ } else {
1437
+ results.failed.push(tool);
1438
+ }
1439
+ }
1440
+
1441
+ return results;
1442
+ }
1443
+
1444
+ // =============================================================================
1445
+ // MAIN INSTALLATION FUNCTION
1446
+ // =============================================================================
1447
+
313
1448
  async function installAIWF(options = {}) {
314
1449
  const debugLog = options.debugLog || false;
315
1450
 
316
- // 언어 선택
317
- let selectedLanguage = 'en'; // 기본값은 영어
1451
+ // 지능적 언어 선택 시스템
1452
+ let selectedLanguage = DEFAULT_LANGUAGE;
1453
+
318
1454
  if (!options.force) {
319
- const languageResponse = await prompts({
320
- type: 'select',
321
- name: 'language',
322
- message: 'Please select language / 언어를 선택해주세요:',
323
- choices: [
324
- { title: 'English', value: 'en' },
325
- { title: '한국어 (Korean)', value: 'ko' }
326
- ]
327
- });
1455
+ // 1. 기존 설정이 있는지 확인
1456
+ const existingConfig = await loadUserLanguageConfig();
1457
+
1458
+ if (existingConfig && existingConfig.language) {
1459
+ selectedLanguage = existingConfig.language;
1460
+ console.log(chalk.blue(`🌐 기존 언어 설정을 사용합니다 / Using existing language setting: ${selectedLanguage}`));
1461
+ } else {
1462
+ // 2. 자동 언어 감지 시도
1463
+ const detectedLang = await detectLanguage();
1464
+
1465
+ console.log(chalk.gray(`🔍 시스템 언어 감지 / System language detected: ${detectedLang}`));
1466
+
1467
+ // 3. 사용자에게 언어 선택 제공 (감지된 언어를 기본값으로)
1468
+ const languageResponse = await prompts({
1469
+ type: 'select',
1470
+ name: 'language',
1471
+ message: 'Please select language / 언어를 선택해주세요:',
1472
+ choices: [
1473
+ { title: 'English', value: 'en' },
1474
+ { title: '한국어 (Korean)', value: 'ko' }
1475
+ ],
1476
+ initial: SUPPORTED_LANGUAGES.indexOf(detectedLang) !== -1 ?
1477
+ SUPPORTED_LANGUAGES.indexOf(detectedLang) : 0
1478
+ });
328
1479
 
329
- if (languageResponse.language) {
330
- selectedLanguage = languageResponse.language;
1480
+ if (languageResponse.language) {
1481
+ selectedLanguage = languageResponse.language;
1482
+ } else {
1483
+ selectedLanguage = detectedLang;
1484
+ }
1485
+
1486
+ // 4. 선택된 언어를 설정으로 저장
1487
+ try {
1488
+ await saveUserLanguageConfig(selectedLanguage, {
1489
+ autoDetect: true,
1490
+ fallback: DEFAULT_LANGUAGE
1491
+ });
1492
+ console.log(chalk.green(`✅ 언어 설정이 저장되었습니다 / Language preference saved: ${selectedLanguage}`));
1493
+ } catch (error) {
1494
+ console.warn(chalk.yellow(`⚠️ 언어 설정 저장 실패 / Failed to save language preference: ${error.message}`));
1495
+ }
1496
+ }
1497
+ } else {
1498
+ // Force 모드일 때도 자동 감지 시도
1499
+ try {
1500
+ selectedLanguage = await detectLanguage();
1501
+ } catch (error) {
1502
+ selectedLanguage = DEFAULT_LANGUAGE;
331
1503
  }
332
1504
  }
333
1505
 
@@ -337,8 +1509,8 @@ async function installAIWF(options = {}) {
337
1509
  console.log(chalk.gray(msg.description));
338
1510
  console.log(chalk.gray(msg.optimized));
339
1511
 
340
- // 언어별 경로 설정
341
- const languagePath = selectedLanguage === 'en' ? 'en' : 'ko';
1512
+ // 언어별 경로 설정 (언어 유틸리티 사용)
1513
+ const languagePath = getInstallationLanguagePath(selectedLanguage);
342
1514
  const GITHUB_CONTENT_LANGUAGE_PREFIX = `${GITHUB_CONTENT_PREFIX}/${languagePath}`;
343
1515
 
344
1516
  const hasExisting = await checkExistingInstallation();
@@ -547,6 +1719,54 @@ async function installAIWF(options = {}) {
547
1719
  console.log(chalk.gray(`\n${msg.checkDocs}`));
548
1720
  }
549
1721
 
1722
+ // 설치 검증 실행
1723
+ console.log(chalk.blue('\n🔍 Installation Validation / 설치 검증을 시작합니다...'));
1724
+ // Use specification-compliant validation function
1725
+ const selectedTools = ['claude-code', 'cursor', 'windsurf']; // Default tools for this version
1726
+ const validationResults = await validateInstallation(selectedTools, selectedLanguage);
1727
+ displaySpecCompliantValidationResults(validationResults, selectedLanguage);
1728
+
1729
+ // 부분 실패 시 재설치/롤백 옵션 제공
1730
+ if (validationResults.failed.length > 0) {
1731
+ const action = await offerReinstallationOptions(validationResults, selectedLanguage);
1732
+
1733
+ if (action === 'reinstall') {
1734
+ console.log(chalk.blue('\n🔄 재설치를 시작합니다... / Starting reinstallation...'));
1735
+ // 재귀적으로 설치를 다시 시도 (한 번만)
1736
+ if (!options.isRetry) {
1737
+ return await installAIWF({ ...options, isRetry: true });
1738
+ }
1739
+ } else if (action === 'rollback') {
1740
+ console.log(chalk.blue('\n↩️ 실패한 도구를 롤백합니다... / Rolling back failed tools...'));
1741
+
1742
+ // 실패한 도구들만 롤백 - 새로운 형식에 맞게 수정
1743
+ const failedTools = validationResults.failed.map(({tool}) => tool);
1744
+
1745
+ let rollbackResults = {};
1746
+ for (const tool of failedTools) {
1747
+ if (BACKUP_DIR) {
1748
+ const rollbackResult = await rollbackTool(tool, BACKUP_DIR, selectedLanguage);
1749
+ rollbackResults[tool] = rollbackResult;
1750
+ }
1751
+ }
1752
+
1753
+ // 롤백 후 다시 검증
1754
+ console.log(chalk.blue('\n🔍 롤백 후 재검증... / Re-validating after rollback...'));
1755
+ const postRollbackValidation = await validateInstallation(selectedTools, selectedLanguage);
1756
+ displaySpecCompliantValidationResults(postRollbackValidation, selectedLanguage);
1757
+
1758
+ // 롤백 결과 요약
1759
+ console.log(chalk.blue('\n=== Rollback Summary / 롤백 요약 ==='));
1760
+ for (const [tool, result] of Object.entries(rollbackResults)) {
1761
+ if (result.success) {
1762
+ console.log(chalk.green(`✅ ${tool}: ${result.restoredCount} files restored`));
1763
+ } else {
1764
+ console.log(chalk.red(`❌ ${tool}: ${result.error}`));
1765
+ }
1766
+ }
1767
+ }
1768
+ }
1769
+
550
1770
  console.log(chalk.green(msg.enjoy));
551
1771
 
552
1772
  } catch (error) {