@versdotsh/reef 0.1.3 → 0.1.4

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/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ ## 0.1.4
4
+
5
+ - Add updater service — auto-update reef from npm
6
+ - `GET /updater/status` — current version, latest available, update history
7
+ - `POST /updater/check` — check npm for newer version
8
+ - `POST /updater/apply` — apply update and restart
9
+ - Optional polling with `UPDATE_POLL_INTERVAL` (minutes)
10
+ - Optional auto-apply with `UPDATE_AUTO_APPLY=true`
11
+ - 126 tests, 324 assertions
12
+
13
+ ## 0.1.3
14
+
15
+ - Add READMEs to all example services (board, commits, feed, journal, log, registry, reports, ui, usage)
16
+
17
+ ## 0.1.2
18
+
19
+ - Switch to npm trusted publishing via OIDC — no tokens needed
20
+ - Scope package under `@versdotsh/reef`
21
+
22
+ ## 0.1.1
23
+
24
+ - Add GitHub Actions CI (test & publish on push to main)
25
+ - Add `.gitignore` for `data/` directory
26
+ - Add test harness (`src/core/testing.ts`) — `createTestHarness()` for isolated service testing
27
+ - Add tests for all example services (board, commits, feed, journal, log, registry, reports, usage)
28
+ - Add UI panels section and testing section to create-service skill
29
+ - 122 tests, 304 assertions
30
+
31
+ ## 0.1.0
32
+
33
+ - Initial release
34
+ - Core: dynamic dispatch server, module discovery with topo-sort, bearer token auth
35
+ - Infrastructure services: agent, docs, installer, services manager
36
+ - Agent service: fire-and-forget tasks via `pi -p`, interactive sessions via `pi --mode rpc`, SSE streaming
37
+ - Installer: git clone, local symlink, fleet-to-fleet tarball install/update
38
+ - Docs service: auto-generated API docs from `routeDocs` metadata
39
+ - Services manager: list, reload, unload modules at runtime
40
+ - Example services: board, commits, feed, journal, log, registry, reports, ui, usage
41
+ - Dynamic panel system: services serve `GET /_panel` HTML fragments, UI discovers and injects them
42
+ - UI service: web dashboard with magic link auth, API proxy, chat interface
43
+ - Pi extension: `filterClientModules()` for client-side tool/behavior registration
44
+ - Create-service skill for teaching agents to write new modules
45
+ - 60 core tests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versdotsh/reef",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Self-improving fleet infrastructure — the minimum kernel agents need to build their own tools",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -0,0 +1,40 @@
1
+ # updater
2
+
3
+ Auto-update the reef server from npm. Checks for new versions of `@versdotsh/reef`, downloads updates, and restarts the process.
4
+
5
+ ## Routes
6
+
7
+ | Method | Path | Description |
8
+ |--------|------|-------------|
9
+ | `GET` | `/updater/status` | Current version, latest available, poll config, update history |
10
+ | `POST` | `/updater/check` | Check npm for a newer version |
11
+ | `POST` | `/updater/apply` | Apply the update and restart the server |
12
+
13
+ ## Configuration
14
+
15
+ | Env var | Default | Description |
16
+ |---------|---------|-------------|
17
+ | `UPDATE_POLL_INTERVAL` | `0` | Check interval in minutes (0 = manual only) |
18
+ | `UPDATE_AUTO_APPLY` | `false` | Automatically apply updates when found |
19
+
20
+ ## Usage
21
+
22
+ **Manual update:**
23
+ ```bash
24
+ # Check for updates
25
+ curl -X POST http://localhost:3000/updater/check -H "Authorization: Bearer $TOKEN"
26
+
27
+ # Apply if available
28
+ curl -X POST http://localhost:3000/updater/apply -H "Authorization: Bearer $TOKEN"
29
+ ```
30
+
31
+ **Auto-update every 30 minutes:**
32
+ ```bash
33
+ UPDATE_POLL_INTERVAL=30 UPDATE_AUTO_APPLY=true bun run start
34
+ ```
35
+
36
+ ## How it works
37
+
38
+ 1. **Check** — fetches `https://registry.npmjs.org/@versdotsh/reef/latest` and compares versions
39
+ 2. **Apply** — runs `bun update @versdotsh/reef`, then spawns a new server process and exits the current one
40
+ 3. **Poll** — if `UPDATE_POLL_INTERVAL` is set, checks on a timer; if `UPDATE_AUTO_APPLY` is also set, applies automatically
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Updater service module — auto-update the reef server.
3
+ *
4
+ * Checks npm for new versions of @versdotsh/reef and applies updates.
5
+ * Can run on a schedule (poll interval) or be triggered manually via API.
6
+ *
7
+ * Routes:
8
+ * GET /updater/status — current version, latest available, update history
9
+ * POST /updater/check — check for updates now
10
+ * POST /updater/apply — download and apply the latest version, then restart
11
+ *
12
+ * Env vars:
13
+ * UPDATE_POLL_INTERVAL — check interval in minutes (default: 0 = disabled)
14
+ * UPDATE_AUTO_APPLY — automatically apply updates when found (default: false)
15
+ */
16
+
17
+ import { Hono } from "hono";
18
+ import { execSync, spawn } from "node:child_process";
19
+ import { readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import type { ServiceModule, ServiceContext } from "../../src/core/types.js";
22
+
23
+ interface UpdateRecord {
24
+ from: string;
25
+ to: string;
26
+ timestamp: string;
27
+ status: "applied" | "failed";
28
+ error?: string;
29
+ }
30
+
31
+ const PACKAGE_NAME = "@versdotsh/reef";
32
+
33
+ let currentVersion: string = "unknown";
34
+ let latestVersion: string | null = null;
35
+ let lastChecked: string | null = null;
36
+ let updateAvailable = false;
37
+ let checking = false;
38
+ let applying = false;
39
+ let pollTimer: ReturnType<typeof setInterval> | null = null;
40
+ let history: UpdateRecord[] = [];
41
+
42
+ function loadCurrentVersion(): string {
43
+ try {
44
+ // Walk up from services/updater to find package.json
45
+ const paths = [
46
+ join(import.meta.dir, "..", "..", "package.json"),
47
+ join(process.cwd(), "package.json"),
48
+ ];
49
+ for (const p of paths) {
50
+ try {
51
+ const pkg = JSON.parse(readFileSync(p, "utf-8"));
52
+ if (pkg.name === PACKAGE_NAME || pkg.name === "reef") {
53
+ return pkg.version;
54
+ }
55
+ } catch {}
56
+ }
57
+ // Fallback: ask bun
58
+ const out = execSync("bun pm ls 2>/dev/null | grep reef || true", {
59
+ encoding: "utf-8",
60
+ timeout: 5000,
61
+ }).trim();
62
+ const match = out.match(/@[\d.]+/);
63
+ return match ? match[0].slice(1) : "unknown";
64
+ } catch {
65
+ return "unknown";
66
+ }
67
+ }
68
+
69
+ async function checkForUpdate(): Promise<{
70
+ current: string;
71
+ latest: string;
72
+ updateAvailable: boolean;
73
+ }> {
74
+ checking = true;
75
+ try {
76
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
77
+ headers: { Accept: "application/json" },
78
+ signal: AbortSignal.timeout(10000),
79
+ });
80
+
81
+ if (!res.ok) {
82
+ throw new Error(`npm registry returned ${res.status}`);
83
+ }
84
+
85
+ const data = (await res.json()) as { version: string };
86
+ latestVersion = data.version;
87
+ lastChecked = new Date().toISOString();
88
+ updateAvailable = latestVersion !== currentVersion;
89
+
90
+ return {
91
+ current: currentVersion,
92
+ latest: latestVersion,
93
+ updateAvailable,
94
+ };
95
+ } finally {
96
+ checking = false;
97
+ }
98
+ }
99
+
100
+ async function applyUpdate(): Promise<UpdateRecord> {
101
+ if (!latestVersion || !updateAvailable) {
102
+ throw new Error("No update available — run check first");
103
+ }
104
+
105
+ applying = true;
106
+ const from = currentVersion;
107
+ const to = latestVersion;
108
+
109
+ try {
110
+ // Update the package
111
+ execSync(`bun update ${PACKAGE_NAME}`, {
112
+ encoding: "utf-8",
113
+ timeout: 60000,
114
+ cwd: process.cwd(),
115
+ stdio: "pipe",
116
+ });
117
+
118
+ const record: UpdateRecord = {
119
+ from,
120
+ to,
121
+ timestamp: new Date().toISOString(),
122
+ status: "applied",
123
+ };
124
+ history.push(record);
125
+
126
+ // Schedule restart — give time for the response to be sent
127
+ setTimeout(() => {
128
+ console.log(` [updater] restarting after update ${from} → ${to}`);
129
+
130
+ // Spawn a new process with the same args, then exit
131
+ const child = spawn(process.argv[0], process.argv.slice(1), {
132
+ cwd: process.cwd(),
133
+ env: process.env,
134
+ stdio: "inherit",
135
+ detached: true,
136
+ });
137
+ child.unref();
138
+
139
+ // Give the child a moment to start, then exit
140
+ setTimeout(() => process.exit(0), 500);
141
+ }, 1000);
142
+
143
+ return record;
144
+ } catch (err) {
145
+ const msg = err instanceof Error ? err.message : String(err);
146
+ const record: UpdateRecord = {
147
+ from,
148
+ to,
149
+ timestamp: new Date().toISOString(),
150
+ status: "failed",
151
+ error: msg,
152
+ };
153
+ history.push(record);
154
+ throw new Error(`Update failed: ${msg}`);
155
+ } finally {
156
+ applying = false;
157
+ }
158
+ }
159
+
160
+ // Routes
161
+ const routes = new Hono();
162
+
163
+ routes.get("/status", (c) =>
164
+ c.json({
165
+ package: PACKAGE_NAME,
166
+ current: currentVersion,
167
+ latest: latestVersion,
168
+ updateAvailable,
169
+ lastChecked,
170
+ checking,
171
+ applying,
172
+ pollInterval: parseInt(process.env.UPDATE_POLL_INTERVAL || "0", 10),
173
+ autoApply: process.env.UPDATE_AUTO_APPLY === "true",
174
+ history,
175
+ }),
176
+ );
177
+
178
+ routes.post("/check", async (c) => {
179
+ if (checking) {
180
+ return c.json({ error: "Already checking" }, 409);
181
+ }
182
+
183
+ try {
184
+ const result = await checkForUpdate();
185
+ return c.json(result);
186
+ } catch (err) {
187
+ const msg = err instanceof Error ? err.message : String(err);
188
+ return c.json({ error: msg }, 502);
189
+ }
190
+ });
191
+
192
+ routes.post("/apply", async (c) => {
193
+ if (applying) {
194
+ return c.json({ error: "Already applying an update" }, 409);
195
+ }
196
+
197
+ if (!updateAvailable) {
198
+ // Check first
199
+ try {
200
+ await checkForUpdate();
201
+ } catch (err) {
202
+ const msg = err instanceof Error ? err.message : String(err);
203
+ return c.json({ error: `Check failed: ${msg}` }, 502);
204
+ }
205
+
206
+ if (!updateAvailable) {
207
+ return c.json({
208
+ message: "Already up to date",
209
+ current: currentVersion,
210
+ });
211
+ }
212
+ }
213
+
214
+ try {
215
+ const record = await applyUpdate();
216
+ return c.json({
217
+ message: `Updated ${record.from} → ${record.to} — restarting...`,
218
+ ...record,
219
+ });
220
+ } catch (err) {
221
+ const msg = err instanceof Error ? err.message : String(err);
222
+ return c.json({ error: msg }, 500);
223
+ }
224
+ });
225
+
226
+ // Module definition
227
+ const updater: ServiceModule = {
228
+ name: "updater",
229
+ description: "Auto-update reef from npm",
230
+ routes,
231
+
232
+ routeDocs: {
233
+ "GET /status": {
234
+ description: "Current version, latest available, update history",
235
+ response: "{ package, current, latest, updateAvailable, lastChecked, checking, applying, pollInterval, autoApply, history }",
236
+ },
237
+ "POST /check": {
238
+ description: "Check npm for a newer version",
239
+ response: "{ current, latest, updateAvailable }",
240
+ },
241
+ "POST /apply": {
242
+ description: "Apply the latest update and restart the server",
243
+ response: "{ message, from, to, timestamp, status }",
244
+ },
245
+ },
246
+
247
+ init(ctx: ServiceContext) {
248
+ currentVersion = loadCurrentVersion();
249
+
250
+ const pollMinutes = parseInt(process.env.UPDATE_POLL_INTERVAL || "0", 10);
251
+ const autoApply = process.env.UPDATE_AUTO_APPLY === "true";
252
+
253
+ if (pollMinutes > 0) {
254
+ console.log(
255
+ ` [updater] polling every ${pollMinutes}m${autoApply ? " (auto-apply)" : ""}`,
256
+ );
257
+
258
+ pollTimer = setInterval(async () => {
259
+ try {
260
+ const result = await checkForUpdate();
261
+ if (result.updateAvailable) {
262
+ console.log(
263
+ ` [updater] new version available: ${result.current} → ${result.latest}`,
264
+ );
265
+ if (autoApply) {
266
+ console.log(` [updater] auto-applying update...`);
267
+ await applyUpdate();
268
+ }
269
+ }
270
+ } catch (err) {
271
+ const msg = err instanceof Error ? err.message : String(err);
272
+ console.error(` [updater] check failed: ${msg}`);
273
+ }
274
+ }, pollMinutes * 60 * 1000);
275
+ }
276
+ },
277
+
278
+ store: {
279
+ close() {
280
+ if (pollTimer) {
281
+ clearInterval(pollTimer);
282
+ pollTimer = null;
283
+ }
284
+ return Promise.resolve();
285
+ },
286
+ },
287
+ };
288
+
289
+ export default updater;
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect, afterAll } from "bun:test";
2
+ import { createTestHarness, type TestHarness } from "../../src/core/testing.js";
3
+ import updater from "./index.js";
4
+
5
+ let t: TestHarness;
6
+ const setup = (async () => {
7
+ t = await createTestHarness({ services: [updater] });
8
+ })();
9
+ afterAll(() => t?.cleanup());
10
+
11
+ describe("updater", () => {
12
+ test("returns status", async () => {
13
+ await setup;
14
+ const { status, data } = await t.json<any>("/updater/status", { auth: true });
15
+ expect(status).toBe(200);
16
+ expect(data.package).toBe("@versdotsh/reef");
17
+ expect(data.current).toBeDefined();
18
+ expect(data.updateAvailable).toBe(false);
19
+ expect(data.checking).toBe(false);
20
+ expect(data.applying).toBe(false);
21
+ expect(data.history).toEqual([]);
22
+ });
23
+
24
+ test("checks for updates from npm", async () => {
25
+ await setup;
26
+ const { status, data } = await t.json<any>("/updater/check", {
27
+ method: "POST",
28
+ auth: true,
29
+ });
30
+ expect(status).toBe(200);
31
+ expect(data.current).toBeDefined();
32
+ expect(data.latest).toBeDefined();
33
+ expect(typeof data.updateAvailable).toBe("boolean");
34
+ });
35
+
36
+ test("apply when already up to date returns message", async () => {
37
+ await setup;
38
+ // After check, if versions match, apply should say "already up to date"
39
+ const { data: checkData } = await t.json<any>("/updater/check", {
40
+ method: "POST",
41
+ auth: true,
42
+ });
43
+
44
+ // Only test the "already up to date" path — don't actually apply
45
+ if (!checkData.updateAvailable) {
46
+ const { status, data } = await t.json<any>("/updater/apply", {
47
+ method: "POST",
48
+ auth: true,
49
+ });
50
+ expect(status).toBe(200);
51
+ expect(data.message).toContain("up to date");
52
+ }
53
+ });
54
+
55
+ test("requires auth", async () => {
56
+ await setup;
57
+ const { status: s1 } = await t.json("/updater/status");
58
+ expect(s1).toBe(401);
59
+ const { status: s2 } = await t.json("/updater/check", { method: "POST" });
60
+ expect(s2).toBe(401);
61
+ const { status: s3 } = await t.json("/updater/apply", { method: "POST" });
62
+ expect(s3).toBe(401);
63
+ });
64
+ });