@jian-ai/remote-control 0.8.3 → 0.8.5

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
@@ -37,7 +37,8 @@ jian-remote stop | start | restart
37
37
  ```
38
38
 
39
39
  `start` / `restart` stop any previous worker before launching one process (LaunchAgent,
40
- systemd --user, or Task Scheduler).
40
+ systemd `--user` or system unit, or Task Scheduler). On Linux headless/root hosts without a
41
+ user bus, `install` falls back to a system unit (or pass `install --system`).
41
42
 
42
43
  ## Configure
43
44
 
@@ -2,17 +2,30 @@
2
2
  import { spawn, spawnSync } from 'node:child_process';
3
3
  import { createHash } from 'node:crypto';
4
4
  import {
5
+ accessSync,
5
6
  existsSync,
7
+ constants as fsConstants,
6
8
  mkdtempSync,
9
+ readdirSync,
7
10
  readFileSync,
8
11
  realpathSync,
9
12
  rmSync,
10
13
  statSync,
11
14
  writeFileSync,
12
15
  } from 'node:fs';
13
- import { chmod, copyFile, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
14
- import { homedir, tmpdir } from 'node:os';
15
- import { dirname, join, resolve } from 'node:path';
16
+ import {
17
+ chmod,
18
+ copyFile,
19
+ mkdir,
20
+ readdir,
21
+ readFile,
22
+ rename,
23
+ rm,
24
+ unlink,
25
+ writeFile,
26
+ } from 'node:fs/promises';
27
+ import { homedir, tmpdir, userInfo } from 'node:os';
28
+ import { basename, dirname, join, resolve } from 'node:path';
16
29
  import { fileURLToPath, pathToFileURL } from 'node:url';
17
30
 
18
31
  const configDir = join(homedir(), '.config', 'jian-remote');
@@ -24,6 +37,9 @@ const workerLockPath = join(configDir, 'worker.pid');
24
37
  const updateStatePath = join(configDir, 'update-state.json');
25
38
  const MACOS_LAUNCH_AGENT_LABEL = 'com.jian.remote';
26
39
  const LINUX_SERVICE_UNIT = 'jian-remote.service';
40
+ /** Remembers whether Linux install used systemd --user or a system unit (headless/root VPS). */
41
+ const linuxServiceScopePath = join(configDir, 'linux-service-scope');
42
+ const LINUX_SYSTEM_UNIT_PATH = join('/etc/systemd/system', LINUX_SERVICE_UNIT);
27
43
  const WINDOWS_TASK_NAME = 'Jian Remote';
28
44
  /** Minimal PATH for launchd — app-identity Node copies otherwise inherit an empty env. */
29
45
  export const LAUNCH_AGENT_PATH = '/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin';
@@ -53,7 +69,7 @@ export function parseCliInvocation(argv = args) {
53
69
  * Stamped to the release tag on npm publish and when the gateway serves this script.
54
70
  * Compatibility with the gateway requires the same major.minor (see versionsCompatible).
55
71
  */
56
- export const PACKAGE_VERSION = '0.8.3';
72
+ export const PACKAGE_VERSION = '0.8.5';
57
73
 
58
74
  /** Soft stdout/stderr caps while capturing a command (chars). Gateway schema matches these. */
59
75
  const STDOUT_CAP = 400_000;
@@ -142,12 +158,291 @@ export function macosAppBundlePaths(home = homedir()) {
142
158
  appRoot,
143
159
  contents: join(appRoot, 'Contents'),
144
160
  macosDir: join(appRoot, 'Contents', 'MacOS'),
161
+ /** Homebrew / official Node often load `@rpath/libnode.*.dylib` from here. */
162
+ libDir: join(appRoot, 'Contents', 'lib'),
145
163
  executable: join(appRoot, 'Contents', 'MacOS', MACOS_APP_NAME),
146
164
  infoPlist: join(appRoot, 'Contents', 'Info.plist'),
147
165
  bundleId: MACOS_BUNDLE_ID,
148
166
  };
149
167
  }
150
168
 
169
+ /** dyld system locations — leave these on the host; do not copy into the app. */
170
+ export function isMacSystemLibrary(depPath) {
171
+ if (typeof depPath !== 'string' || !depPath) return true;
172
+ if (depPath.startsWith('/usr/lib/')) return true;
173
+ if (depPath.startsWith('/System/')) return true;
174
+ if (depPath.startsWith('/Library/Apple/')) return true;
175
+ if (depPath.startsWith('/usr/local/lib/libSystem')) return true;
176
+ return false;
177
+ }
178
+
179
+ /**
180
+ * Parse `otool -L` lines into load-command paths (skips the header "path:" line).
181
+ * Example line: `\t@rpath/libnode.147.dylib (compatibility version 147.0.0, current version …)`
182
+ */
183
+ export function parseOtoolDependencies(stdout) {
184
+ const deps = [];
185
+ for (const line of String(stdout || '').split('\n')) {
186
+ const trimmed = line.trim();
187
+ if (!trimmed || trimmed.endsWith(':')) continue;
188
+ const match = /^(.+?)\s+\(compatibility version\b/.exec(trimmed);
189
+ if (!match) continue;
190
+ const dep = match[1].trim();
191
+ if (dep) deps.push(dep);
192
+ }
193
+ return deps;
194
+ }
195
+
196
+ /**
197
+ * Parse LC_RPATH entries from `otool -l` (or `otool -l -v`) output.
198
+ * Looks for: `cmd LC_RPATH` … `path <value> (offset …)`
199
+ */
200
+ export function parseOtoolRpaths(stdout) {
201
+ const rpaths = [];
202
+ const lines = String(stdout || '').split('\n');
203
+ for (let i = 0; i < lines.length; i += 1) {
204
+ if (!/\bLC_RPATH\b/.test(lines[i])) continue;
205
+ for (let j = i + 1; j < Math.min(i + 6, lines.length); j += 1) {
206
+ const match = /^\s*path\s+(.+?)(?:\s+\(offset\s+\d+\))?\s*$/.exec(lines[j]);
207
+ if (match) {
208
+ rpaths.push(match[1].trim());
209
+ break;
210
+ }
211
+ }
212
+ }
213
+ return rpaths;
214
+ }
215
+
216
+ /**
217
+ * Expand @executable_path / @loader_path / @rpath against the source Node layout.
218
+ * Absolute paths pass through. Unresolvable @rpath names return null.
219
+ */
220
+ export function resolveMachODependency(
221
+ dep,
222
+ { binaryPath, executablePath, rpaths = [], exists = existsSync } = {},
223
+ ) {
224
+ if (typeof dep !== 'string' || !dep) return null;
225
+ const loaderDir = dirname(binaryPath);
226
+ const execDir = dirname(executablePath || binaryPath);
227
+
228
+ const expandPrefix = (value) => {
229
+ if (value.startsWith('@loader_path')) {
230
+ return join(loaderDir, value.slice('@loader_path'.length).replace(/^\//, '') || '.');
231
+ }
232
+ if (value.startsWith('@executable_path')) {
233
+ return join(execDir, value.slice('@executable_path'.length).replace(/^\//, '') || '.');
234
+ }
235
+ return value;
236
+ };
237
+
238
+ if (dep.startsWith('@rpath/')) {
239
+ const rest = dep.slice('@rpath/'.length);
240
+ for (const rpath of rpaths) {
241
+ const base = expandPrefix(rpath);
242
+ const candidate = join(base, rest);
243
+ if (exists(candidate)) return candidate;
244
+ }
245
+ // Common Node layouts when LC_RPATH is missing from a stub parse: bin/ + ../lib/.
246
+ for (const candidate of [
247
+ join(loaderDir, rest),
248
+ join(loaderDir, '..', 'lib', rest),
249
+ join(execDir, rest),
250
+ join(execDir, '..', 'lib', rest),
251
+ ]) {
252
+ if (exists(candidate)) return candidate;
253
+ }
254
+ return null;
255
+ }
256
+
257
+ if (dep.startsWith('@loader_path') || dep.startsWith('@executable_path')) {
258
+ const candidate = expandPrefix(dep);
259
+ return exists(candidate) ? candidate : null;
260
+ }
261
+
262
+ if (dep.startsWith('/')) {
263
+ return exists(dep) ? dep : null;
264
+ }
265
+
266
+ return null;
267
+ }
268
+
269
+ const SIBLING_DYLIB_PREFIXES = [
270
+ 'libnode.',
271
+ 'libuv.',
272
+ 'libbrotli',
273
+ 'libcares.',
274
+ 'libnghttp',
275
+ 'libopenssl.',
276
+ 'libssl.',
277
+ 'libcrypto.',
278
+ 'libicu',
279
+ ];
280
+
281
+ /**
282
+ * Sibling search when otool is unavailable or incomplete — Homebrew Cellar and
283
+ * official tarballs keep libnode next to bin/ or under ../lib/.
284
+ */
285
+ export function discoverSiblingNodeLibraries(nodePath, { exists = existsSync, listDir } = {}) {
286
+ const dir = dirname(nodePath);
287
+ const searchDirs = [dir, join(dir, 'lib'), join(dir, '..', 'lib')];
288
+ const found = [];
289
+ const list =
290
+ listDir ||
291
+ ((path) => {
292
+ try {
293
+ return readdirSync(path);
294
+ } catch {
295
+ return [];
296
+ }
297
+ });
298
+ for (const searchDir of searchDirs) {
299
+ if (!exists(searchDir)) continue;
300
+ for (const name of list(searchDir)) {
301
+ if (!name.endsWith('.dylib')) continue;
302
+ if (!SIBLING_DYLIB_PREFIXES.some((prefix) => name.startsWith(prefix))) continue;
303
+ const full = join(searchDir, name);
304
+ if (exists(full)) found.push(full);
305
+ }
306
+ }
307
+ return found;
308
+ }
309
+
310
+ /**
311
+ * BFS: every non-system Mach-O dependency of `nodePath` that must live inside the .app.
312
+ * Returns absolute source paths (deduped, stable order).
313
+ */
314
+ export function collectNodeRuntimeLibraries(
315
+ nodePath,
316
+ {
317
+ exists = existsSync,
318
+ readDeps = defaultReadOtoolDeps,
319
+ readRpaths = defaultReadOtoolRpaths,
320
+ listDir,
321
+ } = {},
322
+ ) {
323
+ const realNode = (() => {
324
+ try {
325
+ return realpathSync(nodePath);
326
+ } catch {
327
+ return nodePath;
328
+ }
329
+ })();
330
+
331
+ const queue = [realNode];
332
+ const seenBinaries = new Set();
333
+ const libraries = [];
334
+ const seenLibs = new Set();
335
+
336
+ const enqueueLib = (source) => {
337
+ let real = source;
338
+ try {
339
+ real = realpathSync(source);
340
+ } catch {
341
+ // keep source
342
+ }
343
+ if (seenLibs.has(real)) return;
344
+ seenLibs.add(real);
345
+ libraries.push(real);
346
+ queue.push(real);
347
+ };
348
+
349
+ // Seed Cellar/tarball siblings first so a missing otool still walks transitive deps.
350
+ for (const sibling of discoverSiblingNodeLibraries(realNode, { exists, listDir })) {
351
+ enqueueLib(sibling);
352
+ }
353
+
354
+ while (queue.length > 0) {
355
+ const current = queue.shift();
356
+ if (!current || seenBinaries.has(current)) continue;
357
+ seenBinaries.add(current);
358
+
359
+ const rpaths = readRpaths(current);
360
+ const deps = readDeps(current);
361
+ for (const dep of deps) {
362
+ if (isMacSystemLibrary(dep)) continue;
363
+ // Self-id lines sometimes appear as the dylib's install name — skip when it resolves to self.
364
+ const resolved = resolveMachODependency(dep, {
365
+ binaryPath: current,
366
+ executablePath: realNode,
367
+ rpaths,
368
+ exists,
369
+ });
370
+ if (!resolved) continue;
371
+ let realResolved = resolved;
372
+ try {
373
+ realResolved = realpathSync(resolved);
374
+ } catch {
375
+ // keep
376
+ }
377
+ if (realResolved === realNode || realResolved === current) continue;
378
+ if (isMacSystemLibrary(realResolved)) continue;
379
+ enqueueLib(realResolved);
380
+ }
381
+ }
382
+
383
+ return libraries;
384
+ }
385
+
386
+ function defaultReadOtoolDeps(binaryPath) {
387
+ const result = spawnSync('otool', ['-L', binaryPath], { encoding: 'utf8' });
388
+ if (result.status !== 0) return [];
389
+ return parseOtoolDependencies(result.stdout || '');
390
+ }
391
+
392
+ function defaultReadOtoolRpaths(binaryPath) {
393
+ const result = spawnSync('otool', ['-l', binaryPath], { encoding: 'utf8' });
394
+ if (result.status !== 0) return [];
395
+ return parseOtoolRpaths(result.stdout || '');
396
+ }
397
+
398
+ /** Drop Gatekeeper quarantine on a path (best-effort; missing xattr is fine). */
399
+ export function clearMacQuarantine(targetPath) {
400
+ spawnSync('xattr', ['-dr', 'com.apple.quarantine', targetPath], { encoding: 'utf8' });
401
+ }
402
+
403
+ function runInstallNameTool(argv, run) {
404
+ // -add_rpath fails (status 1) when the rpath already exists — that is fine.
405
+ return run('install_name_tool', argv);
406
+ }
407
+
408
+ /**
409
+ * Rewrite load commands so bundled libs resolve via `@rpath/<basename>` inside Contents/lib.
410
+ * Best-effort: missing install_name_tool still leaves copies under Contents/lib for @rpath.
411
+ */
412
+ export function rewriteBundledMachOLinks({
413
+ executablePath,
414
+ libDir,
415
+ libraryBasenames,
416
+ readDeps = defaultReadOtoolDeps,
417
+ run = (cmd, argv) => spawnSync(cmd, argv, { encoding: 'utf8' }),
418
+ }) {
419
+ const names = new Set(libraryBasenames);
420
+
421
+ runInstallNameTool(['-add_rpath', '@executable_path/../lib', executablePath], run);
422
+ runInstallNameTool(['-add_rpath', '@executable_path', executablePath], run);
423
+
424
+ for (const name of names) {
425
+ runInstallNameTool(['-id', `@rpath/${name}`, join(libDir, name)], run);
426
+ }
427
+
428
+ const consumers = [executablePath, ...[...names].map((name) => join(libDir, name))];
429
+ for (const consumer of consumers) {
430
+ for (const dep of readDeps(consumer)) {
431
+ if (isMacSystemLibrary(dep)) continue;
432
+ const depBase = basename(
433
+ dep
434
+ .replace(/^@rpath\//, '')
435
+ .replace(/^@loader_path\//, '')
436
+ .replace(/^@executable_path\//, ''),
437
+ );
438
+ if (!names.has(depBase)) continue;
439
+ const desired = `@rpath/${depBase}`;
440
+ if (dep === desired) continue;
441
+ runInstallNameTool(['-change', dep, desired, consumer], run);
442
+ }
443
+ }
444
+ }
445
+
151
446
  export function buildMacosAppInfoPlist() {
152
447
  return `<?xml version="1.0" encoding="UTF-8"?>
