@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 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).
@@ -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.0';
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 inlineToken = option('--token');
365
- const token = inlineToken
366
- ? inlineToken
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 mkdir(configDir, { recursive: true, mode: 0o700 });
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
- const invokedAsCli =
711
- Boolean(process.argv[1]) && resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1]);
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 (invokedAsCli) {
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: npm install -g @jian-ai/remote-control\nOr: npx @jian-ai/remote-control@latest install --gateway … --machine … --token …\nUpdate: npx @jian-ai/remote-control@latest update\n (reads ~/.config/jian-remote; no token/machine/gateway flags)',
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jian-ai/remote-control",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Jian Remote Control worker — connect a computer to a Jian gateway over HTTPS",
5
5
  "type": "module",
6
6
  "bin": {