| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- #!/usr/bin/env bun
- /**
- * install-context.ts
- * TypeScript context installer for OAC Claude Code Plugin
- *
- * Downloads context files from OpenAgents Control repository using git sparse-checkout
- * Supports profile-based installation (essential, standard, extended, specialized, all)
- */
- import { existsSync, writeFileSync, readFileSync } from 'fs'
- import { join } from 'path'
- import type { InstallOptions, InstallResult, Profile } from './types/registry'
- import type { Manifest, ManifestComponent } from './types/manifest'
- import { fetchRegistry, filterContextByProfile, filterContextByIds, getUniquePaths } from './utils/registry-fetcher'
- import { sparseClone, copyFiles, cleanup, checkGitAvailable } from './utils/git-sparse'
- // Configuration
- const GITHUB_REPO = 'darrenhinde/OpenAgentsControl'
- const GITHUB_BRANCH = 'main'
- const CONTEXT_SOURCE_PATH = '.opencode/context'
- const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || process.cwd()
- const CONTEXT_DIR = join(PLUGIN_ROOT, 'context')
- const MANIFEST_FILE = join(PLUGIN_ROOT, '.context-manifest.json')
- // Colors for output
- const colors = {
- reset: '\x1b[0m',
- red: '\x1b[31m',
- green: '\x1b[32m',
- yellow: '\x1b[33m',
- blue: '\x1b[34m',
- cyan: '\x1b[36m',
- bold: '\x1b[1m',
- }
- // Logging helpers
- const log = {
- info: (msg: string) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
- success: (msg: string) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
- warning: (msg: string) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
- error: (msg: string) => console.error(`${colors.red}✗${colors.reset} ${msg}`),
- header: (msg: string) => console.log(`\n${colors.bold}${colors.cyan}${msg}${colors.reset}\n`),
- }
- /**
- * Main installation function
- */
- export async function installContext(options: InstallOptions = {}): Promise<InstallResult> {
- const {
- profile = 'essential',
- customComponents = [],
- dryRun = false,
- force = false,
- verbose = false,
- } = options
- try {
- // Check dependencies
- if (!checkGitAvailable()) {
- throw new Error('Git is not available. Please install git.')
- }
- // Check if already installed
- if (existsSync(MANIFEST_FILE) && !force) {
- log.warning('Context already installed. Use --force to reinstall.')
- const manifest = JSON.parse(readFileSync(MANIFEST_FILE, 'utf-8')) as Manifest
- return {
- success: true,
- manifest: convertManifestToInstallManifest(manifest),
- }
- }
- log.header('Context Installer')
- log.info(`Profile: ${profile}`)
- log.info(`Repository: ${GITHUB_REPO}`)
- log.info(`Branch: ${GITHUB_BRANCH}`)
- log.info(`Dry run: ${dryRun}`)
- console.log('')
- // Fetch registry
- log.info('Fetching registry from GitHub...')
- const registry = await fetchRegistry({
- source: 'github',
- repository: GITHUB_REPO,
- branch: GITHUB_BRANCH,
- })
- log.success(`Registry version: ${registry.version}`)
- log.success(`Context components available: ${registry.components.contexts?.length || 0}`)
- console.log('')
- // Filter components by profile or custom IDs
- let components
- if (customComponents.length > 0) {
- log.info('Filtering by custom component IDs...')
- components = filterContextByIds(registry, customComponents)
- } else {
- log.info(`Filtering by profile: ${profile}`)
- components = filterContextByProfile(registry, profile)
- }
- log.success(`Selected ${components.length} components`)
-
- if (verbose) {
- console.log('')
- log.info('Components to install:')
- for (const component of components) {
- console.log(` - ${component.id}: ${component.path}`)
- }
- }
- console.log('')
- if (dryRun) {
- log.info('Dry run mode - no files will be downloaded')
- return {
- success: true,
- manifest: {
- version: '1.0.0',
- profile: customComponents.length > 0 ? 'custom' : profile,
- source: {
- repository: GITHUB_REPO,
- branch: GITHUB_BRANCH,
- commit: 'dry-run',
- downloaded_at: new Date().toISOString(),
- },
- context: components.map((c) => ({
- id: c.id,
- name: c.name,
- path: c.path,
- local_path: join(CONTEXT_DIR, c.path.replace(`${CONTEXT_SOURCE_PATH}/`, '')),
- category: c.category,
- })),
- },
- }
- }
- // Get unique directory paths for sparse checkout
- const sparsePaths = getUniquePaths(components)
-
- if (verbose) {
- log.info('Sparse checkout paths:')
- for (const path of sparsePaths) {
- console.log(` - ${path}`)
- }
- console.log('')
- }
- // Download using git sparse-checkout
- log.info('Downloading context files...')
- const tempDir = join(PLUGIN_ROOT, '.tmp-context-download')
-
- const cloneResult = await sparseClone({
- repository: GITHUB_REPO,
- branch: GITHUB_BRANCH,
- paths: sparsePaths,
- targetDir: tempDir,
- verbose,
- })
- if (!cloneResult.success) {
- throw new Error(`Git sparse clone failed: ${cloneResult.error}`)
- }
- log.success('Files downloaded successfully')
- console.log('')
- // Copy files to context directory
- log.info('Copying files to context directory...')
- const sourceContextDir = join(tempDir, CONTEXT_SOURCE_PATH)
- copyFiles(sourceContextDir, CONTEXT_DIR, verbose)
- log.success(`Files copied to: ${CONTEXT_DIR}`)
- console.log('')
- // Create manifest
- log.info('Creating manifest...')
- const manifestComponents: ManifestComponent[] = components.map((c) => ({
- id: c.id,
- name: c.name,
- path: c.path,
- local_path: join(CONTEXT_DIR, c.path.replace(`${CONTEXT_SOURCE_PATH}/`, '')),
- category: c.category,
- }))
- const manifest: Manifest = {
- version: '1.0.0',
- profile: customComponents.length > 0 ? 'custom' : profile,
- source: {
- repository: GITHUB_REPO,
- branch: GITHUB_BRANCH,
- commit: cloneResult.commit,
- downloaded_at: new Date().toISOString(),
- },
- context: manifestComponents,
- }
- writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2))
- log.success(`Manifest created: ${MANIFEST_FILE}`)
- console.log('')
- // Clean up temp directory
- cleanup(tempDir, verbose)
- // Verify installation
- log.info('Verifying installation...')
- let filesExist = 0
- let filesMissing = 0
- for (const component of manifestComponents) {
- if (existsSync(component.local_path)) {
- filesExist++
- if (verbose) {
- log.success(`${component.id}: EXISTS`)
- }
- } else {
- filesMissing++
- log.error(`${component.id}: MISSING`)
- }
- }
- console.log('')
- log.success(`Installation complete!`)
- log.info(`Files verified: ${filesExist}/${manifestComponents.length}`)
-
- if (filesMissing > 0) {
- log.warning(`Missing files: ${filesMissing}`)
- }
- return {
- success: true,
- manifest: convertManifestToInstallManifest(manifest),
- }
- } catch (error) {
- log.error('Installation failed')
- log.error(error instanceof Error ? error.message : String(error))
-
- return {
- success: false,
- manifest: {
- version: '1.0.0',
- profile: 'essential',
- source: {
- repository: GITHUB_REPO,
- branch: GITHUB_BRANCH,
- commit: '',
- downloaded_at: new Date().toISOString(),
- },
- context: [],
- },
- errors: [error instanceof Error ? error.message : String(error)],
- }
- }
- }
- /**
- * Convert Manifest to InstallManifest format
- */
- function convertManifestToInstallManifest(manifest: Manifest): InstallResult['manifest'] {
- return manifest as InstallResult['manifest']
- }
- /**
- * Show usage information
- */
- function showUsage(): void {
- console.log(`
- ${colors.bold}Usage:${colors.reset} bun run install-context.ts [OPTIONS]
- ${colors.bold}OPTIONS:${colors.reset}
- --profile=PROFILE Installation profile (default: essential)
- Options: essential, standard, extended, specialized, all
-
- --component=ID Install specific component by ID (can be used multiple times)
- Example: --component=core-standards --component=openagents-repo
-
- --dry-run Show what would be installed without downloading
- --force Force reinstall even if context exists
- --verbose Show detailed output
- --help Show this help message
- ${colors.bold}PROFILES:${colors.reset}
- essential Minimal components for basic functionality
- standard Standard components for typical use
- extended Extended components for advanced features
- specialized Specialized components for specific domains
- all All available context
- ${colors.bold}EXAMPLES:${colors.reset}
- # Install essential profile (default)
- bun run install-context.ts
- # Install standard profile
- bun run install-context.ts --profile=standard
- # Install specific components
- bun run install-context.ts --component=core-standards --component=openagents-repo
- # Dry run to see what would be installed
- bun run install-context.ts --profile=extended --dry-run
- # Force reinstall
- bun run install-context.ts --force --verbose
- `)
- }
- /**
- * CLI entry point
- */
- async function main(): Promise<void> {
- const args = process.argv.slice(2)
- // Parse arguments
- let profile: Profile = 'essential'
- const customComponents: string[] = []
- let dryRun = false
- let force = false
- let verbose = false
- for (const arg of args) {
- if (arg === '--help' || arg === '-h') {
- showUsage()
- process.exit(0)
- } else if (arg === '--dry-run') {
- dryRun = true
- } else if (arg === '--force') {
- force = true
- } else if (arg === '--verbose' || arg === '-v') {
- verbose = true
- } else if (arg.startsWith('--profile=')) {
- profile = arg.split('=')[1] as Profile
- } else if (arg.startsWith('--component=')) {
- customComponents.push(arg.split('=')[1])
- } else {
- log.error(`Unknown argument: ${arg}`)
- showUsage()
- process.exit(1)
- }
- }
- // Run installation
- const result = await installContext({
- profile,
- customComponents,
- dryRun,
- force,
- verbose,
- })
- if (!result.success) {
- process.exit(1)
- }
- }
- // Run main function
- main().catch((error) => {
- console.error('Fatal error:', error)
- process.exit(1)
- })
|