@revealui/harnesses 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.
@@ -0,0 +1,251 @@
1
+ // src/workboard/workboard-manager.ts
2
+ import { readFileSync, writeFileSync } from "fs";
3
+ import { readFile, writeFile } from "fs/promises";
4
+ import { resolve } from "path";
5
+ var STALE_THRESHOLD_MS = 4 * 60 * 60 * 1e3;
6
+ var WorkboardManager = class {
7
+ constructor(workboardPath) {
8
+ this.workboardPath = workboardPath;
9
+ const resolved = resolve(workboardPath);
10
+ if (!resolved.endsWith(".md")) {
11
+ throw new Error(`Invalid workboard path: must be a .md file`);
12
+ }
13
+ }
14
+ /** Read and parse the workboard. */
15
+ read() {
16
+ let content;
17
+ try {
18
+ content = readFileSync(this.workboardPath, "utf8");
19
+ } catch {
20
+ return { sessions: [], recent: [], plans: "", context: "", planReference: "" };
21
+ }
22
+ return parseWorkboard(content);
23
+ }
24
+ /** Write a workboard state back to disk. */
25
+ write(state) {
26
+ writeFileSync(this.workboardPath, serializeWorkboard(state), "utf8");
27
+ }
28
+ /** Read and parse the workboard asynchronously. */
29
+ async readAsync() {
30
+ try {
31
+ const content = await readFile(this.workboardPath, "utf8");
32
+ return parseWorkboard(content);
33
+ } catch {
34
+ return { sessions: [], recent: [], plans: "", context: "", planReference: "" };
35
+ }
36
+ }
37
+ /** Write a workboard state back to disk asynchronously. */
38
+ async writeAsync(state) {
39
+ await writeFile(this.workboardPath, serializeWorkboard(state), "utf8");
40
+ }
41
+ /** Register a new session, replacing any existing row with the same id. */
42
+ registerSession(session) {
43
+ const state = this.read();
44
+ const existing = state.sessions.findIndex((s) => s.id === session.id);
45
+ if (existing >= 0) {
46
+ state.sessions[existing] = session;
47
+ } else {
48
+ state.sessions.push(session);
49
+ }
50
+ this.write(state);
51
+ }
52
+ /** Remove a session row by id. */
53
+ unregisterSession(id) {
54
+ const state = this.read();
55
+ state.sessions = state.sessions.filter((s) => s.id !== id);
56
+ this.write(state);
57
+ }
58
+ /** Update specific fields of an existing session row. */
59
+ updateSession(id, updates) {
60
+ const state = this.read();
61
+ const idx = state.sessions.findIndex((s) => s.id === id);
62
+ if (idx < 0) return;
63
+ const current = state.sessions[idx];
64
+ if (!current) return;
65
+ state.sessions[idx] = { ...current, ...updates };
66
+ this.write(state);
67
+ }
68
+ /** Update a session's files list and timestamp. */
69
+ claimFiles(id, files) {
70
+ this.updateSession(id, {
71
+ files: files.join(", "),
72
+ updated: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`
73
+ });
74
+ }
75
+ /** Clear a session's file reservations. */
76
+ releaseFiles(id) {
77
+ this.updateSession(id, {
78
+ files: "",
79
+ updated: `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}Z`
80
+ });
81
+ }
82
+ /** Prepend a timestamped entry to the ## Recent section (keeps last 20). */
83
+ addRecentEntry(entry) {
84
+ const state = this.read();
85
+ const formatted = `[${entry.timestamp}] ${entry.sessionId}: ${entry.description}`;
86
+ state.recent.unshift({ ...entry });
87
+ if (state.recent.length > 20) state.recent.splice(20);
88
+ void formatted;
89
+ this.write(state);
90
+ }
91
+ /** Returns sessions whose `updated` timestamp is older than 4 hours. */
92
+ detectStale() {
93
+ const state = this.read();
94
+ const now = Date.now();
95
+ return state.sessions.filter((s) => {
96
+ try {
97
+ return now - new Date(s.updated).getTime() > STALE_THRESHOLD_MS;
98
+ } catch {
99
+ return false;
100
+ }
101
+ });
102
+ }
103
+ /**
104
+ * Check whether the given files conflict with any other active session's reservations.
105
+ * Returns a ConflictResult describing any overlaps found.
106
+ */
107
+ checkConflicts(mySessionId, files) {
108
+ const state = this.read();
109
+ const conflicts = [];
110
+ for (const session of state.sessions) {
111
+ if (session.id === mySessionId) continue;
112
+ const theirFiles = session.files.split(",").map((f) => f.trim()).filter(Boolean);
113
+ const overlapping = files.filter(
114
+ (myFile) => theirFiles.some(
115
+ (theirFile) => myFile === theirFile || myFile.startsWith(theirFile.replace("**", "")) || theirFile.startsWith(myFile.replace("**", ""))
116
+ )
117
+ );
118
+ if (overlapping.length > 0) {
119
+ conflicts.push({
120
+ thisSession: mySessionId,
121
+ otherSession: session.id,
122
+ overlappingFiles: overlapping
123
+ });
124
+ }
125
+ }
126
+ return { clean: conflicts.length === 0, conflicts };
127
+ }
128
+ };
129
+ function parseWorkboard(content) {
130
+ const state = {
131
+ sessions: [],
132
+ recent: [],
133
+ plans: "",
134
+ context: "",
135
+ planReference: ""
136
+ };
137
+ const sectionRe = /^## (.+)$/gm;
138
+ const sections = [];
139
+ for (const m of content.matchAll(sectionRe)) {
140
+ sections.push({ title: (m[1] ?? "").trim(), start: m.index });
141
+ }
142
+ for (let i = 0; i < sections.length; i++) {
143
+ const section = sections[i];
144
+ if (!section) continue;
145
+ const { title, start } = section;
146
+ const nextSection = sections[i + 1];
147
+ const end = nextSection !== void 0 ? nextSection.start : content.length;
148
+ const body = content.slice(start, end);
149
+ if (title === "Sessions") {
150
+ state.sessions = parseSessionsTable(body);
151
+ } else if (title === "Recent") {
152
+ state.recent = parseRecentList(body);
153
+ } else if (title === "Plans") {
154
+ state.plans = body.replace(/^## Plans\n/, "");
155
+ } else if (title === "Context") {
156
+ state.context = body.replace(/^## Context\n/, "");
157
+ } else if (title === "Plan Reference") {
158
+ state.planReference = body.replace(/^## Plan Reference\n/, "");
159
+ }
160
+ }
161
+ return state;
162
+ }
163
+ function parseSessionsTable(section) {
164
+ const sessions = [];
165
+ const lines = section.split("\n");
166
+ for (const line of lines) {
167
+ if (!line.startsWith("|")) continue;
168
+ const parts = line.split("|").slice(1, -1).map((c) => c.trim());
169
+ if (parts.length < 6) continue;
170
+ if (parts[0] === "id" || /^-+$/.test(parts[0] ?? "")) continue;
171
+ sessions.push({
172
+ id: parts[0] ?? "",
173
+ env: parts[1] ?? "",
174
+ started: parts[2] ?? "",
175
+ task: parts[3] ?? "",
176
+ files: parts[4] ?? "",
177
+ updated: parts[5] ?? ""
178
+ });
179
+ }
180
+ return sessions;
181
+ }
182
+ function parseRecentList(section) {
183
+ const entries = [];
184
+ const lineRe = /^- \[(\d{4}-\d{2}-\d{2} \d{2}:\d{2})\] ([\w-]+): (.+)$/;
185
+ for (const line of section.split("\n")) {
186
+ const m = lineRe.exec(line);
187
+ if (!m) continue;
188
+ entries.push({ timestamp: m[1] ?? "", sessionId: m[2] ?? "", description: m[3] ?? "" });
189
+ }
190
+ return entries;
191
+ }
192
+ function serializeWorkboard(state) {
193
+ const lines = ["# Workboard", ""];
194
+ lines.push("## Sessions", "");
195
+ lines.push("| id | env | started | task | files | updated |");
196
+ lines.push("| --- | --- | ------- | ---- | ----- | ------- |");
197
+ for (const s of state.sessions) {
198
+ lines.push(`| ${s.id} | ${s.env} | ${s.started} | ${s.task} | ${s.files} | ${s.updated} |`);
199
+ }
200
+ lines.push("");
201
+ lines.push("## Plans");
202
+ lines.push(state.plans.trimEnd());
203
+ lines.push("");
204
+ lines.push("## Recent", "");
205
+ for (const e of state.recent) {
206
+ lines.push(`- [${e.timestamp}] ${e.sessionId}: ${e.description}`);
207
+ }
208
+ lines.push("");
209
+ lines.push("## Context");
210
+ lines.push(state.context.trimEnd());
211
+ lines.push("");
212
+ lines.push("## Plan Reference");
213
+ lines.push(state.planReference.trimEnd());
214
+ lines.push("");
215
+ return lines.join("\n");
216
+ }
217
+
218
+ // src/workboard/session-identity.ts
219
+ import { readFileSync as readFileSync2 } from "fs";
220
+ function detectSessionType() {
221
+ try {
222
+ let pid = process.ppid;
223
+ for (let depth = 0; depth < 8; depth++) {
224
+ if (!pid || pid <= 1) break;
225
+ const cmdline = readFileSync2(`/proc/${pid}/cmdline`, "utf8").replace(/\0/g, " ").toLowerCase();
226
+ if (cmdline.includes("zed")) return "zed";
227
+ if (cmdline.includes("cursor")) return "cursor";
228
+ const status = readFileSync2(`/proc/${pid}/status`, "utf8");
229
+ const m = status.match(/PPid:\s+(\d+)/);
230
+ if (!m) break;
231
+ pid = parseInt(m[1] ?? "0", 10);
232
+ }
233
+ } catch {
234
+ }
235
+ const termProgram = (process.env.TERM_PROGRAM ?? "").toLowerCase();
236
+ if (termProgram.includes("zed")) return "zed";
237
+ if (termProgram.includes("cursor")) return "cursor";
238
+ return "terminal";
239
+ }
240
+ function deriveSessionId(type, existingIds) {
241
+ const matching = existingIds.filter((id) => id.startsWith(`${type}-`)).map((id) => parseInt(id.split("-")[1] ?? "0", 10)).filter((n) => !Number.isNaN(n));
242
+ const maxN = matching.length > 0 ? Math.max(...matching) : 0;
243
+ return `${type}-${maxN + 1}`;
244
+ }
245
+
246
+ export {
247
+ WorkboardManager,
248
+ detectSessionType,
249
+ deriveSessionId
250
+ };
251
+ //# sourceMappingURL=chunk-PG4RAOWS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/workboard/workboard-manager.ts","../src/workboard/session-identity.ts"],"sourcesContent":["import { readFileSync, writeFileSync } from 'node:fs'\nimport { readFile, writeFile } from 'node:fs/promises'\nimport { resolve } from 'node:path'\nimport type {\n ConflictResult,\n WorkboardEntry,\n WorkboardSession,\n WorkboardState,\n} from './workboard-protocol.js'\n\nconst STALE_THRESHOLD_MS = 4 * 60 * 60 * 1000 // 4 hours\n\n/**\n * WorkboardManager — reads, parses, and writes .claude/workboard.md.\n *\n * The workboard is a markdown file with a specific structure:\n * ## Sessions — markdown table\n * ## Plans — freeform markdown\n * ## Recent — bullet list\n * ## Context — freeform markdown\n * ## Plan Reference — freeform markdown\n *\n * This class provides programmatic access to the Sessions table and Recent list.\n * Plans, Context, and Plan Reference are treated as opaque strings.\n */\nexport class WorkboardManager {\n constructor(private readonly workboardPath: string) {\n const resolved = resolve(workboardPath)\n // Workboard paths must be markdown files. Resolving before checking\n // means path traversal sequences (../../etc) have already been collapsed.\n if (!resolved.endsWith('.md')) {\n throw new Error(`Invalid workboard path: must be a .md file`)\n }\n }\n\n /** Read and parse the workboard. */\n read(): WorkboardState {\n let content: string\n try {\n content = readFileSync(this.workboardPath, 'utf8')\n } catch {\n return { sessions: [], recent: [], plans: '', context: '', planReference: '' }\n }\n return parseWorkboard(content)\n }\n\n /** Write a workboard state back to disk. */\n write(state: WorkboardState): void {\n writeFileSync(this.workboardPath, serializeWorkboard(state), 'utf8')\n }\n\n /** Read and parse the workboard asynchronously. */\n async readAsync(): Promise<WorkboardState> {\n try {\n const content = await readFile(this.workboardPath, 'utf8')\n return parseWorkboard(content)\n } catch {\n return { sessions: [], recent: [], plans: '', context: '', planReference: '' }\n }\n }\n\n /** Write a workboard state back to disk asynchronously. */\n async writeAsync(state: WorkboardState): Promise<void> {\n await writeFile(this.workboardPath, serializeWorkboard(state), 'utf8')\n }\n\n /** Register a new session, replacing any existing row with the same id. */\n registerSession(session: WorkboardSession): void {\n const state = this.read()\n const existing = state.sessions.findIndex((s) => s.id === session.id)\n if (existing >= 0) {\n state.sessions[existing] = session\n } else {\n state.sessions.push(session)\n }\n this.write(state)\n }\n\n /** Remove a session row by id. */\n unregisterSession(id: string): void {\n const state = this.read()\n state.sessions = state.sessions.filter((s) => s.id !== id)\n this.write(state)\n }\n\n /** Update specific fields of an existing session row. */\n updateSession(id: string, updates: Partial<WorkboardSession>): void {\n const state = this.read()\n const idx = state.sessions.findIndex((s) => s.id === id)\n if (idx < 0) return\n const current = state.sessions[idx]\n if (!current) return\n state.sessions[idx] = { ...current, ...updates }\n this.write(state)\n }\n\n /** Update a session's files list and timestamp. */\n claimFiles(id: string, files: string[]): void {\n this.updateSession(id, {\n files: files.join(', '),\n updated: `${new Date().toISOString().slice(0, 16)}Z`,\n })\n }\n\n /** Clear a session's file reservations. */\n releaseFiles(id: string): void {\n this.updateSession(id, {\n files: '',\n updated: `${new Date().toISOString().slice(0, 16)}Z`,\n })\n }\n\n /** Prepend a timestamped entry to the ## Recent section (keeps last 20). */\n addRecentEntry(entry: WorkboardEntry): void {\n const state = this.read()\n const formatted = `[${entry.timestamp}] ${entry.sessionId}: ${entry.description}`\n state.recent.unshift({ ...entry })\n if (state.recent.length > 20) state.recent.splice(20)\n // Also reformat for serialization\n void formatted\n this.write(state)\n }\n\n /** Returns sessions whose `updated` timestamp is older than 4 hours. */\n detectStale(): WorkboardSession[] {\n const state = this.read()\n const now = Date.now()\n return state.sessions.filter((s) => {\n try {\n return now - new Date(s.updated).getTime() > STALE_THRESHOLD_MS\n } catch {\n return false\n }\n })\n }\n\n /**\n * Check whether the given files conflict with any other active session's reservations.\n * Returns a ConflictResult describing any overlaps found.\n */\n checkConflicts(mySessionId: string, files: string[]): ConflictResult {\n const state = this.read()\n const conflicts: ConflictResult['conflicts'] = []\n\n for (const session of state.sessions) {\n if (session.id === mySessionId) continue\n const theirFiles = session.files\n .split(',')\n .map((f) => f.trim())\n .filter(Boolean)\n\n const overlapping = files.filter((myFile) =>\n theirFiles.some(\n (theirFile) =>\n myFile === theirFile ||\n myFile.startsWith(theirFile.replace('**', '')) ||\n theirFile.startsWith(myFile.replace('**', '')),\n ),\n )\n\n if (overlapping.length > 0) {\n conflicts.push({\n thisSession: mySessionId,\n otherSession: session.id,\n overlappingFiles: overlapping,\n })\n }\n }\n\n return { clean: conflicts.length === 0, conflicts }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal parser / serializer\n// ---------------------------------------------------------------------------\n\nfunction parseWorkboard(content: string): WorkboardState {\n const state: WorkboardState = {\n sessions: [],\n recent: [],\n plans: '',\n context: '',\n planReference: '',\n }\n\n // Split into top-level sections at ## headings.\n const sectionRe = /^## (.+)$/gm\n const sections: Array<{ title: string; start: number }> = []\n for (const m of content.matchAll(sectionRe)) {\n sections.push({ title: (m[1] ?? '').trim(), start: m.index })\n }\n\n for (let i = 0; i < sections.length; i++) {\n const section = sections[i]\n if (!section) continue\n const { title, start } = section\n const nextSection = sections[i + 1]\n const end = nextSection !== undefined ? nextSection.start : content.length\n const body = content.slice(start, end)\n\n if (title === 'Sessions') {\n state.sessions = parseSessionsTable(body)\n } else if (title === 'Recent') {\n state.recent = parseRecentList(body)\n } else if (title === 'Plans') {\n state.plans = body.replace(/^## Plans\\n/, '')\n } else if (title === 'Context') {\n state.context = body.replace(/^## Context\\n/, '')\n } else if (title === 'Plan Reference') {\n state.planReference = body.replace(/^## Plan Reference\\n/, '')\n }\n }\n\n return state\n}\n\nfunction parseSessionsTable(section: string): WorkboardSession[] {\n const sessions: WorkboardSession[] = []\n const lines = section.split('\\n')\n for (const line of lines) {\n if (!line.startsWith('|')) continue\n const parts = line\n .split('|')\n .slice(1, -1)\n .map((c) => c.trim())\n if (parts.length < 6) continue\n // Skip header and separator rows\n if (parts[0] === 'id' || /^-+$/.test(parts[0] ?? '')) continue\n sessions.push({\n id: parts[0] ?? '',\n env: parts[1] ?? '',\n started: parts[2] ?? '',\n task: parts[3] ?? '',\n files: parts[4] ?? '',\n updated: parts[5] ?? '',\n })\n }\n return sessions\n}\n\nfunction parseRecentList(section: string): WorkboardEntry[] {\n const entries: WorkboardEntry[] = []\n const lineRe = /^- \\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2})\\] ([\\w-]+): (.+)$/\n for (const line of section.split('\\n')) {\n const m = lineRe.exec(line)\n if (!m) continue\n entries.push({ timestamp: m[1] ?? '', sessionId: m[2] ?? '', description: m[3] ?? '' })\n }\n return entries\n}\n\nfunction serializeWorkboard(state: WorkboardState): string {\n const lines: string[] = ['# Workboard', '']\n\n // Sessions table\n lines.push('## Sessions', '')\n lines.push('| id | env | started | task | files | updated |')\n lines.push('| --- | --- | ------- | ---- | ----- | ------- |')\n for (const s of state.sessions) {\n lines.push(`| ${s.id} | ${s.env} | ${s.started} | ${s.task} | ${s.files} | ${s.updated} |`)\n }\n lines.push('')\n\n // Plans\n lines.push('## Plans')\n lines.push(state.plans.trimEnd())\n lines.push('')\n\n // Recent\n lines.push('## Recent', '')\n for (const e of state.recent) {\n lines.push(`- [${e.timestamp}] ${e.sessionId}: ${e.description}`)\n }\n lines.push('')\n\n // Context\n lines.push('## Context')\n lines.push(state.context.trimEnd())\n lines.push('')\n\n // Plan Reference\n lines.push('## Plan Reference')\n lines.push(state.planReference.trimEnd())\n lines.push('')\n\n return lines.join('\\n')\n}\n","import { readFileSync } from 'node:fs'\n\n/** Type of session detected from the runtime environment. */\nexport type SessionType = 'zed' | 'cursor' | 'terminal'\n\n/**\n * Detects whether the current process is running inside an AI tool (Zed, Cursor)\n * or a plain terminal session by walking the parent process chain.\n *\n * Uses /proc/<pid>/cmdline on Linux/WSL. Falls back to TERM_PROGRAM env var.\n */\nexport function detectSessionType(): SessionType {\n // Walk parent process chain looking for known AI tool process names.\n try {\n let pid = process.ppid\n for (let depth = 0; depth < 8; depth++) {\n if (!pid || pid <= 1) break\n const cmdline = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\\0/g, ' ').toLowerCase()\n if (cmdline.includes('zed')) return 'zed'\n if (cmdline.includes('cursor')) return 'cursor'\n const status = readFileSync(`/proc/${pid}/status`, 'utf8')\n const m = status.match(/PPid:\\s+(\\d+)/)\n if (!m) break\n pid = parseInt(m[1] ?? '0', 10)\n }\n } catch {\n // /proc not available (macOS, Windows non-WSL).\n }\n\n // Fallback: TERM_PROGRAM env var (set by some terminal emulators and IDEs).\n const termProgram = (process.env.TERM_PROGRAM ?? '').toLowerCase()\n if (termProgram.includes('zed')) return 'zed'\n if (termProgram.includes('cursor')) return 'cursor'\n\n return 'terminal'\n}\n\n/**\n * Derives a session ID (e.g. \"zed-1\", \"terminal-2\") given a type and a list\n * of existing session IDs already in the workboard.\n *\n * Picks the next available numeric suffix to avoid collisions.\n */\nexport function deriveSessionId(type: SessionType, existingIds: string[]): string {\n const matching = existingIds\n .filter((id) => id.startsWith(`${type}-`))\n .map((id) => parseInt(id.split('-')[1] ?? '0', 10))\n .filter((n) => !Number.isNaN(n))\n\n const maxN = matching.length > 0 ? Math.max(...matching) : 0\n return `${type}-${maxN + 1}`\n}\n"],"mappings":"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","names":["readFileSync"]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ HarnessCoordinator
4
+ } from "./chunk-BDA7D725.js";
5
+ import {
6
+ WorkboardManager
7
+ } from "./chunk-PG4RAOWS.js";
8
+
9
+ // src/cli.ts
10
+ import { createConnection } from "net";
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+ import { isFeatureEnabled } from "@revealui/core/features";
14
+ import { initializeLicense } from "@revealui/core/license";
15
+ async function checkLicense() {
16
+ await initializeLicense();
17
+ return isFeatureEnabled("harnesses");
18
+ }
19
+ var DEFAULT_SOCKET = join(homedir(), ".local", "share", "revealui", "harness.sock");
20
+ var DEFAULT_PROJECT = process.cwd();
21
+ var [, , command, ...args] = process.argv;
22
+ async function rpcCall(method, params = {}) {
23
+ return new Promise((resolve, reject) => {
24
+ const socket = createConnection(DEFAULT_SOCKET);
25
+ let buffer = "";
26
+ socket.on("connect", () => {
27
+ const req = JSON.stringify({ jsonrpc: "2.0", id: 1, method, params });
28
+ socket.write(`${req}
29
+ `);
30
+ });
31
+ socket.on("data", (chunk) => {
32
+ buffer += chunk.toString();
33
+ const lines = buffer.split("\n");
34
+ buffer = lines.pop() ?? "";
35
+ for (const line of lines) {
36
+ if (!line.trim()) continue;
37
+ try {
38
+ const resp = JSON.parse(line);
39
+ socket.destroy();
40
+ if (resp.error) reject(new Error(resp.error.message));
41
+ else resolve(resp.result);
42
+ } catch {
43
+ reject(new Error(`Invalid JSON: ${line}`));
44
+ }
45
+ }
46
+ });
47
+ socket.on("error", reject);
48
+ setTimeout(() => {
49
+ socket.destroy();
50
+ reject(new Error("RPC timeout"));
51
+ }, 5e3);
52
+ });
53
+ }
54
+ async function main() {
55
+ switch (command) {
56
+ case "start": {
57
+ const licensed = await checkLicense();
58
+ if (!licensed) {
59
+ process.stderr.write(
60
+ "\u26A0 @revealui/harnesses requires a Pro license. Visit https://revealui.com/pricing\n"
61
+ );
62
+ process.exit(2);
63
+ }
64
+ const projectIdx = args.indexOf("--project");
65
+ const projectRoot = projectIdx >= 0 ? args[projectIdx + 1] ?? DEFAULT_PROJECT : DEFAULT_PROJECT;
66
+ const coordinator = new HarnessCoordinator({
67
+ projectRoot,
68
+ task: "Harness coordination active"
69
+ });
70
+ await coordinator.start();
71
+ const ids = await coordinator.getRegistry().listAvailable();
72
+ process.stdout.write(`\u2713 Detected harnesses: ${ids.length > 0 ? ids.join(", ") : "none"}
73
+ `);
74
+ process.stdout.write(`\u2713 RPC server listening on ${DEFAULT_SOCKET}
75
+ `);
76
+ process.stdout.write(`\u2713 Session registered in workboard
77
+ `);
78
+ const shutdown = async () => {
79
+ await coordinator.stop();
80
+ process.exit(0);
81
+ };
82
+ process.on("SIGINT", shutdown);
83
+ process.on("SIGTERM", shutdown);
84
+ break;
85
+ }
86
+ case "status": {
87
+ try {
88
+ const infos = await rpcCall("harness.list");
89
+ if (infos.length === 0) {
90
+ process.stdout.write("No harnesses available\n");
91
+ } else {
92
+ for (const info of infos) {
93
+ process.stdout.write(
94
+ `${info.id} ${info.name}${info.version ? ` ${info.version}` : ""}
95
+ `
96
+ );
97
+ }
98
+ }
99
+ } catch (err) {
100
+ process.stderr.write(`RPC error: ${err instanceof Error ? err.message : String(err)}
101
+ `);
102
+ process.exit(1);
103
+ }
104
+ break;
105
+ }
106
+ case "list": {
107
+ try {
108
+ const infos = await rpcCall("harness.list");
109
+ for (const info of infos) {
110
+ process.stdout.write(`${info.id} ${info.name}
111
+ `);
112
+ }
113
+ } catch (err) {
114
+ process.stderr.write(`RPC error: ${err instanceof Error ? err.message : String(err)}
115
+ `);
116
+ process.exit(1);
117
+ }
118
+ break;
119
+ }
120
+ case "sync": {
121
+ const [harnessId, direction] = args;
122
+ if (!(harnessId && direction && ["push", "pull"].includes(direction))) {
123
+ process.stderr.write("Usage: revealui-harnesses sync <harnessId> <push|pull>\n");
124
+ process.exit(1);
125
+ }
126
+ try {
127
+ const result = await rpcCall("harness.syncConfig", { harnessId, direction });
128
+ process.stdout.write(result.success ? `\u2713 ${result.message}
129
+ ` : `\u2717 ${result.message}
130
+ `);
131
+ if (!result.success) process.exit(1);
132
+ } catch (err) {
133
+ process.stderr.write(`RPC error: ${err instanceof Error ? err.message : String(err)}
134
+ `);
135
+ process.exit(1);
136
+ }
137
+ break;
138
+ }
139
+ case "coordinate": {
140
+ if (args.includes("--init")) {
141
+ const licensed = await checkLicense();
142
+ if (!licensed) {
143
+ process.stderr.write(
144
+ "\u26A0 @revealui/harnesses requires a Pro license. Visit https://revealui.com/pricing\n"
145
+ );
146
+ process.exit(2);
147
+ }
148
+ const pathIdx = args.indexOf("--init");
149
+ const projectRoot = args[pathIdx + 1] ?? DEFAULT_PROJECT;
150
+ const coordinator = new HarnessCoordinator({ projectRoot, task: "Coordinate harnesses" });
151
+ await coordinator.start();
152
+ const workboard = coordinator.getWorkboard();
153
+ const conflicts = workboard.checkConflicts("", []);
154
+ process.stdout.write(
155
+ `\u2713 Session registered. Conflicts: ${conflicts.clean ? "none" : conflicts.conflicts.length}
156
+ `
157
+ );
158
+ await coordinator.stop();
159
+ } else {
160
+ const projectRoot = args[args.indexOf("--project") + 1] ?? DEFAULT_PROJECT;
161
+ const workboardPath = join(projectRoot, ".claude", "workboard.md");
162
+ const manager = new WorkboardManager(workboardPath);
163
+ const state = manager.read();
164
+ process.stdout.write(`Sessions (${state.sessions.length}):
165
+ `);
166
+ for (const s of state.sessions) {
167
+ const stale = Date.now() - new Date(s.updated).getTime() > 4 * 60 * 60 * 1e3;
168
+ process.stdout.write(` ${s.id} [${s.env}] \u2014 ${s.task}${stale ? " (STALE)" : ""}
169
+ `);
170
+ if (s.files) process.stdout.write(` files: ${s.files}
171
+ `);
172
+ }
173
+ if (state.sessions.length === 0) process.stdout.write(" (no active sessions)\n");
174
+ }
175
+ break;
176
+ }
177
+ default:
178
+ process.stdout.write(`revealui-harnesses \u2014 AI harness coordination for RevealUI
179
+
180
+ Commands:
181
+ start [--project <path>] Start daemon (detects harnesses, registers session)
182
+ status List available harnesses (requires daemon)
183
+ list List harnesses in TSV format (requires daemon)
184
+ sync <id> <push|pull> Sync harness config to/from SSD (requires daemon)
185
+ coordinate [--project <path>] Print workboard state
186
+ coordinate --init [<path>] Register + start daemon
187
+ `);
188
+ break;
189
+ }
190
+ }
191
+ main().catch((err) => {
192
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
193
+ `);
194
+ process.exit(1);
195
+ });
196
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * revealui-harnesses — CLI daemon and RPC client for AI harness coordination.\n *\n * Commands:\n * start [--project <path>] Detect harnesses, register in workboard, start RPC server\n * status List available harnesses via RPC\n * list List harnesses in TSV format\n * sync <harnessId> <push|pull> Sync harness config to/from SSD\n * coordinate [--print] Print current workboard state\n * coordinate --init <path> Register this session in the workboard and start daemon\n *\n * License: Pro tier required (isFeatureEnabled(\"harnesses\"))\n */\n\nimport { createConnection } from 'node:net'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { isFeatureEnabled } from '@revealui/core/features'\nimport { initializeLicense } from '@revealui/core/license'\nimport { HarnessCoordinator } from './coordinator.js'\nimport { WorkboardManager } from './workboard/workboard-manager.js'\n\nasync function checkLicense(): Promise<boolean> {\n await initializeLicense()\n return isFeatureEnabled('harnesses')\n}\n\nconst DEFAULT_SOCKET = join(homedir(), '.local', 'share', 'revealui', 'harness.sock')\nconst DEFAULT_PROJECT = process.cwd()\n\nconst [, , command, ...args] = process.argv\n\nasync function rpcCall(method: string, params: unknown = {}): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const socket = createConnection(DEFAULT_SOCKET)\n let buffer = ''\n socket.on('connect', () => {\n const req = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })\n socket.write(`${req}\\n`)\n })\n socket.on('data', (chunk) => {\n buffer += chunk.toString()\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n if (!line.trim()) continue\n try {\n const resp = JSON.parse(line) as { result?: unknown; error?: { message: string } }\n socket.destroy()\n if (resp.error) reject(new Error(resp.error.message))\n else resolve(resp.result)\n } catch {\n reject(new Error(`Invalid JSON: ${line}`))\n }\n }\n })\n socket.on('error', reject)\n setTimeout(() => {\n socket.destroy()\n reject(new Error('RPC timeout'))\n }, 5000)\n })\n}\n\nasync function main() {\n switch (command) {\n case 'start': {\n const licensed = await checkLicense()\n if (!licensed) {\n process.stderr.write(\n '⚠ @revealui/harnesses requires a Pro license. Visit https://revealui.com/pricing\\n',\n )\n process.exit(2)\n }\n\n const projectIdx = args.indexOf('--project')\n const projectRoot =\n projectIdx >= 0 ? (args[projectIdx + 1] ?? DEFAULT_PROJECT) : DEFAULT_PROJECT\n\n const coordinator = new HarnessCoordinator({\n projectRoot,\n task: 'Harness coordination active',\n })\n\n await coordinator.start()\n const ids = await coordinator.getRegistry().listAvailable()\n process.stdout.write(`✓ Detected harnesses: ${ids.length > 0 ? ids.join(', ') : 'none'}\\n`)\n process.stdout.write(`✓ RPC server listening on ${DEFAULT_SOCKET}\\n`)\n process.stdout.write(`✓ Session registered in workboard\\n`)\n\n const shutdown = async () => {\n await coordinator.stop()\n process.exit(0)\n }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n break\n }\n\n case 'status': {\n try {\n const infos = (await rpcCall('harness.list')) as Array<{\n id: string\n name: string\n version?: string\n }>\n if (infos.length === 0) {\n process.stdout.write('No harnesses available\\n')\n } else {\n for (const info of infos) {\n process.stdout.write(\n `${info.id}\\t${info.name}${info.version ? `\\t${info.version}` : ''}\\n`,\n )\n }\n }\n } catch (err) {\n process.stderr.write(`RPC error: ${err instanceof Error ? err.message : String(err)}\\n`)\n process.exit(1)\n }\n break\n }\n\n case 'list': {\n try {\n const infos = (await rpcCall('harness.list')) as Array<{ id: string; name: string }>\n for (const info of infos) {\n process.stdout.write(`${info.id}\\t${info.name}\\n`)\n }\n } catch (err) {\n process.stderr.write(`RPC error: ${err instanceof Error ? err.message : String(err)}\\n`)\n process.exit(1)\n }\n break\n }\n\n case 'sync': {\n const [harnessId, direction] = args\n if (!(harnessId && direction && ['push', 'pull'].includes(direction))) {\n process.stderr.write('Usage: revealui-harnesses sync <harnessId> <push|pull>\\n')\n process.exit(1)\n }\n try {\n const result = (await rpcCall('harness.syncConfig', { harnessId, direction })) as {\n success: boolean\n message?: string\n }\n process.stdout.write(result.success ? `✓ ${result.message}\\n` : `✗ ${result.message}\\n`)\n if (!result.success) process.exit(1)\n } catch (err) {\n process.stderr.write(`RPC error: ${err instanceof Error ? err.message : String(err)}\\n`)\n process.exit(1)\n }\n break\n }\n\n case 'coordinate': {\n if (args.includes('--init')) {\n const licensed = await checkLicense()\n if (!licensed) {\n process.stderr.write(\n '⚠ @revealui/harnesses requires a Pro license. Visit https://revealui.com/pricing\\n',\n )\n process.exit(2)\n }\n\n const pathIdx = args.indexOf('--init')\n const projectRoot = args[pathIdx + 1] ?? DEFAULT_PROJECT\n const coordinator = new HarnessCoordinator({ projectRoot, task: 'Coordinate harnesses' })\n await coordinator.start()\n const workboard = coordinator.getWorkboard()\n const conflicts = workboard.checkConflicts('', [])\n process.stdout.write(\n `✓ Session registered. Conflicts: ${conflicts.clean ? 'none' : conflicts.conflicts.length}\\n`,\n )\n await coordinator.stop()\n } else {\n // --print: dump current workboard to stdout\n const projectRoot = args[args.indexOf('--project') + 1] ?? DEFAULT_PROJECT\n const workboardPath = join(projectRoot, '.claude', 'workboard.md')\n const manager = new WorkboardManager(workboardPath)\n const state = manager.read()\n process.stdout.write(`Sessions (${state.sessions.length}):\\n`)\n for (const s of state.sessions) {\n const stale = Date.now() - new Date(s.updated).getTime() > 4 * 60 * 60 * 1000\n process.stdout.write(` ${s.id} [${s.env}] — ${s.task}${stale ? ' (STALE)' : ''}\\n`)\n if (s.files) process.stdout.write(` files: ${s.files}\\n`)\n }\n if (state.sessions.length === 0) process.stdout.write(' (no active sessions)\\n')\n }\n break\n }\n\n default:\n process.stdout.write(`revealui-harnesses — AI harness coordination for RevealUI\n\nCommands:\n start [--project <path>] Start daemon (detects harnesses, registers session)\n status List available harnesses (requires daemon)\n list List harnesses in TSV format (requires daemon)\n sync <id> <push|pull> Sync harness config to/from SSD (requires daemon)\n coordinate [--project <path>] Print workboard state\n coordinate --init [<path>] Register + start daemon\n`)\n break\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`)\n process.exit(1)\n})\n"],"mappings":";;;;;;;;;AAgBA,SAAS,wBAAwB;AACjC,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,wBAAwB;AACjC,SAAS,yBAAyB;AAIlC,eAAe,eAAiC;AAC9C,QAAM,kBAAkB;AACxB,SAAO,iBAAiB,WAAW;AACrC;AAEA,IAAM,iBAAiB,KAAK,QAAQ,GAAG,UAAU,SAAS,YAAY,cAAc;AACpF,IAAM,kBAAkB,QAAQ,IAAI;AAEpC,IAAM,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,IAAI,QAAQ;AAEvC,eAAe,QAAQ,QAAgB,SAAkB,CAAC,GAAqB;AAC7E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAS,iBAAiB,cAAc;AAC9C,QAAI,SAAS;AACb,WAAO,GAAG,WAAW,MAAM;AACzB,YAAM,MAAM,KAAK,UAAU,EAAE,SAAS,OAAO,IAAI,GAAG,QAAQ,OAAO,CAAC;AACpE,aAAO,MAAM,GAAG,GAAG;AAAA,CAAI;AAAA,IACzB,CAAC;AACD,WAAO,GAAG,QAAQ,CAAC,UAAU;AAC3B,gBAAU,MAAM,SAAS;AACzB,YAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,eAAS,MAAM,IAAI,KAAK;AACxB,iBAAW,QAAQ,OAAO;AACxB,YAAI,CAAC,KAAK,KAAK,EAAG;AAClB,YAAI;AACF,gBAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,iBAAO,QAAQ;AACf,cAAI,KAAK,MAAO,QAAO,IAAI,MAAM,KAAK,MAAM,OAAO,CAAC;AAAA,cAC/C,SAAQ,KAAK,MAAM;AAAA,QAC1B,QAAQ;AACN,iBAAO,IAAI,MAAM,iBAAiB,IAAI,EAAE,CAAC;AAAA,QAC3C;AAAA,MACF;AAAA,IACF,CAAC;AACD,WAAO,GAAG,SAAS,MAAM;AACzB,eAAW,MAAM;AACf,aAAO,QAAQ;AACf,aAAO,IAAI,MAAM,aAAa,CAAC;AAAA,IACjC,GAAG,GAAI;AAAA,EACT,CAAC;AACH;AAEA,eAAe,OAAO;AACpB,UAAQ,SAAS;AAAA,IACf,KAAK,SAAS;AACZ,YAAM,WAAW,MAAM,aAAa;AACpC,UAAI,CAAC,UAAU;AACb,gBAAQ,OAAO;AAAA,UACb;AAAA,QACF;AACA,gBAAQ,KAAK,CAAC;AAAA,MAChB;AAEA,YAAM,aAAa,KAAK,QAAQ,WAAW;AAC3C,YAAM,cACJ,cAAc,IAAK,KAAK,aAAa,CAAC,KAAK,kBAAmB;AAEhE,YAAM,cAAc,IAAI,mBAAmB;AAAA,QACzC;AAAA,QACA,MAAM;AAAA,MACR,CAAC;AAED,YAAM,YAAY,MAAM;AACxB,YAAM,MAAM,MAAM,YAAY,YAAY,EAAE,cAAc;AAC1D,cAAQ,OAAO,MAAM,8BAAyB,IAAI,SAAS,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM;AAAA,CAAI;AAC1F,cAAQ,OAAO,MAAM,kCAA6B,cAAc;AAAA,CAAI;AACpE,cAAQ,OAAO,MAAM;AAAA,CAAqC;AAE1D,YAAM,WAAW,YAAY;AAC3B,cAAM,YAAY,KAAK;AACvB,gBAAQ,KAAK,CAAC;AAAA,MAChB;AACA,cAAQ,GAAG,UAAU,QAAQ;AAC7B,cAAQ,GAAG,WAAW,QAAQ;AAC9B;AAAA,IACF;AAAA,IAEA,KAAK,UAAU;AACb,UAAI;AACF,cAAM,QAAS,MAAM,QAAQ,cAAc;AAK3C,YAAI,MAAM,WAAW,GAAG;AACtB,kBAAQ,OAAO,MAAM,0BAA0B;AAAA,QACjD,OAAO;AACL,qBAAW,QAAQ,OAAO;AACxB,oBAAQ,OAAO;AAAA,cACb,GAAG,KAAK,EAAE,IAAK,KAAK,IAAI,GAAG,KAAK,UAAU,IAAK,KAAK,OAAO,KAAK,EAAE;AAAA;AAAA,YACpE;AAAA,UACF;AAAA,QACF;AAAA,MACF,SAAS,KAAK;AACZ,gBAAQ,OAAO,MAAM,cAAc,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AACvF,gBAAQ,KAAK,CAAC;AAAA,MAChB;AACA;AAAA,IACF;AAAA,IAEA,KAAK,QAAQ;AACX,UAAI;AACF,cAAM,QAAS,MAAM,QAAQ,cAAc;AAC3C,mBAAW,QAAQ,OAAO;AACxB,kBAAQ,OAAO,MAAM,GAAG,KAAK,EAAE,IAAK,KAAK,IAAI;AAAA,CAAI;AAAA,QACnD;AAAA,MACF,SAAS,KAAK;AACZ,gBAAQ,OAAO,MAAM,cAAc,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AACvF,gBAAQ,KAAK,CAAC;AAAA,MAChB;AACA;AAAA,IACF;AAAA,IAEA,KAAK,QAAQ;AACX,YAAM,CAAC,WAAW,SAAS,IAAI;AAC/B,UAAI,EAAE,aAAa,aAAa,CAAC,QAAQ,MAAM,EAAE,SAAS,SAAS,IAAI;AACrE,gBAAQ,OAAO,MAAM,0DAA0D;AAC/E,gBAAQ,KAAK,CAAC;AAAA,MAChB;AACA,UAAI;AACF,cAAM,SAAU,MAAM,QAAQ,sBAAsB,EAAE,WAAW,UAAU,CAAC;AAI5E,gBAAQ,OAAO,MAAM,OAAO,UAAU,UAAK,OAAO,OAAO;AAAA,IAAO,UAAK,OAAO,OAAO;AAAA,CAAI;AACvF,YAAI,CAAC,OAAO,QAAS,SAAQ,KAAK,CAAC;AAAA,MACrC,SAAS,KAAK;AACZ,gBAAQ,OAAO,MAAM,cAAc,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AACvF,gBAAQ,KAAK,CAAC;AAAA,MAChB;AACA;AAAA,IACF;AAAA,IAEA,KAAK,cAAc;AACjB,UAAI,KAAK,SAAS,QAAQ,GAAG;AAC3B,cAAM,WAAW,MAAM,aAAa;AACpC,YAAI,CAAC,UAAU;AACb,kBAAQ,OAAO;AAAA,YACb;AAAA,UACF;AACA,kBAAQ,KAAK,CAAC;AAAA,QAChB;AAEA,cAAM,UAAU,KAAK,QAAQ,QAAQ;AACrC,cAAM,cAAc,KAAK,UAAU,CAAC,KAAK;AACzC,cAAM,cAAc,IAAI,mBAAmB,EAAE,aAAa,MAAM,uBAAuB,CAAC;AACxF,cAAM,YAAY,MAAM;AACxB,cAAM,YAAY,YAAY,aAAa;AAC3C,cAAM,YAAY,UAAU,eAAe,IAAI,CAAC,CAAC;AACjD,gBAAQ,OAAO;AAAA,UACb,yCAAoC,UAAU,QAAQ,SAAS,UAAU,UAAU,MAAM;AAAA;AAAA,QAC3F;AACA,cAAM,YAAY,KAAK;AAAA,MACzB,OAAO;AAEL,cAAM,cAAc,KAAK,KAAK,QAAQ,WAAW,IAAI,CAAC,KAAK;AAC3D,cAAM,gBAAgB,KAAK,aAAa,WAAW,cAAc;AACjE,cAAM,UAAU,IAAI,iBAAiB,aAAa;AAClD,cAAM,QAAQ,QAAQ,KAAK;AAC3B,gBAAQ,OAAO,MAAM,aAAa,MAAM,SAAS,MAAM;AAAA,CAAM;AAC7D,mBAAW,KAAK,MAAM,UAAU;AAC9B,gBAAM,QAAQ,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE,OAAO,EAAE,QAAQ,IAAI,IAAI,KAAK,KAAK;AACzE,kBAAQ,OAAO,MAAM,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,YAAO,EAAE,IAAI,GAAG,QAAQ,aAAa,EAAE;AAAA,CAAI;AACnF,cAAI,EAAE,MAAO,SAAQ,OAAO,MAAM,cAAc,EAAE,KAAK;AAAA,CAAI;AAAA,QAC7D;AACA,YAAI,MAAM,SAAS,WAAW,EAAG,SAAQ,OAAO,MAAM,0BAA0B;AAAA,MAClF;AACA;AAAA,IACF;AAAA,IAEA;AACE,cAAQ,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAS1B;AACK;AAAA,EACJ;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,OAAO,MAAM,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AAC5E,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}