Compare commits

...

2 Commits

Author SHA1 Message Date
Brooklyn Nicholson
8a48d54193 refactor(desktop): extract VS Code Marketplace theme IPC from main.cjs into vscode-theme-ipc.cjs
Seventh main.cjs cluster peel. The two hermes:vscode-theme:* handlers (fetch,
search) move verbatim into electron/vscode-theme-ipc.cjs behind a
registerVscodeThemeIpc({ ipcMain }) registrar. Both delegate to the
vscode-marketplace sibling module, which the new module requires directly — so
the now-dead require in main.cjs is removed.

Channel names unchanged → preload + renderer untouched. Adds
electron/vscode-theme-ipc.test.cjs (surface invariant).
2026-06-30 13:34:12 -05:00
Brooklyn Nicholson
e167ed7bb1 refactor(desktop): extract project-dir + workspace settings IPC from main.cjs into project-dir-ipc.cjs
Sixth main.cjs cluster peel. The hermes:setting:defaultProjectDir:get/set/pick
handlers + hermes:workspace:sanitize move verbatim into
electron/project-dir-ipc.cjs behind a registerProjectDirIpc({ ipcMain,
readDefaultProjectDir, writeDefaultProjectDir, resolveHermesCwd,
sanitizeWorkspaceCwd }) registrar. The config readers/writers + cwd resolvers stay
in the main process and are injected.

Channel names unchanged → preload + renderer untouched. Adds
electron/project-dir-ipc.test.cjs (surface + set/sanitize behavior; get/pick touch
Electron app/dialog and are exercised in-app only).
2026-06-30 13:32:22 -05:00
5 changed files with 179 additions and 43 deletions

View File

@@ -41,7 +41,6 @@ const { adoptServedDashboardToken } = require('./dashboard-token.cjs')
const { waitForDashboardPortAnnouncement } = require('./backend-ready.cjs')
const { dashboardFallbackArgs, sourceDeclaresServe } = require('./backend-command.cjs')
const { serializeJsonBody, setJsonRequestHeaders } = require('./oauth-net-request.cjs')
const { fetchMarketplaceThemes, searchMarketplaceThemes } = require('./vscode-marketplace.cjs')
const { buildDesktopBackendEnv, normalizeHermesHomeRoot } = require('./backend-env.cjs')
const { readWindowsUserEnvVar } = require('./windows-user-env.cjs')
const { readWslWindowsClipboardImage } = require('./wsl-clipboard-image.cjs')
@@ -61,6 +60,8 @@ 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 { registerVscodeThemeIpc } = require('./vscode-theme-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))
@@ -7160,12 +7133,8 @@ ipcMain.handle('hermes:uninstall:run', async (_event, payload) => {
return runDesktopUninstall(String(mode || ''))
})
// Download a VS Code Marketplace extension and return the raw color-theme JSON
// it contributes. No theme code is executed — we only read JSON from the .vsix.
ipcMain.handle('hermes:vscode-theme:fetch', async (_event, id) => fetchMarketplaceThemes(String(id || '')))
// Search the Marketplace for color-theme extensions (empty query = top installs).
ipcMain.handle('hermes:vscode-theme:search', async (_event, query) => searchMarketplaceThemes(String(query || ''), 20))
// VS Code Marketplace theme IPC lives in vscode-theme-ipc.cjs.
registerVscodeThemeIpc({ ipcMain })
// ---------------------------------------------------------------------------
// hermes:// deep links (e.g. hermes://blueprint/morning-brief?time=08:00).

View File

@@ -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 }

View File

@@ -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')
})

View File

@@ -0,0 +1,19 @@
'use strict'
const { fetchMarketplaceThemes, searchMarketplaceThemes } = require('./vscode-marketplace.cjs')
// VS Code Marketplace color-theme IPC: fetch a theme by extension id + search the
// marketplace. Both delegate to the vscode-marketplace sibling module; no theme
// code is ever executed (only JSON is read from the .vsix).
function registerVscodeThemeIpc({ ipcMain }) {
// Download a VS Code Marketplace extension and return the raw color-theme JSON
// it contributes. No theme code is executed — we only read JSON from the .vsix.
ipcMain.handle('hermes:vscode-theme:fetch', async (_event, id) => fetchMarketplaceThemes(String(id || '')))
// Search the Marketplace for color-theme extensions (empty query = top installs).
ipcMain.handle('hermes:vscode-theme:search', async (_event, query) =>
searchMarketplaceThemes(String(query || ''), 20)
)
}
module.exports = { registerVscodeThemeIpc }

View File

@@ -0,0 +1,30 @@
'use strict'
const assert = require('node:assert/strict')
const test = require('node:test')
const { registerVscodeThemeIpc } = require('./vscode-theme-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)
}
}
}
test('registerVscodeThemeIpc wires only hermes:vscode-theme:* channels, each to a handler fn', () => {
const ipcMain = fakeIpcMain()
registerVscodeThemeIpc({ ipcMain })
assert.deepEqual([...ipcMain.handlers.keys()].sort(), ['hermes:vscode-theme:fetch', 'hermes:vscode-theme:search'])
for (const handler of ipcMain.handlers.values()) {
assert.equal(typeof handler, 'function')
}
})