@rynx-ai/cli 0.1.11-beta.5 → 0.1.11-beta.51
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/dist/autostart-environment.d.ts +5 -0
- package/dist/autostart-environment.js +64 -0
- package/dist/autostart-launcher.d.ts +1 -0
- package/dist/autostart-launcher.js +48 -0
- package/dist/autostart.d.ts +29 -0
- package/dist/autostart.js +266 -0
- package/dist/browser-cli-args.d.ts +9 -9
- package/dist/browser-cli-args.js +57 -75
- package/dist/commands/autostart.d.ts +1 -0
- package/dist/commands/autostart.js +58 -0
- package/dist/commands/browser.d.ts +4 -0
- package/dist/commands/browser.js +88 -119
- package/dist/commands/lifecycle.d.ts +3 -1
- package/dist/commands/lifecycle.js +85 -9
- package/dist/commands/plugin.js +52 -26
- package/dist/commands/setup.js +107 -17
- package/dist/commands/skills.js +2 -2
- package/dist/commands/update.js +2 -2
- package/dist/control-client.d.ts +23 -2
- package/dist/control-client.js +109 -11
- package/dist/desktop-browser-host-client.js +43 -10
- package/dist/legacy-maintenance.d.ts +8 -0
- package/dist/legacy-maintenance.js +13 -0
- package/dist/progress-display.d.ts +7 -0
- package/dist/progress-display.js +78 -0
- package/dist/run-cli.js +4 -0
- package/dist/standalone.d.ts +3 -1
- package/dist/standalone.js +20 -1
- package/dist/systemd-service.d.ts +53 -0
- package/dist/systemd-service.js +509 -0
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +4 -7
- package/package.json +7 -7
- package/skill-guides/browser.md +99 -20
- package/skills/rynx-cli/SKILL.md +9 -4
package/dist/commands/setup.js
CHANGED
|
@@ -3,10 +3,15 @@ import { chmodSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, re
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import * as prompts from "@clack/prompts";
|
|
5
5
|
import { AGENT_RUNTIME_IDS, getRuntimeProfile, loadConfig, rynxConfigFile, } from "@rynx-ai/core";
|
|
6
|
-
import { inspectDaemonDiagnostics, prepareBundledPlugins, } from "@rynx-ai/daemon/setup-service";
|
|
6
|
+
import { inspectDaemonDiagnostics, inspectSystemDependencies, prepareBundledPlugins, } from "@rynx-ai/daemon/setup-service";
|
|
7
|
+
import { BUNDLED_TMUX_UNAVAILABLE_MESSAGE, resolveBundledTmux, } from "@rynx-ai/tmux";
|
|
8
|
+
import { enableAutostart, inspectAutostart, } from "../autostart.js";
|
|
9
|
+
import { createProgressDisplay } from "../progress-display.js";
|
|
7
10
|
import { fail } from "./errors.js";
|
|
11
|
+
import { runLifecycleCommand } from "./lifecycle.js";
|
|
8
12
|
export async function runSetupCommand(args) {
|
|
9
13
|
const options = parseSetupOptions(args);
|
|
14
|
+
const progress = options.json ? undefined : createProgressDisplay();
|
|
10
15
|
const interactive = !options.nonInteractive &&
|
|
11
16
|
!options.hasConfigurationArguments &&
|
|
12
17
|
Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -50,23 +55,30 @@ export async function runSetupCommand(args) {
|
|
|
50
55
|
},
|
|
51
56
|
};
|
|
52
57
|
let failed = false;
|
|
58
|
+
progress?.start("正在准备 Rynx 内置插件");
|
|
53
59
|
try {
|
|
54
60
|
const plugins = await prepareBundledPlugins();
|
|
55
61
|
result.plugins = {
|
|
56
62
|
status: plugins.status,
|
|
57
63
|
changed: [...plugins.installed, ...plugins.updated],
|
|
58
64
|
};
|
|
65
|
+
progress?.succeed("Rynx 内置插件已就绪");
|
|
59
66
|
}
|
|
60
67
|
catch (error) {
|
|
61
68
|
failed = true;
|
|
69
|
+
progress?.clear();
|
|
70
|
+
const detail = errorMessage(error);
|
|
62
71
|
result.plugins = {
|
|
63
72
|
status: "error",
|
|
64
|
-
detail
|
|
73
|
+
detail,
|
|
65
74
|
};
|
|
75
|
+
console.error(`Rynx 内置插件准备失败:${detail}`);
|
|
66
76
|
}
|
|
77
|
+
progress?.start("正在检查 Rynx Browser");
|
|
67
78
|
const diagnostics = await inspectDaemonDiagnostics();
|
|
68
79
|
if (!diagnostics.browser.supported) {
|
|
69
80
|
result.browser = { action: options.browser, status: "unsupported" };
|
|
81
|
+
progress?.succeed("当前平台不支持 Rynx Browser");
|
|
70
82
|
}
|
|
71
83
|
else if (diagnostics.browser.installedVersion) {
|
|
72
84
|
result.browser = {
|
|
@@ -74,29 +86,42 @@ export async function runSetupCommand(args) {
|
|
|
74
86
|
status: "ready",
|
|
75
87
|
version: diagnostics.browser.installedVersion,
|
|
76
88
|
};
|
|
89
|
+
progress?.succeed(`Rynx Browser 已就绪:${diagnostics.browser.installedVersion}`);
|
|
77
90
|
}
|
|
78
91
|
else if (options.browser !== "skip") {
|
|
92
|
+
progress?.update("正在安装 Rynx Browser");
|
|
79
93
|
try {
|
|
80
94
|
const { createBrowserArtifactManagementService } = await import("@rynx-ai/daemon/browser-artifacts");
|
|
81
|
-
const installed = await createBrowserArtifactManagementService().install({ channel: "stable" },
|
|
82
|
-
? {}
|
|
83
|
-
: { onProgress: (message) => console.error(message) });
|
|
95
|
+
const installed = await createBrowserArtifactManagementService().install({ channel: "stable" }, progress ? { onProgress: (message) => progress.update(message) } : {});
|
|
84
96
|
result.browser = {
|
|
85
97
|
action: options.browser,
|
|
86
98
|
status: "ready",
|
|
87
99
|
version: installed.installed.version,
|
|
88
100
|
};
|
|
101
|
+
progress?.succeed(`Rynx Browser 已就绪:${installed.installed.version}`);
|
|
89
102
|
}
|
|
90
103
|
catch (error) {
|
|
91
104
|
failed = true;
|
|
105
|
+
progress?.clear();
|
|
106
|
+
const detail = errorMessage(error);
|
|
92
107
|
result.browser = {
|
|
93
108
|
action: options.browser,
|
|
94
109
|
status: "error",
|
|
95
|
-
detail
|
|
110
|
+
detail,
|
|
96
111
|
};
|
|
112
|
+
console.error(`Rynx Browser 安装失败:${detail}`);
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
|
-
|
|
115
|
+
else {
|
|
116
|
+
progress?.succeed("已跳过 Rynx Browser 安装");
|
|
117
|
+
}
|
|
118
|
+
if (interactive && process.env.RYNX_DISTRIBUTION !== "app") {
|
|
119
|
+
failed = await offerBackgroundServiceSetup() || failed;
|
|
120
|
+
}
|
|
121
|
+
if (options.resultFile) {
|
|
122
|
+
writeJsonAtomically(options.resultFile, result);
|
|
123
|
+
}
|
|
124
|
+
else if (options.json) {
|
|
100
125
|
console.log(JSON.stringify(result, null, 2));
|
|
101
126
|
}
|
|
102
127
|
else if (interactive) {
|
|
@@ -140,13 +165,25 @@ export async function runDoctorCommand(args) {
|
|
|
140
165
|
return { id, installed: !probe.error && probe.status === 0 };
|
|
141
166
|
});
|
|
142
167
|
const daemon = await inspectDaemonDiagnostics();
|
|
168
|
+
const tmuxBin = resolveBundledTmux();
|
|
169
|
+
const systemDependencies = tmuxBin
|
|
170
|
+
? await inspectSystemDependencies({ tmuxBin })
|
|
171
|
+
: {
|
|
172
|
+
tmux: {
|
|
173
|
+
installed: false,
|
|
174
|
+
detail: BUNDLED_TMUX_UNAVAILABLE_MESSAGE,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
143
177
|
const result = {
|
|
144
|
-
ok: configError === undefined &&
|
|
178
|
+
ok: configError === undefined &&
|
|
179
|
+
runtimes.some((runtime) => runtime.installed) &&
|
|
180
|
+
systemDependencies.tmux.installed,
|
|
145
181
|
configPath: rynxConfigFile(),
|
|
146
182
|
config,
|
|
147
183
|
configError,
|
|
148
184
|
runtimes,
|
|
149
185
|
daemon,
|
|
186
|
+
systemDependencies,
|
|
150
187
|
};
|
|
151
188
|
if (json)
|
|
152
189
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -159,6 +196,7 @@ export async function runDoctorCommand(args) {
|
|
|
159
196
|
for (const runtime of runtimes) {
|
|
160
197
|
console.log(`${runtime.id}: ${runtime.installed ? "installed" : "not installed"}`);
|
|
161
198
|
}
|
|
199
|
+
console.log(`tmux: ${systemDependencies.tmux.installed ? "installed" : "not installed"}`);
|
|
162
200
|
if (daemon.browser.supported) {
|
|
163
201
|
console.log(`browser: ${daemon.browser.installedVersion ?? daemon.browser.error ?? "not installed"}`);
|
|
164
202
|
}
|
|
@@ -186,6 +224,17 @@ function parseSetupOptions(args) {
|
|
|
186
224
|
options.nonInteractive = true;
|
|
187
225
|
continue;
|
|
188
226
|
}
|
|
227
|
+
if (arg === "--result-file") {
|
|
228
|
+
if (options.resultFile !== undefined)
|
|
229
|
+
fail("setup: duplicate option --result-file");
|
|
230
|
+
const value = args[index + 1];
|
|
231
|
+
if (!value || value.startsWith("--"))
|
|
232
|
+
fail("setup: --result-file requires a value");
|
|
233
|
+
options.resultFile = path.resolve(value);
|
|
234
|
+
options.nonInteractive = true;
|
|
235
|
+
index += 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
189
238
|
if (arg === "--install-browser" || arg === "--skip-browser") {
|
|
190
239
|
if (options.browser !== "auto")
|
|
191
240
|
fail("setup: choose only one Browser action");
|
|
@@ -225,6 +274,9 @@ function parseSetupOptions(args) {
|
|
|
225
274
|
fail(`setup: unknown option ${arg}`);
|
|
226
275
|
}
|
|
227
276
|
}
|
|
277
|
+
if (options.json && options.resultFile) {
|
|
278
|
+
fail("setup: choose either --json or --result-file");
|
|
279
|
+
}
|
|
228
280
|
return options;
|
|
229
281
|
}
|
|
230
282
|
async function collectInteractiveSetup(config) {
|
|
@@ -257,19 +309,55 @@ async function collectInteractiveSetup(config) {
|
|
|
257
309
|
if (prompts.isCancel(host))
|
|
258
310
|
return cancelled();
|
|
259
311
|
config.HOST = host;
|
|
260
|
-
const logLevel = await prompts.text({
|
|
261
|
-
message: "Log level",
|
|
262
|
-
initialValue: String(config.LOG_LEVEL),
|
|
263
|
-
});
|
|
264
|
-
if (prompts.isCancel(logLevel))
|
|
265
|
-
return cancelled();
|
|
266
|
-
config.LOG_LEVEL = logLevel;
|
|
267
312
|
return true;
|
|
268
313
|
}
|
|
269
314
|
function cancelled() {
|
|
270
315
|
prompts.cancel("Cancelled");
|
|
271
316
|
return false;
|
|
272
317
|
}
|
|
318
|
+
async function offerBackgroundServiceSetup() {
|
|
319
|
+
const state = inspectAutostart();
|
|
320
|
+
if (!state.supported) {
|
|
321
|
+
prompts.log.warn(`Background service setup is unavailable${state.detail ? `: ${state.detail}` : ""}`);
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
const confirmed = await prompts.confirm({
|
|
325
|
+
message: state.registered
|
|
326
|
+
? "Start the Rynx background service now? (Autostart is already enabled)"
|
|
327
|
+
: "Start Rynx now and automatically at login?",
|
|
328
|
+
initialValue: true,
|
|
329
|
+
});
|
|
330
|
+
if (prompts.isCancel(confirmed) || !confirmed) {
|
|
331
|
+
prompts.log.info("Skipped background service setup");
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
let failed = false;
|
|
335
|
+
if (!state.registered) {
|
|
336
|
+
try {
|
|
337
|
+
const enabled = enableAutostart();
|
|
338
|
+
prompts.log.success(`Autostart enabled${enabled.registrationPath ? `: ${enabled.registrationPath}` : ""}`);
|
|
339
|
+
if (enabled.manager === "systemd-user" && enabled.lingerEnabled === false) {
|
|
340
|
+
prompts.log.warn("Linger is disabled; Rynx stops when this user logs out");
|
|
341
|
+
if (enabled.lingerEnableCommand) {
|
|
342
|
+
prompts.log.info(`Optional, requires sudo and is never run by setup: ${enabled.lingerEnableCommand}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
failed = true;
|
|
348
|
+
prompts.log.warn(`Could not enable autostart: ${errorMessage(error)}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const startStatus = await runLifecycleCommand("start", [], { quiet: true });
|
|
352
|
+
if (startStatus === 0) {
|
|
353
|
+
prompts.log.success("Rynx background service is running");
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
failed = true;
|
|
357
|
+
prompts.log.warn("Rynx background service failed to start; run `rynx start` for details");
|
|
358
|
+
}
|
|
359
|
+
return failed;
|
|
360
|
+
}
|
|
273
361
|
function validateSetupConfig(config) {
|
|
274
362
|
loadConfig({
|
|
275
363
|
HOST: String(config.HOST),
|
|
@@ -290,14 +378,16 @@ function readRawConfig() {
|
|
|
290
378
|
}
|
|
291
379
|
}
|
|
292
380
|
function writeConfigAtomically(config) {
|
|
293
|
-
|
|
381
|
+
writeJsonAtomically(rynxConfigFile(), config);
|
|
382
|
+
}
|
|
383
|
+
function writeJsonAtomically(file, value) {
|
|
294
384
|
const directory = path.dirname(file);
|
|
295
385
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
296
386
|
const temporary = path.join(directory, `.${path.basename(file)}.${process.pid}.tmp`);
|
|
297
387
|
let descriptor;
|
|
298
388
|
try {
|
|
299
389
|
descriptor = openSync(temporary, "wx", 0o600);
|
|
300
|
-
writeFileSync(descriptor, `${JSON.stringify(
|
|
390
|
+
writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
301
391
|
closeSync(descriptor);
|
|
302
392
|
descriptor = undefined;
|
|
303
393
|
renameSync(temporary, file);
|
package/dist/commands/skills.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isSkillPathComponent } from "@rynx-ai/core";
|
|
4
5
|
import { fail } from "./errors.js";
|
|
5
|
-
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
6
6
|
const MAX_SKILL_BYTES = 256 * 1024;
|
|
7
7
|
const BUILTIN_SKILL_NAMES = ["browser", "emulator"];
|
|
8
8
|
export async function runSkillsCommand(args) {
|
|
@@ -55,7 +55,7 @@ export async function listBuiltinSkills() {
|
|
|
55
55
|
return guides.filter((guide) => guide !== null);
|
|
56
56
|
}
|
|
57
57
|
export async function readBuiltinSkill(name, full = false) {
|
|
58
|
-
if (!
|
|
58
|
+
if (!isSkillPathComponent(name))
|
|
59
59
|
return null;
|
|
60
60
|
if (!BUILTIN_SKILL_NAMES.includes(name))
|
|
61
61
|
return null;
|
package/dist/commands/update.js
CHANGED
|
@@ -3,7 +3,7 @@ import { closeSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join, sep } from "node:path";
|
|
5
5
|
import { rynxHome } from "@rynx-ai/core";
|
|
6
|
-
import {
|
|
6
|
+
import { runLifecycleCommand } from "./lifecycle.js";
|
|
7
7
|
function selfPackage() {
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
const manifest = require("../../package.json");
|
|
@@ -72,7 +72,7 @@ export async function runUpdate(options) {
|
|
|
72
72
|
if (!releaseLock)
|
|
73
73
|
return 1;
|
|
74
74
|
try {
|
|
75
|
-
if (await
|
|
75
|
+
if (await runLifecycleCommand("stop", []) !== 0) {
|
|
76
76
|
console.error("update: could not stop the standalone daemon");
|
|
77
77
|
return 1;
|
|
78
78
|
}
|
package/dist/control-client.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { type DaemonStatus } from "@rynx-ai/protocol/remote-runtime";
|
|
|
2
2
|
import { type DaemonCleanupSessionsInput, type DaemonCleanupSessionsResult, type DaemonChromeInspectionConfigureInput, type DaemonChromeInspectionStatus, type DaemonShutdownIfIdleResult } from "@rynx-ai/protocol/control";
|
|
3
3
|
import { type RemoteRuntimeRpcMethod, type RemoteRuntimeRpcParams, type RemoteRuntimeRpcResultFor } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
4
4
|
import { type PairingOffer } from "@rynx-ai/protocol/direct-runtime";
|
|
5
|
-
import { type RuntimeBrowserBootstrapCredential, type RuntimeBrowserEndpointDescriptor } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
5
|
+
import { type RuntimeBrowserAutomationCommand, type RuntimeBrowserAutomationResult, type RuntimeBrowserBootstrapCredential, type RuntimeBrowserEndpointDescriptor } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
6
6
|
import { type PluginInstallCommitInput, type PluginInstallCommitResult, type PluginInstallPreparation, type PluginInstallPrepareInput, type PluginManagementItem, type PluginManagementState, type PluginMarketplaceAddInput, type PluginMarketplaceItem } from "@rynx-ai/protocol/plugin-management";
|
|
7
|
+
import { type DaemonControlEndpoint } from "./control-endpoint.js";
|
|
7
8
|
export { connectResidentDesktopBrowserHost, type ResidentDesktopBrowserHostConnection, type ResidentDesktopBrowserHostCommandRequest, type ResidentDesktopBrowserHostConnectOptions, type ResidentDesktopBrowserHostFailure, } from "./desktop-browser-host-client.js";
|
|
8
9
|
export interface ResidentDaemonIdentity {
|
|
9
10
|
installationId: string;
|
|
@@ -65,7 +66,9 @@ export declare function getResidentDaemonRuntimeStatus(): Promise<DaemonStatus>;
|
|
|
65
66
|
* when idle. Busy is a normal result so the caller can ask the user to resolve
|
|
66
67
|
* the listed work and explicitly retry.
|
|
67
68
|
*/
|
|
68
|
-
export declare function shutdownResidentDaemonIfIdle(): Promise<DaemonShutdownIfIdleResult>;
|
|
69
|
+
export declare function shutdownResidentDaemonIfIdle(endpoint?: DaemonControlEndpoint): Promise<DaemonShutdownIfIdleResult>;
|
|
70
|
+
/** Wait for accepted graceful shutdown before the lifecycle owner is stopped. */
|
|
71
|
+
export declare function waitForResidentDaemonExit(pid: number, timeoutMs?: number): Promise<boolean>;
|
|
69
72
|
export declare function getResidentChromeInspectionStatus(): Promise<DaemonChromeInspectionStatus>;
|
|
70
73
|
export declare function configureResidentChromeInspection(input: DaemonChromeInspectionConfigureInput): Promise<DaemonChromeInspectionStatus>;
|
|
71
74
|
export declare function cleanupResidentSessions(input?: DaemonCleanupSessionsInput): Promise<DaemonCleanupSessionsResult>;
|
|
@@ -111,6 +114,24 @@ export declare function readOptionalManagedRuntimeBrowserCredential(env?: NodeJS
|
|
|
111
114
|
export declare function callManagedRuntimeBrowser<M extends RemoteRuntimeRpcMethod>(credential: RuntimeBrowserBootstrapCredential, method: M, params: RemoteRuntimeRpcParams<M>, options?: {
|
|
112
115
|
signal?: AbortSignal;
|
|
113
116
|
}): Promise<RemoteRuntimeRpcResultFor<M>>;
|
|
117
|
+
/** Execute native page-command argv within the caller's Session on this Runtime. */
|
|
118
|
+
export declare function executeResidentBrowserCommand(command: RuntimeBrowserAutomationCommand, options?: {
|
|
119
|
+
sessionId?: string;
|
|
120
|
+
credential?: RuntimeBrowserBootstrapCredential;
|
|
121
|
+
}): Promise<RuntimeBrowserAutomationResult>;
|
|
122
|
+
/** Keep the daemon's bounded public diagnostics, including no-replay guidance. */
|
|
123
|
+
export declare class ResidentBrowserCommandError extends Error {
|
|
124
|
+
readonly status: number;
|
|
125
|
+
readonly code: string;
|
|
126
|
+
readonly details: {
|
|
127
|
+
error: string;
|
|
128
|
+
code?: string;
|
|
129
|
+
message?: string;
|
|
130
|
+
hint?: string;
|
|
131
|
+
outcome?: "unknown" | "not_started";
|
|
132
|
+
};
|
|
133
|
+
constructor(status: number, body: string);
|
|
134
|
+
}
|
|
114
135
|
/** Resolve native CDP for the caller's own Session on this Runtime only. */
|
|
115
136
|
export declare function getResidentRuntimeLocalBrowserEndpoint(options?: {
|
|
116
137
|
env?: NodeJS.ProcessEnv;
|
package/dist/control-client.js
CHANGED
|
@@ -4,10 +4,10 @@ import { isDaemonCoreCompatible, parseDaemonStatus, } from "@rynx-ai/protocol/re
|
|
|
4
4
|
import { DAEMON_CHROME_INSPECTION_PATH, DAEMON_CLEANUP_SESSIONS_PATH, DAEMON_SHUTDOWN_IF_IDLE_PATH, parseDaemonCleanupSessionsResult, parseDaemonChromeInspectionConfigureInput, parseDaemonChromeInspectionStatus, parseDaemonShutdownIfIdleResult, } from "@rynx-ai/protocol/control";
|
|
5
5
|
import { parseRemoteRuntimeRpcRequest, parseRemoteRuntimeRpcResponseForMethod, REMOTE_RUNTIME_RPC_MAX_FRAME_BYTES, REMOTE_RUNTIME_RPC_METHOD_METADATA, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
6
6
|
import { parsePairingOffer, } from "@rynx-ai/protocol/direct-runtime";
|
|
7
|
-
import { RUNTIME_BROWSER_BOOTSTRAP_PATH, RUNTIME_BROWSER_CAPABILITY_ENV, RUNTIME_BROWSER_CAPABILITY_HEADER, RUNTIME_BROWSER_CONTEXT_FILE_ENV, RUNTIME_BROWSER_MANAGEMENT_PATH_PREFIX, RUNTIME_BROWSER_RPC_PATH, RUNTIME_BROWSER_SESSION_ID_ENV, RUNTIME_BROWSER_SESSION_ID_HEADER, parseRuntimeBrowserBootstrapCredential, parseRuntimeBrowserEndpointDescriptor, } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
7
|
+
import { RUNTIME_BROWSER_BOOTSTRAP_PATH, RUNTIME_BROWSER_CAPABILITY_ENV, RUNTIME_BROWSER_CAPABILITY_HEADER, RUNTIME_BROWSER_CONTEXT_FILE_ENV, RUNTIME_BROWSER_MANAGEMENT_PATH_PREFIX, RUNTIME_BROWSER_RPC_PATH, RUNTIME_BROWSER_EXECUTE_PATH, RUNTIME_BROWSER_SESSION_ID_ENV, RUNTIME_BROWSER_SESSION_ID_HEADER, parseRuntimeBrowserBootstrapCredential, parseRuntimeBrowserAutomationCommand, parseRuntimeBrowserEndpointDescriptor, } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
8
8
|
import { parseRuntimeBrowserStateGetParams } from "@rynx-ai/protocol/runtime-browser";
|
|
9
9
|
import { PLUGIN_INSTALL_PREPARATIONS_PATH, PLUGIN_MARKETPLACES_PATH, parsePluginInstallCommitInput, parsePluginInstallCommitResult, parsePluginInstallPreparation, parsePluginInstallPrepareInput, parsePluginManagementItem, parsePluginMarketplaceAddInput, parsePluginMarketplaceItem, pluginInstallCommitPath, pluginInstallPreparationPath, } from "@rynx-ai/protocol/plugin-management";
|
|
10
|
-
import { ensureDaemonControlEndpoint } from "./control-endpoint.js";
|
|
10
|
+
import { ensureDaemonControlEndpoint, } from "./control-endpoint.js";
|
|
11
11
|
export { connectResidentDesktopBrowserHost, } from "./desktop-browser-host-client.js";
|
|
12
12
|
const IDENTITY_TIMEOUT_MS = 5_000;
|
|
13
13
|
const RUNTIME_STATUS_TIMEOUT_MS = 5_000;
|
|
@@ -108,15 +108,36 @@ export async function getResidentDaemonRuntimeStatus() {
|
|
|
108
108
|
* when idle. Busy is a normal result so the caller can ask the user to resolve
|
|
109
109
|
* the listed work and explicitly retry.
|
|
110
110
|
*/
|
|
111
|
-
export async function shutdownResidentDaemonIfIdle() {
|
|
112
|
-
const
|
|
113
|
-
assertLoopbackOrigin(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
export async function shutdownResidentDaemonIfIdle(endpoint) {
|
|
112
|
+
const resident = endpoint ?? await ensureDaemonControlEndpoint();
|
|
113
|
+
assertLoopbackOrigin(resident.origin);
|
|
114
|
+
let response;
|
|
115
|
+
try {
|
|
116
|
+
response = await fetch(`${resident.origin}${DAEMON_SHUTDOWN_IF_IDLE_PATH}`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: managementHeaders(resident.managementToken),
|
|
119
|
+
signal: AbortSignal.timeout(DAEMON_SHUTDOWN_TIMEOUT_MS),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// The daemon may close its listener before the 202 reaches the client.
|
|
124
|
+
// The original PID exiting proves that the requested lifecycle boundary
|
|
125
|
+
// completed, so the caller can continue with supervisor cleanup.
|
|
126
|
+
if (await waitForResidentDaemonExit(resident.pid)) {
|
|
127
|
+
return { outcome: "accepted" };
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
let body;
|
|
132
|
+
try {
|
|
133
|
+
body = await readBoundedResponse(response, DAEMON_SHUTDOWN_RESPONSE_MAX_BYTES);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (response.status === 202 && await waitForResidentDaemonExit(resident.pid)) {
|
|
137
|
+
return { outcome: "accepted" };
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
120
141
|
if (response.status !== 202 && response.status !== 409) {
|
|
121
142
|
throw daemonRequestError("daemon shutdown-if-idle request", response.status, body);
|
|
122
143
|
}
|
|
@@ -125,6 +146,9 @@ export async function shutdownResidentDaemonIfIdle() {
|
|
|
125
146
|
result = parseDaemonShutdownIfIdleResult(JSON.parse(body));
|
|
126
147
|
}
|
|
127
148
|
catch (error) {
|
|
149
|
+
if (response.status === 202 && await waitForResidentDaemonExit(resident.pid)) {
|
|
150
|
+
return { outcome: "accepted" };
|
|
151
|
+
}
|
|
128
152
|
throw new Error(`daemon returned an invalid shutdown-if-idle response: ${messageOf(error)}`);
|
|
129
153
|
}
|
|
130
154
|
if (response.status === 202 && result.outcome !== "accepted" ||
|
|
@@ -133,6 +157,25 @@ export async function shutdownResidentDaemonIfIdle() {
|
|
|
133
157
|
}
|
|
134
158
|
return result;
|
|
135
159
|
}
|
|
160
|
+
/** Wait for accepted graceful shutdown before the lifecycle owner is stopped. */
|
|
161
|
+
export async function waitForResidentDaemonExit(pid, timeoutMs = 120_000) {
|
|
162
|
+
const deadline = Date.now() + timeoutMs;
|
|
163
|
+
while (Date.now() <= deadline) {
|
|
164
|
+
if (!processIsAlive(pid))
|
|
165
|
+
return true;
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
167
|
+
}
|
|
168
|
+
return !processIsAlive(pid);
|
|
169
|
+
}
|
|
170
|
+
function processIsAlive(pid) {
|
|
171
|
+
try {
|
|
172
|
+
process.kill(pid, 0);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return error.code === "EPERM";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
136
179
|
export async function getResidentChromeInspectionStatus() {
|
|
137
180
|
const result = await localManagementJsonRequest(DAEMON_CHROME_INSPECTION_PATH, {
|
|
138
181
|
method: "GET",
|
|
@@ -437,6 +480,8 @@ const MANAGED_BROWSER_RPC_METHODS = new Set([
|
|
|
437
480
|
"browser.page.back",
|
|
438
481
|
"browser.page.forward",
|
|
439
482
|
"browser.page.reload",
|
|
483
|
+
"browser.request-headers.get",
|
|
484
|
+
"browser.request-headers.set",
|
|
440
485
|
]);
|
|
441
486
|
/**
|
|
442
487
|
* Invoke Browser control for the caller's own managed Session. Unlike the
|
|
@@ -521,6 +566,59 @@ export async function callManagedRuntimeBrowser(credential, method, params, opti
|
|
|
521
566
|
: "daemon returned an invalid Runtime call result", response.status, { cause: error, ...(neverRetry ? { outcome: "unknown" } : {}) });
|
|
522
567
|
}
|
|
523
568
|
}
|
|
569
|
+
/** Execute native page-command argv within the caller's Session on this Runtime. */
|
|
570
|
+
export async function executeResidentBrowserCommand(command, options = {}) {
|
|
571
|
+
const body = parseRuntimeBrowserAutomationCommand(command);
|
|
572
|
+
const credential = options.credential;
|
|
573
|
+
const sessionId = credential?.sessionId ?? options.sessionId;
|
|
574
|
+
if (!sessionId)
|
|
575
|
+
throw new Error("Browser automation requires a Session");
|
|
576
|
+
const signal = AbortSignal.timeout(180_000);
|
|
577
|
+
const endpoint = await ensureDaemonControlEndpoint({ signal });
|
|
578
|
+
assertLoopbackOrigin(endpoint.origin);
|
|
579
|
+
const route = credential ? RUNTIME_BROWSER_EXECUTE_PATH : `${RUNTIME_BROWSER_MANAGEMENT_PATH_PREFIX}/${encodeURIComponent(sessionId)}/execute`;
|
|
580
|
+
const response = await fetch(`${endpoint.origin}${route}`, {
|
|
581
|
+
method: "POST", signal,
|
|
582
|
+
headers: {
|
|
583
|
+
"content-type": "application/json",
|
|
584
|
+
...(credential ? { [RUNTIME_BROWSER_SESSION_ID_HEADER]: credential.sessionId, [RUNTIME_BROWSER_CAPABILITY_HEADER]: credential.capability } : managementHeaders(endpoint.managementToken)),
|
|
585
|
+
},
|
|
586
|
+
body: JSON.stringify(body),
|
|
587
|
+
});
|
|
588
|
+
const raw = await readBoundedResponse(response, 8 * 1024 * 1024);
|
|
589
|
+
if (!response.ok)
|
|
590
|
+
throw new ResidentBrowserCommandError(response.status, raw);
|
|
591
|
+
const result = JSON.parse(raw);
|
|
592
|
+
if (!result || result.schemaVersion !== 2 || result.sessionId !== sessionId || typeof result.pageId !== "string" ||
|
|
593
|
+
!Number.isSafeInteger(result.browserGeneration) || result.browserGeneration < 1 || result.completed !== true ||
|
|
594
|
+
!result.data || typeof result.data !== "object" || Array.isArray(result.data))
|
|
595
|
+
throw new Error("daemon returned an invalid Browser execution result");
|
|
596
|
+
return result;
|
|
597
|
+
}
|
|
598
|
+
/** Keep the daemon's bounded public diagnostics, including no-replay guidance. */
|
|
599
|
+
export class ResidentBrowserCommandError extends Error {
|
|
600
|
+
status;
|
|
601
|
+
code;
|
|
602
|
+
details;
|
|
603
|
+
constructor(status, body) {
|
|
604
|
+
let parsed;
|
|
605
|
+
try {
|
|
606
|
+
parsed = parseJsonObject(body, "invalid Browser error");
|
|
607
|
+
}
|
|
608
|
+
catch { /* Plain-text errors remain useful. */ }
|
|
609
|
+
const bounded = (value, max = 2048) => typeof value === "string" && value.length > 0 ? value.slice(0, max) : undefined;
|
|
610
|
+
const error = bounded(parsed?.error, 128) ?? "browser_execution_failed";
|
|
611
|
+
const code = bounded(parsed?.code, 128);
|
|
612
|
+
const message = bounded(parsed?.message) ?? (parsed === undefined ? bounded(body.trim()) : undefined);
|
|
613
|
+
const hint = bounded(parsed?.hint);
|
|
614
|
+
super(`Runtime Browser execution failed (${status}): ${error}${message ? `: ${message}` : ""}${hint ? `\n${hint}` : ""}`);
|
|
615
|
+
this.status = status;
|
|
616
|
+
this.name = "ResidentBrowserCommandError";
|
|
617
|
+
this.code = code ?? error;
|
|
618
|
+
this.details = { error, ...(code ? { code } : {}), ...(message ? { message } : {}), ...(hint ? { hint } : {}),
|
|
619
|
+
...(parsed?.outcome === "unknown" || parsed?.outcome === "not_started" ? { outcome: parsed.outcome } : {}) };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
524
622
|
/** Resolve native CDP for the caller's own Session on this Runtime only. */
|
|
525
623
|
export async function getResidentRuntimeLocalBrowserEndpoint(options = {}) {
|
|
526
624
|
return (await getResidentRuntimeLocalBrowserAutomationAccess(options)).descriptor;
|
|
@@ -1,14 +1,36 @@
|
|
|
1
|
-
import { DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_PENDING_COMMANDS, DESKTOP_BROWSER_HOST_PATH, DESKTOP_BROWSER_HOST_PROTOCOL_VERSION, encodeDesktopBrowserHostSurfaceFrame, parseDesktopBrowserHostClientFrame, parseDesktopBrowserHostServerFrame, } from "@rynx-ai/protocol/desktop-browser-host";
|
|
1
|
+
import { DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_PENDING_COMMANDS, DESKTOP_BROWSER_HOST_MINIMUM_PROTOCOL_VERSION, DESKTOP_BROWSER_HOST_PATH, DESKTOP_BROWSER_HOST_PROTOCOL_VERSION, encodeDesktopBrowserHostSurfaceFrame, parseDesktopBrowserHostClientFrame, parseDesktopBrowserHostServerFrame, } from "@rynx-ai/protocol/desktop-browser-host";
|
|
2
2
|
import { WebSocket } from "ws";
|
|
3
3
|
import { ensureDaemonControlEndpoint } from "./control-endpoint.js";
|
|
4
4
|
const CONNECT_TIMEOUT_MS = 5_000;
|
|
5
5
|
const MAX_BUFFERED_BYTES = DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES + DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES;
|
|
6
6
|
/** Open the sole resident daemon's authenticated loopback Desktop Host lease. */
|
|
7
7
|
export async function connectResidentDesktopBrowserHost(options) {
|
|
8
|
+
options.signal?.throwIfAborted();
|
|
9
|
+
const endpoint = options.endpoint
|
|
10
|
+
?? await ensureDaemonControlEndpoint({ signal: options.signal });
|
|
11
|
+
assertDesktopBrowserHostEndpoint(endpoint);
|
|
12
|
+
try {
|
|
13
|
+
return await connectResidentDesktopBrowserHostVersion(options, endpoint, DESKTOP_BROWSER_HOST_PROTOCOL_VERSION);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
options.signal?.throwIfAborted();
|
|
17
|
+
if (!isLegacyProtocolRejection(error))
|
|
18
|
+
throw error;
|
|
19
|
+
// V1 daemons reject a V2 hello before issuing a lease. Reconnect once
|
|
20
|
+
// with their protocol so App and daemon can roll independently. Any
|
|
21
|
+
// failure after a lease is returned stays on that negotiated connection.
|
|
22
|
+
return connectResidentDesktopBrowserHostVersion(options, endpoint, DESKTOP_BROWSER_HOST_MINIMUM_PROTOCOL_VERSION).catch((legacyError) => {
|
|
23
|
+
throw new Error("Desktop Browser Host could not negotiate with the daemon", {
|
|
24
|
+
cause: new AggregateError([error, legacyError]),
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function connectResidentDesktopBrowserHostVersion(options, endpoint, protocolVersion) {
|
|
8
30
|
options.signal?.throwIfAborted();
|
|
9
31
|
const hello = parseDesktopBrowserHostClientFrame({
|
|
10
32
|
type: "desktop.browser.host.hello",
|
|
11
|
-
protocolVersion
|
|
33
|
+
protocolVersion,
|
|
12
34
|
hostInstanceId: options.hostInstanceId,
|
|
13
35
|
capabilities: {
|
|
14
36
|
semanticPageBinding: options.capabilities.semanticPageBinding,
|
|
@@ -19,9 +41,6 @@ export async function connectResidentDesktopBrowserHost(options) {
|
|
|
19
41
|
if (hello.type !== "desktop.browser.host.hello") {
|
|
20
42
|
throw new Error("Desktop Browser Host hello parser returned the wrong frame");
|
|
21
43
|
}
|
|
22
|
-
const endpoint = options.endpoint
|
|
23
|
-
?? await ensureDaemonControlEndpoint({ signal: options.signal });
|
|
24
|
-
assertDesktopBrowserHostEndpoint(endpoint);
|
|
25
44
|
const url = new URL(DESKTOP_BROWSER_HOST_PATH, `${endpoint.origin}/`);
|
|
26
45
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
27
46
|
const socket = new WebSocket(url, {
|
|
@@ -139,7 +158,7 @@ export async function connectResidentDesktopBrowserHost(options) {
|
|
|
139
158
|
socket.once("close", (code, reason) => {
|
|
140
159
|
options.signal?.removeEventListener("abort", onAbort);
|
|
141
160
|
if (!terminalError && !closing) {
|
|
142
|
-
terminalError = new
|
|
161
|
+
terminalError = new DesktopBrowserHostLeaseClosedError(code, `Desktop Browser Host lease closed (${code})${reason.length > 0 ? `: ${reason.toString("utf8")}` : ""}`);
|
|
143
162
|
}
|
|
144
163
|
if (!lease && terminalError)
|
|
145
164
|
leaseReject(terminalError);
|
|
@@ -224,7 +243,7 @@ export async function connectResidentDesktopBrowserHost(options) {
|
|
|
224
243
|
signal: controller.signal,
|
|
225
244
|
reply: (result) => settle(command, {
|
|
226
245
|
type: "desktop.browser.host.reply",
|
|
227
|
-
protocolVersion:
|
|
246
|
+
protocolVersion: activeLease.protocolVersion,
|
|
228
247
|
leaseId: activeLease.leaseId,
|
|
229
248
|
commandId: command.commandId,
|
|
230
249
|
ok: true,
|
|
@@ -232,7 +251,7 @@ export async function connectResidentDesktopBrowserHost(options) {
|
|
|
232
251
|
}),
|
|
233
252
|
reject: (failure) => settle(command, {
|
|
234
253
|
type: "desktop.browser.host.reply",
|
|
235
|
-
protocolVersion:
|
|
254
|
+
protocolVersion: activeLease.protocolVersion,
|
|
236
255
|
leaseId: activeLease.leaseId,
|
|
237
256
|
commandId: command.commandId,
|
|
238
257
|
ok: false,
|
|
@@ -250,7 +269,7 @@ export async function connectResidentDesktopBrowserHost(options) {
|
|
|
250
269
|
const nextSequence = eventSequence + 1;
|
|
251
270
|
const frame = parseDesktopBrowserHostClientFrame({
|
|
252
271
|
type: "desktop.browser.host.event",
|
|
253
|
-
protocolVersion:
|
|
272
|
+
protocolVersion: activeLease.protocolVersion,
|
|
254
273
|
leaseId: activeLease.leaseId,
|
|
255
274
|
eventSequence: nextSequence,
|
|
256
275
|
event,
|
|
@@ -268,7 +287,7 @@ export async function connectResidentDesktopBrowserHost(options) {
|
|
|
268
287
|
const encoded = encodeDesktopBrowserHostSurfaceFrame({
|
|
269
288
|
...frame,
|
|
270
289
|
leaseId: activeLease.leaseId,
|
|
271
|
-
});
|
|
290
|
+
}, activeLease.protocolVersion);
|
|
272
291
|
await sendBinary(socket, encoded);
|
|
273
292
|
}),
|
|
274
293
|
close: async () => {
|
|
@@ -285,6 +304,20 @@ export async function connectResidentDesktopBrowserHost(options) {
|
|
|
285
304
|
},
|
|
286
305
|
};
|
|
287
306
|
}
|
|
307
|
+
class DesktopBrowserHostLeaseClosedError extends Error {
|
|
308
|
+
closeCode;
|
|
309
|
+
constructor(closeCode, message) {
|
|
310
|
+
super(message);
|
|
311
|
+
this.closeCode = closeCode;
|
|
312
|
+
this.name = "DesktopBrowserHostLeaseClosedError";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function isLegacyProtocolRejection(error) {
|
|
316
|
+
return error instanceof DesktopBrowserHostLeaseClosedError &&
|
|
317
|
+
// 4400 is the pre-V2 daemon's generic invalid-frame response. 1002 covers
|
|
318
|
+
// standards-based V1 peers; 4406 is the explicit unsupported-version code.
|
|
319
|
+
(error.closeCode === 1002 || error.closeCode === 4400 || error.closeCode === 4406);
|
|
320
|
+
}
|
|
288
321
|
async function sendBinary(socket, bytes) {
|
|
289
322
|
if (socket.readyState !== WebSocket.OPEN)
|
|
290
323
|
throw new Error("Desktop Browser Host lease is closed");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove the retired cross-process replacement fence.
|
|
3
|
+
*
|
|
4
|
+
* Older daemons may have left this file behind after an updater crash. The
|
|
5
|
+
* standalone stop path makes a best-effort attempt once lifecycle ownership is
|
|
6
|
+
* gone. This compatibility detail is not part of the public lifecycle result.
|
|
7
|
+
*/
|
|
8
|
+
export declare function clearLegacyMaintenanceState(): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { rynxHome } from "@rynx-ai/core";
|
|
4
|
+
/**
|
|
5
|
+
* Remove the retired cross-process replacement fence.
|
|
6
|
+
*
|
|
7
|
+
* Older daemons may have left this file behind after an updater crash. The
|
|
8
|
+
* standalone stop path makes a best-effort attempt once lifecycle ownership is
|
|
9
|
+
* gone. This compatibility detail is not part of the public lifecycle result.
|
|
10
|
+
*/
|
|
11
|
+
export function clearLegacyMaintenanceState() {
|
|
12
|
+
rmSync(join(rynxHome(), "maintenance-lease.json"), { force: true });
|
|
13
|
+
}
|