Simplified path handling part 1

This commit is contained in:
thatben 2025-04-21 10:41:44 +02:00
parent 29ceec8281
commit 280b00802b
No known key found for this signature in database
GPG Key ID: 62C543548433D43E
15 changed files with 347 additions and 259 deletions

View File

@ -13,8 +13,10 @@ export const mockUiService = {
export const mockConfigService = {
get: vi.fn(),
saveConfig: vi.fn(),
getCodexExe: vi.fn(),
getCodexConfigFilePath: vi.fn(),
loadConfig: vi.fn(),
saveConfig: vi.fn(),
writeCodexConfigFile: vi.fn(),
};
@ -25,11 +27,12 @@ export const mockFsService = {
isFile: vi.fn(),
readDir: vi.fn(),
makeDir: vi.fn(),
moveDir: vi.fn(),
deleteDir: vi.fn(),
readJsonFile: vi.fn(),
writeJsonFile: vi.fn(),
writeFile: vi.fn(),
toRelativePath: vi.fn(),
ensureDirExists: vi.fn(),
};
export const mockShellService = {
@ -43,6 +46,7 @@ export const mockOsService = {
isLinux: vi.fn(),
getWorkingDir: vi.fn(),
listProcesses: vi.fn(),
stopProcess: vi.fn(),
};
export const mockCodexGlobals = {

View File

@ -12,7 +12,6 @@ import {
showSuccessMessage,
} from "../utils/messages.js";
import { checkDependencies } from "../services/nodeService.js";
import { getCodexRootPath, getCodexBinPath } from "../utils/appData.js";
const platform = os.platform();

View File

@ -17,14 +17,15 @@ export class Installer {
};
getCodexVersion = async () => {
if (this.config.codexExe.length < 1)
throw new Error("Codex not installed.");
const version = await this.shell.run(`"${this.config.codexExe}" --version`);
const codexExe = this.configService.getCodexExe();
if (!this.fs.isFile(codexExe)) throw new Error("Codex not installed.");
const version = await this.shell.run(`"${codexExe}" --version`);
if (version.length < 1) throw new Error("Version info not found.");
return version;
};
installCodex = async (processCallbacks) => {
this.fs.ensureDirExists(this.config.codexRoot);
if (!(await this.arePrerequisitesCorrect(processCallbacks))) return;
processCallbacks.installStarts();
@ -34,14 +35,15 @@ export class Installer {
await this.installCodexUnix(processCallbacks);
}
if (!(await this.isCodexInstalled()))
if (!(await this.isCodexInstalled())) {
processCallbacks.warn("Codex failed to install.");
throw new Error("Codex installation failed.");
}
processCallbacks.installSuccessful();
};
uninstallCodex = () => {
this.fs.deleteDir(this.config.codexInstallPath);
this.fs.deleteDir(this.config.dataDir);
this.fs.deleteDir(this.config.codexRoot);
};
arePrerequisitesCorrect = async (processCallbacks) => {
@ -49,8 +51,8 @@ export class Installer {
processCallbacks.warn("Codex is already installed.");
return false;
}
if (this.config.codexInstallPath.length < 1) {
processCallbacks.warn("Install path not set.");
if (!this.fs.isDir(this.config.codexRoot)) {
processCallbacks.warn("Root path doesn't exist.");
return false;
}
if (!(await this.isCurlAvailable())) {
@ -71,10 +73,9 @@ export class Installer {
);
processCallbacks.downloadSuccessful();
await this.shell.run(
`set "INSTALL_DIR=${this.config.codexInstallPath}" && ` +
`set "INSTALL_DIR=${this.config.codexRoot}" && ` +
`"${this.os.getWorkingDir()}\\install.cmd"`,
);
await this.saveCodexInstallPath("codex.exe");
await this.shell.run("del /f install.cmd");
};
@ -90,8 +91,6 @@ export class Installer {
} else {
await this.runInstallerLinux();
}
await this.saveCodexInstallPath("codex");
await this.shell.run("rm -f install.sh");
};
@ -100,7 +99,7 @@ export class Installer {
eval {
local $SIG{ALRM} = sub { die "timeout\\n" };
alarm(120);
system("INSTALL_DIR=\\"${this.config.codexInstallPath}\\" bash install.sh");
system("INSTALL_DIR=\\"${this.config.codexRoot}\\" bash install.sh");
alarm(0);
};
die if $@;
@ -110,7 +109,7 @@ export class Installer {
runInstallerLinux = async () => {
await this.shell.run(
`INSTALL_DIR="${this.config.codexInstallPath}" timeout 120 bash install.sh`,
`INSTALL_DIR="${this.config.codexRoot}" timeout 120 bash install.sh`,
);
};
@ -122,14 +121,4 @@ export class Installer {
}
return true;
};
saveCodexInstallPath = async (codexExe) => {
this.config.codexExe = this.fs.pathJoin([
this.config.codexInstallPath,
codexExe,
]);
if (!this.fs.isFile(this.config.codexExe))
throw new Error("Codex executable not found.");
await this.configService.saveConfig();
};
}

View File

@ -9,9 +9,10 @@ import { Installer } from "./installer.js";
describe("Installer", () => {
const config = {
codexInstallPath: "/install-codex",
codexRoot: "/codex-root",
};
const workingDir = "/working-dir";
const exe = "abc.exe";
const processCallbacks = {
installStarts: vi.fn(),
downloadSuccessful: vi.fn(),
@ -24,6 +25,7 @@ describe("Installer", () => {
vi.resetAllMocks();
mockConfigService.get.mockReturnValue(config);
mockOsService.getWorkingDir.mockReturnValue(workingDir);
mockConfigService.getCodexExe.mockReturnValue(exe);
installer = new Installer(
mockConfigService,
@ -34,15 +36,22 @@ describe("Installer", () => {
});
describe("getCodexVersion", () => {
it("throws when codex exe is not set", async () => {
config.codexExe = "";
it("checks if the codex exe file exists", async () => {
mockFsService.isFile.mockReturnValue(true);
mockShellService.run.mockResolvedValueOnce("a");
await installer.getCodexVersion();
expect(mockFsService.isFile).toHaveBeenCalledWith(exe);
});
it("throws when codex exe is not a file", async () => {
mockFsService.isFile.mockReturnValue(false);
await expect(installer.getCodexVersion()).rejects.toThrow(
"Codex not installed.",
);
});
it("throws when version info is not found", async () => {
config.codexExe = "codex.exe";
mockFsService.isFile.mockReturnValue(true);
mockShellService.run.mockResolvedValueOnce("");
await expect(installer.getCodexVersion()).rejects.toThrow(
"Version info not found.",
@ -50,8 +59,8 @@ describe("Installer", () => {
});
it("returns version info", async () => {
mockFsService.isFile.mockReturnValue(true);
const versionInfo = "versionInfo";
config.codexExe = "codex.exe";
mockShellService.run.mockResolvedValueOnce(versionInfo);
const version = await installer.getCodexVersion();
expect(version).toBe(versionInfo);
@ -80,6 +89,15 @@ describe("Installer", () => {
installer.isCodexInstalled = vi.fn();
});
it("ensures codex root dir exists", async () => {
installer.arePrerequisitesCorrect.mockResolvedValue(false);
await installer.installCodex(processCallbacks);
expect(mockFsService.ensureDirExists).toHaveBeenCalledWith(
config.codexRoot,
);
});
it("returns early when prerequisites are not correct", async () => {
installer.arePrerequisitesCorrect.mockResolvedValue(false);
await installer.installCodex(processCallbacks);
@ -125,6 +143,16 @@ describe("Installer", () => {
);
});
it("warns user when codex is not installed after installation", async () => {
installer.isCodexInstalled.mockResolvedValue(false);
await expect(installer.installCodex(processCallbacks)).rejects.toThrow(
"Codex installation failed.",
);
expect(processCallbacks.warn).toHaveBeenCalledWith(
"Codex failed to install.",
);
});
it("calls installSuccessful when installation is successful", async () => {
await installer.installCodex(processCallbacks);
expect(processCallbacks.installSuccessful).toHaveBeenCalled();
@ -136,7 +164,6 @@ describe("Installer", () => {
beforeEach(() => {
installer.isCodexInstalled = vi.fn();
installer.isCurlAvailable = vi.fn();
config.codexInstallPath = "/install-codex";
});
it("returns false when codex is already installed", async () => {
@ -149,18 +176,26 @@ describe("Installer", () => {
);
});
it("returns false when install path is not set", async () => {
config.codexInstallPath = "";
it("checks if the root path exists", async () => {
expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe(
false,
);
expect(mockFsService.isDir).toHaveBeenCalledWith(config.codexRoot);
});
it("returns false when root path does not exist", async () => {
mockFsService.isDir.mockReturnValue(false);
expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe(
false,
);
expect(processCallbacks.warn).toHaveBeenCalledWith(
"Install path not set.",
"Root path doesn't exist.",
);
});
it("returns false when curl is not available", async () => {
installer.isCodexInstalled.mockResolvedValue(false);
mockFsService.isDir.mockReturnValue(true);
installer.isCurlAvailable.mockResolvedValue(false);
expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe(
false,
@ -172,6 +207,7 @@ describe("Installer", () => {
it("returns true when all prerequisites are correct", async () => {
installer.isCodexInstalled.mockResolvedValue(false);
mockFsService.isDir.mockReturnValue(true);
installer.isCurlAvailable.mockResolvedValue(true);
const result = await installer.arePrerequisitesCorrect(processCallbacks);
expect(result).toBe(true);
@ -215,14 +251,7 @@ describe("Installer", () => {
it("runs installer script", async () => {
await installer.installCodexWindows(processCallbacks);
expect(mockShellService.run).toHaveBeenCalledWith(
`set "INSTALL_DIR=${config.codexInstallPath}" && "${workingDir}\\install.cmd"`,
);
});
it("saves the codex install path", async () => {
await installer.installCodexWindows(processCallbacks);
expect(installer.saveCodexInstallPath).toHaveBeenCalledWith(
"codex.exe",
`set "INSTALL_DIR=${config.codexRoot}" && "${workingDir}\\install.cmd"`,
);
});
@ -285,11 +314,6 @@ describe("Installer", () => {
expect(installer.runInstallerLinux).toHaveBeenCalled();
});
it("saves the codex install path", async () => {
await installer.installCodexUnix(processCallbacks);
expect(installer.saveCodexInstallPath).toHaveBeenCalledWith("codex");
});
it("deletes the installer script", async () => {
await installer.installCodexUnix(processCallbacks);
expect(mockShellService.run).toHaveBeenCalledWith("rm -f install.sh");
@ -303,7 +327,7 @@ describe("Installer", () => {
eval {
local $SIG{ALRM} = sub { die "timeout\\n" };
alarm(120);
system("INSTALL_DIR=\\"${config.codexInstallPath}\\" bash install.sh");
system("INSTALL_DIR=\\"${config.codexRoot}\\" bash install.sh");
alarm(0);
};
die if $@;
@ -317,7 +341,7 @@ describe("Installer", () => {
it("runs the installer script using unix timeout command", async () => {
await installer.runInstallerLinux();
expect(mockShellService.run).toHaveBeenCalledWith(
`INSTALL_DIR="${config.codexInstallPath}" timeout 120 bash install.sh`,
`INSTALL_DIR="${config.codexRoot}" timeout 120 bash install.sh`,
);
});
});
@ -351,56 +375,11 @@ describe("Installer", () => {
});
});
describe("saveCodexInstallPath", () => {
const codexExe = "_codex_.exe";
const pathJointResult = "/path-to-codex/_codex_.exe";
beforeEach(() => {
mockFsService.pathJoin.mockReturnValue(pathJointResult);
});
it("combines the install path with the exe", async () => {
mockFsService.isFile.mockReturnValue(true);
await installer.saveCodexInstallPath(codexExe);
expect(mockFsService.pathJoin).toHaveBeenCalledWith([
config.codexInstallPath,
codexExe,
]);
});
it("sets the codex exe path", async () => {
mockFsService.isFile.mockReturnValue(true);
await installer.saveCodexInstallPath(codexExe);
expect(config.codexExe).toBe(pathJointResult);
});
it("throws when file does not exist", async () => {
mockFsService.isFile.mockReturnValue(false);
await expect(installer.saveCodexInstallPath(codexExe)).rejects.toThrow(
"Codex executable not found.",
);
});
it("saves the config", async () => {
mockFsService.isFile.mockReturnValue(true);
await installer.saveCodexInstallPath(codexExe);
expect(mockConfigService.saveConfig).toHaveBeenCalled();
});
});
describe("uninstallCodex", () => {
it("deletes the codex install path", () => {
it("deletes the codex root path", () => {
installer.uninstallCodex();
expect(mockFsService.deleteDir).toHaveBeenCalledWith(
config.codexInstallPath,
);
});
it("deletes the codex data path", () => {
installer.uninstallCodex();
expect(mockFsService.deleteDir).toHaveBeenCalledWith(config.dataDir);
expect(mockFsService.deleteDir).toHaveBeenCalledWith(config.codexRoot);
});
});
});

View File

@ -1,7 +1,6 @@
export class ProcessControl {
constructor(configService, shellService, osService, fsService, codexGlobals) {
this.configService = configService;
this.config = configService.get();
this.shell = shellService;
this.os = osService;
this.fs = fsService;
@ -26,7 +25,7 @@ export class ProcessControl {
if (processes.length < 1) throw new Error("No codex process found");
const pid = processes[0].pid;
process.kill(pid, "SIGINT");
this.os.stopProcess(pid);
await this.sleep();
};
@ -51,8 +50,11 @@ export class ProcessControl {
};
startCodex = async () => {
const executable = this.config.codexExe;
const args = [`--config-file=${this.config.codexConfigFilePath}`];
await this.shell.spawnDetachedProcess(executable, args);
const executable = this.configService.getCodexExe();
const workingDir = this.configService.get().codexRoot;
const args = [
`--config-file=${this.configService.getCodexConfigFilePath()}`,
];
await this.shell.spawnDetachedProcess(executable, workingDir, args);
};
}

View File

@ -0,0 +1,165 @@
import { describe, beforeEach, it, expect, vi } from "vitest";
import {
mockShellService,
mockOsService,
mockFsService,
mockCodexGlobals,
} from "../__mocks__/service.mocks.js";
import { mockConfigService } from "../__mocks__/service.mocks.js";
import { ProcessControl } from "./processControl.js";
describe("ProcessControl", () => {
let processControl;
beforeEach(() => {
vi.resetAllMocks();
processControl = new ProcessControl(
mockConfigService,
mockShellService,
mockOsService,
mockFsService,
mockCodexGlobals,
);
processControl.sleep = vi.fn();
});
describe("getCodexProcesses", () => {
const processes = [
{ id: 0, name: "a.exe" },
{ id: 1, name: "aaa" },
{ id: 2, name: "codex" },
{ id: 3, name: "codex.exe" },
{ id: 4, name: "notcodex" },
{ id: 5, name: "alsonotcodex.exe" },
];
beforeEach(() => {
mockOsService.listProcesses.mockResolvedValue(processes);
});
it("returns codex.exe processes on windows", async () => {
mockOsService.isWindows.mockReturnValue(true);
const p = await processControl.getCodexProcesses();
expect(p.length).toBe(1);
expect(p[0]).toBe(processes[3]);
});
it("returns codex processes on non-windows", async () => {
mockOsService.isWindows.mockReturnValue(false);
const p = await processControl.getCodexProcesses();
expect(p.length).toBe(1);
expect(p[0]).toBe(processes[2]);
});
});
describe("getNumberOfCodexProcesses", () => {
it("counts the results of getCodexProcesses", async () => {
processControl.getCodexProcesses = vi.fn();
processControl.getCodexProcesses.mockResolvedValue(["a", "b", "c"]);
expect(await processControl.getNumberOfCodexProcesses()).toBe(3);
});
});
describe("stopCodexProcess", () => {
beforeEach(() => {
processControl.getCodexProcesses = vi.fn();
});
it("throws when no codex processes are found", async () => {
processControl.getCodexProcesses.mockResolvedValue([]);
await expect(processControl.stopCodexProcess).rejects.toThrow(
"No codex process found",
);
});
it("stops the first codex process", async () => {
const pid = 12345;
processControl.getCodexProcesses.mockResolvedValue([
{ pid: pid },
{ pid: 111 },
{ pid: 222 },
]);
await processControl.stopCodexProcess();
expect(mockOsService.stopProcess).toHaveBeenCalledWith(pid);
});
it("sleeps", async () => {
processControl.getCodexProcesses.mockResolvedValue([
{ pid: 111 },
{ pid: 222 },
]);
await processControl.stopCodexProcess();
expect(processControl.sleep).toHaveBeenCalled();
});
});
describe("startCodexProcess", () => {
beforeEach(() => {
processControl.saveCodexConfigFile = vi.fn();
processControl.startCodex = vi.fn();
});
it("saves the config, starts codex, and sleeps", async () => {
await processControl.startCodexProcess();
expect(processControl.saveCodexConfigFile).toHaveBeenCalled();
expect(processControl.startCodex).toHaveBeenCalled();
expect(processControl.sleep).toHaveBeenCalled();
});
});
describe("saveCodexConfigFile", () => {
const publicIp = "1.2.3.4";
const bootNodes = ["a", "b", "c"];
beforeEach(() => {
mockCodexGlobals.getPublicIp.mockResolvedValue(publicIp);
mockCodexGlobals.getTestnetSPRs.mockResolvedValue(bootNodes);
});
it("writes codex config file using public IP and testnet bootstrap nodes", async () => {
await processControl.saveCodexConfigFile();
expect(mockConfigService.writeCodexConfigFile).toHaveBeenCalledWith(
publicIp,
bootNodes,
);
});
});
describe("startCodex", () => {
const config = {
codexRoot: "/codex-root",
};
const exe = "abc.exe";
const configFile = "/codex/config.toml";
beforeEach(() => {
mockConfigService.getCodexExe.mockReturnValue(exe);
mockConfigService.get.mockReturnValue(config);
mockConfigService.getCodexConfigFilePath.mockReturnValue(configFile);
});
it("spawns a detached codex process in the codex root working directory with the config file as argument", async () => {
await processControl.startCodex();
expect(mockShellService.spawnDetachedProcess).toHaveBeenCalledWith(
exe,
config.codexRoot,
[`--config-file=${configFile}`],
);
});
});
});

View File

@ -103,12 +103,12 @@ export async function main() {
const codexGlobals = new CodexGlobals();
const uiService = new UiService();
const fsService = new FsService();
const configService = new ConfigService(fsService);
const codexApp = new CodexApp(configService);
const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService);
const numberSelector = new NumberSelector(uiService);
const shellService = new ShellService();
const osService = new OsService();
const numberSelector = new NumberSelector(uiService);
const configService = new ConfigService(fsService, osService);
const codexApp = new CodexApp(configService);
const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService);
const installer = new Installer(
configService,
shellService,

View File

@ -1,20 +1,7 @@
import {
getAppDataDir,
getCodexBinPath,
getCodexConfigFilePath,
getCodexDataDirDefaultPath,
getCodexLogsDefaultPath,
} from "../utils/appData.js";
import path from "path";
import { getAppDataDir, getDefaultCodexRootPath } from "../utils/appData.js";
const defaultConfig = {
codexExe: "",
// User-selected config options:
codexInstallPath: getCodexBinPath(),
codexConfigFilePath: getCodexConfigFilePath(),
dataDir: getCodexDataDirDefaultPath(),
logsDir: getCodexLogsDefaultPath(),
codexRoot: getDefaultCodexRootPath(),
storageQuota: 8 * 1024 * 1024 * 1024,
ports: {
discPort: 8090,
@ -23,9 +10,14 @@ const defaultConfig = {
},
};
const datadir = "datadir";
const codexLogFile = "codex.log";
const codexConfigFile = "config.toml";
export class ConfigService {
constructor(fsService) {
constructor(fsService, osService) {
this.fs = fsService;
this.os = osService;
this.loadConfig();
}
@ -33,6 +25,19 @@ export class ConfigService {
return this.config;
};
getCodexExe = () => {
var codexExe = "codex";
if (this.os.isWindows()) {
codexExe = "codex.exe";
}
return this.fs.pathJoin([this.config.codexRoot, codexExe]);
};
getCodexConfigFilePath = () => {
return this.fs.pathJoin([this.config.codexRoot, codexConfigFile]);
};
loadConfig = () => {
const filePath = this.getConfigFilename();
try {
@ -66,29 +71,7 @@ export class ConfigService {
return this.fs.pathJoin([getAppDataDir(), "config.json"]);
};
getLogFilePath = () => {
// function getCurrentLogFile(config) {
// const timestamp = new Date()
// .toISOString()
// .replaceAll(":", "-")
// .replaceAll(".", "-");
// return path.join(config.logsDir, `codex_${timestamp}.log`);
// }
// todo, maybe use timestamp
return this.fs.pathJoin([this.config.logsDir, "codex.log"]);
};
missing = (name) => {
throw new Error(`Missing config value: ${name}`);
};
validateConfiguration = () => {
if (this.config.codexExe.length < 1) this.missing("codexExe");
if (this.config.codexConfigFilePath.length < 1)
this.missing("codexConfigFilePath");
if (this.config.dataDir.length < 1) this.missing("dataDir");
if (this.config.logsDir.length < 1) this.missing("logsDir");
if (this.config.storageQuota < 1024 * 1024 * 100)
throw new Error("Storage quota must be at least 100MB");
};
@ -100,10 +83,10 @@ export class ConfigService {
const bootNodes = bootstrapNodes.map((v) => `"${v}"`).join(",");
this.fs.writeFile(
this.config.codexConfigFilePath,
`data-dir="${this.format(this.toRelative(this.config.dataDir))}"${nl}` +
this.getCodexConfigFilePath(),
`data-dir="${datadir}"${nl}` +
`log-level="DEBUG"${nl}` +
`log-file="${this.format(this.getLogFilePath())}"${nl}` +
`log-file="${codexLogFile}"${nl}` +
`storage-quota=${this.config.storageQuota}${nl}` +
`disc-port=${this.config.ports.discPort}${nl}` +
`listen-addrs=["/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}"]${nl}` +
@ -113,12 +96,4 @@ export class ConfigService {
`bootstrap-node=[${bootNodes}]${nl}`,
);
};
format = (str) => {
return str.replaceAll("\\", "/");
};
toRelative = (str) => {
return this.fs.toRelativePath(this.config.codexInstallPath, str);
};
}

View File

@ -1,21 +1,11 @@
import { describe, beforeEach, it, expect, vi } from "vitest";
import { ConfigService } from "./configService.js";
import { mockFsService } from "../__mocks__/service.mocks.js";
import {
getAppDataDir,
getCodexBinPath,
getCodexConfigFilePath,
getCodexDataDirDefaultPath,
getCodexLogsDefaultPath,
} from "../utils/appData.js";
import { mockFsService, mockOsService } from "../__mocks__/service.mocks.js";
import { getAppDataDir, getDefaultCodexRootPath } from "../utils/appData.js";
function getDefaultConfig() {
return {
codexExe: "",
codexInstallPath: getCodexBinPath(),
codexConfigFilePath: getCodexConfigFilePath(),
dataDir: getCodexDataDirDefaultPath(),
logsDir: getCodexLogsDefaultPath(),
codexRoot: getDefaultCodexRootPath(),
storageQuota: 8 * 1024 * 1024 * 1024,
ports: {
discPort: 8090,
@ -38,7 +28,7 @@ describe("ConfigService", () => {
describe("constructor", () => {
it("formats the config file path", () => {
new ConfigService(mockFsService);
new ConfigService(mockFsService, mockOsService);
expect(mockFsService.pathJoin).toHaveBeenCalledWith([
getAppDataDir(),
@ -49,7 +39,7 @@ describe("ConfigService", () => {
it("saves the default config when the config.json file does not exist", () => {
mockFsService.isFile.mockReturnValue(false);
const service = new ConfigService(mockFsService);
const service = new ConfigService(mockFsService, mockOsService);
expect(mockFsService.isFile).toHaveBeenCalledWith(configPath);
expect(mockFsService.readJsonFile).not.toHaveBeenCalled();
@ -67,7 +57,7 @@ describe("ConfigService", () => {
};
mockFsService.readJsonFile.mockReturnValue(savedConfig);
const service = new ConfigService(mockFsService);
const service = new ConfigService(mockFsService, mockOsService);
expect(mockFsService.isFile).toHaveBeenCalledWith(configPath);
expect(mockFsService.readJsonFile).toHaveBeenCalledWith(configPath);
@ -76,17 +66,48 @@ describe("ConfigService", () => {
});
});
describe("getLogFilePath", () => {
it("joins the logsDir with the log filename", () => {
const service = new ConfigService(mockFsService);
describe("getCodexExe", () => {
var configService;
const result = "path/to/codex";
const result = "path/to/codex.log";
beforeEach(() => {
mockFsService.isFile.mockReturnValue(false);
mockFsService.pathJoin.mockReturnValue(result);
configService = new ConfigService(mockFsService, mockOsService);
});
expect(service.getLogFilePath()).toBe(result);
it("joins the codex root with the non-Windows specific exe name", () => {
mockOsService.isWindows.mockReturnValue(false);
expect(configService.getCodexExe()).toBe(result);
expect(mockFsService.pathJoin).toHaveBeenCalledWith([
expectedDefaultConfig.logsDir,
"codex.log",
expectedDefaultConfig.codexRoot,
"codex",
]);
});
it("joins the codex root with the Windows specific exe name", () => {
mockOsService.isWindows.mockReturnValue(true);
expect(configService.getCodexExe()).toBe(result);
expect(mockFsService.pathJoin).toHaveBeenCalledWith([
expectedDefaultConfig.codexRoot,
"codex.exe",
]);
});
});
describe("getCodexConfigFilePath", () => {
const result = "path/to/codex";
it("joins the codex root and codexConfigFile", () => {
mockFsService.pathJoin.mockReturnValue(result);
const configService = new ConfigService(mockFsService, mockOsService);
expect(configService.getCodexConfigFilePath()).toBe(result);
expect(mockFsService.pathJoin).toHaveBeenCalledWith([
expectedDefaultConfig.codexRoot,
"config.toml",
]);
});
});
@ -99,42 +120,10 @@ describe("ConfigService", () => {
config = expectedDefaultConfig;
config.codexExe = "codex.exe";
configService = new ConfigService(mockFsService);
configService = new ConfigService(mockFsService, mockOsService);
configService.config = config;
});
it("throws when codexExe is not set", () => {
config.codexExe = "";
expect(configService.validateConfiguration).toThrow(
"Missing config value: codexExe",
);
});
it("throws when codexConfigFilePath is not set", () => {
config.codexConfigFilePath = "";
expect(configService.validateConfiguration).toThrow(
"Missing config value: codexConfigFilePath",
);
});
it("throws when dataDir is not set", () => {
config.dataDir = "";
expect(configService.validateConfiguration).toThrow(
"Missing config value: dataDir",
);
});
it("throws when logsDir is not set", () => {
config.logsDir = "";
expect(configService.validateConfiguration).toThrow(
"Missing config value: logsDir",
);
});
it("throws when storageQuota is less than 100 MB", () => {
config.storageQuota = 1024 * 1024 * 99;
@ -148,7 +137,7 @@ describe("ConfigService", () => {
});
});
describe("writecodexConfigFile", () => {
describe("writeCodexConfigFile", () => {
const logsPath = "C:\\path\\codex.log";
var configService;
@ -156,32 +145,31 @@ describe("ConfigService", () => {
// use the default config:
mockFsService.isFile.mockReturnValue(false);
configService = new ConfigService(mockFsService);
configService = new ConfigService(mockFsService, mockOsService);
configService.validateConfiguration = vi.fn();
configService.getLogFilePath = vi.fn();
configService.getLogFilePath.mockReturnValue(logsPath);
});
function formatPath(str) {
return str.replaceAll("\\", "/");
}
it("writes the config file values to the config TOML file", () => {
const publicIp = "1.2.3.4";
const bootstrapNodes = ["boot111", "boot222", "boot333"];
const relativeDataDirPath = "..\\../datadir";
const expectedDataDir = "datadir";
const expectedLogFile = "codex.log";
const codexConfigFilePath = "/path/to/config.toml";
mockFsService.toRelativePath.mockReturnValue(relativeDataDirPath);
configService.getCodexConfigFilePath = vi.fn();
configService.getCodexConfigFilePath.mockReturnValue(codexConfigFilePath);
configService.writeCodexConfigFile(publicIp, bootstrapNodes);
const newLine = "\n";
expect(mockFsService.writeFile).toHaveBeenCalledWith(
expectedDefaultConfig.codexConfigFilePath,
`data-dir=\"${formatPath(relativeDataDirPath)}"${newLine}` +
codexConfigFilePath,
`data-dir=\"${expectedDataDir}"${newLine}` +
`log-level="DEBUG"${newLine}` +
`log-file="${formatPath(logsPath)}"${newLine}` +
`log-file="${expectedLogFile}"${newLine}` +
`storage-quota=${expectedDefaultConfig.storageQuota}${newLine}` +
`disc-port=${expectedDefaultConfig.ports.discPort}${newLine}` +
`listen-addrs=["/ip4/0.0.0.0/tcp/${expectedDefaultConfig.ports.listenPort}"]${newLine}` +
@ -194,11 +182,6 @@ describe("ConfigService", () => {
})
.join(",")}]${newLine}`,
);
expect(mockFsService.toRelativePath).toHaveBeenCalledWith(
expectedDefaultConfig.codexInstallPath,
expectedDefaultConfig.dataDir,
);
});
});
});

View File

@ -67,7 +67,10 @@ export class FsService {
fs.writeFileSync(filePath, content);
};
toRelativePath = (from, to) => {
return path.relative(from, to);
ensureDirExists = (dir) => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
return dir;
};
}

View File

@ -25,4 +25,8 @@ export class OsService {
listProcesses = async () => {
return await psList();
};
stopProcess = (pid) => {
process.kill(pid, "SIGINT");
};
}

View File

@ -16,8 +16,9 @@ export class ShellService {
}
};
spawnDetachedProcess = async (cmd, args) => {
spawnDetachedProcess = async (cmd, workingDir, args) => {
var child = spawn(cmd, args, {
cwd: workingDir,
detached: true,
stdio: ["ignore", "ignore", "ignore"],
});

View File

@ -18,7 +18,7 @@ export class InstallMenu {
showInstallMenu = async () => {
await this.ui.askMultipleChoice("Configure your Codex installation", [
{
label: "Install path: " + this.config.codexInstallPath,
label: "Install path: " + this.config.codexRoot,
action: this.selectInstallPath,
},
{
@ -53,7 +53,8 @@ export class InstallMenu {
this.ui.showInfoMessage(
"You are about to:\n" +
" - Uninstall the Codex application\n" +
" - Delete the data stored in your Codex node",
" - Delete the data stored in your Codex node\n" +
" - Delete the log files of your Codex node",
);
await this.ui.askMultipleChoice(
@ -72,8 +73,8 @@ export class InstallMenu {
};
selectInstallPath = async () => {
this.config.codexInstallPath = await this.pathSelector.show(
this.config.codexInstallPath,
this.config.codexRoot = await this.pathSelector.show(
this.config.codexRoot,
false,
);
this.configService.saveConfig();

View File

@ -7,7 +7,7 @@ import { mockInstaller } from "../__mocks__/handler.mocks.js";
describe("InstallMenu", () => {
const config = {
codexInstallPath: "/codex",
codexRoot: "/codex",
};
let installMenu;
@ -52,7 +52,7 @@ describe("InstallMenu", () => {
"Configure your Codex installation",
[
{
label: "Install path: " + config.codexInstallPath,
label: "Install path: " + config.codexRoot,
action: installMenu.selectInstallPath,
},
{
@ -94,7 +94,8 @@ describe("InstallMenu", () => {
expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
"You are about to:\n" +
" - Uninstall the Codex application\n" +
" - Delete the data stored in your Codex node",
" - Delete the data stored in your Codex node\n" +
" - Delete the log files of your Codex node",
);
expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith(
@ -113,14 +114,14 @@ describe("InstallMenu", () => {
});
it("allows selecting the install path", async () => {
const originalPath = config.codexInstallPath;
const originalPath = config.codexRoot;
const newPath = "/new/path";
mockPathSelector.show.mockResolvedValue(newPath);
await installMenu.selectInstallPath();
expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, false);
expect(config.codexInstallPath).toBe(newPath);
expect(config.codexRoot).toBe(newPath);
expect(mockConfigService.saveConfig).toHaveBeenCalled();
});

View File

@ -5,28 +5,10 @@ export function getAppDataDir() {
return ensureExists(appData("codex-cli"));
}
export function getCodexRootPath() {
export function getDefaultCodexRootPath() {
return ensureExists(appData("codex"));
}
export function getCodexBinPath() {
return ensureExists(path.join(appData("codex"), "bin"));
}
export function getCodexConfigFilePath() {
return path.join(appData("codex"), "bin", "config.toml");
}
export function getCodexDataDirDefaultPath() {
// This path does not exist on first startup. That's good: Codex will
// create it with the required access permissions.
return path.join(appData("codex"), "datadir");
}
export function getCodexLogsDefaultPath() {
return ensureExists(path.join(appData("codex"), "logs"));
}
function ensureExists(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });