Skip to content
Closed
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"vitest": "^3.2.4"
},
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp -r src/command dist/command",
"build": "node scripts/build.mjs",
"prepack": "bun run build",
"test": "vitest run",
"test:watch": "vitest",
Expand Down
23 changes: 23 additions & 0 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { cp, rm } from 'node:fs/promises';
import { spawn } from 'node:child_process';

await rm('dist', { force: true, recursive: true });
await run(process.execPath, ['./node_modules/typescript/bin/tsc', '-p', 'tsconfig.build.json']);
await cp('src/command', 'dist/command', { recursive: true });

function run(command, args) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: 'inherit',
});
child.on('error', reject);
child.on('close', (code) => {
if (code === 0) {
resolve();
return;
}

reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 1}`));
});
});
}
115 changes: 67 additions & 48 deletions src/sync/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ const MODEL_FAVORITES_FILE = 'model.json';
const SKILLS_DIR = 'skills';
const HOME_AGENTS_DIR = '.agents';

function pathForPlatform(platform: NodeJS.Platform): path.PlatformPath {
return platform === 'win32' ? path.win32 : path.posix;
}

export function pathForLocations(locations: SyncLocations): path.PlatformPath {
return locations.xdg.homeDir.includes('\\') || /^[a-zA-Z]:/.test(locations.xdg.homeDir)
? path.win32
: path.posix;
}
Comment thread
eyshoit-commits marked this conversation as resolved.

export function resolveHomeDir(
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform
Expand Down Expand Up @@ -87,16 +97,18 @@ export function resolveXdgPaths(
}

if (platform === 'win32') {
const configDir = env.APPDATA ?? path.join(homeDir, 'AppData', 'Roaming');
const dataDir = env.LOCALAPPDATA ?? path.join(homeDir, 'AppData', 'Local');
const pathApi = pathForPlatform(platform);
const configDir = env.APPDATA ?? pathApi.join(homeDir, 'AppData', 'Roaming');
const dataDir = env.LOCALAPPDATA ?? pathApi.join(homeDir, 'AppData', 'Local');
// Windows doesn't have XDG_STATE_HOME equivalent, use LOCALAPPDATA
const stateDir = env.LOCALAPPDATA ?? path.join(homeDir, 'AppData', 'Local');
const stateDir = env.LOCALAPPDATA ?? pathApi.join(homeDir, 'AppData', 'Local');
return { homeDir, configDir, dataDir, stateDir };
}

const configDir = env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config');
const dataDir = env.XDG_DATA_HOME ?? path.join(homeDir, '.local', 'share');
const stateDir = env.XDG_STATE_HOME ?? path.join(homeDir, '.local', 'state');
const pathApi = pathForPlatform(platform);
const configDir = env.XDG_CONFIG_HOME ?? pathApi.join(homeDir, '.config');
const dataDir = env.XDG_DATA_HOME ?? pathApi.join(homeDir, '.local', 'share');
const stateDir = env.XDG_STATE_HOME ?? pathApi.join(homeDir, '.local', 'state');

return { homeDir, configDir, dataDir, stateDir };
}
Expand All @@ -107,26 +119,31 @@ export function resolveSyncLocations(
): SyncLocations {
const xdg = resolveXdgPaths(env, platform);
const customConfigDir = env.opencode_config_dir;
const pathApi = pathForPlatform(platform);
const configRoot = customConfigDir
? path.resolve(expandHome(customConfigDir, xdg.homeDir))
: path.join(xdg.configDir, 'opencode');
const dataRoot = path.join(xdg.dataDir, 'opencode');
? pathApi.resolve(expandHome(customConfigDir, xdg.homeDir, platform))
: pathApi.join(xdg.configDir, 'opencode');
const dataRoot = pathApi.join(xdg.dataDir, 'opencode');

return {
xdg,
configRoot,
syncConfigPath: path.join(configRoot, DEFAULT_SYNC_CONFIG_NAME),
overridesPath: path.join(configRoot, DEFAULT_OVERRIDES_NAME),
statePath: path.join(dataRoot, DEFAULT_STATE_NAME),
defaultRepoDir: path.join(dataRoot, 'opencode-synced', 'repo'),
syncConfigPath: pathApi.join(configRoot, DEFAULT_SYNC_CONFIG_NAME),
overridesPath: pathApi.join(configRoot, DEFAULT_OVERRIDES_NAME),
statePath: pathApi.join(dataRoot, DEFAULT_STATE_NAME),
defaultRepoDir: pathApi.join(dataRoot, 'opencode-synced', 'repo'),
};
Comment thread
eyshoit-commits marked this conversation as resolved.
}

export function expandHome(inputPath: string, homeDir: string): string {
export function expandHome(
inputPath: string,
homeDir: string,
platform: NodeJS.Platform = process.platform
): string {
if (!inputPath) return inputPath;
if (!homeDir) return inputPath;
if (inputPath === '~') return homeDir;
if (inputPath.startsWith('~/')) return path.join(homeDir, inputPath.slice(2));
if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) return pathForPlatform(platform).join(homeDir, inputPath.slice(2));
return inputPath;
}

Expand All @@ -135,8 +152,9 @@ export function normalizePath(
homeDir: string,
platform: NodeJS.Platform = process.platform
): string {
const expanded = expandHome(inputPath, homeDir);
const resolved = path.resolve(expanded);
const pathApi = pathForPlatform(platform);
const expanded = expandHome(inputPath, homeDir, platform);
const resolved = pathApi.resolve(expanded);
if (platform === 'win32') {
return resolved.toLowerCase();
}
Expand Down Expand Up @@ -177,26 +195,27 @@ export function buildSyncPlan(
platform: NodeJS.Platform = process.platform
): SyncPlan {
const configRoot = locations.configRoot;
const dataRoot = path.join(locations.xdg.dataDir, 'opencode');
const stateRoot = path.join(locations.xdg.stateDir, 'opencode');
const repoConfigRoot = path.join(repoRoot, 'config');
const repoDataRoot = path.join(repoRoot, 'data');
const repoSecretsRoot = path.join(repoRoot, 'secrets');
const repoStateRoot = path.join(repoRoot, 'state');
const repoExtraDir = path.join(repoSecretsRoot, 'extra');
const manifestPath = path.join(repoSecretsRoot, 'extra-manifest.json');
const repoConfigExtraDir = path.join(repoConfigRoot, 'extra');
const configManifestPath = path.join(repoConfigRoot, 'extra-manifest.json');
const pathApi = pathForPlatform(platform);
const dataRoot = pathApi.join(locations.xdg.dataDir, 'opencode');
const stateRoot = pathApi.join(locations.xdg.stateDir, 'opencode');
const repoConfigRoot = pathApi.join(repoRoot, 'config');
const repoDataRoot = pathApi.join(repoRoot, 'data');
const repoSecretsRoot = pathApi.join(repoRoot, 'secrets');
const repoStateRoot = pathApi.join(repoRoot, 'state');
const repoExtraDir = pathApi.join(repoSecretsRoot, 'extra');
const manifestPath = pathApi.join(repoSecretsRoot, 'extra-manifest.json');
const repoConfigExtraDir = pathApi.join(repoConfigRoot, 'extra');
const configManifestPath = pathApi.join(repoConfigRoot, 'extra-manifest.json');

const items: SyncItem[] = [];
const usingSecretsBackend = hasSecretsBackend(config);
const authJsonPath = path.join(dataRoot, 'auth.json');
const mcpAuthJsonPath = path.join(dataRoot, 'mcp-auth.json');
const authJsonPath = pathApi.join(dataRoot, 'auth.json');
const mcpAuthJsonPath = pathApi.join(dataRoot, 'mcp-auth.json');

const addFile = (name: string, isSecret: boolean, isConfigFile: boolean): void => {
items.push({
localPath: path.join(configRoot, name),
repoPath: path.join(repoConfigRoot, name),
localPath: pathApi.join(configRoot, name),
repoPath: pathApi.join(repoConfigRoot, name),
type: 'file',
isSecret,
isConfigFile,
Expand All @@ -210,8 +229,8 @@ export function buildSyncPlan(

for (const dirName of CONFIG_DIRS) {
items.push({
localPath: path.join(configRoot, dirName),
repoPath: path.join(repoConfigRoot, dirName),
localPath: pathApi.join(configRoot, dirName),
repoPath: pathApi.join(repoConfigRoot, dirName),
type: 'dir',
isSecret: false,
isConfigFile: false,
Expand All @@ -220,8 +239,8 @@ export function buildSyncPlan(

if (config.includeOpencodeSkills !== false) {
items.push({
localPath: path.join(configRoot, SKILLS_DIR),
repoPath: path.join(repoConfigRoot, SKILLS_DIR),
localPath: pathApi.join(configRoot, SKILLS_DIR),
repoPath: pathApi.join(repoConfigRoot, SKILLS_DIR),
type: 'dir',
isSecret: false,
isConfigFile: false,
Expand All @@ -230,8 +249,8 @@ export function buildSyncPlan(

if (config.includeAgentsDir !== false) {
items.push({
localPath: path.join(locations.xdg.homeDir, HOME_AGENTS_DIR),
repoPath: path.join(repoConfigRoot, HOME_AGENTS_DIR),
localPath: pathApi.join(locations.xdg.homeDir, HOME_AGENTS_DIR),
repoPath: pathApi.join(repoConfigRoot, HOME_AGENTS_DIR),
type: 'dir',
isSecret: false,
isConfigFile: false,
Expand All @@ -240,8 +259,8 @@ export function buildSyncPlan(

if (config.includeModelFavorites !== false) {
items.push({
localPath: path.join(stateRoot, MODEL_FAVORITES_FILE),
repoPath: path.join(repoStateRoot, MODEL_FAVORITES_FILE),
localPath: pathApi.join(stateRoot, MODEL_FAVORITES_FILE),
repoPath: pathApi.join(repoStateRoot, MODEL_FAVORITES_FILE),
type: 'file',
isSecret: false,
isConfigFile: false,
Expand All @@ -253,14 +272,14 @@ export function buildSyncPlan(
items.push(
{
localPath: authJsonPath,
repoPath: path.join(repoDataRoot, 'auth.json'),
repoPath: pathApi.join(repoDataRoot, 'auth.json'),
type: 'file',
isSecret: true,
isConfigFile: false,
},
{
localPath: mcpAuthJsonPath,
repoPath: path.join(repoDataRoot, 'mcp-auth.json'),
repoPath: pathApi.join(repoDataRoot, 'mcp-auth.json'),
type: 'file',
isSecret: true,
isConfigFile: false,
Expand All @@ -270,8 +289,8 @@ export function buildSyncPlan(

if (config.includeSessions && !isTursoSessionBackend(config)) {
items.push({
localPath: path.join(dataRoot, SESSION_DB_FILE),
repoPath: path.join(repoDataRoot, SESSION_DB_FILE),
localPath: pathApi.join(dataRoot, SESSION_DB_FILE),
repoPath: pathApi.join(repoDataRoot, SESSION_DB_FILE),
type: 'file',
isSecret: true,
isConfigFile: false,
Expand All @@ -280,8 +299,8 @@ export function buildSyncPlan(

for (const dirName of SESSION_DIRS) {
items.push({
localPath: path.join(dataRoot, dirName),
repoPath: path.join(repoDataRoot, dirName),
localPath: pathApi.join(dataRoot, dirName),
repoPath: pathApi.join(repoDataRoot, dirName),
type: 'dir',
isSecret: true,
isConfigFile: false,
Expand All @@ -293,8 +312,8 @@ export function buildSyncPlan(
if (config.includePromptStash) {
for (const fileName of PROMPT_STASH_FILES) {
items.push({
localPath: path.join(stateRoot, fileName),
repoPath: path.join(repoStateRoot, fileName),
localPath: pathApi.join(stateRoot, fileName),
repoPath: pathApi.join(repoStateRoot, fileName),
type: 'file',
isSecret: true,
isConfigFile: false,
Expand Down Expand Up @@ -356,7 +375,7 @@ function buildExtraPathPlan(

const entries = allowlist.map((sourcePath) => ({
sourcePath,
repoPath: path.join(repoExtraDir, encodeExtraPath(sourcePath)),
repoPath: pathForPlatform(platform).join(repoExtraDir, encodeExtraPath(sourcePath)),
}));

return {
Expand Down
16 changes: 12 additions & 4 deletions src/sync/turso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import path from 'node:path';
import type { NormalizedSyncConfig } from './config.js';
import { isPlainObject, isTursoSessionBackend, pathExists, writeJsonFile } from './config.js';
import { SyncCommandError } from './errors.js';
import type { SyncLocations } from './paths.js';
import { pathForLocations, type SyncLocations } from './paths.js';

const SESSION_SYNC_TABLE = 'opencode_session_sync_snapshot';
const CREDENTIAL_VERSION = 1;
Expand Down Expand Up @@ -560,16 +560,24 @@ export function createTursoSessionBackend(options: {
}

export function resolveTursoCredentialPath(locations: SyncLocations): string {
return path.join(locations.xdg.dataDir, 'opencode', 'opencode-synced', 'turso-session.json');
return pathForLocations(locations).join(
locations.xdg.dataDir,
'opencode',
'opencode-synced',
'turso-session.json'
);
}

export function resolveSessionDbPaths(locations: SyncLocations): {
dbPath: string;
walPath: string;
shmPath: string;
} {
const dataRoot = path.join(locations.xdg.dataDir, 'opencode');
const dbPath = path.join(dataRoot, 'opencode.db');
const dbPath = pathForLocations(locations).join(
locations.xdg.dataDir,
'opencode',
'opencode.db'
);
return {
dbPath,
walPath: `${dbPath}-wal`,
Expand Down
Loading