OpenWriterControle/plugins/claude-code/scripts/install-context.ts
Darren Hinde a8fc9c7715
feat(plugin): enhance context management and skill invocation (#239)
* fix(shellcheck): declare and assign separately in router.sh (SC2155)

* feat(plugin): enhance context management and skill invocation

- Update .context-manifest.json to include profile and detailed source information.
- Revise context-scout.md to clarify context root discovery and navigation.
- Modify commands to replace deprecated skills with updated ones (brainstorming → approach, systematic-debugging → debugger).
- Streamline install-context.md for clarity and remove interactive profile selection.
- Improve oac-status.md to provide a more comprehensive status report.
- Add OAC system paths to session-start.sh for better context management.
- Remove obsolete skills and improve documentation for existing commands.
2026-02-23 12:12:29 +00:00

369 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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, relative, dirname } 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('')
// Write .oac.json at project root so context-scout uses fast path on next session
const projectRoot = dirname(PLUGIN_ROOT)
const oacJsonPath = join(projectRoot, '.oac.json')
const contextRelPath = relative(projectRoot, CONTEXT_DIR).replace(/\\/g, '/')
if (!existsSync(oacJsonPath)) {
const oacConfig = { version: '1', context: { root: contextRelPath } }
writeFileSync(oacJsonPath, JSON.stringify(oacConfig, null, 2))
log.success(`.oac.json created at project root → context.root = "${contextRelPath}"`)
} else {
log.info(`.oac.json already exists at project root — skipping (use --force to overwrite)`)
}
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)
})