@zuzuucodes/cli 1.2.3 → 1.3.1
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/bin/zuzuu.mjs +8 -2
- package/package.json +1 -1
- package/web-app/dist/index.js +19 -0
- package/web-app/dist/instance-file.js +62 -0
- package/web-app/dist/server.js +65 -3
- package/web-app/dist/sessions.js +42 -8
- package/web-app/dist/zuzuu-api.js +24 -3
- package/web-app/web-dist/assets/{DiffTab-Clz0uEu_.js → DiffTab-CihRJjzf.js} +1 -1
- package/web-app/web-dist/assets/{MonacoFile-DguRe1Rt.js → MonacoFile-DJvpGyW2.js} +1 -1
- package/web-app/web-dist/assets/{cssMode-C4OLzNbC.js → cssMode-R1Bks9TO.js} +1 -1
- package/web-app/web-dist/assets/{dist-DYKlGApw.js → dist-jCnX6g-O.js} +1 -1
- package/web-app/web-dist/assets/{htmlMode-D1O2jo0s.js → htmlMode-Csqnn3yv.js} +1 -1
- package/web-app/web-dist/assets/index-D_MPtALn.css +2 -0
- package/web-app/web-dist/assets/index-Ye54YyTn.js +267 -0
- package/web-app/web-dist/assets/{jsonMode-D8YStjhJ.js → jsonMode-DRBg9jwi.js} +1 -1
- package/web-app/web-dist/assets/{monaco-setup-BBNGrQzm.js → monaco-setup-Dszx738Y.js} +3 -3
- package/web-app/web-dist/assets/{tsMode-B8P6eQAV.js → tsMode-9YOHYiVQ.js} +1 -1
- package/web-app/web-dist/index.html +2 -2
- package/zuzuu/commands/doctor.mjs +10 -0
- package/zuzuu/commands/hook.mjs +24 -1
- package/zuzuu/commands/session.mjs +103 -0
- package/zuzuu/commands/status.mjs +10 -3
- package/zuzuu/commands/web.mjs +113 -8
- package/zuzuu/live/live-store.mjs +7 -0
- package/zuzuu/session-git.mjs +392 -0
- package/web-app/web-dist/assets/index-Cfwhe1gB.js +0 -270
- package/web-app/web-dist/assets/index-RHYMLHDZ.css +0 -2
package/bin/zuzuu.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { code } from '../zuzuu/commands/code.mjs';
|
|
|
32
32
|
import { web } from '../zuzuu/commands/web.mjs';
|
|
33
33
|
import { explain } from '../zuzuu/commands/explain.mjs';
|
|
34
34
|
import { inbox } from '../zuzuu/commands/inbox.mjs';
|
|
35
|
+
import { session } from '../zuzuu/commands/session.mjs';
|
|
35
36
|
|
|
36
37
|
function parseArgs(argv) {
|
|
37
38
|
const a = { _: [] };
|
|
@@ -60,7 +61,9 @@ function help() {
|
|
|
60
61
|
usage: zuzuu <command> [options]
|
|
61
62
|
|
|
62
63
|
code [dir] launch OpenCode as the bundled default host (faculty home + capture + gate + digest)
|
|
63
|
-
web [dir]
|
|
64
|
+
web [dir] [--stop|--status]
|
|
65
|
+
launch the visual workbench (reuses a running one;
|
|
66
|
+
--stop ends it, --status reports it)
|
|
64
67
|
init scaffold the faculty home (.zuzuu/) — git-style, idempotent
|
|
65
68
|
status detected hosts + recorded sessions
|
|
66
69
|
capture [--host NAME] capture a session → .zuzuu/.traces + .zuzuu/sessions.json
|
|
@@ -89,6 +92,8 @@ usage: zuzuu <command> [options]
|
|
|
89
92
|
pin/list/show/roll back faculty generations (lockfiles)
|
|
90
93
|
enable background hooks: invisible live capture + guardrails gate
|
|
91
94
|
disable remove the background hooks
|
|
95
|
+
session [status|merge|continue|discard]
|
|
96
|
+
the invisible session branch (one per agent session)
|
|
92
97
|
eval [--faculty f] rank pending proposals by eval score, highest first
|
|
93
98
|
migrate [--home] one-time migrators: proposal schema · --home moves agent/ → .zuzuu/
|
|
94
99
|
doctor environment + session health (reconciles lost sessions)
|
|
@@ -105,7 +110,7 @@ const args = parseArgs(rest);
|
|
|
105
110
|
|
|
106
111
|
switch (cmd) {
|
|
107
112
|
case 'code': process.exit(code(args)); break;
|
|
108
|
-
case 'web': web(args); break;
|
|
113
|
+
case 'web': await web(args); break;
|
|
109
114
|
case 'init': init(args); break;
|
|
110
115
|
case 'remember': remember(args); break;
|
|
111
116
|
case 'recall': await recall(args); break;
|
|
@@ -122,6 +127,7 @@ switch (cmd) {
|
|
|
122
127
|
case 'enable': enable(args); break;
|
|
123
128
|
case 'disable': disable(args); break;
|
|
124
129
|
case 'hook': runHook(args._[0], { host: args.host, session: args.session }); break;
|
|
130
|
+
case 'session': session(args); break;
|
|
125
131
|
case 'eval': evalCmd(args); break;
|
|
126
132
|
case 'migrate': migrate(args); break;
|
|
127
133
|
case 'generation': generation(args); break;
|
package/package.json
CHANGED
package/web-app/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import crypto from "node:crypto";
|
|
8
8
|
import { WebcodeServer } from "./server.js";
|
|
9
9
|
import { addRecent } from "./config.js";
|
|
10
|
+
import { writeInstanceFile, removeInstanceFile } from "./instance-file.js";
|
|
10
11
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const DEFAULT_PORT = 7770;
|
|
12
13
|
function parseArgs(argv) {
|
|
@@ -147,14 +148,32 @@ async function main() {
|
|
|
147
148
|
console.log(`\n zuzuu-web v${pkg.version}`);
|
|
148
149
|
console.log(` workspace ${root}`);
|
|
149
150
|
console.log(` url ${url}\n`);
|
|
151
|
+
// Singleton contract: record this instance so `zuzuu web` can reuse it
|
|
152
|
+
// instead of spawning a duplicate (never in hosted mode; never fatal).
|
|
153
|
+
if (!hosted) {
|
|
154
|
+
writeInstanceFile({
|
|
155
|
+
root,
|
|
156
|
+
port: boundPort,
|
|
157
|
+
pid: process.pid,
|
|
158
|
+
token,
|
|
159
|
+
startedAt: new Date().toISOString(),
|
|
160
|
+
version: pkg.version,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
150
163
|
if (args.open)
|
|
151
164
|
openBrowser(url);
|
|
152
165
|
});
|
|
153
166
|
const shutdown = () => {
|
|
167
|
+
if (!hosted)
|
|
168
|
+
removeInstanceFile(root);
|
|
154
169
|
server.stop();
|
|
155
170
|
process.exit(0);
|
|
156
171
|
};
|
|
157
172
|
process.on("SIGINT", shutdown);
|
|
158
173
|
process.on("SIGTERM", shutdown);
|
|
174
|
+
process.on("exit", () => {
|
|
175
|
+
if (!hosted)
|
|
176
|
+
removeInstanceFile(root); // best-effort; idempotent after shutdown()
|
|
177
|
+
});
|
|
159
178
|
}
|
|
160
179
|
void main();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Per-workspace daemon instance state — the singleton contract with `zuzuu web`.
|
|
2
|
+
//
|
|
3
|
+
// After a successful listen the daemon writes
|
|
4
|
+
// ~/.webcode/instances/<sha256(realpath-root).slice(0,16)>.json
|
|
5
|
+
// and removes it on clean shutdown. The zuzuu CLI computes the same path for a
|
|
6
|
+
// workspace to discover (and reuse / stop) an already-running daemon instead of
|
|
7
|
+
// spawning a fresh one — so the port + token stay stable across `zuzuu web` runs.
|
|
8
|
+
//
|
|
9
|
+
// Security note: the file contains the auth token. That's acceptable here —
|
|
10
|
+
// it's 0600 in the user's own home directory, and the same token already
|
|
11
|
+
// appears in the daemon's own stdout URL. Hosted mode never writes this file.
|
|
12
|
+
import crypto from "node:crypto";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
export function instancesDir() {
|
|
17
|
+
return path.join(os.homedir(), ".webcode", "instances");
|
|
18
|
+
}
|
|
19
|
+
/** Deterministic per-workspace file path. `root` must already be realpath'd. */
|
|
20
|
+
export function instancePath(root, dir = instancesDir()) {
|
|
21
|
+
const id = crypto.createHash("sha256").update(root).digest("hex").slice(0, 16);
|
|
22
|
+
return path.join(dir, `${id}.json`);
|
|
23
|
+
}
|
|
24
|
+
/** Write the instance file (0600). Never throws — a failed write only costs reuse. */
|
|
25
|
+
export function writeInstanceFile(info, dir = instancesDir()) {
|
|
26
|
+
const file = instancePath(info.root, dir);
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
fs.writeFileSync(file, JSON.stringify(info, null, 2) + "\n", { mode: 0o600 });
|
|
30
|
+
fs.chmodSync(file, 0o600); // mode option only applies on create; enforce on overwrite too
|
|
31
|
+
return file;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
console.warn(`zuzuu-web: could not write instance state (${String(err)}) — continuing without it`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Best-effort read; null on missing/corrupt. */
|
|
39
|
+
export function readInstanceFile(root, dir = instancesDir()) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(fs.readFileSync(instancePath(root, dir), "utf8"));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Best-effort removal on shutdown. Only removes the file if it still belongs
|
|
49
|
+
* to `pid` — if another daemon raced us and overwrote it, leave theirs alone.
|
|
50
|
+
*/
|
|
51
|
+
export function removeInstanceFile(root, pid = process.pid, dir = instancesDir()) {
|
|
52
|
+
const file = instancePath(root, dir);
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
55
|
+
if (typeof parsed.pid === "number" && parsed.pid !== pid)
|
|
56
|
+
return; // not ours anymore
|
|
57
|
+
fs.unlinkSync(file);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* already gone or unreadable — nothing to do */
|
|
61
|
+
}
|
|
62
|
+
}
|
package/web-app/dist/server.js
CHANGED
|
@@ -11,7 +11,7 @@ import { execFile } from "node:child_process";
|
|
|
11
11
|
import { promisify } from "node:util";
|
|
12
12
|
import { SessionManager } from "./sessions.js";
|
|
13
13
|
import { createFsApi } from "./fs-api.js";
|
|
14
|
-
import { createZuzuuApi } from "./zuzuu-api.js";
|
|
14
|
+
import { createZuzuuApi, runZuzuuMut } from "./zuzuu-api.js";
|
|
15
15
|
import { search } from "./search.js";
|
|
16
16
|
import { listFiles } from "./file-list.js";
|
|
17
17
|
import { listWorkflows, saveWorkflow } from "./workflows.js";
|
|
@@ -25,6 +25,8 @@ import { handleFsSocket } from "./ws-fs.js";
|
|
|
25
25
|
import { PathError, resolveSafe, safeJoin } from "./safe-path.js";
|
|
26
26
|
const AUTH_COOKIE = "webcode_auth";
|
|
27
27
|
const COOKIE_MAX_AGE = 30 * 24 * 3600;
|
|
28
|
+
/** Host CLIs an agent/command session may run. Argv-spawned, never a shell. */
|
|
29
|
+
const DEFAULT_COMMAND_ALLOWLIST = ["claude", "gemini", "codex", "pi", "opencode", "zuzuu"];
|
|
28
30
|
const STATIC_MIME = {
|
|
29
31
|
".html": "text/html; charset=utf-8",
|
|
30
32
|
".js": "text/javascript",
|
|
@@ -46,10 +48,12 @@ export class WebcodeServer {
|
|
|
46
48
|
authSessions = new Set();
|
|
47
49
|
allowedHosts;
|
|
48
50
|
allowedOrigins;
|
|
51
|
+
commandAllowlist;
|
|
49
52
|
server = null;
|
|
50
53
|
constructor(cfg) {
|
|
51
54
|
this.cfg = cfg;
|
|
52
55
|
this.root = cfg.root;
|
|
56
|
+
this.commandAllowlist = new Set(cfg.commandAllowlist ?? DEFAULT_COMMAND_ALLOWLIST);
|
|
53
57
|
this.sessions = new SessionManager(cfg.root);
|
|
54
58
|
const hostNames = ["127.0.0.1", "localhost", "[::1]"];
|
|
55
59
|
this.allowedHosts = new Set(hostNames.flatMap((h) => [h, `${h}:${cfg.port}`]));
|
|
@@ -81,6 +85,22 @@ export class WebcodeServer {
|
|
|
81
85
|
this.root = resolved;
|
|
82
86
|
await config.addRecent(resolved);
|
|
83
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Agent PTY exited → squash-merge its invisible session branch back to
|
|
90
|
+
* main via the zuzuu CLI. Runs in the session's cwd (the repo the agent
|
|
91
|
+
* worked in). CLI-only, like every zuzuu mutation; absent CLI is recorded,
|
|
92
|
+
* never fatal. Session.runCloseHook guarantees this runs once per session.
|
|
93
|
+
*/
|
|
94
|
+
async closeAgentSession(session) {
|
|
95
|
+
const r = await runZuzuuMut(session.cwd, ["session", "merge"], {
|
|
96
|
+
binary: this.cfg.zuzuuBinary,
|
|
97
|
+
});
|
|
98
|
+
if (r.ok)
|
|
99
|
+
return { ok: true, merge: r.data };
|
|
100
|
+
if (r.code === "absent")
|
|
101
|
+
return { cliAbsent: true };
|
|
102
|
+
return { ok: false, ...(r.stderr !== undefined ? { stderr: r.stderr } : {}), ...(r.data !== undefined ? { refusal: r.data } : {}) };
|
|
103
|
+
}
|
|
84
104
|
// ── security gates ─────────────────────────────────────────────────
|
|
85
105
|
/** Host allowlist defeats DNS rebinding: rebinding changes DNS, not the Host header. */
|
|
86
106
|
hostAllowed(host) {
|
|
@@ -152,10 +172,52 @@ export class WebcodeServer {
|
|
|
152
172
|
catch {
|
|
153
173
|
// empty body is fine
|
|
154
174
|
}
|
|
175
|
+
// Direct command sessions: the allowlist keeps the spawn surface honest
|
|
176
|
+
// (authenticated localhost daemon or not). Argv only — never a shell.
|
|
177
|
+
if (body.command !== undefined) {
|
|
178
|
+
if (typeof body.command !== "string" || !this.commandAllowlist.has(body.command)) {
|
|
179
|
+
return c.json({ error: "command not allowed" }, 400);
|
|
180
|
+
}
|
|
181
|
+
if (body.args !== undefined &&
|
|
182
|
+
(!Array.isArray(body.args) || !body.args.every((a) => typeof a === "string"))) {
|
|
183
|
+
return c.json({ error: "args must be an array of strings" }, 400);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else if (body.args !== undefined) {
|
|
187
|
+
return c.json({ error: "args require command" }, 400);
|
|
188
|
+
}
|
|
189
|
+
if (body.type !== undefined && body.type !== "shell" && body.type !== "agent") {
|
|
190
|
+
return c.json({ error: "bad type" }, 400);
|
|
191
|
+
}
|
|
192
|
+
if (body.host !== undefined && (typeof body.host !== "string" || body.host.length > 64)) {
|
|
193
|
+
return c.json({ error: "bad host" }, 400);
|
|
194
|
+
}
|
|
155
195
|
const cwd = body.cwd ? safeJoin(this.root, body.cwd) : this.root;
|
|
156
|
-
const
|
|
196
|
+
const type = body.type ?? "shell";
|
|
197
|
+
const session = this.sessions.create(cwd, body.cols, body.rows, {
|
|
198
|
+
...(body.command !== undefined ? { command: body.command, args: body.args ?? [] } : {}),
|
|
199
|
+
type,
|
|
200
|
+
...(body.host !== undefined ? { host: body.host } : {}),
|
|
201
|
+
...(type === "agent" ? { onClose: (s) => this.closeAgentSession(s) } : {}),
|
|
202
|
+
});
|
|
157
203
|
return c.json(session.info(), 201);
|
|
158
204
|
});
|
|
205
|
+
// Single-session read: the SPA polls this once after the Exit frame to
|
|
206
|
+
// pick up closeResult (the agent-exit auto-merge outcome). Awaiting
|
|
207
|
+
// whenClosed() means a poll that races the merge still gets the result.
|
|
208
|
+
app.get("/api/sessions/:id", async (c) => {
|
|
209
|
+
const session = this.sessions.get(c.req.param("id"));
|
|
210
|
+
if (!session)
|
|
211
|
+
return c.json({ error: "no such session" }, 404);
|
|
212
|
+
await session.whenClosed();
|
|
213
|
+
const body = {
|
|
214
|
+
...session.info(),
|
|
215
|
+
...(session.closeResult !== undefined
|
|
216
|
+
? { closeResult: session.closeResult }
|
|
217
|
+
: {}),
|
|
218
|
+
};
|
|
219
|
+
return c.json(body);
|
|
220
|
+
});
|
|
159
221
|
app.delete("/api/sessions/:id", (c) => {
|
|
160
222
|
const ok = this.sessions.close(c.req.param("id"));
|
|
161
223
|
return ok ? c.json({ ok: true }) : c.json({ error: "no such session" }, 404);
|
|
@@ -333,7 +395,7 @@ export class WebcodeServer {
|
|
|
333
395
|
return c.json({ ok: true, root: this.root });
|
|
334
396
|
});
|
|
335
397
|
app.route("/api/fs", createFsApi(() => this.root));
|
|
336
|
-
app.route("/api/zuzuu", createZuzuuApi(() => this.root));
|
|
398
|
+
app.route("/api/zuzuu", createZuzuuApi(() => this.root, { binary: cfg.zuzuuBinary }));
|
|
337
399
|
// Static SPA with index.html fallback
|
|
338
400
|
app.get("*", async (c) => {
|
|
339
401
|
let rel = decodeURIComponent(new URL(c.req.url).pathname);
|
package/web-app/dist/sessions.js
CHANGED
|
@@ -65,10 +65,17 @@ export class Session {
|
|
|
65
65
|
cwd;
|
|
66
66
|
root;
|
|
67
67
|
onUpdate;
|
|
68
|
+
opts;
|
|
68
69
|
id = crypto.randomBytes(8).toString("hex");
|
|
69
70
|
createdAt = Date.now();
|
|
70
71
|
title;
|
|
71
72
|
alive = true;
|
|
73
|
+
type;
|
|
74
|
+
host;
|
|
75
|
+
/** result of the agent-exit close hook (e.g. the session-git merge) */
|
|
76
|
+
closeResult;
|
|
77
|
+
closeRan = false;
|
|
78
|
+
closeSettled = Promise.resolve();
|
|
72
79
|
/** live working directory of the shell (absolute) */
|
|
73
80
|
cwdAbs;
|
|
74
81
|
pty;
|
|
@@ -87,13 +94,16 @@ export class Session {
|
|
|
87
94
|
castEvents = [];
|
|
88
95
|
castBytes = 0;
|
|
89
96
|
castTruncated = false;
|
|
90
|
-
constructor(cwd, root, cols = 80, rows = 24, onUpdate) {
|
|
97
|
+
constructor(cwd, root, cols = 80, rows = 24, onUpdate, opts = {}) {
|
|
91
98
|
this.cwd = cwd;
|
|
92
99
|
this.root = root;
|
|
93
100
|
this.onUpdate = onUpdate;
|
|
101
|
+
this.opts = opts;
|
|
94
102
|
this.cwdAbs = cwd;
|
|
95
|
-
|
|
96
|
-
this.
|
|
103
|
+
this.type = opts.type ?? "shell";
|
|
104
|
+
this.host = opts.host;
|
|
105
|
+
const file = opts.command ?? pickShell();
|
|
106
|
+
this.title = file.split("/").pop() ?? file;
|
|
97
107
|
this.mirror = new Terminal({ cols, rows, scrollback: SCROLLBACK, allowProposedApi: true });
|
|
98
108
|
this.mirror.loadAddon(this.serializer);
|
|
99
109
|
// OSC 7: the shell reports its real cwd instantly/exactly; this makes
|
|
@@ -111,10 +121,15 @@ export class Session {
|
|
|
111
121
|
}
|
|
112
122
|
return true;
|
|
113
123
|
});
|
|
114
|
-
|
|
124
|
+
// Direct command sessions (agents) get NO shell and NO rc injection:
|
|
125
|
+
// the argv is spawned as-is with a plain env, so nothing a host CLI
|
|
126
|
+
// prints/parses is polluted by our shell-integration hook.
|
|
127
|
+
const injection = opts.command || process.platform === "win32" ? null : buildInjection(file);
|
|
115
128
|
this.tempDir = injection?.tempDir;
|
|
116
|
-
const args =
|
|
117
|
-
|
|
129
|
+
const args = opts.command !== undefined
|
|
130
|
+
? opts.args ?? []
|
|
131
|
+
: injection?.args ?? (process.platform === "win32" ? [] : ["-l"]);
|
|
132
|
+
this.pty = pty.spawn(file, args, {
|
|
118
133
|
name: "xterm-256color",
|
|
119
134
|
cols,
|
|
120
135
|
rows,
|
|
@@ -148,11 +163,28 @@ export class Session {
|
|
|
148
163
|
});
|
|
149
164
|
this.pty.onExit(({ exitCode, signal }) => {
|
|
150
165
|
this.alive = false;
|
|
166
|
+
this.runCloseHook();
|
|
151
167
|
this.exitPayload = JSON.stringify({ exitCode, signal });
|
|
152
168
|
this.send(encodeFrame(ServerOp.Exit, this.exitPayload));
|
|
153
169
|
this.onUpdate();
|
|
154
170
|
});
|
|
155
171
|
}
|
|
172
|
+
/** Agent exit → run the close hook (session-git merge) exactly once. */
|
|
173
|
+
runCloseHook() {
|
|
174
|
+
const onClose = this.opts.onClose;
|
|
175
|
+
if (this.type !== "agent" || !onClose || this.closeRan)
|
|
176
|
+
return;
|
|
177
|
+
this.closeRan = true;
|
|
178
|
+
this.closeSettled = onClose(this).then((result) => {
|
|
179
|
+
this.closeResult = result;
|
|
180
|
+
}, () => {
|
|
181
|
+
this.closeResult = { ok: false, stderr: "close hook failed" };
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** Resolves once any pending agent close hook has settled (immediately otherwise). */
|
|
185
|
+
whenClosed() {
|
|
186
|
+
return this.closeSettled;
|
|
187
|
+
}
|
|
156
188
|
/** Single-attachment model: a new client takes over the session. */
|
|
157
189
|
attach(ws) {
|
|
158
190
|
if (this.socket && this.socket !== ws) {
|
|
@@ -296,6 +328,8 @@ export class Session {
|
|
|
296
328
|
cwd: this.cwdAbs,
|
|
297
329
|
alive: this.alive,
|
|
298
330
|
createdAt: this.createdAt,
|
|
331
|
+
type: this.type,
|
|
332
|
+
...(this.host !== undefined ? { host: this.host } : {}),
|
|
299
333
|
};
|
|
300
334
|
}
|
|
301
335
|
}
|
|
@@ -305,8 +339,8 @@ export class SessionManager {
|
|
|
305
339
|
constructor(defaultCwd = os.homedir()) {
|
|
306
340
|
this.defaultCwd = defaultCwd;
|
|
307
341
|
}
|
|
308
|
-
create(cwd, cols, rows) {
|
|
309
|
-
const session = new Session(cwd ?? this.defaultCwd, this.defaultCwd, cols, rows, () => { });
|
|
342
|
+
create(cwd, cols, rows, opts) {
|
|
343
|
+
const session = new Session(cwd ?? this.defaultCwd, this.defaultCwd, cols, rows, () => { }, opts);
|
|
310
344
|
this.sessions.set(session.id, session);
|
|
311
345
|
return session;
|
|
312
346
|
}
|
|
@@ -98,8 +98,17 @@ export function runZuzuuMut(root, args, opts = {}) {
|
|
|
98
98
|
});
|
|
99
99
|
child.on("close", (code) => {
|
|
100
100
|
clearTimeout(timer);
|
|
101
|
-
if (code !== 0)
|
|
102
|
-
|
|
101
|
+
if (code !== 0) {
|
|
102
|
+
// zuzuu prints structured JSON even on refusals (exit 1, e.g.
|
|
103
|
+
// empty-squash-with-checkpoints) — keep it so the UI can act on reason.
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(out);
|
|
106
|
+
return finish({ ok: false, code: "failed", stderr: err.slice(-STDERR_TAIL), data: parsed });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return finish({ ok: false, code: "failed", stderr: err.slice(-STDERR_TAIL) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
103
112
|
try {
|
|
104
113
|
finish({ ok: true, data: JSON.parse(out) });
|
|
105
114
|
}
|
|
@@ -279,7 +288,7 @@ export function createZuzuuApi(getRoot, opts = {}) {
|
|
|
279
288
|
if (!r.ok) {
|
|
280
289
|
return r.code === "absent"
|
|
281
290
|
? c.json({ error: "zuzuu CLI required" }, 503)
|
|
282
|
-
: c.json({ error: "zuzuu command failed", stderr: r.stderr ?? "" }, 502);
|
|
291
|
+
: c.json({ error: "zuzuu command failed", stderr: r.stderr ?? "", data: r.data ?? null }, 502);
|
|
283
292
|
}
|
|
284
293
|
return c.json(r.data);
|
|
285
294
|
};
|
|
@@ -327,6 +336,18 @@ export function createZuzuuApi(getRoot, opts = {}) {
|
|
|
327
336
|
return c.json({ error: "bad id" }, 400);
|
|
328
337
|
return mutate(c, ["generation", "rollback", id]);
|
|
329
338
|
});
|
|
339
|
+
// ── Session-git (the invisible zz/session-* branch) — CLI-only, no
|
|
340
|
+
// file-read fallback: branch state lives in git, only the CLI computes it.
|
|
341
|
+
app.get("/session", async (c) => {
|
|
342
|
+
const viaCli = await runZuzuu(root, ["session", "status"], { binary: opts.binary });
|
|
343
|
+
if (viaCli)
|
|
344
|
+
return c.json(viaCli);
|
|
345
|
+
return c.json({ enabled: false, cliAbsent: true });
|
|
346
|
+
});
|
|
347
|
+
app.post("/session/merge", (c) => mutate(c, ["session", "merge"]));
|
|
348
|
+
app.post("/session/continue", (c) => mutate(c, ["session", "continue"]));
|
|
349
|
+
// --yes rides server-side: the SPA's confirm dialog is the human gate
|
|
350
|
+
app.post("/session/discard", (c) => mutate(c, ["session", "discard", "--yes"]));
|
|
330
351
|
app.get("/eval", async (c) => {
|
|
331
352
|
const viaCli = await runZuzuu(root, ["eval"], { binary: opts.binary });
|
|
332
353
|
if (viaCli)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{d as e,f as t,u as n}from"./index-
|
|
1
|
+
import{d as e,f as t,u as n}from"./index-Ye54YyTn.js";import{i as r,n as i,t as a}from"./monaco-setup-Dszx738Y.js";var o=t();function s({path:t,name:s}){let{data:l,isLoading:u,error:d}=e({queryKey:[`git`,`diff`,t],queryFn:async()=>{let[e,r]=await Promise.all([n.gitDiff(t),n.readFile(t).catch(()=>``)]);return{original:e.original,working:r}}});return d?(0,o.jsx)(c,{danger:!0,children:d.message}):u||!l?(0,o.jsx)(c,{children:`loading diff…`}):(0,o.jsx)(r,{original:l.original,modified:l.working,language:i(s),theme:a(),options:{readOnly:!0,renderSideBySide:!0,fontFamily:`"JetBrains Mono Variable", ui-monospace, monospace`,fontSize:13,minimap:{enabled:!1},automaticLayout:!0}})}function c({children:e,danger:t}){return(0,o.jsx)(`div`,{className:`flex h-full items-center justify-center text-ui ${t?`text-danger`:`text-ink-500`}`,children:e})}export{s as DiffTab,s as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{f as e,l as t}from"./index-
|
|
1
|
+
import{f as e,l as t}from"./index-Ye54YyTn.js";import{n,r,t as i}from"./monaco-setup-Dszx738Y.js";var a=e();function o({path:e,name:o}){let c=t(t=>t.buffers[e]),l=t(e=>e.setValue),u=t(e=>e.save);return!c||c.loading?(0,a.jsx)(s,{children:`loading…`}):c.error?(0,a.jsx)(s,{danger:!0,children:c.error}):(0,a.jsx)(r,{path:e,language:n(o),theme:i(),value:c.value,onChange:t=>l(e,t??``),onMount:(t,n)=>{t.addCommand(n.KeyMod.CtrlCmd|n.KeyCode.KeyS,()=>{u(e)})},options:{fontFamily:`"JetBrains Mono Variable", ui-monospace, monospace`,fontSize:13,lineHeight:1.5,minimap:{enabled:!0,scale:1},scrollBeyondLastLine:!1,smoothScrolling:!0,renderWhitespace:`selection`,tabSize:2,automaticLayout:!0,padding:{top:8}}})}function s({children:e,danger:t}){return(0,a.jsx)(`div`,{className:`flex h-full items-center justify-center text-ui ${t?`text-danger`:`text-ink-500`}`,children:e})}export{o as MonacoFile,o as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{h as e}from"./editor.api2-BmGoRSl4.js";import{o as t}from"./monaco-setup-
|
|
1
|
+
import{h as e}from"./editor.api2-BmGoRSl4.js";import{o as t}from"./monaco-setup-Dszx738Y.js";import{_ as n,a as r,c as i,d as a,f as o,g as s,h as c,i as l,l as u,m as d,n as f,o as p,p as m,r as h,s as g,t as _,u as v,v as y}from"./lspLanguageFeatures-gTnJsses.js";var b=120*1e3,x=class{constructor(e){this._defaults=e,this._worker=null,this._client=null,this._idleCheckInterval=window.setInterval(()=>this._checkIfIdle(),30*1e3),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange(()=>this._stopWorker())}_stopWorker(){this._worker&&=(this._worker.dispose(),null),this._client=null}dispose(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()}_checkIfIdle(){this._worker&&Date.now()-this._lastUsedTime>b&&this._stopWorker()}_getClient(){return this._lastUsedTime=Date.now(),this._client||=(this._worker=t({moduleId:`vs/language/css/cssWorker`,createWorker:()=>new Worker(new URL(`/assets/css.worker-CvXBzhp8.js`,``+import.meta.url),{type:`module`}),label:this._defaults.languageId,createData:{options:this._defaults.options,languageId:this._defaults.languageId}}),this._worker.getProxy()),this._client}getLanguageServiceWorker(...e){let t;return this._getClient().then(e=>{t=e}).then(t=>{if(this._worker)return this._worker.withSyncedResources(e)}).then(e=>t)}};function S(t){let n=[],s=[],c=new x(t);n.push(c);let g=(...e)=>c.getLanguageServiceWorker(...e);function y(){let{languageId:n,modeConfiguration:c}=t;w(s),c.completionItems&&s.push(e.registerCompletionItemProvider(n,new _(g,[`/`,`-`,`:`]))),c.hovers&&s.push(e.registerHoverProvider(n,new a(g))),c.documentHighlights&&s.push(e.registerDocumentHighlightProvider(n,new p(g))),c.definitions&&s.push(e.registerDefinitionProvider(n,new f(g))),c.references&&s.push(e.registerReferenceProvider(n,new o(g))),c.documentSymbols&&s.push(e.registerDocumentSymbolProvider(n,new u(g))),c.rename&&s.push(e.registerRenameProvider(n,new m(g))),c.colors&&s.push(e.registerColorProvider(n,new l(g))),c.foldingRanges&&s.push(e.registerFoldingRangeProvider(n,new v(g))),c.diagnostics&&s.push(new h(n,g,t.onDidChange)),c.selectionRanges&&s.push(e.registerSelectionRangeProvider(n,new d(g))),c.documentFormattingEdits&&s.push(e.registerDocumentFormattingEditProvider(n,new r(g))),c.documentRangeFormattingEdits&&s.push(e.registerDocumentRangeFormattingEditProvider(n,new i(g)))}return y(),n.push(C(s)),C(n)}function C(e){return{dispose:()=>w(e)}}function w(e){for(;e.length;)e.pop().dispose()}export{_ as CompletionAdapter,f as DefinitionAdapter,h as DiagnosticsAdapter,l as DocumentColorAdapter,r as DocumentFormattingEditProvider,p as DocumentHighlightAdapter,g as DocumentLinkAdapter,i as DocumentRangeFormattingEditProvider,u as DocumentSymbolAdapter,v as FoldingRangeAdapter,a as HoverAdapter,o as ReferenceAdapter,m as RenameAdapter,d as SelectionRangeAdapter,x as WorkerManager,c as fromPosition,s as fromRange,S as setupMode,n as toRange,y as toTextEdit};
|