livedesk 0.1.618 → 0.1.619

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,11 +69,12 @@ and startup retry, then exits without looping.
69
69
  npx -y --prefer-online livedesk@latest client 3
70
70
  ```
71
71
 
72
- The `client` form remains as a legacy compatibility alias. It records the
73
- Client role and uses the same unified runtime lock. The client signs in with
74
- Google, discovers the active Hub, and connects to the wall. On Windows, enable
75
- **Start with Windows** on the connection page to reconnect automatically after
76
- reboot; the generated startup command uses the unified launcher.
72
+ The `client` form remains as a legacy compatibility alias. It records the
73
+ Client role and uses the same unified runtime lock. The client signs in with
74
+ Google, discovers the active Hub, and connects to the wall. Installed Windows,
75
+ macOS, and Linux desktop apps start automatically after sign-in by default,
76
+ reuse the saved session, and reconnect without opening the window. Turn this
77
+ off in **Settings > Desktop app** when a computer should remain manual.
77
78
 
78
79
  ## macOS physical diagnostic
79
80
 
package/client/README.md CHANGED
@@ -30,9 +30,10 @@ discovers the newly published Hub without another Google login. Omit the number
30
30
  for first-available placement, or pass `1` to `999` to pin this machine to a
31
31
  screen wall slot.
32
32
 
33
- On Windows, check **Start with Windows** on the connection page to reconnect this
34
- client automatically after reboot. The startup entry reuses the saved Google
35
- session and only opens the connection page again if sign-in is needed.
33
+ Installed VuvoDesk desktop apps start automatically after sign-in on Windows,
34
+ macOS, and Linux by default. The startup entry reuses the saved Google session,
35
+ reconnects quietly, and only opens the connection page again if sign-in or
36
+ first-run role setup is needed. Turn this off in **Settings > Desktop app**.
36
37
 
37
38
  On the Hub computer, open the VuvoDesk dashboard, sign in, and run **Sync
38
39
  Server** once. The dashboard transfers the signed-in session to the local Hub;
@@ -0,0 +1,31 @@
1
+ const { chmodSync, existsSync, statSync } = require('node:fs');
2
+ const { join } = require('node:path');
3
+
4
+ module.exports = async function afterPack(context) {
5
+ if (context.electronPlatformName !== 'linux') {
6
+ return;
7
+ }
8
+
9
+ const remoteFastExecutable = join(
10
+ context.appOutDir,
11
+ 'resources',
12
+ 'app.asar.unpacked',
13
+ 'node_modules',
14
+ '@livedesk',
15
+ 'fast-linux-x64',
16
+ 'fast',
17
+ 'livedesk-client-fast'
18
+ );
19
+
20
+ if (!existsSync(remoteFastExecutable)) {
21
+ throw new Error(`Packaged Linux RemoteFast executable is missing: ${remoteFastExecutable}`);
22
+ }
23
+
24
+ chmodSync(remoteFastExecutable, 0o755);
25
+ const executableMode = statSync(remoteFastExecutable).mode & 0o777;
26
+ if ((executableMode & 0o111) !== 0o111) {
27
+ throw new Error(
28
+ `Packaged Linux RemoteFast must be executable before AppImage/deb assembly; mode=${executableMode.toString(8)}`
29
+ );
30
+ }
31
+ };
@@ -0,0 +1,190 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+
5
+ export const DESKTOP_AUTO_START_ARGUMENT = '--autostart';
6
+ export const DESKTOP_AUTO_START_ENTRY_NAME = 'LiveDesk Desktop';
7
+ const DESKTOP_AUTO_START_PREFERENCE_VERSION = 1;
8
+ const SUPPORTED_PLATFORMS = new Set(['win32', 'darwin', 'linux']);
9
+
10
+ function readPreference(path) {
11
+ try {
12
+ const record = JSON.parse(readFileSync(path, 'utf8'));
13
+ return record?.version === DESKTOP_AUTO_START_PREFERENCE_VERSION && typeof record.enabled === 'boolean'
14
+ ? record.enabled
15
+ : null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ function writePreference(path, enabled) {
22
+ mkdirSync(dirname(path), { recursive: true });
23
+ writeFileSync(path, `${JSON.stringify({
24
+ version: DESKTOP_AUTO_START_PREFERENCE_VERSION,
25
+ enabled: Boolean(enabled),
26
+ savedAt: new Date().toISOString()
27
+ }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
28
+ }
29
+
30
+ function quoteDesktopExecArgument(value) {
31
+ const escaped = String(value || '')
32
+ .replace(/\\/g, '\\\\')
33
+ .replace(/"/g, '\\"')
34
+ .replace(/\$/g, '\\$')
35
+ .replace(/`/g, '\\`');
36
+ return `"${escaped}"`;
37
+ }
38
+
39
+ function linuxDesktopEntry(executablePath) {
40
+ return [
41
+ '[Desktop Entry]',
42
+ 'Type=Application',
43
+ 'Version=1.0',
44
+ 'Name=VuvoDesk',
45
+ 'Comment=Start VuvoDesk and reconnect this computer automatically',
46
+ `Exec=${quoteDesktopExecArgument(executablePath)} ${DESKTOP_AUTO_START_ARGUMENT}`,
47
+ 'Terminal=false',
48
+ 'StartupNotify=false',
49
+ 'X-GNOME-Autostart-enabled=true',
50
+ 'X-VuvoDesk-Managed=true',
51
+ ''
52
+ ].join('\n');
53
+ }
54
+
55
+ export function createDesktopAutoStartOwner({
56
+ app,
57
+ stateRoot,
58
+ platform = process.platform,
59
+ environment = process.env,
60
+ executablePath = process.execPath,
61
+ argv = process.argv,
62
+ userHome = homedir(),
63
+ isPackaged = app?.isPackaged === true,
64
+ log = () => {}
65
+ }) {
66
+ const supported = isPackaged && SUPPORTED_PLATFORMS.has(platform);
67
+ const preferencePath = join(stateRoot, 'desktop-auto-start.json');
68
+ const linuxConfigRoot = environment.XDG_CONFIG_HOME
69
+ ? resolve(environment.XDG_CONFIG_HOME)
70
+ : join(userHome, '.config');
71
+ const linuxEntryPath = join(linuxConfigRoot, 'autostart', 'vuvodesk.desktop');
72
+ const linuxExecutablePath = resolve(String(environment.APPIMAGE || executablePath));
73
+ const windowsLoginOptions = {
74
+ path: executablePath,
75
+ args: [DESKTOP_AUTO_START_ARGUMENT]
76
+ };
77
+
78
+ function desiredEnabled() {
79
+ return readPreference(preferencePath) ?? true;
80
+ }
81
+
82
+ function platformRegistered() {
83
+ if (!supported) return false;
84
+ if (platform === 'linux') {
85
+ try {
86
+ const entry = readFileSync(linuxEntryPath, 'utf8');
87
+ return entry.includes('X-VuvoDesk-Managed=true')
88
+ && entry.includes(DESKTOP_AUTO_START_ARGUMENT)
89
+ && entry.includes(quoteDesktopExecArgument(linuxExecutablePath));
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ try {
95
+ const settings = platform === 'win32'
96
+ ? app.getLoginItemSettings(windowsLoginOptions)
97
+ : app.getLoginItemSettings();
98
+ return settings?.openAtLogin === true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ function status(extra = {}) {
105
+ return {
106
+ supported,
107
+ enabled: desiredEnabled(),
108
+ registered: platformRegistered(),
109
+ platform,
110
+ ...extra
111
+ };
112
+ }
113
+
114
+ function apply(enabled) {
115
+ if (platform === 'linux') {
116
+ if (!enabled) {
117
+ rmSync(linuxEntryPath, { force: true });
118
+ return;
119
+ }
120
+ mkdirSync(dirname(linuxEntryPath), { recursive: true });
121
+ writeFileSync(linuxEntryPath, linuxDesktopEntry(linuxExecutablePath), { encoding: 'utf8', mode: 0o600 });
122
+ return;
123
+ }
124
+ if (platform === 'win32') {
125
+ app.setLoginItemSettings({
126
+ openAtLogin: enabled,
127
+ name: DESKTOP_AUTO_START_ENTRY_NAME,
128
+ ...windowsLoginOptions
129
+ });
130
+ return;
131
+ }
132
+ app.setLoginItemSettings({
133
+ openAtLogin: enabled,
134
+ openAsHidden: enabled
135
+ });
136
+ }
137
+
138
+ function reconcile() {
139
+ if (!supported) return status({ ok: false, error: 'desktop-auto-start-unavailable' });
140
+ const saved = readPreference(preferencePath);
141
+ const enabled = saved ?? true;
142
+ if (saved === null) writePreference(preferencePath, enabled);
143
+ try {
144
+ apply(enabled);
145
+ const next = status();
146
+ const ok = next.registered === enabled;
147
+ if (!ok) log(`desktop auto-start reconcile did not reach enabled=${enabled}`);
148
+ return { ...next, ok, error: ok ? '' : 'desktop-auto-start-registration-mismatch' };
149
+ } catch (error) {
150
+ const message = String(error?.message || error);
151
+ log(`desktop auto-start reconcile failed: ${message}`);
152
+ return status({ ok: false, error: message });
153
+ }
154
+ }
155
+
156
+ function setEnabled(enabled) {
157
+ if (!supported) return status({ ok: false, error: 'desktop-auto-start-unavailable' });
158
+ const desired = Boolean(enabled);
159
+ writePreference(preferencePath, desired);
160
+ try {
161
+ apply(desired);
162
+ const next = status();
163
+ const ok = next.registered === desired;
164
+ return { ...next, ok, error: ok ? '' : 'desktop-auto-start-registration-mismatch' };
165
+ } catch (error) {
166
+ const message = String(error?.message || error);
167
+ log(`desktop auto-start update failed: ${message}`);
168
+ return status({ ok: false, error: message });
169
+ }
170
+ }
171
+
172
+ function wasOpenedAutomatically() {
173
+ if (argv.includes(DESKTOP_AUTO_START_ARGUMENT)) return true;
174
+ if (platform !== 'darwin' || !supported) return false;
175
+ try {
176
+ return app.getLoginItemSettings()?.wasOpenedAtLogin === true;
177
+ } catch {
178
+ return false;
179
+ }
180
+ }
181
+
182
+ return {
183
+ getStatus: status,
184
+ reconcile,
185
+ setEnabled,
186
+ wasOpenedAutomatically,
187
+ preferencePath,
188
+ linuxEntryPath
189
+ };
190
+ }
@@ -1,5 +1,6 @@
1
- extends: file:packages/livedesk/electron/electron-builder.yml
2
- files:
1
+ extends: file:packages/livedesk/electron/electron-builder.yml
2
+ afterPack: packages/livedesk/electron/after-pack.cjs
3
+ files:
3
4
  - bin/**
4
5
  - bootstrap/**
5
6
  - client/**
package/electron/main.mjs CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  } from './runtime-role-transition.mjs';
48
48
  import { createDesktopClipboardOwner } from './desktop-clipboard-owner.mjs';
49
49
  import { createRuntimeWindowLoadOwner } from './runtime-window-load-owner.mjs';
50
+ import { createDesktopAutoStartOwner } from './desktop-auto-start.mjs';
50
51
 
51
52
  // Chromium otherwise leaves an out-of-process Audio Service (~80-90 MiB on
52
53
  // Windows) alive after the exact Remote Audio owner, AudioContext, worklet,
@@ -55,9 +56,8 @@ import { createRuntimeWindowLoadOwner } from './runtime-window-load-owner.mjs';
55
56
  // warm-idle process baseline instead of retaining a hidden utility process.
56
57
  app.commandLine.appendSwitch('disable-features', 'AudioServiceOutOfProcess');
57
58
 
58
- const APP_NAME = 'VuvoDesk Desktop';
59
- const DESKTOP_STARTUP_ENTRY_NAME = 'LiveDesk Desktop';
60
- const appWindowTitle = () => `${APP_NAME} · ${app.getVersion()}`;
59
+ const APP_NAME = 'VuvoDesk Desktop';
60
+ const appWindowTitle = () => `${APP_NAME} · ${app.getVersion()}`;
61
61
  const LOCAL_RUNTIME_PORT = Number(process.env.LIVEDESK_LOCAL_RUNTIME_PORT || 5179);
62
62
  const LOCAL_RUNTIME_URL = `http://127.0.0.1:${Number.isInteger(LOCAL_RUNTIME_PORT) && LOCAL_RUNTIME_PORT > 0 ? LOCAL_RUNTIME_PORT : 5179}/`;
63
63
  function readDesktopPackageMetadata() {
@@ -172,6 +172,7 @@ let desktopSessionEpoch = 0;
172
172
  let lastKnownDesktopSession = null;
173
173
  let desktopClipboardOwner = null;
174
174
  let runtimeWindowLoadOwner = null;
175
+ let desktopAutoStartOwner = null;
175
176
  const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
176
177
  const installerQuitGate = createInstallerQuitGate({
177
178
  requestQuit: () => app.quit()
@@ -1089,7 +1090,7 @@ function showWindow() {
1089
1090
  requestDesktopResumeAuthRecovery('window-show');
1090
1091
  }
1091
1092
 
1092
- function createMainWindow() {
1093
+ function createMainWindow({ showOnInitialLoad = true } = {}) {
1093
1094
  const iconPath = appResource('electron', 'icon.png');
1094
1095
  Menu.setApplicationMenu(null);
1095
1096
  runtimeWindowLoadOwner?.stop();
@@ -1126,7 +1127,7 @@ function createMainWindow() {
1126
1127
  if (window.webContents.getURL() !== DESKTOP_RUNTIME_RECOVERY_PAGE_URL) {
1127
1128
  await window.loadURL(DESKTOP_RUNTIME_RECOVERY_PAGE_URL);
1128
1129
  }
1129
- if (process.env.LIVEDESK_E2E_HEADLESS !== '1') showWindow();
1130
+ if (showOnInitialLoad && process.env.LIVEDESK_E2E_HEADLESS !== '1') showWindow();
1130
1131
  },
1131
1132
  loadRuntime: async () => {
1132
1133
  if (window.isDestroyed() || window.webContents.isDestroyed()) {
@@ -1138,7 +1139,7 @@ function createMainWindow() {
1138
1139
  loadFailureCount = 0;
1139
1140
  updateTray('Running');
1140
1141
  log(`desktop renderer loaded current runtime; generation=${context.generation}; attempt=${context.attempt}`);
1141
- if (process.env.LIVEDESK_E2E_HEADLESS !== '1') showWindow();
1142
+ if (showOnInitialLoad && process.env.LIVEDESK_E2E_HEADLESS !== '1') showWindow();
1142
1143
  },
1143
1144
  onAttemptError: (error, context) => {
1144
1145
  reportWindowLoadFailure(`${context.phase}: ${error?.message || error}`, context);
@@ -1258,9 +1259,13 @@ function registerIpc() {
1258
1259
  // Supabase may emit a null renderer session after a temporary failure.
1259
1260
  // Durable auth is cleared only by the explicit auth:sign-out IPC.
1260
1261
  return { ok: true, ignoredImplicitSignOut: true };
1261
- });
1262
- handle('app:quit', () => { app.quit(); return { ok: true }; });
1263
- handle('runtime:get-status', async () => {
1262
+ });
1263
+ handle('app:quit', () => { app.quit(); return { ok: true }; });
1264
+ handle('app:get-auto-start', () => desktopAutoStartOwner?.getStatus()
1265
+ || { supported: false, enabled: false, registered: false, platform: process.platform });
1266
+ handle('app:set-auto-start', enabled => desktopAutoStartOwner?.setEnabled(Boolean(enabled))
1267
+ || { ok: false, supported: false, enabled: false, registered: false, platform: process.platform, error: 'desktop-auto-start-unavailable' });
1268
+ handle('runtime:get-status', async () => {
1264
1269
  try { return await fetch(`${LOCAL_RUNTIME_URL}api/runtime/status`).then(response => response.json()); }
1265
1270
  catch (error) { return { ok: false, error: mask(error?.message || error) }; }
1266
1271
  });
@@ -1321,7 +1326,15 @@ function createTray() {
1321
1326
  { label: 'About VuvoDesk', click: () => { void dialog.showMessageBox(mainWindow, { type: 'info', title: APP_NAME, message: APP_NAME, detail: `Version ${app.getVersion()}\nOne VuvoDesk runtime with Hub and Client roles.` }); } },
1322
1327
  { label: 'Open logs', click: () => { void shell.openPath(logDir); } },
1323
1328
  { type: 'separator' },
1324
- { label: 'Start with Windows', type: 'checkbox', checked: app.getLoginItemSettings().openAtLogin, click: item => app.setLoginItemSettings({ openAtLogin: item.checked, name: DESKTOP_STARTUP_ENTRY_NAME }) },
1329
+ {
1330
+ label: 'Start automatically',
1331
+ type: 'checkbox',
1332
+ checked: desktopAutoStartOwner?.getStatus().enabled === true,
1333
+ click: item => {
1334
+ const result = desktopAutoStartOwner?.setEnabled(item.checked);
1335
+ if (result && result.ok !== true) item.checked = !item.checked;
1336
+ }
1337
+ },
1325
1338
  { label: 'Quit VuvoDesk', click: () => { app.quit(); } }
1326
1339
  ]);
1327
1340
  tray.setContextMenu(menu);
@@ -1350,10 +1363,18 @@ if (!app.requestSingleInstanceLock()) {
1350
1363
  if (protocolUrl) void handleProtocolUrl(protocolUrl);
1351
1364
  showWindow();
1352
1365
  });
1353
- app.whenReady().then(async () => {
1366
+ app.whenReady().then(async () => {
1354
1367
  app.setAsDefaultProtocolClient('livedesk');
1355
1368
  removeLegacyStartupEntries();
1356
1369
  migrateLegacyPlaintextSession();
1370
+ desktopAutoStartOwner = createDesktopAutoStartOwner({
1371
+ app,
1372
+ stateRoot,
1373
+ isPackaged: app.isPackaged && DESKTOP_BUILD_FLAVOR !== 'e2e',
1374
+ log
1375
+ });
1376
+ const autoStartStatus = desktopAutoStartOwner.reconcile();
1377
+ log(`desktop auto-start enabled=${autoStartStatus.enabled} registered=${autoStartStatus.registered} platform=${autoStartStatus.platform} ok=${autoStartStatus.ok === true}`);
1357
1378
  let lastProductUpdateLogSignature = '';
1358
1379
  productUpdates = createProductUpdateManager({
1359
1380
  app,
@@ -1422,7 +1443,10 @@ if (!app.requestSingleInstanceLock()) {
1422
1443
  log(`secure auth session runtime sync failed: ${error?.message || error}`);
1423
1444
  }
1424
1445
  }
1425
- createMainWindow();
1446
+ const showOnInitialLoad = !desktopAutoStartOwner.wasOpenedAutomatically()
1447
+ || !startupSession
1448
+ || !startupRole?.role;
1449
+ createMainWindow({ showOnInitialLoad });
1426
1450
  createTray();
1427
1451
  if (DESKTOP_BUILD_FLAVOR !== 'e2e') recurringProductUpdates.start();
1428
1452
  if (DESKTOP_BUILD_FLAVOR === 'e2e') {
@@ -3,10 +3,12 @@ const { contextBridge, ipcRenderer } = require('electron');
3
3
  contextBridge.exposeInMainWorld('liveDesk', {
4
4
  app: {
5
5
  getVersion: () => ipcRenderer.invoke('app:get-version'),
6
- restart: () => ipcRenderer.invoke('app:restart'),
7
- quit: () => ipcRenderer.invoke('app:quit'),
8
- about: () => ipcRenderer.invoke('app:about'),
9
- showWindow: () => ipcRenderer.invoke('app:show-window')
6
+ restart: () => ipcRenderer.invoke('app:restart'),
7
+ quit: () => ipcRenderer.invoke('app:quit'),
8
+ about: () => ipcRenderer.invoke('app:about'),
9
+ showWindow: () => ipcRenderer.invoke('app:show-window'),
10
+ getAutoStart: () => ipcRenderer.invoke('app:get-auto-start'),
11
+ setAutoStart: enabled => ipcRenderer.invoke('app:set-auto-start', Boolean(enabled))
10
12
  },
11
13
  auth: {
12
14
  signInWithGoogle: () => ipcRenderer.invoke('auth:sign-in-google'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.618",
3
+ "version": "0.1.619",
4
4
  "livedeskClientVersion": "0.1.266",
5
5
  "buildFlavor": "production",
6
6
  "description": "VuvoDesk Hub and client launcher",