agp-cli
Version:
Agentic Programming Project CLI - Standardized knowledge layer for AI-assisted development
421 lines • 18.9 kB
JavaScript
;
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