@rynx-ai/cli 0.1.11-beta.2 → 0.1.11-beta.20
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/app-runtime.d.ts +10 -0
- package/dist/app-runtime.js +34 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/commands/app-distribution.d.ts +2 -2
- package/dist/commands/app-distribution.js +2 -6
- package/dist/commands/browser.js +21 -9
- package/dist/commands/lifecycle.d.ts +1 -0
- package/dist/commands/lifecycle.js +21 -0
- package/dist/commands/plugin.js +52 -26
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +363 -0
- package/dist/commands/skills.js +2 -2
- package/dist/commands/update.d.ts +20 -0
- package/dist/commands/update.js +234 -0
- package/dist/control-client.d.ts +1 -10
- package/dist/control-client.js +1 -46
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/progress-display.d.ts +7 -0
- package/dist/progress-display.js +78 -0
- package/dist/run-cli.js +34 -8
- package/dist/standalone.d.ts +13 -0
- package/dist/standalone.js +57 -0
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +6 -2
- package/package.json +19 -5
- package/dist/legacy-adapter.d.ts +0 -7
- package/dist/legacy-adapter.js +0 -63
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CliDependencyVersions {
|
|
2
|
+
cliVersion: string;
|
|
3
|
+
daemonVersion: string;
|
|
4
|
+
}
|
|
5
|
+
/** Read the versions in the dependency closure owned by this CLI installation. */
|
|
6
|
+
export declare function resolveCliDependencyVersions(): CliDependencyVersions;
|
|
7
|
+
/** Resolve the CLI-owned process entry used exclusively by Rynx App. */
|
|
8
|
+
export declare function resolveCliAppRuntimeEntry(moduleUrl?: string): string;
|
|
9
|
+
/** Run the App-owned daemon in this dedicated CLI App Runtime process. */
|
|
10
|
+
export declare function runAppRuntimeProcess(): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { installedDaemonVersion } from "@rynx-ai/daemon/entry-path";
|
|
4
|
+
import { installedVersion } from "./version.js";
|
|
5
|
+
/** Read the versions in the dependency closure owned by this CLI installation. */
|
|
6
|
+
export function resolveCliDependencyVersions() {
|
|
7
|
+
return {
|
|
8
|
+
cliVersion: installedVersion(),
|
|
9
|
+
daemonVersion: installedDaemonVersion(),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Resolve the CLI-owned process entry used exclusively by Rynx App. */
|
|
13
|
+
export function resolveCliAppRuntimeEntry(moduleUrl = import.meta.url) {
|
|
14
|
+
return fileURLToPath(moduleUrl);
|
|
15
|
+
}
|
|
16
|
+
/** Run the App-owned daemon in this dedicated CLI App Runtime process. */
|
|
17
|
+
export async function runAppRuntimeProcess() {
|
|
18
|
+
if (process.env.RYNX_DISTRIBUTION !== "app") {
|
|
19
|
+
throw new Error("CLI App Runtime requires RYNX_DISTRIBUTION=app");
|
|
20
|
+
}
|
|
21
|
+
if (process.env.RYNX_DAEMON_LIFECYCLE !== "follow-app" &&
|
|
22
|
+
process.env.RYNX_DAEMON_LIFECYCLE !== "resident") {
|
|
23
|
+
throw new Error("CLI App Runtime requires an App daemon lifecycle");
|
|
24
|
+
}
|
|
25
|
+
const { runDaemonProcess } = await import("@rynx-ai/daemon/internal/process");
|
|
26
|
+
await runDaemonProcess();
|
|
27
|
+
}
|
|
28
|
+
if (process.argv[1] &&
|
|
29
|
+
path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
30
|
+
runAppRuntimeProcess().catch((error) => {
|
|
31
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
});
|
|
34
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { callManagedRuntimeBrowser, callResidentRuntime, cancelResidentPluginInstallation, commitResidentPluginInstallation,
|
|
1
|
+
export { callManagedRuntimeBrowser, callResidentRuntime, cancelResidentPluginInstallation, commitResidentPluginInstallation, cleanupResidentSessions, createResidentRemoteRuntimePairingOffer, forgetResidentRuntimeTarget, getResidentChromeInspectionStatus, getResidentDaemonIdentity, getResidentDaemonRuntimeStatus, getResidentRuntimeLocalBrowserEndpoint, invokeResidentPluginCommand, listResidentPlugins, listResidentRemoteRuntimeClients, listResidentRuntimeTargets, pairResidentRuntimeTarget, prepareResidentPluginInstallation, readManagedRuntimeBrowserCredential, readOptionalManagedRuntimeBrowserCredential, revokeResidentRemoteRuntimeClient, setResidentPluginEnabled, shutdownResidentDaemonIfIdle, configureResidentChromeInspection, testResidentRuntimeTarget, uninstallResidentPlugin, connectResidentDesktopBrowserHost, ResidentRuntimeCallError, type InvokeResidentPluginCommandOptions, type ResidentDaemonIdentity, type ResidentDesktopBrowserHostCommandRequest, type ResidentDesktopBrowserHostConnectOptions, type ResidentDesktopBrowserHostConnection, type ResidentDesktopBrowserHostFailure, type ResidentPluginCommandResult, type ResidentPluginManagementItem, type ResidentPluginManagementState, type ResidentRemoteRuntimeClientGrant, type ResidentRuntimeCallErrorCode, type ResidentRuntimeCallOutcome, type ResidentRuntimeTargetTestResult, type ResidentRuntimeTargetView, } from "./control-client.js";
|
|
2
2
|
export type { DaemonChromeInspectionConfigureInput, DaemonChromeInspectionStatus, } from "@rynx-ai/protocol/control";
|
|
3
3
|
export type { PluginInstallCommitInput, PluginInstallCommitResult, PluginInstallPreparation, PluginInstallPrepareInput, } from "@rynx-ai/protocol/plugin-management";
|
|
4
4
|
export { ensureDaemonControlEndpoint, resolveDaemonControlEndpoint, type DaemonControlEndpoint, type ResolveDaemonControlEndpointOptions, } from "./control-endpoint.js";
|
package/dist/client.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { callManagedRuntimeBrowser, callResidentRuntime, cancelResidentPluginInstallation, commitResidentPluginInstallation,
|
|
1
|
+
export { callManagedRuntimeBrowser, callResidentRuntime, cancelResidentPluginInstallation, commitResidentPluginInstallation, cleanupResidentSessions, createResidentRemoteRuntimePairingOffer, forgetResidentRuntimeTarget, getResidentChromeInspectionStatus, getResidentDaemonIdentity, getResidentDaemonRuntimeStatus, getResidentRuntimeLocalBrowserEndpoint, invokeResidentPluginCommand, listResidentPlugins, listResidentRemoteRuntimeClients, listResidentRuntimeTargets, pairResidentRuntimeTarget, prepareResidentPluginInstallation, readManagedRuntimeBrowserCredential, readOptionalManagedRuntimeBrowserCredential, revokeResidentRemoteRuntimeClient, setResidentPluginEnabled, shutdownResidentDaemonIfIdle, configureResidentChromeInspection, testResidentRuntimeTarget, uninstallResidentPlugin, connectResidentDesktopBrowserHost, ResidentRuntimeCallError, } from "./control-client.js";
|
|
2
2
|
export { ensureDaemonControlEndpoint, resolveDaemonControlEndpoint, } from "./control-endpoint.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare function shouldUseAppDistributionCommand(command: string | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
2
2
|
/**
|
|
3
|
-
* Prevent the App launcher from falling through to
|
|
4
|
-
*
|
|
3
|
+
* Prevent the App launcher from falling through to standalone lifecycle
|
|
4
|
+
* commands and accidentally creating or inspecting a PM2-owned second owner.
|
|
5
5
|
*/
|
|
6
6
|
export declare function runAppDistributionCommand(command: string, args: readonly string[]): Promise<number>;
|
|
@@ -4,7 +4,6 @@ import { rynxHome } from "@rynx-ai/core";
|
|
|
4
4
|
import { resolveDaemonControlEndpoint } from "../control-endpoint.js";
|
|
5
5
|
import { fail } from "./errors.js";
|
|
6
6
|
const APP_ONLY_COMMANDS = new Set([
|
|
7
|
-
"setup",
|
|
8
7
|
"update",
|
|
9
8
|
"start",
|
|
10
9
|
"restart",
|
|
@@ -19,8 +18,8 @@ export function shouldUseAppDistributionCommand(command, env = process.env) {
|
|
|
19
18
|
&& env.RYNX_DISTRIBUTION === "app";
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
|
-
* Prevent the App launcher from falling through to
|
|
23
|
-
*
|
|
21
|
+
* Prevent the App launcher from falling through to standalone lifecycle
|
|
22
|
+
* commands and accidentally creating or inspecting a PM2-owned second owner.
|
|
24
23
|
*/
|
|
25
24
|
export async function runAppDistributionCommand(command, args) {
|
|
26
25
|
const json = args.includes("--json");
|
|
@@ -44,9 +43,6 @@ export async function runAppDistributionCommand(command, args) {
|
|
|
44
43
|
printRecentDaemonLogs();
|
|
45
44
|
return 0;
|
|
46
45
|
}
|
|
47
|
-
if (command === "setup") {
|
|
48
|
-
fail("Rynx App setup is completed in its onboarding and Desktop Settings.");
|
|
49
|
-
}
|
|
50
46
|
if (command === "update") {
|
|
51
47
|
fail("Rynx App updates are managed from Rynx > Check for Updates.");
|
|
52
48
|
}
|
package/dist/commands/browser.js
CHANGED
|
@@ -3,7 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { connectBrowserAutomation, } from "@rynx-ai/browser-cdp";
|
|
4
4
|
import WebSocket from "ws";
|
|
5
5
|
import { parseBrowserCliArgs, resolveBrowserCliTarget, } from "../browser-cli-args.js";
|
|
6
|
-
import { callManagedRuntimeBrowser, callResidentRuntime,
|
|
6
|
+
import { callManagedRuntimeBrowser, callResidentRuntime, getResidentRuntimeLocalBrowserAutomationAccess, getResidentRuntimeLocalBrowserEndpoint, readOptionalManagedRuntimeBrowserCredential, } from "../control-client.js";
|
|
7
|
+
import { createProgressDisplay } from "../progress-display.js";
|
|
7
8
|
import { fail } from "./errors.js";
|
|
8
9
|
export async function runBrowserCommand(args) {
|
|
9
10
|
let parsed;
|
|
@@ -18,9 +19,20 @@ export async function runBrowserCommand(args) {
|
|
|
18
19
|
fail("browser install: choose either --version or --channel");
|
|
19
20
|
}
|
|
20
21
|
const channel = browserReleaseChannel(parsed.channel);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const { createBrowserArtifactManagementService } = await import("@rynx-ai/daemon/browser-artifacts");
|
|
23
|
+
const artifacts = createBrowserArtifactManagementService();
|
|
24
|
+
const progress = parsed.json ? undefined : createProgressDisplay();
|
|
25
|
+
progress?.start(parsed.subcommand === "install" ? "正在安装 Rynx Browser" : "正在更新 Rynx Browser");
|
|
26
|
+
let result;
|
|
27
|
+
try {
|
|
28
|
+
result = parsed.subcommand === "install"
|
|
29
|
+
? await artifacts.install(parsed.version ? { version: parsed.version } : { channel }, progress ? { onProgress: (message) => progress.update(message) } : {})
|
|
30
|
+
: await artifacts.update({ channel }, progress ? { onProgress: (message) => progress.update(message) } : {});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
progress?.clear();
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
24
36
|
if (parsed.json) {
|
|
25
37
|
console.log(JSON.stringify({
|
|
26
38
|
resolved: result.resolved,
|
|
@@ -28,14 +40,13 @@ export async function runBrowserCommand(args) {
|
|
|
28
40
|
}, null, 2));
|
|
29
41
|
}
|
|
30
42
|
else {
|
|
31
|
-
|
|
32
|
-
console.log(message);
|
|
33
|
-
console.log(`Active Browser engine: Chrome for Testing ${result.installed.version}.`);
|
|
43
|
+
progress.succeed(`Rynx Browser 已就绪:Chrome for Testing ${result.installed.version}`);
|
|
34
44
|
}
|
|
35
45
|
return 0;
|
|
36
46
|
}
|
|
37
47
|
if (parsed.subcommand === "version") {
|
|
38
|
-
const {
|
|
48
|
+
const { createBrowserArtifactManagementService } = await import("@rynx-ai/daemon/browser-artifacts");
|
|
49
|
+
const { installed } = createBrowserArtifactManagementService().version();
|
|
39
50
|
if (parsed.json)
|
|
40
51
|
console.log(JSON.stringify({ installed }, null, 2));
|
|
41
52
|
else if (installed) {
|
|
@@ -47,7 +58,8 @@ export async function runBrowserCommand(args) {
|
|
|
47
58
|
return 0;
|
|
48
59
|
}
|
|
49
60
|
if (parsed.subcommand === "clean") {
|
|
50
|
-
const
|
|
61
|
+
const { createBrowserArtifactManagementService } = await import("@rynx-ai/daemon/browser-artifacts");
|
|
62
|
+
const result = createBrowserArtifactManagementService().clean();
|
|
51
63
|
if (parsed.json)
|
|
52
64
|
console.log(JSON.stringify(result, null, 2));
|
|
53
65
|
else if (result.removedBuilds.length === 0) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runLifecycleCommand(command: string, args: readonly string[]): Promise<number>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { startStandaloneDaemon, statusStandaloneDaemon, stopStandaloneDaemon, streamStandaloneDaemonLogs, } from "../standalone.js";
|
|
2
|
+
import { fail } from "./errors.js";
|
|
3
|
+
export async function runLifecycleCommand(command, args) {
|
|
4
|
+
if (args.length > 0)
|
|
5
|
+
fail(`${command}: unexpected argument ${args[0]}`);
|
|
6
|
+
switch (command) {
|
|
7
|
+
case "start":
|
|
8
|
+
return startStandaloneDaemon();
|
|
9
|
+
case "restart":
|
|
10
|
+
return startStandaloneDaemon({ restart: true });
|
|
11
|
+
case "stop":
|
|
12
|
+
return stopStandaloneDaemon();
|
|
13
|
+
case "status":
|
|
14
|
+
return statusStandaloneDaemon();
|
|
15
|
+
case "logs":
|
|
16
|
+
await streamStandaloneDaemonLogs();
|
|
17
|
+
return 0;
|
|
18
|
+
default:
|
|
19
|
+
fail(`unknown lifecycle command: ${command}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
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
|
-
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import * as prompts from "@clack/prompts";
|
|
5
|
+
import { AGENT_RUNTIME_IDS, getRuntimeProfile, loadConfig, rynxConfigFile, } from "@rynx-ai/core";
|
|
6
|
+
import { inspectDaemonDiagnostics, inspectSystemDependencies, prepareBundledPlugins, } from "@rynx-ai/daemon/setup-service";
|
|
7
|
+
import { BUNDLED_TMUX_UNAVAILABLE_MESSAGE, resolveBundledTmux, } from "@rynx-ai/tmux";
|
|
8
|
+
import { createProgressDisplay } from "../progress-display.js";
|
|
9
|
+
import { fail } from "./errors.js";
|
|
10
|
+
export async function runSetupCommand(args) {
|
|
11
|
+
const options = parseSetupOptions(args);
|
|
12
|
+
const progress = options.json ? undefined : createProgressDisplay();
|
|
13
|
+
const interactive = !options.nonInteractive &&
|
|
14
|
+
!options.hasConfigurationArguments &&
|
|
15
|
+
Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
16
|
+
const existing = readRawConfig();
|
|
17
|
+
const created = !existsSync(rynxConfigFile());
|
|
18
|
+
const defaults = loadConfig({});
|
|
19
|
+
const next = {
|
|
20
|
+
...existing,
|
|
21
|
+
HOST: existing.HOST ?? defaults.HOST,
|
|
22
|
+
PORT: existing.PORT ?? defaults.PORT,
|
|
23
|
+
LOG_LEVEL: existing.LOG_LEVEL ?? defaults.LOG_LEVEL,
|
|
24
|
+
DEFAULT_RUNTIME: existing.DEFAULT_RUNTIME ?? defaults.DEFAULT_RUNTIME,
|
|
25
|
+
};
|
|
26
|
+
if (interactive) {
|
|
27
|
+
if (!await collectInteractiveSetup(next))
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
if (options.defaultRuntime !== undefined)
|
|
32
|
+
next.DEFAULT_RUNTIME = options.defaultRuntime;
|
|
33
|
+
if (options.host !== undefined)
|
|
34
|
+
next.HOST = options.host;
|
|
35
|
+
if (options.port !== undefined)
|
|
36
|
+
next.PORT = options.port;
|
|
37
|
+
if (options.logLevel !== undefined)
|
|
38
|
+
next.LOG_LEVEL = options.logLevel;
|
|
39
|
+
}
|
|
40
|
+
validateSetupConfig(next);
|
|
41
|
+
const updated = ["HOST", "PORT", "LOG_LEVEL", "DEFAULT_RUNTIME"].filter((key) => existing[key] !== next[key]);
|
|
42
|
+
writeConfigAtomically(next);
|
|
43
|
+
const result = {
|
|
44
|
+
configPath: rynxConfigFile(),
|
|
45
|
+
created,
|
|
46
|
+
updated,
|
|
47
|
+
browser: {
|
|
48
|
+
action: options.browser,
|
|
49
|
+
status: "skipped",
|
|
50
|
+
},
|
|
51
|
+
plugins: {
|
|
52
|
+
status: "ready",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
let failed = false;
|
|
56
|
+
progress?.start("正在准备 Rynx 内置插件");
|
|
57
|
+
try {
|
|
58
|
+
const plugins = await prepareBundledPlugins();
|
|
59
|
+
result.plugins = {
|
|
60
|
+
status: plugins.status,
|
|
61
|
+
changed: [...plugins.installed, ...plugins.updated],
|
|
62
|
+
};
|
|
63
|
+
progress?.succeed("Rynx 内置插件已就绪");
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
failed = true;
|
|
67
|
+
progress?.clear();
|
|
68
|
+
const detail = errorMessage(error);
|
|
69
|
+
result.plugins = {
|
|
70
|
+
status: "error",
|
|
71
|
+
detail,
|
|
72
|
+
};
|
|
73
|
+
console.error(`Rynx 内置插件准备失败:${detail}`);
|
|
74
|
+
}
|
|
75
|
+
progress?.start("正在检查 Rynx Browser");
|
|
76
|
+
const diagnostics = await inspectDaemonDiagnostics();
|
|
77
|
+
if (!diagnostics.browser.supported) {
|
|
78
|
+
result.browser = { action: options.browser, status: "unsupported" };
|
|
79
|
+
progress?.succeed("当前平台不支持 Rynx Browser");
|
|
80
|
+
}
|
|
81
|
+
else if (diagnostics.browser.installedVersion) {
|
|
82
|
+
result.browser = {
|
|
83
|
+
action: options.browser,
|
|
84
|
+
status: "ready",
|
|
85
|
+
version: diagnostics.browser.installedVersion,
|
|
86
|
+
};
|
|
87
|
+
progress?.succeed(`Rynx Browser 已就绪:${diagnostics.browser.installedVersion}`);
|
|
88
|
+
}
|
|
89
|
+
else if (options.browser !== "skip") {
|
|
90
|
+
progress?.update("正在安装 Rynx Browser");
|
|
91
|
+
try {
|
|
92
|
+
const { createBrowserArtifactManagementService } = await import("@rynx-ai/daemon/browser-artifacts");
|
|
93
|
+
const installed = await createBrowserArtifactManagementService().install({ channel: "stable" }, progress ? { onProgress: (message) => progress.update(message) } : {});
|
|
94
|
+
result.browser = {
|
|
95
|
+
action: options.browser,
|
|
96
|
+
status: "ready",
|
|
97
|
+
version: installed.installed.version,
|
|
98
|
+
};
|
|
99
|
+
progress?.succeed(`Rynx Browser 已就绪:${installed.installed.version}`);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
failed = true;
|
|
103
|
+
progress?.clear();
|
|
104
|
+
const detail = errorMessage(error);
|
|
105
|
+
result.browser = {
|
|
106
|
+
action: options.browser,
|
|
107
|
+
status: "error",
|
|
108
|
+
detail,
|
|
109
|
+
};
|
|
110
|
+
console.error(`Rynx Browser 安装失败:${detail}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
progress?.succeed("已跳过 Rynx Browser 安装");
|
|
115
|
+
}
|
|
116
|
+
if (options.resultFile) {
|
|
117
|
+
writeJsonAtomically(options.resultFile, result);
|
|
118
|
+
}
|
|
119
|
+
else if (options.json) {
|
|
120
|
+
console.log(JSON.stringify(result, null, 2));
|
|
121
|
+
}
|
|
122
|
+
else if (interactive) {
|
|
123
|
+
prompts.log.success(`已写入 ${result.configPath}`);
|
|
124
|
+
prompts.log.info(`HOST=${next.HOST} PORT=${next.PORT} DEFAULT_RUNTIME=${next.DEFAULT_RUNTIME}`);
|
|
125
|
+
if (result.browser.status === "ready") {
|
|
126
|
+
prompts.log.success(`Session Browser ${result.browser.version ?? "ready"}`);
|
|
127
|
+
}
|
|
128
|
+
else if (result.browser.status === "error") {
|
|
129
|
+
prompts.log.warn(`Session Browser: ${result.browser.detail}`);
|
|
130
|
+
}
|
|
131
|
+
prompts.outro(failed ? "初始化完成,但存在失败项" : "配置完成");
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.log(`Initialized ${result.configPath}`);
|
|
135
|
+
if (result.browser.status === "ready") {
|
|
136
|
+
console.log(`Browser ready${result.browser.version ? `: ${result.browser.version}` : ""}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return failed ? 1 : 0;
|
|
140
|
+
}
|
|
141
|
+
export async function runDoctorCommand(args) {
|
|
142
|
+
const json = args.includes("--json");
|
|
143
|
+
if (args.some((arg) => arg !== "--json")) {
|
|
144
|
+
fail(`doctor: unexpected argument ${args.find((arg) => arg !== "--json")}`);
|
|
145
|
+
}
|
|
146
|
+
let config;
|
|
147
|
+
let configError;
|
|
148
|
+
try {
|
|
149
|
+
config = loadConfig();
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
configError = errorMessage(error);
|
|
153
|
+
}
|
|
154
|
+
const runtimes = AGENT_RUNTIME_IDS.map((id) => {
|
|
155
|
+
const binary = getRuntimeProfile(id).defaultBinary;
|
|
156
|
+
const probe = spawnSync(binary, ["--version"], {
|
|
157
|
+
stdio: "ignore",
|
|
158
|
+
timeout: 8_000,
|
|
159
|
+
});
|
|
160
|
+
return { id, installed: !probe.error && probe.status === 0 };
|
|
161
|
+
});
|
|
162
|
+
const daemon = await inspectDaemonDiagnostics();
|
|
163
|
+
const tmuxBin = resolveBundledTmux();
|
|
164
|
+
const systemDependencies = tmuxBin
|
|
165
|
+
? await inspectSystemDependencies({ tmuxBin })
|
|
166
|
+
: {
|
|
167
|
+
tmux: {
|
|
168
|
+
installed: false,
|
|
169
|
+
detail: BUNDLED_TMUX_UNAVAILABLE_MESSAGE,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const result = {
|
|
173
|
+
ok: configError === undefined &&
|
|
174
|
+
runtimes.some((runtime) => runtime.installed) &&
|
|
175
|
+
systemDependencies.tmux.installed,
|
|
176
|
+
configPath: rynxConfigFile(),
|
|
177
|
+
config,
|
|
178
|
+
configError,
|
|
179
|
+
runtimes,
|
|
180
|
+
daemon,
|
|
181
|
+
systemDependencies,
|
|
182
|
+
};
|
|
183
|
+
if (json)
|
|
184
|
+
console.log(JSON.stringify(result, null, 2));
|
|
185
|
+
else {
|
|
186
|
+
console.log(`config: ${result.configPath}`);
|
|
187
|
+
if (configError)
|
|
188
|
+
console.error(`config: ${configError}`);
|
|
189
|
+
else
|
|
190
|
+
console.log(`default runtime: ${config.DEFAULT_RUNTIME}`);
|
|
191
|
+
for (const runtime of runtimes) {
|
|
192
|
+
console.log(`${runtime.id}: ${runtime.installed ? "installed" : "not installed"}`);
|
|
193
|
+
}
|
|
194
|
+
console.log(`tmux: ${systemDependencies.tmux.installed ? "installed" : "not installed"}`);
|
|
195
|
+
if (daemon.browser.supported) {
|
|
196
|
+
console.log(`browser: ${daemon.browser.installedVersion ?? daemon.browser.error ?? "not installed"}`);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.log("browser: unsupported");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return result.ok ? 0 : 1;
|
|
203
|
+
}
|
|
204
|
+
function parseSetupOptions(args) {
|
|
205
|
+
const options = {
|
|
206
|
+
nonInteractive: false,
|
|
207
|
+
json: false,
|
|
208
|
+
browser: "auto",
|
|
209
|
+
hasConfigurationArguments: false,
|
|
210
|
+
};
|
|
211
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
212
|
+
const arg = args[index];
|
|
213
|
+
if (arg === "--non-interactive") {
|
|
214
|
+
options.nonInteractive = true;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (arg === "--json") {
|
|
218
|
+
options.json = true;
|
|
219
|
+
options.nonInteractive = true;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (arg === "--result-file") {
|
|
223
|
+
if (options.resultFile !== undefined)
|
|
224
|
+
fail("setup: duplicate option --result-file");
|
|
225
|
+
const value = args[index + 1];
|
|
226
|
+
if (!value || value.startsWith("--"))
|
|
227
|
+
fail("setup: --result-file requires a value");
|
|
228
|
+
options.resultFile = path.resolve(value);
|
|
229
|
+
options.nonInteractive = true;
|
|
230
|
+
index += 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (arg === "--install-browser" || arg === "--skip-browser") {
|
|
234
|
+
if (options.browser !== "auto")
|
|
235
|
+
fail("setup: choose only one Browser action");
|
|
236
|
+
options.browser = arg === "--install-browser" ? "install" : "skip";
|
|
237
|
+
options.hasConfigurationArguments = true;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const value = args[index + 1];
|
|
241
|
+
if (!value || value.startsWith("--"))
|
|
242
|
+
fail(`setup: ${arg} requires a value`);
|
|
243
|
+
index += 1;
|
|
244
|
+
options.hasConfigurationArguments = true;
|
|
245
|
+
if (arg === "--default-runtime") {
|
|
246
|
+
if (!AGENT_RUNTIME_IDS.includes(value)) {
|
|
247
|
+
fail("setup: --default-runtime must be codex, traex, or claude");
|
|
248
|
+
}
|
|
249
|
+
options.defaultRuntime = value;
|
|
250
|
+
}
|
|
251
|
+
else if (arg === "--host") {
|
|
252
|
+
if (!value.trim() || value.length > 255)
|
|
253
|
+
fail("setup: --host is invalid");
|
|
254
|
+
options.host = value;
|
|
255
|
+
}
|
|
256
|
+
else if (arg === "--port") {
|
|
257
|
+
const port = Number(value);
|
|
258
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
259
|
+
fail("setup: --port must be an integer from 1 to 65535");
|
|
260
|
+
}
|
|
261
|
+
options.port = port;
|
|
262
|
+
}
|
|
263
|
+
else if (arg === "--log-level") {
|
|
264
|
+
if (!value.trim() || value.length > 32)
|
|
265
|
+
fail("setup: --log-level is invalid");
|
|
266
|
+
options.logLevel = value;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
fail(`setup: unknown option ${arg}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (options.json && options.resultFile) {
|
|
273
|
+
fail("setup: choose either --json or --result-file");
|
|
274
|
+
}
|
|
275
|
+
return options;
|
|
276
|
+
}
|
|
277
|
+
async function collectInteractiveSetup(config) {
|
|
278
|
+
prompts.intro("rynx setup");
|
|
279
|
+
const runtime = await prompts.select({
|
|
280
|
+
message: "Default runtime",
|
|
281
|
+
initialValue: config.DEFAULT_RUNTIME,
|
|
282
|
+
options: AGENT_RUNTIME_IDS.map((id) => ({ value: id, label: id })),
|
|
283
|
+
});
|
|
284
|
+
if (prompts.isCancel(runtime))
|
|
285
|
+
return cancelled();
|
|
286
|
+
config.DEFAULT_RUNTIME = runtime;
|
|
287
|
+
const port = await prompts.text({
|
|
288
|
+
message: "HTTP port",
|
|
289
|
+
initialValue: String(config.PORT),
|
|
290
|
+
validate: (value) => {
|
|
291
|
+
const parsed = Number(value);
|
|
292
|
+
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65_535
|
|
293
|
+
? undefined
|
|
294
|
+
: "Enter a port from 1 to 65535";
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
if (prompts.isCancel(port))
|
|
298
|
+
return cancelled();
|
|
299
|
+
config.PORT = Number(port);
|
|
300
|
+
const host = await prompts.text({
|
|
301
|
+
message: "Bind host",
|
|
302
|
+
initialValue: String(config.HOST),
|
|
303
|
+
});
|
|
304
|
+
if (prompts.isCancel(host))
|
|
305
|
+
return cancelled();
|
|
306
|
+
config.HOST = host;
|
|
307
|
+
const logLevel = await prompts.text({
|
|
308
|
+
message: "Log level",
|
|
309
|
+
initialValue: String(config.LOG_LEVEL),
|
|
310
|
+
});
|
|
311
|
+
if (prompts.isCancel(logLevel))
|
|
312
|
+
return cancelled();
|
|
313
|
+
config.LOG_LEVEL = logLevel;
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
function cancelled() {
|
|
317
|
+
prompts.cancel("Cancelled");
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
function validateSetupConfig(config) {
|
|
321
|
+
loadConfig({
|
|
322
|
+
HOST: String(config.HOST),
|
|
323
|
+
PORT: String(config.PORT),
|
|
324
|
+
LOG_LEVEL: String(config.LOG_LEVEL),
|
|
325
|
+
DEFAULT_RUNTIME: String(config.DEFAULT_RUNTIME),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function readRawConfig() {
|
|
329
|
+
try {
|
|
330
|
+
const parsed = JSON.parse(readFileSync(rynxConfigFile(), "utf8"));
|
|
331
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
332
|
+
? parsed
|
|
333
|
+
: {};
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return {};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function writeConfigAtomically(config) {
|
|
340
|
+
writeJsonAtomically(rynxConfigFile(), config);
|
|
341
|
+
}
|
|
342
|
+
function writeJsonAtomically(file, value) {
|
|
343
|
+
const directory = path.dirname(file);
|
|
344
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
345
|
+
const temporary = path.join(directory, `.${path.basename(file)}.${process.pid}.tmp`);
|
|
346
|
+
let descriptor;
|
|
347
|
+
try {
|
|
348
|
+
descriptor = openSync(temporary, "wx", 0o600);
|
|
349
|
+
writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
350
|
+
closeSync(descriptor);
|
|
351
|
+
descriptor = undefined;
|
|
352
|
+
renameSync(temporary, file);
|
|
353
|
+
chmodSync(file, 0o600);
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
if (descriptor !== undefined)
|
|
357
|
+
closeSync(descriptor);
|
|
358
|
+
rmSync(temporary, { force: true });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function errorMessage(error) {
|
|
362
|
+
return error instanceof Error ? error.message : String(error);
|
|
363
|
+
}
|