Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/commands/code/__tests__/auth-sync.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { decodeJwtPayload, hasBergetCodeSeat } from '../../../auth/jwt.js';
import {
Expand All @@ -17,6 +17,26 @@ import { FakeAuthService } from './fake-auth-service.js';
import { FakeFileStore } from './fake-file-store.js';
import { confirm, FakePrompter, select } from './fake-prompter.js';

const ENV_KEYS = [
'XDG_CONFIG_HOME',
'XDG_DATA_HOME',
'OPENCODE_CONFIG',
'OPENCODE_CONFIG_DIR',
'PI_CODING_AGENT_DIR',
];

beforeEach(() => {
for (const key of ENV_KEYS) {
delete process.env[key];
}
});

afterEach(() => {
for (const key of ENV_KEYS) {
delete process.env[key];
}
});

function base64urlEncode(data: string): string {
return Buffer.from(data).toString('base64url');
}
Expand Down
22 changes: 21 additions & 1 deletion src/commands/code/__tests__/init.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import type { ApiKeyServicePort, AuthServicePort } from '../ports/auth-services.js';
import type { CommandRunner } from '../ports/command-runner.js';
Expand All @@ -11,6 +11,26 @@ import { FakeCommandRunner } from './fake-command-runner.js';
import { FakeFileStore } from './fake-file-store.js';
import { CANCEL, confirm, FakePrompter, multiselect, select } from './fake-prompter.js';

const ENV_KEYS = [
'XDG_CONFIG_HOME',
'XDG_DATA_HOME',
'OPENCODE_CONFIG',
'OPENCODE_CONFIG_DIR',
'PI_CODING_AGENT_DIR',
];

beforeEach(() => {
for (const key of ENV_KEYS) {
delete process.env[key];
}
});

afterEach(() => {
for (const key of ENV_KEYS) {
delete process.env[key];
}
});

const makeDeps = (
overrides: Partial<Parameters<typeof runInit>[0]> = {},
): Parameters<typeof runInit>[0] => {
Expand Down
3 changes: 2 additions & 1 deletion src/commands/code/auth-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../../auth/jwt.js';
import { logger } from '../../utils/logger.js';
import { FatalError } from './errors.js';
import { getOpencodeAuthPath } from './xdg-paths.js';

export interface AuthDeps {
apiKeyService: ApiKeyServicePort;
Expand All @@ -32,7 +33,7 @@ export interface CliAuth {
const CLI_AUTH_PATH = (homeDir: string) => homeDir + '/.berget/auth.json';

const TOOL_AUTH_PATHS = {
opencode: (homeDir: string) => homeDir + '/.local/share/opencode/auth.json',
opencode: getOpencodeAuthPath,
pi: (homeDir: string) => homeDir + '/.pi/agent/auth.json',
} as const;

Expand Down
21 changes: 6 additions & 15 deletions src/commands/code/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { Prompter } from './ports/prompter.js';
import { getAllAgents, toMarkdown } from '../../agents/index.js';
import { CancelledError } from './errors.js';
import { readJsonMaybe } from './utils.js';
import { getOpencodeConfigDir, resolveGlobalConfigPath } from './xdg-paths.js';

const OPENCODE_PLUGIN = '@bergetai/opencode-auth@1.0.24';
const OPENCODE_PLUGIN_NAME = '@bergetai/opencode-auth';
Expand All @@ -33,14 +34,9 @@ export async function getOpencodeState(
): Promise<{ global: boolean; project: boolean }> {
const projectJsonc = await readJsonMaybe(files, path.join(cwd, 'opencode.jsonc'));
const projectJson = await readJsonMaybe(files, path.join(cwd, 'opencode.json'));
const globalJsonc = await readJsonMaybe(
files,
path.join(homeDir, '.config', 'opencode', 'opencode.jsonc'),
);
const globalJson = await readJsonMaybe(
files,
path.join(homeDir, '.config', 'opencode', 'opencode.json'),
);
const globalDir = getOpencodeConfigDir(homeDir);
const globalJsonc = await readJsonMaybe(files, path.join(globalDir, 'opencode.jsonc'));
const globalJson = await readJsonMaybe(files, path.join(globalDir, 'opencode.json'));

return {
global: hasPluginInConfig(globalJsonc) || hasPluginInConfig(globalJson),
Expand Down Expand Up @@ -102,7 +98,7 @@ export async function initOpenCodeAgents(deps: {
const agentsDir =
scope === 'project'
? path.join(cwd, '.opencode', 'agents')
: path.join(homeDir, '.config', 'opencode', 'agents');
: path.join(getOpencodeConfigDir(homeDir), 'agents');

prompter.note('Space to toggle, Enter to confirm.', 'Agent Setup');

Expand Down Expand Up @@ -273,10 +269,5 @@ async function resolveOpencodeConfigPath(
return jsonPath;
}

const globalDir = path.join(homeDir, '.config', 'opencode');
const jsoncPath = path.join(globalDir, 'opencode.jsonc');
const jsonPath = path.join(globalDir, 'opencode.json');
if (await files.exists(jsoncPath)) return jsoncPath;
if (await files.exists(jsonPath)) return jsonPath;
return jsonPath;
return resolveGlobalConfigPath(homeDir, (p) => files.exists(p));
}
54 changes: 54 additions & 0 deletions src/commands/code/xdg-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as path from 'node:path';

/**
* Resolve the path to the OpenCode auth file.
*/
export function getOpencodeAuthPath(homeDir: string): string {
return path.join(getOpencodeDataDir(homeDir), 'auth.json');
}

/**
* Resolve the OpenCode global config directory.
* Honors XDG_CONFIG_HOME, then falls back to ~/.config/opencode.
* Also honors OPENCODE_CONFIG_DIR if set (highest priority).
*/
export function getOpencodeConfigDir(homeDir: string): string {
const envDir = process.env.OPENCODE_CONFIG_DIR;
if (envDir) {
return envDir;
}
const xdgConfig = process.env.XDG_CONFIG_HOME;
return path.join(xdgConfig || path.join(homeDir, '.config'), 'opencode');
}

/**
* Resolve the OpenCode data directory (for auth.json, sessions, etc.).
* Honors XDG_DATA_HOME, then falls back to ~/.local/share/opencode.
*/
export function getOpencodeDataDir(homeDir: string): string {
const xdgData = process.env.XDG_DATA_HOME;
return path.join(xdgData || path.join(homeDir, '.local', 'share'), 'opencode');
}

/**
* Resolve the path to the global opencode config file.
* Honors OPENCODE_CONFIG (single-file override), then probes
* OPENCODE_CONFIG_DIR / XDG_CONFIG_HOME / ~/.config.
*/
export async function resolveGlobalConfigPath(
homeDir: string,
exists: (p: string) => Promise<boolean>,
): Promise<string> {
const envConfig = process.env.OPENCODE_CONFIG;
if (envConfig) {
return envConfig;
}

const configDir = getOpencodeConfigDir(homeDir);
const jsoncPath = path.join(configDir, 'opencode.jsonc');
const jsonPath = path.join(configDir, 'opencode.json');

if (await exists(jsoncPath)) return jsoncPath;
if (await exists(jsonPath)) return jsonPath;
return jsonPath;
}
Loading