fix: complete config path and review follow-ups (#3348)

This commit is contained in:
Bohdan Triapitsyn
2026-09-05 02:19:59 +03:00
committed by GitHub
parent 0543b954c3
commit 260bfe666b
14 changed files with 119 additions and 29 deletions
+11
View File
@@ -172,3 +172,14 @@ Reachable filesystem routes: `api:fs:read` (attachments, config), `api:fs:search
Maintenance: reviews, changelog entries, and parity claims consult this map;
whoever mounts or unmounts a surface updates it in the same change.
## Global OpenCode paths
`opencodeConfigPaths.ts` owns the global config directory for config CRUD,
skill discovery/install, global AGENTS.md, and quota config-file lookup. It
resolves `$XDG_CONFIG_HOME/opencode` at extension startup, falling back to
`~/.config/opencode` when unset or blank. Project paths, the explicit
`OPENCODE_CONFIG` file layer, and the auth data directory stay separate.
No files are migrated. The behavior GET bridge response includes the effective
`path` for both existing and missing AGENTS.md files; shared Settings uses it
in the warning.
+4 -4
View File
@@ -1,6 +1,6 @@
import { OPENCODE_CONFIG_DIR } from './opencodeConfigPaths';
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
createAgent,
@@ -78,7 +78,7 @@ type ConfigRuntimeDeps = {
clientReloadDelayMs: number;
};
const AGENTS_MD_PATH = path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md');
const AGENTS_MD_PATH = path.join(OPENCODE_CONFIG_DIR, 'AGENTS.md');
const MAX_BEHAVIOR_PROMPT_SIZE = 1024 * 1024;
const resolveWorkingDirectory = (ctx: BridgeContext | undefined, directory?: string): string | undefined => (
@@ -186,10 +186,10 @@ export async function handleConfigBridgeMessage(
case 'api:behavior/agents-md:get': {
try {
const content = await fs.promises.readFile(AGENTS_MD_PATH, 'utf8');
return { id, type, success: true, data: { content, exists: true } };
return { id, type, success: true, data: { content, exists: true, path: AGENTS_MD_PATH } };
} catch (error) {
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
return { id, type, success: true, data: { content: '', exists: false } };
return { id, type, success: true, data: { content: '', exists: false, path: AGENTS_MD_PATH } };
}
throw error;
}
@@ -1,3 +1,4 @@
import { OPENCODE_CONFIG_DIR } from './opencodeConfigPaths';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
@@ -76,7 +77,7 @@ const inferSkillScopeAndSourceFromLocation = (location: string, workingDirectory
const home = os.homedir();
const userRoots = [
path.join(home, '.config', 'opencode'),
OPENCODE_CONFIG_DIR,
path.join(home, '.opencode'),
path.join(home, '.claude', 'skills'),
path.join(home, '.agents', 'skills'),
+1 -1
View File
@@ -1,10 +1,10 @@
import { OPENCODE_CONFIG_DIR } from './opencodeConfigPaths';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import yaml from 'yaml';
import { parse as parseJsonc, printParseErrorCode, type ParseError } from 'jsonc-parser';
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands');
const GLOBAL_SNIPPET_DIR = path.join(OPENCODE_CONFIG_DIR, 'snippet');
@@ -0,0 +1,51 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const moduleUrl = new URL('./opencodeConfigPaths.ts', import.meta.url).href;
const configUrl = new URL('./opencodeConfig.ts', import.meta.url).href;
test('unset and blank XDG keep the existing global directory', () => {
for (const value of [undefined, '', ' ']) {
const env = { ...process.env };
if (value === undefined) delete env.XDG_CONFIG_HOME;
else env.XDG_CONFIG_HOME = value;
const result = spawnSync(process.execPath, ['--eval', `
const { OPENCODE_CONFIG_DIR } = await import(${JSON.stringify(moduleUrl)});
process.stdout.write(OPENCODE_CONFIG_DIR);
`], { env, encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, path.join(os.homedir(), '.config', 'opencode'));
}
});
test('global CRUD uses XDG while project writes stay in the project', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-vscode-xdg-'));
const configHome = path.join(root, 'config');
const project = path.join(root, 'project');
try {
const result = spawnSync(process.execPath, ['--eval', `
const { createAgent, createCommand, createSkill } = await import(${JSON.stringify(configUrl)});
const project = ${JSON.stringify(project)};
for (const scope of ['user', 'project']) {
createAgent(scope + '-agent', { description: 'Test', prompt: 'Test prompt' }, project, scope);
createCommand(scope + '-command', { description: 'Test', template: 'Test command' }, project, scope);
createSkill(scope + '-skill', { description: 'Test', instructions: 'Test skill' }, project, scope);
}
`], {
env: { ...process.env, XDG_CONFIG_HOME: configHome, OPENCODE_CONFIG: '', OPENCODE_CONFIG_DIR: '' },
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
for (const [scope, directory] of [['user', path.join(configHome, 'opencode')], ['project', path.join(project, '.opencode')]]) {
assert.match(fs.readFileSync(path.join(directory, 'agents', `${scope}-agent.md`), 'utf8'), /Test prompt/);
assert.match(fs.readFileSync(path.join(directory, 'commands', `${scope}-command.md`), 'utf8'), /Test command/);
assert.match(fs.readFileSync(path.join(directory, 'skills', `${scope}-skill`, 'SKILL.md'), 'utf8'), /Test skill/);
}
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
@@ -0,0 +1,8 @@
import os from 'node:os';
import path from 'node:path';
// Resolve once at extension startup, matching the web backend.
export const OPENCODE_CONFIG_DIR = path.join(
process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config'),
'opencode',
);
+1 -1
View File
@@ -1,3 +1,4 @@
import { OPENCODE_CONFIG_DIR } from './opencodeConfigPaths';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
@@ -191,7 +192,6 @@ export type ProviderResult = {
planLabel?: string | null;
};
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
const OPENCODE_DATA_DIR = path.join(os.homedir(), '.local', 'share', 'opencode');
const AUTH_FILE = path.join(OPENCODE_DATA_DIR, 'auth.json');
+3 -2
View File
@@ -1,3 +1,4 @@
import { OPENCODE_CONFIG_DIR } from './opencodeConfigPaths';
import fs from 'fs';
import os from 'os';
import path from 'path';
@@ -418,8 +419,8 @@ async function copyDirectoryNoSymlinks(srcDir: string, dstDir: string) {
}
function getUserSkillBaseDir() {
const pluralPath = path.join(os.homedir(), '.config', 'opencode', 'skills');
const legacyPath = path.join(os.homedir(), '.config', 'opencode', 'skill');
const pluralPath = path.join(OPENCODE_CONFIG_DIR, 'skills');
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'skill');
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
return pluralPath;
}
+5 -5
View File
@@ -228,11 +228,11 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
return {
startingApi: 'OpenCode API başlatılıyor…',
initializing: 'Başlatılıyor…',
connecting: 'Bağlanılıyor…',
connecting: 'Bağlanıyor…',
connected: 'Bağlandı!',
connectionError: 'Bağlantı hatası',
reconnecting: 'Yeniden bağlanılıyor…',
cliNotFound: 'OpenCode CLI bulunamadı. Lütfen önce yükleyin.',
reconnecting: 'Yeniden bağlanıyor…',
cliNotFound: 'OpenCode CLI bulunamadı. Lütfen önce kurun.',
};
}
return {
@@ -310,8 +310,8 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
}
if (detected.indexOf('tr') === 0) {
return {
startingDevServer: (host) => 'Webview geliştirme sunucusu başlatılıyor (' + host + ')...',
waitingDevServer: (host, attempt) => 'Webview geliştirme sunucusu bekleniyor (' + host + ')... deneme ' + attempt,
startingDevServer: (host) => 'Webview dev sunucusu başlatılıyor (' + host + ')...',
waitingDevServer: (host, attempt) => 'Webview dev sunucusu bekleniyor (' + host + ')... deneme ' + attempt,
};
}
}