From 280b00802bcecc63b2524a1536730c8db1e7824c Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 10:41:44 +0200 Subject: [PATCH] Simplified path handling part 1 --- src/__mocks__/service.mocks.js | 8 +- src/handlers/installationHandlers.js | 1 - src/handlers/installer.js | 37 +++--- src/handlers/installer.test.js | 121 ++++++++------------ src/handlers/processControl.js | 12 +- src/handlers/processControl.test.js | 165 +++++++++++++++++++++++++++ src/main.js | 8 +- src/services/configService.js | 73 ++++-------- src/services/configService.test.js | 127 +++++++++------------ src/services/fsService.js | 7 +- src/services/osService.js | 4 + src/services/shellService.js | 3 +- src/ui/installMenu.js | 9 +- src/ui/installMenu.test.js | 11 +- src/utils/appData.js | 20 +--- 15 files changed, 347 insertions(+), 259 deletions(-) create mode 100644 src/handlers/processControl.test.js diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index b399b1f..8b90113 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -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 = { diff --git a/src/handlers/installationHandlers.js b/src/handlers/installationHandlers.js index d026c5c..5e3cddd 100644 --- a/src/handlers/installationHandlers.js +++ b/src/handlers/installationHandlers.js @@ -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(); diff --git a/src/handlers/installer.js b/src/handlers/installer.js index ca0eb4f..dffe6ec 100644 --- a/src/handlers/installer.js +++ b/src/handlers/installer.js @@ -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(); - }; } diff --git a/src/handlers/installer.test.js b/src/handlers/installer.test.js index 3eea1cf..bcbbff0 100644 --- a/src/handlers/installer.test.js +++ b/src/handlers/installer.test.js @@ -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); }); }); }); diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 8787991..407d830 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -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); }; } diff --git a/src/handlers/processControl.test.js b/src/handlers/processControl.test.js new file mode 100644 index 0000000..70a6963 --- /dev/null +++ b/src/handlers/processControl.test.js @@ -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}`], + ); + }); + }); +}); diff --git a/src/main.js b/src/main.js index f12da02..bef9420 100644 --- a/src/main.js +++ b/src/main.js @@ -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, diff --git a/src/services/configService.js b/src/services/configService.js index 9185177..73627e6 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -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); - }; } diff --git a/src/services/configService.test.js b/src/services/configService.test.js index ee58b0f..990fe61 100644 --- a/src/services/configService.test.js +++ b/src/services/configService.test.js @@ -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, - ); }); }); }); diff --git a/src/services/fsService.js b/src/services/fsService.js index 52556d4..67ac664 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -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; }; } diff --git a/src/services/osService.js b/src/services/osService.js index 6e9502b..2778b58 100644 --- a/src/services/osService.js +++ b/src/services/osService.js @@ -25,4 +25,8 @@ export class OsService { listProcesses = async () => { return await psList(); }; + + stopProcess = (pid) => { + process.kill(pid, "SIGINT"); + }; } diff --git a/src/services/shellService.js b/src/services/shellService.js index 79ae419..6fde68e 100644 --- a/src/services/shellService.js +++ b/src/services/shellService.js @@ -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"], }); diff --git a/src/ui/installMenu.js b/src/ui/installMenu.js index 6ce3053..f87069d 100644 --- a/src/ui/installMenu.js +++ b/src/ui/installMenu.js @@ -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(); diff --git a/src/ui/installMenu.test.js b/src/ui/installMenu.test.js index 1b47cd4..6dd281f 100644 --- a/src/ui/installMenu.test.js +++ b/src/ui/installMenu.test.js @@ -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(); }); diff --git a/src/utils/appData.js b/src/utils/appData.js index d375cad..9343c40 100644 --- a/src/utils/appData.js +++ b/src/utils/appData.js @@ -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 });