wmdev 0.2.1 → 0.2.2
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/README.md +6 -17
- package/backend/dist/server.js +8657 -0
- package/bin/wmdev.js +1 -1
- package/package.json +4 -5
- package/backend/src/config.ts +0 -103
- package/backend/src/docker.ts +0 -432
- package/backend/src/env.ts +0 -95
- package/backend/src/http.ts +0 -10
- package/backend/src/lib/log.ts +0 -14
- package/backend/src/pr.ts +0 -333
- package/backend/src/rpc-secret.ts +0 -21
- package/backend/src/rpc.ts +0 -96
- package/backend/src/server.ts +0 -471
- package/backend/src/terminal.ts +0 -310
- package/backend/src/workmux.ts +0 -474
- package/backend/tsconfig.json +0 -15
package/backend/src/pr.ts
DELETED
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
import { readEnvLocal, writeEnvLocal } from "./env";
|
|
2
|
-
import type { LinkedRepoConfig } from "./config";
|
|
3
|
-
import { log } from "./lib/log";
|
|
4
|
-
|
|
5
|
-
const PR_FETCH_LIMIT = 50;
|
|
6
|
-
const GH_TIMEOUT_MS = 15_000;
|
|
7
|
-
|
|
8
|
-
// ── Internal GH API shapes ────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
type GhCheckStatus =
|
|
11
|
-
| "QUEUED"
|
|
12
|
-
| "IN_PROGRESS"
|
|
13
|
-
| "COMPLETED"
|
|
14
|
-
| "WAITING"
|
|
15
|
-
| "REQUESTED"
|
|
16
|
-
| "PENDING";
|
|
17
|
-
|
|
18
|
-
type GhCheckConclusion =
|
|
19
|
-
| "SUCCESS"
|
|
20
|
-
| "FAILURE"
|
|
21
|
-
| "NEUTRAL"
|
|
22
|
-
| "CANCELLED"
|
|
23
|
-
| "SKIPPED"
|
|
24
|
-
| "TIMED_OUT"
|
|
25
|
-
| "ACTION_REQUIRED";
|
|
26
|
-
|
|
27
|
-
export interface PrComment {
|
|
28
|
-
author: string;
|
|
29
|
-
body: string;
|
|
30
|
-
createdAt: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface GhComment {
|
|
34
|
-
author: { login: string };
|
|
35
|
-
body: string;
|
|
36
|
-
createdAt: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface GhCheckEntry {
|
|
40
|
-
conclusion: GhCheckConclusion | null;
|
|
41
|
-
status: GhCheckStatus;
|
|
42
|
-
name: string;
|
|
43
|
-
detailsUrl: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface GhPrEntry {
|
|
47
|
-
number: number;
|
|
48
|
-
headRefName: string;
|
|
49
|
-
state: string;
|
|
50
|
-
statusCheckRollup: GhCheckEntry[] | null;
|
|
51
|
-
url: string;
|
|
52
|
-
comments: GhComment[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ── Public types ──────────────────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
export interface CiCheck {
|
|
58
|
-
name: string;
|
|
59
|
-
status: "pending" | "success" | "failed" | "skipped";
|
|
60
|
-
url: string;
|
|
61
|
-
runId: number | null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface PrEntry {
|
|
65
|
-
repo: string;
|
|
66
|
-
number: number;
|
|
67
|
-
state: "open" | "closed" | "merged";
|
|
68
|
-
url: string;
|
|
69
|
-
ciStatus: "none" | "pending" | "success" | "failed";
|
|
70
|
-
ciChecks: CiCheck[];
|
|
71
|
-
comments: PrComment[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
type FetchPrsResult =
|
|
75
|
-
| { ok: true; data: Map<string, PrEntry> }
|
|
76
|
-
| { ok: false; error: string };
|
|
77
|
-
|
|
78
|
-
// ── Pure helper functions (exported for unit testing) ─────────────────────────
|
|
79
|
-
|
|
80
|
-
/** Summarize CI check status from a statusCheckRollup array. */
|
|
81
|
-
export function summarizeChecks(
|
|
82
|
-
checks: GhCheckEntry[] | null,
|
|
83
|
-
): PrEntry["ciStatus"] {
|
|
84
|
-
if (!checks || checks.length === 0) return "none";
|
|
85
|
-
const allDone = checks.every((c) => c.status === "COMPLETED");
|
|
86
|
-
if (!allDone) return "pending";
|
|
87
|
-
const allPass = checks.every(
|
|
88
|
-
(c) =>
|
|
89
|
-
c.conclusion === "SUCCESS" ||
|
|
90
|
-
c.conclusion === "NEUTRAL" ||
|
|
91
|
-
c.conclusion === "SKIPPED",
|
|
92
|
-
);
|
|
93
|
-
return allPass ? "success" : "failed";
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Parse a GitHub Actions run ID from a details URL. Returns null when not found. */
|
|
97
|
-
export function parseRunId(detailsUrl: string): number | null {
|
|
98
|
-
const match = detailsUrl.match(/\/actions\/runs\/(\d+)/);
|
|
99
|
-
return match ? parseInt(match[1], 10) : null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Derive a typed check status from GH conclusion/status fields. */
|
|
103
|
-
export function deriveCheckStatus(check: GhCheckEntry): CiCheck["status"] {
|
|
104
|
-
if (check.status !== "COMPLETED") return "pending";
|
|
105
|
-
const c = check.conclusion;
|
|
106
|
-
if (c === "SUCCESS" || c === "NEUTRAL") return "success";
|
|
107
|
-
if (c === "SKIPPED") return "skipped";
|
|
108
|
-
return "failed";
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Map raw GH check entries to typed CiCheck array. */
|
|
112
|
-
export function mapChecks(checks: GhCheckEntry[] | null): CiCheck[] {
|
|
113
|
-
if (!checks || checks.length === 0) return [];
|
|
114
|
-
return checks.map((c) => ({
|
|
115
|
-
name: c.name,
|
|
116
|
-
status: deriveCheckStatus(c),
|
|
117
|
-
url: c.detailsUrl,
|
|
118
|
-
runId: parseRunId(c.detailsUrl),
|
|
119
|
-
}));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Parse raw `gh pr list --json` output into a branch → PrEntry map. Throws on invalid JSON. */
|
|
123
|
-
export function parsePrResponse(
|
|
124
|
-
json: string,
|
|
125
|
-
repoLabel?: string,
|
|
126
|
-
): Map<string, PrEntry> {
|
|
127
|
-
const prs = new Map<string, PrEntry>();
|
|
128
|
-
const entries = JSON.parse(json) as GhPrEntry[];
|
|
129
|
-
for (const entry of entries) {
|
|
130
|
-
// If multiple PRs share the same branch in one repo, the first (most recent) wins.
|
|
131
|
-
if (prs.has(entry.headRefName)) continue;
|
|
132
|
-
prs.set(entry.headRefName, {
|
|
133
|
-
repo: repoLabel ?? "",
|
|
134
|
-
number: entry.number,
|
|
135
|
-
state: entry.state.toLowerCase() as PrEntry["state"],
|
|
136
|
-
url: entry.url,
|
|
137
|
-
ciStatus: summarizeChecks(entry.statusCheckRollup),
|
|
138
|
-
ciChecks: mapChecks(entry.statusCheckRollup),
|
|
139
|
-
comments: (entry.comments ?? []).map((c) => ({
|
|
140
|
-
author: c.author?.login ?? "unknown",
|
|
141
|
-
body: c.body ?? "",
|
|
142
|
-
createdAt: c.createdAt ?? "",
|
|
143
|
-
})),
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
return prs;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ── I/O functions ─────────────────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Fetch all open PRs from a repo via `gh` CLI.
|
|
153
|
-
* Returns a Result: on success, a map of branch name → PrEntry; on failure, an error string.
|
|
154
|
-
* Applies a hard timeout so a hung `gh` process never stalls the caller.
|
|
155
|
-
*/
|
|
156
|
-
export async function fetchAllPrs(
|
|
157
|
-
repoSlug?: string,
|
|
158
|
-
repoLabel?: string,
|
|
159
|
-
cwd?: string,
|
|
160
|
-
): Promise<FetchPrsResult> {
|
|
161
|
-
const label = repoSlug ?? "current";
|
|
162
|
-
const args = [
|
|
163
|
-
"gh",
|
|
164
|
-
"pr",
|
|
165
|
-
"list",
|
|
166
|
-
"--state",
|
|
167
|
-
"open",
|
|
168
|
-
"--json",
|
|
169
|
-
"number,headRefName,state,statusCheckRollup,url,comments",
|
|
170
|
-
"--limit",
|
|
171
|
-
String(PR_FETCH_LIMIT),
|
|
172
|
-
];
|
|
173
|
-
if (repoSlug) args.push("--repo", repoSlug);
|
|
174
|
-
|
|
175
|
-
const proc = Bun.spawn(args, {
|
|
176
|
-
stdout: "pipe",
|
|
177
|
-
stderr: "pipe",
|
|
178
|
-
...(cwd ? { cwd } : {}),
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
const timeout = Bun.sleep(GH_TIMEOUT_MS).then(() => {
|
|
182
|
-
proc.kill();
|
|
183
|
-
return "timeout" as const;
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const raceResult = await Promise.race([proc.exited, timeout]);
|
|
187
|
-
if (raceResult === "timeout") {
|
|
188
|
-
return { ok: false, error: `gh pr list timed out for ${label}` };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (raceResult !== 0) {
|
|
192
|
-
const stderr = (await new Response(proc.stderr).text()).trim();
|
|
193
|
-
return {
|
|
194
|
-
ok: false,
|
|
195
|
-
error: `gh pr list failed for ${label} (exit ${raceResult}): ${stderr}`,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
const json = await new Response(proc.stdout).text();
|
|
201
|
-
return { ok: true, data: parsePrResponse(json, repoLabel) };
|
|
202
|
-
} catch (err) {
|
|
203
|
-
return { ok: false, error: `failed to parse gh output for ${label}: ${err}` };
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Fetch the current state of a PR by its URL. Returns null on error. */
|
|
208
|
-
async function fetchPrState(url: string): Promise<PrEntry["state"] | null> {
|
|
209
|
-
const proc = Bun.spawn(["gh", "pr", "view", url, "--json", "state"], {
|
|
210
|
-
stdout: "pipe",
|
|
211
|
-
stderr: "pipe",
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const timeout = Bun.sleep(GH_TIMEOUT_MS).then(() => {
|
|
215
|
-
proc.kill();
|
|
216
|
-
return "timeout" as const;
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const raceResult = await Promise.race([proc.exited, timeout]);
|
|
220
|
-
if (raceResult === "timeout" || raceResult !== 0) return null;
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
const data = JSON.parse(await new Response(proc.stdout).text()) as { state: string };
|
|
224
|
-
return data.state.toLowerCase() as PrEntry["state"];
|
|
225
|
-
} catch {
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** Update PR_DATA for a worktree whose PR is no longer in the open PR list.
|
|
231
|
-
* Fetches the actual current state for any entry still marked "open". */
|
|
232
|
-
async function refreshStalePrData(wtDir: string): Promise<void> {
|
|
233
|
-
const env = await readEnvLocal(wtDir);
|
|
234
|
-
if (!env.PR_DATA) return;
|
|
235
|
-
|
|
236
|
-
let entries: PrEntry[];
|
|
237
|
-
try {
|
|
238
|
-
entries = JSON.parse(env.PR_DATA) as PrEntry[];
|
|
239
|
-
} catch {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (!entries.some((e) => e.state === "open")) return;
|
|
244
|
-
|
|
245
|
-
const updated = await Promise.all(
|
|
246
|
-
entries.map(async (entry) => {
|
|
247
|
-
if (entry.state !== "open") return entry;
|
|
248
|
-
const state = await fetchPrState(entry.url);
|
|
249
|
-
return state ? { ...entry, state } : entry;
|
|
250
|
-
}),
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
await writeEnvLocal(wtDir, { PR_DATA: JSON.stringify(updated) });
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Sync PR status to .env.local for all worktrees that have open PRs. */
|
|
257
|
-
export async function syncPrStatus(
|
|
258
|
-
getWorktreePaths: () => Promise<Map<string, string>>,
|
|
259
|
-
linkedRepos: LinkedRepoConfig[],
|
|
260
|
-
projectDir?: string,
|
|
261
|
-
): Promise<void> {
|
|
262
|
-
// Fetch current repo + all linked repos in parallel.
|
|
263
|
-
const allRepoResults = await Promise.all([
|
|
264
|
-
fetchAllPrs(undefined, undefined, projectDir),
|
|
265
|
-
...linkedRepos.map(({ repo, alias }) => fetchAllPrs(repo, alias, projectDir)),
|
|
266
|
-
]);
|
|
267
|
-
|
|
268
|
-
// Log fetch errors; aggregate successes into branch → PrEntry[].
|
|
269
|
-
const branchPrs = new Map<string, PrEntry[]>();
|
|
270
|
-
for (const result of allRepoResults) {
|
|
271
|
-
if (!result.ok) {
|
|
272
|
-
log.error(`[pr] ${result.error}`);
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
for (const [branch, entry] of result.data) {
|
|
276
|
-
const existing = branchPrs.get(branch) ?? [];
|
|
277
|
-
existing.push(entry);
|
|
278
|
-
branchPrs.set(branch, existing);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const wtPaths = await getWorktreePaths();
|
|
283
|
-
const seen = new Set<string>();
|
|
284
|
-
|
|
285
|
-
for (const [branch, entries] of branchPrs) {
|
|
286
|
-
const wtDir = wtPaths.get(branch);
|
|
287
|
-
if (!wtDir || seen.has(wtDir)) continue;
|
|
288
|
-
seen.add(wtDir);
|
|
289
|
-
|
|
290
|
-
await writeEnvLocal(wtDir, { PR_DATA: JSON.stringify(entries) });
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (seen.size > 0) {
|
|
294
|
-
log.debug(
|
|
295
|
-
`[pr] synced ${seen.size} worktree(s) with PR data from ${allRepoResults.length} repo(s)`,
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// For worktrees not matched by the open-PR sync, refresh any stale "open"
|
|
300
|
-
// entries so merged/closed PRs are reflected in PR_DATA.
|
|
301
|
-
const uniqueDirs = new Set(wtPaths.values());
|
|
302
|
-
const staleRefreshes: Promise<void>[] = [];
|
|
303
|
-
for (const wtDir of uniqueDirs) {
|
|
304
|
-
if (seen.has(wtDir)) continue;
|
|
305
|
-
staleRefreshes.push(refreshStalePrData(wtDir));
|
|
306
|
-
}
|
|
307
|
-
await Promise.all(staleRefreshes);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** Start periodic PR status sync. Returns a cleanup function that stops the monitor. */
|
|
311
|
-
export function startPrMonitor(
|
|
312
|
-
getWorktreePaths: () => Promise<Map<string, string>>,
|
|
313
|
-
linkedRepos: LinkedRepoConfig[],
|
|
314
|
-
projectDir?: string,
|
|
315
|
-
intervalMs: number = 20_000,
|
|
316
|
-
): () => void {
|
|
317
|
-
const run = (): void => {
|
|
318
|
-
syncPrStatus(getWorktreePaths, linkedRepos, projectDir).catch(
|
|
319
|
-
(err: unknown) => {
|
|
320
|
-
log.error(`[pr] sync error: ${err}`);
|
|
321
|
-
},
|
|
322
|
-
);
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
// Run once immediately (non-blocking).
|
|
326
|
-
run();
|
|
327
|
-
|
|
328
|
-
const timer = setInterval(run, intervalMs);
|
|
329
|
-
|
|
330
|
-
return (): void => {
|
|
331
|
-
clearInterval(timer);
|
|
332
|
-
};
|
|
333
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { chmod, mkdir } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
|
|
4
|
-
const SECRET_PATH = `${Bun.env.HOME ?? "/root"}/.config/workmux/rpc-secret`;
|
|
5
|
-
|
|
6
|
-
let cached: string | null = null;
|
|
7
|
-
|
|
8
|
-
export async function loadRpcSecret(): Promise<string> {
|
|
9
|
-
if (cached) return cached;
|
|
10
|
-
const file = Bun.file(SECRET_PATH);
|
|
11
|
-
if (await file.exists()) {
|
|
12
|
-
cached = (await file.text()).trim();
|
|
13
|
-
return cached;
|
|
14
|
-
}
|
|
15
|
-
const secret = crypto.randomUUID();
|
|
16
|
-
await mkdir(dirname(SECRET_PATH), { recursive: true });
|
|
17
|
-
await Bun.write(SECRET_PATH, secret);
|
|
18
|
-
await chmod(SECRET_PATH, 0o600);
|
|
19
|
-
cached = secret;
|
|
20
|
-
return secret;
|
|
21
|
-
}
|
package/backend/src/rpc.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { loadRpcSecret } from "./rpc-secret";
|
|
2
|
-
import { jsonResponse } from "./http";
|
|
3
|
-
|
|
4
|
-
interface RpcRequest {
|
|
5
|
-
command: string;
|
|
6
|
-
args?: string[];
|
|
7
|
-
branch?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type RpcResponse = { ok: true; output: string } | { ok: false; error: string }
|
|
11
|
-
|
|
12
|
-
/** Build env with TMUX set so workmux can resolve agent states outside tmux. */
|
|
13
|
-
function tmuxEnv(): Record<string, string | undefined> {
|
|
14
|
-
if (Bun.env.TMUX) return Bun.env;
|
|
15
|
-
const tmpdir = Bun.env.TMUX_TMPDIR || "/tmp";
|
|
16
|
-
const uid = process.getuid?.() ?? 1000;
|
|
17
|
-
return { ...Bun.env, TMUX: `${tmpdir}/tmux-${uid}/default,0,0` };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Resolve the tmux pane ID for a worktree window (wm-{branch}).
|
|
22
|
-
* Returns the first pane ID, or null if the window doesn't exist.
|
|
23
|
-
*/
|
|
24
|
-
async function resolvePaneId(branch: string): Promise<string | null> {
|
|
25
|
-
const proc = Bun.spawn(
|
|
26
|
-
["tmux", "list-panes", "-a", "-F", "#{window_name}\t#{pane_id}"],
|
|
27
|
-
{ stdout: "pipe", stderr: "pipe", env: tmuxEnv() },
|
|
28
|
-
);
|
|
29
|
-
const [stdout, , exitCode] = await Promise.all([
|
|
30
|
-
new Response(proc.stdout).text(),
|
|
31
|
-
new Response(proc.stderr).text(),
|
|
32
|
-
proc.exited,
|
|
33
|
-
]);
|
|
34
|
-
if (exitCode !== 0) return null;
|
|
35
|
-
|
|
36
|
-
const target = `wm-${branch}`;
|
|
37
|
-
for (const line of stdout.trim().split("\n")) {
|
|
38
|
-
const [windowName, paneId] = line.split("\t");
|
|
39
|
-
if (windowName === target && paneId) return paneId;
|
|
40
|
-
}
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function handleWorkmuxRpc(req: Request): Promise<Response> {
|
|
45
|
-
const secret = await loadRpcSecret();
|
|
46
|
-
const authHeader = req.headers.get("Authorization");
|
|
47
|
-
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
48
|
-
if (token !== secret) {
|
|
49
|
-
return new Response("Unauthorized", { status: 401 });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let raw: RpcRequest;
|
|
53
|
-
try {
|
|
54
|
-
raw = await req.json() as RpcRequest;
|
|
55
|
-
} catch {
|
|
56
|
-
return jsonResponse({ ok: false, error: "Invalid JSON" } satisfies RpcResponse, 400);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const { command, args = [], branch } = raw;
|
|
60
|
-
if (!command) {
|
|
61
|
-
return jsonResponse({ ok: false, error: "Missing command" } satisfies RpcResponse, 400);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
// Build spawn environment. For set-window-status from a container,
|
|
66
|
-
// resolve the tmux pane ID so the workmux binary can target the right window.
|
|
67
|
-
const env = tmuxEnv();
|
|
68
|
-
if (command === "set-window-status" && branch) {
|
|
69
|
-
const paneId = await resolvePaneId(branch);
|
|
70
|
-
if (paneId) {
|
|
71
|
-
env.TMUX_PANE = paneId;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const proc = Bun.spawn(["workmux", command, ...args], {
|
|
76
|
-
stdout: "pipe",
|
|
77
|
-
stderr: "pipe",
|
|
78
|
-
env,
|
|
79
|
-
});
|
|
80
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
81
|
-
new Response(proc.stdout).text(),
|
|
82
|
-
new Response(proc.stderr).text(),
|
|
83
|
-
proc.exited,
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
|
-
if (exitCode !== 0) {
|
|
87
|
-
return jsonResponse({ ok: false, error: stderr.trim() || `exit code ${exitCode}` } satisfies RpcResponse, 422);
|
|
88
|
-
}
|
|
89
|
-
return jsonResponse({ ok: true, output: stdout.trim() } satisfies RpcResponse);
|
|
90
|
-
} catch (err: unknown) {
|
|
91
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
92
|
-
return jsonResponse({ ok: false, error } satisfies RpcResponse, 500);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export type { RpcRequest, RpcResponse };
|