install-context.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. #!/usr/bin/env node
  2. /**
  3. * install-context.js
  4. * Simple context installer for OAC Claude Code Plugin
  5. *
  6. * Downloads context files from OpenAgents Control repository
  7. * Supports profile-based installation (core, full, custom)
  8. */
  9. const { execSync } = require('child_process');
  10. const { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } = require('fs');
  11. const { join } = require('path');
  12. // Configuration
  13. const GITHUB_REPO = 'darrenhinde/OpenAgentsControl';
  14. const GITHUB_BRANCH = 'main';
  15. const CONTEXT_SOURCE_PATH = '.opencode/context';
  16. const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || process.cwd();
  17. const CONTEXT_DIR = join(PLUGIN_ROOT, 'context');
  18. const MANIFEST_FILE = join(PLUGIN_ROOT, '.context-manifest.json');
  19. // Installation profiles
  20. const PROFILES = {
  21. core: {
  22. name: 'Core',
  23. description: 'Essential standards and workflows',
  24. categories: ['core', 'openagents-repo']
  25. },
  26. full: {
  27. name: 'Full',
  28. description: 'All available context',
  29. categories: [
  30. 'core',
  31. 'openagents-repo',
  32. 'development',
  33. 'ui',
  34. 'content-creation',
  35. 'data',
  36. 'product',
  37. 'learning',
  38. 'project',
  39. 'project-intelligence'
  40. ]
  41. }
  42. };
  43. // Colors for output
  44. const colors = {
  45. reset: '\x1b[0m',
  46. red: '\x1b[31m',
  47. green: '\x1b[32m',
  48. yellow: '\x1b[33m',
  49. blue: '\x1b[34m',
  50. };
  51. // Logging helpers
  52. const log = {
  53. info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
  54. success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
  55. warning: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
  56. error: (msg) => console.error(`${colors.red}✗${colors.reset} ${msg}`),
  57. };
  58. /**
  59. * Check if required commands are available
  60. */
  61. function checkDependencies() {
  62. const required = ['git'];
  63. const missing = [];
  64. for (const cmd of required) {
  65. try {
  66. execSync(`command -v ${cmd}`, { stdio: 'ignore' });
  67. } catch {
  68. missing.push(cmd);
  69. }
  70. }
  71. if (missing.length > 0) {
  72. log.error(`Missing required dependencies: ${missing.join(', ')}`);
  73. log.info('Install with: brew install ' + missing.join(' '));
  74. process.exit(1);
  75. }
  76. }
  77. /**
  78. * Download context using git sparse-checkout
  79. */
  80. function downloadContext(categories) {
  81. const tempDir = join(PLUGIN_ROOT, '.tmp-context-download');
  82. try {
  83. log.info(`Downloading context from ${GITHUB_REPO}...`);
  84. log.info(`Categories: ${categories.join(', ')}`);
  85. // Clean up temp directory if it exists
  86. if (existsSync(tempDir)) {
  87. rmSync(tempDir, { recursive: true, force: true });
  88. }
  89. // Clone with sparse checkout (no working tree files)
  90. log.info('Cloning repository...');
  91. execSync(
  92. `git clone --depth 1 --filter=blob:none --sparse https://github.com/${GITHUB_REPO}.git "${tempDir}"`,
  93. { stdio: 'pipe' }
  94. );
  95. // Configure sparse checkout for requested categories
  96. log.info('Configuring sparse checkout...');
  97. const sparseCheckoutPaths = categories.map(cat => `${CONTEXT_SOURCE_PATH}/${cat}`);
  98. // Also include root navigation
  99. sparseCheckoutPaths.push(`${CONTEXT_SOURCE_PATH}/navigation.md`);
  100. execSync(
  101. `cd "${tempDir}" && git sparse-checkout set ${sparseCheckoutPaths.join(' ')}`,
  102. { stdio: 'pipe' }
  103. );
  104. // Create context directory if it doesn't exist
  105. if (!existsSync(CONTEXT_DIR)) {
  106. mkdirSync(CONTEXT_DIR, { recursive: true });
  107. }
  108. // Copy files to context directory
  109. log.info('Copying context files...');
  110. const sourceContextDir = join(tempDir, CONTEXT_SOURCE_PATH);
  111. if (existsSync(sourceContextDir)) {
  112. execSync(`cp -r "${sourceContextDir}"/* "${CONTEXT_DIR}"/`, { stdio: 'pipe' });
  113. log.success('Context files downloaded successfully');
  114. } else {
  115. throw new Error('Context directory not found in repository');
  116. }
  117. // Count downloaded files
  118. const fileCount = execSync(`find "${CONTEXT_DIR}" -type f | wc -l`, { encoding: 'utf-8' }).trim();
  119. log.success(`Downloaded ${fileCount} files`);
  120. // Clean up temp directory
  121. rmSync(tempDir, { recursive: true, force: true });
  122. } catch (error) {
  123. log.error('Failed to download context');
  124. if (error instanceof Error) {
  125. log.error(error.message);
  126. }
  127. // Clean up on error
  128. if (existsSync(tempDir)) {
  129. rmSync(tempDir, { recursive: true, force: true });
  130. }
  131. process.exit(1);
  132. }
  133. }
  134. /**
  135. * Create manifest file tracking installation
  136. */
  137. function createManifest(profile, categories) {
  138. try {
  139. // Get commit SHA
  140. const tempDir = join(PLUGIN_ROOT, '.tmp-manifest-check');
  141. if (existsSync(tempDir)) {
  142. rmSync(tempDir, { recursive: true, force: true });
  143. }
  144. execSync(
  145. `git clone --depth 1 https://github.com/${GITHUB_REPO}.git "${tempDir}"`,
  146. { stdio: 'pipe' }
  147. );
  148. const commitSha = execSync(`cd "${tempDir}" && git rev-parse HEAD`, { encoding: 'utf-8' }).trim();
  149. rmSync(tempDir, { recursive: true, force: true });
  150. // Count files per category
  151. const files = {};
  152. for (const category of categories) {
  153. const categoryPath = join(CONTEXT_DIR, category);
  154. if (existsSync(categoryPath)) {
  155. const count = parseInt(
  156. execSync(`find "${categoryPath}" -type f | wc -l`, { encoding: 'utf-8' }).trim(),
  157. 10
  158. );
  159. files[category] = count;
  160. }
  161. }
  162. // Create manifest
  163. const manifest = {
  164. version: '1.0.0',
  165. profile,
  166. source: {
  167. repository: GITHUB_REPO,
  168. branch: GITHUB_BRANCH,
  169. commit: commitSha,
  170. downloaded_at: new Date().toISOString(),
  171. },
  172. categories,
  173. files,
  174. };
  175. writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
  176. log.success(`Created manifest: ${MANIFEST_FILE}`);
  177. } catch (error) {
  178. log.warning('Failed to create manifest (non-fatal)');
  179. }
  180. }
  181. /**
  182. * Show usage information
  183. */
  184. function showUsage() {
  185. console.log(`
  186. Usage: node install-context.js [PROFILE] [OPTIONS]
  187. PROFILES:
  188. core Download core context only (default)
  189. Categories: core, openagents-repo
  190. full Download all available context
  191. Categories: all domains
  192. custom Specify custom categories
  193. Use with --category flags
  194. OPTIONS:
  195. --category=NAME Add specific category (use with 'custom' profile)
  196. --force Force re-download even if context exists
  197. --help Show this help message
  198. EXAMPLES:
  199. # Install core context (default)
  200. node install-context.js
  201. # Install full context
  202. node install-context.js full
  203. # Install custom categories
  204. node install-context.js custom --category=core --category=development
  205. # Force reinstall
  206. node install-context.js core --force
  207. `);
  208. }
  209. /**
  210. * Main installation function
  211. */
  212. function main() {
  213. const args = process.argv.slice(2);
  214. // Parse arguments
  215. let profile = 'core';
  216. let customCategories = [];
  217. let force = false;
  218. for (const arg of args) {
  219. if (arg === '--help' || arg === '-h') {
  220. showUsage();
  221. process.exit(0);
  222. } else if (arg === '--force') {
  223. force = true;
  224. } else if (arg.startsWith('--category=')) {
  225. customCategories.push(arg.split('=')[1]);
  226. } else if (arg === 'core' || arg === 'full' || arg === 'custom') {
  227. profile = arg;
  228. } else {
  229. log.error(`Unknown argument: ${arg}`);
  230. showUsage();
  231. process.exit(1);
  232. }
  233. }
  234. // Determine categories to install
  235. let categories;
  236. if (profile === 'custom') {
  237. if (customCategories.length === 0) {
  238. log.error('Custom profile requires --category flags');
  239. showUsage();
  240. process.exit(1);
  241. }
  242. categories = customCategories;
  243. } else {
  244. categories = PROFILES[profile].categories;
  245. }
  246. // Check if context already exists
  247. if (existsSync(MANIFEST_FILE) && !force) {
  248. log.warning('Context already installed. Use --force to reinstall.');
  249. log.info('Current installation:');
  250. try {
  251. const manifest = JSON.parse(readFileSync(MANIFEST_FILE, 'utf-8'));
  252. console.log(JSON.stringify(manifest, null, 2));
  253. } catch {
  254. log.error('Failed to read manifest');
  255. }
  256. process.exit(0);
  257. }
  258. // Check dependencies
  259. checkDependencies();
  260. // Download context
  261. console.log('');
  262. log.info(`Installing ${profile} profile`);
  263. log.info(`Repository: ${GITHUB_REPO}`);
  264. log.info(`Branch: ${GITHUB_BRANCH}`);
  265. console.log('');
  266. downloadContext(categories);
  267. // Create manifest
  268. createManifest(profile, categories);
  269. console.log('');
  270. log.success('Context installation complete!');
  271. log.info(`Context location: ${CONTEXT_DIR}`);
  272. log.info(`Manifest: ${MANIFEST_FILE}`);
  273. console.log('');
  274. }
  275. // Run main function
  276. main();