@rynx-ai/cli 0.1.11-beta.4 → 0.1.11-beta.40

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.
@@ -1,14 +1,25 @@
1
1
  import { createInterface } from "node:readline/promises";
2
- import { cancelResidentPluginInstallation, commitResidentPluginInstallation, invokeResidentPluginCommand, listResidentPlugins, prepareResidentPluginInstallation, setResidentPluginEnabled, uninstallResidentPlugin, } from "../control-client.js";
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 plugins = await listResidentPlugins();
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 stdin = process.stdin.isTTY
135
- ? undefined
136
- : await readStdinBounded(PLUGIN_STDIN_MAX_BYTES);
137
- const result = await invokeResidentPluginCommand(pluginId, args.slice(1), stdin === undefined ? {} : { stdin });
138
- if (result.stdout)
139
- process.stdout.write(result.stdout);
140
- if (result.stderr)
141
- process.stderr.write(result.stderr);
142
- if (result.truncated) {
143
- process.stderr.write("rynx: plugin command output was truncated by the daemon\n");
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
- }
@@ -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: errorMessage(error),
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" }, options.json
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: errorMessage(error),
110
+ detail,
96
111
  };
112
+ console.error(`Rynx Browser 安装失败:${detail}`);
97
113
  }
98
114
  }
99
- if (options.json) {
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 && runtimes.some((runtime) => runtime.installed),
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
- const file = rynxConfigFile();
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(config, null, 2)}\n`, "utf8");
390
+ writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`, "utf8");
301
391
  closeSync(descriptor);
302
392
  descriptor = undefined;
303
393
  renameSync(temporary, file);
@@ -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 (!SKILL_NAME_PATTERN.test(name))
58
+ if (!isSkillPathComponent(name))
59
59
  return null;
60
60
  if (!BUILTIN_SKILL_NAMES.includes(name))
61
61
  return null;
@@ -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 { stopStandaloneDaemon } from "../standalone.js";
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 stopStandaloneDaemon() !== 0) {
75
+ if (await runLifecycleCommand("stop", []) !== 0) {
76
76
  console.error("update: could not stop the standalone daemon");
77
77
  return 1;
78
78
  }
@@ -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: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
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 Error(`Desktop Browser Host lease closed (${code})${reason.length > 0 ? `: ${reason.toString("utf8")}` : ""}`);
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: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
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: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
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: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
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,7 @@
1
+ export interface ProgressDisplay {
2
+ start(message: string): void;
3
+ update(message: string): void;
4
+ succeed(message: string): void;
5
+ clear(): void;
6
+ }
7
+ export declare function createProgressDisplay(): ProgressDisplay;
@@ -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));
@@ -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
  }