153
448
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -214,26 +509,78 @@ export function buildLaunchAgentPlist({
214
509
  }
215
510
 
216
511
  /**
217
- * Copy the current Node binary into Jian Remote.app and ad-hoc sign so Login Items / TCC
218
- * attach to Jian, not the shared system node (unsigned / ad-hoc — not Developer ID).
512
+ * Copy the current Node binary *and* its @rpath shared libs (libnode, ICU, …) into
513
+ * Jian Remote.app, then ad-hoc sign so Login Items / TCC attach to Jian — not system node.
514
+ * Homebrew / nvm / fnm / official pkg layouts resolve via Contents/lib (@executable_path/../lib).
219
515
  */
220
- async function materializeMacosAppRuntime() {
221
- const paths = macosAppBundlePaths();
516
+ export async function materializeMacosAppRuntime({
517
+ sourceNode = process.execPath,
518
+ home = homedir(),
519
+ collectLibraries = collectNodeRuntimeLibraries,
520
+ } = {}) {
521
+ const paths = macosAppBundlePaths(home);
222
522
  await mkdir(paths.macosDir, { recursive: true, mode: 0o755 });
523
+ await mkdir(paths.libDir, { recursive: true, mode: 0o755 });
223
524
  await writeFile(paths.infoPlist, buildMacosAppInfoPlist(), { mode: 0o644 });
224
- const source = process.execPath;
525
+
526
+ let realSource = sourceNode;
527
+ try {
528
+ realSource = realpathSync(sourceNode);
529
+ } catch {
530
+ // keep sourceNode
531
+ }
532
+
225
533
  const temp = `${paths.executable}.${process.pid}.tmp`;
226
- await copyFile(source, temp);
534
+ await copyFile(realSource, temp);
227
535
  await chmod(temp, 0o755);
228
536
  await rename(temp, paths.executable);
229
- // Best-effort ad-hoc sign with our bundle id; ignore failures on locked-down hosts.
537
+
538
+ // Drop stale dylibs from a previous Node major so @rpath cannot pick an old libnode.
539
+ try {
540
+ for (const name of await readdir(paths.libDir)) {
541
+ if (!name.endsWith('.dylib')) continue;
542
+ await rm(join(paths.libDir, name), { force: true });
543
+ }
544
+ } catch {
545
+ // lib dir may be brand new
546
+ }
547
+
548
+ const librarySources = collectLibraries(realSource);
549
+ const libraryBasenames = [];
550
+ for (const libSource of librarySources) {
551
+ const name = basename(libSource);
552
+ if (!name.endsWith('.dylib')) continue;
553
+ const dest = join(paths.libDir, name);
554
+ await copyFile(libSource, dest);
555
+ await chmod(dest, 0o755);
556
+ libraryBasenames.push(name);
557
+ }
558
+
559
+ if (libraryBasenames.length > 0) {
560
+ rewriteBundledMachOLinks({
561
+ executablePath: paths.executable,
562
+ libDir: paths.libDir,
563
+ libraryBasenames,
564
+ });
565
+ }
566
+
567
+ clearMacQuarantine(paths.appRoot);
568
+
569
+ // Sign nested dylibs first, then the executable, then the bundle (ad-hoc / unsigned path).
570
+ for (const name of libraryBasenames) {
571
+ spawnSync(
572
+ 'codesign',
573
+ ['--force', '-s', '-', '--identifier', MACOS_BUNDLE_ID, join(paths.libDir, name)],
574
+ { stdio: 'ignore' },
575
+ );
576
+ }
230
577
  spawnSync('codesign', ['--force', '-s', '-', '--identifier', MACOS_BUNDLE_ID, paths.executable], {
231
578
  stdio: 'ignore',
232
579
  });
233
580
  spawnSync('codesign', ['--force', '-s', '-', '--identifier', MACOS_BUNDLE_ID, paths.appRoot], {
234
581
  stdio: 'ignore',
235
582
  });
236
- return paths;
583
+ return { ...paths, bundledLibraries: libraryBasenames };
237
584
  }
238
585
 
239
586
  export function parseRelease(version) {
@@ -726,13 +1073,174 @@ function macosPlistPath() {
726
1073
  return join(homedir(), 'Library', 'LaunchAgents', `${MACOS_LAUNCH_AGENT_LABEL}.plist`);
727
1074
  }
728
1075
 
729
- function linuxUnitPath() {
1076
+ function linuxUserUnitPath() {
730
1077
  return join(homedir(), '.config', 'systemd', 'user', LINUX_SERVICE_UNIT);
731
1078
  }
732
1079
 
733
- export function serviceKindForPlatform(platform = process.platform) {
1080
+ /**
1081
+ * True when this process can talk to the per-user systemd bus.
1082
+ * Headless VPS / root SSH sessions often lack XDG_RUNTIME_DIR and fail with
1083
+ * "Failed to connect to bus: No medium found".
1084
+ */
1085
+ export function linuxUserBusAvailable({
1086
+ env = process.env,
1087
+ run = (argv) => spawnSync('systemctl', ['--user', ...argv], { encoding: 'utf8' }),
1088
+ } = {}) {
1089
+ const runtimeDir = env.XDG_RUNTIME_DIR;
1090
+ if (!runtimeDir || typeof runtimeDir !== 'string') return false;
1091
+ try {
1092
+ if (!statSync(runtimeDir).isDirectory()) return false;
1093
+ } catch {
1094
+ return false;
1095
+ }
1096
+ const result = run(['show-environment']);
1097
+ if (result.error) return false;
1098
+ const stderr = `${result.stderr ?? ''}`;
1099
+ if (/Failed to connect to bus|No medium found|No such file or directory/i.test(stderr)) {
1100
+ return false;
1101
+ }
1102
+ return result.status === 0;
1103
+ }
1104
+
1105
+ /** True when this process can install `/etc/systemd/system/jian-remote.service`. */
1106
+ export function canInstallLinuxSystemUnit({
1107
+ uid = typeof process.getuid === 'function' ? process.getuid() : undefined,
1108
+ systemDir = '/etc/systemd/system',
1109
+ access = (path, mode) => accessSync(path, mode),
1110
+ } = {}) {
1111
+ if (uid === 0) return true;
1112
+ try {
1113
+ access(systemDir, fsConstants.W_OK);
1114
+ return true;
1115
+ } catch {
1116
+ return false;
1117
+ }
1118
+ }
1119
+
1120
+ export function linuxServiceKind(scope = 'user') {
1121
+ return scope === 'system' ? 'systemd (system)' : 'systemd --user';
1122
+ }
1123
+
1124
+ /**
1125
+ * Pick systemd --user vs system unit. Prefer --user when the bus works; fall back to a
1126
+ * system unit on headless/root hosts; `--system` forces the system unit.
1127
+ */
1128
+ export function chooseLinuxServiceScope({
1129
+ preferSystem = false,
1130
+ env = process.env,
1131
+ userBusAvailable = () => linuxUserBusAvailable({ env }),
1132
+ canSystem = () => canInstallLinuxSystemUnit(),
1133
+ } = {}) {
1134
+ if (preferSystem) {
1135
+ if (!canSystem()) {
1136
+ throw new Error(
1137
+ 'install --system needs write access to /etc/systemd/system (run as root, or use sudo).',
1138
+ );
1139
+ }
1140
+ return 'system';
1141
+ }
1142
+ if (userBusAvailable()) return 'user';
1143
+ if (canSystem()) return 'system';
1144
+ throw new Error(
1145
+ [
1146
+ 'Could not reach systemd --user (no user bus). This is common on a headless VPS or root SSH session',
1147
+ '("Failed to connect to bus: No medium found" / missing XDG_RUNTIME_DIR).',
1148
+ '',
1149
+ 'Fix options:',
1150
+ ' 1. Prefer a normal user + lingering, then reinstall:',
1151
+ ' sudo loginctl enable-linger USERNAME',
1152
+ ' # as USERNAME:',
1153
+ ' export XDG_RUNTIME_DIR=/run/user/$(id -u)',
1154
+ ' jian-remote install --gateway … --machine … --token …',
1155
+ ' 2. Install a system unit (as root):',
1156
+ ' jian-remote install --system --gateway … --machine … --token …',
1157
+ ' 3. Foreground smoke test (config may already be on disk from a failed install):',
1158
+ ' node ~/.config/jian-remote/jian-remote.mjs run',
1159
+ ].join('\n'),
1160
+ );
1161
+ }
1162
+
1163
+ async function writeLinuxServiceScope(scope) {
1164
+ await mkdir(configDir, { recursive: true, mode: 0o700 });
1165
+ await writeFile(linuxServiceScopePath, `${scope}\n`, { mode: 0o600 });
1166
+ }
1167
+
1168
+ async function readLinuxServiceScope() {
1169
+ try {
1170
+ const text = (await readFile(linuxServiceScopePath, 'utf8')).trim();
1171
+ if (text === 'system' || text === 'user') return text;
1172
+ } catch {
1173
+ // absent or unreadable — fall through to heuristics
1174
+ }
1175
+ if (existsSync(LINUX_SYSTEM_UNIT_PATH)) return 'system';
1176
+ return 'user';
1177
+ }
1178
+
1179
+ /**
1180
+ * Fresh `install` probes the bus (and honors `--system`). Later start/restart/update/configure
1181
+ * keep the saved scope so a system-unit VPS install does not flip back if a user bus appears.
1182
+ */
1183
+ async function resolveLinuxInstallScope() {
1184
+ const preferSystem = args.includes('--system');
1185
+ if (preferSystem) return chooseLinuxServiceScope({ preferSystem: true });
1186
+ const command = parseCliInvocation(args).command;
1187
+ if (command && command !== 'install') {
1188
+ try {
1189
+ const saved = (await readFile(linuxServiceScopePath, 'utf8')).trim();
1190
+ if (saved === 'system') {
1191
+ if (canInstallLinuxSystemUnit()) return 'system';
1192
+ } else if (saved === 'user' && linuxUserBusAvailable()) {
1193
+ return 'user';
1194
+ }
1195
+ } catch {
1196
+ // choose below
1197
+ }
1198
+ }
1199
+ return chooseLinuxServiceScope({ preferSystem: false });
1200
+ }
1201
+
1202
+ export function buildLinuxServiceUnit({
1203
+ script,
1204
+ scope,
1205
+ home = homedir(),
1206
+ nodePath = process.execPath,
1207
+ uid = typeof process.getuid === 'function' ? process.getuid() : 0,
1208
+ username,
1209
+ } = {}) {
1210
+ const quote = (value) => `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
1211
+ const wantedBy = scope === 'system' ? 'multi-user.target' : 'default.target';
1212
+ const lines = [
1213
+ '[Unit]',
1214
+ 'Description=Jian Remote worker',
1215
+ 'After=network-online.target',
1216
+ 'Wants=network-online.target',
1217
+ '',
1218
+ '[Service]',
1219
+ `ExecStart=${quote(nodePath)} ${quote(script)} run`,
1220
+ 'Restart=always',
1221
+ 'RestartSec=3',
1222
+ // System units do not inherit the installing shell's HOME; pin it so config stays findable.
1223
+ `Environment=${quote(`HOME=${home}`)}`,
1224
+ `WorkingDirectory=${quote(home)}`,
1225
+ ];
1226
+ if (scope === 'system' && uid !== 0) {
1227
+ let name = username;
1228
+ if (!name) {
1229
+ try {
1230
+ name = userInfo().username;
1231
+ } catch {
1232
+ name = undefined;
1233
+ }
1234
+ }
1235
+ if (name) lines.push(`User=${name}`);
1236
+ }
1237
+ lines.push('', '[Install]', `WantedBy=${wantedBy}`, '');
1238
+ return lines.join('\n');
1239
+ }
1240
+
1241
+ export function serviceKindForPlatform(platform = process.platform, linuxScope = 'user') {
734
1242
  if (platform === 'darwin') return 'LaunchAgent';
735
- if (platform === 'linux') return 'systemd --user';
1243
+ if (platform === 'linux') return linuxServiceKind(linuxScope);
736
1244
  if (platform === 'win32') return 'Task Scheduler';
737
1245
  return 'none';
738
1246
  }
@@ -742,7 +1250,8 @@ export function serviceKindForPlatform(platform = process.platform) {
742
1250
  * Clear success/failure for the `jian-remote stop` CLI.
743
1251
  */
744
1252
  async function stopService() {
745
- const kind = serviceKindForPlatform();
1253
+ const linuxScope = process.platform === 'linux' ? await readLinuxServiceScope() : 'user';
1254
+ const kind = serviceKindForPlatform(process.platform, linuxScope);
746
1255
  if (process.platform === 'darwin') {
747
1256
  const plistPath = macosPlistPath();
748
1257
  const domain = `gui/${process.getuid()}`;
@@ -751,7 +1260,11 @@ async function stopService() {
751
1260
  stdio: 'ignore',
752
1261
  });
753
1262
  } else if (process.platform === 'linux') {
754
- spawnSync('systemctl', ['--user', 'stop', LINUX_SERVICE_UNIT], { stdio: 'ignore' });
1263
+ if (linuxScope === 'system') {
1264
+ spawnSync('systemctl', ['stop', LINUX_SERVICE_UNIT], { stdio: 'ignore' });
1265
+ } else {
1266
+ spawnSync('systemctl', ['--user', 'stop', LINUX_SERVICE_UNIT], { stdio: 'ignore' });
1267
+ }
755
1268
  } else if (process.platform === 'win32') {
756
1269
  spawnSync('schtasks.exe', ['/End', '/TN', WINDOWS_TASK_NAME], {
757
1270
  stdio: 'ignore',
@@ -829,17 +1342,62 @@ async function startService(script = installedScript) {
829
1342
  };
830
1343
  }
831
1344
  if (process.platform === 'linux') {
1345
+ const scope = await resolveLinuxInstallScope();
1346
+ const unitBody = buildLinuxServiceUnit({ script, scope });
1347
+ if (scope === 'system') {
1348
+ // Drop a leftover user unit so stop/start do not fight two managers.
1349
+ spawnSync('systemctl', ['--user', 'disable', '--now', LINUX_SERVICE_UNIT], {
1350
+ stdio: 'ignore',
1351
+ });
1352
+ try {
1353
+ await unlink(linuxUserUnitPath());
1354
+ } catch {
1355
+ // absent
1356
+ }
1357
+ await writeFile(LINUX_SYSTEM_UNIT_PATH, unitBody, { mode: 0o644 });
1358
+ await writeLinuxServiceScope('system');
1359
+ for (const command of [['daemon-reload'], ['enable', '--now', LINUX_SERVICE_UNIT]]) {
1360
+ const result = spawnSync('systemctl', command, { stdio: 'inherit' });
1361
+ if (result.status !== 0) {
1362
+ throw new Error('Could not install/start Jian Remote with systemd (system unit)');
1363
+ }
1364
+ }
1365
+ if (!args.includes('--system') && parseCliInvocation(args).command === 'install') {
1366
+ printInfo(
1367
+ 'systemd --user bus unavailable; installed a system unit instead (headless/root). Prefer a normal user + loginctl enable-linger when you can.',
1368
+ );
1369
+ }
1370
+ return { service: linuxServiceKind('system'), detail: LINUX_SYSTEM_UNIT_PATH };
1371
+ }
1372
+
832
1373
  const unitDir = join(homedir(), '.config', 'systemd', 'user');
833
1374
  await mkdir(unitDir, { recursive: true, mode: 0o700 });
834
- const quote = (value) => `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
835
- const unit = `[Unit]\nDescription=Jian Remote worker\nAfter=network-online.target\n\n[Service]\nExecStart=${quote(process.execPath)} ${quote(script)} run\nRestart=always\nRestartSec=3\n\n[Install]\nWantedBy=default.target\n`;
836
- await writeFile(linuxUnitPath(), unit, { mode: 0o600 });
1375
+ await writeFile(linuxUserUnitPath(), unitBody, { mode: 0o600 });
1376
+ await writeLinuxServiceScope('user');
1377
+ // Best-effort: clear a previous system install if this user can.
1378
+ if (existsSync(LINUX_SYSTEM_UNIT_PATH) && canInstallLinuxSystemUnit()) {
1379
+ spawnSync('systemctl', ['disable', '--now', LINUX_SERVICE_UNIT], { stdio: 'ignore' });
1380
+ try {
1381
+ await unlink(LINUX_SYSTEM_UNIT_PATH);
1382
+ spawnSync('systemctl', ['daemon-reload'], { stdio: 'ignore' });
1383
+ } catch {
1384
+ // leave system unit; user scope marker wins for this CLI
1385
+ }
1386
+ }
837
1387
  for (const command of [['daemon-reload'], ['enable', '--now', LINUX_SERVICE_UNIT]]) {
838
1388
  const result = spawnSync('systemctl', ['--user', ...command], { stdio: 'inherit' });
839
- if (result.status !== 0)
840
- throw new Error('Could not install/start Jian Remote with systemd --user');
1389
+ if (result.status !== 0) {
1390
+ throw new Error(
1391
+ [
1392
+ 'Could not install/start Jian Remote with systemd --user.',
1393
+ 'If you see "Failed to connect to bus: No medium found", the user bus is missing',
1394
+ '(common on headless VPS / root SSH). Retry with: jian-remote install --system …',
1395
+ 'or enable lingering: sudo loginctl enable-linger "$(whoami)" && export XDG_RUNTIME_DIR=/run/user/$(id -u)',
1396
+ ].join(' '),
1397
+ );
1398
+ }
841
1399
  }
842
- return { service: 'systemd --user', detail: LINUX_SERVICE_UNIT };
1400
+ return { service: linuxServiceKind('user'), detail: LINUX_SERVICE_UNIT };
843
1401
  }
844
1402
  if (process.platform === 'win32') {
845
1403
  const action = `"${process.execPath}" "${script}" run`;
@@ -914,7 +1472,7 @@ async function install() {
914
1472
  const gatewayBase = parseGatewayBase(gateway);
915
1473
  if (!gatewayBase || !MACHINE_ID.test(machineId ?? '') || !token?.trim()) {
916
1474
  throw new Error(
917
- 'Usage: jian-remote install --gateway https://... --machine UUID (--token VALUE | --token-prompt | --token-stdin)',
1475
+ 'Usage: jian-remote install [--system] --gateway https://... --machine UUID (--token VALUE | --token-prompt | --token-stdin)',
918
1476
  );
919
1477
  }
920
1478
  const gatewayVersion = await assertCompatibleWithGateway(gatewayBase);
@@ -1744,7 +2302,7 @@ if (isCliEntry()) {
1744
2302
  const pkg = paint('(@jian-ai/remote-control)', ANSI.dim, color);
1745
2303
  console.log(`${title} worker ${pkg}
1746
2304
  version | --version | -V
1747
- install --gateway HTTPS_URL --machine UUID (--token VALUE | --token-prompt | --token-stdin)
2305
+ install [--system] --gateway HTTPS_URL --machine UUID (--token VALUE | --token-prompt | --token-stdin)
1748
2306
  configure [--gateway HTTPS_URL] [--machine UUID] (--token VALUE | --token-prompt | --token-stdin)
1749
2307
  update [--version X.Y.Z|latest]
1750
2308
  stop | start | restart
@@ -1752,6 +2310,7 @@ if (isCliEntry()) {
1752
2310
 
1753
2311
  Install: npm install -g @jian-ai/remote-control
1754
2312
  Or: npx @jian-ai/remote-control@latest install --gateway … --machine … --token …
2313
+ (Linux headless/root VPS: add --system, or use a normal user + loginctl enable-linger)
1755
2314
  Configure: jian-remote configure --token … (or --gateway / --machine; restarts the service)
1756
2315
  Update: npx @jian-ai/remote-control@latest update
1757
2316
  (upgrades the worker binary; reads ~/.config/jian-remote)
Binary file
@@ -94,127 +94,201 @@ private let hotspot = CGPoint(x: 12, y: 12)
94
94
 
95
95
  /// Ship asset `macos/jian-pointer.png` (@3x master) as base64 — single-file helper stays
96
96
  /// reliable when only the binary is copied into Application Support (no sidecar PNG).
97
- private let jianPointerPngBase64 =
98
- "iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAYAAAA9zQYyAAAaf0lEQVR4nO1dW6wkV3Vd+1T38EcQ"
99
- "Fi+bGc+MZ+w4g4AoJIoQkSIh8oNEflAUPhARQrZxHIcoxglfiD+IQTaDI5IoEZHgI5FCflBeH0mk"
100
- "RCbGCEuQsQdzPXc8Y5vwCijhJ3du19n5OK99TlXfe7v7VN+err3s7nr0o2qq1121zt7n7AMoFAqF"
101
- "QqFQKBQKhUKhUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCoVCoTh20HGfwHyQODfm4zsPxc2EYyQ0"
102
- "VTq2kl2RsCZC95B31SPPpbESfMxYA6EFmXuPRnM35oB7V3s2ldzjw4CELlS5y9tDjl2+fCA3ufOe"
103
- "7O1K7LFgIELPU+U+IpNcULG3A04rXRJnO5TcY8QAhO4jc0lkAigxueckDjqvDlPZPfURnPuJraTe"
104
- "VlQmNM3T2STDFNgtX6O+M+nZyfFJrqLLaZ5LbCX1VqMioXvJfACRKawWxD2kEemoW8ouC4KnHUrs"
105
- "0aESoQ8nc5fIgbgkxbrfX2eQqizJGYjJGbl7iK2k3mJM6n/lXDKT42sgvyB011NT8X0Ckb2UFJn9"
106
- "+8Sqf4UAcNpmt0VuiwK3iZTU24EKCi3VeR6ZpSqLdU9wimTPuEy9ycRcjd0Tc3xy+5j9WxhJoVWt"
107
- "R4A6Ct3hXYfMXoUDodOOqNpOsenOj/3wp+FbvvPJ170yHYDlsTjxjhlgJqfDgbhecYMgu/eSV29h"
108
- "sv0fEHM6hKr1zYwVFbpPnecocyKzYzZRRvI7P/aDn/YdYedTt77KfS6KqfDK0ic7YoulV225H/F9"
109
- "c9VaY9Y3NWoSOnjjaJZz9XVLgnHbhxB5HnYeOXkLgEBMwKk1g9mz2HaJHdaL/eg2GtWC3OSoROh5"
110
- "6izJbIIqE8gQiMydf/j9/13mqM9/5sxrI6EjkQNRreW0fhDZD/DWQc2Rbys2Hit46DJUB3S8MoQy"
111
- "JzIbENGyZAYAMpOGBaHJk9XtM0Rk3TbIghypyVkOL82e1MJbewcewiRui4Jah9apEnvTsYJCp2gc"
112
- "kIjsNNoAgAEZgMgkm2GMU2pjQGTOP/zyj1c5+SuP3XVbICdHNbaWnRJbp9CWmdM6/I6uNQF61Fot"
113
- "yE2GGoSOcWXKIxnk1JKMsxhOmYkaAzLm/MMvrUTmgN2LF05xICuz9es2EdtaWGbAitekorNFEm+1"
114
- "IDc5KhE6euegxADIkPfKgdBOmRs6//BLP6l0/gCA3cfffMYR1BHZq7FlG4md9ol1562TzxYNR09p"
115
- "ZsliVevNR8VMobfMyYL4DZ88caR2Sl0ZZx/41lUA2H38LWeTOreGYFtma4hbC6aWmYiYCEyWmQC2"
116
- "TL616FhqQ6aRiV2LoIhZc0o/Ahqz3jwsQa6gwkAQeOrkqlOgw9kMQwCZqNID4ewD39ylZjqFmU7J"
117
- "TCdophNqJg2ZSQMzmRA1DfzDrRsDagyRMd4auQecPUL4h6ZsPIk7Uroeio3BquSSzE6ZvxiLjiE6"
118
- "H7YbRqElztz/9A410wk10wkZ94CZTsg4YrtHY2Aak0jtrFC6i1D2iFYqkTeRWqb0FceOupbDL72j"
119
- "TmQIihcVcFic+fDXnwOAq5//xbvAxoJMCzbEZCzZ1lkP27ZsYIkJYAKzJTAsOQthmRmgaEFC2tx1"
120
- "itK0+caiErlILoV0CZWLUQ6zNiU78+GvP0cmqPVkQmYygVs2aRktiIGPwKBrQWiOBYFakM1CDUKn"
121
- "BItMrGRZwmg5DMzwCi1x+r6vXSZzYormxDSzIs5bT4gmDZmmgWkaIiMtiFELcvNhBXJ1MoVhW0hX"
122
- "SLYEhVvMQ1/++CuWPz2B0/d+9Zkz9z31bfKkRhO89aShZuKW1BiYiSHTGOexTWzEUqHSgdRRtPPI"
123
- "jqr1MWI1tSwyheG/dBuWqmaWinLUIjUAnL73yWc9qaNaIzUWJ06pJ30WxFsmtSCbjsoeOhkP8YNL"
124
- "H+1u4QuiKqnv8WrtQnvT4LHRJFJHC2KahowRvlotyKajrp+NiRXpnfMedstGOS5//BV1iX3f1y5T"
125
- "c2JCzYkpUniv8Q1HR2oXszaO4H0WxKgF2TCsSGgSi/jjdW65FNXM37pXQF0L8h/PevvhH5NIah+v"
126
- "dkkZaky/BSG1IBuGlRMrc7YzqwGYQrFXQ2ULcslZkBOTaEOaaRbWS8tgQRq1IBuKumE7v0KxsZga"
127
- "hxSiHagTtqtvQZ70FmQaiB0yjBMkpfYWxKgF2VBUjgnnCRb/HHxJahhWRH0LcmKKEK9uJhMy08al"
128
- "0Bu1IDcBKhA6KHG+wzcQw3r+qIy6FuSJSz5mPaHmxATJW6sFuQkwQNaOslXZ7S766AFQ3YLc++Tl"
129
- "GNaL1iMsJ2pBNhQVCU0kFMc/idtt+jEH/dEqW5BnnAXx3rpJ5A72Qy3IZqF2KTAqVsm7j8EVWiKQ"
130
- "+u5P7K38XafveeISALzwp7/8c6AZsW/fsiUitK3rfkfE3PrBA3CDB6xlAiyHgQMCYlAufO88AkgH"
131
- "5VbAMI1CKrY9qZP9WA+qp807FmSS+oRIC0JqQY4LQ/R88w3BuOmNiBDrNWJ4CzKZpERMGB1T24Io"
132
- "sY+KgaqPzldqrJvRWKcFIWdBLBGbxSwIqGf8IslBuURqQQ7HkH2T0+0zhaWl9Kwdm2xBetRaLcgS"
133
- "GJDQKdxx8L71YqMtSB6z9kcRFkRj1odiAMshIZMr2AQ+A9hgC6IlFFbGmoZDldG8zcCwFmQ6z4I0"
134
- "cyyIK8hzaAkFqAU5AGsc30dRozcJx2BBjLAgjbAgvr+4iNerBVkYA1uOEuuNQx8Vw1kQ6rMgRJZa"
135
- "b0GsK6FAWsWpEtY6AnvTMYgFkQVvfO+92NdaqzhVhxK6QHULIksohH4gzVGrOFHnoT33DsaaLcfN"
136
- "gcEtiCViBAtCfVWcAD/zka9njSNUcVILAlXoAzFICQVTWpBJgxSv7liQWMXpaIkYd7ARWxAl9CGo"
137
- "XULBR0FyCxKiIKmKkyFRlqxIxJRRkH5SR4yL1Go5joDBLAjKKAiCBQEb+CgIwBaAi4VYlkkYIFgQ"
138
- "ZmIXDwlFJElOUzce+6EKvQDqW5BOPZBJqrPn+oH4uiByFoR8mBd67Qcg+9K4lVEotRJ6QdQelHvm"
139
- "w19/jprp1A/GFX56YsgkP51XcCIqrUc/qUtPvf1QQi+B2uMX3aDciR9lHhqJvnKTSRnFVDA+xqqj"
140
- "Qh/aUAQwBpVWQq+AunVBnvo2ykZiHFneITX5GcaKcmtIpC5leiTWQwm9Iuoq9dcun7n/6Z1QkN1X"
141
- "QhU+OpZL8DMixJnGQtmIRN+s68xWcziDEroCqluQ+7+xE5Q5VkPt+GjfjzqyWSp19M55NnEEUEJX"
142
- "RF1SP71DSaVD1KMR8WlKE5qaLOKB6EOyDo7lOM+thBK6MmqT+uwD37qa+nu4KT3IhBAeBesh/bT7"
143
- "cNLmPOqx5aRWQg+A2hbk7O/85wtuygxP6uijTayFAGmkc7Uei9sAoIQeFFVJ/eClayT6d6RZebNE"
144
- "C7KeeOn5gNT4dkFT3wOjZtr87IPPXgeAK4+ev5XJMoGYXZiDmVMcml1HPM9gHkfO20MVek2oqdZ3"
145
- "/N7Od8/9/tXvd3rgSZWOXjqEp3NXHV/cMiih14iapAaAcw+98MNYVSyPcABxRIsc97b9jUMl9JpR"
146
- "u8F47qHrPxIKDUjPXCQMO8zdQlIroY8JNUl9/uGXfhzZm4IbiaS0hcydAyX0MaIWqXceOXVLsh0A"
147
- "ksXI+bvtbIYS+thRw4Kc/+j1/z7/8Ms/EYNk0ekXnfcl3drwnRJ6Q1BHrbeTpItACb1BWFmp/+C/"
148
- "/kdsFoo8DiihNwyrWxA6YHP7ea2ZwgVRI+M3LMaUF+xCFXoBbD6ZM7B4znZtM5TQiq2CEnrrwfFp"
149
- "DFBCbxG+88nXvdIVlGEWHC7IvN3cPgZCb/cFPWZw4Zk5e20EWDOhmTGSC7tu7Hzy9T/jLy+H/+OL"
150
- "gddiz7b+DGsitL/QYVVRFTufesOrOLcaoq7dfMneRgwch45EdnoxrvbJ4Nh55OSrwS2DOXsIcos7"
151
- "4jiu+4CE7mOvMroGnv/06dcwtxZsGeyeMgJLHRb3Re79OYBtEu6BCC2Imyvz1nq3oXHl0fO3Mrct"
152
- "rLXM1pPZsuOztVGd2RObpQXJPHXxG2wPmYFBCC1uc3nkKPi6tV/Ao2b4DutDcfnjr1gqW/i9r/z2"
153
- "e7nd2+d2f5/bG/vc3phxu78Puz/jdn/Gdtaync0Ql61l27bMsxbWWnDbMrfWrUcye2JnVgMZX3mO"
154
- "gmyhMgdUbhR2bnVIBOdCKIbH3Z/YW4iAR3nvUh2HbGth25btrIWdtbBtG7bZti3btvVk9tszT+a2"
155
- "ZW5bto7MzK11VqO1zOzJ7EjNsMlHI1dnPw8LMIff24TaUY4+ZUZgMstox4BYlMjlZ2vj9b/+J3/L"
156
- "bB0prQ3qa2HbFuzJzG0guoVfd6ocCNxGdRaWwwKCzNJLs2yvbDeJJSoSurx2SZkdk6W3Gw7r6EC0"
157
- "lEqzZXh1RSA3W8u2tcy2hW2jAnOwFs5uBM8syMyJzFKVmaXJ8Jd9nnfeTtRUaJ6nzEh85qEUehVV"
158
- "Xgdufe8X/4EjQkMuLFsWpHUkto74XTKnRqD/nrwR6MUjkXn7IxsSFQhd2ojcM6fXh1Po2kQezEuD"
159
- "GbAMWO/AJEETeTkncKHM3LUZmTJzce3dgXOB3k4yA8M2CoMyF4F/W+1ibroql3jj+/7mX8HhP5EA"
160
- "kZEKjkQNcWbOGoDI3iPJHBuC3EfmkaAGoYuGYNhI8hxVmq37cSpgaCIPp9KAvCzcR+zgjSN5rVf2"
161
- "XjJnNmMumUegzsAgUY4Q0QAn9xEi/uGWuxpuJlUucfL9X3kije3rOLWCqIVVm9sA7PXMoyMzsDKh"
162
- "WahzvHipEdj7oyxP6E20GMuo9Knf+qenitoZBeL1y3ZySVCOF/UIZB4HKkc5sh8hWg0ORGZrl7Uc"
163
- "x0HkQY+ZzWAFzCG3VGaU4hBuhEXiZA6Zt1+dgWqEliSGuB32NXjsQoTeRFUusYxK3/7Bf3k6q4Hb"
164
- "U0tRIrJU3g3dqlBmQeTMZoyDzEC1sF1YC1cyqIlv8yCLoR754g5B5B/840PvX+T961PpUPX2ABvi"
165
- "ltHLdVrifS5lZFiB0CwvYBKQtJ03YHwMlRdU6Jr43lfuf6+d7e19/+8/8r7a372USn/o376ZzQRL"
166
- "afLMo9eEKdTZrY9KlSVq9LZjwM2D4H4EZjATyEkGeQNNsMalc9u1E/q7X/7Au90EO36OEtM06z6H"
167
- "uaBUhT9MAMREAOczabplxxv3sHmcRA6o66FZ3BZjiMnHUJ06L+yhV8XLf/Ub7+TZ3h7P/u+Gne3d"
168
- "4HbvBs/2bizyHbW6n/bh9D1PXJKkzubsTlNJ9Ot1T1Bj7KjfOUkEnyWxWfbhXQNe/NJ73vHil97z"
169
- "DtsmEvvlvm339r/75Q+8e4jjLkNqIhKT0RvqeOswJb1/+3w7Mm51BpYidAqHAvB5XL8a8lQpqpR6"
170
- "g4l+C88/eu4NdU6/H9f/8td+yRN4n2eOxNze2PfLG/6xv8h3Dtk4PH3vk8+KqdmKGWHD5Gxid8D2"
171
- "115cGEsqdCB1bAIKaYgKjdQ48X0S4Elth1Ppa3/xq291hN3b55kjriBx2N7n9sb+y3/9m+8a4hyW"
172
- "SonHOQezWWEFwaX1SGERv13pzG9+VByC5TlMFNZTYgXMBMtgYoa1CzThj4wX/uztFzIfGarZE4k5"
173
- "sRsDtpZ44u8cm+M8z9z31Lev/vHPnwMMsfDRDL/q2tm+5d1z2gTfkBy37RggsRIag9J/2KDOvntk"
174
- "a5//9OnX1Dk2cPXzb7uTZ0l5ex6z4rHvxvLtzxY5ziK2YzmVDopc+migUGqoKvejZm87kcnyKylV"
175
- "KzukR1KveuDdx996dvfxt5x15Lyxnz08Ybndn7nBqX5Qqt8XBqi++KX3vGPV86iFM/c/vRMnoxeV"
176
- "9+NkQMW0KVTuUNRIrIieMrGJWHSDFA1DSeqdR07esuzRdy9eOMX2hiesJ3DxQCCuTQRGO5uxnaV9"
177
- "drZZKi0noCefPRQWmsJchH1cjpMGjRe1PLRLrqR0oZ9yGgC5FiPBsvv7sZYBQ4AFmHf+6I2vdkkP"
178
- "onMPXfvRvANceezO24ojyiyDCMSmmc2iFzXGMAzDiM49/r1ERC9+8d1vP/n+v/tqnUuxIrIJNP06"
179
- "IzE6XecDLfVYUYnQ/vo65sKx2KuI28cMMLFFIjUT2BgitgATM9HOIydvIRCd++j1SOznP3PmtZnq"
180
- "ZD8gJ1KHn1gQ2iubARsLMobADUcOSI9qBlO1ZWp5EIg42gpQh6+uAQjPZ3L/em0QAit7L+nt0vx3"
181
- "KQ8gwk4iUeCSCATAmJgZS0kEkTFLp1j22uEyPRZT7yK05b7KuDtAY0CmITNpYCYNmUlDzXQKM52Y"
182
- "5sT01Af/+RuL/MsXsROLEnr34oXbud3fZ7vfws5m7OtzpJIGceR3snUAOuHUEWKITGHoCoasYSgH"
183
- "eGZFUnwj0VpZhyIWY4Gdtb4uRRsfRcEW956ZhfW1LmIhF1eByFUimvnPz0RRl1kLdtWKrn/hXW+r"
184
- "dy1yLOqlzz74zLX5s7GN2iIfihUtB7tbXbzdR+/hXyQQOHVc4uRL3GvEIv1FADHH7xM2g/PIdSdV"
185
- "WQagKUi9u0swmIk5/vGyIRATwZqWyRAwc3HqBXD3J/aqztctsXvxwu0H+2I1zvNQoz+0CNdlRBNP"
186
- "PePkfGclzofri0IsbZvVqRCP2GvPVR6yKD8f6sH5SkWpwKG/XYfacUHpffWi61945y+sfj36sQj5"
187
- "zz74zDW3No+0SuZ5GKDguUh9+4R3TINn9iMUSAlVNCPBU0FC6y2IJ6V7BFviKw+xFWRPxPWVijhV"
188
- "LArkdvbFVzEKteQSwRfAsCNp8r4FMjyqmI9aUY7Cevh4nW+KMxPge/lK8yFSMeTb6t6OsLinytvr"
189
- "gT9mOB7YH5tcdJBY+BdmIrIEhm0BS0StddZjRrCL2Y5FsAT5g5Uqb3uKA1CzL8ccUntPzcRMHANO"
190
- "ediY2XdoR/oRQ7Ks50ec35L3n3PcZrLefxv4/iQW7EJixC2BybKlFmSIrCHQzFz781958+0f+vdv"
191
- "1boqS6t46GvCJakVB6FyfejDSQ0ATIK9IdgWgtgkvuuwn7F8vfvHEXI4Bm6kgSEm686RLHNriQ3B"
192
- "tpZhWsQ+yUdHX+NwVSty5bG7bktp2BAzSt4tb58oJAaIARVJkHyjzNPGJC51P7D0CYhIhw92GEOu"
193
- "w4+BaQz5YVhEjYtJN5OGzGRCZjpBM52QmU5O3/PEpRonswyuPHrnraHBGsOUoTELa2GZAR+LhghD"
194
- "axx6sAr+iEoNlGrt98Qkn/fN4T04tNcNx88VkH8u3rcjdPkjC4KxYOsP78OF3BIsESNYj7IX/Xpx"
195
- "5bN3n4xlv1LPRaSl2+g2K8ZLYokhJw3iSIxI43T1kXG2+C0EWRf6lTgchInJPTlSc/wbcBuWicky"
196
- "iIiJmC2RI3bLnWEha4YrxsPo1LBDMNTZJdmgLt0bgYHnKSxUg8sN4ROlRzzyg7n7+RgiRHjqvq8Y"
197
- "42hbl6m0rYWfFuLq599216CXZh5i2NJlVUUX3HhLE2/urIwdA89TCPSTeo4CdrvhzPnKOftJREjc"
198
- "cWPsMPkUy+6WQOyGhoVGYgswWuaQRTzaqdTElcfuui1UG+U4bM03Cv3foXsns3K4H2sgdB8kyWWK"
199
- "e9WvRYp0OA/P3kcnjhN8nxLrXT3BWQ+ALRGhdX56jdi9eOF2DnOqhPp/he1I3WNEC7D0GyNvEALH"
200
- "RmiJmj9A1hANwWif1+GYo2Hi0KOVmS0TyLIFyLTENqVydi++6XaYpjn7wDd3651jFzFzWU7bltRZ"
201
- "WCf0XDIV7IANIHRNiIZolOvCZ7oOUiHRwgBs7FkVOM8AWYAN8x0PXLo25Blf+ezPvjGl4+WIntA7"
202
- "0Xp/ESxHCErH9qGg8rjVGdjavoiyNlzqZ+2i0yZVJyJj0ohwQ2SMATVNKBlGxvWhvuMjz71U+wyv"
203
- "XLxwSvRvjrNgxXhzNr+KIzZEwROECq9ZAkoJvaWEBhKpxUABigMJ4kiVWLHIkdoQGQPTEJFp3H5P"
204
- "cNOYO3738ournNHu5958Jp+mLU3hJuYhlFMf2zgFhZ9fpYjeQAmdY7sJDSCROg6mkYNPZRaRIqE9"
205
- "wR2RfT2PWOzR/RGcffDZ60c9k93Pvem0S+iV0xpHAnP/umwYhqgHhDrHvh5QMjtsMaEBodL+qUNq"
206
- "EyyI2yfsh1DunOSyboaZq9pXLl44FX1vTJAUE2fKuQdhLdtgLcQ8hLG7bWodZuE7VecM209oABmp"
207
- "Y80L8rWYU1GXkqxSud3rYQyk+Ez2XbGTqkjhhTF/Yi5ulvMUynolro9GLP2Qz0VYWI3QUAz/ViU0"
208
- "sHVRjhI9UQ9m5lAcwMXzYvyWYYhgjSemG5kOQwRmMBl2I8SIYOOAX1cqQZA5D6yI2PFBxE4eOdoM"
209
- "mSVMmXvBYCVzH7ac0B4x4eLXGMxpwIEgoQWDbBgY4LMxxEwEMuzGR/p6H+zFmYmEUU+x7kg3EW6L"
210
- "EbcsUtElt4xixMhzDNNBrihybLnlCOgvtxArLkjLIKIgVGzn+0GxsRmOIa9mUNW44Q1DQeyk3N39"
211
- "6c4RLIxajcMwEkIDh5MaKfohQ34lsd1u0SMvkNt/BVIGxy8KIkbljS1GkSsRoTjP5j4yi69XQucY"
212
- "EaGBIuHinxIhuw28EA2RjUmkfdmXUX41O4SLigt4z5HHkXNVdu9UMi+K8REaQC+pg4noJW2RpBGv"
213
- "iabgPKTYWt6ok4rsXy1VGVAyL4aRERqYT2q/oELGY2dAaTf6PnsgpOntiSMn0oa4SN4AVDIfFSMk"
214
- "NHAwqf0qlaY7cxg9163cJxuFxT7OX+9XcBklyb5DyXwARkpoICPgwcQWb6biE0cxHNmK2OLOSv6p"
215
- "3t1K5kMwYkID/aSOG8W1Kcnd85a54Hytn8R+ByN/M6BEPjpGTmjgcH4e5JMXuXy9nOS+hXhZibwg"
216
- "lNAdFF64q9y9G4egT3YP2KVEXhZK6LnoafjVulr9Yq0krgAl9JFRu1aHElihUCgUCoVCoVAoFAqF"
217
- "QqFQKBQKhUKhUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUa8X/A+26fqLnlvojAAAAAElFTkSuQmCC"
97
+ private let jianPointerPngBase64 = """
98
+ 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
291
+ """
218
292
 
219
293
  private let jianPointerImage: NSImage? = {
220
294
  guard let data = Data(base64Encoded: jianPointerPngBase64, options: .ignoreUnknownCharacters)
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jian-ai/remote-control",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Jian Remote Control worker — connect a computer to a Jian gateway over HTTPS",
5
5
  "type": "module",
6
6
  "bin": {