tasq/node_modules/@claude-flow/codex/dist/initializer.js

666 lines
23 KiB
JavaScript

/**
* @claude-flow/codex - CodexInitializer
*
* Main initialization class for setting up Codex projects
*/
import fs from 'fs-extra';
import path from 'path';
import { generateAgentsMd } from './generators/agents-md.js';
import { generateSkillMd, generateBuiltInSkill } from './generators/skill-md.js';
import { generateConfigToml } from './generators/config-toml.js';
import { DEFAULT_SKILLS_BY_TEMPLATE, AGENTS_OVERRIDE_TEMPLATE, GITIGNORE_ENTRIES } from './templates/index.js';
/**
* Bundled skills source directory (relative to package)
*/
const BUNDLED_SKILLS_DIR = '../../../../.agents/skills';
/**
* Main initializer for Codex projects
*/
export class CodexInitializer {
projectPath = '';
template = 'default';
skills = [];
force = false;
dual = false;
bundledSkillsPath = '';
/**
* Initialize a new Codex project
*/
async initialize(options) {
this.projectPath = path.resolve(options.projectPath);
this.template = options.template ?? 'default';
this.skills = options.skills ?? DEFAULT_SKILLS_BY_TEMPLATE[this.template];
this.force = options.force ?? false;
this.dual = options.dual ?? false;
// Resolve bundled skills path (relative to this file's location)
this.bundledSkillsPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), BUNDLED_SKILLS_DIR);
const filesCreated = [];
const skillsGenerated = [];
const warnings = [];
const errors = [];
try {
// Validate project path
await this.validateProjectPath();
// Check if already initialized
const alreadyInitialized = await this.isAlreadyInitialized();
if (alreadyInitialized && !this.force) {
return {
success: false,
filesCreated,
skillsGenerated,
warnings: ['Project already initialized. Use --force to overwrite.'],
errors: ['Project already initialized'],
};
}
if (alreadyInitialized && this.force) {
warnings.push('Overwriting existing configuration files');
}
// Create directory structure
await this.createDirectoryStructure();
// Generate AGENTS.md
const agentsMd = await this.generateAgentsMd();
const agentsMdPath = path.join(this.projectPath, 'AGENTS.md');
if (await this.shouldWriteFile(agentsMdPath)) {
await fs.writeFile(agentsMdPath, agentsMd, 'utf-8');
filesCreated.push('AGENTS.md');
}
else {
warnings.push('AGENTS.md already exists - skipped');
}
// Generate config.toml
const configToml = await this.generateConfigToml();
const configTomlPath = path.join(this.projectPath, '.agents', 'config.toml');
if (await this.shouldWriteFile(configTomlPath)) {
await fs.writeFile(configTomlPath, configToml, 'utf-8');
filesCreated.push('.agents/config.toml');
}
else {
warnings.push('.agents/config.toml already exists - skipped');
}
// Copy bundled skills first (for full/enterprise templates or specific skills)
const bundledResult = await this.copyBundledSkills();
skillsGenerated.push(...bundledResult.copied);
warnings.push(...bundledResult.warnings);
// For skills not bundled, generate from templates
for (const skillName of this.skills) {
// Skip if already copied as bundled skill
if (bundledResult.copied.includes(skillName)) {
filesCreated.push(`.agents/skills/${skillName}/SKILL.md`);
continue;
}
try {
const skillResult = await this.generateSkill(skillName);
if (skillResult.created) {
skillsGenerated.push(skillName);
filesCreated.push(skillResult.path);
}
else if (skillResult.skipped) {
// Only warn if not already in bundled warnings
if (!bundledResult.warnings.some(w => w.includes(skillName))) {
warnings.push(`Skill ${skillName} already exists - skipped`);
}
}
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
warnings.push(`Failed to generate skill ${skillName}: ${errorMessage}`);
}
}
// Generate local overrides template
const overridePath = path.join(this.projectPath, '.codex', 'AGENTS.override.md');
if (await this.shouldWriteFile(overridePath)) {
await fs.writeFile(overridePath, AGENTS_OVERRIDE_TEMPLATE, 'utf-8');
filesCreated.push('.codex/AGENTS.override.md');
}
// Generate local config.toml
const localConfigPath = path.join(this.projectPath, '.codex', 'config.toml');
if (await this.shouldWriteFile(localConfigPath)) {
await fs.writeFile(localConfigPath, await this.generateLocalConfigToml(), 'utf-8');
filesCreated.push('.codex/config.toml');
}
// Update .gitignore
const gitignoreUpdated = await this.updateGitignore();
if (gitignoreUpdated) {
filesCreated.push('.gitignore (updated)');
}
// Register MCP server with Codex
const mcpResult = await this.registerMCPServer();
if (mcpResult.registered) {
filesCreated.push('MCP server (claude-flow) registered');
}
if (mcpResult.warning) {
warnings.push(mcpResult.warning);
}
// If dual mode, also generate Claude Code files
if (this.dual) {
const dualResult = await this.generateDualPlatformFiles();
filesCreated.push(...dualResult.files);
if (dualResult.warnings) {
warnings.push(...dualResult.warnings);
}
}
// Create a README for the .agents directory
const agentsReadmePath = path.join(this.projectPath, '.agents', 'README.md');
if (await this.shouldWriteFile(agentsReadmePath)) {
await fs.writeFile(agentsReadmePath, this.generateAgentsReadme(), 'utf-8');
filesCreated.push('.agents/README.md');
}
const result = {
success: true,
filesCreated,
skillsGenerated,
};
if (warnings.length > 0) {
result.warnings = warnings;
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
errors.push(errorMessage);
const result = {
success: false,
filesCreated,
skillsGenerated,
errors,
};
if (warnings.length > 0) {
result.warnings = warnings;
}
return result;
}
}
/**
* Validate that the project path is valid and writable
*/
async validateProjectPath() {
try {
await fs.ensureDir(this.projectPath);
// Check write permissions by attempting to create a temp file
const tempFile = path.join(this.projectPath, '.codex-init-test');
await fs.writeFile(tempFile, 'test', 'utf-8');
await fs.remove(tempFile);
}
catch (error) {
throw new Error(`Cannot write to project path: ${this.projectPath}`);
}
}
/**
* Check if project is already initialized
*/
async isAlreadyInitialized() {
const agentsMdExists = await fs.pathExists(path.join(this.projectPath, 'AGENTS.md'));
const agentsConfigExists = await fs.pathExists(path.join(this.projectPath, '.agents', 'config.toml'));
return agentsMdExists || agentsConfigExists;
}
/**
* Check if we should write a file (force mode or doesn't exist)
*/
async shouldWriteFile(filePath) {
if (this.force) {
return true;
}
return !(await fs.pathExists(filePath));
}
/**
* Create the directory structure
*/
async createDirectoryStructure() {
const dirs = [
'.agents',
'.agents/skills',
'.codex',
'.claude-flow',
'.claude-flow/data',
'.claude-flow/logs',
];
for (const dir of dirs) {
const fullPath = path.join(this.projectPath, dir);
await fs.ensureDir(fullPath);
}
}
/**
* Copy bundled skills from the package or source directory
* Returns the list of skills copied
*/
async copyBundledSkills() {
const copied = [];
const warnings = [];
// Check if bundled skills directory exists
if (!await fs.pathExists(this.bundledSkillsPath)) {
warnings.push(`Bundled skills directory not found: ${this.bundledSkillsPath}`);
return { copied, warnings };
}
const destSkillsDir = path.join(this.projectPath, '.agents', 'skills');
// Get all skill directories
const skillDirs = await fs.readdir(this.bundledSkillsPath, { withFileTypes: true });
for (const dirent of skillDirs) {
if (!dirent.isDirectory())
continue;
const skillName = dirent.name;
const srcPath = path.join(this.bundledSkillsPath, skillName);
const destPath = path.join(destSkillsDir, skillName);
// Skip if skill should be filtered (based on template)
// For 'full' and 'enterprise' templates, include all skills
const includeAll = this.template === 'full' || this.template === 'enterprise';
if (!includeAll && !this.skills.includes(skillName)) {
continue;
}
try {
// Check if skill already exists and we're not forcing
if (!this.force && await fs.pathExists(destPath)) {
warnings.push(`Skill ${skillName} already exists - skipped`);
continue;
}
// Copy the entire skill directory
await fs.copy(srcPath, destPath, { overwrite: this.force });
copied.push(skillName);
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
warnings.push(`Failed to copy skill ${skillName}: ${errorMessage}`);
}
}
return { copied, warnings };
}
/**
* Check if a skill is bundled (exists in source directory)
*/
async isBundledSkill(skillName) {
const skillPath = path.join(this.bundledSkillsPath, skillName);
return fs.pathExists(skillPath);
}
/**
* Register claude-flow as MCP server with Codex
*/
async registerMCPServer() {
try {
const { execSync } = await import('child_process');
// Check if codex CLI is available
try {
execSync('which codex', { stdio: 'pipe' });
}
catch {
return {
registered: false,
warning: 'Codex CLI not found. Run: codex mcp add claude-flow -- npx claude-flow mcp start',
};
}
// Check if already registered
try {
const list = execSync('codex mcp list 2>&1', { encoding: 'utf-8' });
if (list.includes('claude-flow')) {
return { registered: true }; // Already registered
}
}
catch {
// Ignore list errors
}
// Register the MCP server
try {
execSync('codex mcp add claude-flow -- npx claude-flow mcp start', {
stdio: 'pipe',
timeout: 10000,
});
return { registered: true };
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
return {
registered: false,
warning: `Failed to register MCP server: ${errorMessage}. Run manually: codex mcp add claude-flow -- npx claude-flow mcp start`,
};
}
}
catch {
return {
registered: false,
warning: 'Could not register MCP server. Run manually: codex mcp add claude-flow -- npx claude-flow mcp start',
};
}
}
/**
* Generate AGENTS.md content
*/
async generateAgentsMd() {
const projectName = path.basename(this.projectPath);
return generateAgentsMd({
projectName,
template: this.template,
skills: this.skills,
});
}
/**
* Generate config.toml content
*/
async generateConfigToml() {
return generateConfigToml({
skills: this.skills.map(skill => ({
path: `.agents/skills/${skill}`,
enabled: true,
})),
});
}
/**
* Generate local config.toml for .codex directory
*/
async generateLocalConfigToml() {
return `# Local Codex Configuration
# This file overrides .agents/config.toml for local development
# DO NOT commit this file to version control
# Development profile - more permissive
approval_policy = "never"
sandbox_mode = "danger-full-access"
web_search = "live"
# Debug settings
# Uncomment to enable debug logging
# CODEX_LOG_LEVEL = "debug"
# Local MCP server overrides
# [mcp_servers.local]
# command = "node"
# args = ["./local-mcp-server.js"]
# enabled = true
# Environment-specific settings
# [env]
# ANTHROPIC_API_KEY = "your-local-key"
`;
}
/**
* Generate a skill
*/
async generateSkill(skillName) {
const skillDir = path.join(this.projectPath, '.agents', 'skills', skillName);
const skillPath = path.join(skillDir, 'SKILL.md');
// Check if skill already exists
if (!this.force && await fs.pathExists(skillPath)) {
return { created: false, skipped: true, path: `.agents/skills/${skillName}/SKILL.md` };
}
await fs.ensureDir(skillDir);
// Check if it's a built-in skill
const builtInSkills = [
'swarm-orchestration',
'memory-management',
'sparc-methodology',
'security-audit',
'performance-analysis',
'github-automation',
];
let skillMd;
if (builtInSkills.includes(skillName)) {
const result = await generateBuiltInSkill(skillName);
skillMd = result.skillMd;
// Also write any associated scripts or references
if (Object.keys(result.scripts).length > 0) {
const scriptsDir = path.join(skillDir, 'scripts');
await fs.ensureDir(scriptsDir);
for (const [scriptName, scriptContent] of Object.entries(result.scripts)) {
await fs.writeFile(path.join(scriptsDir, scriptName), scriptContent, 'utf-8');
}
}
if (Object.keys(result.references).length > 0) {
const refsDir = path.join(skillDir, 'docs');
await fs.ensureDir(refsDir);
for (const [refName, refContent] of Object.entries(result.references)) {
await fs.writeFile(path.join(refsDir, refName), refContent, 'utf-8');
}
}
}
else {
// Generate a custom skill template
skillMd = await generateSkillMd({
name: skillName,
description: `Custom skill: ${skillName}`,
triggers: ['Define when to trigger this skill'],
skipWhen: ['Define when to skip this skill'],
});
}
await fs.writeFile(skillPath, skillMd, 'utf-8');
return { created: true, skipped: false, path: `.agents/skills/${skillName}/SKILL.md` };
}
/**
* Update .gitignore with Codex entries
*/
async updateGitignore() {
const gitignorePath = path.join(this.projectPath, '.gitignore');
let content = '';
if (await fs.pathExists(gitignorePath)) {
content = await fs.readFile(gitignorePath, 'utf-8');
}
// Check if Codex entries already exist
if (content.includes('.codex/')) {
return false; // Already has entries
}
// Add entries with proper spacing
const separator = content.length > 0 && !content.endsWith('\n') ? '\n\n' : '\n';
const newContent = content + separator + GITIGNORE_ENTRIES.join('\n') + '\n';
await fs.writeFile(gitignorePath, newContent, 'utf-8');
return true;
}
/**
* Generate README for .agents directory
*/
generateAgentsReadme() {
return `# .agents Directory
This directory contains agent configuration and skills for OpenAI Codex CLI.
## Structure
\`\`\`
.agents/
config.toml # Main configuration file
skills/ # Skill definitions
skill-name/
SKILL.md # Skill instructions
scripts/ # Optional scripts
docs/ # Optional documentation
README.md # This file
\`\`\`
## Configuration
The \`config.toml\` file controls:
- Model selection
- Approval policies
- Sandbox modes
- MCP server connections
- Skills configuration
## Skills
Skills are invoked using \`$skill-name\` syntax. Each skill has:
- YAML frontmatter with metadata
- Trigger and skip conditions
- Commands and examples
## Documentation
- Main instructions: \`AGENTS.md\` (project root)
- Local overrides: \`.codex/AGENTS.override.md\` (gitignored)
- Claude Flow: https://github.com/ruvnet/claude-flow
`;
}
/**
* Generate dual-platform files (Claude Code + Codex)
*/
async generateDualPlatformFiles() {
const files = [];
const warnings = [];
// Check if CLAUDE.md already exists
const claudeMdPath = path.join(this.projectPath, 'CLAUDE.md');
const claudeMdExists = await fs.pathExists(claudeMdPath);
if (claudeMdExists && !this.force) {
warnings.push('CLAUDE.md already exists - not overwriting. Use --force to replace.');
return { files, warnings };
}
const projectName = path.basename(this.projectPath);
// Generate a CLAUDE.md that references AGENTS.md
const claudeMd = `# ${projectName}
> This project supports both Claude Code and OpenAI Codex.
## Platform Compatibility
| Platform | Config File | Skill Syntax |
|----------|-------------|--------------|
| Claude Code | CLAUDE.md | /skill-name |
| OpenAI Codex | AGENTS.md | $skill-name |
## Instructions
**Primary instructions are in \`AGENTS.md\`** (Agentic AI Foundation standard).
This file provides compatibility for Claude Code users.
## Quick Start
\`\`\`bash
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm test
\`\`\`
## Available Skills
Both platforms share the same skills in \`.agents/skills/\`:
${this.skills.map(s => `- \`$${s}\` (Codex) / \`/${s}\` (Claude Code)`).join('\n')}
## Configuration
### Codex Configuration
- Main: \`.agents/config.toml\`
- Local: \`.codex/config.toml\` (gitignored)
### Claude Code Configuration
- This file: \`CLAUDE.md\`
- Local: \`CLAUDE.local.md\` (gitignored)
## MCP Integration
\`\`\`bash
# Start MCP server
npx @claude-flow/cli mcp start
\`\`\`
## Swarm Orchestration
This project uses hierarchical swarm coordination:
| Setting | Value |
|---------|-------|
| Topology | hierarchical |
| Max Agents | 8 |
| Strategy | specialized |
## Code Standards
- Files under 500 lines
- No hardcoded secrets
- Input validation at boundaries
- Typed interfaces for APIs
## Security
- NEVER commit .env files or secrets
- Always validate user input
- Use parameterized queries for SQL
## Full Documentation
For complete instructions, see \`AGENTS.md\`.
---
*Generated by @claude-flow/codex - Dual platform mode*
`;
await fs.writeFile(claudeMdPath, claudeMd, 'utf-8');
files.push('CLAUDE.md');
// Generate CLAUDE.local.md template
const claudeLocalPath = path.join(this.projectPath, 'CLAUDE.local.md');
if (await this.shouldWriteFile(claudeLocalPath)) {
const claudeLocal = `# Local Development Configuration
## Environment
\`\`\`bash
# Development settings
CLAUDE_FLOW_LOG_LEVEL=debug
\`\`\`
## Personal Preferences
[Add your preferences here]
## Debug Settings
Enable verbose logging for development.
---
*This file is gitignored and contains local-only settings.*
`;
await fs.writeFile(claudeLocalPath, claudeLocal, 'utf-8');
files.push('CLAUDE.local.md');
}
// Update .gitignore for CLAUDE.local.md
const gitignorePath = path.join(this.projectPath, '.gitignore');
if (await fs.pathExists(gitignorePath)) {
let content = await fs.readFile(gitignorePath, 'utf-8');
if (!content.includes('CLAUDE.local.md')) {
content += '\n# Claude Code local config\nCLAUDE.local.md\n';
await fs.writeFile(gitignorePath, content, 'utf-8');
}
}
warnings.push('Generated dual-platform setup. AGENTS.md is the canonical source.');
return { files, warnings };
}
/**
* Get the list of files that would be created (dry-run)
*/
async dryRun(options) {
const files = [
'AGENTS.md',
'.agents/config.toml',
'.agents/README.md',
'.codex/AGENTS.override.md',
'.codex/config.toml',
'.gitignore (updated)',
];
const skills = options.skills ?? DEFAULT_SKILLS_BY_TEMPLATE[options.template ?? 'default'];
for (const skill of skills) {
files.push(`.agents/skills/${skill}/SKILL.md`);
}
if (options.dual) {
files.push('CLAUDE.md');
files.push('CLAUDE.local.md');
}
return files;
}
}
/**
* Quick initialization function for programmatic use
*/
export async function initializeCodexProject(projectPath, options) {
const initializer = new CodexInitializer();
const initOptions = {
projectPath,
template: options?.template ?? 'default',
force: options?.force ?? false,
dual: options?.dual ?? false,
};
if (options?.skills) {
initOptions.skills = options.skills;
}
return initializer.initialize(initOptions);
}
//# sourceMappingURL=initializer.js.map