Skip to content

Commit 6ebd8de

Browse files
committed
Merge main into PR #8 branch - resolve conflicts
2 parents f0d0513 + 56fc8f2 commit 6ebd8de

File tree

11 files changed

+756
-228
lines changed

11 files changed

+756
-228
lines changed

src/config.ts

Lines changed: 165 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@ import * as os from "os";
55
import * as jsonc from "jsonc-parser";
66
import type { WorkspaceMetadata } from "./types/workspace";
77

8-
export const CONFIG_DIR = path.join(os.homedir(), ".cmux");
9-
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
10-
const PROVIDERS_FILE = path.join(CONFIG_DIR, "providers.jsonc");
11-
export const SESSIONS_DIR = path.join(CONFIG_DIR, "sessions");
12-
138
export interface Workspace {
149
branch: string;
1510
path: string;
@@ -20,7 +15,7 @@ export interface ProjectConfig {
2015
workspaces: Workspace[];
2116
}
2217

23-
export interface Config {
18+
export interface ProjectsConfig {
2419
projects: Map<string, ProjectConfig>;
2520
}
2621

@@ -32,164 +27,187 @@ export interface ProviderConfig {
3227

3328
export type ProvidersConfig = Record<string, ProviderConfig>;
3429

35-
export function load_config_or_default(): Config {
36-
try {
37-
if (fs.existsSync(CONFIG_FILE)) {
38-
const data = fs.readFileSync(CONFIG_FILE, "utf-8");
39-
const parsed = JSON.parse(data) as { projects?: unknown };
40-
41-
// Config is stored as array of [path, config] pairs
42-
if (parsed.projects && Array.isArray(parsed.projects)) {
43-
const projectsMap = new Map<string, ProjectConfig>(
44-
parsed.projects as Array<[string, ProjectConfig]>
45-
);
46-
return {
47-
projects: projectsMap,
48-
};
49-
}
50-
}
51-
} catch (error) {
52-
console.error("Error loading config:", error);
30+
/**
31+
* Config - Centralized configuration management
32+
*
33+
* Encapsulates all config paths and operations, making them dependency-injectable
34+
* and testable. Pass a custom rootDir for tests to avoid polluting ~/.cmux
35+
*/
36+
export class Config {
37+
readonly rootDir: string;
38+
readonly sessionsDir: string;
39+
readonly srcDir: string;
40+
private readonly configFile: string;
41+
private readonly providersFile: string;
42+
43+
constructor(rootDir?: string) {
44+
this.rootDir = rootDir ?? path.join(os.homedir(), ".cmux");
45+
this.sessionsDir = path.join(this.rootDir, "sessions");
46+
this.srcDir = path.join(this.rootDir, "src");
47+
this.configFile = path.join(this.rootDir, "config.json");
48+
this.providersFile = path.join(this.rootDir, "providers.jsonc");
5349
}
5450

55-
// Return default config
56-
return {
57-
projects: new Map(),
58-
};
59-
}
60-
61-
export function save_config(config: Config): void {
62-
try {
63-
if (!fs.existsSync(CONFIG_DIR)) {
64-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
51+
loadConfigOrDefault(): ProjectsConfig {
52+
try {
53+
if (fs.existsSync(this.configFile)) {
54+
const data = fs.readFileSync(this.configFile, "utf-8");
55+
const parsed = JSON.parse(data) as { projects?: unknown };
56+
57+
// Config is stored as array of [path, config] pairs
58+
if (parsed.projects && Array.isArray(parsed.projects)) {
59+
const projectsMap = new Map<string, ProjectConfig>(
60+
parsed.projects as Array<[string, ProjectConfig]>
61+
);
62+
return {
63+
projects: projectsMap,
64+
};
65+
}
66+
}
67+
} catch (error) {
68+
console.error("Error loading config:", error);
6569
}
6670

67-
const data = {
68-
projects: Array.from(config.projects.entries()),
71+
// Return default config
72+
return {
73+
projects: new Map(),
6974
};
70-
71-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
72-
} catch (error) {
73-
console.error("Error saving config:", error);
7475
}
75-
}
76-
77-
export function getProjectName(projectPath: string): string {
78-
return projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "unknown";
79-
}
8076

81-
export function getWorkspacePath(projectPath: string, branch: string): string {
82-
const projectName = getProjectName(projectPath);
83-
return path.join(CONFIG_DIR, "src", projectName, branch);
84-
}
85-
86-
/**
87-
* Find a workspace path by project name and branch
88-
* @returns The workspace path or null if not found
89-
*/
90-
export function findWorkspacePath(projectName: string, branch: string): string | null {
91-
const config = load_config_or_default();
77+
saveConfig(config: ProjectsConfig): void {
78+
try {
79+
if (!fs.existsSync(this.rootDir)) {
80+
fs.mkdirSync(this.rootDir, { recursive: true });
81+
}
9282

93-
for (const [projectPath, project] of config.projects) {
94-
const currentProjectName = path.basename(projectPath);
83+
const data = {
84+
projects: Array.from(config.projects.entries()),
85+
};
9586

96-
if (currentProjectName === projectName) {
97-
const workspace = project.workspaces.find((w: Workspace) => w.branch === branch);
98-
if (workspace) {
99-
return workspace.path;
100-
}
87+
fs.writeFileSync(this.configFile, JSON.stringify(data, null, 2));
88+
} catch (error) {
89+
console.error("Error saving config:", error);
10190
}
10291
}
10392

104-
return null;
105-
}
106-
107-
/**
108-
* WARNING: Never try to derive workspace path from workspace ID!
109-
* This is a code smell that leads to bugs.
110-
*
111-
* The workspace path should always:
112-
* 1. Be stored in WorkspaceMetadata when the workspace is created
113-
* 2. Be retrieved from WorkspaceMetadata when needed
114-
* 3. Be passed through the call stack explicitly
115-
*
116-
* Parsing workspaceId strings to derive paths is fragile and error-prone.
117-
* The workspace path is established when the git worktree is created,
118-
* and that canonical path should be preserved and used throughout.
119-
*/
93+
private getProjectName(projectPath: string): string {
94+
return projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "unknown";
95+
}
12096

121-
/**
122-
* Get the session directory for a specific workspace
123-
*/
124-
export function getSessionDir(workspaceId: string): string {
125-
return path.join(SESSIONS_DIR, workspaceId);
126-
}
97+
getWorkspacePath(projectPath: string, branch: string): string {
98+
const projectName = this.getProjectName(projectPath);
99+
return path.join(this.srcDir, projectName, branch);
100+
}
127101

128-
/**
129-
* Get all workspace metadata by scanning sessions directory and loading metadata files
130-
* This centralizes the logic for workspace discovery and metadata loading
131-
*/
132-
export async function getAllWorkspaceMetadata(): Promise<
133-
Array<{ workspaceId: string; metadata: WorkspaceMetadata }>
134-
> {
135-
try {
136-
// Scan sessions directory for workspace directories
137-
await fsPromises.access(SESSIONS_DIR);
138-
const entries = await fsPromises.readdir(SESSIONS_DIR, { withFileTypes: true });
139-
const workspaceIds = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
140-
141-
const workspaceMetadata: Array<{ workspaceId: string; metadata: WorkspaceMetadata }> = [];
142-
143-
for (const workspaceId of workspaceIds) {
144-
try {
145-
const metadataPath = path.join(getSessionDir(workspaceId), "metadata.json");
146-
const data = await fsPromises.readFile(metadataPath, "utf-8");
147-
const metadata = JSON.parse(data) as WorkspaceMetadata;
148-
workspaceMetadata.push({ workspaceId, metadata });
149-
} catch (error) {
150-
// Skip workspaces with missing or invalid metadata
151-
console.warn(`Failed to load metadata for workspace ${workspaceId}:`, error);
102+
/**
103+
* Find a workspace path by project name and branch
104+
* @returns The workspace path or null if not found
105+
*/
106+
findWorkspacePath(projectName: string, branch: string): string | null {
107+
const config = this.loadConfigOrDefault();
108+
109+
for (const [projectPath, project] of config.projects) {
110+
const currentProjectName = path.basename(projectPath);
111+
112+
if (currentProjectName === projectName) {
113+
const workspace = project.workspaces.find((w: Workspace) => w.branch === branch);
114+
if (workspace) {
115+
return workspace.path;
116+
}
152117
}
153118
}
154119

155-
return workspaceMetadata;
156-
} catch {
157-
return []; // Sessions directory doesn't exist yet
120+
return null;
158121
}
159-
}
160122

161-
/**
162-
* Load providers configuration from JSONC file
163-
* Supports comments in JSONC format
164-
*/
165-
export function loadProvidersConfig(): ProvidersConfig | null {
166-
try {
167-
if (fs.existsSync(PROVIDERS_FILE)) {
168-
const data = fs.readFileSync(PROVIDERS_FILE, "utf-8");
169-
return jsonc.parse(data) as ProvidersConfig;
170-
}
171-
} catch (error) {
172-
console.error("Error loading providers config:", error);
123+
/**
124+
* WARNING: Never try to derive workspace path from workspace ID!
125+
* This is a code smell that leads to bugs.
126+
*
127+
* The workspace path should always:
128+
* 1. Be stored in WorkspaceMetadata when the workspace is created
129+
* 2. Be retrieved from WorkspaceMetadata when needed
130+
* 3. Be passed through the call stack explicitly
131+
*
132+
* Parsing workspaceId strings to derive paths is fragile and error-prone.
133+
* The workspace path is established when the git worktree is created,
134+
* and that canonical path should be preserved and used throughout.
135+
*/
136+
137+
/**
138+
* Get the session directory for a specific workspace
139+
*/
140+
getSessionDir(workspaceId: string): string {
141+
return path.join(this.sessionsDir, workspaceId);
173142
}
174143

175-
return null;
176-
}
144+
/**
145+
* Get all workspace metadata by scanning sessions directory and loading metadata files
146+
* This centralizes the logic for workspace discovery and metadata loading
147+
*/
148+
async getAllWorkspaceMetadata(): Promise<
149+
Array<{ workspaceId: string; metadata: WorkspaceMetadata }>
150+
> {
151+
try {
152+
// Scan sessions directory for workspace directories
153+
await fsPromises.access(this.sessionsDir);
154+
const entries = await fsPromises.readdir(this.sessionsDir, { withFileTypes: true });
155+
const workspaceIds = entries
156+
.filter((entry) => entry.isDirectory())
157+
.map((entry) => entry.name);
158+
159+
const workspaceMetadata: Array<{ workspaceId: string; metadata: WorkspaceMetadata }> = [];
160+
161+
for (const workspaceId of workspaceIds) {
162+
try {
163+
const metadataPath = path.join(this.getSessionDir(workspaceId), "metadata.json");
164+
const data = await fsPromises.readFile(metadataPath, "utf-8");
165+
const metadata = JSON.parse(data) as WorkspaceMetadata;
166+
workspaceMetadata.push({ workspaceId, metadata });
167+
} catch (error) {
168+
// Skip workspaces with missing or invalid metadata
169+
console.warn(`Failed to load metadata for workspace ${workspaceId}:`, error);
170+
}
171+
}
177172

178-
/**
179-
* Save providers configuration to JSONC file
180-
* @param config The providers configuration to save
181-
*/
182-
export function saveProvidersConfig(config: ProvidersConfig): void {
183-
try {
184-
if (!fs.existsSync(CONFIG_DIR)) {
185-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
173+
return workspaceMetadata;
174+
} catch {
175+
return []; // Sessions directory doesn't exist yet
176+
}
177+
}
178+
179+
/**
180+
* Load providers configuration from JSONC file
181+
* Supports comments in JSONC format
182+
*/
183+
loadProvidersConfig(): ProvidersConfig | null {
184+
try {
185+
if (fs.existsSync(this.providersFile)) {
186+
const data = fs.readFileSync(this.providersFile, "utf-8");
187+
return jsonc.parse(data) as ProvidersConfig;
188+
}
189+
} catch (error) {
190+
console.error("Error loading providers config:", error);
186191
}
187192

188-
// Format with 2-space indentation for readability
189-
const jsonString = JSON.stringify(config, null, 2);
193+
return null;
194+
}
195+
196+
/**
197+
* Save providers configuration to JSONC file
198+
* @param config The providers configuration to save
199+
*/
200+
saveProvidersConfig(config: ProvidersConfig): void {
201+
try {
202+
if (!fs.existsSync(this.rootDir)) {
203+
fs.mkdirSync(this.rootDir, { recursive: true });
204+
}
190205

191-
// Add a comment header to the file
192-
const contentWithComments = `// Providers configuration for cmux
206+
// Format with 2-space indentation for readability
207+
const jsonString = JSON.stringify(config, null, 2);
208+
209+
// Add a comment header to the file
210+
const contentWithComments = `// Providers configuration for cmux
193211
// Configure your AI providers here
194212
// Example:
195213
// {
@@ -200,9 +218,13 @@ export function saveProvidersConfig(config: ProvidersConfig): void {
200218
// }
201219
${jsonString}`;
202220

203-
fs.writeFileSync(PROVIDERS_FILE, contentWithComments);
204-
} catch (error) {
205-
console.error("Error saving providers config:", error);
206-
throw error; // Re-throw to let caller handle
221+
fs.writeFileSync(this.providersFile, contentWithComments);
222+
} catch (error) {
223+
console.error("Error saving providers config:", error);
224+
throw error; // Re-throw to let caller handle
225+
}
207226
}
208227
}
228+
229+
// Default instance for application use
230+
export const defaultConfig = new Config();

src/debug/costs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as fs from "fs";
22
import * as path from "path";
3-
import { getSessionDir } from "../config";
3+
import { defaultConfig } from "../config";
44
import type { CmuxMessage } from "../types/message";
55
import { calculateTokenStats } from "../utils/tokenStatsCalculator";
66

@@ -12,7 +12,7 @@ export async function costsCommand(workspaceId: string) {
1212
console.log(`\n=== Cost Statistics for workspace: ${workspaceId} ===\n`);
1313

1414
// Load chat history
15-
const sessionDir = getSessionDir(workspaceId);
15+
const sessionDir = defaultConfig.getSessionDir(workspaceId);
1616
const chatHistoryPath = path.join(sessionDir, "chat.jsonl");
1717

1818
if (!fs.existsSync(chatHistoryPath)) {

src/debug/list-workspaces.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { load_config_or_default, findWorkspacePath } from "../config";
1+
import { defaultConfig } from "../config";
22
import * as path from "path";
33
import * as fs from "fs";
44

55
export function listWorkspacesCommand() {
6-
const config = load_config_or_default();
6+
const config = defaultConfig.loadConfigOrDefault();
77

88
console.log("\n=== Configuration Debug ===\n");
99
console.log("Projects in config:", config.projects.size);
@@ -32,7 +32,7 @@ export function listWorkspacesCommand() {
3232
];
3333

3434
for (const test of testCases) {
35-
const result = findWorkspacePath(test.project, test.branch);
35+
const result = defaultConfig.findWorkspacePath(test.project, test.branch);
3636
console.log(`findWorkspacePath('${test.project}', '${test.branch}'):`);
3737
if (result) {
3838
console.log(` Found: ${result}`);

0 commit comments

Comments
 (0)