aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-21 17:50:08 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-21 17:50:08 +0200
commit73490d331179cb640828b8625abb75208cd7ae5f (patch)
tree5782835ae06d3f84c4a9c11c8babcec0d8126157 /packages/meshbay-client/src
parent8730281d739d9ed1d6f2d366772582eea8ba0294 (diff)
downloadmeshbay-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.js114
-rw-r--r--packages/meshbay-client/src/preload.js2
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),