UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

348 lines (346 loc) 16.7 kB
/** * Session Command Handler * * Starts an agentic session for a configured provider with full pre-flight: * 1. Version check — updates aiwg if stale * 2. Doctor — auto-repairs fixable issues * 3. Deployment check — redeploys framework files if missing/stale * 4. MCP inject (optional, `aiwg session mcp`) * 5. Launch binary (spawnable providers) or print start instructions (IDE providers) * * Usage: * aiwg session # default provider, full pre-flight + launch * aiwg session mcp # inject configured MCPs first, then launch * aiwg session --provider codex # explicit provider * aiwg session mcp --provider cursor # MCP inject for cursor + start instructions * aiwg session --no-repair # skip auto-repair (still check and report) * * @issue #884 */ import { spawnSync } from 'child_process'; import { ensureRuntimeHome, writeProfileConfig, launchWithProfile } from '../../mcp/adapters/codex-runtime.js'; import { getFrameworkRoot } from '../../channel/manager.mjs'; import { forceUpdateCheck } from '../../update/checker.mjs'; import { readAiwgConfig, getDeploymentSummary, VALID_PROVIDERS, } from '../../config/aiwg-config.js'; import { getProviderConfig, isSpawnableProvider, PROVIDER_CONFIGS, } from '../agent-spawn.js'; import { useHandler as useFrameworkHandler } from './use.js'; import { debug } from '../log.js'; // ── Provider resolution ────────────────────────────────────────────────────── /** * Resolve which provider to target. * Precedence: --provider flag > project config providers[0] > 'claude' */ async function resolveProvider(explicitProvider, cwd) { if (explicitProvider) { if (!VALID_PROVIDERS.includes(explicitProvider)) { console.warn(` WARN Unknown provider '${explicitProvider}' — falling back to 'claude'`); return 'claude'; } return explicitProvider; } const config = await readAiwgConfig(cwd); if (config?.providers?.[0]) { return config.providers[0]; } return 'claude'; } function parseSessionArgs(args) { let mcp = false; let provider; let noRepair = false; let profile; let persist = false; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === 'mcp') { mcp = true; } else if (a === '--provider' && args[i + 1]) { provider = args[++i]; } else if (a === '--no-repair') { noRepair = true; } else if (a === '--profile' && args[i + 1]) { profile = args[++i]; } else if (a === '--persist') { persist = true; } } return { mcp, provider, noRepair, profile, persist }; } // ── Pre-flight steps ───────────────────────────────────────────────────────── /** Run `npm view aiwg version` and compare to current. Returns true if up-to-date. */ async function checkAndUpdateVersion(noRepair) { console.log(' Checking aiwg version...'); try { await forceUpdateCheck(); return true; } catch (err) { // forceUpdateCheck handles its own user-visible output — the underlying // error is only relevant for troubleshooting, gated behind AIWG_DEBUG. debug('cli:session:update', 'forceUpdateCheck failed', err); if (!noRepair) { console.log(' Version check failed — attempting sync...'); const r = spawnSync('npm', ['install', '-g', 'aiwg@latest'], { stdio: 'inherit' }); if (r.status !== 0) { console.warn(' WARN Could not update aiwg — continuing with current version.'); return false; } } return true; } } /** * Run `aiwg doctor` and return whether it passed. * Uses the same script runner path as doctorHandler to avoid duplication. */ function runDoctor(_frameworkRoot, cwd) { const result = spawnSync(process.execPath, [process.argv[1], 'doctor'], { stdio: 'inherit', cwd }); return (result.status ?? 1) === 0; } /** * Attempt to repair a failed doctor result. * Strategy: `aiwg sync` first; if that fails, offer full reinstall. * Returns true if repair succeeded (doctor now passes). */ function repairInstallation(frameworkRoot, cwd, provider, installedFrameworks) { // Strategy 1: sync (update + redeploy) console.log('\n Attempting auto-repair via `aiwg sync`...'); const syncResult = spawnSync(process.execPath, [process.argv[1], 'sync'], { stdio: 'inherit', cwd }); if (syncResult.status === 0) { // Re-check doctor const doctorOk = runDoctor(frameworkRoot, cwd); if (doctorOk) { console.log(' OK Auto-repair succeeded.'); return true; } } // Strategy 2: full reinstall console.log('\n Sync did not fully resolve the issue. Attempting full reinstall...'); const reinstallResult = spawnSync('npm', ['install', '-g', 'aiwg@latest'], { stdio: 'inherit' }); if (reinstallResult.status === 0 && installedFrameworks.length > 0) { // Redeploy all installed frameworks for this provider console.log(`\n Redeploying frameworks to ${provider}...`); for (const fw of installedFrameworks) { spawnSync(process.execPath, [process.argv[1], 'use', fw, '--provider', provider], { stdio: 'inherit', cwd }); } // Final doctor check const finalOk = runDoctor(frameworkRoot, cwd); if (finalOk) { console.log(' OK Full reinstall + redeploy succeeded.'); return true; } } // Could not auto-repair console.log(` ✗ Auto-repair could not resolve all issues. Manual options: aiwg sync — sync and redeploy npm install -g aiwg@latest — reinstall package aiwg use all --provider ${provider.padEnd(10)} — redeploy all frameworks Report this issue: aiwg feedback --type bug `); return false; } /** * Check whether framework files are deployed for the given provider. * Returns list of framework names that need (re)deployment. */ async function checkDeployment(cwd, provider) { const config = await readAiwgConfig(cwd); if (!config || Object.keys(config.installed).length === 0) { return []; // no frameworks configured — nothing to check } const summary = getDeploymentSummary(config, provider); const total = summary.agents + summary.commands + summary.skills + summary.rules; if (total === 0) { // Frameworks are registered but not deployed to this provider return Object.keys(config.installed); } return []; // deployed OK } /** * Redeploy all given frameworks to the provider. */ async function redeployFrameworks(ctx, frameworks, provider) { console.log(`\n Redeploying ${frameworks.join(', ')} to ${provider}...`); for (const fw of frameworks) { await useFrameworkHandler.execute({ ...ctx, args: [fw, '--provider', provider], }); } } // ── MCP inject ──────────────────────────────────────────────────────────────── function injectMcp(provider, cwd) { console.log(`\n Injecting configured MCP servers into ${provider}...`); const result = spawnSync(process.execPath, [process.argv[1], 'mcp', 'inject', '--provider', provider], { stdio: 'inherit', cwd }); if (result.status !== 0) { console.warn(' WARN MCP inject reported issues — continuing. Run `aiwg mcp list` to check registered servers.'); return false; } return true; } /** * Profile-aware inject (#891). * Default is ephemeral (no persistent config mutation). * * For Claude: returns the ephemeral config path (for claude --mcp-config). * For Codex: sets up the runtime home via the codex-runtime adapter (#892). * Returns null for persistent mode or on failure. */ function injectMcpProfile(provider, profileName, cwd, persist) { console.log(`\n Resolving profile "${profileName}" for ${provider}...`); if (persist) { // Persistent: write to provider's default config const result = spawnSync(process.execPath, [process.argv[1], 'mcp', 'inject', '--provider', provider, '--profile', profileName], { stdio: 'inherit', cwd }); if (result.status !== 0) { console.warn(' WARN Profile inject failed — continuing without profile.'); } return null; // persistent mode, no ephemeral path } // Codex: runtime-home adapter handles ephemeral config (#892) // The codex launch is also handled specially in launchProvider. if (provider === 'codex' || provider === 'openai') { return null; // signal handled via runtime home (see launchProvider) } // Claude/others: generate a temp config file const tmpPath = `${process.env.TMPDIR ?? '/tmp'}/aiwg-mcp-${profileName}-${provider}-${Date.now()}.json`; const result = spawnSync(process.execPath, [ process.argv[1], 'mcp', 'inject', '--provider', provider, '--profile', profileName, '--ephemeral', '--out', tmpPath, ], { stdio: 'inherit', cwd }); if (result.status !== 0) { console.warn(' WARN Ephemeral profile inject failed — launching without profile MCP config.'); return null; } return tmpPath; } // ── Launch ──────────────────────────────────────────────────────────────────── /** * Launch a spawnable provider binary, replacing the current process (exec-style). * For IDE-integrated providers, print guidance and exit. */ function launchProvider(provider, mcpInjected, mcpConfigPath, profile) { const cfg = getProviderConfig(provider); if (!isSpawnableProvider(provider)) { // Not spawnable — print context-aware guidance const mcpNote = mcpInjected ? '\n MCP servers have been injected into your provider config.\n' : ''; const ephemeralNote = mcpConfigPath ? `\n MCP config (ephemeral): ${mcpConfigPath}\n` : ''; console.log(` ── Ready to start ${cfg.name} ── ${mcpNote}${ephemeralNote} ${cfg.guidanceMessage ?? `Open ${cfg.name} in your project directory to begin.`} `); return { exitCode: 0 }; } // Spawnable — hand off the terminal console.log(`\n── Launching ${cfg.name} ──\n`); // Codex + profile: use runtime-home adapter (#892) if ((provider === 'codex' || provider === 'openai') && profile) { console.log(` Using runtime home for profile "${profile}"`); const result = launchWithProfile(profile); return { exitCode: result.status ?? 0 }; } // Claude: if we have an ephemeral MCP config, pass it via --mcp-config const binaryArgs = []; if (mcpConfigPath && (provider === 'claude' || provider === 'claude-code')) { binaryArgs.push('--mcp-config', mcpConfigPath); console.log(` Using ephemeral MCP config: ${mcpConfigPath}`); } const result = spawnSync(cfg.binary, binaryArgs, { stdio: 'inherit', env: process.env, }); return { exitCode: result.status ?? 0 }; } // ── Handler ─────────────────────────────────────────────────────────────────── export const sessionHandler = { id: 'session', name: 'Session', description: 'Start an agentic session — pre-flight check, auto-repair, optional MCP inject, then launch', category: 'project', aliases: [], async execute(ctx) { const { mcp, provider: explicitProvider, noRepair, profile, persist } = parseSessionArgs(ctx.args); const cwd = ctx.cwd || process.cwd(); // ── Resolve provider ───────────────────────────────────────── const provider = await resolveProvider(explicitProvider, cwd); const providerCfg = PROVIDER_CONFIGS[provider]; const profileSuffix = profile ? ` · profile: ${profile}` : ''; console.log(`\n── aiwg session ── provider: ${providerCfg?.name ?? provider}${profileSuffix} ──\n`); const frameworkRoot = await getFrameworkRoot(); // ── Step 1: Version check ───────────────────────────────────── if (!noRepair) { await checkAndUpdateVersion(noRepair); } // ── Step 2: Doctor ──────────────────────────────────────────── console.log('\n Running health checks...'); const doctorOk = runDoctor(frameworkRoot, cwd); if (!doctorOk && !noRepair) { // Read installed frameworks before attempting repair (needed for redeploy) const config = await readAiwgConfig(cwd); const installedFrameworks = Object.keys(config?.installed ?? {}); const repaired = repairInstallation(frameworkRoot, cwd, provider, installedFrameworks); if (!repaired) { // Repair failed — still continue (user was already informed) } } else if (!doctorOk && noRepair) { console.log('\n WARN Health checks found issues. Run without --no-repair to auto-fix, or `aiwg feedback` to report.'); } // ── Step 3: Deployment check ────────────────────────────────── const undeployed = await checkDeployment(cwd, provider); if (undeployed.length > 0 && !noRepair) { console.log(`\n Frameworks not deployed to ${provider}: ${undeployed.join(', ')}`); await redeployFrameworks(ctx, undeployed, provider); } // ── Step 4: MCP inject ──────────────────────────────────────── let mcpInjected = false; let mcpConfigPath = null; if (profile) { // Profile-aware injection (#891) — ephemeral by default // For codex: set up the runtime home and write profile config (#892) if ((provider === 'codex' || provider === 'openai') && !persist) { try { console.log(`\n Setting up Codex runtime home for profile "${profile}"...`); // Import server list for this profile const { McpProfileRegistry } = await import('../../mcp/profiles.js'); const { McpServerRegistry } = await import('../../mcp/registry.js'); const profiles = new McpProfileRegistry(); const registry = new McpServerRegistry(); const resolvedServers = await profiles.resolveServers(profile, registry); await ensureRuntimeHome(profile); await writeProfileConfig(profile, resolvedServers); console.log(` Runtime home ready. Profile servers: ${resolvedServers.map((s) => s.name).join(', ') || '(none)'}`); mcpInjected = true; } catch (err) { console.warn(` WARN Codex runtime home setup failed: ${err instanceof Error ? err.message : String(err)}`); console.warn(' Falling back to standard launch.'); } } else { mcpConfigPath = injectMcpProfile(provider, profile, cwd, persist); mcpInjected = mcpConfigPath !== null || persist; } } else if (mcp) { mcpInjected = injectMcp(provider, cwd); } // ── Step 5: Launch ──────────────────────────────────────────── return launchProvider(provider, mcpInjected, mcpConfigPath, profile); }, }; //# sourceMappingURL=session.js.map