@jian-ai/remote-control 0.7.0 → 0.7.1
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 +10 -0
- package/bin/jian-remote.mjs +144 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,16 @@ npx @jian-ai/remote-control@latest update
|
|
|
29
29
|
Reads `~/.config/jian-remote` (no token, machine id, or gateway flags), installs a compatible
|
|
30
30
|
build, and restarts the worker service.
|
|
31
31
|
|
|
32
|
+
## Configure
|
|
33
|
+
|
|
34
|
+
Change the stored gateway URL and/or worker token without a full reinstall (restarts the
|
|
35
|
+
existing OS service):
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
jian-remote configure --token WORKER_TOKEN
|
|
39
|
+
jian-remote configure --gateway https://gateway.example --token WORKER_TOKEN
|
|
40
|
+
```
|
|
41
|
+
|
|
32
42
|
## Compatibility
|
|
33
43
|
|
|
34
44
|
Worker and gateway must share the same major.minor. See [docs/remote.md](../../docs/remote.md).
|
package/bin/jian-remote.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
5
5
|
import { chmod, copyFile, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { homedir, tmpdir } from 'node:os';
|
|
7
7
|
import { join, resolve } from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
9
|
|
|
10
10
|
const configDir = join(homedir(), '.config', 'jian-remote');
|
|
11
11
|
const configPath = join(configDir, 'config.json');
|
|
@@ -21,7 +21,7 @@ const option = (name) => {
|
|
|
21
21
|
* Stamped to the release tag on npm publish and when the gateway serves this script.
|
|
22
22
|
* Compatibility with the gateway requires the same major.minor (see versionsCompatible).
|
|
23
23
|
*/
|
|
24
|
-
export const PACKAGE_VERSION = '0.7.
|
|
24
|
+
export const PACKAGE_VERSION = '0.7.1';
|
|
25
25
|
|
|
26
26
|
/** Soft stdout/stderr caps while capturing a command (chars). Gateway schema matches these. */
|
|
27
27
|
const STDOUT_CAP = 400_000;
|
|
@@ -210,6 +210,61 @@ async function loadConfig() {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
const MACHINE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
214
|
+
|
|
215
|
+
function parseGatewayBase(gateway) {
|
|
216
|
+
let gatewayUrl;
|
|
217
|
+
try {
|
|
218
|
+
gatewayUrl = new URL(gateway);
|
|
219
|
+
} catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
gatewayUrl.protocol !== 'https:' ||
|
|
224
|
+
gatewayUrl.username ||
|
|
225
|
+
gatewayUrl.password ||
|
|
226
|
+
gatewayUrl.search ||
|
|
227
|
+
gatewayUrl.hash
|
|
228
|
+
) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return gatewayUrl.origin + gatewayUrl.pathname.replace(/\/$/, '');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function readTokenOption() {
|
|
235
|
+
const inlineToken = option('--token');
|
|
236
|
+
if (inlineToken) return inlineToken;
|
|
237
|
+
if (args.includes('--token-stdin')) return await readStdin();
|
|
238
|
+
if (args.includes('--token-prompt')) return await readSecret('Worker token (input hidden): ');
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Persist credentials with owner-only permissions under ~/.config/jian-remote. */
|
|
243
|
+
async function writeConfigFile(config) {
|
|
244
|
+
await mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
245
|
+
if (process.platform === 'win32') {
|
|
246
|
+
const account = process.env.USERNAME;
|
|
247
|
+
if (!account) throw new Error('Could not identify the current Windows user');
|
|
248
|
+
runChecked(
|
|
249
|
+
'icacls.exe',
|
|
250
|
+
[configDir, '/inheritance:r', '/grant:r', `${account}:(OI)(CI)F`],
|
|
251
|
+
'Could not restrict permissions on the Jian Remote config directory',
|
|
252
|
+
);
|
|
253
|
+
} else {
|
|
254
|
+
await chmod(configDir, 0o700);
|
|
255
|
+
}
|
|
256
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
257
|
+
if (process.platform === 'win32') {
|
|
258
|
+
runChecked(
|
|
259
|
+
'icacls.exe',
|
|
260
|
+
[configPath, '/inheritance:r', '/grant:r', `${process.env.USERNAME}:F`],
|
|
261
|
+
'Could not restrict permissions on the Jian Remote config file',
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
await chmod(configPath, 0o600);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
213
268
|
/** Public /health carries the gateway release so install/update can gate without a worker token. */
|
|
214
269
|
export async function fetchGatewayVersion(gatewayBase, fetcher = fetch) {
|
|
215
270
|
const response = await fetcher(`${gatewayBase.replace(/\/$/, '')}/health`, {
|
|
@@ -361,62 +416,20 @@ async function install() {
|
|
|
361
416
|
const machineId = option('--machine');
|
|
362
417
|
// Prefer an explicit --token for one-liner installs from the panel; stdin/prompt keep
|
|
363
418
|
// the token out of shell history when someone installs by hand.
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
: args.includes('--token-stdin')
|
|
368
|
-
? await readStdin()
|
|
369
|
-
: args.includes('--token-prompt')
|
|
370
|
-
? await readSecret('Worker token (input hidden): ')
|
|
371
|
-
: undefined;
|
|
372
|
-
let gatewayUrl;
|
|
373
|
-
try {
|
|
374
|
-
gatewayUrl = new URL(gateway);
|
|
375
|
-
} catch {}
|
|
376
|
-
if (
|
|
377
|
-
gatewayUrl?.protocol !== 'https:' ||
|
|
378
|
-
gatewayUrl.username ||
|
|
379
|
-
gatewayUrl.password ||
|
|
380
|
-
gatewayUrl.search ||
|
|
381
|
-
gatewayUrl.hash ||
|
|
382
|
-
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
383
|
-
machineId ?? '',
|
|
384
|
-
) ||
|
|
385
|
-
!token?.trim()
|
|
386
|
-
) {
|
|
419
|
+
const token = await readTokenOption();
|
|
420
|
+
const gatewayBase = parseGatewayBase(gateway);
|
|
421
|
+
if (!gatewayBase || !MACHINE_ID.test(machineId ?? '') || !token?.trim()) {
|
|
387
422
|
throw new Error(
|
|
388
423
|
'Usage: jian-remote install --gateway https://... --machine UUID (--token VALUE | --token-prompt | --token-stdin)',
|
|
389
424
|
);
|
|
390
425
|
}
|
|
391
|
-
const gatewayBase = gatewayUrl.origin + gatewayUrl.pathname.replace(/\/$/, '');
|
|
392
426
|
const gatewayVersion = await assertCompatibleWithGateway(gatewayBase);
|
|
393
427
|
const config = {
|
|
394
428
|
gateway: gatewayBase,
|
|
395
429
|
machineId,
|
|
396
430
|
token: token.trim(),
|
|
397
431
|
};
|
|
398
|
-
await
|
|
399
|
-
if (process.platform === 'win32') {
|
|
400
|
-
const account = process.env.USERNAME;
|
|
401
|
-
if (!account) throw new Error('Could not identify the current Windows user');
|
|
402
|
-
runChecked(
|
|
403
|
-
'icacls.exe',
|
|
404
|
-
[configDir, '/inheritance:r', '/grant:r', `${account}:(OI)(CI)F`],
|
|
405
|
-
'Could not restrict permissions on the Jian Remote config directory',
|
|
406
|
-
);
|
|
407
|
-
} else {
|
|
408
|
-
await chmod(configDir, 0o700);
|
|
409
|
-
}
|
|
410
|
-
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
411
|
-
if (process.platform === 'win32') {
|
|
412
|
-
runChecked(
|
|
413
|
-
'icacls.exe',
|
|
414
|
-
[configPath, '/inheritance:r', '/grant:r', `${process.env.USERNAME}:F`],
|
|
415
|
-
'Could not restrict permissions on the Jian Remote config file',
|
|
416
|
-
);
|
|
417
|
-
} else {
|
|
418
|
-
await chmod(configPath, 0o600);
|
|
419
|
-
}
|
|
432
|
+
await writeConfigFile(config);
|
|
420
433
|
const script = await materializeWorkerScript();
|
|
421
434
|
await startOrRestartService(script);
|
|
422
435
|
console.log(`Credentials are stored with owner-only permissions in ${configPath}.`);
|
|
@@ -428,11 +441,78 @@ async function install() {
|
|
|
428
441
|
console.log(
|
|
429
442
|
'Later upgrades: npx @jian-ai/remote-control@latest update (reads this config; no token again).',
|
|
430
443
|
);
|
|
444
|
+
console.log(
|
|
445
|
+
'To change gateway URL or token without reinstalling: jian-remote configure --gateway … --token …',
|
|
446
|
+
);
|
|
431
447
|
console.log(
|
|
432
448
|
'If the worker later looks stuck offline, restart the service (or run update) and check ~/.config/jian-remote/worker.log.',
|
|
433
449
|
);
|
|
434
450
|
}
|
|
435
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Rewrite stored credentials (gateway URL, worker token, and/or machine id) and restart the
|
|
454
|
+
* existing OS service — does not wipe the LaunchAgent / systemd unit / scheduled task.
|
|
455
|
+
*/
|
|
456
|
+
async function configure() {
|
|
457
|
+
const gatewayFlag = option('--gateway');
|
|
458
|
+
const machineFlag = option('--machine');
|
|
459
|
+
const tokenFlag =
|
|
460
|
+
option('--token') !== undefined ||
|
|
461
|
+
args.includes('--token-stdin') ||
|
|
462
|
+
args.includes('--token-prompt');
|
|
463
|
+
if (!gatewayFlag && !machineFlag && !tokenFlag) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
'Usage: jian-remote configure [--gateway https://...] [--machine UUID] (--token VALUE | --token-prompt | --token-stdin)\nChange at least one of gateway, machine, or token. Reads ~/.config/jian-remote; restarts the service.',
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const existing = await loadConfig();
|
|
469
|
+
|
|
470
|
+
const gatewayBase = gatewayFlag ? parseGatewayBase(gatewayFlag) : undefined;
|
|
471
|
+
if (gatewayFlag && !gatewayBase) {
|
|
472
|
+
throw new Error('Gateway must be an https:// URL with no credentials, query, or hash.');
|
|
473
|
+
}
|
|
474
|
+
const machineId = machineFlag ?? existing.machineId;
|
|
475
|
+
if (!MACHINE_ID.test(machineId ?? '')) {
|
|
476
|
+
throw new Error('Machine id must be a UUID.');
|
|
477
|
+
}
|
|
478
|
+
const token = tokenFlag ? await readTokenOption() : existing.token;
|
|
479
|
+
if (!token?.trim()) {
|
|
480
|
+
throw new Error('Worker token must not be empty.');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const config = {
|
|
484
|
+
gateway: gatewayBase ?? existing.gateway,
|
|
485
|
+
machineId,
|
|
486
|
+
token: token.trim(),
|
|
487
|
+
};
|
|
488
|
+
if (typeof config.gateway !== 'string' || !config.gateway.startsWith('https://')) {
|
|
489
|
+
throw new Error(`Invalid gateway in ${configPath}; re-run install from the panel.`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const gatewayVersion = await assertCompatibleWithGateway(config.gateway);
|
|
493
|
+
await writeConfigFile(config);
|
|
494
|
+
// Prefer the already-installed script so configure does not depend on the npx cache.
|
|
495
|
+
let script = installedScript;
|
|
496
|
+
try {
|
|
497
|
+
await readFile(script);
|
|
498
|
+
} catch {
|
|
499
|
+
script = await materializeWorkerScript();
|
|
500
|
+
}
|
|
501
|
+
await startOrRestartService(script);
|
|
502
|
+
|
|
503
|
+
const changed = [
|
|
504
|
+
gatewayFlag ? 'gateway' : null,
|
|
505
|
+
machineFlag ? 'machine' : null,
|
|
506
|
+
tokenFlag ? 'token' : null,
|
|
507
|
+
].filter(Boolean);
|
|
508
|
+
console.log(`Updated ${changed.join(', ')} in ${configPath}. Worker service restarted.`);
|
|
509
|
+
if (gatewayVersion) {
|
|
510
|
+
console.log(
|
|
511
|
+
`Compatible with gateway ${gatewayVersion} (major.minor ${parseRelease(gatewayVersion)?.major}.${parseRelease(gatewayVersion)?.minor}).`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
436
516
|
/**
|
|
437
517
|
* Refresh the installed worker from npm (or this process when already the right build), using
|
|
438
518
|
* credentials already stored under ~/.config/jian-remote — never re-ask for token/machine/gateway.
|
|
@@ -707,17 +787,28 @@ async function run() {
|
|
|
707
787
|
}
|
|
708
788
|
}
|
|
709
789
|
|
|
710
|
-
|
|
711
|
-
|
|
790
|
+
/**
|
|
791
|
+
* True when this file is the process entry. npm’s `.bin/jian-remote` is a symlink; comparing
|
|
792
|
+
* resolve() paths alone misses that and made `npx … install` exit 0 with no stdout/stderr.
|
|
793
|
+
*/
|
|
794
|
+
export function isCliEntry(argv1 = process.argv[1], metaUrl = import.meta.url) {
|
|
795
|
+
if (!argv1) return false;
|
|
796
|
+
try {
|
|
797
|
+
return metaUrl === pathToFileURL(realpathSync(argv1)).href;
|
|
798
|
+
} catch {
|
|
799
|
+
return resolve(fileURLToPath(metaUrl)) === resolve(argv1);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
712
802
|
|
|
713
|
-
if (
|
|
803
|
+
if (isCliEntry()) {
|
|
714
804
|
try {
|
|
715
805
|
if (args[0] === 'install') await install();
|
|
806
|
+
else if (args[0] === 'configure') await configure();
|
|
716
807
|
else if (args[0] === 'update') await update();
|
|
717
808
|
else if (args[0] === 'run') await run();
|
|
718
809
|
else
|
|
719
810
|
console.log(
|
|
720
|
-
'Jian Remote worker (@jian-ai/remote-control)\n install --gateway HTTPS_URL --machine UUID (--token VALUE | --token-prompt | --token-stdin)\n update [--version X.Y.Z|latest]\n run\n\nInstall:
|
|
811
|
+
'Jian Remote worker (@jian-ai/remote-control)\n install --gateway HTTPS_URL --machine UUID (--token VALUE | --token-prompt | --token-stdin)\n configure [--gateway HTTPS_URL] [--machine UUID] (--token VALUE | --token-prompt | --token-stdin)\n update [--version X.Y.Z|latest]\n run\n\nInstall: npm install -g @jian-ai/remote-control\nOr: npx @jian-ai/remote-control@latest install --gateway … --machine … --token …\nConfigure: jian-remote configure --token … (or --gateway / --machine; restarts the service)\nUpdate: npx @jian-ai/remote-control@latest update\n (upgrades the worker binary; reads ~/.config/jian-remote)',
|
|
721
812
|
);
|
|
722
813
|
} catch (error) {
|
|
723
814
|
console.error(error instanceof Error ? error.message : error);
|