UNPKG

agp-cli

Version:

Agentic Programming Project CLI - Standardized knowledge layer for AI-assisted development

421 lines 18.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.initializeAgpDirectory = initializeAgpDirectory; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const project_detector_1 = require("./project-detector"); const template_manager_1 = require("./template-manager"); const project_analyzer_1 = require("./project-analyzer"); const logger_1 = require("./logger"); const DEFAULT_TEMPLATE_URL = 'https://github.com/bang9/agp-template.git'; async function initializeAgpDirectory(options) { const cwd = process.cwd(); const agpPath = path.join(cwd, '.agp'); const configPath = path.join(agpPath, '.config.json'); // Check if .agp already exists if ((await fs.pathExists(agpPath)) && !options.force) { // Check if .agp directory is empty (cloned project case) const agpContents = await fs.readdir(agpPath); const nonHiddenFiles = agpContents.filter((file) => !file.startsWith('.')); if (nonHiddenFiles.length === 0) { await pullSubmoduleContent(cwd); return; } else { throw new Error('AGP directory already exists. Use --force to overwrite.'); } } // Check if we're in a git repository const gitPath = path.join(cwd, '.git'); if (!(await fs.pathExists(gitPath))) { throw new Error('AGP requires a Git repository. Please run "git init" first.'); } // Backup existing config if force is enabled let existingConfig = null; if (options.force && (await fs.pathExists(configPath))) { try { const configContent = await fs.readFile(configPath, 'utf8'); existingConfig = JSON.parse(configContent); } catch (error) { // Cannot read existing config, will create new one } } // Setup AGP structure const templateUrl = options.templateUrl || DEFAULT_TEMPLATE_URL; let detectedProjectInfo; await logger_1.logger.withSpinner('Setting up AGP structure', async () => { detectedProjectInfo = await (0, project_detector_1.detectProjectType)(cwd); // Remove existing .agp if force is enabled if (await fs.pathExists(agpPath)) { await fs.remove(agpPath); } // Download template await (0, template_manager_1.downloadTemplate)(templateUrl, agpPath); // Ensure project directory exists const projectPath = path.join(agpPath, 'project'); await fs.ensureDir(projectPath); // Analyze project and generate documentation await (0, project_analyzer_1.analyzeProject)(cwd, detectedProjectInfo); // Create additional required directories and files await setupAdditionalStructure(agpPath); }); // Initialize git submodule (has user interaction - outside spinner) const submoduleUrl = await initializeSubmodule(agpPath, existingConfig?.submodule?.repository || undefined, templateUrl); // Finalize setup await logger_1.logger.withSpinner('Finalizing setup', async () => { // Create or restore config file await createConfigFile(agpPath, existingConfig, submoduleUrl); // Validation step await validateAgpSetup(agpPath); }); } async function initializeSubmodule(agpPath, existingUrl, templateUrl) { const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process'))); const inquirer = await Promise.resolve().then(() => __importStar(require('inquirer'))); const cwd = process.cwd(); // Handle Ctrl+C gracefully const originalDirectory = process.cwd(); const cleanup = () => { try { process.chdir(originalDirectory); } catch { // Ignore cleanup errors } }; process.on('SIGINT', () => { cleanup(); logger_1.logger.warning('Operation cancelled by user'); process.exit(0); }); let repositoryUrl = ''; let success = false; let retryCount = 0; const maxRetries = 3; // Use existing URL if available if (existingUrl) { repositoryUrl = existingUrl; logger_1.logger.info(`Using existing repository URL: ${existingUrl}`); } else { logger_1.logger.info('AGP requires a remote repository for the .agp submodule.'); logger_1.logger.info('Please create an empty repository and provide the URL:'); logger_1.logger.info('Examples:'); logger_1.logger.step('git@github.com:username/my-project-agp.git'); logger_1.logger.step('https://github.com/username/my-project-agp.git'); } while (!success && retryCount < maxRetries) { try { // Get repository URL from user if not already provided if (!repositoryUrl) { const answers = await inquirer.default.prompt([ { type: 'input', name: 'repoUrl', message: 'Enter repository URL:', validate: (input) => { if (!input.trim()) { return 'Repository URL is required'; } if (!isValidGitUrl(input.trim())) { return 'Please enter a valid Git repository URL'; } return true; }, }, ]); repositoryUrl = answers.repoUrl.trim(); } // Initialize .agp as a git repository logger_1.logger.progress('Initializing .agp repository'); process.chdir(agpPath); execSync('git init', { stdio: 'pipe' }); execSync('git add .', { stdio: 'pipe' }); execSync('git commit -m "Initial AGP setup"', { stdio: 'pipe' }); logger_1.logger.progressDone('.agp repository initialized'); // Add remote origin logger_1.logger.progress('Connecting to remote repository'); execSync(`git remote add origin ${repositoryUrl}`, { stdio: 'pipe' }); // Try to push to remote try { execSync('git push -u origin main', { stdio: 'pipe' }); logger_1.logger.progressDone('Remote repository connected'); } catch (error) { // Repository might not be empty, ask user what to do process.chdir(cwd); // Go back to original directory first logger_1.logger.warning('Repository contains existing content.'); const action = await inquirer.default.prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', choices: [ { name: 'Overwrite with new template', value: 'overwrite' }, { name: 'Merge with existing content', value: 'merge' }, { name: 'Cancel and use different repository', value: 'cancel' }, ], }, ]); if (action.action === 'cancel') { // Remove the created .agp directory and start over await fs.remove(agpPath); await setupAgpDirectoryAgain(agpPath); repositoryUrl = ''; continue; // Go back to asking for repository URL } else if (action.action === 'overwrite') { // Force push to overwrite existing content await logger_1.logger.withSpinner('Setting up repository', async () => { process.chdir(agpPath); // First try to pull and merge if possible try { execSync('git pull origin main --allow-unrelated-histories', { stdio: 'pipe' }); execSync('git push origin main', { stdio: 'pipe' }); } catch { // If pull fails, do a force push execSync('git push --force origin main', { stdio: 'pipe' }); } }); } else if (action.action === 'merge') { // Clone existing repository first, then merge template await logger_1.logger.withSpinner('Setting up repository', async () => { await fs.remove(agpPath); // Remove current .agp execSync(`git submodule add ${repositoryUrl} .agp`, { cwd, stdio: 'pipe' }); // Merge template content with existing await mergeTemplateWithExisting(agpPath, templateUrl); // Commit merged changes process.chdir(agpPath); execSync('git add .', { stdio: 'pipe' }); try { execSync('git commit -m "Merge AGP template with existing content"', { stdio: 'pipe' }); execSync('git push origin main', { stdio: 'pipe' }); } catch { // No changes to commit or push failed, that's ok } }); } process.chdir(cwd); // Return to original directory success = true; return repositoryUrl; } // Return to parent directory process.chdir(cwd); // Remove .agp from parent git tracking (in case it was already tracked) try { execSync('git rm -rf --cached .agp', { stdio: 'pipe' }); } catch { // Ignore if .agp wasn't tracked } // Add .agp as a submodule await logger_1.logger.withSpinner('Setting up repository', async () => { execSync(`git submodule add ${repositoryUrl} .agp`, { stdio: 'pipe' }); }); success = true; return repositoryUrl; } catch (error) { process.chdir(cwd); // Ensure we're back in original directory // Clean up .agp directory if it was created if (await Promise.resolve().then(() => __importStar(require('fs-extra'))).then((fs) => fs.pathExists(agpPath))) { await Promise.resolve().then(() => __importStar(require('fs-extra'))).then((fs) => fs.remove(agpPath)); // Recreate .agp with original content await setupAgpDirectoryAgain(agpPath); } retryCount++; logger_1.logger.error(`Failed to connect to repository (attempt ${retryCount}/${maxRetries})`); logger_1.logger.warning('Please check:'); logger_1.logger.step('Repository exists and is empty'); logger_1.logger.step('You have push permissions'); logger_1.logger.step('URL format is correct'); logger_1.logger.step('Repository is accessible'); // Reset repository URL for retry if we haven't exceeded max retries if (retryCount < maxRetries) { repositoryUrl = ''; if (existingUrl && retryCount === 1) { logger_1.logger.warning('Cannot connect with existing URL. Please enter a new URL.'); } } else { logger_1.logger.error('Maximum retry attempts exceeded. Please check your repository and try again.'); throw new Error('Failed to initialize submodule after maximum attempts'); } } } // This should never be reached, but TypeScript requires it throw new Error('Failed to initialize submodule after multiple attempts'); } function isValidGitUrl(url) { // Check for common Git URL patterns const patterns = [ /^git@[\w\.-]+:[\w\.-]+\/[\w\.-]+\.git$/, // SSH: git@github.com:user/repo.git /^https?:\/\/[\w\.-]+\/[\w\.-]+\/[\w\.-]+\.git$/, // HTTPS: https://github.com/user/repo.git /^https?:\/\/[\w\.-]+\/[\w\.-]+\/[\w\.-]+$/, // HTTPS without .git ]; return patterns.some((pattern) => pattern.test(url)); } async function setupAdditionalStructure(agpPath) { // Create sessions directory const sessionsPath = path.join(agpPath, 'sessions'); await fs.ensureDir(sessionsPath); // Create .gitignore const gitignorePath = path.join(agpPath, '.gitignore'); const gitignoreContent = `.config.json *.tmp `; await fs.writeFile(gitignorePath, gitignoreContent); } async function createConfigFile(agpPath, existingConfig, submoduleUrl) { const configPath = path.join(agpPath, '.config.json'); const config = { session: existingConfig?.session || { user: '', current: '', }, submodule: { repository: submoduleUrl, lastUpdated: new Date().toISOString(), }, }; await fs.writeFile(configPath, JSON.stringify(config, null, 2)); } async function validateAgpSetup(agpPath) { const requiredFiles = [ 'instructions.md', '.config.json', '.gitignore', 'architecture/overview.md', 'patterns/overview.md', 'architecture/feature-domains.md', 'architecture/project-overview.md', ]; const requiredDirs = ['sessions', 'architecture', 'patterns', 'project']; // Check required files for (const file of requiredFiles) { const filePath = path.join(agpPath, file); if (!(await fs.pathExists(filePath))) { throw new Error(`Required file missing: ${file}`); } } // Check required directories for (const dir of requiredDirs) { const dirPath = path.join(agpPath, dir); if (!(await fs.pathExists(dirPath))) { throw new Error(`Required directory missing: ${dir}`); } } // Check Git submodule status const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process'))); const cwd = process.cwd(); try { const submoduleStatus = execSync('git submodule status .agp', { cwd, encoding: 'utf8', stdio: 'pipe', }); if (!submoduleStatus.trim()) { throw new Error('Git submodule not properly initialized'); } // Validation passed } catch (error) { throw new Error('Git submodule validation failed'); } } async function mergeTemplateWithExisting(agpPath, templateUrl) { // Create a temporary directory for template download const tempDir = path.join(agpPath, '.temp-template'); await fs.ensureDir(tempDir); try { // Download template to temporary directory await (0, template_manager_1.downloadTemplate)(templateUrl, tempDir); // Merge template files with existing content // Priority: existing files > template files (don't overwrite existing) const templateFiles = await fs.readdir(tempDir); for (const file of templateFiles) { const templateFilePath = path.join(tempDir, file); const targetFilePath = path.join(agpPath, file); if (!(await fs.pathExists(targetFilePath))) { // File doesn't exist in target, copy from template await fs.copy(templateFilePath, targetFilePath); } else if (file === 'instructions.md') { // Always update instructions.md from template await fs.copy(templateFilePath, targetFilePath); } // For other existing files, keep the existing version } // Clean up temporary directory await fs.remove(tempDir); } catch (error) { // Clean up on error await fs.remove(tempDir); throw error; } } async function pullSubmoduleContent(cwd) { const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process'))); try { // Initialize and update submodules execSync('git submodule init', { cwd, stdio: 'pipe' }); execSync('git submodule update --remote', { cwd, stdio: 'pipe' }); // Validate the setup const agpPath = path.join(cwd, '.agp'); await validateAgpSetup(agpPath); logger_1.logger.success('Submodule content pulled successfully!'); } catch (error) { throw new Error(`Failed to pull submodule content: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async function setupAgpDirectoryAgain(agpPath) { // This function recreates the .agp directory structure after a failed submodule setup const fs = await Promise.resolve().then(() => __importStar(require('fs-extra'))); const path = await Promise.resolve().then(() => __importStar(require('path'))); await fs.ensureDir(agpPath); await fs.ensureDir(path.join(agpPath, 'architecture')); await fs.ensureDir(path.join(agpPath, 'patterns')); await fs.ensureDir(path.join(agpPath, 'project')); // Re-download template files if needed // For now, just recreate basic structure } //# sourceMappingURL=agp-init.js.map