@rynx-ai/daemon 0.1.11-beta.2 → 0.1.11-beta.3
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/bundled-plugins/plugins/lark/dist/runtime.js +2 -5
- package/bundled-plugins/plugins/lark/dist/settings.js +2 -2
- package/bundled-plugins/plugins/lark/package.json +1 -1
- package/dist/browser-artifact-management.d.ts +4 -1
- package/dist/browser-artifact-management.js +18 -10
- package/dist/browser-artifacts.d.ts +1 -0
- package/dist/browser-artifacts.js +1 -0
- package/dist/chrome-for-testing-runtime.d.ts +27 -0
- package/dist/chrome-for-testing-runtime.js +193 -0
- package/dist/chrome-for-testing-store.d.ts +2 -0
- package/dist/chrome-for-testing-store.js +9 -0
- package/dist/daemon-server.d.ts +4 -1
- package/dist/daemon-server.js +137 -32
- package/dist/db.js +2 -0
- package/dist/entry-path.d.ts +3 -0
- package/dist/entry-path.js +12 -0
- package/dist/index-daemon.js +3 -87
- package/dist/lifecycle.d.ts +12 -0
- package/dist/lifecycle.js +16 -0
- package/dist/maintenance-lease.d.ts +39 -0
- package/dist/maintenance-lease.js +250 -0
- package/dist/plugin-host-rpc.d.ts +19 -1
- package/dist/plugin-host-rpc.js +163 -23
- package/dist/plugin-installer.js +59 -0
- package/dist/plugin-supervisor.d.ts +5 -0
- package/dist/plugin-supervisor.js +218 -29
- package/dist/pm2.d.ts +11 -2
- package/dist/pm2.js +116 -19
- package/dist/process.d.ts +12 -0
- package/dist/process.js +91 -0
- package/dist/session-launch-operations.d.ts +10 -1
- package/dist/session-launch-operations.js +15 -6
- package/dist/setup-service.d.ts +20 -0
- package/dist/setup-service.js +38 -0
- package/package.json +33 -20
- package/dist/browser-cli-args.d.ts +0 -20
- package/dist/browser-cli-args.js +0 -100
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -550
- package/dist/control-client.d.ts +0 -122
- package/dist/control-client.js +0 -855
- package/dist/control-endpoint.d.ts +0 -14
- package/dist/control-endpoint.js +0 -124
- package/dist/desktop-browser-host-client.d.ts +0 -35
- package/dist/desktop-browser-host-client.js +0 -415
- package/dist/setup.d.ts +0 -20
- package/dist/setup.js +0 -386
- package/dist/update.d.ts +0 -25
- package/dist/update.js +0 -133
package/dist/pm2.js
CHANGED
|
@@ -10,14 +10,18 @@
|
|
|
10
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import { mkdirSync, realpathSync } from "node:fs";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { userInfo } from "node:os";
|
|
14
|
+
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
|
15
15
|
import { loadConfig, rynxHome } from "@rynx-ai/core";
|
|
16
16
|
import { daemonEntryRestartRequired } from "./daemon-build-status.js";
|
|
17
|
+
import { resolveDaemonEntry } from "./entry-path.js";
|
|
17
18
|
const require = createRequire(import.meta.url);
|
|
18
19
|
/** pm2 app name — single resident daemon per machine. */
|
|
19
20
|
const APP_NAME = "rynx";
|
|
20
21
|
const PM2_COMMAND_TIMEOUT_MS = 5_000;
|
|
22
|
+
const LOGIN_SHELL_PATH_TIMEOUT_MS = 5_000;
|
|
23
|
+
const LOGIN_SHELL_PATH_MAX_BYTES = 256 * 1024;
|
|
24
|
+
const REFRESH_LOGIN_SHELL_PATH_ENV = "RYNX_REFRESH_LOGIN_SHELL_PATH";
|
|
21
25
|
const DAEMON_PACKAGE_VERSION = require("../package.json").version;
|
|
22
26
|
/**
|
|
23
27
|
* Build a dialable control-console URL from the daemon's bind host/port. Bind-all
|
|
@@ -49,10 +53,6 @@ function pm2Bin() {
|
|
|
49
53
|
// pm2 ships its CLI at bin/pm2; run it with our own node.
|
|
50
54
|
return require.resolve("pm2/bin/pm2");
|
|
51
55
|
}
|
|
52
|
-
/** `index-daemon.js` sits next to this file in `dist/`. */
|
|
53
|
-
function daemonEntryPath() {
|
|
54
|
-
return join(dirname(fileURLToPath(import.meta.url)), "index-daemon.js");
|
|
55
|
-
}
|
|
56
56
|
function logsDir() {
|
|
57
57
|
const dir = join(rynxHome(), "logs");
|
|
58
58
|
mkdirSync(dir, { recursive: true });
|
|
@@ -71,11 +71,58 @@ function nodeRuntimeEnv() {
|
|
|
71
71
|
: {}),
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* A detached plugin updater intentionally receives a reduced environment and
|
|
76
|
+
* therefore has no SHELL. Starting the replacement daemon with that reduced
|
|
77
|
+
* PATH would make user-installed runtimes (for example an fnm-managed Codex)
|
|
78
|
+
* disappear after every upgrade. Recover the user's login-shell PATH only at
|
|
79
|
+
* this boundary; normal interactive starts keep their exact caller environment.
|
|
80
|
+
*/
|
|
81
|
+
function daemonStartEnv() {
|
|
82
|
+
const env = nodeRuntimeEnv();
|
|
83
|
+
const refreshLoginPath = env[REFRESH_LOGIN_SHELL_PATH_ENV] === "1";
|
|
84
|
+
delete env[REFRESH_LOGIN_SHELL_PATH_ENV];
|
|
85
|
+
if (!refreshLoginPath)
|
|
86
|
+
return env;
|
|
87
|
+
const shell = userInfo().shell?.trim();
|
|
88
|
+
if (!shell) {
|
|
89
|
+
throw new Error("cannot refresh daemon PATH: the user login shell is unavailable");
|
|
90
|
+
}
|
|
91
|
+
const result = spawnSync(shell, ["-lic", `printf '\\036%s\\037' "$PATH"`], {
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
// Do not feed an fnm/nvm "multishell" directory inherited from the old
|
|
94
|
+
// daemon back into the next login shell. Version managers prepend a new
|
|
95
|
+
// directory on every shell startup, so reusing the old PATH would grow
|
|
96
|
+
// it indefinitely across upgrades.
|
|
97
|
+
env: { ...env, SHELL: shell, PATH: loginShellBaselinePath() },
|
|
98
|
+
timeout: LOGIN_SHELL_PATH_TIMEOUT_MS,
|
|
99
|
+
killSignal: "SIGTERM",
|
|
100
|
+
maxBuffer: LOGIN_SHELL_PATH_MAX_BYTES,
|
|
101
|
+
});
|
|
102
|
+
if (result.error) {
|
|
103
|
+
throw new Error(`cannot refresh daemon PATH: ${result.error.message}`);
|
|
104
|
+
}
|
|
105
|
+
if (result.status !== 0) {
|
|
106
|
+
throw new Error(`cannot refresh daemon PATH: login shell exited ${result.status ?? "without status"}`);
|
|
107
|
+
}
|
|
108
|
+
const path = extractFramedPath(result.stdout);
|
|
109
|
+
if (!path) {
|
|
110
|
+
throw new Error("cannot refresh daemon PATH: login shell returned no framed PATH");
|
|
111
|
+
}
|
|
112
|
+
const npmPrefix = env.NPM_CONFIG_PREFIX?.trim();
|
|
113
|
+
const npmBin = npmPrefix && isAbsolute(npmPrefix)
|
|
114
|
+
? join(npmPrefix, "bin")
|
|
115
|
+
: "";
|
|
116
|
+
const refreshedPath = npmBin === ""
|
|
117
|
+
? path
|
|
118
|
+
: [npmBin, ...path.split(delimiter).filter((entry) => entry !== npmBin)].join(delimiter);
|
|
119
|
+
return { ...env, PATH: refreshedPath, SHELL: shell };
|
|
120
|
+
}
|
|
74
121
|
/** Run a pm2 command inheriting stdio; returns its exit code. */
|
|
75
|
-
function runPm2(args, quiet = false) {
|
|
122
|
+
function runPm2(args, quiet = false, env = nodeRuntimeEnv()) {
|
|
76
123
|
const res = spawnSync(process.execPath, [pm2Bin(), ...args], {
|
|
77
124
|
stdio: quiet ? "ignore" : "inherit",
|
|
78
|
-
env
|
|
125
|
+
env,
|
|
79
126
|
timeout: PM2_COMMAND_TIMEOUT_MS,
|
|
80
127
|
killSignal: "SIGTERM",
|
|
81
128
|
});
|
|
@@ -117,7 +164,7 @@ function pm2ErrorMessage(error) {
|
|
|
117
164
|
* entry from scratch (delete + start) so config flags are always fresh; the
|
|
118
165
|
* `force === false` path short-circuits when it's already online.
|
|
119
166
|
*/
|
|
120
|
-
export function startDaemon({ force, quiet = false }) {
|
|
167
|
+
export function startDaemon({ force, quiet = false, launchEnvironment, }) {
|
|
121
168
|
let existing;
|
|
122
169
|
try {
|
|
123
170
|
existing = describe();
|
|
@@ -139,6 +186,20 @@ export function startDaemon({ force, quiet = false }) {
|
|
|
139
186
|
if (staleBuild && !quiet) {
|
|
140
187
|
console.log("A newer daemon build is present on disk; restarting Rynx.");
|
|
141
188
|
}
|
|
189
|
+
let startEnv;
|
|
190
|
+
try {
|
|
191
|
+
// Resolve the replacement environment before deleting an existing app.
|
|
192
|
+
// A broken/hung login shell must leave the known-good daemon untouched.
|
|
193
|
+
startEnv = {
|
|
194
|
+
...daemonStartEnv(),
|
|
195
|
+
...launchEnvironment,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (!quiet)
|
|
200
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
142
203
|
const dir = logsDir();
|
|
143
204
|
if (existing) {
|
|
144
205
|
const removed = runPm2(["delete", APP_NAME], quiet);
|
|
@@ -147,7 +208,7 @@ export function startDaemon({ force, quiet = false }) {
|
|
|
147
208
|
}
|
|
148
209
|
const status = runPm2([
|
|
149
210
|
"start",
|
|
150
|
-
|
|
211
|
+
resolveDaemonEntry(),
|
|
151
212
|
"--name",
|
|
152
213
|
APP_NAME,
|
|
153
214
|
// App and CLI share one daemon-owned state/workspace root.
|
|
@@ -163,7 +224,7 @@ export function startDaemon({ force, quiet = false }) {
|
|
|
163
224
|
join(dir, "err.log"),
|
|
164
225
|
"--merge-logs",
|
|
165
226
|
"--time",
|
|
166
|
-
], quiet);
|
|
227
|
+
], quiet, startEnv);
|
|
167
228
|
if (status === 0 && !quiet) {
|
|
168
229
|
console.log(`rynx ${force ? "restarted" : "started"} (pm2 app "${APP_NAME}", logs: ${dir}).`);
|
|
169
230
|
console.log(`Control console: ${styleUrl(controlConsoleUrl())}`);
|
|
@@ -173,16 +234,35 @@ export function startDaemon({ force, quiet = false }) {
|
|
|
173
234
|
export function isPm2DaemonOnline() {
|
|
174
235
|
return describe()?.pm2_env?.status === "online";
|
|
175
236
|
}
|
|
176
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* Stop the daemon and terminate its dedicated pm2 supervisor.
|
|
239
|
+
*
|
|
240
|
+
* PM2_HOME lives below RYNX_HOME, so this supervisor cannot own unrelated
|
|
241
|
+
* applications. Leaving it alive across an npm replacement is unsafe: the
|
|
242
|
+
* daemon process container path is resolved inside the old pm2 package. Once
|
|
243
|
+
* npm swaps @rynx-ai/daemon, that cached path may no longer exist and every
|
|
244
|
+
* subsequent `rynx start` enters a restart loop before our code can run.
|
|
245
|
+
*/
|
|
177
246
|
export function stopDaemon() {
|
|
178
|
-
|
|
247
|
+
const existing = describe();
|
|
248
|
+
if (!existing) {
|
|
249
|
+
// Even an empty process list can belong to a resident PM2 daemon whose
|
|
250
|
+
// module path is about to be replaced. `pm2 kill` is idempotent and is
|
|
251
|
+
// the only reliable package-upgrade boundary.
|
|
252
|
+
const killed = runPm2(["kill"], true);
|
|
253
|
+
if (killed !== 0)
|
|
254
|
+
return killed;
|
|
179
255
|
console.log("rynx is not running.");
|
|
180
256
|
return 0;
|
|
181
257
|
}
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
258
|
+
const removed = runPm2(["delete", APP_NAME]);
|
|
259
|
+
if (removed !== 0)
|
|
260
|
+
return removed;
|
|
261
|
+
const killed = runPm2(["kill"]);
|
|
262
|
+
if (killed !== 0)
|
|
263
|
+
return killed;
|
|
264
|
+
console.log("rynx stopped.");
|
|
265
|
+
return 0;
|
|
186
266
|
}
|
|
187
267
|
/** Print a one-line status; exit 0 only when online. */
|
|
188
268
|
export function statusDaemon() {
|
|
@@ -209,7 +289,7 @@ export function streamLogs() {
|
|
|
209
289
|
}
|
|
210
290
|
function residentBuildRestartRequired(app) {
|
|
211
291
|
const runningEntry = app.pm2_env?.pm_exec_path;
|
|
212
|
-
if (runningEntry && !sameEntryPath(runningEntry,
|
|
292
|
+
if (runningEntry && !sameEntryPath(runningEntry, resolveDaemonEntry()))
|
|
213
293
|
return true;
|
|
214
294
|
const runningVersion = app.pm2_env?.version;
|
|
215
295
|
if (runningVersion &&
|
|
@@ -220,7 +300,7 @@ function residentBuildRestartRequired(app) {
|
|
|
220
300
|
const startedAt = app.pm2_env?.pm_uptime;
|
|
221
301
|
return typeof startedAt === "number" &&
|
|
222
302
|
Number.isFinite(startedAt) &&
|
|
223
|
-
daemonEntryRestartRequired(
|
|
303
|
+
daemonEntryRestartRequired(resolveDaemonEntry(), startedAt);
|
|
224
304
|
}
|
|
225
305
|
function sameEntryPath(left, right) {
|
|
226
306
|
try {
|
|
@@ -230,3 +310,20 @@ function sameEntryPath(left, right) {
|
|
|
230
310
|
return resolve(left) === resolve(right);
|
|
231
311
|
}
|
|
232
312
|
}
|
|
313
|
+
function extractFramedPath(stdout) {
|
|
314
|
+
if (!stdout)
|
|
315
|
+
return undefined;
|
|
316
|
+
const start = stdout.lastIndexOf("\x1e");
|
|
317
|
+
if (start < 0)
|
|
318
|
+
return undefined;
|
|
319
|
+
const end = stdout.indexOf("\x1f", start + 1);
|
|
320
|
+
if (end < 0)
|
|
321
|
+
return undefined;
|
|
322
|
+
const value = stdout.slice(start + 1, end);
|
|
323
|
+
return value.trim() ? value : undefined;
|
|
324
|
+
}
|
|
325
|
+
function loginShellBaselinePath() {
|
|
326
|
+
return process.platform === "darwin"
|
|
327
|
+
? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
328
|
+
: "/usr/local/bin:/usr/bin:/bin";
|
|
329
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface SupervisorProcess {
|
|
2
|
+
connected?: boolean;
|
|
3
|
+
disconnect?: () => void;
|
|
4
|
+
}
|
|
5
|
+
/** Release a parent/supervisor IPC handle after graceful daemon shutdown. */
|
|
6
|
+
export declare function disconnectSupervisorIpc(target?: SupervisorProcess): void;
|
|
7
|
+
/**
|
|
8
|
+
* Run the daemon in the current process. Process ownership and executable
|
|
9
|
+
* selection belong to the caller (CLI standalone lifecycle or App lifecycle).
|
|
10
|
+
*/
|
|
11
|
+
export declare function runDaemonProcess(): Promise<void>;
|
|
12
|
+
export {};
|
package/dist/process.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { startRynxDaemonServer } from "./daemon-server.js";
|
|
2
|
+
import { ensureBundledOfficialPlugins } from "./bundled-plugins.js";
|
|
3
|
+
import { installStdioEpipeGuard } from "./stdio-epipe-guard.js";
|
|
4
|
+
/** Release a parent/supervisor IPC handle after graceful daemon shutdown. */
|
|
5
|
+
export function disconnectSupervisorIpc(target = process) {
|
|
6
|
+
if (target.connected && typeof target.disconnect === "function") {
|
|
7
|
+
target.disconnect();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run the daemon in the current process. Process ownership and executable
|
|
12
|
+
* selection belong to the caller (CLI standalone lifecycle or App lifecycle).
|
|
13
|
+
*/
|
|
14
|
+
export async function runDaemonProcess() {
|
|
15
|
+
installStdioEpipeGuard();
|
|
16
|
+
const followsApp = process.env.RYNX_DAEMON_LIFECYCLE === "follow-app";
|
|
17
|
+
let parentDisconnected = followsApp && !process.connected;
|
|
18
|
+
const recordParentDisconnect = () => {
|
|
19
|
+
parentDisconnected = true;
|
|
20
|
+
};
|
|
21
|
+
if (followsApp)
|
|
22
|
+
process.once("disconnect", recordParentDisconnect);
|
|
23
|
+
const preparePlugins = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const bundled = await ensureBundledOfficialPlugins({ reclaimHostLeases: true });
|
|
26
|
+
if (bundled.status === "ready") {
|
|
27
|
+
console.log(JSON.stringify({
|
|
28
|
+
level: "info",
|
|
29
|
+
type: "bundled_plugins_ready",
|
|
30
|
+
installed: bundled.installed,
|
|
31
|
+
updated: bundled.updated,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error(JSON.stringify({
|
|
37
|
+
level: "error",
|
|
38
|
+
type: "bundled_plugins_failed",
|
|
39
|
+
error: error instanceof Error ? error.message : String(error),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
let server;
|
|
44
|
+
try {
|
|
45
|
+
server = await startRynxDaemonServer({
|
|
46
|
+
preparePlugins,
|
|
47
|
+
onShutdownComplete: () => disconnectSupervisorIpc(),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
process.off("disconnect", recordParentDisconnect);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
let shutdownPromise;
|
|
55
|
+
const shutdown = (signal) => {
|
|
56
|
+
if (shutdownPromise)
|
|
57
|
+
return shutdownPromise;
|
|
58
|
+
process.off("SIGINT", onSigint);
|
|
59
|
+
process.off("SIGTERM", onSigterm);
|
|
60
|
+
process.off("disconnect", onDisconnect);
|
|
61
|
+
shutdownPromise = (async () => {
|
|
62
|
+
console.log(JSON.stringify({ level: "info", type: "shutdown", signal }));
|
|
63
|
+
await server.shutdown();
|
|
64
|
+
})();
|
|
65
|
+
shutdownPromise.catch((error) => {
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
console.error(`Shutdown failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
68
|
+
});
|
|
69
|
+
return shutdownPromise;
|
|
70
|
+
};
|
|
71
|
+
const onSigint = () => {
|
|
72
|
+
void shutdown("SIGINT");
|
|
73
|
+
};
|
|
74
|
+
const onSigterm = () => {
|
|
75
|
+
void shutdown("SIGTERM");
|
|
76
|
+
};
|
|
77
|
+
const onDisconnect = () => {
|
|
78
|
+
if (process.env.RYNX_DAEMON_LIFECYCLE === "follow-app") {
|
|
79
|
+
void shutdown("APP_DISCONNECT");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
process.once("SIGINT", onSigint);
|
|
83
|
+
process.once("SIGTERM", onSigterm);
|
|
84
|
+
if (followsApp) {
|
|
85
|
+
process.once("disconnect", onDisconnect);
|
|
86
|
+
process.off("disconnect", recordParentDisconnect);
|
|
87
|
+
if (parentDisconnected || !process.connected) {
|
|
88
|
+
void shutdown("APP_DISCONNECT");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -9,13 +9,22 @@ export interface EnsurePluginSessionLaunchInput {
|
|
|
9
9
|
operationId: string;
|
|
10
10
|
/** SHA-256 of the host-normalized, execution-affecting launch request. */
|
|
11
11
|
requestHash: string;
|
|
12
|
+
/** SHA-256 before mutable filesystem selectors are canonicalized. */
|
|
13
|
+
rawRequestHash?: string;
|
|
12
14
|
session: PluginSessionLaunchMeta;
|
|
13
15
|
}
|
|
14
16
|
export interface EnsurePluginSessionLaunchResult {
|
|
15
17
|
sessionId: string;
|
|
16
18
|
disposition: "created" | "replayed";
|
|
17
19
|
}
|
|
18
|
-
export type ReplayPluginSessionLaunchInput = Omit<EnsurePluginSessionLaunchInput, "session"
|
|
20
|
+
export type ReplayPluginSessionLaunchInput = Omit<EnsurePluginSessionLaunchInput, "session"> & {
|
|
21
|
+
/**
|
|
22
|
+
* A caller doing a pre-canonicalization lookup may treat a hash mismatch as
|
|
23
|
+
* a cache miss, then retry with the canonical hash. The canonical lookup
|
|
24
|
+
* must keep the default strict conflict behavior.
|
|
25
|
+
*/
|
|
26
|
+
allowRequestHashMismatch?: boolean;
|
|
27
|
+
};
|
|
19
28
|
export declare class SessionLaunchOperationConflictError extends Error {
|
|
20
29
|
readonly pluginId: string;
|
|
21
30
|
readonly scopeId: string;
|
|
@@ -48,7 +48,10 @@ export function replayPluginSessionLaunch(input) {
|
|
|
48
48
|
const existing = readOperation(pluginId, scopeId, operationId);
|
|
49
49
|
if (!existing)
|
|
50
50
|
return undefined;
|
|
51
|
-
if (existing.request_hash !== requestHash
|
|
51
|
+
if (existing.request_hash !== requestHash &&
|
|
52
|
+
existing.raw_request_hash !== requestHash) {
|
|
53
|
+
if (input.allowRequestHashMismatch)
|
|
54
|
+
return undefined;
|
|
52
55
|
throw new SessionLaunchOperationConflictError(pluginId, scopeId, operationId);
|
|
53
56
|
}
|
|
54
57
|
if (existing.state === "deleted") {
|
|
@@ -75,10 +78,16 @@ export function ensurePluginSessionLaunch(input) {
|
|
|
75
78
|
const scopeId = requiredId(input.scopeId, "scopeId");
|
|
76
79
|
const operationId = requiredId(input.operationId, "operationId");
|
|
77
80
|
const requestHash = requiredRequestHash(input.requestHash);
|
|
81
|
+
const rawRequestHash = input.rawRequestHash
|
|
82
|
+
? requiredRequestHash(input.rawRequestHash)
|
|
83
|
+
: requestHash;
|
|
78
84
|
const ensure = db().transaction(() => {
|
|
79
85
|
const existing = readOperation(pluginId, scopeId, operationId);
|
|
80
86
|
if (existing) {
|
|
81
|
-
if (existing.request_hash !== requestHash
|
|
87
|
+
if (existing.request_hash !== requestHash &&
|
|
88
|
+
existing.raw_request_hash !== requestHash &&
|
|
89
|
+
existing.request_hash !== rawRequestHash &&
|
|
90
|
+
existing.raw_request_hash !== rawRequestHash) {
|
|
82
91
|
throw new SessionLaunchOperationConflictError(pluginId, scopeId, operationId);
|
|
83
92
|
}
|
|
84
93
|
if (existing.state === "deleted") {
|
|
@@ -110,10 +119,10 @@ export function ensurePluginSessionLaunch(input) {
|
|
|
110
119
|
}
|
|
111
120
|
db()
|
|
112
121
|
.prepare(`INSERT INTO plugin_session_launch_operations
|
|
113
|
-
(plugin_id, scope_id, operation_id, request_hash, session_id, state,
|
|
122
|
+
(plugin_id, scope_id, operation_id, request_hash, raw_request_hash, session_id, state,
|
|
114
123
|
created_at, updated_at, deleted_at)
|
|
115
|
-
VALUES (?, ?, ?, ?, ?, 'active', ?, ?, NULL)`)
|
|
116
|
-
.run(pluginId, scopeId, operationId, requestHash, sessionId, now, now);
|
|
124
|
+
VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, NULL)`)
|
|
125
|
+
.run(pluginId, scopeId, operationId, requestHash, rawRequestHash, sessionId, now, now);
|
|
117
126
|
return { disposition: "created", sessionId };
|
|
118
127
|
});
|
|
119
128
|
const result = ensure.immediate();
|
|
@@ -124,7 +133,7 @@ export function ensurePluginSessionLaunch(input) {
|
|
|
124
133
|
}
|
|
125
134
|
function readOperation(pluginId, scopeId, operationId) {
|
|
126
135
|
return db()
|
|
127
|
-
.prepare(`SELECT request_hash, session_id, state
|
|
136
|
+
.prepare(`SELECT request_hash, raw_request_hash, session_id, state
|
|
128
137
|
FROM plugin_session_launch_operations
|
|
129
138
|
WHERE plugin_id = ? AND scope_id = ? AND operation_id = ?`)
|
|
130
139
|
.get(pluginId, scopeId, operationId);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface DaemonDiagnosticSnapshot {
|
|
2
|
+
databasePath: string;
|
|
3
|
+
databaseExists: boolean;
|
|
4
|
+
pluginCount: number;
|
|
5
|
+
agentCount: number;
|
|
6
|
+
browser: {
|
|
7
|
+
supported: false;
|
|
8
|
+
} | {
|
|
9
|
+
supported: true;
|
|
10
|
+
installedVersion?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function prepareBundledPlugins(): Promise<{
|
|
15
|
+
status: "ready" | "unavailable";
|
|
16
|
+
installed: string[];
|
|
17
|
+
updated: string[];
|
|
18
|
+
unchanged: string[];
|
|
19
|
+
}>;
|
|
20
|
+
export declare function inspectDaemonDiagnostics(): Promise<DaemonDiagnosticSnapshot>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { listAgentSpecs } from "@rynx-ai/core";
|
|
3
|
+
import { createChromeForTestingArtifactStore, isChromeForTestingHostSupported } from "./chrome-for-testing-store.js";
|
|
4
|
+
import { ensureBundledOfficialPlugins } from "./bundled-plugins.js";
|
|
5
|
+
import { dbPath } from "./db.js";
|
|
6
|
+
import { listPlugins } from "./plugin-store.js";
|
|
7
|
+
export async function prepareBundledPlugins() {
|
|
8
|
+
return ensureBundledOfficialPlugins();
|
|
9
|
+
}
|
|
10
|
+
export async function inspectDaemonDiagnostics() {
|
|
11
|
+
const databasePath = dbPath();
|
|
12
|
+
let browser;
|
|
13
|
+
if (!isChromeForTestingHostSupported()) {
|
|
14
|
+
browser = { supported: false };
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
try {
|
|
18
|
+
const installed = createChromeForTestingArtifactStore().readActive();
|
|
19
|
+
browser = {
|
|
20
|
+
supported: true,
|
|
21
|
+
...(installed ? { installedVersion: installed.version } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
browser = {
|
|
26
|
+
supported: true,
|
|
27
|
+
error: error instanceof Error ? error.message : String(error),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
databasePath,
|
|
33
|
+
databaseExists: existsSync(databasePath),
|
|
34
|
+
pluginCount: existsSync(databasePath) ? listPlugins().length : 0,
|
|
35
|
+
agentCount: (await listAgentSpecs()).length,
|
|
36
|
+
browser,
|
|
37
|
+
};
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/daemon",
|
|
3
|
-
"version": "0.1.11-beta.
|
|
3
|
+
"version": "0.1.11-beta.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
7
7
|
"directory": "packages/daemon"
|
|
8
8
|
},
|
|
9
|
-
"description": "Rynx
|
|
9
|
+
"description": "Rynx resident control-plane service and standalone lifecycle implementation.",
|
|
10
10
|
"type": "module",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22"
|
|
13
|
+
},
|
|
11
14
|
"publishConfig": {
|
|
12
15
|
"registry": "https://registry.npmjs.org/",
|
|
13
16
|
"access": "public"
|
|
@@ -16,38 +19,48 @@
|
|
|
16
19
|
"dist",
|
|
17
20
|
"bundled-plugins"
|
|
18
21
|
],
|
|
19
|
-
"bin": {
|
|
20
|
-
"rynx": "./dist/cli.js"
|
|
21
|
-
},
|
|
22
|
-
"main": "./dist/index-daemon.js",
|
|
23
22
|
"exports": {
|
|
24
|
-
"./
|
|
25
|
-
"types": "./dist/
|
|
26
|
-
"default": "./dist/
|
|
23
|
+
"./entry-path": {
|
|
24
|
+
"types": "./dist/entry-path.d.ts",
|
|
25
|
+
"default": "./dist/entry-path.js"
|
|
26
|
+
},
|
|
27
|
+
"./lifecycle": {
|
|
28
|
+
"types": "./dist/lifecycle.d.ts",
|
|
29
|
+
"default": "./dist/lifecycle.js"
|
|
30
|
+
},
|
|
31
|
+
"./browser-artifacts": {
|
|
32
|
+
"types": "./dist/browser-artifacts.d.ts",
|
|
33
|
+
"default": "./dist/browser-artifacts.js"
|
|
34
|
+
},
|
|
35
|
+
"./setup-service": {
|
|
36
|
+
"types": "./dist/setup-service.d.ts",
|
|
37
|
+
"default": "./dist/setup-service.js"
|
|
38
|
+
},
|
|
39
|
+
"./internal/process": {
|
|
40
|
+
"types": "./dist/process.d.ts",
|
|
41
|
+
"default": "./dist/process.js"
|
|
27
42
|
}
|
|
28
43
|
},
|
|
29
44
|
"dependencies": {
|
|
30
|
-
"@clack/prompts": "^1.6.0",
|
|
31
45
|
"better-sqlite3": "^12.11.1",
|
|
32
46
|
"extract-zip": "2.0.1",
|
|
33
47
|
"pm2": "^6.0.0",
|
|
34
48
|
"tar": "^7.5.19",
|
|
35
49
|
"ws": "^8.21.0",
|
|
36
|
-
"@rynx-ai/
|
|
37
|
-
"@rynx-ai/
|
|
38
|
-
"@rynx-ai/
|
|
39
|
-
"@rynx-ai/
|
|
40
|
-
"@rynx-ai/
|
|
41
|
-
"@rynx-ai/
|
|
42
|
-
"@rynx-ai/
|
|
43
|
-
"@rynx-ai/server": "0.1.11-beta.2"
|
|
50
|
+
"@rynx-ai/core": "0.1.11-beta.3",
|
|
51
|
+
"@rynx-ai/emulator": "0.1.11-beta.3",
|
|
52
|
+
"@rynx-ai/plugin-runner": "0.1.11-beta.3",
|
|
53
|
+
"@rynx-ai/protocol": "0.1.11-beta.3",
|
|
54
|
+
"@rynx-ai/remote-runtime-client": "0.1.11-beta.3",
|
|
55
|
+
"@rynx-ai/plugin-sdk": "0.1.11-beta.3",
|
|
56
|
+
"@rynx-ai/server": "0.1.11-beta.3"
|
|
44
57
|
},
|
|
45
58
|
"devDependencies": {
|
|
46
59
|
"@types/ws": "^8.18.1",
|
|
47
|
-
"@rynx-ai/plugin-channel-lark": "0.1.11"
|
|
60
|
+
"@rynx-ai/plugin-channel-lark": "0.1.11-beta.3"
|
|
48
61
|
},
|
|
49
62
|
"scripts": {
|
|
50
|
-
"build": "rm -rf dist bundled-plugins && tsc -p tsconfig.json && chmod +x dist/
|
|
63
|
+
"build": "rm -rf dist bundled-plugins && tsc -p tsconfig.json && chmod +x dist/index-daemon.js && node ../../scripts/stage-bundled-plugins.mjs",
|
|
51
64
|
"dev": "tsx watch --tsconfig ../../tsconfig.json src/index-daemon.ts"
|
|
52
65
|
}
|
|
53
66
|
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export type BrowserCliSubcommand = "install" | "update" | "version" | "clean" | "open" | "status" | "pages" | "endpoint" | "close";
|
|
2
|
-
export interface BrowserCliArgs {
|
|
3
|
-
subcommand: BrowserCliSubcommand;
|
|
4
|
-
json: boolean;
|
|
5
|
-
ensure: boolean;
|
|
6
|
-
channel?: string;
|
|
7
|
-
version?: string;
|
|
8
|
-
runtime?: string;
|
|
9
|
-
session?: string;
|
|
10
|
-
url?: string;
|
|
11
|
-
}
|
|
12
|
-
export interface BrowserCliTarget {
|
|
13
|
-
runtimeSelector: string;
|
|
14
|
-
sessionId: string;
|
|
15
|
-
credential?: RuntimeBrowserBootstrapCredential;
|
|
16
|
-
}
|
|
17
|
-
export declare function parseBrowserCliArgs(args: readonly string[]): BrowserCliArgs;
|
|
18
|
-
/** Resolve authority without ever turning a managed Session into an operator. */
|
|
19
|
-
export declare function resolveBrowserCliTarget(args: BrowserCliArgs, managedCredential: RuntimeBrowserBootstrapCredential | undefined): BrowserCliTarget;
|
|
20
|
-
import type { RuntimeBrowserBootstrapCredential } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
package/dist/browser-cli-args.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
const SPECS = {
|
|
2
|
-
install: { values: ["--channel", "--version"], flags: ["--json"] },
|
|
3
|
-
update: { values: ["--channel"], flags: ["--json"] },
|
|
4
|
-
version: { values: [], flags: ["--json"] },
|
|
5
|
-
clean: { values: [], flags: ["--json"] },
|
|
6
|
-
open: {
|
|
7
|
-
values: ["--url", "--session", "--runtime"],
|
|
8
|
-
flags: ["--json"],
|
|
9
|
-
positionalUrl: true,
|
|
10
|
-
},
|
|
11
|
-
status: { values: ["--session", "--runtime"], flags: ["--json"] },
|
|
12
|
-
pages: { values: ["--session", "--runtime"], flags: ["--json"] },
|
|
13
|
-
endpoint: { values: ["--session"], flags: ["--ensure", "--json"] },
|
|
14
|
-
close: { values: ["--session", "--runtime"], flags: ["--json"] },
|
|
15
|
-
};
|
|
16
|
-
export function parseBrowserCliArgs(args) {
|
|
17
|
-
const subcommand = args[0];
|
|
18
|
-
if (!isBrowserSubcommand(subcommand)) {
|
|
19
|
-
throw new Error("browser: expected install, update, version, clean, open, status, pages, endpoint, or close");
|
|
20
|
-
}
|
|
21
|
-
const spec = SPECS[subcommand];
|
|
22
|
-
const values = new Map();
|
|
23
|
-
const flags = new Set();
|
|
24
|
-
const positionals = [];
|
|
25
|
-
for (let index = 1; index < args.length; index += 1) {
|
|
26
|
-
const arg = args[index];
|
|
27
|
-
if (arg.startsWith("--")) {
|
|
28
|
-
if (spec.flags.includes(arg)) {
|
|
29
|
-
if (flags.has(arg))
|
|
30
|
-
throw new Error(`browser ${subcommand}: duplicate option ${arg}`);
|
|
31
|
-
flags.add(arg);
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (spec.values.includes(arg)) {
|
|
35
|
-
if (values.has(arg))
|
|
36
|
-
throw new Error(`browser ${subcommand}: duplicate option ${arg}`);
|
|
37
|
-
const value = args[index + 1];
|
|
38
|
-
if (!value || value.startsWith("--")) {
|
|
39
|
-
throw new Error(`browser ${subcommand}: ${arg} requires a value`);
|
|
40
|
-
}
|
|
41
|
-
values.set(arg, value);
|
|
42
|
-
index += 1;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
throw new Error(`browser ${subcommand}: unknown option ${arg}`);
|
|
46
|
-
}
|
|
47
|
-
positionals.push(arg);
|
|
48
|
-
}
|
|
49
|
-
if (!spec.positionalUrl && positionals.length > 0) {
|
|
50
|
-
throw new Error(`browser ${subcommand}: unexpected argument ${positionals[0]}`);
|
|
51
|
-
}
|
|
52
|
-
if (positionals.length > 1) {
|
|
53
|
-
throw new Error(`browser ${subcommand}: unexpected extra argument ${positionals[1]}`);
|
|
54
|
-
}
|
|
55
|
-
if (positionals.length === 1 && values.has("--url")) {
|
|
56
|
-
throw new Error("browser open: pass the URL once, either positionally or with --url");
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
subcommand,
|
|
60
|
-
json: flags.has("--json"),
|
|
61
|
-
ensure: flags.has("--ensure"),
|
|
62
|
-
...(values.has("--channel") ? { channel: values.get("--channel") } : {}),
|
|
63
|
-
...(values.has("--version") ? { version: values.get("--version") } : {}),
|
|
64
|
-
...(values.has("--runtime") ? { runtime: values.get("--runtime") } : {}),
|
|
65
|
-
...(values.has("--session") ? { session: values.get("--session") } : {}),
|
|
66
|
-
...(values.has("--url") || positionals[0]
|
|
67
|
-
? { url: values.get("--url") ?? positionals[0] }
|
|
68
|
-
: {}),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
/** Resolve authority without ever turning a managed Session into an operator. */
|
|
72
|
-
export function resolveBrowserCliTarget(args, managedCredential) {
|
|
73
|
-
const runtimeSelector = args.runtime ?? "local";
|
|
74
|
-
if (runtimeSelector !== "local") {
|
|
75
|
-
if (managedCredential) {
|
|
76
|
-
throw new Error("browser: a managed Session cannot select another Runtime");
|
|
77
|
-
}
|
|
78
|
-
if (!args.session) {
|
|
79
|
-
throw new Error("browser: --session is required when selecting a remote Runtime");
|
|
80
|
-
}
|
|
81
|
-
return { runtimeSelector, sessionId: args.session };
|
|
82
|
-
}
|
|
83
|
-
if (managedCredential) {
|
|
84
|
-
if (args.session && args.session !== managedCredential.sessionId) {
|
|
85
|
-
throw new Error("browser: a managed Session cannot control another Session with --session");
|
|
86
|
-
}
|
|
87
|
-
return {
|
|
88
|
-
runtimeSelector: "local",
|
|
89
|
-
sessionId: managedCredential.sessionId,
|
|
90
|
-
credential: managedCredential,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
if (!args.session) {
|
|
94
|
-
throw new Error("browser: run inside a managed Rynx Session or pass --session for operator access");
|
|
95
|
-
}
|
|
96
|
-
return { runtimeSelector: "local", sessionId: args.session };
|
|
97
|
-
}
|
|
98
|
-
function isBrowserSubcommand(value) {
|
|
99
|
-
return value !== undefined && Object.hasOwn(SPECS, value);
|
|
100
|
-
}
|
package/dist/cli.d.ts
DELETED