install-context.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. #!/usr/bin/env bun
  2. /**
  3. * install-context.ts
  4. * TypeScript context installer for OAC Claude Code Plugin
  5. *
  6. * Downloads context files from OpenAgents Control repository using git sparse-checkout
  7. * Supports profile-based installation (essential, standard, extended, specialized, all)
  8. */
  9. import { existsSync, writeFileSync, readFileSync } from 'fs'
  10. import { join } from 'path'
  11. import type { InstallOptions, InstallResult, Profile } from './types/registry'
  12. import type { Manifest, ManifestComponent } from './types/manifest'
  13. import { fetchRegistry, filterContextByProfile, filterContextByIds, getUniquePaths } from './utils/registry-fetcher'
  14. import { sparseClone, copyFiles, cleanup, checkGitAvailable } from './utils/git-sparse'
  15. // Configuration
  16. const GITHUB_REPO = 'darrenhinde/OpenAgentsControl'
  17. const GITHUB_BRANCH = 'main'
  18. const CONTEXT_SOURCE_PATH = '.opencode/context'
  19. const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || process.cwd()
  20. const CONTEXT_DIR = join(PLUGIN_ROOT, 'context')
  21. const MANIFEST_FILE = join(PLUGIN_ROOT, '.context-manifest.json')
  22. // Colors for output
  23. const colors = {
  24. reset: '\x1b[0m',
  25. red: '\x1b[31m',
  26. green: '\x1b[32m',
  27. yellow: '\x1b[33m',
  28. blue: '\x1b[34m',
  29. cyan: '\x1b[36m',
  30. bold: '\x1b[1m',
  31. }
  32. // Logging helpers
  33. const log = {
  34. info: (msg: string) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
  35. success: (msg: string) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
  36. warning: (msg: string) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
  37. error: (msg: string) => console.error(`${colors.red}✗${colors.reset} ${msg}`),
  38. header: (msg: string) => console.log(`\n${colors.bold}${colors.cyan}${msg}${colors.reset}\n`),
  39. }
  40. /**
  41. * Main installation function
  42. */
  43. export async function installContext(options: InstallOptions = {}): Promise<InstallResult> {
  44. const {
  45. profile = 'essential',
  46. customComponents = [],
  47. dryRun = false,
  48. force = false,
  49. verbose = false,
  50. } = options
  51. try {
  52. // Check dependencies
  53. if (!checkGitAvailable()) {
  54. throw new Error('Git is not available. Please install git.')
  55. }
  56. // Check if already installed
  57. if (existsSync(MANIFEST_FILE) && !force) {
  58. log.warning('Context already installed. Use --force to reinstall.')
  59. const manifest = JSON.parse(readFileSync(MANIFEST_FILE, 'utf-8')) as Manifest
  60. return {
  61. success: true,
  62. manifest: convertManifestToInstallManifest(manifest),
  63. }
  64. }
  65. log.header('Context Installer')
  66. log.info(`Profile: ${profile}`)
  67. log.info(`Repository: ${GITHUB_REPO}`)
  68. log.info(`Branch: ${GITHUB_BRANCH}`)
  69. log.info(`Dry run: ${dryRun}`)
  70. console.log('')
  71. // Fetch registry
  72. log.info('Fetching registry from GitHub...')
  73. const registry = await fetchRegistry({
  74. source: 'github',
  75. repository: GITHUB_REPO,
  76. branch: GITHUB_BRANCH,
  77. })
  78. log.success(`Registry version: ${registry.version}`)
  79. log.success(`Context components available: ${registry.components.contexts?.length || 0}`)
  80. console.log('')
  81. // Filter components by profile or custom IDs
  82. let components
  83. if (customComponents.length > 0) {
  84. log.info('Filtering by custom component IDs...')
  85. components = filterContextByIds(registry, customComponents)
  86. } else {
  87. log.info(`Filtering by profile: ${profile}`)
  88. components = filterContextByProfile(registry, profile)
  89. }
  90. log.success(`Selected ${components.length} components`)
  91. if (verbose) {
  92. console.log('')
  93. log.info('Components to install:')
  94. for (const component of components) {
  95. console.log(` - ${component.id}: ${component.path}`)
  96. }
  97. }
  98. console.log('')
  99. if (dryRun) {
  100. log.info('Dry run mode - no files will be downloaded')
  101. return {
  102. success: true,
  103. manifest: {
  104. version: '1.0.0',
  105. profile: customComponents.length > 0 ? 'custom' : profile,
  106. source: {
  107. repository: GITHUB_REPO,
  108. branch: GITHUB_BRANCH,
  109. commit: 'dry-run',
  110. downloaded_at: new Date().toISOString(),
  111. },
  112. context: components.map((c) => ({
  113. id: c.id,
  114. name: c.name,
  115. path: c.path,
  116. local_path: join(CONTEXT_DIR, c.path.replace(`${CONTEXT_SOURCE_PATH}/`, '')),
  117. category: c.category,
  118. })),
  119. },
  120. }
  121. }
  122. // Get unique directory paths for sparse checkout
  123. const sparsePaths = getUniquePaths(components)
  124. if (verbose) {
  125. log.info('Sparse checkout paths:')
  126. for (const path of sparsePaths) {
  127. console.log(` - ${path}`)
  128. }
  129. console.log('')
  130. }
  131. // Download using git sparse-checkout
  132. log.info('Downloading context files...')
  133. const tempDir = join(PLUGIN_ROOT, '.tmp-context-download')
  134. const cloneResult = await sparseClone({
  135. repository: GITHUB_REPO,
  136. branch: GITHUB_BRANCH,
  137. paths: sparsePaths,
  138. targetDir: tempDir,
  139. verbose,
  140. })
  141. if (!cloneResult.success) {
  142. throw new Error(`Git sparse clone failed: ${cloneResult.error}`)
  143. }
  144. log.success('Files downloaded successfully')
  145. console.log('')
  146. // Copy files to context directory
  147. log.info('Copying files to context directory...')
  148. const sourceContextDir = join(tempDir, CONTEXT_SOURCE_PATH)
  149. copyFiles(sourceContextDir, CONTEXT_DIR, verbose)
  150. log.success(`Files copied to: ${CONTEXT_DIR}`)
  151. console.log('')
  152. // Create manifest
  153. log.info('Creating manifest...')
  154. const manifestComponents: ManifestComponent[] = components.map((c) => ({
  155. id: c.id,
  156. name: c.name,
  157. path: c.path,
  158. local_path: join(CONTEXT_DIR, c.path.replace(`${CONTEXT_SOURCE_PATH}/`, '')),
  159. category: c.category,
  160. }))
  161. const manifest: Manifest = {
  162. version: '1.0.0',
  163. profile: customComponents.length > 0 ? 'custom' : profile,
  164. source: {
  165. repository: GITHUB_REPO,
  166. branch: GITHUB_BRANCH,
  167. commit: cloneResult.commit,
  168. downloaded_at: new Date().toISOString(),
  169. },
  170. context: manifestComponents,
  171. }
  172. writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2))
  173. log.success(`Manifest created: ${MANIFEST_FILE}`)
  174. console.log('')
  175. // Clean up temp directory
  176. cleanup(tempDir, verbose)
  177. // Verify installation
  178. log.info('Verifying installation...')
  179. let filesExist = 0
  180. let filesMissing = 0
  181. for (const component of manifestComponents) {
  182. if (existsSync(component.local_path)) {
  183. filesExist++
  184. if (verbose) {
  185. log.success(`${component.id}: EXISTS`)
  186. }
  187. } else {
  188. filesMissing++
  189. log.error(`${component.id}: MISSING`)
  190. }
  191. }
  192. console.log('')
  193. log.success(`Installation complete!`)
  194. log.info(`Files verified: ${filesExist}/${manifestComponents.length}`)
  195. if (filesMissing > 0) {
  196. log.warning(`Missing files: ${filesMissing}`)
  197. }
  198. return {
  199. success: true,
  200. manifest: convertManifestToInstallManifest(manifest),
  201. }
  202. } catch (error) {
  203. log.error('Installation failed')
  204. log.error(error instanceof Error ? error.message : String(error))
  205. return {
  206. success: false,
  207. manifest: {
  208. version: '1.0.0',
  209. profile: 'essential',
  210. source: {
  211. repository: GITHUB_REPO,
  212. branch: GITHUB_BRANCH,
  213. commit: '',
  214. downloaded_at: new Date().toISOString(),
  215. },
  216. context: [],
  217. },
  218. errors: [error instanceof Error ? error.message : String(error)],
  219. }
  220. }
  221. }
  222. /**
  223. * Convert Manifest to InstallManifest format
  224. */
  225. function convertManifestToInstallManifest(manifest: Manifest): InstallResult['manifest'] {
  226. return manifest as InstallResult['manifest']
  227. }
  228. /**
  229. * Show usage information
  230. */
  231. function showUsage(): void {
  232. console.log(`
  233. ${colors.bold}Usage:${colors.reset} bun run install-context.ts [OPTIONS]
  234. ${colors.bold}OPTIONS:${colors.reset}
  235. --profile=PROFILE Installation profile (default: essential)
  236. Options: essential, standard, extended, specialized, all
  237. --component=ID Install specific component by ID (can be used multiple times)
  238. Example: --component=core-standards --component=openagents-repo
  239. --dry-run Show what would be installed without downloading
  240. --force Force reinstall even if context exists
  241. --verbose Show detailed output
  242. --help Show this help message
  243. ${colors.bold}PROFILES:${colors.reset}
  244. essential Minimal components for basic functionality
  245. standard Standard components for typical use
  246. extended Extended components for advanced features
  247. specialized Specialized components for specific domains
  248. all All available context
  249. ${colors.bold}EXAMPLES:${colors.reset}
  250. # Install essential profile (default)
  251. bun run install-context.ts
  252. # Install standard profile
  253. bun run install-context.ts --profile=standard
  254. # Install specific components
  255. bun run install-context.ts --component=core-standards --component=openagents-repo
  256. # Dry run to see what would be installed
  257. bun run install-context.ts --profile=extended --dry-run
  258. # Force reinstall
  259. bun run install-context.ts --force --verbose
  260. `)
  261. }
  262. /**
  263. * CLI entry point
  264. */
  265. async function main(): Promise<void> {
  266. const args = process.argv.slice(2)
  267. // Parse arguments
  268. let profile: Profile = 'essential'
  269. const customComponents: string[] = []
  270. let dryRun = false
  271. let force = false
  272. let verbose = false
  273. for (const arg of args) {
  274. if (arg === '--help' || arg === '-h') {
  275. showUsage()
  276. process.exit(0)
  277. } else if (arg === '--dry-run') {
  278. dryRun = true
  279. } else if (arg === '--force') {
  280. force = true
  281. } else if (arg === '--verbose' || arg === '-v') {
  282. verbose = true
  283. } else if (arg.startsWith('--profile=')) {
  284. profile = arg.split('=')[1] as Profile
  285. } else if (arg.startsWith('--component=')) {
  286. customComponents.push(arg.split('=')[1])
  287. } else {
  288. log.error(`Unknown argument: ${arg}`)
  289. showUsage()
  290. process.exit(1)
  291. }
  292. }
  293. // Run installation
  294. const result = await installContext({
  295. profile,
  296. customComponents,
  297. dryRun,
  298. force,
  299. verbose,
  300. })
  301. if (!result.success) {
  302. process.exit(1)
  303. }
  304. }
  305. // Run main function
  306. main().catch((error) => {
  307. console.error('Fatal error:', error)
  308. process.exit(1)
  309. })