background-subagents.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
  2. import { homedir } from 'node:os';
  3. import { dirname, join } from 'node:path';
  4. export type BackgroundSubagentsMode = 'ask' | 'yes' | 'no';
  5. export type ShellKind = 'bash' | 'fish' | 'zsh';
  6. const ENV_NAME = 'OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS';
  7. const EXA_ENV_NAME = 'OPENCODE_ENABLE_EXA';
  8. const START_MARKER = '# >>> oh-my-opencode-slim background subagents >>>';
  9. const END_MARKER = '# <<< oh-my-opencode-slim background subagents <<<';
  10. export function isBackgroundSubagentsEnabled(
  11. value: string | undefined,
  12. ): boolean {
  13. if (!value) return false;
  14. const normalized = value.trim().toLowerCase();
  15. return normalized !== '' && !['0', 'false', 'no', 'off'].includes(normalized);
  16. }
  17. export function detectShellKind(
  18. shell: string | undefined,
  19. ): ShellKind | undefined {
  20. const name = shell?.split('/').at(-1);
  21. if (name === 'zsh' || name === 'bash' || name === 'fish') return name;
  22. return undefined;
  23. }
  24. export function detectBackgroundSubagentsTarget(
  25. env: NodeJS.ProcessEnv = process.env,
  26. ): string | undefined {
  27. const shell = detectShellKind(env.SHELL);
  28. const home = env.HOME || homedir();
  29. if (shell === 'zsh') return join(home, '.zshrc');
  30. if (shell === 'bash') return join(home, '.bashrc');
  31. if (shell === 'fish') {
  32. const configHome = env.XDG_CONFIG_HOME || join(home, '.config');
  33. return join(
  34. configHome,
  35. 'fish',
  36. 'conf.d',
  37. 'opencode-background-subagents.fish',
  38. );
  39. }
  40. return undefined;
  41. }
  42. export function getBackgroundSubagentsBlock(targetPath: string): string {
  43. const isFish = targetPath.endsWith('.fish');
  44. const commands = isFish
  45. ? `set -gx ${ENV_NAME} true\nset -gx ${EXA_ENV_NAME} 1`
  46. : `export ${ENV_NAME}=true\nexport ${EXA_ENV_NAME}=1`;
  47. return `${START_MARKER}\n${commands}\n${END_MARKER}`;
  48. }
  49. export function manualBackgroundSubagentsInstructions(options?: {
  50. targetPath?: string;
  51. shell?: ShellKind;
  52. }): string {
  53. const shell =
  54. options?.shell ??
  55. (options?.targetPath?.endsWith('.fish') ? 'fish' : undefined) ??
  56. detectShellKind(options?.targetPath);
  57. const bashZshSnippet = `export ${ENV_NAME}=true`;
  58. const fishSnippet = `set -gx ${ENV_NAME} true`;
  59. const bashZshExaSnippet = `export ${EXA_ENV_NAME}=1`;
  60. const fishExaSnippet = `set -gx ${EXA_ENV_NAME} 1`;
  61. if (shell === 'fish') {
  62. return `Start OpenCode with background subagents and websearch enabled:\n env ${ENV_NAME}=true ${EXA_ENV_NAME}=1 opencode\n\nOr add these to your fish startup file:\n ${fishSnippet}\n ${fishExaSnippet}`;
  63. }
  64. if (shell === 'bash' || shell === 'zsh') {
  65. return `Start OpenCode with background subagents and websearch enabled:\n ${ENV_NAME}=true ${EXA_ENV_NAME}=1 opencode\n\nOr add these to your shell startup file:\n ${bashZshSnippet}\n ${bashZshExaSnippet}`;
  66. }
  67. return `Start OpenCode with background subagents and websearch enabled:\n ${ENV_NAME}=true ${EXA_ENV_NAME}=1 opencode\n\nOr add the matching pair to your shell startup file:\n bash/zsh: ${bashZshSnippet}\n ${bashZshExaSnippet}\n fish: ${fishSnippet}\n ${fishExaSnippet}`;
  68. }
  69. export function expandHomePath(targetPath: string): string {
  70. if (targetPath === '~') return homedir();
  71. if (targetPath.startsWith('~/')) return join(homedir(), targetPath.slice(2));
  72. return targetPath;
  73. }
  74. export function upsertBackgroundSubagentsBlock(
  75. content: string,
  76. block: string,
  77. ): string {
  78. const start = content.indexOf(START_MARKER);
  79. const end = content.indexOf(END_MARKER);
  80. if (start !== -1 && end !== -1 && end > start) {
  81. const afterEnd = end + END_MARKER.length;
  82. return `${content.slice(0, start)}${block}${content.slice(afterEnd)}`;
  83. }
  84. const separator = content.length > 0 && !content.endsWith('\n') ? '\n\n' : '';
  85. const prefix =
  86. content.length > 0 && content.endsWith('\n') ? '\n' : separator;
  87. return `${content}${prefix}${block}\n`;
  88. }
  89. export function writeBackgroundSubagentsBlock(targetPath: string): void {
  90. const block = getBackgroundSubagentsBlock(targetPath);
  91. const content = existsSync(targetPath)
  92. ? readFileSync(targetPath, 'utf8')
  93. : '';
  94. const nextContent = upsertBackgroundSubagentsBlock(content, block);
  95. mkdirSync(dirname(targetPath), { recursive: true });
  96. writeFileSync(targetPath, nextContent);
  97. }