aiwf
Version:
AI Workflow Framework for Claude Code with multi-language support (Korean/English)
1,270 lines (1,124 loc) β’ 41.2 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import ora from 'ora';
import chalk from 'chalk';
import prompts from 'prompts';
import {
fetchContent,
downloadFile,
downloadDirectory,
GITHUB_RAW_URL,
GITHUB_CONTENT_PREFIX
} from './file-downloader.js';
import {
backupCommandsAndDocs,
restoreFromBackup,
getCurrentBackupDir,
setBackupDir
} from './backup-manager.js';
import { getMessages } from '../utils/messages.js';
import {
validateInstallation,
displaySpecCompliantValidationResults
} from './validator.js';
import { rollbackTool } from './rollback-manager.js';
import {
detectLanguage,
loadUserLanguageConfig,
saveUserLanguageConfig,
getInstallationLanguagePath,
SUPPORTED_LANGUAGES,
DEFAULT_LANGUAGE
} from '../utils/language-utils.js';
import {
FILES,
TOOL_DIRS,
AIWF_DIRS,
BASE_DIRS,
getAllAiwfDirectories,
getAllClaudeMdFiles
} from '../utils/paths.js';
/**
* Check if AIWF is already installed
* @returns {Promise<boolean>} Installation status
*/
async function checkExistingInstallation() {
const flagExists = await fs
.access(FILES.INSTALLED_FLAG)
.then(() => true)
.catch(() => false);
return flagExists;
}
/**
* Check if existing project files exist
* @returns {Promise<{hasProject: boolean, projectFiles: string[]}>} Project status
*/
async function checkExistingProject() {
const projectFiles = [];
let hasProject = false;
// Check for key project files
const keyFiles = [
FILES.PROJECT_MANIFEST,
path.join(AIWF_DIRS.PROJECT_DOCS, 'README.md'),
path.join(AIWF_DIRS.REQUIREMENTS, 'requirements.md'),
path.join(AIWF_DIRS.SPRINTS, 'sprint-01.md'),
path.join(AIWF_DIRS.GENERAL_TASKS, 'tasks.md'),
path.join(AIWF_DIRS.ARCHITECTURAL_DECISIONS, 'decisions.md'),
path.join(AIWF_DIRS.STATE_OF_PROJECT, 'current-state.md')
];
for (const file of keyFiles) {
try {
await fs.access(file);
projectFiles.push(file);
hasProject = true;
} catch (error) {
// File doesn't exist, continue
}
}
// Check for any .md files in project directories
const projectDirs = [
AIWF_DIRS.PROJECT_DOCS,
AIWF_DIRS.REQUIREMENTS,
AIWF_DIRS.SPRINTS,
AIWF_DIRS.GENERAL_TASKS,
AIWF_DIRS.ARCHITECTURAL_DECISIONS,
AIWF_DIRS.STATE_OF_PROJECT
];
for (const dir of projectDirs) {
try {
const files = await fs.readdir(dir);
const mdFiles = files.filter(file => file.endsWith('.md'));
if (mdFiles.length > 0) {
hasProject = true;
projectFiles.push(...mdFiles.map(file => path.join(dir, file)));
}
} catch (error) {
// Directory doesn't exist, continue
}
}
return { hasProject, projectFiles };
}
/**
* Log with spinner for debug mode
* @param {Object} spinner - Ora spinner instance
* @param {string} message - Message to log
* @param {boolean} debugLog - Debug mode flag
*/
function logWithSpinner(spinner, message, debugLog) {
if (debugLog) console.log(chalk.gray(message));
if (spinner) spinner.text = message;
}
/**
* Select language for installation
* @param {Object} options - Installation options
* @returns {Promise<string>} Selected language
*/
async function selectLanguage(options) {
let selectedLanguage = DEFAULT_LANGUAGE;
if (!options.force) {
// 1. Check existing configuration
const existingConfig = await loadUserLanguageConfig();
if (existingConfig && existingConfig.language) {
selectedLanguage = existingConfig.language;
console.log(
chalk.blue(
`π κΈ°μ‘΄ μΈμ΄ μ€μ μ μ¬μ©ν©λλ€ / Using existing language setting: ${selectedLanguage}`
)
);
} else {
// 2. Try auto-detection
const detectedLang = await detectLanguage();
console.log(
chalk.gray(`π μμ€ν
μΈμ΄ κ°μ§ / System language detected: ${detectedLang}`)
);
// 3. Prompt user for language selection
const languageResponse = await prompts({
type: 'select',
name: 'language',
message: 'Please select language / μΈμ΄λ₯Ό μ νν΄μ£ΌμΈμ:',
choices: [
{ title: 'English', value: 'en' },
{ title: 'νκ΅μ΄ (Korean)', value: 'ko' }
],
initial:
SUPPORTED_LANGUAGES.indexOf(detectedLang) !== -1
? SUPPORTED_LANGUAGES.indexOf(detectedLang)
: 0
});
if (languageResponse.language) {
selectedLanguage = languageResponse.language;
} else {
selectedLanguage = detectedLang;
}
// 4. Save selected language
try {
await saveUserLanguageConfig(selectedLanguage, {
autoDetect: true,
fallback: DEFAULT_LANGUAGE
});
console.log(
chalk.green(
`β
μΈμ΄ μ€μ μ΄ μ μ₯λμμ΅λλ€ / Language preference saved: ${selectedLanguage}`
)
);
} catch (error) {
console.warn(
chalk.yellow(
`β οΈ μΈμ΄ μ€μ μ μ₯ μ€ν¨ / Failed to save language preference: ${error.message}`
)
);
}
}
} else {
// Force mode: try auto-detection
try {
selectedLanguage = await detectLanguage();
} catch (error) {
selectedLanguage = DEFAULT_LANGUAGE;
}
}
return selectedLanguage;
}
/**
* Handle existing installation
* @param {Object} msg - Localized messages
* @param {Object} options - Installation options
* @returns {Promise<{continue: boolean, installType: string, preserveProject: boolean}>} Installation decision
*/
async function handleExistingInstallation(msg, options) {
if (options.force) {
return { continue: true, installType: 'reinstall', preserveProject: false };
}
// First, ask about installation type
const installResponse = await prompts({
type: 'select',
name: 'action',
message: msg.existingDetected,
choices: [
{ title: msg.updateOption, value: 'update' },
{ title: msg.reinstallOption, value: 'reinstall' },
{ title: msg.skipOption, value: 'skip' },
{ title: msg.cancelOption, value: 'cancel' }
]
});
if (installResponse.action === 'skip' || installResponse.action === 'cancel') {
console.log(chalk.yellow(msg.installCancelled));
process.exit(0);
}
let preserveProject = true; // Default to preserve
// If reinstalling, don't preserve project (complete fresh install)
if (installResponse.action === 'reinstall') {
preserveProject = false;
}
// Backup if updating
if (installResponse.action === 'update') {
await backupCommandsAndDocs(msg);
}
return {
continue: true,
installType: installResponse.action,
preserveProject
};
}
/**
* Backup project files
* @param {string[]} projectFiles - List of project files to backup
* @param {Object} msg - Localized messages
* @returns {Promise<string>} Backup directory path
*/
async function backupProjectFiles(projectFiles, msg) {
if (projectFiles.length === 0) {
return null;
}
const backupDir = path.join(os.tmpdir(), `aiwf-project-backup-${Date.now()}`);
await fs.mkdir(backupDir, { recursive: true });
console.log(chalk.cyan('πΎ Backing up project files...'));
for (const file of projectFiles) {
try {
const relativePath = path.relative(process.cwd(), file);
const backupPath = path.join(backupDir, relativePath);
const backupDirPath = path.dirname(backupPath);
await fs.mkdir(backupDirPath, { recursive: true });
await fs.copyFile(file, backupPath);
} catch (error) {
console.log(chalk.yellow(`Warning: Could not backup ${file}: ${error.message}`));
}
}
console.log(chalk.green(`β
Project files backed up to: ${backupDir}`));
return backupDir;
}
/**
* Restore project files from backup
* @param {string} backupDir - Backup directory path
* @param {Object} msg - Localized messages
*/
async function restoreProjectFiles(backupDir, msg) {
if (!backupDir) {
return;
}
console.log(chalk.cyan('π Restoring project files...'));
try {
const backupFiles = await fs.readdir(backupDir, { recursive: true });
for (const file of backupFiles) {
const backupFilePath = path.join(backupDir, file);
const stat = await fs.stat(backupFilePath);
if (stat.isFile()) {
const targetPath = path.join(process.cwd(), file);
const targetDir = path.dirname(targetPath);
await fs.mkdir(targetDir, { recursive: true });
await fs.copyFile(backupFilePath, targetPath);
}
}
console.log(chalk.green(msg.projectPreserved));
// Clean up backup directory
await fs.rm(backupDir, { recursive: true, force: true });
} catch (error) {
console.log(chalk.yellow(`Warning: Could not restore some project files: ${error.message}`));
console.log(chalk.yellow(`Backup preserved at: ${backupDir}`));
}
}
/**
* Remove project files
* @param {string[]} projectFiles - List of project files to remove
* @param {Object} msg - Localized messages
*/
async function removeProjectFiles(projectFiles, msg) {
if (projectFiles.length === 0) {
return;
}
console.log(chalk.cyan('ποΈ Removing existing project files...'));
for (const file of projectFiles) {
try {
await fs.unlink(file);
} catch (error) {
// File might already be deleted, continue
}
}
console.log(chalk.green(msg.projectOverwritten));
}
/**
* Create AIWF directory structure
*/
async function createDirectoryStructure() {
const aiwfDirs = getAllAiwfDirectories();
// Create AIWF directories
for (const dir of aiwfDirs) {
await fs.mkdir(dir, { recursive: true });
}
// Create tool-specific directories
await fs.mkdir(TOOL_DIRS.CURSOR_RULES, { recursive: true });
await fs.mkdir(TOOL_DIRS.WINDSURF_RULES, { recursive: true });
await fs.mkdir(TOOL_DIRS.CLAUDE_AGENTS, { recursive: true });
}
/**
* Download manifest file
* @param {string} languagePath - Language path
* @param {Object} spinner - Ora spinner instance
* @param {Object} msg - Localized messages
* @param {boolean} debugLog - Debug mode flag
*/
async function downloadManifest(languagePath, spinner, msg, debugLog) {
logWithSpinner(spinner, msg.downloading, debugLog);
try {
const manifestUrl = `${GITHUB_RAW_URL}/${GITHUB_CONTENT_PREFIX}/${languagePath}/.aiwf/00_PROJECT_MANIFEST.md`;
await downloadFile(manifestUrl, FILES.PROJECT_MANIFEST);
} catch (error) {
// If manifest doesn't exist, that's okay
}
}
/**
* Download templates
* @param {string} languagePath - Language path
* @param {Object} spinner - Ora spinner instance
* @param {Object} msg - Localized messages
* @param {boolean} debugLog - Debug mode flag
*/
async function downloadTemplates(languagePath, spinner, msg, debugLog) {
try {
logWithSpinner(spinner, msg.downloadingTemplates, debugLog);
// .aiwf μ 체 λλ ν 리λ₯Ό λμ μΌλ‘ λ€μ΄λ‘λ
logWithSpinner(spinner, `Downloading .aiwf directory structure...`, debugLog);
await downloadDirectory(
`${GITHUB_CONTENT_PREFIX}/${languagePath}/.aiwf`,
AIWF_DIRS.ROOT,
spinner,
msg,
languagePath,
true // useDynamicFetch = true
);
logWithSpinner(spinner, `Successfully downloaded .aiwf directory`, debugLog);
} catch (error) {
logWithSpinner(spinner, `Error downloading .aiwf: ${error.message}`, debugLog);
// Fallback: κ°λ³ λλ ν 리 λ€μ΄λ‘λ μλ
try {
logWithSpinner(spinner, `Trying individual directory downloads...`, debugLog);
await downloadDirectory(
`${GITHUB_CONTENT_PREFIX}/${languagePath}/.aiwf/98_PROMPTS`,
AIWF_DIRS.PROMPTS,
spinner,
msg,
languagePath,
true
);
await downloadDirectory(
`${GITHUB_CONTENT_PREFIX}/${languagePath}/.aiwf/99_TEMPLATES`,
AIWF_DIRS.TEMPLATES,
spinner,
msg,
languagePath,
true
);
} catch (fallbackError) {
logWithSpinner(spinner, msg.templatesNotFound, debugLog);
}
}
}
/**
* Update CLAUDE.md documentation files
* @param {string} languagePath - Language path
* @param {Object} spinner - Ora spinner instance
* @param {Object} msg - Localized messages
* @param {boolean} debugLog - Debug mode flag
*/
async function updateDocumentation(languagePath, spinner, msg, debugLog) {
logWithSpinner(spinner, msg.updatingDocs, debugLog);
const claudeFiles = getAllClaudeMdFiles();
for (const claudeFile of claudeFiles) {
try {
const relativePath = claudeFile.replace(process.cwd() + '/', '');
const claudeUrl = `${GITHUB_RAW_URL}/${GITHUB_CONTENT_PREFIX}/${languagePath}/${relativePath}`;
await downloadFile(claudeUrl, claudeFile);
} catch (error) {
// If CLAUDE.md doesn't exist, that's okay
}
}
}
/**
* Update Claude commands and agents
* @param {string} languagePath - Language path
* @param {Object} spinner - Ora spinner instance
* @param {Object} msg - Localized messages
* @param {boolean} debugLog - Debug mode flag
*/
async function updateCommands(languagePath, spinner, msg, debugLog) {
// Delete existing commands folder if it exists
if (await fs.access(TOOL_DIRS.CLAUDE_COMMANDS).then(() => true).catch(() => false)) {
logWithSpinner(spinner, msg.deletingOldCommands, debugLog);
await fs.rm(TOOL_DIRS.CLAUDE_COMMANDS, { recursive: true, force: true });
}
// Delete existing agents folder if it exists
if (await fs.access(TOOL_DIRS.CLAUDE_AGENTS).then(() => true).catch(() => false)) {
logWithSpinner(spinner, 'Deleting old agents...', debugLog);
await fs.rm(TOOL_DIRS.CLAUDE_AGENTS, { recursive: true, force: true });
}
await fs.mkdir(TOOL_DIRS.CLAUDE_COMMANDS, { recursive: true });
await fs.mkdir(TOOL_DIRS.CLAUDE_AGENTS, { recursive: true });
// Download commands
logWithSpinner(spinner, msg.downloadingCommands, debugLog);
try {
await downloadDirectory(
`${GITHUB_CONTENT_PREFIX}/${languagePath}/${TOOL_DIRS.CLAUDE_COMMANDS}`,
TOOL_DIRS.CLAUDE_COMMANDS,
spinner,
msg,
languagePath,
true // commandsλ λμ λλ ν°λ¦¬ νμ μ¬μ© (νμΌ λ¦¬μ€νΈ μμ‘΄ μ κ±°)
);
} catch (error) {
logWithSpinner(spinner, msg.commandsNotFound, debugLog);
}
// Download agents
logWithSpinner(spinner, 'Downloading agents...', debugLog);
try {
await downloadDirectory(
`${GITHUB_CONTENT_PREFIX}/${languagePath}/${TOOL_DIRS.CLAUDE_AGENTS}`,
TOOL_DIRS.CLAUDE_AGENTS,
spinner,
msg,
languagePath,
true // agentsλ λμ λλ ν°λ¦¬ νμ μ¬μ© (νμΌ λ¦¬μ€νΈ μμ‘΄ μ κ±°)
);
logWithSpinner(spinner, 'Successfully downloaded agents', debugLog);
} catch (error) {
logWithSpinner(spinner, 'Agents not found or failed to download', debugLog);
}
// Download full .claude directory to include tools, workflow, and any other subfolders
logWithSpinner(spinner, 'Downloading full .claude directory (tools, workflow, etc.)...', debugLog);
try {
await downloadDirectory(
`${GITHUB_CONTENT_PREFIX}/${languagePath}/.claude`,
'.claude',
spinner,
msg,
languagePath,
true // μ 체 .claude ν΄λ λμ λ€μ΄λ‘λ
);
logWithSpinner(spinner, 'Successfully downloaded .claude directory', debugLog);
} catch (error) {
logWithSpinner(spinner, `Failed to download full .claude directory: ${error.message}`, debugLog);
}
}
/**
* Check if tool directories exist and handle them
* @param {Object} spinner - Ora spinner instance
* @param {Object} msg - Localized messages
* @param {boolean} debugLog - Debug mode flag
* @returns {Promise<Object>} Directory status info
*/
async function checkAndHandleExistingToolDirs(spinner, msg, debugLog) {
const toolDirStatus = {
cursor: {
exists: false,
hadFiles: false,
backedUp: false
},
windsurf: {
exists: false,
hadFiles: false,
backedUp: false
}
};
// Check Cursor directory
try {
await fs.access(TOOL_DIRS.CURSOR_RULES);
toolDirStatus.cursor.exists = true;
const cursorFiles = await fs.readdir(TOOL_DIRS.CURSOR_RULES);
const mdcFiles = cursorFiles.filter(file => file.endsWith('.mdc'));
if (mdcFiles.length > 0) {
toolDirStatus.cursor.hadFiles = true;
logWithSpinner(spinner, `${msg.foundExistingCursor} (${mdcFiles.length} files)`, debugLog);
// Backup existing files
logWithSpinner(spinner, msg.backingUpToolFiles, debugLog);
const backupDir = path.join(TOOL_DIRS.CURSOR_RULES, `backup_${Date.now()}`);
await fs.mkdir(backupDir, { recursive: true });
for (const file of mdcFiles) {
const srcPath = path.join(TOOL_DIRS.CURSOR_RULES, file);
const backupPath = path.join(backupDir, file);
await fs.copyFile(srcPath, backupPath);
}
toolDirStatus.cursor.backedUp = true;
logWithSpinner(spinner, `${msg.toolBackupCreated}: ${backupDir}`, debugLog);
// Remove existing files
for (const file of mdcFiles) {
await fs.unlink(path.join(TOOL_DIRS.CURSOR_RULES, file));
}
}
} catch (error) {
// Directory doesn't exist, that's fine
logWithSpinner(spinner, `Cursor rules directory not found, will create new`, debugLog);
}
// Check Windsurf directory
try {
await fs.access(TOOL_DIRS.WINDSURF_RULES);
toolDirStatus.windsurf.exists = true;
const windsurfFiles = await fs.readdir(TOOL_DIRS.WINDSURF_RULES);
const mdFiles = windsurfFiles.filter(file => file.endsWith('.md'));
if (mdFiles.length > 0) {
toolDirStatus.windsurf.hadFiles = true;
logWithSpinner(spinner, `${msg.foundExistingWindsurf} (${mdFiles.length} files)`, debugLog);
// Backup existing files
logWithSpinner(spinner, msg.backingUpToolFiles, debugLog);
const backupDir = path.join(TOOL_DIRS.WINDSURF_RULES, `backup_${Date.now()}`);
await fs.mkdir(backupDir, { recursive: true });
for (const file of mdFiles) {
const srcPath = path.join(TOOL_DIRS.WINDSURF_RULES, file);
const backupPath = path.join(backupDir, file);
await fs.copyFile(srcPath, backupPath);
}
toolDirStatus.windsurf.backedUp = true;
logWithSpinner(spinner, `${msg.toolBackupCreated}: ${backupDir}`, debugLog);
// Remove existing files
for (const file of mdFiles) {
await fs.unlink(path.join(TOOL_DIRS.WINDSURF_RULES, file));
}
}
} catch (error) {
// Directory doesn't exist, that's fine
logWithSpinner(spinner, `Windsurf rules directory not found, will create new`, debugLog);
}
return toolDirStatus;
}
/**
* Download and process rules
* @param {Object} spinner - Ora spinner instance
* @param {Object} msg - Localized messages
* @param {boolean} debugLog - Debug mode flag
*/
async function downloadAndProcessRules(spinner, msg, debugLog) {
// Check and handle existing tool directories first
logWithSpinner(spinner, msg.checkingExistingTools, debugLog);
const toolDirStatus = await checkAndHandleExistingToolDirs(spinner, msg, debugLog);
// Try to download from GitHub first, fallback to local files
const tmpRulesGlobal = path.join(os.tmpdir(), 'aiwf-rules-global');
const tmpRulesManual = path.join(os.tmpdir(), 'aiwf-rules-manual');
let useLocalGlobal = false;
let useLocalManual = false;
// Try to download rules/global from GitHub
try {
await downloadDirectory(
`rules/global`,
tmpRulesGlobal,
spinner,
debugLog
);
logWithSpinner(spinner, `Downloaded rules/global from GitHub`, debugLog);
} catch (error) {
logWithSpinner(spinner, `Failed to download rules/global from GitHub, trying local files`, debugLog);
// Fallback to local files
const localRulesGlobal = path.join(process.cwd(), 'rules/global');
try {
logWithSpinner(spinner, `Trying to access: ${localRulesGlobal}`, debugLog);
await fs.access(localRulesGlobal);
logWithSpinner(spinner, `Successfully accessed local rules/global`, debugLog);
// Copy local files to temp directory
await fs.mkdir(tmpRulesGlobal, { recursive: true });
const files = await fs.readdir(localRulesGlobal);
logWithSpinner(spinner, `Found ${files.length} files in rules/global: ${files.join(', ')}`, debugLog);
for (const file of files) {
const srcPath = path.join(localRulesGlobal, file);
const destPath = path.join(tmpRulesGlobal, file);
await fs.copyFile(srcPath, destPath);
logWithSpinner(spinner, `Copied: ${file}`, debugLog);
}
useLocalGlobal = true;
logWithSpinner(spinner, `Using local rules/global directory`, debugLog);
} catch (localError) {
logWithSpinner(spinner, `Local rules/global error: ${localError.message}`, debugLog);
logWithSpinner(spinner, msg.rulesGlobalNotFound, debugLog);
}
}
// Try to download rules/manual from GitHub
try {
await downloadDirectory(
`rules/manual`,
tmpRulesManual,
spinner,
debugLog
);
logWithSpinner(spinner, `Downloaded rules/manual from GitHub`, debugLog);
} catch (error) {
logWithSpinner(spinner, `Failed to download rules/manual from GitHub, trying local files`, debugLog);
// Fallback to local files
const localRulesManual = path.join(process.cwd(), 'rules/manual');
try {
await fs.access(localRulesManual);
// Copy local files to temp directory
await fs.mkdir(tmpRulesManual, { recursive: true });
const files = await fs.readdir(localRulesManual);
for (const file of files) {
const srcPath = path.join(localRulesManual, file);
const destPath = path.join(tmpRulesManual, file);
await fs.copyFile(srcPath, destPath);
}
useLocalManual = true;
logWithSpinner(spinner, `Using local rules/manual directory`, debugLog);
} catch (localError) {
logWithSpinner(spinner, msg.rulesManualNotFound, debugLog);
}
}
// Process rules/global
try {
await fs.access(tmpRulesGlobal);
await fs.mkdir(TOOL_DIRS.CURSOR_RULES, { recursive: true });
await fs.mkdir(TOOL_DIRS.WINDSURF_RULES, { recursive: true });
const files = await fs.readdir(tmpRulesGlobal);
for (const file of files) {
const srcPath = path.join(tmpRulesGlobal, file);
const stat = await fs.stat(srcPath);
if (!stat.isFile()) continue;
// .cursor/rules: add .mdc extension and header
const base = path.parse(file).name;
const cursorTarget = path.join(TOOL_DIRS.CURSOR_RULES, base + '.mdc');
const content = await fs.readFile(srcPath, 'utf8');
const header = '---\ndescription: \nglobs: \nalwaysApply: true\n---\n';
await fs.writeFile(cursorTarget, header + content, 'utf8');
logWithSpinner(
spinner,
`[rules/global] ${file} -> ${TOOL_DIRS.CURSOR_RULES}/${base}.mdc`,
debugLog
);
// .windsurf/rules: keep extension, no header
const windsurfTarget = path.join(TOOL_DIRS.WINDSURF_RULES, file);
const windsurfContent = await fs.readFile(srcPath, 'utf8');
const windsurfHeader = '---\ntrigger: always_on\n---\n';
await fs.writeFile(windsurfTarget, windsurfHeader + windsurfContent, 'utf8');
logWithSpinner(
spinner,
`[rules/global] ${file} -> ${TOOL_DIRS.WINDSURF_RULES}/${file}`,
debugLog
);
}
// Clean up temp folder (only if not using local files)
if (!useLocalGlobal) {
await fs.rm(tmpRulesGlobal, { recursive: true, force: true });
}
} catch (error) {
logWithSpinner(spinner, `No rules/global directory found`, debugLog);
}
// Process rules/manual
try {
await fs.access(tmpRulesManual);
await fs.mkdir(TOOL_DIRS.CURSOR_RULES, { recursive: true });
await fs.mkdir(TOOL_DIRS.WINDSURF_RULES, { recursive: true });
const files = await fs.readdir(tmpRulesManual);
for (const file of files) {
const srcPath = path.join(tmpRulesManual, file);
const stat = await fs.stat(srcPath);
if (!stat.isFile()) continue;
// .cursor/rules: add .mdc extension and header (alwaysApply: false)
const base = path.parse(file).name;
const cursorTarget = path.join(TOOL_DIRS.CURSOR_RULES, base + '.mdc');
const content = await fs.readFile(srcPath, 'utf8');
const header = '---\ndescription: \nglobs: \nalwaysApply: false\n---\n';
await fs.writeFile(cursorTarget, header + content, 'utf8');
logWithSpinner(
spinner,
`[rules/manual] ${file} -> ${TOOL_DIRS.CURSOR_RULES}/${base}.mdc`,
debugLog
);
// .windsurf/rules: keep extension, no header
const windsurfTarget = path.join(TOOL_DIRS.WINDSURF_RULES, file);
const windsurfContent = await fs.readFile(srcPath, 'utf8');
const windsurfHeader = '---\ntrigger: manual\n---\n';
await fs.writeFile(windsurfTarget, windsurfHeader + windsurfContent, 'utf8');
logWithSpinner(
spinner,
`[rules/manual] ${file} -> ${TOOL_DIRS.WINDSURF_RULES}/${file}`,
debugLog
);
}
// Clean up temp folder (only if not using local files)
if (!useLocalManual) {
await fs.rm(tmpRulesManual, { recursive: true, force: true });
}
} catch (error) {
logWithSpinner(spinner, `No rules/manual directory found`, debugLog);
}
// Success message for tool rules installation
logWithSpinner(spinner, msg.toolRulesInstalled, debugLog);
// Display installation results for tool directories
if (toolDirStatus.cursor.backedUp || toolDirStatus.windsurf.backedUp) {
console.log(chalk.cyan(`\nπ Tool Directory Installation Results:`));
if (toolDirStatus.cursor.backedUp) {
console.log(chalk.yellow(` β’ Cursor: Existing rules backed up and replaced`));
} else if (toolDirStatus.cursor.exists) {
console.log(chalk.green(` β’ Cursor: Rules installed (directory was empty)`));
} else {
console.log(chalk.green(` β’ Cursor: Rules installed (new directory)`));
}
if (toolDirStatus.windsurf.backedUp) {
console.log(chalk.yellow(` β’ Windsurf: Existing rules backed up and replaced`));
} else if (toolDirStatus.windsurf.exists) {
console.log(chalk.green(` β’ Windsurf: Rules installed (directory was empty)`));
} else {
console.log(chalk.green(` β’ Windsurf: Rules installed (new directory)`));
}
}
return toolDirStatus;
}
/**
* Display installation summary
* @param {boolean} hasExisting - Has existing installation
* @param {Object} msg - Localized messages
*/
function displaySummary(hasExisting, msg) {
if (hasExisting) {
console.log(chalk.blue(`\n${msg.updateHistory}`));
console.log(chalk.gray(` β’ ${TOOL_DIRS.CLAUDE_COMMANDS}${msg.updatedCommands}`));
console.log(chalk.gray(` β’ ${msg.updatedDocs}`));
console.log(chalk.green(`\n${msg.workPreserved}`));
console.log(chalk.gray(` β’ ${msg.allFilesPreserved}`));
console.log(chalk.gray(` β’ ${msg.backupCreated}`));
} else {
console.log(chalk.blue(`\n${msg.structureCreated}`));
console.log(chalk.gray(` ${AIWF_DIRS.ROOT}/ - ${msg.aiwfRoot}`));
console.log(chalk.gray(` ${TOOL_DIRS.CLAUDE_COMMANDS}/ - ${msg.claudeCommands}`));
console.log(chalk.green(`\n${msg.nextSteps}`));
console.log(chalk.white(` 1. ${msg.nextStep1}`));
console.log(chalk.white(` 2. ${msg.nextStep2}`));
console.log(chalk.white(` 3. ${msg.nextStep3}\n`));
console.log(chalk.blue(`\n${msg.gettingStarted}`));
console.log(chalk.gray(` 1. ${msg.startStep1}`));
console.log(chalk.gray(` 2. ${msg.startStep2}`));
console.log(chalk.gray(`\n${msg.checkDocs}`));
}
}
/**
* Offer reinstallation options
* @param {Object} validationResults - Validation results
* @param {string} language - Language code
* @returns {Promise<string>} Selected action
*/
async function offerReinstallationOptions(validationResults, language) {
console.log(
chalk.yellow(
language === 'ko'
? '\nβ οΈ μΌλΆ μ€μΉκ° μ€ν¨νμ΅λλ€. 무μμ νμκ² μ΅λκΉ?'
: '\nβ οΈ Some installations failed. What would you like to do?'
)
);
// List failed tools
const failedTools = validationResults.failed.map(({ tool }) => tool);
if (failedTools.length > 0) {
console.log(
chalk.red(
language === 'ko'
? `μ€ν¨ν λꡬ: ${failedTools.join(', ')}`
: `Failed tools: ${failedTools.join(', ')}`
)
);
}
const response = await prompts({
type: 'select',
name: 'action',
message:
language === 'ko' ? 'μ΄λ€ μμ
μ μννμκ² μ΅λκΉ?' : 'What would you like to do?',
choices: [
{
title: language === 'ko' ? 'μ 체 μ¬μ€μΉ' : 'Full reinstallation',
value: 'reinstall'
},
{
title: language === 'ko' ? 'μ€ν¨ν λκ΅¬λ§ λ‘€λ°±' : 'Rollback failed tools only',
value: 'rollback'
},
{
title: language === 'ko' ? 'νμ¬ μν μ μ§' : 'Keep current state',
value: 'keep'
}
]
});
return response.action;
}
/**
* μ¬μ©μ μ€μΉ μ΅μ
μ ν
* @param {Object} msg - μ§μνλ λ©μμ§
* @returns {Promise<Array>} μ νλ μ€μΉ μ΅μ
λͺ©λ‘
*/
async function selectInstallationOptions(msg) {
console.log(chalk.cyan(`\n${msg.installationOptions}`));
const installOptions = [
{
title: msg.aiwfDocs,
value: 'aiwf-docs',
description: 'AIWF λ¬Έμ ν΄λ'
},
{
title: msg.claudeCodeCommands,
value: 'claude-code-commands',
description: 'Claude Code λͺ
λ Ήμ΄ λ° μμ΄μ νΈ'
},
{
title: msg.windsurfRules,
value: 'windsurf-rules',
description: 'Windsurf κ·μΉ νμΌ'
},
{
title: msg.cursorRules,
value: 'cursor-rules',
description: 'Cursor κ·μΉ νμΌ'
}
];
// 1λ¨κ³: μ 체 μ€μΉ vs κ°λ³ μ ν
const installTypeResponse = await prompts({
type: 'select',
name: 'installType',
message: msg.installationOptions || 'μ€μΉ μ΅μ
μ μ ννμΈμ:',
choices: [
{
title: msg.selectAll || 'β
λͺ¨λ νλͺ© μ€μΉ (κΆμ₯)',
value: 'all',
description: 'λͺ¨λ AIWF μ»΄ν¬λνΈλ₯Ό μ€μΉν©λλ€'
},
{
title: 'π― κ°λ³ νλͺ© μ ν',
value: 'custom',
description: 'μνλ νλͺ©λ§ μ ννμ¬ μ€μΉν©λλ€'
}
]
});
// μ·¨μλ κ²½μ°
if (!installTypeResponse.installType) {
console.log(chalk.yellow(msg.noOptionsSelected || 'μ€μΉκ° μ·¨μλμμ΅λλ€.'));
process.exit(0);
}
// μ 체 μ€μΉ μ ν
if (installTypeResponse.installType === 'all') {
return installOptions.map(opt => opt.value);
}
// 2λ¨κ³: κ°λ³ νλͺ© μ ν
const customResponse = await prompts({
type: 'multiselect',
name: 'selectedOptions',
message: 'μ€μΉν νλͺ©μ μ ννμΈμ (μ€νμ΄μ€λ‘ μ ν/ν΄μ , μν°λ‘ νμΈ):',
choices: installOptions,
hint: 'ββ μ΄λ, μ€νμ΄μ€ μ ν/ν΄μ , μν° νμΈ',
instructions: false
});
// μ·¨μλκ±°λ μ무κ²λ μ ννμ§ μμ κ²½μ°
if (!customResponse.selectedOptions || customResponse.selectedOptions.length === 0) {
console.log(chalk.yellow(msg.noOptionsSelected || 'μ νλ νλͺ©μ΄ μμ΅λλ€.'));
process.exit(0);
}
return customResponse.selectedOptions;
}
/**
* μ νλ μ΅μ
λ³ μ€μΉ μ€ν
* @param {Array} selectedOptions - μ νλ μ€μΉ μ΅μ
λ€
* @param {string} languagePath - μΈμ΄ κ²½λ‘
* @param {Object} msg - μ§μνλ λ©μμ§
* @param {boolean} debugLog - λλ²κ·Έ λ‘κ·Έ μ¬λΆ
*/
async function installSelectedOptions(selectedOptions, languagePath, msg, debugLog) {
for (const option of selectedOptions) {
const spinner = ora(`${msg.installingOption}${option}...`).start();
try {
switch (option) {
case 'aiwf-docs':
await installAIWFDocs(languagePath, debugLog);
break;
case 'claude-code-commands':
await installClaudeCodeCommands(languagePath, debugLog);
break;
case 'windsurf-rules':
await installWindsurfRules(debugLog);
break;
case 'cursor-rules':
await installCursorRules(debugLog);
break;
default:
spinner.warn(`Unknown option: ${option}`);
continue;
}
spinner.succeed(`${msg.optionInstalled}${option}`);
} catch (error) {
spinner.fail(`Failed to install ${option}: ${error.message}`);
if (debugLog) {
console.error(error);
}
}
}
}
/**
* AIWF λ¬Έμ μ€μΉ
*/
async function installAIWFDocs(languagePath, debugLog) {
await createDirectoryStructure();
const spinner = ora('Downloading AIWF files...').start();
const msg = {
downloading: 'Downloading AIWF documentation...',
downloadingTemplates: 'Downloading templates...',
templatesNotFound: 'Templates not found'
};
try {
// λ§€λνμ€νΈ λ€μ΄λ‘λ
await downloadManifest(languagePath, spinner, msg, debugLog);
// μ 체 .aiwf λλ ν 리 λ€μ΄λ‘λ
await downloadTemplates(languagePath, spinner, msg, debugLog);
spinner.succeed('AIWF documentation installed successfully');
} catch (error) {
spinner.fail('Failed to install AIWF documentation');
if (debugLog) {
console.error(error);
}
throw error;
}
}
/**
* Claude Code λͺ
λ Ήμ΄ μ€μΉ
*/
async function installClaudeCodeCommands(languagePath, debugLog) {
const GITHUB_CONTENT_LANGUAGE_PREFIX = `${GITHUB_CONTENT_PREFIX}/${languagePath}`;
const dummySpinner = null;
const dummyMsg = { downloadingCommands: 'Downloading Claude Code commands...' };
await updateCommands(languagePath, dummySpinner, dummyMsg, debugLog);
}
/**
* Windsurf κ·μΉ μ€μΉ
*/
async function installWindsurfRules(debugLog) {
const spinner = ora('Installing Windsurf rules...').start();
const msg = {
checkingExistingTools: 'Checking existing tools...',
foundExistingCursor: 'Found existing Cursor rules',
foundExistingWindsurf: 'Found existing Windsurf rules',
backingUpToolFiles: 'Backing up tool files...',
toolBackupCreated: 'Tool backup created',
rulesGlobalNotFound: 'Rules global not found',
rulesManualNotFound: 'Rules manual not found',
toolRulesInstalled: 'Tool rules installed successfully'
};
try {
await downloadAndProcessRules(spinner, msg, debugLog);
spinner.succeed('Windsurf rules installed successfully');
} catch (error) {
spinner.fail('Failed to install Windsurf rules');
if (debugLog) {
console.error(error);
}
throw error;
}
}
/**
* Cursor κ·μΉ μ€μΉ
*/
async function installCursorRules(debugLog) {
const spinner = ora('Installing Cursor rules...').start();
const msg = {
checkingExistingTools: 'Checking existing tools...',
foundExistingCursor: 'Found existing Cursor rules',
foundExistingWindsurf: 'Found existing Windsurf rules',
backingUpToolFiles: 'Backing up tool files...',
toolBackupCreated: 'Tool backup created',
rulesGlobalNotFound: 'Rules global not found',
rulesManualNotFound: 'Rules manual not found',
toolRulesInstalled: 'Tool rules installed successfully'
};
try {
await downloadAndProcessRules(spinner, msg, debugLog);
spinner.succeed('Cursor rules installed successfully');
} catch (error) {
spinner.fail('Failed to install Cursor rules');
if (debugLog) {
console.error(error);
}
throw error;
}
}
/**
* Main installation function
* @param {Object} options - Installation options
*/
export async function installAIWF(options = {}) {
const debugLog = options.debugLog || false;
// Select language
const selectedLanguage = await selectLanguage(options);
const msg = getMessages(selectedLanguage);
console.log(chalk.blue.bold(msg.welcome));
console.log(chalk.gray(msg.description));
console.log(chalk.gray(msg.optimized));
// Set language path
const languagePath = getInstallationLanguagePath(selectedLanguage);
// Check existing installation
const hasExisting = await checkExistingInstallation();
let installDecision = { continue: true, installType: 'fresh', preserveProject: false };
let projectBackupDir = null;
if (hasExisting) {
installDecision = await handleExistingInstallation(msg, options);
if (!installDecision.continue) return;
// If reinstalling and preserving project, backup project files
if (installDecision.installType === 'reinstall' && installDecision.preserveProject) {
const { hasProject, projectFiles } = await checkExistingProject();
if (hasProject) {
projectBackupDir = await backupProjectFiles(projectFiles, msg);
}
}
}
// μ€μΉ μ΅μ
μ ν
let selectedOptions = ['aiwf-docs', 'claude-code-commands', 'windsurf-rules', 'cursor-rules'];
if (options.preselectedOptions) {
// CLIμμ 미리 μ νλ μ΅μ
λ€μ μ¬μ©
selectedOptions = options.preselectedOptions;
} else if (!options.force && !options.interactive) {
// κΈ°λ³Έμ μΌλ‘λ λνν μ νμ μ¬μ©
selectedOptions = await selectInstallationOptions(msg);
} else if (options.interactive) {
// --interactive νλκ·Έκ° λͺ
μμ μΌλ‘ μ€μ λ κ²½μ°
selectedOptions = await selectInstallationOptions(msg);
}
console.log(chalk.blue(`\nμ€μΉν μ΅μ
: ${selectedOptions.join(', ')}`));
try {
// μ νλ μ΅μ
λ€μ μ€μΉ
await installSelectedOptions(selectedOptions, languagePath, msg, debugLog);
// Restore project files if needed
if (projectBackupDir) {
await restoreProjectFiles(projectBackupDir, msg);
}
// Success message
if (hasExisting && installDecision.installType === 'update') {
console.log(chalk.green(`\nβ
${msg.updateSuccess}`));
} else {
console.log(chalk.green(`\nβ
${msg.installSuccess}`));
}
// Display summary
displaySummary(hasExisting, msg);
// Run validation based on selected options
console.log(
chalk.blue('\nπ Installation Validation / μ€μΉ κ²μ¦μ μμν©λλ€...')
);
// Map selected options to validation tools
const validationTools = [];
if (selectedOptions.includes('claude-code-commands')) {
validationTools.push('claude-code');
}
if (selectedOptions.includes('cursor-rules')) {
validationTools.push('cursor');
}
if (selectedOptions.includes('windsurf-rules')) {
validationTools.push('windsurf');
}
if (validationTools.length > 0) {
const validationResults = await validateInstallation(
validationTools,
selectedLanguage
);
displaySpecCompliantValidationResults(validationResults, selectedLanguage);
// Handle partial failures
if (validationResults.failed.length > 0) {
const action = await offerReinstallationOptions(
validationResults,
selectedLanguage
);
if (action === 'reinstall') {
console.log(
chalk.blue('\nπ μ¬μ€μΉλ₯Ό μμν©λλ€... / Starting reinstallation...')
);
// Retry installation once
if (!options.isRetry) {
return await installAIWF({ ...options, isRetry: true });
}
} else if (action === 'rollback') {
console.log(
chalk.blue('\nβ©οΈ μ€ν¨ν λꡬλ₯Ό λ‘€λ°±ν©λλ€... / Rolling back failed tools...')
);
// Rollback failed tools
const failedTools = validationResults.failed.map(({ tool }) => tool);
const backupDir = getCurrentBackupDir();
for (const tool of failedTools) {
if (backupDir) {
await rollbackTool(
tool,
backupDir,
selectedLanguage
);
}
}
// Re-validate after rollback
console.log(
chalk.blue('\nπ λ‘€λ°± ν μ¬κ²μ¦... / Re-validating after rollback...')
);
const postRollbackValidation = await validateInstallation(
validationTools,
selectedLanguage
);
displaySpecCompliantValidationResults(postRollbackValidation, selectedLanguage);
}
}
} else {
console.log(chalk.yellow('\nβ οΈ κ²μ¦ν λκ΅¬κ° μ νλμ§ μμμ΅λλ€.'));
}
console.log(chalk.green(msg.enjoy));
} catch (error) {
if (hasExisting) {
spinner.fail(chalk.red(msg.updateFailed));
await restoreFromBackup(spinner, msg);
} else {
spinner.fail(chalk.red(msg.installFailed));
}
console.error(chalk.red(error.message));
process.exit(1);
}
// Create installation flag file
await fs.writeFile(FILES.INSTALLED_FLAG, 'installed', 'utf8');
}