diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-21 17:50:08 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-21 17:50:08 +0200 |
| commit | 73490d331179cb640828b8625abb75208cd7ae5f (patch) | |
| tree | 5782835ae06d3f84c4a9c11c8babcec0d8126157 /packages/meshbay-client/src | |
| parent | 8730281d739d9ed1d6f2d366772582eea8ba0294 (diff) | |
| download | meshbay-73490d331179cb640828b8625abb75208cd7ae5f.tar.gz | |
feat: D.6 first-run wizard — detect, start, link, group, gek-init, pair
Desktop client guides new users through the full onboarding sequence
without a terminal: detect local node → start daemon (systemd with
direct-start fallback for dev) → link node key to hub → create group →
attach directories → gek-init → operator pair.
- node:detect returns configured field to distinguish missing vs stopped
- node:start tries systemctl first, checks is-active, falls back to
spawning the binary directly when the unit crashes (dev mode)
- probeNode() extracts the shared config→token→fetch pattern
- SetupWelcome on empty HomePage links to /create-group
- CreateGroupWizard step 0 handles node detection and start
- platform.js exposes node.installed() and node.start()
- 12 setup.* i18n keys added to all 10 locales
- Integration test for node:start fallback logic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-client/src')
| -rw-r--r-- | packages/meshbay-client/src/main.js | 114 | ||||
| -rw-r--r-- | packages/meshbay-client/src/preload.js | 2 |
2 files changed, 112 insertions, 4 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index 45e8462..22e0162 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -22,6 +22,7 @@ const { app, BrowserWindow, dialog, ipcMain, protocol, safeStorage, shell } = require('electron'); +const { execFile, spawn } = require('node:child_process'); const crypto = require('node:crypto'); const fs = require('node:fs'); const fsp = require('node:fs/promises'); @@ -691,27 +692,132 @@ function registerBridge() { ipcMain.handle('node:detect', async () => { const nc = readNodeConfig(); - if (!nc) return { detected: false }; + if (!nc) return { detected: false, configured: false }; const token = readNodeToken(nc.dataDir); - if (!token) return { detected: false }; + if (!token) return { detected: false, configured: true }; _nodeToken = token; _nodePort = nc.uiPort; try { const r = await fetch( `http://127.0.0.1:${_nodePort}/api/status?t=${_nodeToken}`, { signal: AbortSignal.timeout(3000) }); - if (!r.ok) return { detected: false }; + if (!r.ok) return { detected: false, configured: true }; const status = await r.json(); return { detected: true, + configured: true, status: status.status, pk_node_ed25519: status.pk_node_ed25519 || '', }; } catch { - return { detected: false }; + return { detected: false, configured: true }; } }); + ipcMain.handle('node:installed', async () => { + if (process.platform !== 'linux') return { installed: false }; + return new Promise((resolve) => { + execFile('systemctl', ['--user', 'show', 'meshbay-node.service', + '--property=LoadState'], (err, stdout) => { + if (err) return resolve({ installed: false }); + resolve({ installed: stdout.trim() === 'LoadState=loaded' }); + }); + }); + }); + + async function probeNode() { + const nc = readNodeConfig(); + const dataDir = nc ? nc.dataDir + : path.join(os.homedir(), '.local', 'share', 'meshbay'); + const port = nc ? nc.uiPort : 18000; + const token = readNodeToken(dataDir); + if (!token) return null; + try { + const r = await fetch( + `http://127.0.0.1:${port}/api/status?t=${token}`, + { signal: AbortSignal.timeout(3000) }); + if (!r.ok) return null; + const status = await r.json(); + _nodeToken = token; + _nodePort = port; + return { pk_node_ed25519: status.pk_node_ed25519 || '' }; + } catch { return null; } + } + + ipcMain.handle('node:start', async () => { + const already = await probeNode(); + if (already) return { started: true, ...already }; + + if (process.platform !== 'linux') { + throw new Error('Automatic node start is only supported on Linux'); + } + + const deadline = Date.now() + 15000; + const configFile = nodeConfigPath(); + let launched = false; + + // Try systemctl first — the production path. + try { + await new Promise((resolve, reject) => { + execFile('systemctl', ['--user', 'enable', '--now', 'meshbay-node'], + (err, _stdout, stderr) => { + if (err) return reject(new Error(stderr.trim() || err.message)); + resolve(); + }); + }); + // Give the service a moment, then check if it actually stayed up. + for (let i = 0; i < 6 && Date.now() < deadline; i++) { + await new Promise((r) => setTimeout(r, 500)); + const result = await probeNode(); + if (result) return { started: true, ...result }; + } + // The unit may be stuck in auto-restart (e.g. ExecStart points to + // /usr/bin which doesn't exist in dev). is-active returns 0 only when + // the service is genuinely running. + const isActive = await new Promise((resolve) => { + execFile('systemctl', ['--user', 'is-active', 'meshbay-node'], + (err) => resolve(!err)); + }); + if (isActive) { + launched = true; + } else { + // Stop the broken unit so it doesn't compete with the direct start. + await new Promise((resolve) => { + execFile('systemctl', ['--user', 'stop', 'meshbay-node'], + () => resolve()); + }); + } + } catch { + // systemctl itself failed (e.g. no unit file). + } + + // Fallback: start the binary directly (dev mode, or no unit installed). + if (!launched) { + const binPath = await new Promise((resolve) => { + execFile('which', ['meshbay-node'], (err, stdout) => { + resolve(err ? null : stdout.trim()); + }); + }); + if (!binPath) { + throw new Error( + 'meshbay-node is not installed'); + } + const child = spawn(binPath, ['--config', configFile], { + detached: true, + stdio: 'ignore', + }); + child.unref(); + } + + while (Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 500)); + const result = await probeNode(); + if (result) return { started: true, ...result }; + } + throw new Error( + 'meshbay-node was started but did not become ready within 15 seconds'); + }); + ipcMain.handle('node:call', async (_e, method, apiPath, body) => { if (!_nodeToken) throw new Error('Node not detected'); const sep = apiPath.includes('?') ? '&' : '?'; diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js index e9fd375..c8553a5 100644 --- a/packages/meshbay-client/src/preload.js +++ b/packages/meshbay-client/src/preload.js @@ -86,6 +86,8 @@ contextBridge.exposeInMainWorld('meshbay', { // pattern as hub:fetch. node: { detect: () => ipcRenderer.invoke('node:detect'), + installed: () => ipcRenderer.invoke('node:installed'), + start: () => ipcRenderer.invoke('node:start'), call: (method, path, body) => ipcRenderer.invoke('node:call', method, path, body), pairingCode: () => ipcRenderer.invoke('node:pairing-code'), setPairingCode: (code) => ipcRenderer.invoke('node:set-pairing-code', code), |