Skip to content

Commit 9b5c611

Browse files
committed
refactor: split ode config store and channel accessors
1 parent 2938c94 commit 9b5c611

File tree

3 files changed

+371
-327
lines changed

3 files changed

+371
-327
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { normalizeCwd } from "../paths";
2+
import { isAgentProviderId } from "@/shared/agent-provider";
3+
import {
4+
type AgentProvider,
5+
type ChannelDetail,
6+
type WorkspaceConfig,
7+
} from "./ode-schema";
8+
import {
9+
loadOdeConfig,
10+
normalizeBaseBranch,
11+
updateOdeConfig,
12+
} from "./ode-store";
13+
14+
export type ChannelCwdInfo = {
15+
cwd: string;
16+
workingDirectory: string | null;
17+
hasCustomCwd: boolean;
18+
};
19+
20+
export function getDefaultCwd(): string {
21+
return normalizeCwd(process.cwd());
22+
}
23+
24+
function getWorkspaces(): WorkspaceConfig[] {
25+
return loadOdeConfig().workspaces;
26+
}
27+
28+
export function getChannelDetails(channelId: string): ChannelDetail | null {
29+
for (const workspace of getWorkspaces()) {
30+
const match = workspace.channelDetails.find((channel) => channel.id === channelId);
31+
if (match) return match;
32+
}
33+
return null;
34+
}
35+
36+
export function resolveChannelCwd(channelId: string): ChannelCwdInfo {
37+
const channel = getChannelDetails(channelId);
38+
const workingDirectory = channel?.workingDirectory?.trim();
39+
const normalized = workingDirectory && workingDirectory.length > 0
40+
? normalizeCwd(workingDirectory)
41+
: null;
42+
return {
43+
cwd: normalized ?? getDefaultCwd(),
44+
workingDirectory: normalized,
45+
hasCustomCwd: Boolean(normalized),
46+
};
47+
}
48+
49+
export function setChannelCwd(channelId: string, cwd: string): void {
50+
updateChannel(channelId, (channel) => ({
51+
...channel,
52+
workingDirectory: normalizeCwd(cwd),
53+
}));
54+
}
55+
56+
export function setChannelWorkingDirectory(channelId: string, workingDirectory: string | null): void {
57+
const normalized = workingDirectory && workingDirectory.trim().length > 0
58+
? normalizeCwd(workingDirectory)
59+
: "";
60+
updateChannel(channelId, (channel) => ({
61+
...channel,
62+
workingDirectory: normalized,
63+
}));
64+
}
65+
66+
export function getChannelBaseBranch(channelId: string): string {
67+
return normalizeBaseBranch(getChannelDetails(channelId)?.baseBranch);
68+
}
69+
70+
export function setChannelBaseBranch(channelId: string, baseBranch: string | null): void {
71+
const normalized = normalizeBaseBranch(baseBranch);
72+
updateChannel(channelId, (channel) => ({
73+
...channel,
74+
baseBranch: normalized,
75+
}));
76+
}
77+
78+
export function getChannelSystemMessage(channelId: string): string | null {
79+
return getChannelDetails(channelId)?.channelSystemMessage ?? null;
80+
}
81+
82+
export function setChannelSystemMessage(channelId: string, channelSystemMessage: string | null): void {
83+
const normalized = channelSystemMessage?.trim() ?? "";
84+
updateChannel(channelId, (channel) => ({
85+
...channel,
86+
channelSystemMessage: normalized,
87+
}));
88+
}
89+
90+
export function getChannelModel(channelId: string): string | null {
91+
return getChannelDetails(channelId)?.model ?? null;
92+
}
93+
94+
export function getChannelAgentProvider(channelId: string): AgentProvider {
95+
const provider = getChannelDetails(channelId)?.agentProvider;
96+
return isAgentProviderId(provider) ? provider : "opencode";
97+
}
98+
99+
export function setChannelModel(channelId: string, model: string): void {
100+
updateChannel(channelId, (channel) => ({ ...channel, model }));
101+
}
102+
103+
export function setChannelAgentProvider(
104+
channelId: string,
105+
agentProvider: AgentProvider
106+
): void {
107+
updateChannel(channelId, (channel) => ({ ...channel, agentProvider }));
108+
}
109+
110+
function updateChannel(
111+
channelId: string,
112+
updater: (channel: ChannelDetail) => ChannelDetail
113+
): void {
114+
let updated = false;
115+
updateOdeConfig((config) => {
116+
const workspaces = config.workspaces.map((workspace) => {
117+
const channelDetails = workspace.channelDetails.map((channel) => {
118+
if (channel.id !== channelId) return channel;
119+
updated = true;
120+
return updater(channel);
121+
});
122+
return { ...workspace, channelDetails };
123+
});
124+
125+
if (!updated) {
126+
throw new Error("Channel not found in ~/.config/ode/ode.json");
127+
}
128+
129+
return { ...config, workspaces };
130+
});
131+
}

