claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
1,181 lines • 78.9 kB
JavaScript
/**
* Init Executor
* Main execution logic for V3 initialization
*/
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import { dirname } from 'path';
// ESM-compatible __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
import { detectPlatform, DEFAULT_INIT_OPTIONS } from './types.js';
import { generateSettingsJson, generateSettings } from './settings-generator.js';
import { generateMCPJson } from './mcp-generator.js';
import { generateStatuslineScript } from './statusline-generator.js';
import { generatePreCommitHook, generatePostCommitHook, generateSessionManager, generateAgentRouter, generateMemoryHelper, generateHookHandler, generateIntelligenceStub, generateAutoMemoryHook, generateRufloHookCjs, } from './helpers-generator.js';
import { generateClaudeMd } from './claudemd-generator.js';
/**
* Skills to copy based on configuration
*/
const SKILLS_MAP = {
core: [
'swarm-orchestration',
'swarm-advanced',
'sparc-methodology',
'hooks-automation',
'pair-programming',
'verification-quality',
'stream-chain',
'skill-builder',
],
browser: ['browser'], // agent-browser integration
dualMode: ['dual-mode'], // Claude Code + Codex hybrid execution
agentdb: [
'agentdb-advanced',
'agentdb-learning',
'agentdb-memory-patterns',
'agentdb-optimization',
'agentdb-vector-search',
'reasoningbank-agentdb',
'reasoningbank-intelligence',
],
github: [
'github-code-review',
'github-multi-repo',
'github-project-management',
'github-release-management',
'github-workflow-automation',
],
flowNexus: [
'flow-nexus-neural',
'flow-nexus-platform',
'flow-nexus-swarm',
],
v3: [
'v3-cli-modernization',
'v3-core-implementation',
'v3-ddd-architecture',
'v3-integration-deep',
'v3-mcp-optimization',
'v3-memory-unification',
'v3-performance-optimization',
'v3-security-overhaul',
'v3-swarm-coordination',
],
};
/**
* Commands to copy based on configuration
* ADR-128 Phase 4: every subdirectory under .claude/commands/ now has a
* corresponding key. The flow-nexus/ dir was deleted (belongs to the plugin).
* New substrate keys default true; opt-in keys (pair, training, stream-chain,
* truth, verify) default false per ADR-128 §Phase 3 opt-in rationale.
*/
const COMMANDS_MAP = {
core: ['claude-flow-help.md', 'claude-flow-swarm.md', 'claude-flow-memory.md'],
analysis: ['analysis'],
automation: ['automation'],
github: ['github'],
hooks: ['hooks'],
monitoring: ['monitoring'],
optimization: ['optimization'],
sparc: ['sparc'],
// ADR-128 Phase 4 promotions (previously orphaned)
agents: ['agents'],
coordination: ['coordination'],
hiveMind: ['hive-mind'],
memory: ['memory'],
swarm: ['swarm'],
workflows: ['workflows'],
// Opt-in categories (non-universal; default false in CommandsConfig)
pair: ['pair'],
training: ['training'],
streamChain: ['stream-chain'],
truth: ['truth'],
verify: ['verify'],
};
/**
* Agents to copy based on configuration
*/
const AGENTS_MAP = {
core: ['core'],
consensus: ['consensus'],
github: ['github'],
hiveMind: ['hive-mind'],
sparc: ['sparc'],
swarm: ['swarm'],
browser: ['browser'], // agent-browser integration
dualMode: ['dual-mode'], // Claude Code + Codex hybrid execution
// V3-specific agents
v3: ['v3'],
optimization: ['optimization'],
templates: ['templates'],
testing: ['testing'],
sublinear: ['sublinear'],
flowNexus: ['flow-nexus'],
analysis: ['analysis'],
architecture: ['architecture'],
development: ['development'],
devops: ['devops'],
documentation: ['documentation'],
specialized: ['specialized'],
goal: ['goal'],
sona: ['sona'],
payments: ['payments'],
data: ['data'],
custom: ['custom'],
};
/**
* Directory structure to create
*/
const DIRECTORIES = {
claude: [
'.claude',
'.claude/skills',
'.claude/commands',
'.claude/agents',
'.claude/helpers',
],
runtime: [
'.claude-flow',
'.claude-flow/data',
'.claude-flow/logs',
'.claude-flow/sessions',
'.claude-flow/hooks',
'.claude-flow/agents',
'.claude-flow/workflows',
],
};
/**
* Execute initialization
*/
export async function executeInit(options) {
// Detect platform
const platform = detectPlatform();
const result = {
success: true,
platform,
created: {
directories: [],
files: [],
},
skipped: [],
errors: [],
summary: {
skillsCount: 0,
commandsCount: 0,
agentsCount: 0,
hooksEnabled: 0,
},
};
const targetDir = options.targetDir;
try {
// Create directory structure
await createDirectories(targetDir, options, result);
// Generate and write settings.json
if (options.components.settings) {
await writeSettings(targetDir, options, result);
}
// Generate and write .mcp.json
if (options.components.mcp) {
await writeMCPConfig(targetDir, options, result);
}
// Copy skills
if (options.components.skills) {
await copySkills(targetDir, options, result);
}
// Copy commands
if (options.components.commands) {
await copyCommands(targetDir, options, result);
}
// Copy agents
if (options.components.agents) {
await copyAgents(targetDir, options, result);
}
// Generate helpers
if (options.components.helpers) {
await writeHelpers(targetDir, options, result);
}
// Generate statusline
if (options.components.statusline) {
await writeStatusline(targetDir, options, result);
}
// Generate runtime config
if (options.components.runtime) {
await writeRuntimeConfig(targetDir, options, result);
}
// Create initial metrics for statusline (prevents "all zeros" display)
if (options.components.statusline) {
await writeInitialMetrics(targetDir, options, result);
}
// Generate CLAUDE.md
if (options.components.claudeMd) {
await writeClaudeMd(targetDir, options, result);
}
// Count enabled hooks
result.summary.hooksEnabled = countEnabledHooks(options);
}
catch (error) {
result.success = false;
result.errors.push(error instanceof Error ? error.message : String(error));
}
return result;
}
/**
* Merge new settings into existing settings.json
* Preserves user customizations while adding new features like Agent Teams
* Uses platform-specific commands for Mac, Linux, and Windows
*/
function mergeSettingsForUpgrade(existing) {
const merged = { ...existing };
const platform = detectPlatform();
const isWindows = platform.os === 'windows';
// Platform-specific command wrappers
// Windows: Use PowerShell-compatible commands
// Mac/Linux: Use bash-compatible commands with 2>/dev/null
// NOTE: teammateIdleCmd and taskCompletedCmd were removed.
// TeammateIdle/TaskCompleted are not valid Claude Code hook events and caused warnings.
// Agent Teams hook config lives in claudeFlow.agentTeams.hooks instead.
// 1. Merge env vars (preserve existing, add new)
const existingEnv = existing.env || {};
merged.env = {
...existingEnv,
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
CLAUDE_FLOW_V3_ENABLED: existingEnv.CLAUDE_FLOW_V3_ENABLED || 'true',
CLAUDE_FLOW_HOOKS_ENABLED: existingEnv.CLAUDE_FLOW_HOOKS_ENABLED || 'true',
};
// 2. Merge hooks (preserve existing, add new Agent Teams + auto-memory hooks)
const existingHooks = existing.hooks || {};
merged.hooks = { ...existingHooks };
// Cross-platform auto-memory hook commands that resolve paths via git root.
// Uses node -e with git rev-parse so hooks work regardless of CWD (#1259, #1284).
const gitRootResolver = "var c=require('child_process'),p=require('path'),u=require('url'),r;"
+ "try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}"
+ 'catch(e){r=process.cwd()}';
const autoMemoryScript = '.claude/helpers/auto-memory-hook.mjs';
const autoMemoryImportCmd = `node -e "${gitRootResolver}var f=p.join(r,'${autoMemoryScript}');import(u.pathToFileURL(f).href)" import`;
const autoMemorySyncCmd = `node -e "${gitRootResolver}var f=p.join(r,'${autoMemoryScript}');import(u.pathToFileURL(f).href)" sync`;
// Add auto-memory import to SessionStart (if not already present)
const sessionStartHooks = existingHooks.SessionStart;
const hasAutoMemoryImport = sessionStartHooks?.some(group => group.hooks?.some(h => h.command?.includes('auto-memory-hook')));
if (!hasAutoMemoryImport) {
const startHooks = merged.hooks;
if (!startHooks.SessionStart) {
startHooks.SessionStart = [{ hooks: [] }];
}
const startGroup = startHooks.SessionStart[0];
if (!startGroup.hooks)
startGroup.hooks = [];
startGroup.hooks.push({
type: 'command',
command: autoMemoryImportCmd,
timeout: 6000,
continueOnError: true,
});
}
// Add auto-memory sync to SessionEnd (if not already present)
const sessionEndHooks = existingHooks.SessionEnd;
const hasAutoMemorySync = sessionEndHooks?.some(group => group.hooks?.some(h => h.command?.includes('auto-memory-hook')));
if (!hasAutoMemorySync) {
const endHooks = merged.hooks;
if (!endHooks.SessionEnd) {
endHooks.SessionEnd = [{ hooks: [] }];
}
const endGroup = endHooks.SessionEnd[0];
if (!endGroup.hooks)
endGroup.hooks = [];
// Insert at beginning so sync runs before other cleanup
endGroup.hooks.unshift({
type: 'command',
command: autoMemorySyncCmd,
timeout: 8000,
continueOnError: true,
});
}
// NOTE: TeammateIdle and TaskCompleted are NOT valid Claude Code hook events.
// They cause warnings when present in settings.json hooks.
// Remove them if they exist from a previous init.
delete merged.hooks.TeammateIdle;
delete merged.hooks.TaskCompleted;
// Their configuration lives in claudeFlow.agentTeams.hooks instead.
// 3. Fix statusLine config (remove invalid fields, ensure correct format)
// Claude Code only supports: type, command, padding
const existingStatusLine = existing.statusLine;
if (existingStatusLine) {
merged.statusLine = {
type: 'command',
command: existingStatusLine.command || `node -e "var c=require('child_process'),p=require('path'),r;try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}catch(e){r=process.cwd()}var s=p.join(r,'.claude/helpers/statusline.cjs');process.argv.splice(1,0,s);require(s)"`,
// Remove invalid fields: refreshMs, enabled (not supported by Claude Code)
};
}
// 4. Merge claudeFlow settings (preserve existing, add agentTeams + memory)
const existingClaudeFlow = existing.claudeFlow || {};
const existingMemory = existingClaudeFlow.memory || {};
merged.claudeFlow = {
...existingClaudeFlow,
version: existingClaudeFlow.version || '3.0.0',
enabled: existingClaudeFlow.enabled !== false,
agentTeams: {
enabled: true,
teammateMode: 'auto',
taskListEnabled: true,
mailboxEnabled: true,
coordination: {
autoAssignOnIdle: true,
trainPatternsOnComplete: true,
notifyLeadOnComplete: true,
sharedMemoryNamespace: 'agent-teams',
},
hooks: {
teammateIdle: { enabled: true, autoAssign: true, checkTaskList: true },
taskCompleted: { enabled: true, trainPatterns: true, notifyLead: true },
},
},
memory: {
...existingMemory,
learningBridge: existingMemory.learningBridge ?? { enabled: true },
memoryGraph: existingMemory.memoryGraph ?? { enabled: true },
agentScopes: existingMemory.agentScopes ?? { enabled: true },
},
};
return merged;
}
/**
* Execute upgrade - updates helpers and creates missing metrics without losing data
* This is safe for existing users who want the latest statusline fixes
* @param targetDir - Target directory
* @param upgradeSettings - If true, merge new settings into existing settings.json
*/
export async function executeUpgrade(targetDir, upgradeSettings = false) {
const result = {
success: true,
updated: [],
created: [],
preserved: [],
errors: [],
settingsUpdated: [],
};
try {
// Ensure required directories exist
const dirs = [
'.claude/helpers',
'.claude-flow/metrics',
'.claude-flow/security',
'.claude-flow/learning',
];
for (const dir of dirs) {
const fullPath = path.join(targetDir, dir);
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath, { recursive: true });
}
}
// 0. ALWAYS update critical helpers (force overwrite)
const sourceHelpersForUpgrade = findSourceHelpersDir();
if (sourceHelpersForUpgrade) {
const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs'];
for (const helperName of criticalHelpers) {
const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
const sourcePath = path.join(sourceHelpersForUpgrade, helperName);
if (fs.existsSync(sourcePath)) {
if (fs.existsSync(targetPath)) {
result.updated.push(`.claude/helpers/${helperName}`);
}
else {
result.created.push(`.claude/helpers/${helperName}`);
}
fs.copyFileSync(sourcePath, targetPath);
try {
fs.chmodSync(targetPath, '755');
}
catch { }
}
}
}
else {
// Source not found (npx with broken paths) — use generated fallbacks
const generatedCritical = {
'hook-handler.cjs': generateHookHandler(),
'intelligence.cjs': generateIntelligenceStub(),
'auto-memory-hook.mjs': generateAutoMemoryHook(),
};
for (const [helperName, content] of Object.entries(generatedCritical)) {
const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
if (fs.existsSync(targetPath)) {
result.updated.push(`.claude/helpers/${helperName}`);
}
else {
result.created.push(`.claude/helpers/${helperName}`);
}
fs.writeFileSync(targetPath, content, 'utf-8');
try {
fs.chmodSync(targetPath, '755');
}
catch { }
}
}
// 1. ALWAYS update statusline helper (force overwrite)
const statuslinePath = path.join(targetDir, '.claude', 'helpers', 'statusline.cjs');
// Use default options with statusline config
const upgradeOptions = {
...DEFAULT_INIT_OPTIONS,
targetDir,
force: true,
statusline: {
...DEFAULT_INIT_OPTIONS.statusline,
refreshInterval: 5000,
},
};
const statuslineContent = generateStatuslineScript(upgradeOptions);
if (fs.existsSync(statuslinePath)) {
result.updated.push('.claude/helpers/statusline.cjs');
}
else {
result.created.push('.claude/helpers/statusline.cjs');
}
fs.writeFileSync(statuslinePath, statuslineContent, 'utf-8');
// 2. Create MISSING metrics files only (preserve existing data)
const metricsDir = path.join(targetDir, '.claude-flow', 'metrics');
const securityDir = path.join(targetDir, '.claude-flow', 'security');
// v3-progress.json
const progressPath = path.join(metricsDir, 'v3-progress.json');
if (!fs.existsSync(progressPath)) {
const progress = {
version: '3.0.0',
initialized: new Date().toISOString(),
domains: { completed: 0, total: 5, status: 'INITIALIZING' },
ddd: { progress: 0, modules: 0, totalFiles: 0, totalLines: 0 },
swarm: { activeAgents: 0, maxAgents: 15, topology: 'hierarchical-mesh' },
learning: { status: 'READY', patternsLearned: 0, sessionsCompleted: 0 },
_note: 'Metrics will update as you use Ruflo'
};
fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
result.created.push('.claude-flow/metrics/v3-progress.json');
}
else {
result.preserved.push('.claude-flow/metrics/v3-progress.json');
}
// swarm-activity.json
const activityPath = path.join(metricsDir, 'swarm-activity.json');
if (!fs.existsSync(activityPath)) {
const activity = {
timestamp: new Date().toISOString(),
processes: { agentic_flow: 0, mcp_server: 0, estimated_agents: 0 },
swarm: { active: false, agent_count: 0, coordination_active: false },
integration: { agentic_flow_active: false, mcp_active: false },
_initialized: true
};
fs.writeFileSync(activityPath, JSON.stringify(activity, null, 2), 'utf-8');
result.created.push('.claude-flow/metrics/swarm-activity.json');
}
else {
result.preserved.push('.claude-flow/metrics/swarm-activity.json');
}
// learning.json
const learningPath = path.join(metricsDir, 'learning.json');
if (!fs.existsSync(learningPath)) {
const learning = {
initialized: new Date().toISOString(),
routing: { accuracy: 0, decisions: 0 },
patterns: { shortTerm: 0, longTerm: 0, quality: 0 },
sessions: { total: 0, current: null },
_note: 'Intelligence grows as you use Ruflo'
};
fs.writeFileSync(learningPath, JSON.stringify(learning, null, 2), 'utf-8');
result.created.push('.claude-flow/metrics/learning.json');
}
else {
result.preserved.push('.claude-flow/metrics/learning.json');
}
// audit-status.json
const auditPath = path.join(securityDir, 'audit-status.json');
if (!fs.existsSync(auditPath)) {
const audit = {
initialized: new Date().toISOString(),
status: 'PENDING',
cvesFixed: 0,
totalCves: 3,
lastScan: null,
_note: 'Run: npx @claude-flow/cli@latest security scan'
};
fs.writeFileSync(auditPath, JSON.stringify(audit, null, 2), 'utf-8');
result.created.push('.claude-flow/security/audit-status.json');
}
else {
result.preserved.push('.claude-flow/security/audit-status.json');
}
// 3. Merge settings if requested
if (upgradeSettings) {
const settingsPath = path.join(targetDir, '.claude', 'settings.json');
if (fs.existsSync(settingsPath)) {
try {
const existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
const mergedSettings = mergeSettingsForUpgrade(existingSettings);
fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf-8');
result.updated.push('.claude/settings.json');
result.settingsUpdated = [
'env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
'hooks.SessionStart (auto-memory import)',
'hooks.SessionEnd (auto-memory sync)',
'hooks.TeammateIdle (removed — not a valid Claude Code hook)',
'hooks.TaskCompleted (removed — not a valid Claude Code hook)',
'claudeFlow.agentTeams',
'claudeFlow.memory (learningBridge, memoryGraph, agentScopes)',
];
}
catch (settingsError) {
result.errors.push(`Settings merge failed: ${settingsError instanceof Error ? settingsError.message : String(settingsError)}`);
}
}
else {
// Create new settings.json with defaults
const defaultSettings = generateSettings(DEFAULT_INIT_OPTIONS);
fs.writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2), 'utf-8');
result.created.push('.claude/settings.json');
result.settingsUpdated = ['Created new settings.json with Agent Teams'];
}
}
}
catch (error) {
result.success = false;
result.errors.push(error instanceof Error ? error.message : String(error));
}
return result;
}
/**
* Execute upgrade with --add-missing flag
* Adds any new skills, agents, and commands that don't exist yet
* @param targetDir - Target directory
* @param upgradeSettings - If true, merge new settings into existing settings.json
*/
export async function executeUpgradeWithMissing(targetDir, upgradeSettings = false) {
// First do the normal upgrade (pass through upgradeSettings)
const result = await executeUpgrade(targetDir, upgradeSettings);
if (!result.success) {
return result;
}
// Initialize tracking arrays
result.addedSkills = [];
result.addedAgents = [];
result.addedCommands = [];
try {
// Ensure target directories exist
const skillsDir = path.join(targetDir, '.claude', 'skills');
const agentsDir = path.join(targetDir, '.claude', 'agents');
const commandsDir = path.join(targetDir, '.claude', 'commands');
for (const dir of [skillsDir, agentsDir, commandsDir]) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
// Find source directories
const sourceSkillsDir = findSourceDir('skills');
const sourceAgentsDir = findSourceDir('agents');
const sourceCommandsDir = findSourceDir('commands');
// Debug: Log source directories found
if (process.env.DEBUG || process.env.CLAUDE_FLOW_DEBUG) {
console.log('[DEBUG] Source directories:');
console.log(` Skills: ${sourceSkillsDir || 'NOT FOUND'}`);
console.log(` Agents: ${sourceAgentsDir || 'NOT FOUND'}`);
console.log(` Commands: ${sourceCommandsDir || 'NOT FOUND'}`);
}
// Add missing skills
if (sourceSkillsDir) {
const allSkills = Object.values(SKILLS_MAP).flat();
const debugMode = process.env.DEBUG || process.env.CLAUDE_FLOW_DEBUG;
if (debugMode) {
console.log(`[DEBUG] Checking ${allSkills.length} skills from SKILLS_MAP`);
}
for (const skillName of [...new Set(allSkills)]) {
const sourcePath = path.join(sourceSkillsDir, skillName);
const targetPath = path.join(skillsDir, skillName);
const sourceExists = fs.existsSync(sourcePath);
const targetExists = fs.existsSync(targetPath);
if (debugMode) {
console.log(`[DEBUG] Skill '${skillName}': source=${sourceExists}, target=${targetExists}`);
}
if (sourceExists && !targetExists) {
copyDirRecursive(sourcePath, targetPath);
result.addedSkills.push(skillName);
result.created.push(`.claude/skills/${skillName}`);
}
}
}
// Add missing agents
if (sourceAgentsDir) {
const allAgents = Object.values(AGENTS_MAP).flat();
for (const agentCategory of [...new Set(allAgents)]) {
const sourcePath = path.join(sourceAgentsDir, agentCategory);
const targetPath = path.join(agentsDir, agentCategory);
if (fs.existsSync(sourcePath) && !fs.existsSync(targetPath)) {
copyDirRecursive(sourcePath, targetPath);
result.addedAgents.push(agentCategory);
result.created.push(`.claude/agents/${agentCategory}`);
}
}
}
// Add missing commands
if (sourceCommandsDir) {
const allCommands = Object.values(COMMANDS_MAP).flat();
for (const cmdName of [...new Set(allCommands)]) {
const sourcePath = path.join(sourceCommandsDir, cmdName);
const targetPath = path.join(commandsDir, cmdName);
if (fs.existsSync(sourcePath) && !fs.existsSync(targetPath)) {
if (fs.statSync(sourcePath).isDirectory()) {
copyDirRecursive(sourcePath, targetPath);
}
else {
fs.copyFileSync(sourcePath, targetPath);
}
result.addedCommands.push(cmdName);
result.created.push(`.claude/commands/${cmdName}`);
}
}
}
}
catch (error) {
result.errors.push(`Add missing failed: ${error instanceof Error ? error.message : String(error)}`);
}
return result;
}
/**
* Create directory structure
*/
async function createDirectories(targetDir, options, result) {
const dirs = [
...DIRECTORIES.claude,
...(options.components.runtime ? DIRECTORIES.runtime : []),
];
for (const dir of dirs) {
const fullPath = path.join(targetDir, dir);
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath, { recursive: true });
result.created.directories.push(dir);
}
}
}
/**
* Write settings.json
*/
async function writeSettings(targetDir, options, result) {
const settingsPath = path.join(targetDir, '.claude', 'settings.json');
const generated = JSON.parse(generateSettingsJson(options));
if (fs.existsSync(settingsPath) && !options.force) {
// Merge hooks/env/permissions into existing settings instead of skipping
try {
const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
let merged = false;
// Merge hooks (the critical missing piece — #1484)
if (generated.hooks && !existing.hooks) {
existing.hooks = generated.hooks;
merged = true;
}
// Merge env vars (for CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS etc.)
if (generated.env) {
existing.env = { ...(existing.env || {}), ...generated.env };
merged = true;
}
// Merge permissions (add ruflo allow rules)
if (generated.permissions?.allow) {
const existingAllow = existing.permissions?.allow || [];
const newRules = generated.permissions.allow.filter((r) => !existingAllow.includes(r));
if (newRules.length > 0) {
existing.permissions = existing.permissions || {};
existing.permissions.allow = [...existingAllow, ...newRules];
merged = true;
}
}
if (merged) {
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2), 'utf-8');
result.created.files.push('.claude/settings.json (merged hooks)');
}
else {
result.skipped.push('.claude/settings.json');
}
}
catch {
// Existing file is corrupt — overwrite
fs.writeFileSync(settingsPath, JSON.stringify(generated, null, 2), 'utf-8');
result.created.files.push('.claude/settings.json');
}
return;
}
fs.writeFileSync(settingsPath, JSON.stringify(generated, null, 2), 'utf-8');
result.created.files.push('.claude/settings.json');
}
/**
* #1779 — Walk parents of `targetDir` plus the user-global Claude Code
* config locations, looking for any `.mcp.json` (or `~/.claude.json`)
* that already declares a `ruflo`-keyed MCP server. We use this to skip
* writing our own `claude-flow`-keyed entry when the user has already
* registered the same binary under the new name — that's exactly the
* "same MCP server twice under two different prefixes" duplication the
* issue describes.
*
* Returns the path of the file that already declares `ruflo` (so we can
* surface it in the skipped-message), or null if none found.
*/
function detectExistingRufloMCP(targetDir) {
const home = (process.env.HOME ?? process.env.USERPROFILE) ?? '';
const candidates = new Set();
// User-global Claude Code config locations
if (home) {
candidates.add(path.join(home, '.claude.json'));
candidates.add(path.join(home, '.claude', 'mcp.json'));
}
// Walk parents of targetDir up to root, checking for .mcp.json at each
const targetResolved = path.resolve(targetDir);
let dir = targetResolved;
const targetAncestors = new Set();
while (true) {
candidates.add(path.join(dir, '.mcp.json'));
targetAncestors.add(normalizeProjectKey(dir));
const parent = path.dirname(dir);
if (parent === dir)
break;
dir = parent;
}
// Skip the targetDir itself — that's the one we're about to write
candidates.delete(path.join(targetResolved, '.mcp.json'));
for (const candidate of candidates) {
if (!fs.existsSync(candidate))
continue;
try {
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
if (!parsed || typeof parsed !== 'object')
continue;
// (a) Top-level mcpServers (legacy / global form).
// #2207: accept BOTH the old 'ruflo' key AND the new 'claude-flow' key so that
// a prior install with either key is correctly detected as already-initialized.
// This also avoids the reverse problem: after #2206 fixed the generator to write
// 'claude-flow', a second `ruflo init` must still recognise the existing install.
if (parsed.mcpServers && typeof parsed.mcpServers === 'object') {
const servers = parsed.mcpServers;
if ('claude-flow' in servers || 'ruflo' in servers)
return candidate;
}
// (b) #1840: Claude Code project-scoped registrations under
// parsed.projects[<projectPath>].mcpServers. Match by
// normalized path against targetDir or any of its ancestors so
// a `claude mcp add claude-flow` (or legacy `ruflo`) in this repo is
// detected even when Claude stored the key with different casing/slash style.
// #2207: accept both keys here too.
if (parsed.projects && typeof parsed.projects === 'object') {
for (const [projectKey, projectVal] of Object.entries(parsed.projects)) {
if (!projectVal || typeof projectVal !== 'object')
continue;
const projectMcp = projectVal.mcpServers;
if (!projectMcp || typeof projectMcp !== 'object')
continue;
const mcp = projectMcp;
if (!('claude-flow' in mcp) && !('ruflo' in mcp))
continue;
if (targetAncestors.has(normalizeProjectKey(projectKey))) {
return `${candidate} (projects[${projectKey}])`;
}
}
}
}
catch { /* malformed JSON — ignore */ }
}
return null;
}
/**
* Normalize a project path key for cross-platform comparison.
* Claude Code stores Windows paths like "C:/Users/.../Project" while
* Node's `path.resolve()` may emit "C:\Users\...\Project". Lowercase +
* forward-slash gives a stable comparison key on both platforms.
*/
function normalizeProjectKey(p) {
return path.resolve(p).replace(/\\/g, '/').toLowerCase();
}
/**
* Write .mcp.json
*/
async function writeMCPConfig(targetDir, options, result) {
const mcpPath = path.join(targetDir, '.mcp.json');
if (fs.existsSync(mcpPath) && !options.force) {
result.skipped.push('.mcp.json');
return;
}
// #1779 — Skip writing if the user already has a `ruflo`-keyed MCP
// server registered elsewhere (parent .mcp.json, ~/.claude.json, etc).
// Writing our `claude-flow`-keyed entry on top of that produces the
// duplicate-registration the issue describes (~250 duplicate tools).
// Force-mode (`--force`) bypasses this guard for users who actually
// want both registrations.
if (!options.force) {
const existingRufloPath = detectExistingRufloMCP(targetDir);
if (existingRufloPath) {
result.skipped.push(`.mcp.json (existing 'ruflo' MCP registration found at ${existingRufloPath} — would create duplicate; pass --force to write anyway)`);
return;
}
}
const content = generateMCPJson(options);
fs.writeFileSync(mcpPath, content, 'utf-8');
result.created.files.push('.mcp.json');
}
/**
* Copy skills from source
*/
async function copySkills(targetDir, options, result) {
const skillsConfig = options.skills;
const targetSkillsDir = path.join(targetDir, '.claude', 'skills');
// Determine which skills to copy
const skillsToCopy = [];
if (skillsConfig.all) {
// Copy all available skills
Object.values(SKILLS_MAP).forEach(skills => skillsToCopy.push(...skills));
}
else {
if (skillsConfig.core)
skillsToCopy.push(...SKILLS_MAP.core);
if (skillsConfig.agentdb)
skillsToCopy.push(...SKILLS_MAP.agentdb);
if (skillsConfig.github)
skillsToCopy.push(...SKILLS_MAP.github);
if (skillsConfig.flowNexus)
skillsToCopy.push(...SKILLS_MAP.flowNexus);
if (skillsConfig.browser)
skillsToCopy.push(...SKILLS_MAP.browser);
if (skillsConfig.v3)
skillsToCopy.push(...SKILLS_MAP.v3);
if (skillsConfig.dualMode)
skillsToCopy.push(...SKILLS_MAP.dualMode);
}
// Find source skills directory
const sourceSkillsDir = findSourceDir('skills', options.sourceBaseDir);
if (!sourceSkillsDir) {
result.errors.push('Could not find source skills directory');
return;
}
// Copy each skill
for (const skillName of [...new Set(skillsToCopy)]) {
const sourcePath = path.join(sourceSkillsDir, skillName);
const targetPath = path.join(targetSkillsDir, skillName);
if (fs.existsSync(sourcePath)) {
if (!fs.existsSync(targetPath) || options.force) {
copyDirRecursive(sourcePath, targetPath);
result.created.files.push(`.claude/skills/${skillName}`);
result.summary.skillsCount++;
}
else {
result.skipped.push(`.claude/skills/${skillName}`);
}
}
}
}
/**
* Copy commands from source
*/
async function copyCommands(targetDir, options, result) {
const commandsConfig = options.commands;
const targetCommandsDir = path.join(targetDir, '.claude', 'commands');
// Determine which commands to copy
const commandsToCopy = [];
if (commandsConfig.all) {
Object.values(COMMANDS_MAP).forEach(cmds => commandsToCopy.push(...cmds));
}
else {
if (commandsConfig.core)
commandsToCopy.push(...COMMANDS_MAP.core);
if (commandsConfig.analysis)
commandsToCopy.push(...COMMANDS_MAP.analysis);
if (commandsConfig.automation)
commandsToCopy.push(...COMMANDS_MAP.automation);
if (commandsConfig.github)
commandsToCopy.push(...COMMANDS_MAP.github);
if (commandsConfig.hooks)
commandsToCopy.push(...COMMANDS_MAP.hooks);
if (commandsConfig.monitoring)
commandsToCopy.push(...COMMANDS_MAP.monitoring);
if (commandsConfig.optimization)
commandsToCopy.push(...COMMANDS_MAP.optimization);
if (commandsConfig.sparc)
commandsToCopy.push(...COMMANDS_MAP.sparc);
// ADR-128 Phase 4 substrate promotions
if (commandsConfig.agents)
commandsToCopy.push(...(COMMANDS_MAP.agents || []));
if (commandsConfig.coordination)
commandsToCopy.push(...(COMMANDS_MAP.coordination || []));
if (commandsConfig.hiveMind)
commandsToCopy.push(...(COMMANDS_MAP.hiveMind || []));
if (commandsConfig.memory)
commandsToCopy.push(...(COMMANDS_MAP.memory || []));
if (commandsConfig.swarm)
commandsToCopy.push(...(COMMANDS_MAP.swarm || []));
if (commandsConfig.workflows)
commandsToCopy.push(...(COMMANDS_MAP.workflows || []));
// ADR-128 Phase 4 opt-in categories
if (commandsConfig.pair)
commandsToCopy.push(...(COMMANDS_MAP.pair || []));
if (commandsConfig.training)
commandsToCopy.push(...(COMMANDS_MAP.training || []));
if (commandsConfig.streamChain)
commandsToCopy.push(...(COMMANDS_MAP.streamChain || []));
if (commandsConfig.truth)
commandsToCopy.push(...(COMMANDS_MAP.truth || []));
if (commandsConfig.verify)
commandsToCopy.push(...(COMMANDS_MAP.verify || []));
}
// Find source commands directory
const sourceCommandsDir = findSourceDir('commands', options.sourceBaseDir);
if (!sourceCommandsDir) {
result.errors.push('Could not find source commands directory');
return;
}
// Copy each command/directory
for (const cmdName of [...new Set(commandsToCopy)]) {
const sourcePath = path.join(sourceCommandsDir, cmdName);
const targetPath = path.join(targetCommandsDir, cmdName);
if (fs.existsSync(sourcePath)) {
if (!fs.existsSync(targetPath) || options.force) {
if (fs.statSync(sourcePath).isDirectory()) {
copyDirRecursive(sourcePath, targetPath);
}
else {
fs.copyFileSync(sourcePath, targetPath);
}
result.created.files.push(`.claude/commands/${cmdName}`);
result.summary.commandsCount++;
}
else {
result.skipped.push(`.claude/commands/${cmdName}`);
}
}
}
}
/**
* Copy agents from source
*/
async function copyAgents(targetDir, options, result) {
const agentsConfig = options.agents;
const targetAgentsDir = path.join(targetDir, '.claude', 'agents');
// Determine which agents to copy
const agentsToCopy = [];
if (agentsConfig.all) {
Object.values(AGENTS_MAP).forEach(agents => agentsToCopy.push(...agents));
}
else {
if (agentsConfig.core)
agentsToCopy.push(...AGENTS_MAP.core);
if (agentsConfig.consensus)
agentsToCopy.push(...AGENTS_MAP.consensus);
if (agentsConfig.github)
agentsToCopy.push(...AGENTS_MAP.github);
if (agentsConfig.hiveMind)
agentsToCopy.push(...AGENTS_MAP.hiveMind);
if (agentsConfig.sparc)
agentsToCopy.push(...AGENTS_MAP.sparc);
if (agentsConfig.swarm)
agentsToCopy.push(...AGENTS_MAP.swarm);
if (agentsConfig.browser)
agentsToCopy.push(...AGENTS_MAP.browser);
// V3-specific agent categories
if (agentsConfig.v3)
agentsToCopy.push(...(AGENTS_MAP.v3 || []));
if (agentsConfig.optimization)
agentsToCopy.push(...(AGENTS_MAP.optimization || []));
if (agentsConfig.testing)
agentsToCopy.push(...(AGENTS_MAP.testing || []));
// Dual-mode agents (Claude Code + Codex hybrid)
if (agentsConfig.dualMode)
agentsToCopy.push(...(AGENTS_MAP.dualMode || []));
}
// Find source agents directory
const sourceAgentsDir = findSourceDir('agents', options.sourceBaseDir);
if (!sourceAgentsDir) {
result.errors.push('Could not find source agents directory');
return;
}
// Copy each agent category
for (const agentCategory of [...new Set(agentsToCopy)]) {
const sourcePath = path.join(sourceAgentsDir, agentCategory);
const targetPath = path.join(targetAgentsDir, agentCategory);
if (fs.existsSync(sourcePath)) {
if (!fs.existsSync(targetPath) || options.force) {
copyDirRecursive(sourcePath, targetPath);
// Count agent files (.md only — .yaml agents were migrated to .md)
const mdFiles = countFiles(sourcePath, '.md');
result.summary.agentsCount += mdFiles;
result.created.files.push(`.claude/agents/${agentCategory}`);
}
else {
result.skipped.push(`.claude/agents/${agentCategory}`);
}
}
}
}
/**
* Find source helpers directory.
* Validates that the directory contains hook-handler.cjs to avoid
* returning the target directory or an incomplete source.
*/
function findSourceHelpersDir(sourceBaseDir) {
const possiblePaths = [];
const SENTINEL_FILE = 'hook-handler.cjs'; // Must exist in valid source
// If explicit source base directory is provided, check it first
if (sourceBaseDir) {
possiblePaths.push(path.join(sourceBaseDir, '.claude', 'helpers'));
}
// Strategy 1: require.resolve to find package root (most reliable for npx)
try {
const esmRequire = createRequire(import.meta.url);
const pkgJsonPath = esmRequire.resolve('@claude-flow/cli/package.json');
const pkgRoot = path.dirname(pkgJsonPath);
possiblePaths.push(path.join(pkgRoot, '.claude', 'helpers'));
}
catch {
// Not installed as a package — skip
}
// Strategy 2: __dirname-based (dist/src/init -> package root)
const packageRoot = path.resolve(__dirname, '..', '..', '..');
const packageHelpers = path.join(packageRoot, '.claude', 'helpers');
possiblePaths.push(packageHelpers);
// Strategy 3: Walk up from __dirname looking for package root
let currentDir = __dirname;
for (let i = 0; i < 10; i++) {
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir)
break; // hit filesystem root
const helpersPath = path.join(parentDir, '.claude', 'helpers');
possiblePaths.push(helpersPath);
currentDir = parentDir;
}
// Strategy 4: Check cwd-relative paths (for local dev)
const cwdBased = [
path.join(process.cwd(), '.claude', 'helpers'),
path.join(process.cwd(), '..', '.claude', 'helpers'),
path.join(process.cwd(), '..', '..', '.claude', 'helpers'),
];
possiblePaths.push(...cwdBased);
// Return first path that exists AND contains the sentinel file
for (const p of possiblePaths) {
if (fs.existsSync(p) && fs.existsSync(path.join(p, SENTINEL_FILE))) {
return p;
}
}
return null;
}
/**
* Write helper scripts
*/
async function writeHelpers(targetDir, options, result) {
const helpersDir = path.join(targetDir, '.claude', 'helpers');
// Find source helpers directory (works for npm package and local dev)
const sourceHelpersDir = findSourceHelpersDir(options.sourceBaseDir);
// On Windows: emit a notice before writing helpers — the settings.json
// hooks will use node-based commands instead of bash shims (#2132).
if (process.platform === 'win32') {
console.log('Detected Windows — adding cross-platform hook overrides to .claude/settings.json (#2132)');
}
// Try to copy existing helpers from source first
if (sourceHelpersDir && fs.existsSync(sourceHelpersDir)) {
const helperFiles = fs.readdirSync(sourceHelpersDir);
let copiedCount = 0;
for (const file of helperFiles) {
const sourcePath = path.join(sourceHelpersDir, file);
const destPath = path.join(helpersDir, file);
// Skip directories and only copy files
if (!fs.statSync(sourcePath).isFile())
continue;
if (!fs.existsSync(destPath) || options.force) {
fs.copyFileSync(sourcePath, destPath);
// Make shell scripts and mjs files executable
if (file.endsWith('.sh') || file.endsWith('.mjs')) {
fs.chmodSync(destPath, '755');
}
result.created.files.push(`.claude/helpers/${file}`);
copiedCount++;
}
else {
result.skipped.push(`.claude/helpers/${file}`);
}
}
// #2132: Always generate ruflo-hook.cjs regardless of source copy path.
// The source helpers dir may not contain this file (it lives in
// plugins/ruflo-core/scripts/, not .claude/helpers/), but it must
// always be present so Windows users can use the node-based shim.
const rufloHookDest = path.join(helpersDir, 'ruflo-hook.cjs');
if (!fs.existsSync(rufloHookDest) || options.force) {
fs.writeFileSync(rufloHookDest, generateRufloHookCjs(), 'utf-8');
result.created.files.push('.claude/helpers/ruflo-hook.cjs');
}
else {
result.skipped.push('.claude/helpers/ruflo-hook.cjs');
}
if (copiedCount > 0) {
return; // Skip generating if we copied from source
}
}
// Fall back to generating helpers if source not available
const helpers = {
'pre-commit': generatePreCommitHook(),
'post-commit': generatePostCommitHook(),
'session.js': generateSessionManager(),
'router.js': generateAgentRouter(),
'memory.js': generateMemoryHelper(),
'hook-handler.cjs': generateHookHandler(),
'intelligence.cjs': generateIntelligenceStub(),
'auto-memory-hook.mjs': generateAutoMemoryHook(),
// #2132: cross-platform Node.js port of ruflo-hook.sh — always deployed so
// Windows users have a working shim even if the plugin's hooks.json bash
// commands are overridden via settings.json.
'ruflo-hook.cjs': generateRufloHookCjs(),
};
for (const [name, content] of Object.entries(helpers)) {
const filePath = path.join(helpersDir, name);
if (!fs.existsSync(filePath) || options.force) {
fs.writeFileSync(filePath, content, 'utf-8');
// Make shell scripts executable
if (!name.endsWith('.js')) {
fs.chmodSync(filePath, '755');
}
result.created.files.push(`.claude/helpers/${name}`);
}
else {
result.skipped.push(`.claude/helpers/${name}`);
}
}
}
/**
* Find source .claude directory for statusline files
*/
function findSourceClaudeDir(sourceBaseDir) {
const possiblePaths = [];
// If explicit source base directory is provided, check it first
if (sourceBaseDir) {
possiblePaths.push(path.join(sourceBaseDir, '.claude'));
}
// IMPORTANT: Check the package's own .claude directory
// Go up 3 levels: dist/src/init -> dist/src -> dist -> root
const packageRoot = path.resolve(__dirname, '..', '..', '..');
const packageClaude = path.join(packageRoot, '.claude');
if (fs.existsSync(packageClaude)) {
possiblePaths.unshift(packageClaude); // Add to beginning (highest priority)
}
// From dist/src/init -> go up to project root
let currentDir = __dirname;
for (let i = 0; i < 10; i++) {
const parentDir = path.dirname(currentDir);
const claudePath = path.join(parentDir, '.claude');
if (fs.existsSync(claudePath)) {
possiblePaths.push(claudePath);
}
currentDir = parentDir;
}
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
return p;
}
}
return null;
}
/**
* Write statusline configuration
*/
async function writeStatusline(targetDir, options, result) {
const claudeDir = path.join(targetDir, '.claude');
const helpersDir = path.join(targetDir, '.claude', 'helpers');
// Find source .claude directory (works for npm package and local dev)
const sourceClaudeDir = findSourceClaudeDir(options.sourceBaseDir