install.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import { existsSync } from 'node:fs';
  2. import { createInterface } from 'node:readline/promises';
  3. import {
  4. addPluginToOpenCodeConfig,
  5. addPluginToOpenCodeTuiConfig,
  6. detectCurrentConfig,
  7. disableDefaultAgents,
  8. enableLspByDefault,
  9. generateLiteConfig,
  10. getOpenCodePath,
  11. getOpenCodeVersion,
  12. isOpenCodeInstalled,
  13. writeLiteConfig,
  14. } from './config-manager';
  15. import { CUSTOM_SKILLS, installCustomSkill } from './custom-skills';
  16. import { getExistingLiteConfigPath } from './paths';
  17. import { installSkill, RECOMMENDED_SKILLS } from './skills';
  18. import type { ConfigMergeResult, InstallArgs, InstallConfig } from './types';
  19. // Colors
  20. const GREEN = '\x1b[32m';
  21. const BLUE = '\x1b[34m';
  22. const YELLOW = '\x1b[33m';
  23. const RED = '\x1b[31m';
  24. const BOLD = '\x1b[1m';
  25. const DIM = '\x1b[2m';
  26. const RESET = '\x1b[0m';
  27. const SYMBOLS = {
  28. check: `${GREEN}[ok]${RESET}`,
  29. cross: `${RED}[x]${RESET}`,
  30. arrow: `${BLUE}->${RESET}`,
  31. bullet: `${DIM}-${RESET}`,
  32. info: `${BLUE}[i]${RESET}`,
  33. warn: `${YELLOW}[!]${RESET}`,
  34. star: `${YELLOW}★${RESET}`,
  35. };
  36. const GITHUB_REPO = 'alvinunreal/oh-my-opencode-slim';
  37. const GITHUB_URL = `https://github.com/${GITHUB_REPO}`;
  38. function printHeader(isUpdate: boolean): void {
  39. console.log();
  40. console.log(
  41. `${BOLD}oh-my-opencode-slim ${isUpdate ? 'Update' : 'Install'}${RESET}`,
  42. );
  43. console.log('='.repeat(30));
  44. console.log();
  45. }
  46. function printStep(step: number, total: number, message: string): void {
  47. console.log(`${DIM}[${step}/${total}]${RESET} ${message}`);
  48. }
  49. function printSuccess(message: string): void {
  50. console.log(`${SYMBOLS.check} ${message}`);
  51. }
  52. function printError(message: string): void {
  53. console.log(`${SYMBOLS.cross} ${RED}${message}${RESET}`);
  54. }
  55. function printInfo(message: string): void {
  56. console.log(`${SYMBOLS.info} ${message}`);
  57. }
  58. async function confirm(message: string, defaultYes = true): Promise<boolean> {
  59. const suffix = defaultYes ? ' (Y/n) ' : ' (y/N) ';
  60. const rl = createInterface({ input: process.stdin, output: process.stdout });
  61. try {
  62. const answer = (await rl.question(`${message}${suffix}`))
  63. .trim()
  64. .toLowerCase();
  65. if (!answer) return defaultYes;
  66. return answer === 'y' || answer === 'yes';
  67. } finally {
  68. rl.close();
  69. }
  70. }
  71. async function askToStarRepo(config: InstallConfig): Promise<void> {
  72. if (!config.promptForStar || config.dryRun || !process.stdin.isTTY) return;
  73. console.log();
  74. const shouldStar = await confirm(
  75. `${SYMBOLS.star} Star the repo on GitHub?`,
  76. true,
  77. );
  78. if (!shouldStar) return;
  79. try {
  80. const { execFileSync } = await import('node:child_process');
  81. execFileSync(
  82. 'gh',
  83. ['api', '--silent', '--method', 'PUT', `/user/starred/${GITHUB_REPO}`],
  84. { stdio: 'ignore', timeout: 10_000 },
  85. );
  86. printSuccess('Thanks for starring! ★');
  87. } catch {
  88. printInfo(
  89. `Couldn't star automatically. You can star manually:\n ${BLUE}${GITHUB_URL}${RESET}`,
  90. );
  91. }
  92. }
  93. async function checkOpenCodeInstalled(): Promise<{
  94. ok: boolean;
  95. version?: string;
  96. path?: string;
  97. }> {
  98. const installed = await isOpenCodeInstalled();
  99. if (!installed) {
  100. printError('OpenCode is not installed on this system.');
  101. printInfo('Install it with:');
  102. console.log(
  103. ` ${BLUE}curl -fsSL https://opencode.ai/install | bash${RESET}`,
  104. );
  105. console.log();
  106. printInfo('Or if already installed, add it to your PATH:');
  107. console.log(` ${BLUE}export PATH="$HOME/.local/bin:$PATH"${RESET}`);
  108. console.log(` ${BLUE}export PATH="$HOME/.opencode/bin:$PATH"${RESET}`);
  109. return { ok: false };
  110. }
  111. const version = await getOpenCodeVersion();
  112. const path = getOpenCodePath();
  113. const detectedVersion = version ?? '';
  114. const pathInfo = path ? ` (${DIM}${path}${RESET})` : '';
  115. printSuccess(`OpenCode ${detectedVersion} detected${pathInfo}`);
  116. return { ok: true, version: version ?? undefined, path: path ?? undefined };
  117. }
  118. function handleStepResult(
  119. result: ConfigMergeResult,
  120. successMsg: string,
  121. ): boolean {
  122. if (!result.success) {
  123. printError(`Failed: ${result.error}`);
  124. return false;
  125. }
  126. printSuccess(
  127. `${successMsg} ${SYMBOLS.arrow} ${DIM}${result.configPath}${RESET}`,
  128. );
  129. return true;
  130. }
  131. async function runInstall(config: InstallConfig): Promise<number> {
  132. const detected = detectCurrentConfig();
  133. const isUpdate = detected.isInstalled;
  134. printHeader(isUpdate);
  135. let totalSteps = 6;
  136. if (config.installSkills) totalSteps += 1;
  137. if (config.installCustomSkills) totalSteps += 1;
  138. let step = 1;
  139. printStep(step++, totalSteps, 'Checking OpenCode installation...');
  140. if (config.dryRun) {
  141. printInfo('Dry run mode - skipping OpenCode check');
  142. } else {
  143. const { ok } = await checkOpenCodeInstalled();
  144. if (!ok) return 1;
  145. }
  146. printStep(step++, totalSteps, 'Adding oh-my-opencode-slim plugin...');
  147. if (config.dryRun) {
  148. printInfo('Dry run mode - skipping plugin installation');
  149. } else {
  150. const pluginResult = await addPluginToOpenCodeConfig();
  151. if (!handleStepResult(pluginResult, 'Plugin added')) return 1;
  152. }
  153. printStep(step++, totalSteps, 'Adding TUI version badge...');
  154. if (config.dryRun) {
  155. printInfo('Dry run mode - skipping TUI plugin installation');
  156. } else {
  157. const tuiResult = await addPluginToOpenCodeTuiConfig();
  158. if (!tuiResult.success) {
  159. printInfo(`Skipped TUI badge: ${tuiResult.error}`);
  160. } else {
  161. handleStepResult(tuiResult, 'TUI badge added');
  162. }
  163. }
  164. printStep(step++, totalSteps, 'Disabling OpenCode default agents...');
  165. if (config.dryRun) {
  166. printInfo('Dry run mode - skipping agent disabling');
  167. } else {
  168. const agentResult = disableDefaultAgents();
  169. if (!handleStepResult(agentResult, 'Default agents disabled')) return 1;
  170. }
  171. printStep(step++, totalSteps, 'Enabling OpenCode LSP integration...');
  172. if (config.dryRun) {
  173. printInfo('Dry run mode - skipping LSP configuration');
  174. } else {
  175. const lspResult = enableLspByDefault();
  176. if (!handleStepResult(lspResult, 'LSP enabled')) return 1;
  177. }
  178. printStep(step++, totalSteps, 'Writing oh-my-opencode-slim configuration...');
  179. if (config.dryRun) {
  180. const liteConfig = generateLiteConfig(config);
  181. printInfo('Dry run mode - configuration that would be written:');
  182. console.log(`\n${JSON.stringify(liteConfig, null, 2)}\n`);
  183. } else {
  184. const configPath = getExistingLiteConfigPath();
  185. const configExists = existsSync(configPath);
  186. if (configExists && !config.reset) {
  187. printInfo(
  188. `Configuration already exists at ${configPath}. ` +
  189. 'Use --reset to overwrite.',
  190. );
  191. } else {
  192. const liteResult = writeLiteConfig(
  193. config,
  194. configExists ? configPath : undefined,
  195. );
  196. if (
  197. !handleStepResult(
  198. liteResult,
  199. configExists ? 'Config reset' : 'Config written',
  200. )
  201. )
  202. return 1;
  203. }
  204. }
  205. // Install skills if requested
  206. if (config.installSkills) {
  207. printStep(step++, totalSteps, 'Installing recommended skills...');
  208. if (config.dryRun) {
  209. printInfo('Dry run mode - would install skills:');
  210. for (const skill of RECOMMENDED_SKILLS) {
  211. printInfo(` - ${skill.name}`);
  212. }
  213. } else {
  214. let skillsInstalled = 0;
  215. for (const skill of RECOMMENDED_SKILLS) {
  216. printInfo(`Installing ${skill.name}...`);
  217. if (installSkill(skill)) {
  218. printSuccess(`Installed: ${skill.name}`);
  219. skillsInstalled++;
  220. } else {
  221. printInfo(`Skipped: ${skill.name} (already installed)`);
  222. }
  223. }
  224. printSuccess(
  225. `${skillsInstalled}/${RECOMMENDED_SKILLS.length} skills processed`,
  226. );
  227. }
  228. }
  229. // Install custom skills if requested
  230. if (config.installCustomSkills) {
  231. printStep(step++, totalSteps, 'Installing custom skills...');
  232. if (config.dryRun) {
  233. printInfo('Dry run mode - would install custom skills:');
  234. for (const skill of CUSTOM_SKILLS) {
  235. printInfo(` - ${skill.name}`);
  236. }
  237. } else {
  238. let customSkillsInstalled = 0;
  239. for (const skill of CUSTOM_SKILLS) {
  240. printInfo(`Installing ${skill.name}...`);
  241. if (installCustomSkill(skill)) {
  242. printSuccess(`Installed: ${skill.name}`);
  243. customSkillsInstalled++;
  244. } else {
  245. printInfo(`Skipped: ${skill.name} (already installed)`);
  246. }
  247. }
  248. const totalCustom = CUSTOM_SKILLS.length;
  249. printSuccess(
  250. `${customSkillsInstalled}/${totalCustom} custom skills processed`,
  251. );
  252. }
  253. }
  254. const statusMsg = isUpdate
  255. ? 'Configuration updated!'
  256. : 'Installation complete!';
  257. console.log(`${SYMBOLS.star} ${BOLD}${GREEN}${statusMsg}${RESET}`);
  258. console.log();
  259. console.log(`${BOLD}Next steps:${RESET}`);
  260. console.log();
  261. const configPath = getExistingLiteConfigPath();
  262. console.log(' 1. Log in to the provider(s) you want to use:');
  263. console.log(` ${BLUE}$ opencode auth login${RESET}`);
  264. console.log();
  265. console.log(' 2. Refresh the models OpenCode can see:');
  266. console.log(` ${BLUE}$ opencode models --refresh${RESET}`);
  267. console.log();
  268. console.log(' 3. Review your generated config:');
  269. console.log(` ${BLUE}${configPath}${RESET}`);
  270. console.log();
  271. console.log(' 4. Start OpenCode:');
  272. console.log(` ${BLUE}$ opencode${RESET}`);
  273. console.log();
  274. console.log(' 5. Verify the agents are responding:');
  275. console.log(` ${BLUE}> ping all agents${RESET}`);
  276. console.log();
  277. const modelsInfo =
  278. config.preset && config.preset !== 'openai'
  279. ? `Generated OpenAI and OpenCode Go presets; ${config.preset} is active.`
  280. : 'Generated OpenAI and OpenCode Go presets; OpenAI is active by default.';
  281. console.log(`${modelsInfo}`);
  282. const altProviders = 'For the full configuration reference, see:';
  283. console.log(altProviders);
  284. const docsUrl =
  285. 'https://github.com/alvinunreal/oh-my-opencode-slim/' +
  286. 'blob/master/docs/configuration.md';
  287. console.log(` ${BLUE}${docsUrl}${RESET}`);
  288. console.log();
  289. await askToStarRepo(config);
  290. return 0;
  291. }
  292. export async function install(args: InstallArgs): Promise<number> {
  293. const config: InstallConfig = {
  294. hasTmux: false,
  295. installSkills: args.skills === 'yes',
  296. installCustomSkills: args.skills === 'yes',
  297. preset: args.preset,
  298. promptForStar: args.tui,
  299. dryRun: args.dryRun,
  300. reset: args.reset ?? false,
  301. };
  302. return runInstall(config);
  303. }