Alvin Unreal пре 4 месеци
родитељ
комит
2c20f32e15
4 измењених фајлова са 118 додато и 23 уклоњено
  1. 18 0
      src/cli/paths.test.ts
  2. 17 0
      src/cli/paths.ts
  3. 58 16
      src/config/loader.test.ts
  4. 25 7
      src/config/loader.ts

+ 18 - 0
src/cli/paths.test.ts

@@ -9,6 +9,7 @@ import {
   getConfigDir,
   getConfigJson,
   getConfigJsonc,
+  getConfigSearchDirs,
   getExistingConfigPath,
   getLiteConfig,
   getOpenCodeConfigPaths,
@@ -44,6 +45,23 @@ describe('paths', () => {
     expect(getConfigDir()).toBe(expected);
   });
 
+  test('getConfigSearchDirs() returns custom dir first, then default dir', () => {
+    process.env.OPENCODE_CONFIG_DIR = '/custom/directory';
+    process.env.XDG_CONFIG_HOME = '/tmp/xdg-config';
+
+    expect(getConfigSearchDirs()).toEqual([
+      '/custom/directory',
+      '/tmp/xdg-config/opencode',
+    ]);
+  });
+
+  test('getConfigSearchDirs() de-duplicates identical dirs', () => {
+    process.env.OPENCODE_CONFIG_DIR = '/tmp/xdg-config/opencode';
+    process.env.XDG_CONFIG_HOME = '/tmp/xdg-config';
+
+    expect(getConfigSearchDirs()).toEqual(['/tmp/xdg-config/opencode']);
+  });
+
   test('getOpenCodeConfigPaths() returns both json and jsonc paths', () => {
     process.env.XDG_CONFIG_HOME = '/tmp/xdg-config';
     expect(getOpenCodeConfigPaths()).toEqual([

+ 17 - 0
src/cli/paths.ts

@@ -32,6 +32,23 @@ export function getConfigDir(): string {
   return getDefaultOpenCodeConfigDir();
 }
 
+/**
+ * Get OpenCode config directories in read/search order.
+ *
+ * Resolution order:
+ * 1. OPENCODE_CONFIG_DIR (if set)
+ * 2. XDG_CONFIG_HOME/opencode or ~/.config/opencode
+ *
+ * Duplicate entries are removed.
+ */
+export function getConfigSearchDirs(): string[] {
+  const dirs = [getCustomOpenCodeConfigDir(), getDefaultOpenCodeConfigDir()];
+
+  return dirs.filter((dir, index): dir is string => {
+    return Boolean(dir) && dirs.indexOf(dir) === index;
+  });
+}
+
 export function getOpenCodeConfigPaths(): string[] {
   const configDir = getDefaultOpenCodeConfigDir();
   return [join(configDir, 'opencode.json'), join(configDir, 'opencode.jsonc')];

+ 58 - 16
src/config/loader.test.ts

@@ -178,6 +178,30 @@ describe('loadPluginConfig', () => {
 
     fs.rmSync(customDir, { recursive: true, force: true });
   });
+
+  test('falls back to default user config dir when OPENCODE_CONFIG_DIR has no config', () => {
+    const customDir = fs.mkdtempSync(
+      path.join(os.tmpdir(), 'omc-opencode-config-empty-'),
+    );
+    process.env.OPENCODE_CONFIG_DIR = customDir;
+
+    const defaultConfigDir = path.join(userConfigDir, 'opencode');
+    fs.mkdirSync(defaultConfigDir, { recursive: true });
+    fs.writeFileSync(
+      path.join(defaultConfigDir, 'oh-my-opencode-slim.json'),
+      JSON.stringify({
+        agents: { oracle: { model: 'fallback/default-config' } },
+      }),
+    );
+
+    const projectDir = path.join(tempDir, 'project');
+    fs.mkdirSync(projectDir, { recursive: true });
+
+    const config = loadPluginConfig(projectDir);
+    expect(config.agents?.oracle?.model).toBe('fallback/default-config');
+
+    fs.rmSync(customDir, { recursive: true, force: true });
+  });
 });
 
 describe('deepMerge behavior', () => {
@@ -989,14 +1013,15 @@ describe('loadAgentPrompt', () => {
 
     // Use a unique agent name and check for it specifically
     const originalReadFileSync = fs.readFileSync;
-    const readSpy = spyOn(fs, 'readFileSync').mockImplementation(
-      (p: any, o: any) => {
-        if (typeof p === 'string' && p.includes('error-agent.md')) {
-          throw new Error('Read error');
-        }
-        return originalReadFileSync(p, o);
-      },
-    );
+    const readSpy = spyOn(fs, 'readFileSync').mockImplementation(((
+      ...args: Parameters<typeof fs.readFileSync>
+    ) => {
+      const [p] = args;
+      if (typeof p === 'string' && p.includes('error-agent.md')) {
+        throw new Error('Read error');
+      }
+      return originalReadFileSync(...args);
+    }) as typeof fs.readFileSync);
 
     try {
       const result = loadAgentPrompt('error-agent');
@@ -1084,14 +1109,15 @@ describe('loadAgentPrompt', () => {
 
     const consoleWarnSpy = spyOn(console, 'warn');
     const originalReadFileSync = fs.readFileSync;
-    const readSpy = spyOn(fs, 'readFileSync').mockImplementation(
-      (p: any, o: any) => {
-        if (typeof p === 'string' && p === presetPromptPath) {
-          throw new Error('Preset read error');
-        }
-        return originalReadFileSync(p, o);
-      },
-    );
+    const readSpy = spyOn(fs, 'readFileSync').mockImplementation(((
+      ...args: Parameters<typeof fs.readFileSync>
+    ) => {
+      const [p] = args;
+      if (typeof p === 'string' && p === presetPromptPath) {
+        throw new Error('Preset read error');
+      }
+      return originalReadFileSync(...args);
+    }) as typeof fs.readFileSync);
 
     try {
       const result = loadAgentPrompt('oracle', 'test');
@@ -1136,4 +1162,20 @@ describe('loadAgentPrompt', () => {
 
     fs.rmSync(customDir, { recursive: true, force: true });
   });
+
+  test('falls back to default prompt dir when OPENCODE_CONFIG_DIR has no prompt', () => {
+    const customDir = fs.mkdtempSync(
+      path.join(os.tmpdir(), 'omc-prompt-config-empty-'),
+    );
+    process.env.OPENCODE_CONFIG_DIR = customDir;
+
+    const promptsDir = path.join(tempDir, 'opencode', 'oh-my-opencode-slim');
+    fs.mkdirSync(promptsDir, { recursive: true });
+    fs.writeFileSync(path.join(promptsDir, 'oracle.md'), 'fallback prompt');
+
+    const result = loadAgentPrompt('oracle');
+    expect(result.prompt).toBe('fallback prompt');
+
+    fs.rmSync(customDir, { recursive: true, force: true });
+  });
 });

+ 25 - 7
src/config/loader.ts

@@ -1,7 +1,7 @@
 import * as fs from 'node:fs';
 import * as path from 'node:path';
 import { stripJsonComments } from '../cli/config-io';
-import { getConfigDir } from '../cli/paths';
+import { getConfigDir, getConfigSearchDirs } from '../cli/paths';
 import { type PluginConfig, PluginConfigSchema } from './schema';
 
 const PROMPTS_DIR_NAME = 'oh-my-opencode-slim';
@@ -66,6 +66,20 @@ function findConfigPath(basePath: string): string | null {
   return null;
 }
 
+function findConfigPathInDirs(
+  configDirs: string[],
+  baseName: string,
+): string | null {
+  for (const configDir of configDirs) {
+    const configPath = findConfigPath(path.join(configDir, baseName));
+    if (configPath) {
+      return configPath;
+    }
+  }
+
+  return null;
+}
+
 /**
  * Recursively merge two objects, with override values taking precedence.
  * For nested objects, merges recursively. For arrays and primitives, override replaces base.
@@ -121,7 +135,10 @@ function deepMerge<T extends Record<string, unknown>>(
  * @returns Merged plugin configuration (empty object if no configs found)
  */
 export function loadPluginConfig(directory: string): PluginConfig {
-  const userConfigBasePath = path.join(getConfigDir(), 'oh-my-opencode-slim');
+  const userConfigPath = findConfigPathInDirs(
+    getConfigSearchDirs(),
+    'oh-my-opencode-slim',
+  );
 
   const projectConfigBasePath = path.join(
     directory,
@@ -130,7 +147,6 @@ export function loadPluginConfig(directory: string): PluginConfig {
   );
 
   // Find existing config files (preferring .jsonc over .json)
-  const userConfigPath = findConfigPath(userConfigBasePath);
   const projectConfigPath = findConfigPath(projectConfigBasePath);
 
   let config: PluginConfig = userConfigPath
@@ -197,10 +213,12 @@ export function loadAgentPrompt(
 } {
   const presetDirName =
     preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : undefined;
-  const promptsDir = path.join(getConfigDir(), PROMPTS_DIR_NAME);
-  const promptSearchDirs = presetDirName
-    ? [path.join(promptsDir, presetDirName), promptsDir]
-    : [promptsDir];
+  const promptSearchDirs = getConfigSearchDirs().flatMap((configDir) => {
+    const promptsDir = path.join(configDir, PROMPTS_DIR_NAME);
+    return presetDirName
+      ? [path.join(promptsDir, presetDirName), promptsDir]
+      : [promptsDir];
+  });
   const result: { prompt?: string; appendPrompt?: string } = {};
 
   const readFirstPrompt = (