@jian-ai/remote-control 0.7.1 → 0.8.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/bin/computer-macos.mjs +479 -0
- package/bin/jian-remote.mjs +284 -48
- package/macos/computer-helper.swift +735 -0
- package/package.json +2 -1
package/bin/jian-remote.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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, realpathSync, rmSync } from 'node:fs';
|
|
4
|
+
import { existsSync, 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
|
-
import { join, resolve } from 'node:path';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
9
|
|
|
10
10
|
const configDir = join(homedir(), '.config', 'jian-remote');
|
|
@@ -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.
|
|
24
|
+
export const PACKAGE_VERSION = '0.8.0';
|
|
25
25
|
|
|
26
26
|
/** Soft stdout/stderr caps while capturing a command (chars). Gateway schema matches these. */
|
|
27
27
|
const STDOUT_CAP = 400_000;
|
|
@@ -34,6 +34,150 @@ const RESULT_BODY_BUDGET = 480_000;
|
|
|
34
34
|
/** Permanent HTTP statuses — retrying forever leaves the task stuck and the machine offline. */
|
|
35
35
|
const PERMANENT_RESULT_STATUS = new Set([400, 401, 403, 404, 413]);
|
|
36
36
|
const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
37
|
+
const MACOS_BUNDLE_ID = 'com.jian.remote';
|
|
38
|
+
const MACOS_APP_NAME = 'Jian Remote';
|
|
39
|
+
|
|
40
|
+
const ANSI = {
|
|
41
|
+
reset: '\u001b[0m',
|
|
42
|
+
bold: '\u001b[1m',
|
|
43
|
+
dim: '\u001b[2m',
|
|
44
|
+
green: '\u001b[32m',
|
|
45
|
+
yellow: '\u001b[33m',
|
|
46
|
+
red: '\u001b[31m',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Color when stdout is a TTY, unless NO_COLOR; FORCE_COLOR=1 enables even when piped. */
|
|
50
|
+
export function shouldColorize(options = {}) {
|
|
51
|
+
// Use `in` so tests can pass forceColor: undefined without falling back to the process env.
|
|
52
|
+
const isTTY = 'isTTY' in options ? options.isTTY : process.stdout.isTTY;
|
|
53
|
+
const noColor = 'noColor' in options ? options.noColor : process.env.NO_COLOR;
|
|
54
|
+
const forceColor = 'forceColor' in options ? options.forceColor : process.env.FORCE_COLOR;
|
|
55
|
+
if (noColor !== undefined && noColor !== '') return false;
|
|
56
|
+
if (forceColor !== undefined && forceColor !== '' && forceColor !== '0') return true;
|
|
57
|
+
return Boolean(isTTY);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
|
|
61
|
+
|
|
62
|
+
export function stripAnsi(text) {
|
|
63
|
+
return text.replace(ANSI_PATTERN, '');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function paint(text, code, color) {
|
|
67
|
+
return color ? `${code}${text}${ANSI.reset}` : text;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tasteful success block for install/configure/update — headline plus aligned facts.
|
|
72
|
+
* Keep facts short; avoid rainbow spam.
|
|
73
|
+
*/
|
|
74
|
+
export function formatSuccessBlock({ headline, facts = [], color = shouldColorize() }) {
|
|
75
|
+
const mark = paint('✓', ANSI.green, color);
|
|
76
|
+
const title = paint(headline, ANSI.bold, color);
|
|
77
|
+
const lines = [`${mark} ${title}`];
|
|
78
|
+
const width = Math.min(12, Math.max(0, ...facts.map(([label]) => String(label).length)));
|
|
79
|
+
for (const [label, value] of facts) {
|
|
80
|
+
const key = paint(String(label).padEnd(width), ANSI.dim, color);
|
|
81
|
+
lines.push(` ${key} ${value}`);
|
|
82
|
+
}
|
|
83
|
+
return `${lines.join('\n')}\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function printSuccess(headline, facts) {
|
|
87
|
+
process.stdout.write(formatSuccessBlock({ headline, facts, color: shouldColorize() }));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printInfo(message) {
|
|
91
|
+
const color = shouldColorize();
|
|
92
|
+
const prefix = paint('·', ANSI.dim, color);
|
|
93
|
+
console.log(`${prefix} ${message}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function xmlEscape(value) {
|
|
97
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Paths for the thin macOS .app that owns Login Items / TCC identity (not system node). */
|
|
101
|
+
export function macosAppBundlePaths(home = homedir()) {
|
|
102
|
+
const appRoot = join(
|
|
103
|
+
home,
|
|
104
|
+
'Library',
|
|
105
|
+
'Application Support',
|
|
106
|
+
MACOS_APP_NAME,
|
|
107
|
+
`${MACOS_APP_NAME}.app`,
|
|
108
|
+
);
|
|
109
|
+
return {
|
|
110
|
+
appRoot,
|
|
111
|
+
contents: join(appRoot, 'Contents'),
|
|
112
|
+
macosDir: join(appRoot, 'Contents', 'MacOS'),
|
|
113
|
+
executable: join(appRoot, 'Contents', 'MacOS', MACOS_APP_NAME),
|
|
114
|
+
infoPlist: join(appRoot, 'Contents', 'Info.plist'),
|
|
115
|
+
bundleId: MACOS_BUNDLE_ID,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildMacosAppInfoPlist() {
|
|
120
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
121
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
122
|
+
<plist version="1.0"><dict>
|
|
123
|
+
<key>CFBundleDevelopmentRegion</key><string>en</string>
|
|
124
|
+
<key>CFBundleDisplayName</key><string>${MACOS_APP_NAME}</string>
|
|
125
|
+
<key>CFBundleExecutable</key><string>${MACOS_APP_NAME}</string>
|
|
126
|
+
<key>CFBundleIdentifier</key><string>${MACOS_BUNDLE_ID}</string>
|
|
127
|
+
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
|
128
|
+
<key>CFBundleName</key><string>${MACOS_APP_NAME}</string>
|
|
129
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
130
|
+
<key>CFBundleShortVersionString</key><string>1.0</string>
|
|
131
|
+
<key>CFBundleVersion</key><string>1</string>
|
|
132
|
+
<key>LSMinimumSystemVersion</key><string>12.0</string>
|
|
133
|
+
<key>NSHighResolutionCapable</key><true/>
|
|
134
|
+
</dict></plist>
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function buildLaunchAgentPlist({ label, executable, script, logPath }) {
|
|
139
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
140
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
141
|
+
<plist version="1.0"><dict>
|
|
142
|
+
<key>Label</key><string>${xmlEscape(label)}</string>
|
|
143
|
+
<key>AssociatedBundleIdentifiers</key>
|
|
144
|
+
<array><string>${MACOS_BUNDLE_ID}</string></array>
|
|
145
|
+
<key>ProgramArguments</key>
|
|
146
|
+
<array>
|
|
147
|
+
<string>${xmlEscape(executable)}</string>
|
|
148
|
+
<string>${xmlEscape(script)}</string>
|
|
149
|
+
<string>run</string>
|
|
150
|
+
</array>
|
|
151
|
+
<key>RunAtLoad</key><true/>
|
|
152
|
+
<key>KeepAlive</key><true/>
|
|
153
|
+
<key>StandardOutPath</key><string>${xmlEscape(logPath)}</string>
|
|
154
|
+
<key>StandardErrorPath</key><string>${xmlEscape(logPath)}</string>
|
|
155
|
+
</dict></plist>
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Copy the current Node binary into Jian Remote.app and ad-hoc sign so Login Items / TCC
|
|
161
|
+
* attach to Jian, not the shared system node (unsigned / ad-hoc — not Developer ID).
|
|
162
|
+
*/
|
|
163
|
+
async function materializeMacosAppRuntime() {
|
|
164
|
+
const paths = macosAppBundlePaths();
|
|
165
|
+
await mkdir(paths.macosDir, { recursive: true, mode: 0o755 });
|
|
166
|
+
await writeFile(paths.infoPlist, buildMacosAppInfoPlist(), { mode: 0o644 });
|
|
167
|
+
const source = process.execPath;
|
|
168
|
+
const temp = `${paths.executable}.${process.pid}.tmp`;
|
|
169
|
+
await copyFile(source, temp);
|
|
170
|
+
await chmod(temp, 0o755);
|
|
171
|
+
await rename(temp, paths.executable);
|
|
172
|
+
// Best-effort ad-hoc sign with our bundle id; ignore failures on locked-down hosts.
|
|
173
|
+
spawnSync('codesign', ['--force', '-s', '-', '--identifier', MACOS_BUNDLE_ID, paths.executable], {
|
|
174
|
+
stdio: 'ignore',
|
|
175
|
+
});
|
|
176
|
+
spawnSync('codesign', ['--force', '-s', '-', '--identifier', MACOS_BUNDLE_ID, paths.appRoot], {
|
|
177
|
+
stdio: 'ignore',
|
|
178
|
+
});
|
|
179
|
+
return paths;
|
|
180
|
+
}
|
|
37
181
|
|
|
38
182
|
export function parseRelease(version) {
|
|
39
183
|
const raw = typeof version === 'string' ? version.replace(/^v/, '').trim() : '';
|
|
@@ -112,6 +256,7 @@ export function cappedAppend(current, chunk, cap = STDOUT_CAP) {
|
|
|
112
256
|
export function fitResult(result, budget = RESULT_BODY_BUDGET) {
|
|
113
257
|
let stdout = result.stdout ?? '';
|
|
114
258
|
let stderr = result.stderr ?? '';
|
|
259
|
+
const computer = result.computer;
|
|
115
260
|
const pack = () =>
|
|
116
261
|
JSON.stringify({
|
|
117
262
|
taskId: result.taskId,
|
|
@@ -119,10 +264,14 @@ export function fitResult(result, budget = RESULT_BODY_BUDGET) {
|
|
|
119
264
|
stdout,
|
|
120
265
|
stderr,
|
|
121
266
|
timedOut: Boolean(result.timedOut),
|
|
267
|
+
...(computer ? { computer } : {}),
|
|
122
268
|
});
|
|
123
269
|
let body = pack();
|
|
124
270
|
if (Buffer.byteLength(body) <= budget) {
|
|
125
|
-
return {
|
|
271
|
+
return {
|
|
272
|
+
result: { ...result, stdout, stderr, timedOut: Boolean(result.timedOut), computer },
|
|
273
|
+
body,
|
|
274
|
+
};
|
|
126
275
|
}
|
|
127
276
|
|
|
128
277
|
const note = '\n… truncated to fit the gateway size limit';
|
|
@@ -337,29 +486,57 @@ async function materializeWorkerScript(sourceBytes) {
|
|
|
337
486
|
if (source !== installedScript) await copyFile(source, installedScript);
|
|
338
487
|
}
|
|
339
488
|
if (process.platform !== 'win32') await chmod(installedScript, 0o700);
|
|
489
|
+
// Companion modules + macOS helper source (computer use). Best-effort beside the worker.
|
|
490
|
+
try {
|
|
491
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
492
|
+
const computerSrc = join(here, 'computer-macos.mjs');
|
|
493
|
+
const computerDest = join(configDir, 'computer-macos.mjs');
|
|
494
|
+
if (existsSync(computerSrc) && computerSrc !== computerDest) {
|
|
495
|
+
await copyFile(computerSrc, computerDest);
|
|
496
|
+
if (process.platform !== 'win32') await chmod(computerDest, 0o700);
|
|
497
|
+
}
|
|
498
|
+
if (process.platform === 'darwin') {
|
|
499
|
+
const { materializeComputerHelperSources } = await import('./computer-macos.mjs');
|
|
500
|
+
await materializeComputerHelperSources(configDir);
|
|
501
|
+
}
|
|
502
|
+
} catch {
|
|
503
|
+
// Install still succeeds; computer-use probe will explain if the helper is missing.
|
|
504
|
+
}
|
|
340
505
|
return installedScript;
|
|
341
506
|
}
|
|
342
507
|
|
|
343
|
-
/**
|
|
508
|
+
/**
|
|
509
|
+
* Register or bounce the per-user service so it runs the installed script and reconnects.
|
|
510
|
+
* On macOS, ProgramArguments[0] is the Jian Remote.app Node copy — not system node — so
|
|
511
|
+
* Login Items / TCC show "Jian Remote".
|
|
512
|
+
*/
|
|
344
513
|
async function startOrRestartService(script) {
|
|
345
514
|
if (process.platform === 'darwin') {
|
|
346
515
|
const launchAgents = join(homedir(), 'Library', 'LaunchAgents');
|
|
347
516
|
const plistPath = join(launchAgents, 'com.jian.remote.plist');
|
|
348
|
-
const xml = (value) =>
|
|
349
|
-
value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
350
517
|
const label = 'com.jian.remote';
|
|
518
|
+
const app = await materializeMacosAppRuntime();
|
|
351
519
|
await mkdir(launchAgents, { recursive: true, mode: 0o700 });
|
|
352
520
|
await writeFile(
|
|
353
521
|
plistPath,
|
|
354
|
-
|
|
522
|
+
buildLaunchAgentPlist({
|
|
523
|
+
label,
|
|
524
|
+
executable: app.executable,
|
|
525
|
+
script,
|
|
526
|
+
logPath: join(configDir, 'worker.log'),
|
|
527
|
+
}),
|
|
355
528
|
{ mode: 0o600 },
|
|
356
529
|
);
|
|
357
530
|
const domain = `gui/${process.getuid()}`;
|
|
358
531
|
spawnSync('launchctl', ['bootout', domain, plistPath], { stdio: 'ignore' });
|
|
359
532
|
const loaded = spawnSync('launchctl', ['bootstrap', domain, plistPath], { stdio: 'inherit' });
|
|
360
533
|
if (loaded.status !== 0) throw new Error('Could not start Jian Remote with launchd');
|
|
361
|
-
|
|
362
|
-
|
|
534
|
+
return {
|
|
535
|
+
service: 'LaunchAgent',
|
|
536
|
+
detail: `Login Items identity: ${MACOS_APP_NAME}.app (${app.appRoot})`,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
if (process.platform === 'linux') {
|
|
363
540
|
const unitDir = join(homedir(), '.config', 'systemd', 'user');
|
|
364
541
|
await mkdir(unitDir, { recursive: true, mode: 0o700 });
|
|
365
542
|
const quote = (value) => `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
@@ -374,8 +551,9 @@ async function startOrRestartService(script) {
|
|
|
374
551
|
if (result.status !== 0)
|
|
375
552
|
throw new Error('Could not install/start Jian Remote with systemd --user');
|
|
376
553
|
}
|
|
377
|
-
|
|
378
|
-
}
|
|
554
|
+
return { service: 'systemd --user', detail: 'jian-remote.service' };
|
|
555
|
+
}
|
|
556
|
+
if (process.platform === 'win32') {
|
|
379
557
|
const action = `"${process.execPath}" "${script}" run`;
|
|
380
558
|
spawnSync('schtasks.exe', ['/End', '/TN', 'Jian Remote'], {
|
|
381
559
|
stdio: 'ignore',
|
|
@@ -403,12 +581,10 @@ async function startOrRestartService(script) {
|
|
|
403
581
|
['/Run', '/TN', 'Jian Remote'],
|
|
404
582
|
'Could not start the Jian Remote scheduled task',
|
|
405
583
|
);
|
|
406
|
-
|
|
407
|
-
} else {
|
|
408
|
-
console.log(
|
|
409
|
-
`Service auto-install is unsupported on ${process.platform}. Run: node ${script} run`,
|
|
410
|
-
);
|
|
584
|
+
return { service: 'Task Scheduler', detail: 'Jian Remote (ONLOGON)' };
|
|
411
585
|
}
|
|
586
|
+
printInfo(`Service auto-install is unsupported on ${process.platform}. Run: node ${script} run`);
|
|
587
|
+
return { service: 'none', detail: process.platform };
|
|
412
588
|
}
|
|
413
589
|
|
|
414
590
|
async function install() {
|
|
@@ -431,21 +607,21 @@ async function install() {
|
|
|
431
607
|
};
|
|
432
608
|
await writeConfigFile(config);
|
|
433
609
|
const script = await materializeWorkerScript();
|
|
434
|
-
await startOrRestartService(script);
|
|
435
|
-
|
|
610
|
+
const started = await startOrRestartService(script);
|
|
611
|
+
const facts = [
|
|
612
|
+
['Service', `${started.service} started`],
|
|
613
|
+
['Config', configPath],
|
|
614
|
+
];
|
|
436
615
|
if (gatewayVersion) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
);
|
|
616
|
+
const release = parseRelease(gatewayVersion);
|
|
617
|
+
facts.push(['Gateway', `${gatewayVersion} (major.minor ${release?.major}.${release?.minor})`]);
|
|
440
618
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
console.log(
|
|
448
|
-
'If the worker later looks stuck offline, restart the service (or run update) and check ~/.config/jian-remote/worker.log.',
|
|
619
|
+
if (started.detail) facts.push(['Identity', started.detail]);
|
|
620
|
+
printSuccess('Installed Jian Remote', facts);
|
|
621
|
+
printInfo('Upgrade later: npx @jian-ai/remote-control@latest update (no token again)');
|
|
622
|
+
printInfo('Change URL or token: jian-remote configure --gateway … --token …');
|
|
623
|
+
printInfo(
|
|
624
|
+
'If Offline sticks: update or restart the service; check ~/.config/jian-remote/worker.log',
|
|
449
625
|
);
|
|
450
626
|
}
|
|
451
627
|
|
|
@@ -498,19 +674,23 @@ async function configure() {
|
|
|
498
674
|
} catch {
|
|
499
675
|
script = await materializeWorkerScript();
|
|
500
676
|
}
|
|
501
|
-
await startOrRestartService(script);
|
|
677
|
+
const started = await startOrRestartService(script);
|
|
502
678
|
|
|
503
679
|
const changed = [
|
|
504
680
|
gatewayFlag ? 'gateway' : null,
|
|
505
681
|
machineFlag ? 'machine' : null,
|
|
506
682
|
tokenFlag ? 'token' : null,
|
|
507
683
|
].filter(Boolean);
|
|
508
|
-
|
|
684
|
+
const facts = [
|
|
685
|
+
['Changed', changed.join(', ')],
|
|
686
|
+
['Config', configPath],
|
|
687
|
+
['Service', `${started.service} restarted`],
|
|
688
|
+
];
|
|
509
689
|
if (gatewayVersion) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
);
|
|
690
|
+
const release = parseRelease(gatewayVersion);
|
|
691
|
+
facts.push(['Gateway', `${gatewayVersion} (major.minor ${release?.major}.${release?.minor})`]);
|
|
513
692
|
}
|
|
693
|
+
printSuccess('Configured Jian Remote', facts);
|
|
514
694
|
}
|
|
515
695
|
|
|
516
696
|
/**
|
|
@@ -540,7 +720,7 @@ async function update() {
|
|
|
540
720
|
scriptBytes = await readFile(fileURLToPath(import.meta.url));
|
|
541
721
|
installedVersion = PACKAGE_VERSION;
|
|
542
722
|
} else {
|
|
543
|
-
|
|
723
|
+
printInfo(`Downloading @jian-ai/remote-control@${target}…`);
|
|
544
724
|
scriptBytes = fetchNpmWorkerScript(target);
|
|
545
725
|
installedVersion = readEmbeddedVersion(scriptBytes.toString('utf8'));
|
|
546
726
|
}
|
|
@@ -557,18 +737,22 @@ async function update() {
|
|
|
557
737
|
{ stdio: 'inherit', windowsHide: true },
|
|
558
738
|
);
|
|
559
739
|
if (npmInstall.status !== 0) {
|
|
560
|
-
|
|
740
|
+
printInfo(
|
|
561
741
|
'Global npm install skipped or failed; the service still uses ~/.config/jian-remote/jian-remote.mjs.',
|
|
562
742
|
);
|
|
563
743
|
}
|
|
564
744
|
|
|
565
|
-
await startOrRestartService(script);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
745
|
+
const started = await startOrRestartService(script);
|
|
746
|
+
const facts = [
|
|
747
|
+
['Version', installedVersion],
|
|
748
|
+
['Service', `${started.service} restarted`],
|
|
749
|
+
];
|
|
750
|
+
if (gatewayVersion) facts.push(['Gateway', gatewayVersion]);
|
|
751
|
+
if (started.detail) facts.push(['Identity', started.detail]);
|
|
752
|
+
printSuccess('Updated Jian Remote', facts);
|
|
569
753
|
}
|
|
570
754
|
|
|
571
|
-
async function
|
|
755
|
+
async function executeShell(task) {
|
|
572
756
|
return await new Promise((resolveResult) => {
|
|
573
757
|
const command = process.platform === 'win32' ? 'powershell.exe' : '/bin/sh';
|
|
574
758
|
const commandArgs =
|
|
@@ -582,6 +766,7 @@ async function execute(task) {
|
|
|
582
766
|
});
|
|
583
767
|
let stdout = '';
|
|
584
768
|
let stderr = '';
|
|
769
|
+
let stdoutSeen = 0;
|
|
585
770
|
let timedOut = false;
|
|
586
771
|
const timeout = setTimeout(() => {
|
|
587
772
|
timedOut = true;
|
|
@@ -598,6 +783,7 @@ async function execute(task) {
|
|
|
598
783
|
}, 2000).unref();
|
|
599
784
|
}, task.timeoutMs);
|
|
600
785
|
child.stdout.on('data', (chunk) => {
|
|
786
|
+
stdoutSeen += chunk.toString('utf8').length;
|
|
601
787
|
stdout = cappedAppend(stdout, chunk, STDOUT_CAP);
|
|
602
788
|
});
|
|
603
789
|
child.stderr.on('data', (chunk) => {
|
|
@@ -615,11 +801,37 @@ async function execute(task) {
|
|
|
615
801
|
});
|
|
616
802
|
child.on('close', (code) => {
|
|
617
803
|
clearTimeout(timeout);
|
|
804
|
+
// Mark silent stdout cuts so screenshot tools can reject grey/truncated JPEGs.
|
|
805
|
+
if (stdoutSeen > STDOUT_CAP) {
|
|
806
|
+
stderr = cappedAppend(
|
|
807
|
+
stderr,
|
|
808
|
+
'\n[jian-remote] stdout truncated at the worker channel cap\n',
|
|
809
|
+
STDERR_CAP,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
618
812
|
resolveResult({ taskId: task.id, exitCode: code, stdout, stderr, timedOut });
|
|
619
813
|
});
|
|
620
814
|
});
|
|
621
815
|
}
|
|
622
816
|
|
|
817
|
+
async function execute(task) {
|
|
818
|
+
if (task.kind === 'computer') {
|
|
819
|
+
if (process.platform !== 'darwin') {
|
|
820
|
+
return {
|
|
821
|
+
taskId: task.id,
|
|
822
|
+
exitCode: 1,
|
|
823
|
+
stdout: '',
|
|
824
|
+
stderr: 'Computer use is supported on macOS Jian Remote only.',
|
|
825
|
+
timedOut: false,
|
|
826
|
+
computer: { ok: false, error: 'Computer use is supported on macOS Jian Remote only.' },
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
const { executeComputerTask } = await import('./computer-macos.mjs');
|
|
830
|
+
return executeComputerTask(task);
|
|
831
|
+
}
|
|
832
|
+
return executeShell(task);
|
|
833
|
+
}
|
|
834
|
+
|
|
623
835
|
async function updateIfNeeded(base, headers) {
|
|
624
836
|
// Prefer the installed copy under ~/.config so updates land where the service points.
|
|
625
837
|
const script = installedScript;
|
|
@@ -727,16 +939,26 @@ async function postResult(base, headers, result) {
|
|
|
727
939
|
|
|
728
940
|
async function run() {
|
|
729
941
|
const config = await loadConfig();
|
|
942
|
+
const capabilities = [];
|
|
943
|
+
if (process.platform === 'darwin') {
|
|
944
|
+
try {
|
|
945
|
+
const { probeComputerCapability, COMPUTER_CAPABILITY } = await import('./computer-macos.mjs');
|
|
946
|
+
if (await probeComputerCapability()) capabilities.push(COMPUTER_CAPABILITY);
|
|
947
|
+
} catch {
|
|
948
|
+
// Computer use unavailable until helper builds; shell tasks still work.
|
|
949
|
+
}
|
|
950
|
+
}
|
|
730
951
|
const headers = {
|
|
731
952
|
authorization: `Bearer ${config.token}`,
|
|
732
953
|
'x-jian-platform': `${process.platform}-${process.arch}`,
|
|
733
954
|
'x-jian-remote-version': PACKAGE_VERSION,
|
|
955
|
+
...(capabilities.length ? { 'x-jian-remote-capabilities': capabilities.join(',') } : {}),
|
|
734
956
|
};
|
|
735
957
|
const base = `${config.gateway}/v1/remote/worker/${config.machineId}`;
|
|
736
958
|
// Fail fast before the poll loop so an incompatible build does not look "online then stuck".
|
|
737
959
|
const gatewayVersion = await assertCompatibleWithGateway(config.gateway);
|
|
738
960
|
console.log(
|
|
739
|
-
`Connected to Jian Remote at ${config.gateway} (worker ${PACKAGE_VERSION}${gatewayVersion ? `, gateway ${gatewayVersion}` : ''})`,
|
|
961
|
+
`Connected to Jian Remote at ${config.gateway} (worker ${PACKAGE_VERSION}${gatewayVersion ? `, gateway ${gatewayVersion}` : ''}${capabilities.length ? `, capabilities ${capabilities.join(',')}` : ''})`,
|
|
740
962
|
);
|
|
741
963
|
let failures = 0;
|
|
742
964
|
let nextUpdateCheck = 0;
|
|
@@ -806,12 +1028,26 @@ if (isCliEntry()) {
|
|
|
806
1028
|
else if (args[0] === 'configure') await configure();
|
|
807
1029
|
else if (args[0] === 'update') await update();
|
|
808
1030
|
else if (args[0] === 'run') await run();
|
|
809
|
-
else
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
);
|
|
1031
|
+
else {
|
|
1032
|
+
const color = shouldColorize();
|
|
1033
|
+
const title = paint('Jian Remote', ANSI.bold, color);
|
|
1034
|
+
const pkg = paint('(@jian-ai/remote-control)', ANSI.dim, color);
|
|
1035
|
+
console.log(`${title} worker ${pkg}
|
|
1036
|
+
install --gateway HTTPS_URL --machine UUID (--token VALUE | --token-prompt | --token-stdin)
|
|
1037
|
+
configure [--gateway HTTPS_URL] [--machine UUID] (--token VALUE | --token-prompt | --token-stdin)
|
|
1038
|
+
update [--version X.Y.Z|latest]
|
|
1039
|
+
run
|
|
1040
|
+
|
|
1041
|
+
Install: npm install -g @jian-ai/remote-control
|
|
1042
|
+
Or: npx @jian-ai/remote-control@latest install --gateway … --machine … --token …
|
|
1043
|
+
Configure: jian-remote configure --token … (or --gateway / --machine; restarts the service)
|
|
1044
|
+
Update: npx @jian-ai/remote-control@latest update
|
|
1045
|
+
(upgrades the worker binary; reads ~/.config/jian-remote)`);
|
|
1046
|
+
}
|
|
813
1047
|
} catch (error) {
|
|
814
|
-
|
|
1048
|
+
const color = shouldColorize({ isTTY: process.stderr.isTTY });
|
|
1049
|
+
const prefix = paint('Error', ANSI.red, color);
|
|
1050
|
+
console.error(`${prefix}: ${error instanceof Error ? error.message : error}`);
|
|
815
1051
|
process.exitCode = 1;
|
|
816
1052
|
}
|
|
817
1053
|
}
|