@jian-ai/remote-control 0.8.4 → 1.0.0
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 +8 -13
- package/bin/jian-remote.mjs +250 -17
- package/macos/computer-helper +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,25 +5,19 @@ over outbound HTTPS polling.
|
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
npm install -g @jian-ai/remote-control
|
|
10
|
-
jian-remote install --gateway https://gateway.example --machine MACHINE_UUID --token WORKER_TOKEN
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Or without a global install:
|
|
8
|
+
The Remote Control page of the panel shows this command with your values filled in:
|
|
14
9
|
|
|
15
10
|
```sh
|
|
16
|
-
|
|
17
|
-
--gateway https://gateway.example
|
|
18
|
-
--machine MACHINE_UUID \
|
|
19
|
-
--token WORKER_TOKEN
|
|
11
|
+
npm install -g @jian-ai/remote-control && jian-remote install \
|
|
12
|
+
--gateway https://gateway.example --machine MACHINE_UUID --token WORKER_TOKEN
|
|
20
13
|
```
|
|
21
14
|
|
|
22
15
|
## Update
|
|
23
16
|
|
|
17
|
+
The worker keeps itself on the gateway's version. To update by hand:
|
|
18
|
+
|
|
24
19
|
```sh
|
|
25
|
-
|
|
26
|
-
# or: jian-remote update [--version 0.6.0]
|
|
20
|
+
jian-remote update [--version 0.6.0]
|
|
27
21
|
```
|
|
28
22
|
|
|
29
23
|
Reads `~/.config/jian-remote` (no token, machine id, or gateway flags), installs a compatible
|
|
@@ -37,7 +31,8 @@ jian-remote stop | start | restart
|
|
|
37
31
|
```
|
|
38
32
|
|
|
39
33
|
`start` / `restart` stop any previous worker before launching one process (LaunchAgent,
|
|
40
|
-
systemd
|
|
34
|
+
systemd `--user` or system unit, or Task Scheduler). On Linux headless/root hosts without a
|
|
35
|
+
user bus, `install` falls back to a system unit (or pass `install --system`).
|
|
41
36
|
|
|
42
37
|
## Configure
|
|
43
38
|
|
package/bin/jian-remote.mjs
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
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,
|
|
7
9
|
readdirSync,
|
|
8
10
|
readFileSync,
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
unlink,
|
|
23
25
|
writeFile,
|
|
24
26
|
} from 'node:fs/promises';
|
|
25
|
-
import { homedir, tmpdir } from 'node:os';
|
|
27
|
+
import { homedir, tmpdir, userInfo } from 'node:os';
|
|
26
28
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
27
29
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
30
|
|
|
@@ -35,6 +37,9 @@ const workerLockPath = join(configDir, 'worker.pid');
|
|
|
35
37
|
const updateStatePath = join(configDir, 'update-state.json');
|
|
36
38
|
const MACOS_LAUNCH_AGENT_LABEL = 'com.jian.remote';
|
|
37
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);
|
|
38
43
|
const WINDOWS_TASK_NAME = 'Jian Remote';
|
|
39
44
|
/** Minimal PATH for launchd — app-identity Node copies otherwise inherit an empty env. */
|
|
40
45
|
export const LAUNCH_AGENT_PATH = '/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin';
|
|
@@ -64,7 +69,7 @@ export function parseCliInvocation(argv = args) {
|
|
|
64
69
|
* Stamped to the release tag on npm publish and when the gateway serves this script.
|
|
65
70
|
* Compatibility with the gateway requires the same major.minor (see versionsCompatible).
|
|
66
71
|
*/
|
|
67
|
-
export const PACKAGE_VERSION = '0.
|
|
72
|
+
export const PACKAGE_VERSION = '1.0.0';
|
|
68
73
|
|
|
69
74
|
/** Soft stdout/stderr caps while capturing a command (chars). Gateway schema matches these. */
|
|
70
75
|
const STDOUT_CAP = 400_000;
|
|
@@ -1068,13 +1073,174 @@ function macosPlistPath() {
|
|
|
1068
1073
|
return join(homedir(), 'Library', 'LaunchAgents', `${MACOS_LAUNCH_AGENT_LABEL}.plist`);
|
|
1069
1074
|
}
|
|
1070
1075
|
|
|
1071
|
-
function
|
|
1076
|
+
function linuxUserUnitPath() {
|
|
1072
1077
|
return join(homedir(), '.config', 'systemd', 'user', LINUX_SERVICE_UNIT);
|
|
1073
1078
|
}
|
|
1074
1079
|
|
|
1075
|
-
|
|
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') {
|
|
1076
1242
|
if (platform === 'darwin') return 'LaunchAgent';
|
|
1077
|
-
if (platform === 'linux') return
|
|
1243
|
+
if (platform === 'linux') return linuxServiceKind(linuxScope);
|
|
1078
1244
|
if (platform === 'win32') return 'Task Scheduler';
|
|
1079
1245
|
return 'none';
|
|
1080
1246
|
}
|
|
@@ -1084,7 +1250,8 @@ export function serviceKindForPlatform(platform = process.platform) {
|
|
|
1084
1250
|
* Clear success/failure for the `jian-remote stop` CLI.
|
|
1085
1251
|
*/
|
|
1086
1252
|
async function stopService() {
|
|
1087
|
-
const
|
|
1253
|
+
const linuxScope = process.platform === 'linux' ? await readLinuxServiceScope() : 'user';
|
|
1254
|
+
const kind = serviceKindForPlatform(process.platform, linuxScope);
|
|
1088
1255
|
if (process.platform === 'darwin') {
|
|
1089
1256
|
const plistPath = macosPlistPath();
|
|
1090
1257
|
const domain = `gui/${process.getuid()}`;
|
|
@@ -1093,7 +1260,11 @@ async function stopService() {
|
|
|
1093
1260
|
stdio: 'ignore',
|
|
1094
1261
|
});
|
|
1095
1262
|
} else if (process.platform === 'linux') {
|
|
1096
|
-
|
|
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
|
+
}
|
|
1097
1268
|
} else if (process.platform === 'win32') {
|
|
1098
1269
|
spawnSync('schtasks.exe', ['/End', '/TN', WINDOWS_TASK_NAME], {
|
|
1099
1270
|
stdio: 'ignore',
|
|
@@ -1171,17 +1342,62 @@ async function startService(script = installedScript) {
|
|
|
1171
1342
|
};
|
|
1172
1343
|
}
|
|
1173
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
|
+
|
|
1174
1373
|
const unitDir = join(homedir(), '.config', 'systemd', 'user');
|
|
1175
1374
|
await mkdir(unitDir, { recursive: true, mode: 0o700 });
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
+
}
|
|
1179
1387
|
for (const command of [['daemon-reload'], ['enable', '--now', LINUX_SERVICE_UNIT]]) {
|
|
1180
1388
|
const result = spawnSync('systemctl', ['--user', ...command], { stdio: 'inherit' });
|
|
1181
|
-
if (result.status !== 0)
|
|
1182
|
-
throw new Error(
|
|
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
|
+
}
|
|
1183
1399
|
}
|
|
1184
|
-
return { service: '
|
|
1400
|
+
return { service: linuxServiceKind('user'), detail: LINUX_SERVICE_UNIT };
|
|
1185
1401
|
}
|
|
1186
1402
|
if (process.platform === 'win32') {
|
|
1187
1403
|
const action = `"${process.execPath}" "${script}" run`;
|
|
@@ -1256,7 +1472,7 @@ async function install() {
|
|
|
1256
1472
|
const gatewayBase = parseGatewayBase(gateway);
|
|
1257
1473
|
if (!gatewayBase || !MACHINE_ID.test(machineId ?? '') || !token?.trim()) {
|
|
1258
1474
|
throw new Error(
|
|
1259
|
-
'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)',
|
|
1260
1476
|
);
|
|
1261
1477
|
}
|
|
1262
1478
|
const gatewayVersion = await assertCompatibleWithGateway(gatewayBase);
|
|
@@ -1959,6 +2175,9 @@ async function postResult(base, headers, result) {
|
|
|
1959
2175
|
async function run() {
|
|
1960
2176
|
await acquireWorkerLock();
|
|
1961
2177
|
let stopMacIdle = () => {};
|
|
2178
|
+
// Set once the gateway is known: tells it this worker is stopping, so the panel shows the
|
|
2179
|
+
// machine offline at once instead of when its last poll grows old.
|
|
2180
|
+
let sayGoodbye = async () => {};
|
|
1962
2181
|
const shutdown = () => {
|
|
1963
2182
|
try {
|
|
1964
2183
|
stopMacIdle();
|
|
@@ -1974,8 +2193,10 @@ async function run() {
|
|
|
1974
2193
|
};
|
|
1975
2194
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
1976
2195
|
process.once(signal, () => {
|
|
1977
|
-
|
|
1978
|
-
|
|
2196
|
+
void sayGoodbye().finally(() => {
|
|
2197
|
+
shutdown();
|
|
2198
|
+
process.exit(0);
|
|
2199
|
+
});
|
|
1979
2200
|
});
|
|
1980
2201
|
}
|
|
1981
2202
|
process.once('exit', shutdown);
|
|
@@ -2008,6 +2229,17 @@ async function run() {
|
|
|
2008
2229
|
...(capabilities.length ? { 'x-jian-remote-capabilities': capabilities.join(',') } : {}),
|
|
2009
2230
|
});
|
|
2010
2231
|
const base = `${config.gateway}/v1/remote/worker/${config.machineId}`;
|
|
2232
|
+
// Best effort and brief: a stop must not hang on an unreachable gateway.
|
|
2233
|
+
sayGoodbye = () =>
|
|
2234
|
+
fetch(`${base}/disconnect`, {
|
|
2235
|
+
method: 'POST',
|
|
2236
|
+
headers: pollHeaders(),
|
|
2237
|
+
redirect: 'error',
|
|
2238
|
+
signal: AbortSignal.timeout(2000),
|
|
2239
|
+
}).then(
|
|
2240
|
+
() => {},
|
|
2241
|
+
() => {},
|
|
2242
|
+
);
|
|
2011
2243
|
// Match the gateway worker (including major.minor skew) before polling so fleets do not
|
|
2012
2244
|
// crash-loop on refuse without attempting a self-update first.
|
|
2013
2245
|
const gatewayVersion = await reconcileWorkerWithGateway(config, base, pollHeaders());
|
|
@@ -2086,7 +2318,7 @@ if (isCliEntry()) {
|
|
|
2086
2318
|
const pkg = paint('(@jian-ai/remote-control)', ANSI.dim, color);
|
|
2087
2319
|
console.log(`${title} worker ${pkg}
|
|
2088
2320
|
version | --version | -V
|
|
2089
|
-
install --gateway HTTPS_URL --machine UUID (--token VALUE | --token-prompt | --token-stdin)
|
|
2321
|
+
install [--system] --gateway HTTPS_URL --machine UUID (--token VALUE | --token-prompt | --token-stdin)
|
|
2090
2322
|
configure [--gateway HTTPS_URL] [--machine UUID] (--token VALUE | --token-prompt | --token-stdin)
|
|
2091
2323
|
update [--version X.Y.Z|latest]
|
|
2092
2324
|
stop | start | restart
|
|
@@ -2094,6 +2326,7 @@ if (isCliEntry()) {
|
|
|
2094
2326
|
|
|
2095
2327
|
Install: npm install -g @jian-ai/remote-control
|
|
2096
2328
|
Or: npx @jian-ai/remote-control@latest install --gateway … --machine … --token …
|
|
2329
|
+
(Linux headless/root VPS: add --system, or use a normal user + loginctl enable-linger)
|
|
2097
2330
|
Configure: jian-remote configure --token … (or --gateway / --machine; restarts the service)
|
|
2098
2331
|
Update: npx @jian-ai/remote-control@latest update
|
|
2099
2332
|
(upgrades the worker binary; reads ~/.config/jian-remote)
|
package/macos/computer-helper
CHANGED
|
Binary file
|