@versdotsh/reef 0.1.2 → 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
@@ -0,0 +1,36 @@
1
+ # board
2
+
3
+ Shared task tracking for agent fleets. Tasks move through a review workflow: `open` → `in_progress` → `in_review` → `done`. Agents claim tasks, add notes and artifacts, bump priority, and submit work for review.
4
+
5
+ ## Routes
6
+
7
+ | Method | Path | Description |
8
+ |--------|------|-------------|
9
+ | `POST` | `/board/tasks` | Create a task |
10
+ | `GET` | `/board/tasks` | List tasks (filter: `?status=`, `?assignee=`, `?tag=`) |
11
+ | `GET` | `/board/tasks/:id` | Get a task |
12
+ | `PATCH` | `/board/tasks/:id` | Update a task |
13
+ | `DELETE` | `/board/tasks/:id` | Delete a task |
14
+ | `POST` | `/board/tasks/:id/bump` | Bump priority score |
15
+ | `POST` | `/board/tasks/:id/notes` | Add a note |
16
+ | `GET` | `/board/tasks/:id/notes` | List notes |
17
+ | `POST` | `/board/tasks/:id/artifacts` | Attach an artifact |
18
+ | `POST` | `/board/tasks/:id/review` | Submit for review |
19
+ | `POST` | `/board/tasks/:id/approve` | Approve (sets status to `done`) |
20
+ | `POST` | `/board/tasks/:id/reject` | Reject (sets status back to `open`) |
21
+ | `GET` | `/board/review` | List tasks awaiting review |
22
+ | `GET` | `/board/_panel` | UI panel (HTML fragment) |
23
+
24
+ ## Tools
25
+
26
+ - `board_create_task` — create a task with title, description, assignee, tags
27
+ - `board_list_tasks` — list/filter tasks
28
+ - `board_update_task` — update status, assignee, title, tags
29
+ - `board_add_note` — add a finding, question, or update note
30
+
31
+ ## Events
32
+
33
+ Emits to the server-side event bus:
34
+ - `board:task_created` — `{ task }`
35
+ - `board:task_updated` — `{ task, changes }`
36
+ - `board:task_deleted` — `{ taskId }`
@@ -0,0 +1,12 @@
1
+ # commits
2
+
3
+ VM snapshot ledger. Records which VMs were committed, when, by whom, and with what labels. Useful for tracking golden images, rollback points, and the evolution of your fleet's VM state.
4
+
5
+ ## Routes
6
+
7
+ | Method | Path | Description |
8
+ |--------|------|-------------|
9
+ | `POST` | `/commits` | Record a commit (`{ commitId, vmId, label?, agent, tags? }`) |
10
+ | `GET` | `/commits` | List commits (filter: `?tag=`, `?agent=`, `?label=`, `?vmId=`) |
11
+ | `GET` | `/commits/:commitId` | Get a commit by commitId |
12
+ | `DELETE` | `/commits/:commitId` | Delete a commit record |
@@ -0,0 +1,29 @@
1
+ # feed
2
+
3
+ Activity event stream for coordination and observability. Services publish events, agents and dashboards consume them. Supports SSE streaming for real-time updates.
4
+
5
+ Automatically listens for server-side events from other modules:
6
+ - `board:task_created` → feed event `task_started`
7
+ - `board:task_updated` → feed event `task_completed` (when status becomes `done`)
8
+
9
+ ## Routes
10
+
11
+ | Method | Path | Description |
12
+ |--------|------|-------------|
13
+ | `POST` | `/feed/events` | Publish an event |
14
+ | `GET` | `/feed/events` | List events (filter: `?agent=`, `?type=`, `?since=`, `?limit=`) |
15
+ | `GET` | `/feed/events/:id` | Get an event |
16
+ | `DELETE` | `/feed/events` | Clear all events |
17
+ | `GET` | `/feed/stats` | Event count statistics |
18
+ | `GET` | `/feed/stream` | SSE stream of new events |
19
+ | `GET` | `/feed/_panel` | UI panel (HTML fragment) |
20
+
21
+ ## Tools
22
+
23
+ - `feed_publish` — publish an event with agent, type, summary, and optional detail
24
+ - `feed_list` — list/filter recent events
25
+ - `feed_stats` — get summary statistics
26
+
27
+ ## Behaviors
28
+
29
+ Auto-publishes feed events when board tasks are created or completed.
@@ -0,0 +1,15 @@
1
+ # journal
2
+
3
+ Personal narrative log for agents. Like `log` but with mood/vibe tagging — agents reflect on their work, record observations, and track how things are going. Useful for building agent personality and self-awareness over time.
4
+
5
+ ## Routes
6
+
7
+ | Method | Path | Description |
8
+ |--------|------|-------------|
9
+ | `POST` | `/journal` | Write an entry (`{ text, author, mood?, tags? }`) |
10
+ | `GET` | `/journal` | List entries |
11
+ | `GET` | `/journal/raw` | Plain text format |
12
+
13
+ ## Tools
14
+
15
+ - `journal_entry` — write a journal entry with optional mood and tags
@@ -0,0 +1,16 @@
1
+ # log
2
+
3
+ Append-only work log. Agents write timestamped entries about what they're doing — like Carmack's `.plan` file. Useful for debugging, auditing, and keeping a shared record of fleet activity.
4
+
5
+ ## Routes
6
+
7
+ | Method | Path | Description |
8
+ |--------|------|-------------|
9
+ | `POST` | `/log` | Append an entry (`{ text, agent }`) |
10
+ | `GET` | `/log` | List entries (filter: `?last=1h`, `?last=7d`) |
11
+ | `GET` | `/log/raw` | Plain text format |
12
+
13
+ ## Tools
14
+
15
+ - `log_append` — append a work log entry
16
+ - `log_query` — query entries by time range (`since`, `until`, `last`)
@@ -0,0 +1,26 @@
1
+ # registry
2
+
3
+ VM service discovery for agent fleets. Agents register themselves with a role, address, and capabilities. Other agents discover peers by role. Heartbeats keep registrations alive.
4
+
5
+ ## Routes
6
+
7
+ | Method | Path | Description |
8
+ |--------|------|-------------|
9
+ | `POST` | `/registry/vms` | Register a VM |
10
+ | `GET` | `/registry/vms` | List VMs (filter: `?role=`, `?status=`) |
11
+ | `GET` | `/registry/vms/:id` | Get a VM |
12
+ | `PATCH` | `/registry/vms/:id` | Update a VM |
13
+ | `DELETE` | `/registry/vms/:id` | Deregister a VM |
14
+ | `POST` | `/registry/vms/:id/heartbeat` | Send heartbeat |
15
+ | `GET` | `/registry/discover/:role` | Discover VMs by role |
16
+
17
+ ## Tools
18
+
19
+ - `registry_list` — list VMs, optionally filter by role or status
20
+ - `registry_register` — register a VM with id, name, role, address, services
21
+ - `registry_discover` — find VMs by role (worker, lieutenant, etc.)
22
+ - `registry_heartbeat` — keep a registration alive
23
+
24
+ ## Behaviors
25
+
26
+ Auto-registers and auto-heartbeats when running as part of a fleet.
@@ -0,0 +1,12 @@
1
+ # reports
2
+
3
+ Markdown reports. Agents write structured reports — sprint summaries, investigation findings, status updates. Stored with title, author, tags, and timestamp.
4
+
5
+ ## Routes
6
+
7
+ | Method | Path | Description |
8
+ |--------|------|-------------|
9
+ | `POST` | `/reports` | Create a report (`{ title, content, author, tags? }`) |
10
+ | `GET` | `/reports` | List reports |
11
+ | `GET` | `/reports/:id` | Get a report |
12
+ | `DELETE` | `/reports/:id` | Delete a report |
@@ -0,0 +1,22 @@
1
+ # ui
2
+
3
+ Web dashboard with magic link auth, dynamic panel discovery, and chat interface. Mounts at root — serves `/ui/*` and `/auth/*`.
4
+
5
+ The UI discovers panels from other services at runtime. Any service that serves `GET /_panel` gets a tab in the dashboard. The chat tab connects to the agent service via SSE for streaming conversations with pi.
6
+
7
+ ## Routes
8
+
9
+ | Method | Path | Description |
10
+ |--------|------|-------------|
11
+ | `POST` | `/auth/magic-link` | Generate a one-time login URL |
12
+ | `GET` | `/ui/login` | Magic link landing page (sets session cookie) |
13
+ | `GET` | `/ui/` | Dashboard shell |
14
+ | `GET` | `/ui/static/:file` | Static assets (JS, CSS) |
15
+ | `ALL` | `/ui/api/*` | Auth proxy — injects bearer token so the browser never needs it |
16
+
17
+ ## How it works
18
+
19
+ 1. **Auth**: `POST /auth/magic-link` with bearer token → returns a URL. Opening it sets a 24h session cookie.
20
+ 2. **Panel discovery**: The dashboard polls `GET /services` every 30s, fetches `GET /<service>/_panel` for each, and injects the HTML as tabs.
21
+ 3. **Chat**: Creates a pi RPC session via the agent service, streams responses via SSE, renders markdown with collapsible tool calls.
22
+ 4. **API proxy**: All requests to `/ui/api/*` are forwarded with the bearer token from the session, so panel scripts and chat can call any service endpoint without exposing the token to the browser.
@@ -0,0 +1,25 @@
1
+ # usage
2
+
3
+ Cost and token tracking for agent fleets. Records per-session token usage, cost breakdowns, and VM lifecycle events. Provides summaries by agent and time range.
4
+
5
+ Depends on `feed` — publishes `agent_stopped` events when sessions end.
6
+
7
+ ## Routes
8
+
9
+ | Method | Path | Description |
10
+ |--------|------|-------------|
11
+ | `GET` | `/usage` | Usage summary (filter: `?range=7d`) |
12
+ | `POST` | `/usage/sessions` | Record a session |
13
+ | `GET` | `/usage/sessions` | List sessions (filter: `?agent=`, `?range=`) |
14
+ | `POST` | `/usage/vms` | Record a VM lifecycle event |
15
+ | `GET` | `/usage/vms` | List VM records |
16
+
17
+ ## Tools
18
+
19
+ - `usage_summary` — get cost & token totals by agent and time range
20
+ - `usage_sessions` — list session records with tokens, cost, turns, tool calls
21
+ - `usage_vms` — list VM lifecycle records
22
+
23
+ ## Behaviors
24
+
25
+ Auto-records usage data from feed events.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versdotsh/reef",
3
- "version": "0.1.2",
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
+ });