packages/config/local/ode-store.ts

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import * as fs from "fs";
2+
import * as os from "os";
3+
import * as path from "path";
4+
import { DEFAULT_STATUS_MESSAGE_FREQUENCY_MS } from "../status-message-frequency";
5+
import {
6+
odeConfigSchema,
7+
type OdeConfig,
8+
} from "./ode-schema";
9+
10+
const existsSync = fs.existsSync;
11+
const mkdirSync = fs.mkdirSync;
12+
const readFileSync = fs.readFileSync;
13+
const writeFileSync = fs.writeFileSync;
14+
const join = typeof path.join === "function" ? path.join : (...parts: string[]) => parts.join("/");
15+
const homedir = typeof os.homedir === "function" ? os.homedir : () => "";
16+
17+
const XDG_CONFIG_HOME = join(homedir(), ".config");
18+
const ODE_CONFIG_DIR = join(XDG_CONFIG_HOME, "ode");
19+
export const ODE_CONFIG_FILE = join(ODE_CONFIG_DIR, "ode.json");
20+
21+
const DEFAULT_UPDATE_INTERVAL_MS = 60 * 60 * 1000;
22+
const MIN_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
23+
const DEFAULT_MESSAGE_UPDATE_INTERVAL_MS = DEFAULT_STATUS_MESSAGE_FREQUENCY_MS;
24+
const MIN_MESSAGE_UPDATE_INTERVAL_MS = 250;
25+
26+
let cachedConfig: OdeConfig | null = null;
27+
28+
const EMPTY_TEMPLATE: OdeConfig = {
29+
user: {
30+
name: "",
31+
email: "",
32+
initials: "",
33+
avatar: "",
34+
gitStrategy: "worktree",
35+
defaultStatusMessageFormat: "medium",
36+
IM_MESSAGE_UPDATE_INTERVAL_MS: DEFAULT_STATUS_MESSAGE_FREQUENCY_MS,
37+
},
38+
githubInfos: {},
39+
agents: {
40+
opencode: { enabled: true, models: [] },
41+
claudecode: { enabled: true },
42+
codex: { enabled: true, models: [] },
43+
kimi: { enabled: true },
44+
kiro: { enabled: true },
45+
kilo: { enabled: true, models: [] },
46+
qwen: { enabled: true },
47+
goose: { enabled: true },
48+
gemini: { enabled: true },
49+
},
50+
completeOnboarding: false,
51+
workspaces: [],
52+
updates: {
53+
autoUpgrade: true,
54+
checkIntervalMs: DEFAULT_UPDATE_INTERVAL_MS,
55+
},
56+
};
57+
58+
function ensureConfigDir(): void {
59+
if (!existsSync(ODE_CONFIG_DIR)) {
60+
mkdirSync(ODE_CONFIG_DIR, { recursive: true });
61+
}
62+
}
63+
64+
function ensureConfigFile(): void {
65+
if (existsSync(ODE_CONFIG_FILE)) return;
66+
ensureConfigDir();
67+
writeFileSync(ODE_CONFIG_FILE, JSON.stringify(EMPTY_TEMPLATE, null, 2));
68+
}
69+
70+
export function normalizeBaseBranch(baseBranch: string | null | undefined): string {
71+
const normalized = baseBranch?.trim();
72+
return normalized && normalized.length > 0 ? normalized : "main";
73+
}
74+
75+
function normalizeConfig(config: OdeConfig): OdeConfig {
76+
const {
77+
defaultMessageFrequency: _deprecatedMessageFrequency,
78+
messageUpdateIntervalMs: _deprecatedMessageUpdateIntervalMs,
79+
...normalizedUser
80+
} = config.user;
81+
const statusMessageFormat = config.user.defaultStatusMessageFormat
82+
?? config.user.defaultMessageFrequency
83+
?? "medium";
84+
const normalizedFrequency =
85+
statusMessageFormat === "low"
86+
? "minimum"
87+
: statusMessageFormat === "high"
88+
? "aggressive"
89+
: statusMessageFormat;
90+
const normalizedGitStrategy =
91+
config.user.gitStrategy === "default" ? "default" : "worktree";
92+
const messageUpdateIntervalCandidate =
93+
config.user.IM_MESSAGE_UPDATE_INTERVAL_MS
94+
?? config.user.messageUpdateIntervalMs
95+
?? DEFAULT_MESSAGE_UPDATE_INTERVAL_MS;
96+
const normalizedMessageUpdateInterval =
97+
Number.isFinite(messageUpdateIntervalCandidate) && messageUpdateIntervalCandidate > 0
98+
? Math.max(messageUpdateIntervalCandidate, MIN_MESSAGE_UPDATE_INTERVAL_MS)
99+
: DEFAULT_MESSAGE_UPDATE_INTERVAL_MS;
100+
const intervalCandidate = config.updates?.checkIntervalMs ?? DEFAULT_UPDATE_INTERVAL_MS;
101+
const normalizedInterval =
102+
Number.isFinite(intervalCandidate) && intervalCandidate > 0
103+
? Math.max(intervalCandidate, MIN_UPDATE_INTERVAL_MS)
104+
: DEFAULT_UPDATE_INTERVAL_MS;
105+
const autoUpgrade = config.updates?.autoUpgrade ?? true;
106+
const opencodeModels = Array.from(new Set((config.agents?.opencode?.models ?? [])
107+
.map((model) => model.trim())
108+
.filter(Boolean)));
109+
const codexModels = Array.from(new Set((config.agents?.codex?.models ?? [])
110+
.map((model) => model.trim())
111+
.filter(Boolean)));
112+
const kiloModels = Array.from(new Set((config.agents?.kilo?.models ?? [])
113+
.map((model) => model.trim())
114+
.filter(Boolean)));
115+
const completeOnboarding = config.completeOnboarding === true;
116+
const workspaces = config.workspaces.map((workspace) => ({
117+
...workspace,
118+
type:
119+
workspace.type === "discord"
120+
? "discord" as const
121+
: workspace.type === "lark"
122+
? "lark" as const
123+
: "slack" as const,
124+
channelDetails: workspace.channelDetails.map((channel) => ({
125+
...channel,
126+
baseBranch: normalizeBaseBranch(channel.baseBranch),
127+
})),
128+
}));
129+
return {
130+
...config,
131+
user: {
132+
...normalizedUser,
133+
gitStrategy: normalizedGitStrategy,
134+
defaultStatusMessageFormat: normalizedFrequency,
135+
IM_MESSAGE_UPDATE_INTERVAL_MS: normalizedMessageUpdateInterval,
136+
},
137+
updates: {
138+
autoUpgrade,
139+
checkIntervalMs: normalizedInterval,
140+
},
141+
agents: {
142+
opencode: {
143+
enabled: config.agents?.opencode?.enabled ?? true,
144+
models: opencodeModels,
145+
},
146+
claudecode: {
147+
enabled: config.agents?.claudecode?.enabled ?? true,
148+
},
149+
codex: {
150+
enabled: config.agents?.codex?.enabled ?? true,
151+
models: codexModels,
152+
},
153+
kimi: {
154+
enabled: config.agents?.kimi?.enabled ?? true,
155+
},
156+
kiro: {
157+
enabled: config.agents?.kiro?.enabled ?? true,
158+
},
159+
kilo: {
160+
enabled: config.agents?.kilo?.enabled ?? true,
161+
models: kiloModels,
162+
},
163+
qwen: {
164+
enabled: config.agents?.qwen?.enabled ?? true,
165+
},
166+
goose: {
167+
enabled: config.agents?.goose?.enabled ?? true,
168+
},
169+
gemini: {
170+
enabled: config.agents?.gemini?.enabled ?? true,
171+
},
172+
},
173+
completeOnboarding,
174+
workspaces,
175+
};
176+
}
177+
178+
export function loadOdeConfig(): OdeConfig {
179+
if (cachedConfig) return cachedConfig;
180+
181+
ensureConfigFile();
182+
183+
if (!existsSync(ODE_CONFIG_FILE)) {
184+
cachedConfig = normalizeConfig(EMPTY_TEMPLATE);
185+
return cachedConfig;
186+
}
187+
188+
try {
189+
const raw = readFileSync(ODE_CONFIG_FILE, "utf-8");
190+
const parsedJson = JSON.parse(raw) as Record<string, unknown>;
191+
const parsed = odeConfigSchema.safeParse(parsedJson);
192+
const base = parsed.success ? parsed.data : EMPTY_TEMPLATE;
193+
cachedConfig = normalizeConfig(base);
194+
return cachedConfig;
195+
} catch {
196+
cachedConfig = normalizeConfig(EMPTY_TEMPLATE);
197+
return cachedConfig;
198+
}
199+
}
200+
201+
export function invalidateOdeConfigCache(): void {
202+
cachedConfig = null;
203+
}
204+
205+
export function saveOdeConfig(config: OdeConfig): void {
206+
ensureConfigDir();
207+
cachedConfig = normalizeConfig(config);
208+
writeFileSync(ODE_CONFIG_FILE, JSON.stringify(cachedConfig, null, 2));
209+
}
210+
211+
export function updateOdeConfig(updater: (config: OdeConfig) => OdeConfig): OdeConfig {
212+
const next = updater(structuredClone(loadOdeConfig()));
213+
saveOdeConfig(next);
214+
return loadOdeConfig();
215+
}

0 commit comments

Comments
 (0)