diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index af2db3c4180..6c8e6df8f8e 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -61,6 +61,7 @@ const { registerFsIpc } = require('./fs-ipc.cjs') const { registerTerminalIpc } = require('./terminal-ipc.cjs') const { registerUpdatesIpc } = require('./updates-ipc.cjs') const { registerLogsIpc } = require('./logs-ipc.cjs') +const { registerProjectDirIpc } = require('./project-dir-ipc.cjs') const { OFFICIAL_REPO_HTTPS_URL, isOfficialSshRemote } = require('./update-remote.cjs') const { resolveBehindCount, shouldCountCommits } = require('./update-count.cjs') const { runRebuildWithRetry } = require('./update-rebuild.cjs') @@ -6663,42 +6664,14 @@ ipcMain.handle('hermes:openPreviewInBrowser', async (_event, url) => { // settings mount and seeds the value into the picker; writing back persists // it via writeDefaultProjectDir so resolveHermesCwd picks it up on the next // session spawn (no app restart needed). -ipcMain.handle('hermes:setting:defaultProjectDir:get', async () => ({ - dir: readDefaultProjectDir(), - defaultLabel: app.getPath('home'), - resolvedCwd: resolveHermesCwd() -})) - -ipcMain.handle('hermes:workspace:sanitize', async (_event, cwd) => sanitizeWorkspaceCwd(cwd)) - -ipcMain.handle('hermes:setting:defaultProjectDir:set', async (_event, dir) => { - const next = typeof dir === 'string' && dir.trim() ? dir.trim() : null - - if (next) { - try { - fs.mkdirSync(next, { recursive: true }) - } catch (error) { - throw new Error(`Could not create directory: ${error.message}`) - } - } - - writeDefaultProjectDir(next) - - return { dir: next } -}) - -ipcMain.handle('hermes:setting:defaultProjectDir:pick', async () => { - const result = await dialog.showOpenDialog({ - title: 'Choose default project directory', - properties: ['openDirectory', 'createDirectory'], - defaultPath: readDefaultProjectDir() || app.getPath('home') - }) - - if (result.canceled || result.filePaths.length === 0) { - return { canceled: true, dir: null } - } - - return { canceled: false, dir: result.filePaths[0] } +// Default-project-dir + workspace settings IPC lives in project-dir-ipc.cjs; +// config readers/writers + cwd resolvers are injected. +registerProjectDirIpc({ + ipcMain, + readDefaultProjectDir, + resolveHermesCwd, + sanitizeWorkspaceCwd, + writeDefaultProjectDir }) ipcMain.handle('hermes:fetchLinkTitle', (_event, url) => fetchLinkTitle(url)) diff --git a/apps/desktop/electron/project-dir-ipc.cjs b/apps/desktop/electron/project-dir-ipc.cjs new file mode 100644 index 00000000000..9d4197504ba --- /dev/null +++ b/apps/desktop/electron/project-dir-ipc.cjs @@ -0,0 +1,55 @@ +'use strict' + +const { app, dialog } = require('electron') +const fs = require('fs') + +// Default-project-directory + workspace-cwd settings IPC: read / write / native +// directory picker, plus workspace-cwd sanitize. The config readers/writers and +// cwd resolvers live in the main process and are injected. +function registerProjectDirIpc({ + ipcMain, + readDefaultProjectDir, + resolveHermesCwd, + sanitizeWorkspaceCwd, + writeDefaultProjectDir +}) { + ipcMain.handle('hermes:setting:defaultProjectDir:get', async () => ({ + dir: readDefaultProjectDir(), + defaultLabel: app.getPath('home'), + resolvedCwd: resolveHermesCwd() + })) + + ipcMain.handle('hermes:workspace:sanitize', async (_event, cwd) => sanitizeWorkspaceCwd(cwd)) + + ipcMain.handle('hermes:setting:defaultProjectDir:set', async (_event, dir) => { + const next = typeof dir === 'string' && dir.trim() ? dir.trim() : null + + if (next) { + try { + fs.mkdirSync(next, { recursive: true }) + } catch (error) { + throw new Error(`Could not create directory: ${error.message}`) + } + } + + writeDefaultProjectDir(next) + + return { dir: next } + }) + + ipcMain.handle('hermes:setting:defaultProjectDir:pick', async () => { + const result = await dialog.showOpenDialog({ + title: 'Choose default project directory', + properties: ['openDirectory', 'createDirectory'], + defaultPath: readDefaultProjectDir() || app.getPath('home') + }) + + if (result.canceled || result.filePaths.length === 0) { + return { canceled: true, dir: null } + } + + return { canceled: false, dir: result.filePaths[0] } + }) +} + +module.exports = { registerProjectDirIpc } diff --git a/apps/desktop/electron/project-dir-ipc.test.cjs b/apps/desktop/electron/project-dir-ipc.test.cjs new file mode 100644 index 00000000000..7159e390788 --- /dev/null +++ b/apps/desktop/electron/project-dir-ipc.test.cjs @@ -0,0 +1,63 @@ +'use strict' + +const assert = require('node:assert/strict') +const test = require('node:test') + +const { registerProjectDirIpc } = require('./project-dir-ipc.cjs') + +function fakeIpcMain() { + const handlers = new Map() + + return { + handlers, + handle(channel, handler) { + assert.ok(!handlers.has(channel), `duplicate registration for ${channel}`) + handlers.set(channel, handler) + } + } +} + +function deps(overrides = {}) { + return { + readDefaultProjectDir: () => '/projects', + resolveHermesCwd: () => '/cwd', + sanitizeWorkspaceCwd: cwd => `safe:${cwd}`, + writeDefaultProjectDir: () => {}, + ...overrides + } +} + +test('registerProjectDirIpc wires the project-dir + workspace settings channels', () => { + const ipcMain = fakeIpcMain() + + registerProjectDirIpc({ ipcMain, ...deps() }) + + assert.deepEqual([...ipcMain.handlers.keys()].sort(), [ + 'hermes:setting:defaultProjectDir:get', + 'hermes:setting:defaultProjectDir:pick', + 'hermes:setting:defaultProjectDir:set', + 'hermes:workspace:sanitize' + ]) +}) + +// `get` / `pick` touch Electron's `app` / `dialog`, which are unavailable under +// `node --test` (require('electron') is a path stub), so they're exercised in-app +// only. The wiring of all four channels is covered by the surface test above. + +test('set normalizes a blank dir to null and persists that (clears the override)', async () => { + const ipcMain = fakeIpcMain() + const writes = [] + + registerProjectDirIpc({ ipcMain, ...deps({ writeDefaultProjectDir: d => writes.push(d) }) }) + + assert.deepEqual(await ipcMain.handlers.get('hermes:setting:defaultProjectDir:set')({}, ' '), { dir: null }) + assert.deepEqual(writes, [null]) +}) + +test('workspace:sanitize delegates to the injected sanitizer', async () => { + const ipcMain = fakeIpcMain() + + registerProjectDirIpc({ ipcMain, ...deps() }) + + assert.equal(await ipcMain.handlers.get('hermes:workspace:sanitize')({}, '/x'), 'safe:/x') +})