diff --git a/index.js b/index.js index c599ac3..0fefaa7 100755 --- a/index.js +++ b/index.js @@ -15,9 +15,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const program = new Command(); -const GITHUB_API_URL = 'https://api.github.com/repos/helmi/claude-simone'; -const GITHUB_RAW_URL = 'https://raw.githubusercontent.com/helmi/claude-simone/master'; - async function fetchGitHubContent(url) { return new Promise((resolve, reject) => { https.get(url, { headers: { 'User-Agent': 'hello-simone' } }, (res) => { @@ -25,7 +22,11 @@ async function fetchGitHubContent(url) { res.on('data', chunk => data += chunk); res.on('end', () => { if (res.statusCode === 200) { - resolve(data); + try { + resolve(JSON.parse(data)); + } catch (e) { + reject(new Error('Failed to parse JSON response from GitHub.')); + } } else { reject(new Error(`Failed to fetch ${url}: ${res.statusCode}`)); } @@ -35,235 +36,233 @@ async function fetchGitHubContent(url) { } async function downloadFile(url, destPath) { - return new Promise((resolve, reject) => { - const file = createWriteStream(destPath); - https.get(url, { headers: { 'User-Agent': 'hello-simone' } }, (response) => { - if (response.statusCode !== 200) { - reject(new Error(`Failed to download ${url}: ${response.statusCode}`)); - return; - } - pipeline(response, file) - .then(() => resolve()) - .catch(reject); - }).on('error', reject); - }); + await fs.mkdir(path.dirname(destPath), { recursive: true }); + return new Promise((resolve, reject) => { + const file = createWriteStream(destPath); + https.get(url, { headers: { 'User-Agent': 'hello-simone' } }, (response) => { + if (response.statusCode !== 200) { + reject(new Error(`Failed to download ${url}: ${response.statusCode}`)); + return; + } + pipeline(response, file) + .then(() => resolve()) + .catch(reject); + }).on('error', reject); + }); } -async function getDirectoryStructure(path = '') { - const url = `${GITHUB_API_URL}/contents/${path}`; - const content = await fetchGitHubContent(url); - return JSON.parse(content); -} +async function downloadDirectory(githubPath, localPath, spinner, branch) { + await fs.mkdir(localPath, { recursive: true }); + const apiUrl = `https://api.github.com/repos/helmi/claude-simone/contents/${githubPath}?ref=${branch}`; + const items = await fetchGitHubContent(apiUrl); -async function checkExistingInstallation() { - const simoneExists = await fs.access('.simone').then(() => true).catch(() => false); - const claudeCommandsExists = await fs.access('.claude/commands/simone').then(() => true).catch(() => false); - return simoneExists || claudeCommandsExists; + for (const item of items) { + const itemLocalPath = path.join(localPath, item.name); + if (item.type === 'dir') { + await downloadDirectory(item.path, itemLocalPath, spinner, branch); + } else if (item.type === 'file') { + spinner.text = `Downloading ${item.path}...`; + await downloadFile(item.download_url, itemLocalPath); + } + } } -async function backupFile(filePath) { - try { - const exists = await fs.access(filePath).then(() => true).catch(() => false); - if (exists) { - const backupPath = `${filePath}.bak`; - await fs.copyFile(filePath, backupPath); - return backupPath; +async function checkExistingV3Installation() { + // A v0.3 installation is identified by its unique directory structure. + const v3Dirs = ['.simone/03_SPRINTS', '.simone/02_REQUIREMENTS']; + for (const dir of v3Dirs) { + try { + await fs.access(dir); + return true; // Found a v0.3 directory, so it's a v0.3 project. + } catch { + // Directory not found, continue checking. } - } catch (error) { - // Backup failed, but continue } - return null; + return false; } -async function backupCommandsAndDocs() { - const spinner = ora('Backing up existing commands and documentation...').start(); - const backedUpFiles = []; - +async function checkExistingV4Installation() { try { - // Files that will be updated and need backup - const filesToBackup = [ - '.simone/CLAUDE.md', - '.simone/02_REQUIREMENTS/CLAUDE.md', - '.simone/03_SPRINTS/CLAUDE.md', - '.simone/04_GENERAL_TASKS/CLAUDE.md' - ]; + await fs.access('.simone/00_FOUNDATION/CONSTITUTION.md'); + return true; + } catch { + return false; + } +} - // Backup CLAUDE.md files - for (const file of filesToBackup) { - const backupPath = await backupFile(file); - if (backupPath) { - backedUpFiles.push(backupPath); - } - } +async function backupV3ForMigration() { + const backupDir = '.simone_backup'; + await fs.mkdir(backupDir, { recursive: true }); + const spinner = ora('Backing up existing v0.3 installation...').start(); + try { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupPath = path.join(backupDir, `simone-v0.3-backup-${timestamp}`); + await fs.mkdir(backupPath, { recursive: true }); - // Backup all command files - const commandsDir = '.claude/commands/simone'; - const commandsExist = await fs.access(commandsDir).then(() => true).catch(() => false); - if (commandsExist) { - try { - const commandFiles = await fs.readdir(commandsDir, { recursive: true }); - for (const file of commandFiles) { - const filePath = path.join(commandsDir, file); - const stat = await fs.stat(filePath); - if (stat.isFile()) { - const backupPath = await backupFile(filePath); - if (backupPath) { - backedUpFiles.push(backupPath); - } - } + if (await fs.access('.simone').then(() => true).catch(() => false)) { + await fs.rename('.simone', path.join(backupPath, '.simone')); } - } catch (error) { - // Commands directory might be empty or have issues - } - } - - if (backedUpFiles.length > 0) { - spinner.succeed(chalk.green(`Backed up ${backedUpFiles.length} files (*.bak)`)); - } else { - spinner.succeed(chalk.gray('No existing files to backup')); + if (await fs.access('.claude/commands/simone').then(() => true).catch(() => false)) { + await fs.mkdir(path.join(backupPath, '.claude/commands'), { recursive: true }); + await fs.rename('.claude/commands/simone', path.join(backupPath, '.claude/commands/simone')); + } + spinner.succeed(chalk.green(`Backup complete! Old version moved to ${backupPath}`)); + } catch (error) { + spinner.fail(chalk.red('Backup failed')); + console.error(error); + throw new Error('Migration backup failed.'); } - return backedUpFiles; - } catch (error) { - spinner.fail(chalk.red('Backup failed')); - throw error; - } } -async function downloadDirectory(githubPath, localPath, spinner) { - await fs.mkdir(localPath, { recursive: true }); - - const items = await getDirectoryStructure(githubPath); - - for (const item of items) { - const itemLocalPath = path.join(localPath, item.name); - - if (item.type === 'dir') { - await downloadDirectory(item.path, itemLocalPath, spinner); - } else if (item.type === 'file') { - spinner.text = `Downloading ${item.path}...`; - await downloadFile(item.download_url, itemLocalPath); +async function updateV4Commands(spinner, branch) { + const commandsDir = '.claude/commands/simone'; + const backupDir = '.simone_backup'; + await fs.mkdir(backupDir, { recursive: true }); + spinner.start('Backing up existing v0.4 commands...'); + try { + if (await fs.access(commandsDir).then(() => true).catch(() => false)) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupPath = path.join(backupDir, `commands-v0.4-backup-${timestamp}`); + await fs.rename(commandsDir, backupPath); + spinner.succeed(chalk.green(`Backed up commands to ${backupPath}`)); + } else { + spinner.info(chalk.gray('No existing commands to back up.')); + } + } catch (error) { + spinner.fail(chalk.red('Command backup failed.')); + throw error; } - } + spinner.start('Downloading latest v0.4 commands...'); + await downloadDirectory('framework/commands', commandsDir, spinner, branch); } -async function installSimone(options = {}) { - console.log(chalk.blue.bold('\nšŸŽ‰ Welcome to HelloSimone!\n')); - console.log(chalk.gray('This installer will set up the Simone project management framework')); - console.log(chalk.gray('for your Claude Code project.\n')); - const hasExisting = await checkExistingInstallation(); - - if (hasExisting && !options.force) { - const response = await prompts({ - type: 'select', - name: 'action', - message: 'Existing Simone installation detected. What would you like to do?', - choices: [ - { title: 'Update (updates commands and docs only, preserves your work)', value: 'update' }, - { title: 'Skip installation', value: 'skip' }, - { title: 'Cancel', value: 'cancel' } - ] - }); - - if (response.action === 'skip' || response.action === 'cancel') { - console.log(chalk.yellow('\nInstallation cancelled.')); - process.exit(0); - } - - if (response.action === 'update') { - await backupCommandsAndDocs(); - } +async function installSimone(options = {}) { + const isPreview = options.preview; + const branch = isPreview ? 'v0.4' : 'master'; + + if (isPreview) { + console.log(chalk.yellow.bold('\nšŸŽ‰ Welcome to HelloSimone v0.4 PREVIEW!\n')); + console.log(chalk.red.bold('This is a preview version and may have bugs. Use with caution.\n')); + } else { + console.log(chalk.blue.bold('\nšŸŽ‰ Welcome to HelloSimone!\n')); + console.log(chalk.gray('This installer will set up the Simone v0.3 project management framework')); } + console.log(chalk.gray('for your Claude Code project.\n')); - const spinner = ora('Fetching Simone framework from GitHub...').start(); + const spinner = ora('Initializing...').start(); // Start spinner early try { - // Create .simone directory structure - const simoneDirs = [ - '.simone', - '.simone/01_PROJECT_DOCS', - '.simone/02_REQUIREMENTS', - '.simone/03_SPRINTS', - '.simone/04_GENERAL_TASKS', - '.simone/05_ARCHITECTURE_DECISIONS', - '.simone/10_STATE_OF_PROJECT', - '.simone/99_TEMPLATES' - ]; - - for (const dir of simoneDirs) { - await fs.mkdir(dir, { recursive: true }); - } + spinner.text = 'Checking for existing v0.3 installation...'; + const hasV3 = await checkExistingV3Installation(); + spinner.text = `v0.3 detected: ${hasV3}. Checking for existing v0.4 installation...`; + const hasV4 = await checkExistingV4Installation(); + spinner.text = `v0.4 detected: ${hasV4}. Determining installation path...`; + + if (isPreview) { + // --- V0.4 PREVIEW WORKFLOW --- + if (hasV4) { + // SCENARIO: Update existing v0.4 + spinner.stop(); // Stop spinner for prompt + const { confirm } = await prompts({ + type: 'confirm', name: 'confirm', + message: 'Existing v0.4 installation detected. Update framework commands?', + initial: true + }); + spinner.start('Proceeding with v0.4 update...'); // Restart spinner + if (!confirm) { console.log(chalk.yellow('Update cancelled.')); process.exit(0); } + await updateV4Commands(spinner, branch); + spinner.succeed(chalk.green('āœ… Simone v0.4 commands updated successfully!')); + console.log(chalk.gray(' Your .simone directory (Foundation, Progress, Docs) remains untouched.')); + + } else if (hasV3) { + // SCENARIO: Migrate from v0.3 to v0.4 + spinner.stop(); // Stop spinner for prompt + const { confirm } = await prompts({ + type: 'confirm', name: 'confirm', + message: 'Existing v0.3 installation detected. Upgrade to v0.4? This will back up and replace your current setup.', + initial: true + }); + spinner.start('Proceeding with v0.3 to v0.4 migration...'); // Restart spinner + if (!confirm) { console.log(chalk.yellow('Upgrade cancelled.')); process.exit(0); } + await backupV3ForMigration(); + spinner.text = 'Installing v0.4 framework...'; + await downloadDirectory('framework/.simone', '.simone', spinner, branch); + await downloadDirectory('framework/commands', '.claude/commands/simone', spinner, branch); + spinner.succeed(chalk.green('āœ… Simone v0.4 PREVIEW installed successfully!')); + console.log(chalk.yellow.bold('\nāš ļø IMPORTANT: Your old work has been backed up.')); + console.log(chalk.green('\nšŸš€ Your next step is crucial:')); + console.log(chalk.white(' Run /initialize to begin the guided migration of your tasks into the new Epic format.')); - // Only download manifest on fresh installs - if (!hasExisting) { - spinner.text = 'Downloading Simone framework files...'; - - // Get the root manifest - try { - const manifestUrl = `${GITHUB_RAW_URL}/.simone/00_PROJECT_MANIFEST.md`; - await downloadFile(manifestUrl, '.simone/00_PROJECT_MANIFEST.md'); - } catch (error) { - // If manifest doesn't exist, that's okay - } + } else { + // SCENARIO: Fresh install of v0.4 + spinner.text = 'Installing v0.4 framework...'; + await downloadDirectory('framework/.simone', '.simone', spinner, branch); + await downloadDirectory('framework/commands', '.claude/commands/simone', spinner, branch); + spinner.succeed(chalk.green('āœ… Simone v0.4 PREVIEW installed successfully!')); + console.log(chalk.green('\nšŸš€ Next steps:')); + console.log(chalk.white(' 1. Open this project in Claude Code')); + console.log(chalk.white(' 2. Use /initialize to set up your project foundation and first epic.')); + } - // Download templates on fresh install - try { - await downloadDirectory('.simone/99_TEMPLATES', '.simone/99_TEMPLATES', spinner); - } catch (error) { - spinner.text = 'Templates directory not found, skipping...'; - } - } + } else { + // --- V0.3 STABLE WORKFLOW --- + console.log(chalk.blue.bold('\nšŸŽ‰ Welcome to HelloSimone!\n')); + const spinner = ora('Initializing...').start(); // Start spinner early - // Always update CLAUDE.md documentation files - spinner.text = 'Updating documentation...'; - const claudeFiles = [ - '.simone/CLAUDE.md', - '.simone/02_REQUIREMENTS/CLAUDE.md', - '.simone/03_SPRINTS/CLAUDE.md', - '.simone/04_GENERAL_TASKS/CLAUDE.md' - ]; + try { + spinner.text = 'Checking for existing v0.3 installation...'; + const hasV3 = await checkExistingV3Installation(); + spinner.text = `v0.3 detected: ${hasV3}. Checking for existing v0.4 installation...`; + const hasV4 = await checkExistingV4Installation(); + spinner.text = `v0.4 detected: ${hasV4}. Determining installation path...`; + + if (hasV4) { + spinner.fail(chalk.red('A v0.4 project was detected.')); + console.error(chalk.red('Downgrading to v0.3 is not supported. Installation cancelled.')); + process.exit(1); + } - for (const claudeFile of claudeFiles) { - try { - const claudeUrl = `${GITHUB_RAW_URL}/${claudeFile}`; - await downloadFile(claudeUrl, claudeFile); - } catch (error) { - // If CLAUDE.md doesn't exist, that's okay - } - } + if (hasV3) { + // SCENARIO: Update existing v0.3 + spinner.succeed(chalk.green('Existing v0.3 installation detected. Updating commands and docs...')); + } else { + // SCENARIO: Fresh install of v0.3 + spinner.text = 'Installing v0.3 framework...'; + const simoneDirs = [ + '.simone', '.simone/01_PROJECT_DOCS', '.simone/02_REQUIREMENTS', + '.simone/03_SPRINTS', '.simone/04_GENERAL_TASKS', '.simone/05_ARCHITECTURE_DECISIONS', + '.simone/10_STATE_OF_PROJECT', '.simone/99_TEMPLATES' + ]; + for (const dir of simoneDirs) { + await fs.mkdir(dir, { recursive: true }); + } + const GITHUB_RAW_URL = `https://raw.githubusercontent.com/helmi/claude-simone/${branch}`; + await downloadFile(`${GITHUB_RAW_URL}/.simone/00_PROJECT_MANIFEST.md`, '.simone/00_PROJECT_MANIFEST.md').catch(()=>{}); + await downloadDirectory('.simone/99_TEMPLATES', '.simone/99_TEMPLATES', spinner, branch).catch(()=>{}); + } - // Create .claude/commands/simone directory - await fs.mkdir('.claude/commands/simone', { recursive: true }); + const GITHUB_RAW_URL = `https://raw.githubusercontent.com/helmi/claude-simone/${branch}`; + const claudeFiles = [ + '.simone/CLAUDE.md', '.simone/02_REQUIREMENTS/CLAUDE.md', + '.simone/03_SPRINTS/CLAUDE.md', '.simone/04_GENERAL_TASKS/CLAUDE.md' + ]; + for (const claudeFile of claudeFiles) { + await downloadFile(`${GITHUB_RAW_URL}/${claudeFile}`, claudeFile).catch(()=>{}); + } + await downloadDirectory('.claude/commands/simone', '.claude/commands/simone', spinner, branch).catch(()=>{}); + spinner.succeed(chalk.green('āœ… Simone v0.3 framework installed/updated successfully!')); + console.log(chalk.green('\nšŸš€ Next steps:')); + console.log(chalk.white(' 1. Open this project in Claude Code')); + console.log(chalk.white(' 2. Use /project:simone:initialize to set up your project\n')); - // Always update commands - spinner.text = 'Updating Simone commands...'; - try { - await downloadDirectory('.claude/commands/simone', '.claude/commands/simone', spinner); } catch (error) { - spinner.text = 'Commands directory not found, skipping...'; - } - - if (hasExisting) { - spinner.succeed(chalk.green('āœ… Simone framework updated successfully!')); - console.log(chalk.blue('\nšŸ”„ Updated:')); - console.log(chalk.gray(' • Commands in .claude/commands/simone/')); - console.log(chalk.gray(' • Documentation (CLAUDE.md files)')); - console.log(chalk.green('\nšŸ’¾ Your work is preserved:')); - console.log(chalk.gray(' • All tasks, sprints, and project files remain untouched')); - console.log(chalk.gray(' • Backups created as *.bak files')); - } else { - spinner.succeed(chalk.green('āœ… Simone framework installed successfully!')); + spinner.fail(chalk.red('Installation failed')); + console.error(chalk.red('\nError details:'), error.message); + process.exit(1); } - - console.log(chalk.blue('\nšŸ“ Created structure:')); - console.log(chalk.gray(' .simone/ - Project management root')); - console.log(chalk.gray(' .claude/commands/ - Claude custom commands')); - - console.log(chalk.green('\nšŸš€ Next steps:')); - console.log(chalk.white(' 1. Open this project in Claude Code')); - console.log(chalk.white(' 2. Use /project:simone commands to manage your project')); - console.log(chalk.white(' 3. Start with /project:simone:initialize to set up your project\n')); - + } + } catch (error) { spinner.fail(chalk.red('Installation failed')); console.error(chalk.red('\nError details:'), error.message); @@ -274,8 +273,8 @@ async function installSimone(options = {}) { program .name('hello-simone') .description('Installer for the Simone project management framework') - .version('0.3.0') - .option('-f, --force', 'Force installation without prompts') + .version('0.5.1') + .option('--preview', 'Install the preview version (v0.4) of Simone') .action(installSimone); program.parse(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 691f117..e446615 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hello-simone", - "version": "0.1.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hello-simone", - "version": "0.1.0", + "version": "0.5.1", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/package.json b/package.json index 6549de3..eb7785f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hello-simone", - "version": "0.3.0", + "version": "0.5.1", "description": "Installer for the Simone project management framework for Claude Code", "main": "index.js", "type": "module",