#!/usr/bin/env node /** * install-context.js * Downloads OAC context files to .claude/context/ in the current project. * * Requirements: node, git (nothing else to install) * * Run from project root: * node install-context.js [--profile=standard] [--force] [--dry-run] */ const { execSync } = require('child_process'); const { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, mkdtempSync } = require('fs'); const { join } = require('path'); const os = require('os'); // Configuration const GITHUB_REPO = 'darrenhinde/OpenAgentsControl'; const GITHUB_BRANCH = 'main'; const CONTEXT_SOURCE_PATH = '.opencode/context'; // Roots resolved after parsing --global flag in main() const PROJECT_ROOT = process.cwd(); const GLOBAL_ROOT = join(os.homedir(), '.claude'); // Installation profiles const PROFILES = { core: { categories: ['core', 'openagents-repo'], }, full: { categories: [ 'core', 'openagents-repo', 'development', 'ui', 'content-creation', 'data', 'product', 'learning', 'project', 'project-intelligence', ], }, }; // Map user-facing profile names → internal profile names const PROFILE_MAP = { 'essential': 'core', 'standard': 'core', 'extended': 'full', 'specialized': 'full', 'all': 'full', 'core': 'core', 'full': 'full', }; // Colors const c = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', }; const log = { info: (msg) => console.log(`${c.blue}ℹ${c.reset} ${msg}`), success: (msg) => console.log(`${c.green}✓${c.reset} ${msg}`), warning: (msg) => console.log(`${c.yellow}⚠${c.reset} ${msg}`), error: (msg) => console.error(`${c.red}✗${c.reset} ${msg}`), }; function checkDependencies() { try { execSync('command -v git', { stdio: 'ignore' }); } catch { log.error('git is required but not found.'); log.info('Install: brew install git (Mac) or sudo apt install git (Linux)'); process.exit(1); } } /** * Download context via git sparse-checkout. * Returns the commit SHA of the downloaded content. */ function downloadContext(categories, contextDir) { const tempDir = mkdtempSync(join(os.tmpdir(), 'oac-context-')); try { log.info(`Downloading from ${GITHUB_REPO}...`); log.info('Cloning repository...'); execSync( `git clone --depth 1 --filter=blob:none --sparse https://github.com/${GITHUB_REPO}.git "${tempDir}"`, { stdio: 'pipe' } ); const commitSha = execSync(`git -C "${tempDir}" rev-parse HEAD`, { encoding: 'utf-8' }).trim(); log.info('Configuring sparse checkout...'); const sparsePaths = [ ...categories.map(cat => `${CONTEXT_SOURCE_PATH}/${cat}`), `${CONTEXT_SOURCE_PATH}/navigation.md`, ]; execSync( `git -C "${tempDir}" sparse-checkout set --skip-checks ${sparsePaths.join(' ')}`, { stdio: 'pipe' } ); log.info('Copying context files...'); mkdirSync(contextDir, { recursive: true }); const sourceDir = join(tempDir, CONTEXT_SOURCE_PATH); if (!existsSync(sourceDir)) { throw new Error('Context directory not found in repository'); } execSync(`cp -r "${sourceDir}/"* "${contextDir}/"`, { stdio: 'pipe' }); const fileCount = execSync(`find "${contextDir}" -type f | wc -l`, { encoding: 'utf-8' }).trim(); log.success(`Downloaded ${fileCount.trim()} files`); return commitSha; } catch (error) { log.error('Failed to download context'); if (error instanceof Error) log.error(error.message); process.exit(1); } finally { rmSync(tempDir, { recursive: true, force: true }); } } function createManifest(profile, categories, commitSha, contextDir, manifestFile) { const files = {}; for (const category of categories) { const categoryPath = join(contextDir, category); if (existsSync(categoryPath)) { files[category] = parseInt( execSync(`find "${categoryPath}" -type f | wc -l`, { encoding: 'utf-8' }).trim(), 10 ); } } const manifest = { version: '1.0.0', profile, source: { repository: GITHUB_REPO, branch: GITHUB_BRANCH, commit: commitSha, downloaded_at: new Date().toISOString(), }, categories, files, }; mkdirSync(join(manifestFile, '..'), { recursive: true }); writeFileSync(manifestFile, JSON.stringify(manifest, null, 2)); log.success(`Manifest created: ${manifestFile}`); } function showUsage() { console.log(` Usage: node install-context.js [OPTIONS] Downloads OAC context files. Requirements: node, git (nothing else to install) Works on Mac, Linux, and Windows. OPTIONS: --profile=NAME Profile: essential, standard, extended, all (default: standard) --global Install to ~/.claude/context/ (all projects share it) --force Re-download even if already installed --dry-run Show what would be installed without downloading --help Show this help PROFILES: essential/standard Core context (core + openagents-repo) extended/all Full context (all categories) SCOPE: default (no flag) Installs to .claude/context/ in the current project --global Installs to ~/.claude/context/ for all projects `); } function main() { const args = process.argv.slice(2); let profileName = 'standard'; let customCategories = []; let isGlobal = false; let force = false; let dryRun = false; for (const arg of args) { if (arg === '--help' || arg === '-h') { showUsage(); process.exit(0); } else if (arg === '--force') { force = true; } else if (arg === '--dry-run') { dryRun = true; } else if (arg === '--global') { isGlobal = true; } else if (arg.startsWith('--profile=')) { profileName = arg.split('=')[1]; if (!PROFILE_MAP[profileName]) { log.error(`Unknown profile: ${profileName}`); log.info(`Valid profiles: ${Object.keys(PROFILE_MAP).join(', ')}`); process.exit(1); } } else if (arg.startsWith('--category=')) { customCategories.push(arg.split('=')[1]); } else if (PROFILE_MAP[arg]) { profileName = arg; } else { log.error(`Unknown argument: ${arg}`); showUsage(); process.exit(1); } } // Resolve install targets based on scope const installRoot = isGlobal ? GLOBAL_ROOT : join(PROJECT_ROOT, '.claude'); const CONTEXT_DIR = join(installRoot, 'context'); const MANIFEST_FILE = join(installRoot, '.context-manifest.json'); const scopeLabel = isGlobal ? 'global (~/.claude/context)' : 'project (.claude/context)'; const categories = customCategories.length > 0 ? (profileName = 'custom', customCategories) : PROFILES[PROFILE_MAP[profileName] || 'core'].categories; // Already installed? if (existsSync(MANIFEST_FILE) && !force) { log.warning(`Context already installed at ${scopeLabel}. Use --force to reinstall.`); try { const manifest = JSON.parse(readFileSync(MANIFEST_FILE, 'utf-8')); log.info(`Profile: ${manifest.profile}, installed: ${manifest.source?.downloaded_at?.slice(0, 10)}`); } catch { /* ignore */ } process.exit(0); } checkDependencies(); console.log(''); log.info(`Scope: ${scopeLabel}`); log.info(`Profile: ${profileName}`); log.info(`Categories: ${categories.join(', ')}`); log.info(`Target: ${CONTEXT_DIR}`); console.log(''); if (dryRun) { log.info('Dry run — no files downloaded'); return; } const commitSha = downloadContext(categories, CONTEXT_DIR); createManifest(profileName, categories, commitSha, CONTEXT_DIR, MANIFEST_FILE); if (isGlobal) { log.info('Global install — no .oac.json needed (discovery chain finds ~/.claude/context automatically)'); } else { const oacJson = join(PROJECT_ROOT, '.oac.json'); if (!existsSync(oacJson)) { writeFileSync(oacJson, JSON.stringify({ version: '1', context: { root: '.claude/context' } }, null, 2)); log.success('.oac.json created at project root'); } else { log.info('.oac.json already exists — skipping'); } } console.log(''); log.success('Context installation complete!'); log.info(`Scope: ${scopeLabel}`); log.info(`Context: ${CONTEXT_DIR}`); log.info(`Manifest: ${MANIFEST_FILE}`); console.log(''); } main();