@rynx-ai/daemon 0.1.0

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/update.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * `rynx update` — upgrade the globally-installed daemon npm package and restart.
3
+ *
4
+ * The daemon ships as a global npm package (`@rynx-ai/daemon`, bin `rynx`), so
5
+ * updating = `npm install -g <pkg>@latest` + restart.
6
+ *
7
+ * rynx update [version] upgrade (to a version, or latest) + restart
8
+ * rynx update --check report status only (human one-liner)
9
+ * rynx update --check --json report status only (typed UpdateCheck JSON)
10
+ * rynx update --result-file <path> after finishing, write the outcome JSON to
11
+ * <path> (channel-agnostic UpdateOutcome; a
12
+ * plugin reads it post-restart to notify a chat)
13
+ */
14
+ import { spawnSync } from "node:child_process";
15
+ import { mkdirSync, writeFileSync } from "node:fs";
16
+ import { createRequire } from "node:module";
17
+ import { dirname, sep } from "node:path";
18
+ import { loadConfig } from "@rynx-ai/core";
19
+ import { formatControlUrl, startDaemon } from "./pm2.js";
20
+ /** This daemon package's name + installed version (read from its own package.json). */
21
+ function selfPackage() {
22
+ const require = createRequire(import.meta.url);
23
+ const pkg = require("../package.json");
24
+ return { name: pkg.name ?? "@rynx-ai/daemon", version: pkg.version ?? "0.0.0" };
25
+ }
26
+ /** This package's root directory (the dir holding its package.json). */
27
+ function packageRoot() {
28
+ const require = createRequire(import.meta.url);
29
+ return dirname(require.resolve("../package.json"));
30
+ }
31
+ /**
32
+ * True when running from a working checkout / linked dev build rather than an
33
+ * npm install. We refuse to `npm install -g` over it — that would clobber the
34
+ * dev build. An installed package always lives under a `node_modules` directory;
35
+ * a checkout (e.g. `packages/daemon`) does not.
36
+ */
37
+ function isLocalDevInstall() {
38
+ return !packageRoot().split(sep).includes("node_modules");
39
+ }
40
+ /** Latest published version from the configured npm registry, or null on failure. */
41
+ function latestVersion(name) {
42
+ const result = spawnSync("npm", ["view", name, "version"], { encoding: "utf8", timeout: 60_000 });
43
+ if (result.error || result.status !== 0)
44
+ return null;
45
+ return result.stdout.trim() || null;
46
+ }
47
+ /** Is `latest` a newer release than `current`? Numeric major.minor.patch (prerelease ignored). */
48
+ export function isNewer(latest, current) {
49
+ const parse = (v) => v.split("-")[0].split(".").map((n) => Number(n) || 0);
50
+ const a = parse(latest);
51
+ const b = parse(current);
52
+ for (let i = 0; i < 3; i++) {
53
+ const x = a[i] ?? 0;
54
+ const y = b[i] ?? 0;
55
+ if (x !== y)
56
+ return x > y;
57
+ }
58
+ return false;
59
+ }
60
+ /** Typed `--check` result a channel consumes via `--check --json`. */
61
+ export function buildUpdateCheck(current, latest) {
62
+ if (!latest)
63
+ return { status: "error", detail: "could not resolve latest version" };
64
+ return isNewer(latest, current)
65
+ ? { status: "behind", current, latest }
66
+ : { status: "up_to_date", current };
67
+ }
68
+ /** Format the one-line, human-readable status emitted by a bare `--check`. */
69
+ export function checkStatusLine(current, latest) {
70
+ const check = buildUpdateCheck(current, latest);
71
+ switch (check.status) {
72
+ case "behind":
73
+ return `behind ${check.current} ${check.latest}`;
74
+ case "up_to_date":
75
+ return `up_to_date ${check.current}`;
76
+ case "error":
77
+ return `error ${check.detail}`;
78
+ }
79
+ }
80
+ function npmInstallGlobal(name, version) {
81
+ const result = spawnSync("npm", ["install", "-g", `${name}@${version}`, "--no-audit", "--no-fund"], { stdio: "inherit", timeout: 600_000 });
82
+ return !result.error && result.status === 0;
83
+ }
84
+ /** Poll `/health` until the restarted daemon answers (or give up). */
85
+ async function verifyHealthy() {
86
+ const config = loadConfig();
87
+ const url = `${formatControlUrl(config.HOST, config.PORT)}health`;
88
+ for (let attempt = 0; attempt < 10; attempt++) {
89
+ await new Promise((r) => setTimeout(r, 1000));
90
+ try {
91
+ const res = await fetch(url);
92
+ if (res.ok)
93
+ return true;
94
+ }
95
+ catch {
96
+ /* not back up yet */
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+ /** Write the post-restart outcome to `--result-file` (best-effort; stamps `at`). */
102
+ function writeOutcome(file, outcome) {
103
+ if (!file)
104
+ return;
105
+ try {
106
+ mkdirSync(dirname(file), { recursive: true });
107
+ const full = { ...outcome, at: new Date().toISOString() };
108
+ writeFileSync(file, `${JSON.stringify(full, null, 2)}\n`);
109
+ }
110
+ catch {
111
+ /* best-effort */
112
+ }
113
+ }
114
+ /** Run `rynx update`. Returns a process exit code. */
115
+ export async function runUpdate(opts) {
116
+ const { name, version: current } = selfPackage();
117
+ const latest = latestVersion(name);
118
+ if (opts.check) {
119
+ console.log(opts.json ? JSON.stringify(buildUpdateCheck(current, latest)) : checkStatusLine(current, latest));
120
+ return 0;
121
+ }
122
+ // Never `npm install -g` over a working checkout / linked dev build.
123
+ if (isLocalDevInstall()) {
124
+ console.error(`update: ${name} is a local dev install — update your checkout (e.g. pnpm build), not npm`);
125
+ writeOutcome(opts.resultFile, { status: "fail", from: current, to: current, error: "local dev install" });
126
+ return 1;
127
+ }
128
+ const target = opts.version ?? latest;
129
+ if (!target) {
130
+ console.error(`update: could not resolve a target version for ${name} (registry unreachable?)`);
131
+ writeOutcome(opts.resultFile, { status: "fail", from: current, to: current, error: "could not resolve target version" });
132
+ return 1;
133
+ }
134
+ if (!opts.version && latest && !isNewer(latest, current)) {
135
+ console.log(`Already on the latest ${name}@${current}.`);
136
+ return 0;
137
+ }
138
+ console.log(`Updating ${name}: ${current} → ${target} …`);
139
+ if (!npmInstallGlobal(name, target)) {
140
+ console.error("update: npm install failed");
141
+ writeOutcome(opts.resultFile, { status: "fail", from: current, to: target, error: "npm install failed" });
142
+ // The install failed before any restart, so the channel that spawned us never
143
+ // reboots to drain the outcome. Bounce the daemon (still on the old, healthy
144
+ // version) so its boot-time drain delivers the failure to chat.
145
+ if (opts.resultFile)
146
+ startDaemon({ force: true });
147
+ return 1;
148
+ }
149
+ startDaemon({ force: true });
150
+ if (!(await verifyHealthy())) {
151
+ console.error(`update: ${name}@${target} did not come back up — rolling back to ${current}`);
152
+ npmInstallGlobal(name, current);
153
+ startDaemon({ force: true });
154
+ writeOutcome(opts.resultFile, { status: "fail", from: current, to: target, error: "did not come up; rolled back" });
155
+ return 1;
156
+ }
157
+ console.log(`Updated ${name} → ${target} and restarted.`);
158
+ writeOutcome(opts.resultFile, { status: "ok", from: current, to: target });
159
+ return 0;
160
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@rynx-ai/daemon",
3
+ "version": "0.1.0",
4
+ "description": "rynx daemon CLI: supervises the resident @rynx-ai/server via pm2 and loads channel plugins recorded in ~/.rynx/rynx.db.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "registry": "https://registry.npmjs.org/",
8
+ "access": "public"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "bin": {
14
+ "rynx": "./dist/cli.js"
15
+ },
16
+ "main": "./dist/index-daemon.js",
17
+ "exports": {
18
+ "./server": {
19
+ "types": "./dist/daemon-server.d.ts",
20
+ "default": "./dist/daemon-server.js"
21
+ }
22
+ },
23
+ "dependencies": {
24
+ "@clack/prompts": "^1.6.0",
25
+ "better-sqlite3": "^12.11.1",
26
+ "pm2": "^6.0.0",
27
+ "zod": "^4.3.6",
28
+ "@rynx-ai/core": "0.1.0",
29
+ "@rynx-ai/protocol": "0.1.0",
30
+ "@rynx-ai/server": "0.1.0",
31
+ "@rynx-ai/emulator": "0.1.7"
32
+ },
33
+ "scripts": {
34
+ "build": "rm -rf dist && tsc -p tsconfig.json && chmod +x dist/cli.js dist/index-daemon.js",
35
+ "dev": "tsx watch --tsconfig ../../tsconfig.json src/index-daemon.ts"
36
+ }
37
+ }