@rynx-ai/cli 0.1.11-beta.4 → 0.1.11-beta.41
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.d.ts +27 -0
- package/dist/autostart.js +229 -0
- package/dist/browser-cli-args.d.ts +7 -1
- package/dist/browser-cli-args.js +51 -6
- 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 +75 -6
- package/dist/commands/lifecycle.d.ts +3 -1
- package/dist/commands/lifecycle.js +22 -5
- 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.js +2 -0
- package/dist/desktop-browser-host-client.js +43 -10
- 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.js +3 -0
- package/dist/systemd-service.d.ts +58 -0
- package/dist/systemd-service.js +636 -0
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +2 -1
- package/package.json +8 -7
- package/skill-guides/browser.md +41 -1
- package/skills/rynx-cli/SKILL.md +9 -4
package/dist/commands/plugin.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import { cancelResidentPluginInstallation, commitResidentPluginInstallation,
|
|
2
|
+
import { cancelResidentPluginInstallation, commitResidentPluginInstallation, prepareResidentPluginInstallation, setResidentPluginEnabled, uninstallResidentPlugin, } from "../control-client.js";
|
|
3
3
|
import { fail } from "./errors.js";
|
|
4
|
-
const PLUGIN_STDIN_MAX_BYTES = 256 * 1024;
|
|
5
4
|
const PLUGIN_DIGEST_PATTERN = /^sha256-[A-Za-z0-9+/]{43}={0,2}$/;
|
|
6
5
|
const CANONICAL_PLUGIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,62}@[a-z0-9][a-z0-9-]{0,62}(?:\/[a-z0-9][a-z0-9._-]{0,99})?$/;
|
|
6
|
+
const PLUGIN_USAGE = `Usage: rynx plugin <command|plugin-id>
|
|
7
|
+
|
|
8
|
+
Management:
|
|
9
|
+
list
|
|
10
|
+
install <source|plugin@market> [--force] [--expect-digest <sha256-...>]
|
|
11
|
+
update <plugin@market> [--expect-digest <sha256-...>]
|
|
12
|
+
enable|disable|uninstall <plugin@market>
|
|
13
|
+
|
|
14
|
+
Plugin commands:
|
|
15
|
+
<plugin-id> <command> [args...]
|
|
16
|
+
<plugin-id> --help`;
|
|
7
17
|
export async function runPluginManageCommand(args, options = {}) {
|
|
8
18
|
const [subcommand, pluginId] = args;
|
|
9
19
|
switch (subcommand) {
|
|
10
20
|
case "list": {
|
|
11
|
-
const
|
|
21
|
+
const { listInstalledPlugins } = await import("@rynx-ai/daemon/plugin-cli");
|
|
22
|
+
const plugins = listInstalledPlugins();
|
|
12
23
|
console.log("Plugins");
|
|
13
24
|
if (plugins.length === 0) {
|
|
14
25
|
console.log(" (none)");
|
|
@@ -126,23 +137,50 @@ export async function runPluginCommand(args) {
|
|
|
126
137
|
const [pluginId, command] = args;
|
|
127
138
|
if (!pluginId)
|
|
128
139
|
fail("plugin: missing plugin id");
|
|
140
|
+
if (pluginId === "--help" || pluginId === "-h" || pluginId === "help") {
|
|
141
|
+
console.log(PLUGIN_USAGE);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
129
144
|
if (["list", "install", "update", "uninstall", "enable", "disable"].includes(pluginId)) {
|
|
130
145
|
return runPluginManageCommand(args);
|
|
131
146
|
}
|
|
132
147
|
if (!command)
|
|
133
148
|
fail(`plugin ${pluginId}: missing command`);
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
const pluginCli = await import("@rynx-ai/daemon/plugin-cli");
|
|
150
|
+
if (command === "--help" || command === "-h") {
|
|
151
|
+
console.log(pluginCli.installedPluginCommandUsage(pluginId));
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
return runPluginCliWithForwardedSignals((signal) => pluginCli.runPluginCliCommand(pluginId, args.slice(1), {
|
|
155
|
+
stdio: "inherit",
|
|
156
|
+
signal,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
async function runPluginCliWithForwardedSignals(run) {
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
let interruptedBy;
|
|
162
|
+
const interrupt = (signal) => {
|
|
163
|
+
interruptedBy ??= signal;
|
|
164
|
+
if (!controller.signal.aborted) {
|
|
165
|
+
controller.abort(new Error(`plugin command interrupted by ${signal}`));
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const onSigint = () => interrupt("SIGINT");
|
|
169
|
+
const onSigterm = () => interrupt("SIGTERM");
|
|
170
|
+
process.on("SIGINT", onSigint);
|
|
171
|
+
process.on("SIGTERM", onSigterm);
|
|
172
|
+
try {
|
|
173
|
+
return (await run(controller.signal)).code;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (interruptedBy)
|
|
177
|
+
return interruptedBy === "SIGINT" ? 130 : 143;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
process.off("SIGINT", onSigint);
|
|
182
|
+
process.off("SIGTERM", onSigterm);
|
|
144
183
|
}
|
|
145
|
-
return result.code;
|
|
146
184
|
}
|
|
147
185
|
function parseInstallationArgs(operation, args) {
|
|
148
186
|
const target = args[0];
|
|
@@ -219,15 +257,3 @@ async function confirmInTerminal(message) {
|
|
|
219
257
|
prompt.close();
|
|
220
258
|
}
|
|
221
259
|
}
|
|
222
|
-
async function readStdinBounded(maxBytes) {
|
|
223
|
-
const chunks = [];
|
|
224
|
-
let bytes = 0;
|
|
225
|
-
for await (const chunk of process.stdin) {
|
|
226
|
-
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
227
|
-
bytes += value.byteLength;
|
|
228
|
-
if (bytes > maxBytes)
|
|
229
|
-
fail(`plugin stdin exceeds ${maxBytes} bytes`);
|
|
230
|
-
chunks.push(value);
|
|
231
|
-
}
|
|
232
|
-
return Buffer.concat(chunks, bytes).toString("utf8");
|
|
233
|
-
}
|
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.js
CHANGED
|
@@ -437,6 +437,8 @@ const MANAGED_BROWSER_RPC_METHODS = new Set([
|
|
|
437
437
|
"browser.page.back",
|
|
438
438
|
"browser.page.forward",
|
|
439
439
|
"browser.page.reload",
|
|
440
|
+
"browser.request-headers.get",
|
|
441
|
+
"browser.request-headers.set",
|
|
440
442
|
]);
|
|
441
443
|
/**
|
|
442
444
|
* Invoke Browser control for the caller's own managed Session. Unlike the
|
|
@@ -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,78 @@
|
|
|
1
|
+
import * as prompts from "@clack/prompts";
|
|
2
|
+
const DOWNLOAD_PERCENT_PATTERN = /下载进度:(\d{1,3})%$/u;
|
|
3
|
+
export function createProgressDisplay() {
|
|
4
|
+
if (!prompts.isTTY(process.stdout) || prompts.isCI()) {
|
|
5
|
+
return {
|
|
6
|
+
start: (message) => console.log(message),
|
|
7
|
+
update: (message) => console.log(message),
|
|
8
|
+
succeed: (message) => console.log(message),
|
|
9
|
+
clear: () => undefined,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const spinner = prompts.spinner({ output: process.stdout });
|
|
13
|
+
const progress = prompts.progress({
|
|
14
|
+
output: process.stdout,
|
|
15
|
+
max: 100,
|
|
16
|
+
size: 28,
|
|
17
|
+
style: "block",
|
|
18
|
+
});
|
|
19
|
+
let spinnerActive = false;
|
|
20
|
+
let progressActive = false;
|
|
21
|
+
let completedPercent = 0;
|
|
22
|
+
const clear = () => {
|
|
23
|
+
if (progressActive)
|
|
24
|
+
progress.clear();
|
|
25
|
+
if (spinnerActive)
|
|
26
|
+
spinner.clear();
|
|
27
|
+
progressActive = false;
|
|
28
|
+
spinnerActive = false;
|
|
29
|
+
completedPercent = 0;
|
|
30
|
+
};
|
|
31
|
+
const finishDownload = () => {
|
|
32
|
+
if (!progressActive)
|
|
33
|
+
return;
|
|
34
|
+
progress.stop("Chrome for Testing 下载完成");
|
|
35
|
+
progressActive = false;
|
|
36
|
+
completedPercent = 0;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
start(message) {
|
|
40
|
+
clear();
|
|
41
|
+
spinner.start(message);
|
|
42
|
+
spinnerActive = true;
|
|
43
|
+
},
|
|
44
|
+
update(message) {
|
|
45
|
+
const match = DOWNLOAD_PERCENT_PATTERN.exec(message);
|
|
46
|
+
if (match) {
|
|
47
|
+
const percent = Math.min(100, Number(match[1]));
|
|
48
|
+
if (spinnerActive) {
|
|
49
|
+
spinner.clear();
|
|
50
|
+
spinnerActive = false;
|
|
51
|
+
}
|
|
52
|
+
if (!progressActive) {
|
|
53
|
+
progress.start(message);
|
|
54
|
+
progressActive = true;
|
|
55
|
+
}
|
|
56
|
+
progress.advance(percent - completedPercent, message);
|
|
57
|
+
completedPercent = percent;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
finishDownload();
|
|
61
|
+
if (spinnerActive)
|
|
62
|
+
spinner.message(message);
|
|
63
|
+
else {
|
|
64
|
+
spinner.start(message);
|
|
65
|
+
spinnerActive = true;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
succeed(message) {
|
|
69
|
+
finishDownload();
|
|
70
|
+
if (spinnerActive)
|
|
71
|
+
spinner.stop(message);
|
|
72
|
+
else
|
|
73
|
+
prompts.log.success(message);
|
|
74
|
+
spinnerActive = false;
|
|
75
|
+
},
|
|
76
|
+
clear,
|
|
77
|
+
};
|
|
78
|
+
}
|
package/dist/run-cli.js
CHANGED
|
@@ -62,6 +62,10 @@ export async function runCli(argv) {
|
|
|
62
62
|
return runBrowserCommand(argv.slice(1));
|
|
63
63
|
case "cleanup":
|
|
64
64
|
return runCleanupCommand(argv.slice(1));
|
|
65
|
+
case "autostart": {
|
|
66
|
+
const { runAutostartCommand } = await import("./commands/autostart.js");
|
|
67
|
+
return runAutostartCommand(argv.slice(1));
|
|
68
|
+
}
|
|
65
69
|
case "setup": {
|
|
66
70
|
const { runSetupCommand } = await import("./commands/setup.js");
|
|
67
71
|
return runSetupCommand(argv.slice(1));
|
package/dist/standalone.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { resolveBundledTmux } from "@rynx-ai/tmux";
|
|
3
4
|
import { installedVersion } from "./version.js";
|
|
4
5
|
import { ensureDaemonControlEndpoint, resolveDaemonControlEndpoint, } from "./control-endpoint.js";
|
|
5
6
|
/** Resolve the canonical Builtin Skill directory shipped by this exact CLI. */
|
|
@@ -8,6 +9,7 @@ export function standaloneBuiltinSkillsDirectory(moduleUrl = import.meta.url) {
|
|
|
8
9
|
}
|
|
9
10
|
export async function startStandaloneDaemon(options = {}) {
|
|
10
11
|
const cliVersion = installedVersion();
|
|
12
|
+
const tmuxBin = resolveBundledTmux();
|
|
11
13
|
const { startDaemon } = await import("@rynx-ai/daemon/lifecycle");
|
|
12
14
|
return startDaemon({
|
|
13
15
|
force: options.restart ?? false,
|
|
@@ -16,6 +18,7 @@ export async function startStandaloneDaemon(options = {}) {
|
|
|
16
18
|
builtinSkillsDir: standaloneBuiltinSkillsDirectory(),
|
|
17
19
|
cliVersion,
|
|
18
20
|
productVersion: cliVersion,
|
|
21
|
+
...(tmuxBin ? { tmuxBin } : {}),
|
|
19
22
|
},
|
|
20
23
|
});
|
|
21
24
|
}
|