@xbghc/warden 0.15.0 → 0.16.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.
- package/README.md +43 -462
- package/dist/cli.js +556 -347
- package/dist/web/assets/index-gtbD7YAQ.css +1 -0
- package/dist/web/assets/index-iclVlGDF.js +198 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-W_TDLgHi.css +0 -1
- package/dist/web/assets/index-yrM09beV.js +0 -200
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
|
|
9
9
|
// packages/server/src/index.ts
|
|
10
10
|
import net from "net";
|
|
11
|
-
import { randomUUID as
|
|
11
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
12
12
|
|
|
13
13
|
// node_modules/.pnpm/@hono+node-server@1.19.17_hono@4.13.7/node_modules/@hono/node-server/dist/index.mjs
|
|
14
14
|
import { createServer as createServerHTTP } from "http";
|
|
@@ -653,7 +653,8 @@ var serve = (options, listeningListener) => {
|
|
|
653
653
|
|
|
654
654
|
// packages/server/src/app.ts
|
|
655
655
|
import path9 from "path";
|
|
656
|
-
import {
|
|
656
|
+
import { stat as stat7 } from "fs/promises";
|
|
657
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
657
658
|
|
|
658
659
|
// node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/compose.js
|
|
659
660
|
var compose = (middleware, onError, onNotFound) => {
|
|
@@ -3000,6 +3001,17 @@ function moveBefore(list, id, before) {
|
|
|
3000
3001
|
return rest;
|
|
3001
3002
|
}
|
|
3002
3003
|
|
|
3004
|
+
// packages/shared/src/comments.ts
|
|
3005
|
+
function commentWorktree(c) {
|
|
3006
|
+
return tryParseTargetKey(c.targetKey)?.worktree;
|
|
3007
|
+
}
|
|
3008
|
+
function awaitsAgent(c) {
|
|
3009
|
+
return !c.exportedAt;
|
|
3010
|
+
}
|
|
3011
|
+
function awaitsReviewer(c) {
|
|
3012
|
+
return c.replies?.at(-1)?.author === "agent";
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3003
3015
|
// packages/server/src/errors.ts
|
|
3004
3016
|
var HttpError = class extends Error {
|
|
3005
3017
|
constructor(status, message, code) {
|
|
@@ -3399,35 +3411,245 @@ async function getRepoInfo(ctx, defaultTarget) {
|
|
|
3399
3411
|
}
|
|
3400
3412
|
|
|
3401
3413
|
// packages/server/src/targets.ts
|
|
3402
|
-
import
|
|
3403
|
-
import { readFile } from "fs/promises";
|
|
3414
|
+
import path4 from "path";
|
|
3415
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
3404
3416
|
|
|
3405
3417
|
// packages/server/src/checkpoints.ts
|
|
3406
|
-
import
|
|
3418
|
+
import path3 from "path";
|
|
3407
3419
|
import { randomUUID } from "crypto";
|
|
3408
|
-
import { copyFile, mkdir, rm, stat, utimes } from "fs/promises";
|
|
3420
|
+
import { copyFile, mkdir as mkdir2, rm, stat as stat2, utimes } from "fs/promises";
|
|
3421
|
+
|
|
3422
|
+
// packages/server/src/state.ts
|
|
3423
|
+
import os from "os";
|
|
3424
|
+
import path2 from "path";
|
|
3425
|
+
import { mkdir, open, readFile, rename, stat, unlink, writeFile } from "fs/promises";
|
|
3426
|
+
|
|
3427
|
+
// packages/server/src/hash.ts
|
|
3428
|
+
import { createHash } from "crypto";
|
|
3429
|
+
function sha1(input) {
|
|
3430
|
+
return createHash("sha1").update(input).digest("hex");
|
|
3431
|
+
}
|
|
3432
|
+
function lineHash(content) {
|
|
3433
|
+
return sha1(content.trimEnd());
|
|
3434
|
+
}
|
|
3435
|
+
function hunkHash(lineContents) {
|
|
3436
|
+
return sha1(
|
|
3437
|
+
lineContents.map((l) => l.trimEnd()).join("\n").trim()
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// packages/server/src/state.ts
|
|
3442
|
+
function dataDir() {
|
|
3443
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
3444
|
+
const base = xdg?.trim() ? xdg : path2.join(os.homedir(), ".local", "share");
|
|
3445
|
+
return path2.join(base, "warden");
|
|
3446
|
+
}
|
|
3447
|
+
function repoHash(repoRoot) {
|
|
3448
|
+
return sha1(repoRoot).slice(0, 12);
|
|
3449
|
+
}
|
|
3450
|
+
function stateFilePath(repoRoot, baseDir = dataDir()) {
|
|
3451
|
+
return path2.join(baseDir, repoHash(repoRoot), "state.json");
|
|
3452
|
+
}
|
|
3453
|
+
function defaultPrefs() {
|
|
3454
|
+
return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false, ignoreDebug: true };
|
|
3455
|
+
}
|
|
3456
|
+
function defaultState(repoRoot) {
|
|
3457
|
+
return { schemaVersion: 1, repoRoot, targets: {}, todos: [], checkpoints: [], prefs: defaultPrefs() };
|
|
3458
|
+
}
|
|
3459
|
+
function ensureTarget(state, key) {
|
|
3460
|
+
let t = state.targets[key];
|
|
3461
|
+
if (!t) {
|
|
3462
|
+
t = { viewed: {}, comments: [] };
|
|
3463
|
+
state.targets[key] = t;
|
|
3464
|
+
}
|
|
3465
|
+
return t;
|
|
3466
|
+
}
|
|
3467
|
+
var usable = (v) => !!v && typeof v === "object" && typeof v.id === "string";
|
|
3468
|
+
var usableCheckpoint = (v) => {
|
|
3469
|
+
const c = v;
|
|
3470
|
+
return !!c && typeof c === "object" && Number.isInteger(c.id) && typeof c.tree === "string" && (c.worktree === void 0 || typeof c.worktree === "string");
|
|
3471
|
+
};
|
|
3472
|
+
var usableComment = (v) => usable(v) && typeof v.anchor === "object" && !!v.anchor;
|
|
3473
|
+
function normalise(raw2, repoRoot) {
|
|
3474
|
+
const base = defaultState(repoRoot);
|
|
3475
|
+
if (!raw2 || typeof raw2 !== "object") return base;
|
|
3476
|
+
const r = raw2;
|
|
3477
|
+
if (r.schemaVersion !== 1) return base;
|
|
3478
|
+
const targets = {};
|
|
3479
|
+
for (const [k, v] of Object.entries(r.targets ?? {})) {
|
|
3480
|
+
if (!v || typeof v !== "object") continue;
|
|
3481
|
+
targets[k] = {
|
|
3482
|
+
viewed: typeof v.viewed === "object" && v.viewed ? v.viewed : {},
|
|
3483
|
+
comments: Array.isArray(v.comments) ? v.comments.filter(usableComment) : [],
|
|
3484
|
+
...typeof v.head === "string" ? { head: v.head } : {}
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
migrateLocalViews(targets);
|
|
3488
|
+
return {
|
|
3489
|
+
schemaVersion: 1,
|
|
3490
|
+
repoRoot: r.repoRoot ?? repoRoot,
|
|
3491
|
+
targets,
|
|
3492
|
+
// Issues were folded into todos (a todo links comments now); the few there were are let go,
|
|
3493
|
+
// and the next write leaves them out of the file.
|
|
3494
|
+
todos: Array.isArray(r.todos) ? r.todos.filter(usable) : [],
|
|
3495
|
+
checkpoints: Array.isArray(r.checkpoints) ? r.checkpoints.filter(usableCheckpoint) : [],
|
|
3496
|
+
prefs: {
|
|
3497
|
+
...defaultPrefs(),
|
|
3498
|
+
...r.prefs ?? {},
|
|
3499
|
+
nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
|
|
3500
|
+
autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
|
|
3501
|
+
railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false,
|
|
3502
|
+
ignoreDebug: typeof r.prefs?.ignoreDebug === "boolean" ? r.prefs.ignoreDebug : true
|
|
3503
|
+
}
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
function migrateLocalViews(targets) {
|
|
3507
|
+
for (const key of Object.keys(targets).sort()) {
|
|
3508
|
+
const scope = commentScopeKey(key);
|
|
3509
|
+
if (scope === key) continue;
|
|
3510
|
+
const src = targets[key];
|
|
3511
|
+
delete targets[key];
|
|
3512
|
+
if (src.comments.length === 0) continue;
|
|
3513
|
+
let dst = targets[scope];
|
|
3514
|
+
if (!dst) {
|
|
3515
|
+
dst = { viewed: {}, comments: [] };
|
|
3516
|
+
targets[scope] = dst;
|
|
3517
|
+
}
|
|
3518
|
+
dst.comments.push(...src.comments);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
var LOCK_STALE_MS = 5e3;
|
|
3522
|
+
var LOCK_WAIT_MS = 5e3;
|
|
3523
|
+
async function sleep(ms) {
|
|
3524
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
3525
|
+
}
|
|
3526
|
+
var StateStore = class {
|
|
3527
|
+
constructor(file, repoRoot) {
|
|
3528
|
+
this.file = file;
|
|
3529
|
+
this.repoRoot = repoRoot;
|
|
3530
|
+
}
|
|
3531
|
+
file;
|
|
3532
|
+
repoRoot;
|
|
3533
|
+
queue = Promise.resolve();
|
|
3534
|
+
async load() {
|
|
3535
|
+
try {
|
|
3536
|
+
const text = await readFile(this.file, "utf8");
|
|
3537
|
+
return normalise(JSON.parse(text), this.repoRoot);
|
|
3538
|
+
} catch (e) {
|
|
3539
|
+
const err = e;
|
|
3540
|
+
if (err.code === "ENOENT") return defaultState(this.repoRoot);
|
|
3541
|
+
if (e instanceof SyntaxError) {
|
|
3542
|
+
try {
|
|
3543
|
+
await rename(this.file, `${this.file}.corrupt-${Date.now()}`);
|
|
3544
|
+
} catch {
|
|
3545
|
+
}
|
|
3546
|
+
return defaultState(this.repoRoot);
|
|
3547
|
+
}
|
|
3548
|
+
throw e;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
/** Apply a mutation under lock. The callback receives the freshest on-disk state. */
|
|
3552
|
+
update(fn) {
|
|
3553
|
+
const run2 = async () => {
|
|
3554
|
+
await mkdir(path2.dirname(this.file), { recursive: true });
|
|
3555
|
+
const release = await this.acquireLock();
|
|
3556
|
+
try {
|
|
3557
|
+
const state = await this.load();
|
|
3558
|
+
const result = await fn(state);
|
|
3559
|
+
await this.writeAtomic(state);
|
|
3560
|
+
return result;
|
|
3561
|
+
} finally {
|
|
3562
|
+
await release();
|
|
3563
|
+
}
|
|
3564
|
+
};
|
|
3565
|
+
const p = this.queue.then(run2, run2);
|
|
3566
|
+
this.queue = p.catch(() => void 0);
|
|
3567
|
+
return p;
|
|
3568
|
+
}
|
|
3569
|
+
async writeAtomic(state) {
|
|
3570
|
+
const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
|
|
3571
|
+
await writeFile(tmp, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
3572
|
+
await rename(tmp, this.file);
|
|
3573
|
+
}
|
|
3574
|
+
async acquireLock() {
|
|
3575
|
+
const lock = `${this.file}.lock`;
|
|
3576
|
+
const started = Date.now();
|
|
3577
|
+
for (; ; ) {
|
|
3578
|
+
try {
|
|
3579
|
+
const fh = await open(lock, "wx");
|
|
3580
|
+
await fh.writeFile(String(process.pid));
|
|
3581
|
+
await fh.close();
|
|
3582
|
+
return async () => {
|
|
3583
|
+
try {
|
|
3584
|
+
await unlink(lock);
|
|
3585
|
+
} catch {
|
|
3586
|
+
}
|
|
3587
|
+
};
|
|
3588
|
+
} catch (e) {
|
|
3589
|
+
const err = e;
|
|
3590
|
+
if (err.code !== "EEXIST") throw e;
|
|
3591
|
+
try {
|
|
3592
|
+
const st = await stat(lock);
|
|
3593
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
3594
|
+
await unlink(lock).catch(() => void 0);
|
|
3595
|
+
continue;
|
|
3596
|
+
}
|
|
3597
|
+
} catch {
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
if (Date.now() - started > LOCK_WAIT_MS) {
|
|
3601
|
+
throw new Error(`timed out waiting for state lock ${lock}`);
|
|
3602
|
+
}
|
|
3603
|
+
await sleep(20);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
function forgetWorktreeTargets(state, worktreePath) {
|
|
3609
|
+
const prefix = `worktree:${worktreePath}:`;
|
|
3610
|
+
const gone = /* @__PURE__ */ new Set();
|
|
3611
|
+
for (const key of Object.keys(state.targets)) {
|
|
3612
|
+
if (!key.startsWith(prefix)) continue;
|
|
3613
|
+
for (const c of state.targets[key].comments) gone.add(c.id);
|
|
3614
|
+
delete state.targets[key];
|
|
3615
|
+
}
|
|
3616
|
+
unlinkComments(state, gone);
|
|
3617
|
+
state.checkpoints = state.checkpoints.filter((c) => c.worktree !== worktreePath);
|
|
3618
|
+
}
|
|
3619
|
+
function unlinkComments(state, ids) {
|
|
3620
|
+
const gone = new Set(ids);
|
|
3621
|
+
if (gone.size === 0) return;
|
|
3622
|
+
for (const todo of state.todos) {
|
|
3623
|
+
if (!todo.commentIds?.some((id) => gone.has(id))) continue;
|
|
3624
|
+
const kept = todo.commentIds.filter((id) => !gone.has(id));
|
|
3625
|
+
if (kept.length) todo.commentIds = kept;
|
|
3626
|
+
else delete todo.commentIds;
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// packages/server/src/checkpoints.ts
|
|
3409
3631
|
function checkpointStore(stateFile) {
|
|
3410
|
-
return
|
|
3632
|
+
return path3.join(path3.dirname(stateFile), "checkpoints");
|
|
3411
3633
|
}
|
|
3412
3634
|
async function repoObjects(cwd) {
|
|
3413
3635
|
const r = await runGit(["rev-parse", "--git-path", "objects"], { cwd });
|
|
3414
|
-
return
|
|
3636
|
+
return path3.resolve(cwd, r.stdout.trim());
|
|
3415
3637
|
}
|
|
3416
3638
|
async function repoIndex(cwd) {
|
|
3417
3639
|
const r = await runGit(["rev-parse", "--git-path", "index"], { cwd });
|
|
3418
|
-
return
|
|
3640
|
+
return path3.resolve(cwd, r.stdout.trim());
|
|
3419
3641
|
}
|
|
3420
3642
|
async function checkpointObjects(cwd, store) {
|
|
3421
|
-
return { objects:
|
|
3643
|
+
return { objects: path3.join(store, "objects"), alternates: await repoObjects(cwd) };
|
|
3422
3644
|
}
|
|
3423
3645
|
async function withIndexCopy(cwd, store, fn) {
|
|
3424
|
-
const tmp =
|
|
3425
|
-
await
|
|
3426
|
-
await
|
|
3646
|
+
const tmp = path3.join(store, "tmp");
|
|
3647
|
+
await mkdir2(path3.join(store, "objects"), { recursive: true });
|
|
3648
|
+
await mkdir2(tmp, { recursive: true });
|
|
3427
3649
|
const [objects, index] = await Promise.all([checkpointObjects(cwd, store), repoIndex(cwd)]);
|
|
3428
|
-
const copy =
|
|
3650
|
+
const copy = path3.join(tmp, `index-${randomUUID()}`);
|
|
3429
3651
|
try {
|
|
3430
|
-
const st = await
|
|
3652
|
+
const st = await stat2(index).catch(() => void 0);
|
|
3431
3653
|
if (st) {
|
|
3432
3654
|
await copyFile(index, copy);
|
|
3433
3655
|
const earlier = Math.floor(st.mtimeMs / 1e3) - 1;
|
|
@@ -3468,29 +3690,24 @@ function forgetCheckpoint(state, c) {
|
|
|
3468
3690
|
const key = checkpointKey(c);
|
|
3469
3691
|
const gone = new Set((state.targets[key]?.comments ?? []).map((x) => x.id));
|
|
3470
3692
|
delete state.targets[key];
|
|
3471
|
-
|
|
3693
|
+
unlinkComments(state, gone);
|
|
3472
3694
|
}
|
|
3473
|
-
function addCheckpoint(state, worktree, tree, head) {
|
|
3695
|
+
function addCheckpoint(state, worktree, tree, head, handoff = false) {
|
|
3474
3696
|
const mine = checkpointsOf(state, worktree);
|
|
3475
3697
|
const id = mine.reduce((n, c) => Math.max(n, c.id), 0) + 1;
|
|
3476
|
-
const checkpoint = { id, ...worktree ? { worktree } : {}, tree, head, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3698
|
+
const checkpoint = { id, ...worktree ? { worktree } : {}, tree, head, createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...handoff ? { handoff } : {} };
|
|
3477
3699
|
state.checkpoints.push(checkpoint);
|
|
3478
3700
|
for (const old of mine.slice(0, Math.max(0, mine.length + 1 - MAX_CHECKPOINTS))) forgetCheckpoint(state, old);
|
|
3479
3701
|
return checkpoint;
|
|
3480
3702
|
}
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
}
|
|
3490
|
-
function hunkHash(lineContents) {
|
|
3491
|
-
return sha1(
|
|
3492
|
-
lineContents.map((l) => l.trimEnd()).join("\n").trim()
|
|
3493
|
-
);
|
|
3703
|
+
async function takeCheckpoint(store, cwd, worktree, opts = {}) {
|
|
3704
|
+
const tree = await snapshotWorktree(cwd, checkpointStore(store.file));
|
|
3705
|
+
const head = await revParse(cwd, "HEAD") ?? "";
|
|
3706
|
+
return store.update((s) => {
|
|
3707
|
+
const newest = checkpointsOf(s, worktree).at(-1);
|
|
3708
|
+
if (newest?.tree === tree) return { checkpoint: newest, unchanged: true };
|
|
3709
|
+
return { checkpoint: addCheckpoint(s, worktree, tree, head, opts.handoff), unchanged: false };
|
|
3710
|
+
});
|
|
3494
3711
|
}
|
|
3495
3712
|
|
|
3496
3713
|
// packages/server/src/diffparse.ts
|
|
@@ -3892,7 +4109,7 @@ async function getFullFile(ctx, filePath, side) {
|
|
|
3892
4109
|
const ref = await refForSide(ctx, side);
|
|
3893
4110
|
if (ref === void 0) {
|
|
3894
4111
|
try {
|
|
3895
|
-
const buf = await
|
|
4112
|
+
const buf = await readFile2(path4.join(ctx.cwd, filePath));
|
|
3896
4113
|
return buf.toString("utf8");
|
|
3897
4114
|
} catch {
|
|
3898
4115
|
return null;
|
|
@@ -4192,15 +4409,15 @@ function buildStagePatch(diff, mode, selection, opts = {}) {
|
|
|
4192
4409
|
}
|
|
4193
4410
|
|
|
4194
4411
|
// packages/server/src/worktrees.ts
|
|
4195
|
-
import
|
|
4196
|
-
import { readdir, realpath as realpath2, stat as
|
|
4412
|
+
import path5 from "path";
|
|
4413
|
+
import { readdir, realpath as realpath2, stat as stat3 } from "fs/promises";
|
|
4197
4414
|
function slotPath(ctx, slot) {
|
|
4198
|
-
return
|
|
4415
|
+
return path5.join(path5.dirname(ctx.commonRoot), `${path5.basename(ctx.commonRoot)}-${slot}`);
|
|
4199
4416
|
}
|
|
4200
4417
|
function slotOf(ctx, p) {
|
|
4201
|
-
if (
|
|
4202
|
-
const prefix = `${
|
|
4203
|
-
const name =
|
|
4418
|
+
if (path5.dirname(p) !== path5.dirname(ctx.commonRoot)) return void 0;
|
|
4419
|
+
const prefix = `${path5.basename(ctx.commonRoot)}-`;
|
|
4420
|
+
const name = path5.basename(p);
|
|
4204
4421
|
const n = name.startsWith(prefix) ? name.slice(prefix.length) : "";
|
|
4205
4422
|
return /^[1-9]\d*$/.test(n) ? Number(n) : void 0;
|
|
4206
4423
|
}
|
|
@@ -4285,7 +4502,7 @@ async function assertBranchName(ctx, name) {
|
|
|
4285
4502
|
}
|
|
4286
4503
|
}
|
|
4287
4504
|
async function dirUsable(p) {
|
|
4288
|
-
const st = await
|
|
4505
|
+
const st = await stat3(p).catch((e) => {
|
|
4289
4506
|
if (e.code === "ENOENT") return void 0;
|
|
4290
4507
|
throw e;
|
|
4291
4508
|
});
|
|
@@ -4363,8 +4580,8 @@ async function checkoutWorktree(ctx, req) {
|
|
|
4363
4580
|
if (e instanceof GitError && /already (checked out|used by worktree)/.test(e.stderr)) throw new HttpError(409, e.message, "branch_in_use");
|
|
4364
4581
|
throw e;
|
|
4365
4582
|
}
|
|
4366
|
-
const
|
|
4367
|
-
const worktree = (await listWorktrees(ctx)).find((w) => w.path ===
|
|
4583
|
+
const real2 = await realpath2(target.path).catch(() => target.path);
|
|
4584
|
+
const worktree = (await listWorktrees(ctx)).find((w) => w.path === real2);
|
|
4368
4585
|
if (!worktree) throw new HttpError(500, `git checked ${branch} out at ${target.path} but does not list it`, "git_failed");
|
|
4369
4586
|
return { worktree, slot: target.slot, reused: target.reuse };
|
|
4370
4587
|
}
|
|
@@ -4382,7 +4599,7 @@ async function dropBranch(ctx, wt, asked) {
|
|
|
4382
4599
|
}
|
|
4383
4600
|
async function releaseWorktree(ctx, req) {
|
|
4384
4601
|
const p = req.path.trim();
|
|
4385
|
-
const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path ===
|
|
4602
|
+
const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path5.resolve(p));
|
|
4386
4603
|
if (!wt) throw badRequest(`unknown worktree: ${p}`, "unknown_worktree");
|
|
4387
4604
|
if (wt.isMain) throw badRequest("the main worktree cannot be released", "main_worktree");
|
|
4388
4605
|
if (wt.bare || slotOf(ctx, wt.path) === void 0) throw badRequest(`${wt.path} is not a slot; remove it instead`, "not_a_slot");
|
|
@@ -4398,7 +4615,7 @@ async function releaseWorktree(ctx, req) {
|
|
|
4398
4615
|
}
|
|
4399
4616
|
async function removeWorktree(ctx, req) {
|
|
4400
4617
|
const p = req.path.trim();
|
|
4401
|
-
const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path ===
|
|
4618
|
+
const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path5.resolve(p));
|
|
4402
4619
|
if (!wt) throw badRequest(`unknown worktree: ${p}`, "unknown_worktree");
|
|
4403
4620
|
if (wt.isMain) throw badRequest("the main worktree cannot be removed", "main_worktree");
|
|
4404
4621
|
const args = ["worktree", "remove"];
|
|
@@ -4418,187 +4635,6 @@ async function removeWorktree(ctx, req) {
|
|
|
4418
4635
|
return dropBranch(ctx, wt, req.deleteBranch);
|
|
4419
4636
|
}
|
|
4420
4637
|
|
|
4421
|
-
// packages/server/src/state.ts
|
|
4422
|
-
import os from "os";
|
|
4423
|
-
import path5 from "path";
|
|
4424
|
-
import { mkdir as mkdir2, open, readFile as readFile2, rename, stat as stat3, unlink, writeFile } from "fs/promises";
|
|
4425
|
-
function dataDir() {
|
|
4426
|
-
const xdg = process.env.XDG_DATA_HOME;
|
|
4427
|
-
const base = xdg?.trim() ? xdg : path5.join(os.homedir(), ".local", "share");
|
|
4428
|
-
return path5.join(base, "warden");
|
|
4429
|
-
}
|
|
4430
|
-
function repoHash(repoRoot) {
|
|
4431
|
-
return sha1(repoRoot).slice(0, 12);
|
|
4432
|
-
}
|
|
4433
|
-
function stateFilePath(repoRoot, baseDir = dataDir()) {
|
|
4434
|
-
return path5.join(baseDir, repoHash(repoRoot), "state.json");
|
|
4435
|
-
}
|
|
4436
|
-
function defaultPrefs() {
|
|
4437
|
-
return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false, ignoreDebug: true };
|
|
4438
|
-
}
|
|
4439
|
-
function defaultState(repoRoot) {
|
|
4440
|
-
return { schemaVersion: 1, repoRoot, targets: {}, issues: [], todos: [], checkpoints: [], prefs: defaultPrefs() };
|
|
4441
|
-
}
|
|
4442
|
-
function ensureTarget(state, key) {
|
|
4443
|
-
let t = state.targets[key];
|
|
4444
|
-
if (!t) {
|
|
4445
|
-
t = { viewed: {}, comments: [] };
|
|
4446
|
-
state.targets[key] = t;
|
|
4447
|
-
}
|
|
4448
|
-
return t;
|
|
4449
|
-
}
|
|
4450
|
-
var usable = (v) => !!v && typeof v === "object" && typeof v.id === "string";
|
|
4451
|
-
var usableCheckpoint = (v) => {
|
|
4452
|
-
const c = v;
|
|
4453
|
-
return !!c && typeof c === "object" && Number.isInteger(c.id) && typeof c.tree === "string" && (c.worktree === void 0 || typeof c.worktree === "string");
|
|
4454
|
-
};
|
|
4455
|
-
var usableComment = (v) => usable(v) && typeof v.anchor === "object" && !!v.anchor;
|
|
4456
|
-
function normalise(raw2, repoRoot) {
|
|
4457
|
-
const base = defaultState(repoRoot);
|
|
4458
|
-
if (!raw2 || typeof raw2 !== "object") return base;
|
|
4459
|
-
const r = raw2;
|
|
4460
|
-
if (r.schemaVersion !== 1) return base;
|
|
4461
|
-
const targets = {};
|
|
4462
|
-
for (const [k, v] of Object.entries(r.targets ?? {})) {
|
|
4463
|
-
if (!v || typeof v !== "object") continue;
|
|
4464
|
-
targets[k] = {
|
|
4465
|
-
viewed: typeof v.viewed === "object" && v.viewed ? v.viewed : {},
|
|
4466
|
-
comments: Array.isArray(v.comments) ? v.comments.filter(usableComment) : [],
|
|
4467
|
-
...typeof v.head === "string" ? { head: v.head } : {}
|
|
4468
|
-
};
|
|
4469
|
-
}
|
|
4470
|
-
migrateLocalViews(targets);
|
|
4471
|
-
return {
|
|
4472
|
-
schemaVersion: 1,
|
|
4473
|
-
repoRoot: r.repoRoot ?? repoRoot,
|
|
4474
|
-
targets,
|
|
4475
|
-
issues: Array.isArray(r.issues) ? r.issues.filter(usable) : [],
|
|
4476
|
-
todos: Array.isArray(r.todos) ? r.todos.filter(usable) : [],
|
|
4477
|
-
checkpoints: Array.isArray(r.checkpoints) ? r.checkpoints.filter(usableCheckpoint) : [],
|
|
4478
|
-
prefs: {
|
|
4479
|
-
...defaultPrefs(),
|
|
4480
|
-
...r.prefs ?? {},
|
|
4481
|
-
nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
|
|
4482
|
-
autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
|
|
4483
|
-
railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false,
|
|
4484
|
-
ignoreDebug: typeof r.prefs?.ignoreDebug === "boolean" ? r.prefs.ignoreDebug : true
|
|
4485
|
-
}
|
|
4486
|
-
};
|
|
4487
|
-
}
|
|
4488
|
-
function migrateLocalViews(targets) {
|
|
4489
|
-
for (const key of Object.keys(targets).sort()) {
|
|
4490
|
-
const scope = commentScopeKey(key);
|
|
4491
|
-
if (scope === key) continue;
|
|
4492
|
-
const src = targets[key];
|
|
4493
|
-
delete targets[key];
|
|
4494
|
-
if (src.comments.length === 0) continue;
|
|
4495
|
-
let dst = targets[scope];
|
|
4496
|
-
if (!dst) {
|
|
4497
|
-
dst = { viewed: {}, comments: [] };
|
|
4498
|
-
targets[scope] = dst;
|
|
4499
|
-
}
|
|
4500
|
-
dst.comments.push(...src.comments);
|
|
4501
|
-
}
|
|
4502
|
-
}
|
|
4503
|
-
var LOCK_STALE_MS = 5e3;
|
|
4504
|
-
var LOCK_WAIT_MS = 5e3;
|
|
4505
|
-
async function sleep(ms) {
|
|
4506
|
-
await new Promise((r) => setTimeout(r, ms));
|
|
4507
|
-
}
|
|
4508
|
-
var StateStore = class {
|
|
4509
|
-
constructor(file, repoRoot) {
|
|
4510
|
-
this.file = file;
|
|
4511
|
-
this.repoRoot = repoRoot;
|
|
4512
|
-
}
|
|
4513
|
-
file;
|
|
4514
|
-
repoRoot;
|
|
4515
|
-
queue = Promise.resolve();
|
|
4516
|
-
async load() {
|
|
4517
|
-
try {
|
|
4518
|
-
const text = await readFile2(this.file, "utf8");
|
|
4519
|
-
return normalise(JSON.parse(text), this.repoRoot);
|
|
4520
|
-
} catch (e) {
|
|
4521
|
-
const err = e;
|
|
4522
|
-
if (err.code === "ENOENT") return defaultState(this.repoRoot);
|
|
4523
|
-
if (e instanceof SyntaxError) {
|
|
4524
|
-
try {
|
|
4525
|
-
await rename(this.file, `${this.file}.corrupt-${Date.now()}`);
|
|
4526
|
-
} catch {
|
|
4527
|
-
}
|
|
4528
|
-
return defaultState(this.repoRoot);
|
|
4529
|
-
}
|
|
4530
|
-
throw e;
|
|
4531
|
-
}
|
|
4532
|
-
}
|
|
4533
|
-
/** Apply a mutation under lock. The callback receives the freshest on-disk state. */
|
|
4534
|
-
update(fn) {
|
|
4535
|
-
const run2 = async () => {
|
|
4536
|
-
await mkdir2(path5.dirname(this.file), { recursive: true });
|
|
4537
|
-
const release = await this.acquireLock();
|
|
4538
|
-
try {
|
|
4539
|
-
const state = await this.load();
|
|
4540
|
-
const result = await fn(state);
|
|
4541
|
-
await this.writeAtomic(state);
|
|
4542
|
-
return result;
|
|
4543
|
-
} finally {
|
|
4544
|
-
await release();
|
|
4545
|
-
}
|
|
4546
|
-
};
|
|
4547
|
-
const p = this.queue.then(run2, run2);
|
|
4548
|
-
this.queue = p.catch(() => void 0);
|
|
4549
|
-
return p;
|
|
4550
|
-
}
|
|
4551
|
-
async writeAtomic(state) {
|
|
4552
|
-
const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
|
|
4553
|
-
await writeFile(tmp, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
4554
|
-
await rename(tmp, this.file);
|
|
4555
|
-
}
|
|
4556
|
-
async acquireLock() {
|
|
4557
|
-
const lock = `${this.file}.lock`;
|
|
4558
|
-
const started = Date.now();
|
|
4559
|
-
for (; ; ) {
|
|
4560
|
-
try {
|
|
4561
|
-
const fh = await open(lock, "wx");
|
|
4562
|
-
await fh.writeFile(String(process.pid));
|
|
4563
|
-
await fh.close();
|
|
4564
|
-
return async () => {
|
|
4565
|
-
try {
|
|
4566
|
-
await unlink(lock);
|
|
4567
|
-
} catch {
|
|
4568
|
-
}
|
|
4569
|
-
};
|
|
4570
|
-
} catch (e) {
|
|
4571
|
-
const err = e;
|
|
4572
|
-
if (err.code !== "EEXIST") throw e;
|
|
4573
|
-
try {
|
|
4574
|
-
const st = await stat3(lock);
|
|
4575
|
-
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
4576
|
-
await unlink(lock).catch(() => void 0);
|
|
4577
|
-
continue;
|
|
4578
|
-
}
|
|
4579
|
-
} catch {
|
|
4580
|
-
continue;
|
|
4581
|
-
}
|
|
4582
|
-
if (Date.now() - started > LOCK_WAIT_MS) {
|
|
4583
|
-
throw new Error(`timed out waiting for state lock ${lock}`);
|
|
4584
|
-
}
|
|
4585
|
-
await sleep(20);
|
|
4586
|
-
}
|
|
4587
|
-
}
|
|
4588
|
-
}
|
|
4589
|
-
};
|
|
4590
|
-
function forgetWorktreeTargets(state, worktreePath) {
|
|
4591
|
-
const prefix = `worktree:${worktreePath}:`;
|
|
4592
|
-
const gone = /* @__PURE__ */ new Set();
|
|
4593
|
-
for (const key of Object.keys(state.targets)) {
|
|
4594
|
-
if (!key.startsWith(prefix)) continue;
|
|
4595
|
-
for (const c of state.targets[key].comments) gone.add(c.id);
|
|
4596
|
-
delete state.targets[key];
|
|
4597
|
-
}
|
|
4598
|
-
if (gone.size) for (const issue of state.issues) issue.commentIds = issue.commentIds.filter((id) => !gone.has(id));
|
|
4599
|
-
state.checkpoints = state.checkpoints.filter((c) => c.worktree !== worktreePath);
|
|
4600
|
-
}
|
|
4601
|
-
|
|
4602
4638
|
// packages/server/src/export.ts
|
|
4603
4639
|
var LANG_BY_EXT = {
|
|
4604
4640
|
ts: "ts",
|
|
@@ -4646,29 +4682,116 @@ function langForPath(filePath) {
|
|
|
4646
4682
|
if (!m?.[1]) return "";
|
|
4647
4683
|
return LANG_BY_EXT[m[1].toLowerCase()] ?? m[1].toLowerCase();
|
|
4648
4684
|
}
|
|
4685
|
+
function shortId(id) {
|
|
4686
|
+
return id.slice(0, 8);
|
|
4687
|
+
}
|
|
4688
|
+
var quote = (text) => text.replace(/\r\n/g, "\n").split("\n").map((l) => l ? `> ${l}` : ">").join("\n");
|
|
4649
4689
|
function formatCommentSection(c) {
|
|
4650
4690
|
const range = c.startLine === c.endLine ? `${c.startLine}` : `${c.startLine}-${c.endLine}`;
|
|
4651
4691
|
const width = String(c.endLine).length;
|
|
4652
4692
|
const code = c.codeSnippet.map((line, i) => `${String(c.startLine + i).padStart(width, " ")} | ${line}`).join("\n");
|
|
4653
4693
|
const lang = langForPath(c.filePath);
|
|
4654
|
-
const body = c.body.replace(/\r\n/g, "\n").split("\n").map((l) => l ? `> ${l}` : ">").join("\n");
|
|
4655
4694
|
const fence = code.includes("```") ? "````" : "```";
|
|
4656
4695
|
const orphanNote = c.status === "orphaned" ? " [orphaned: original location no longer exists]" : "";
|
|
4657
|
-
|
|
4696
|
+
const parts = [`## ${c.filePath}:${range} (${c.side}) [id: ${shortId(c.id)}]${orphanNote}`, `${fence}${lang}`, code, fence, quote(c.body)];
|
|
4697
|
+
for (const r of c.replies ?? []) parts.push(`${r.author === "agent" ? "Agent" : "Reviewer"} replied:
|
|
4698
|
+
${quote(r.body)}`);
|
|
4699
|
+
return parts.join("\n");
|
|
4700
|
+
}
|
|
4701
|
+
function replyFooter(replyCommand) {
|
|
4702
|
+
return `When you have dealt with a comment, answer it with \`${replyCommand} reply <id> "<what you changed, or why you did not>"\` so the reviewer sees your answer beside it.`;
|
|
4658
4703
|
}
|
|
4659
|
-
function formatCommentsExport({ repoRoot, comments }) {
|
|
4704
|
+
function formatCommentsExport({ repoRoot, comments, replyCommand }) {
|
|
4660
4705
|
const targets = [...new Set(comments.map((c) => c.targetKey))];
|
|
4661
4706
|
const head = ["# Review comments", `Target: ${targets.join(", ") || "-"}`, `Repo: ${repoRoot}`, `Count: ${comments.length}`];
|
|
4662
4707
|
const sections = comments.map(formatCommentSection);
|
|
4663
|
-
|
|
4708
|
+
const tail = replyCommand && comments.length > 0 ? [replyFooter(replyCommand)] : [];
|
|
4709
|
+
return [head.join("\n"), ...sections, ...tail].join("\n\n") + "\n";
|
|
4664
4710
|
}
|
|
4665
|
-
function
|
|
4666
|
-
const
|
|
4667
|
-
const
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4711
|
+
function formatTodoExport({ todo, comments, replyCommand }) {
|
|
4712
|
+
const body = todo.body.replace(/\r\n/g, "\n").trim();
|
|
4713
|
+
const parts = [todo.title, ...body ? [body] : [], ...comments.map(formatCommentSection)];
|
|
4714
|
+
if (replyCommand && comments.length > 0) parts.push(replyFooter(replyCommand));
|
|
4715
|
+
return parts.join("\n\n");
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
// packages/server/src/feedback.ts
|
|
4719
|
+
import { realpath as realpath3 } from "fs/promises";
|
|
4720
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4721
|
+
async function real(p) {
|
|
4722
|
+
try {
|
|
4723
|
+
return await realpath3(p);
|
|
4724
|
+
} catch {
|
|
4725
|
+
return p;
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
function worktreeOf(c, mainRoot) {
|
|
4729
|
+
return commentWorktree(c) ?? mainRoot;
|
|
4730
|
+
}
|
|
4731
|
+
async function commentsIn(state, worktreeRoot, mainRoot) {
|
|
4732
|
+
const want = await real(worktreeRoot);
|
|
4733
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
4734
|
+
const out = [];
|
|
4735
|
+
for (const t of Object.values(state.targets)) {
|
|
4736
|
+
for (const c of t.comments) {
|
|
4737
|
+
const wt = worktreeOf(c, mainRoot);
|
|
4738
|
+
let p = resolved.get(wt);
|
|
4739
|
+
if (!p) {
|
|
4740
|
+
p = real(wt);
|
|
4741
|
+
resolved.set(wt, p);
|
|
4742
|
+
}
|
|
4743
|
+
if (await p === want) out.push(c);
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
return out;
|
|
4747
|
+
}
|
|
4748
|
+
async function takeFeedback(store, opts) {
|
|
4749
|
+
if (opts.peek) return (await commentsIn(await store.load(), opts.worktreeRoot, opts.mainRoot)).filter(awaitsAgent);
|
|
4750
|
+
return store.update(async (s) => {
|
|
4751
|
+
const ids = new Set((await commentsIn(s, opts.worktreeRoot, opts.mainRoot)).filter(awaitsAgent).map((c) => c.id));
|
|
4752
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4753
|
+
const taken = [];
|
|
4754
|
+
for (const t of Object.values(s.targets)) {
|
|
4755
|
+
t.comments = t.comments.map((c) => {
|
|
4756
|
+
if (!ids.has(c.id)) return c;
|
|
4757
|
+
const next = { ...c, exportedAt: now, updatedAt: now, status: c.status === "active" ? "exported" : c.status };
|
|
4758
|
+
taken.push(next);
|
|
4759
|
+
return next;
|
|
4760
|
+
});
|
|
4761
|
+
}
|
|
4762
|
+
return taken;
|
|
4763
|
+
});
|
|
4764
|
+
}
|
|
4765
|
+
function locate(state, ref) {
|
|
4766
|
+
const needle = ref.trim().toLowerCase();
|
|
4767
|
+
if (!needle) throw badRequest("a comment id is required", "bad_comment_id");
|
|
4768
|
+
const hits = [];
|
|
4769
|
+
for (const t of Object.values(state.targets)) {
|
|
4770
|
+
t.comments.forEach((c, index) => {
|
|
4771
|
+
if (c.id === needle) hits.unshift({ list: t.comments, index });
|
|
4772
|
+
else if (c.id.startsWith(needle)) hits.push({ list: t.comments, index });
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
const exact = hits[0] && hits[0].list[hits[0].index].id === needle;
|
|
4776
|
+
if (hits.length === 0) throw notFound(`no comment with id ${ref}; it may have been resolved or deleted`, "unknown_comment");
|
|
4777
|
+
if (!exact && hits.length > 1) throw badRequest(`id ${ref} matches ${hits.length} comments; give more of it`, "ambiguous_comment");
|
|
4778
|
+
return hits[0];
|
|
4779
|
+
}
|
|
4780
|
+
async function addReply(store, ref, author, body) {
|
|
4781
|
+
if (!body.trim()) throw badRequest("reply must not be empty", "empty_reply");
|
|
4782
|
+
return store.update((s) => {
|
|
4783
|
+
const { list, index } = locate(s, ref);
|
|
4784
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4785
|
+
const reply2 = { id: randomUUID2(), author, body: body.trim(), at: now };
|
|
4786
|
+
const prev = list[index];
|
|
4787
|
+
const next = { ...prev, replies: [...prev.replies ?? [], reply2], updatedAt: now };
|
|
4788
|
+
if (author === "reviewer") {
|
|
4789
|
+
delete next.exportedAt;
|
|
4790
|
+
if (next.status === "exported") next.status = "active";
|
|
4791
|
+
}
|
|
4792
|
+
list[index] = next;
|
|
4793
|
+
return next;
|
|
4794
|
+
});
|
|
4672
4795
|
}
|
|
4673
4796
|
|
|
4674
4797
|
// packages/server/src/nvim.ts
|
|
@@ -4792,7 +4915,7 @@ var NvimService = class {
|
|
|
4792
4915
|
|
|
4793
4916
|
// packages/server/src/tmux.ts
|
|
4794
4917
|
import { execFile as execFile3 } from "child_process";
|
|
4795
|
-
import { realpath as
|
|
4918
|
+
import { realpath as realpath4 } from "fs/promises";
|
|
4796
4919
|
var runTmux = (args) => new Promise((resolve, reject) => {
|
|
4797
4920
|
execFile3("tmux", args, { encoding: "utf8", timeout: 5e3, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
4798
4921
|
if (!error) return resolve(stdout);
|
|
@@ -4808,21 +4931,21 @@ var TmuxService = class {
|
|
|
4808
4931
|
}
|
|
4809
4932
|
run;
|
|
4810
4933
|
async sessions(mainRoot) {
|
|
4811
|
-
const root = await
|
|
4934
|
+
const root = await realpath4(mainRoot);
|
|
4812
4935
|
const output = await this.run(["list-sessions", "-F", "#{session_id} #{session_name} #{session_path}"]);
|
|
4813
4936
|
const sessions = [];
|
|
4814
4937
|
for (const line of output.trimEnd().split("\n")) {
|
|
4815
4938
|
const [id, name, ...parts] = line.split(" ");
|
|
4816
4939
|
const path12 = parts.join(" ");
|
|
4817
4940
|
if (!id || !/^\$\d+$/.test(id) || !name || !path12) continue;
|
|
4818
|
-
if (await
|
|
4941
|
+
if (await realpath4(path12).catch(() => null) === root) sessions.push({ id, name, path: path12 });
|
|
4819
4942
|
}
|
|
4820
4943
|
return sessions;
|
|
4821
4944
|
}
|
|
4822
4945
|
async open(mainRoot, worktreePath, sessionId) {
|
|
4823
4946
|
const session = (await this.sessions(mainRoot)).find((s) => s.id === sessionId);
|
|
4824
4947
|
if (!session) throw new HttpError(409, "\u4E3B\u4ED3\u5E93\u5BF9\u5E94\u7684 tmux session \u5DF2\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5", "tmux_session_missing");
|
|
4825
|
-
const directory = await
|
|
4948
|
+
const directory = await realpath4(worktreePath);
|
|
4826
4949
|
const window = (await this.run(["new-window", "-d", "-P", "-F", "#{window_id}", "-t", `${session.id}:`, "-c", directory.replaceAll("#", "##")])).trim();
|
|
4827
4950
|
return { session: session.name, window };
|
|
4828
4951
|
}
|
|
@@ -4990,6 +5113,7 @@ async function serveStaticFile(c, webDir) {
|
|
|
4990
5113
|
|
|
4991
5114
|
// packages/server/src/app.ts
|
|
4992
5115
|
var SSE_HEARTBEAT_MS = 15e3;
|
|
5116
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
|
|
4993
5117
|
var LISTING_HINT_TTL_MS = 5 * 6e4;
|
|
4994
5118
|
function createApp(opts) {
|
|
4995
5119
|
const { repo, store } = opts;
|
|
@@ -5014,6 +5138,10 @@ function createApp(opts) {
|
|
|
5014
5138
|
return list;
|
|
5015
5139
|
};
|
|
5016
5140
|
const checkpoints = checkpointStore(store.file);
|
|
5141
|
+
const stateStamp = () => stat7(store.file).then(
|
|
5142
|
+
(st) => st.mtimeMs,
|
|
5143
|
+
() => 0
|
|
5144
|
+
);
|
|
5017
5145
|
const worktreeCtx = async (key) => {
|
|
5018
5146
|
let list = await worktrees();
|
|
5019
5147
|
try {
|
|
@@ -5041,6 +5169,14 @@ function createApp(opts) {
|
|
|
5041
5169
|
untracked: explicit?.untracked ?? hint?.untracked
|
|
5042
5170
|
});
|
|
5043
5171
|
};
|
|
5172
|
+
const checkpointHandoff = (comments) => {
|
|
5173
|
+
const worktrees2 = new Set(comments.map((x) => tryParseTargetKey(x.targetKey)?.worktree));
|
|
5174
|
+
for (const wt of worktrees2) {
|
|
5175
|
+
takeCheckpoint(store, wt ?? repo.root, wt, { handoff: true }).catch((e) => {
|
|
5176
|
+
console.error(`warden: no checkpoint for the comments handed over in ${wt ?? repo.root}: ${e instanceof Error ? e.message : e}`);
|
|
5177
|
+
});
|
|
5178
|
+
}
|
|
5179
|
+
};
|
|
5044
5180
|
const findComment = (state, id) => {
|
|
5045
5181
|
for (const [targetKey, t] of Object.entries(state.targets)) {
|
|
5046
5182
|
const index = t.comments.findIndex((c) => c.id === id);
|
|
@@ -5048,6 +5184,13 @@ function createApp(opts) {
|
|
|
5048
5184
|
}
|
|
5049
5185
|
return void 0;
|
|
5050
5186
|
};
|
|
5187
|
+
const linkable = (state, raw2) => {
|
|
5188
|
+
if (!Array.isArray(raw2) || raw2.some((x) => typeof x !== "string")) throw badRequest("commentIds must be a list of ids");
|
|
5189
|
+
const ids = [...new Set(raw2)];
|
|
5190
|
+
const missing = ids.find((x) => !findComment(state, x));
|
|
5191
|
+
if (missing) throw notFound(`no comment with id ${missing}`, "unknown_comment");
|
|
5192
|
+
return ids;
|
|
5193
|
+
};
|
|
5051
5194
|
app.onError((err, c) => {
|
|
5052
5195
|
if (err instanceof HttpError) {
|
|
5053
5196
|
return c.json({ error: err.message, code: err.code }, err.status);
|
|
@@ -5055,10 +5198,20 @@ function createApp(opts) {
|
|
|
5055
5198
|
console.error(err);
|
|
5056
5199
|
return c.json({ error: err instanceof Error ? err.message : String(err), code: "internal" }, 500);
|
|
5057
5200
|
});
|
|
5201
|
+
app.use("*", async (c, next) => {
|
|
5202
|
+
const host = new URL(c.req.url).hostname;
|
|
5203
|
+
if (!LOOPBACK_HOSTS.has(host)) {
|
|
5204
|
+
throw new HttpError(403, `requests for host ${host} are refused; open warden at http://127.0.0.1 or http://localhost`, "bad_host");
|
|
5205
|
+
}
|
|
5206
|
+
await next();
|
|
5207
|
+
});
|
|
5058
5208
|
const api = new Hono2();
|
|
5059
5209
|
api.use("*", async (c, next) => {
|
|
5060
|
-
if (c.req.method !== "GET" && c.req.method !== "HEAD"
|
|
5061
|
-
|
|
5210
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
5211
|
+
const origin = c.req.header("origin");
|
|
5212
|
+
if (c.req.header("sec-fetch-site") === "cross-site" || origin && origin !== new URL(c.req.url).origin) {
|
|
5213
|
+
throw new HttpError(403, "cross-site requests are refused", "cross_site");
|
|
5214
|
+
}
|
|
5062
5215
|
}
|
|
5063
5216
|
await next();
|
|
5064
5217
|
});
|
|
@@ -5175,15 +5328,9 @@ function createApp(opts) {
|
|
|
5175
5328
|
});
|
|
5176
5329
|
api.post("/targets/:key/checkpoints", async (c) => {
|
|
5177
5330
|
const ctx = await worktreeCtx(c.req.param("key"));
|
|
5178
|
-
const
|
|
5179
|
-
const
|
|
5180
|
-
|
|
5181
|
-
const newest = checkpointsOf(s, ctx.target.worktree).at(-1);
|
|
5182
|
-
const unchanged = newest?.tree === tree;
|
|
5183
|
-
const checkpoint = unchanged ? newest : addCheckpoint(s, ctx.target.worktree, tree, head);
|
|
5184
|
-
return { checkpoint, unchanged, targetKey: checkpointKey(checkpoint) };
|
|
5185
|
-
});
|
|
5186
|
-
return c.json(res, res.unchanged ? 200 : 201);
|
|
5331
|
+
const { checkpoint, unchanged } = await takeCheckpoint(store, ctx.cwd, ctx.target.worktree);
|
|
5332
|
+
const res = { checkpoint, unchanged, targetKey: checkpointKey(checkpoint) };
|
|
5333
|
+
return c.json(res, unchanged ? 200 : 201);
|
|
5187
5334
|
});
|
|
5188
5335
|
api.delete("/targets/:key/checkpoints/:id", async (c) => {
|
|
5189
5336
|
const ctx = await worktreeCtx(c.req.param("key"));
|
|
@@ -5236,7 +5383,7 @@ function createApp(opts) {
|
|
|
5236
5383
|
if (!anchored) throw badRequest("selection is not fully visible in the diff on that side (must be inside one hunk)", "bad_selection");
|
|
5237
5384
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5238
5385
|
const comment = {
|
|
5239
|
-
id:
|
|
5386
|
+
id: randomUUID3(),
|
|
5240
5387
|
targetKey: key,
|
|
5241
5388
|
filePath: body.filePath,
|
|
5242
5389
|
side: body.side,
|
|
@@ -5307,12 +5454,19 @@ function createApp(opts) {
|
|
|
5307
5454
|
if (!t) return false;
|
|
5308
5455
|
const before = t.comments.length;
|
|
5309
5456
|
t.comments = t.comments.filter((x) => x.id !== id);
|
|
5310
|
-
|
|
5457
|
+
unlinkComments(s, [id]);
|
|
5311
5458
|
return t.comments.length !== before;
|
|
5312
5459
|
});
|
|
5313
5460
|
if (!removed) throw notFound("comment not found");
|
|
5314
5461
|
return c.json({ ok: true });
|
|
5315
5462
|
});
|
|
5463
|
+
api.post("/targets/:key/comments/:id/replies", async (c) => {
|
|
5464
|
+
const body = await c.req.json();
|
|
5465
|
+
if (typeof body.body !== "string") throw badRequest("body is required");
|
|
5466
|
+
const found = findComment(await store.load(), c.req.param("id"));
|
|
5467
|
+
if (!found || found.targetKey !== commentScopeKey(c.req.param("key"))) throw notFound("comment not found");
|
|
5468
|
+
return c.json(await addReply(store, found.comment.id, "reviewer", body.body), 201);
|
|
5469
|
+
});
|
|
5316
5470
|
api.post("/targets/:key/comments/reanchor", async (c) => {
|
|
5317
5471
|
const key = c.req.param("key");
|
|
5318
5472
|
const ctx = await targetCtx(key);
|
|
@@ -5384,7 +5538,7 @@ function createApp(opts) {
|
|
|
5384
5538
|
const patch = located.get(cm.id);
|
|
5385
5539
|
if (!patch) {
|
|
5386
5540
|
if (!comments.some((x) => x.id === cm.id)) return [cm];
|
|
5387
|
-
if (committed) {
|
|
5541
|
+
if (committed && !cm.replies?.length) {
|
|
5388
5542
|
dropped.push(cm.id);
|
|
5389
5543
|
return [];
|
|
5390
5544
|
}
|
|
@@ -5394,9 +5548,7 @@ function createApp(opts) {
|
|
|
5394
5548
|
const unchanged = cm.targetKey === next.targetKey && cm.status === next.status && cm.startLine === next.startLine && cm.endLine === next.endLine && cm.anchor.hunkHash === next.anchor.hunkHash;
|
|
5395
5549
|
return [unchanged ? cm : { ...next, updatedAt: now }];
|
|
5396
5550
|
});
|
|
5397
|
-
|
|
5398
|
-
for (const issue of s.issues) issue.commentIds = issue.commentIds.filter((x) => x !== id);
|
|
5399
|
-
}
|
|
5551
|
+
unlinkComments(s, dropped);
|
|
5400
5552
|
t.head = head;
|
|
5401
5553
|
return t.comments;
|
|
5402
5554
|
});
|
|
@@ -5407,6 +5559,7 @@ function createApp(opts) {
|
|
|
5407
5559
|
const body = await c.req.json();
|
|
5408
5560
|
if (!Array.isArray(body.commentIds)) throw badRequest("commentIds required");
|
|
5409
5561
|
const ids = body.commentIds.filter((x) => typeof x === "string");
|
|
5562
|
+
let handed = [];
|
|
5410
5563
|
const res = await store.update((s) => {
|
|
5411
5564
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5412
5565
|
const selected = [];
|
|
@@ -5418,94 +5571,14 @@ function createApp(opts) {
|
|
|
5418
5571
|
s.targets[found.targetKey].comments[found.index] = next;
|
|
5419
5572
|
selected.push(next);
|
|
5420
5573
|
}
|
|
5574
|
+
handed = selected;
|
|
5421
5575
|
return {
|
|
5422
|
-
text: formatCommentsExport({ repoRoot: repo.root, comments: selected }),
|
|
5576
|
+
text: formatCommentsExport({ repoRoot: repo.root, comments: selected, replyCommand: opts.replyCommand }),
|
|
5423
5577
|
count: selected.length,
|
|
5424
5578
|
commentIds: selected.map((x) => x.id)
|
|
5425
5579
|
};
|
|
5426
5580
|
});
|
|
5427
|
-
|
|
5428
|
-
});
|
|
5429
|
-
api.get("/issues", async (c) => {
|
|
5430
|
-
const s = await store.load();
|
|
5431
|
-
return c.json({ issues: s.issues });
|
|
5432
|
-
});
|
|
5433
|
-
api.post("/issues", async (c) => {
|
|
5434
|
-
const body = await c.req.json();
|
|
5435
|
-
if (!body.title?.trim()) throw badRequest("title is required");
|
|
5436
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5437
|
-
const issue = {
|
|
5438
|
-
id: randomUUID2(),
|
|
5439
|
-
title: body.title.trim(),
|
|
5440
|
-
body: typeof body.body === "string" ? body.body : "",
|
|
5441
|
-
status: body.status === "closed" ? "closed" : "open",
|
|
5442
|
-
commentIds: Array.isArray(body.commentIds) ? body.commentIds.filter((x) => typeof x === "string") : [],
|
|
5443
|
-
createdAt: now,
|
|
5444
|
-
updatedAt: now
|
|
5445
|
-
};
|
|
5446
|
-
await store.update((s) => {
|
|
5447
|
-
s.issues = insertAfter(s.issues, issue, typeof body.after === "string" ? body.after : void 0);
|
|
5448
|
-
});
|
|
5449
|
-
return c.json(issue, 201);
|
|
5450
|
-
});
|
|
5451
|
-
api.post("/issues/:id/move", async (c) => {
|
|
5452
|
-
const id = c.req.param("id");
|
|
5453
|
-
const body = await c.req.json();
|
|
5454
|
-
if (body.before !== null && typeof body.before !== "string") throw badRequest("before must be an id or null");
|
|
5455
|
-
const issues = await store.update((s) => {
|
|
5456
|
-
const next = moveBefore(s.issues, id, body.before);
|
|
5457
|
-
if (!next) throw notFound("issue not found");
|
|
5458
|
-
s.issues = next;
|
|
5459
|
-
return s.issues;
|
|
5460
|
-
});
|
|
5461
|
-
return c.json({ issues });
|
|
5462
|
-
});
|
|
5463
|
-
api.patch("/issues/:id", async (c) => {
|
|
5464
|
-
const id = c.req.param("id");
|
|
5465
|
-
const body = await c.req.json();
|
|
5466
|
-
const updated = await store.update((s) => {
|
|
5467
|
-
const issue = s.issues.find((i) => i.id === id);
|
|
5468
|
-
if (!issue) throw notFound("issue not found");
|
|
5469
|
-
if (typeof body.title === "string" && body.title.trim()) issue.title = body.title.trim();
|
|
5470
|
-
if (typeof body.body === "string") issue.body = body.body;
|
|
5471
|
-
if (body.status === "open" || body.status === "closed") issue.status = body.status;
|
|
5472
|
-
if (Array.isArray(body.commentIds)) issue.commentIds = [...new Set(body.commentIds.filter((x) => typeof x === "string"))];
|
|
5473
|
-
issue.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5474
|
-
return issue;
|
|
5475
|
-
});
|
|
5476
|
-
return c.json(updated);
|
|
5477
|
-
});
|
|
5478
|
-
api.delete("/issues/:id", async (c) => {
|
|
5479
|
-
const id = c.req.param("id");
|
|
5480
|
-
const removed = await store.update((s) => {
|
|
5481
|
-
const before = s.issues.length;
|
|
5482
|
-
s.issues = s.issues.filter((i) => i.id !== id);
|
|
5483
|
-
return s.issues.length !== before;
|
|
5484
|
-
});
|
|
5485
|
-
if (!removed) throw notFound("issue not found");
|
|
5486
|
-
return c.json({ ok: true });
|
|
5487
|
-
});
|
|
5488
|
-
api.post("/issues/:id/export", async (c) => {
|
|
5489
|
-
const id = c.req.param("id");
|
|
5490
|
-
const res = await store.update((s) => {
|
|
5491
|
-
const issue = s.issues.find((i) => i.id === id);
|
|
5492
|
-
if (!issue) throw notFound("issue not found");
|
|
5493
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5494
|
-
const comments = [];
|
|
5495
|
-
for (const cid of issue.commentIds) {
|
|
5496
|
-
const found = findComment(s, cid);
|
|
5497
|
-
if (!found) continue;
|
|
5498
|
-
const next = { ...found.comment, exportedAt: now, updatedAt: now };
|
|
5499
|
-
if (next.status === "active") next.status = "exported";
|
|
5500
|
-
s.targets[found.targetKey].comments[found.index] = next;
|
|
5501
|
-
comments.push(next);
|
|
5502
|
-
}
|
|
5503
|
-
return {
|
|
5504
|
-
text: formatIssueExport({ repoRoot: repo.root, issue, comments }),
|
|
5505
|
-
count: comments.length,
|
|
5506
|
-
commentIds: comments.map((x) => x.id)
|
|
5507
|
-
};
|
|
5508
|
-
});
|
|
5581
|
+
checkpointHandoff(handed);
|
|
5509
5582
|
return c.json(res);
|
|
5510
5583
|
});
|
|
5511
5584
|
const knownRoot = async (rootParam) => {
|
|
@@ -5529,7 +5602,7 @@ function createApp(opts) {
|
|
|
5529
5602
|
const branch = body.branch?.trim() || await currentBranch(await knownRoot(body.root));
|
|
5530
5603
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5531
5604
|
const todo = {
|
|
5532
|
-
id:
|
|
5605
|
+
id: randomUUID3(),
|
|
5533
5606
|
branch,
|
|
5534
5607
|
title: body.title.trim(),
|
|
5535
5608
|
body: typeof body.body === "string" ? body.body : "",
|
|
@@ -5538,6 +5611,10 @@ function createApp(opts) {
|
|
|
5538
5611
|
updatedAt: now
|
|
5539
5612
|
};
|
|
5540
5613
|
await store.update((s) => {
|
|
5614
|
+
if (body.commentIds !== void 0) {
|
|
5615
|
+
const ids = linkable(s, body.commentIds);
|
|
5616
|
+
if (ids.length) todo.commentIds = ids;
|
|
5617
|
+
}
|
|
5541
5618
|
s.todos = insertAfter(s.todos, todo, typeof body.after === "string" ? body.after : void 0);
|
|
5542
5619
|
});
|
|
5543
5620
|
return c.json(todo, 201);
|
|
@@ -5564,11 +5641,40 @@ function createApp(opts) {
|
|
|
5564
5641
|
if (typeof body.title === "string" && body.title.trim()) todo.title = body.title.trim();
|
|
5565
5642
|
if (typeof body.body === "string") todo.body = body.body;
|
|
5566
5643
|
if (body.status === "open" || body.status === "done") todo.status = body.status;
|
|
5644
|
+
if (body.commentIds !== void 0) {
|
|
5645
|
+
const ids = linkable(s, body.commentIds);
|
|
5646
|
+
if (ids.length) todo.commentIds = ids;
|
|
5647
|
+
else delete todo.commentIds;
|
|
5648
|
+
}
|
|
5567
5649
|
todo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5568
5650
|
return todo;
|
|
5569
5651
|
});
|
|
5570
5652
|
return c.json(updated);
|
|
5571
5653
|
});
|
|
5654
|
+
api.post("/todos/:id/export", async (c) => {
|
|
5655
|
+
const id = c.req.param("id");
|
|
5656
|
+
const handed = [];
|
|
5657
|
+
const res = await store.update((s) => {
|
|
5658
|
+
const todo = s.todos.find((t) => t.id === id);
|
|
5659
|
+
if (!todo) throw notFound("todo not found");
|
|
5660
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5661
|
+
for (const cid of todo.commentIds ?? []) {
|
|
5662
|
+
const found = findComment(s, cid);
|
|
5663
|
+
if (!found) continue;
|
|
5664
|
+
const next = { ...found.comment, exportedAt: now, updatedAt: now };
|
|
5665
|
+
if (next.status === "active") next.status = "exported";
|
|
5666
|
+
s.targets[found.targetKey].comments[found.index] = next;
|
|
5667
|
+
handed.push(next);
|
|
5668
|
+
}
|
|
5669
|
+
return {
|
|
5670
|
+
text: formatTodoExport({ todo, comments: handed, replyCommand: opts.replyCommand }),
|
|
5671
|
+
count: handed.length,
|
|
5672
|
+
commentIds: handed.map((x) => x.id)
|
|
5673
|
+
};
|
|
5674
|
+
});
|
|
5675
|
+
checkpointHandoff(handed);
|
|
5676
|
+
return c.json(res);
|
|
5677
|
+
});
|
|
5572
5678
|
api.delete("/todos/:id", async (c) => {
|
|
5573
5679
|
const id = c.req.param("id");
|
|
5574
5680
|
const removed = await store.update((s) => {
|
|
@@ -5647,8 +5753,31 @@ function createApp(opts) {
|
|
|
5647
5753
|
return c.json(await tmux.open(repo.commonRoot, wt.path, body.sessionId), 201);
|
|
5648
5754
|
});
|
|
5649
5755
|
api.get("/worktrees", async (c) => {
|
|
5650
|
-
const list = await listWorktreesDetailed(repo);
|
|
5756
|
+
const [list, state] = await Promise.all([listWorktreesDetailed(repo), store.load()]);
|
|
5651
5757
|
const [branches, newSlot] = await Promise.all([listBranches(repo, list), nextSlot(repo, list)]);
|
|
5758
|
+
const review = /* @__PURE__ */ new Map();
|
|
5759
|
+
const latest = /* @__PURE__ */ new Map();
|
|
5760
|
+
for (const t of Object.values(state.targets)) {
|
|
5761
|
+
for (const cm of t.comments) {
|
|
5762
|
+
const at = commentWorktree(cm) ?? repo.root;
|
|
5763
|
+
const r = review.get(at) ?? { toAgent: 0, toReviewer: 0 };
|
|
5764
|
+
const seen = latest.get(at) ?? { agent: "", reviewer: "" };
|
|
5765
|
+
if (awaitsAgent(cm)) {
|
|
5766
|
+
r.toAgent++;
|
|
5767
|
+
if (cm.updatedAt >= seen.agent) [seen.agent, r.toAgentTarget] = [cm.updatedAt, cm.targetKey];
|
|
5768
|
+
}
|
|
5769
|
+
if (awaitsReviewer(cm)) {
|
|
5770
|
+
r.toReviewer++;
|
|
5771
|
+
if (cm.updatedAt >= seen.reviewer) [seen.reviewer, r.toReviewerTarget] = [cm.updatedAt, cm.targetKey];
|
|
5772
|
+
}
|
|
5773
|
+
review.set(at, r);
|
|
5774
|
+
latest.set(at, seen);
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
for (const wt of list) {
|
|
5778
|
+
const r = review.get(wt.path);
|
|
5779
|
+
if (r && (r.toAgent || r.toReviewer)) wt.review = r;
|
|
5780
|
+
}
|
|
5652
5781
|
const res = { worktrees: list, branches, newSlot };
|
|
5653
5782
|
return c.json(res);
|
|
5654
5783
|
});
|
|
@@ -5695,7 +5824,19 @@ function createApp(opts) {
|
|
|
5695
5824
|
const event = { type: "changed", ...change };
|
|
5696
5825
|
void enqueue(() => stream2.writeSSE({ event: "changed", data: JSON.stringify(event) }));
|
|
5697
5826
|
});
|
|
5698
|
-
|
|
5827
|
+
let stamp = await stateStamp();
|
|
5828
|
+
const statePoll = setInterval(() => {
|
|
5829
|
+
void stateStamp().then((next) => {
|
|
5830
|
+
if (next === stamp) return;
|
|
5831
|
+
stamp = next;
|
|
5832
|
+
const event = { type: "state", at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5833
|
+
void enqueue(() => stream2.writeSSE({ event: "state", data: JSON.stringify(event) }));
|
|
5834
|
+
});
|
|
5835
|
+
}, opts.watchIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
5836
|
+
stream2.onAbort(() => {
|
|
5837
|
+
clearInterval(statePoll);
|
|
5838
|
+
release();
|
|
5839
|
+
});
|
|
5699
5840
|
try {
|
|
5700
5841
|
while (!stream2.aborted && !stream2.closed) {
|
|
5701
5842
|
await stream2.sleep(SSE_HEARTBEAT_MS);
|
|
@@ -5703,6 +5844,7 @@ function createApp(opts) {
|
|
|
5703
5844
|
await enqueue(() => stream2.write(": ping\n\n"));
|
|
5704
5845
|
}
|
|
5705
5846
|
} finally {
|
|
5847
|
+
clearInterval(statePoll);
|
|
5706
5848
|
release();
|
|
5707
5849
|
}
|
|
5708
5850
|
});
|
|
@@ -5877,8 +6019,8 @@ async function startServer(opts) {
|
|
|
5877
6019
|
const repo = await resolveRepo(opts.repoPath);
|
|
5878
6020
|
const stateFile = opts.stateFile ?? stateFilePath(repo.commonRoot);
|
|
5879
6021
|
const store = new StateStore(stateFile, repo.commonRoot);
|
|
5880
|
-
const instanceToken =
|
|
5881
|
-
const app = createApp({ repo, store, nvim: new NvimService(), webDir: opts.webDir, instanceToken, update: opts.update });
|
|
6022
|
+
const instanceToken = randomUUID4();
|
|
6023
|
+
const app = createApp({ repo, store, nvim: new NvimService(), webDir: opts.webDir, instanceToken, update: opts.update, replyCommand: opts.replyCommand });
|
|
5882
6024
|
const listen = (p) => new Promise((resolve, reject) => {
|
|
5883
6025
|
const s = serve({ fetch: app.fetch, hostname: HOST, port: p }, () => resolve(s));
|
|
5884
6026
|
s.once("error", reject);
|
|
@@ -5920,8 +6062,68 @@ async function startServer(opts) {
|
|
|
5920
6062
|
};
|
|
5921
6063
|
}
|
|
5922
6064
|
|
|
6065
|
+
// bin/agent.ts
|
|
6066
|
+
var AGENT_COMMANDS = ["feedback", "reply"];
|
|
6067
|
+
function agentUsage(cmd) {
|
|
6068
|
+
return `Commands for the agent, run inside the worktree under review:
|
|
6069
|
+
${cmd} feedback [--peek] Print the review comments not yet handed over, and mark them handed
|
|
6070
|
+
over (--peek leaves them as they are)
|
|
6071
|
+
${cmd} reply <id> <message> Answer a comment; <id> is the one in its heading. A message of "-",
|
|
6072
|
+
or none with stdin piped, is read from stdin
|
|
6073
|
+
`;
|
|
6074
|
+
}
|
|
6075
|
+
async function readStdin() {
|
|
6076
|
+
const chunks = [];
|
|
6077
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
6078
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
6079
|
+
}
|
|
6080
|
+
async function openStore() {
|
|
6081
|
+
const repo = await resolveRepo(process.cwd());
|
|
6082
|
+
return { store: new StateStore(stateFilePath(repo.commonRoot), repo.commonRoot), root: repo.root, commonRoot: repo.commonRoot };
|
|
6083
|
+
}
|
|
6084
|
+
async function feedback(args, replyCommand) {
|
|
6085
|
+
const unknown = args.find((a) => a !== "--peek");
|
|
6086
|
+
if (unknown) throw new Error(`unknown argument: ${unknown}`);
|
|
6087
|
+
const { store, root, commonRoot } = await openStore();
|
|
6088
|
+
const peek = args.includes("--peek");
|
|
6089
|
+
const comments = await takeFeedback(store, { worktreeRoot: root, mainRoot: commonRoot, peek });
|
|
6090
|
+
if (comments.length === 0) {
|
|
6091
|
+
console.log("No review comments are waiting for you.");
|
|
6092
|
+
return;
|
|
6093
|
+
}
|
|
6094
|
+
if (!peek) {
|
|
6095
|
+
await takeCheckpoint(store, root, root === commonRoot ? void 0 : root, { handoff: true }).catch((e) => {
|
|
6096
|
+
console.error(`warden feedback: no checkpoint taken: ${e instanceof Error ? e.message : e}`);
|
|
6097
|
+
});
|
|
6098
|
+
}
|
|
6099
|
+
process.stdout.write(formatCommentsExport({ repoRoot: root, comments, replyCommand }));
|
|
6100
|
+
}
|
|
6101
|
+
async function reply(args) {
|
|
6102
|
+
const [id, ...words] = args;
|
|
6103
|
+
if (!id) throw new Error("reply needs a comment id");
|
|
6104
|
+
let body = words.join(" ");
|
|
6105
|
+
if (body === "-" || !body && !process.stdin.isTTY) body = await readStdin();
|
|
6106
|
+
if (!body.trim()) throw new Error("reply needs a message");
|
|
6107
|
+
const { store } = await openStore();
|
|
6108
|
+
const c = await addReply(store, id, "agent", body);
|
|
6109
|
+
const range = c.startLine === c.endLine ? `${c.startLine}` : `${c.startLine}-${c.endLine}`;
|
|
6110
|
+
console.log(`Replied to ${shortId(c.id)} (${c.filePath}:${range}).`);
|
|
6111
|
+
}
|
|
6112
|
+
async function runAgentCommand(argv, replyCommand) {
|
|
6113
|
+
const [cmd, ...rest] = argv;
|
|
6114
|
+
if (!cmd || !AGENT_COMMANDS.includes(cmd)) return void 0;
|
|
6115
|
+
try {
|
|
6116
|
+
if (cmd === "feedback") await feedback(rest, replyCommand);
|
|
6117
|
+
else await reply(rest);
|
|
6118
|
+
return 0;
|
|
6119
|
+
} catch (e) {
|
|
6120
|
+
console.error(`warden ${cmd}: ${e instanceof HttpError || e instanceof Error ? e.message : String(e)}`);
|
|
6121
|
+
return 1;
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
|
|
5923
6125
|
// bin/cli.ts
|
|
5924
|
-
var VERSION = true ? "0.
|
|
6126
|
+
var VERSION = true ? "0.16.0" : "dev";
|
|
5925
6127
|
function parseArgs(argv) {
|
|
5926
6128
|
const args = { repoPath: process.cwd(), open: true, updateCheck: true, help: false, version: false };
|
|
5927
6129
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -5944,10 +6146,13 @@ function parseArgs(argv) {
|
|
|
5944
6146
|
}
|
|
5945
6147
|
return args;
|
|
5946
6148
|
}
|
|
6149
|
+
var viaNpx = fileURLToPath(import.meta.url).includes(`${path11.sep}_npx${path11.sep}`);
|
|
6150
|
+
var selfCommand = viaNpx ? "npx @xbghc/warden" : "warden";
|
|
5947
6151
|
function usage() {
|
|
5948
6152
|
return `warden \u2014 local git diff review UI
|
|
5949
6153
|
|
|
5950
6154
|
Usage: warden [repoPath] [options]
|
|
6155
|
+
warden <command> [args]
|
|
5951
6156
|
|
|
5952
6157
|
Options:
|
|
5953
6158
|
--port <n>, -p <n> Listen on a fixed port (default: first free port from 4100, and under WSL
|
|
@@ -5957,6 +6162,9 @@ Options:
|
|
|
5957
6162
|
(also: WARDEN_NO_UPDATE_CHECK=1, NO_UPDATE_NOTIFIER=1, CI)
|
|
5958
6163
|
-h, --help Show this help
|
|
5959
6164
|
-v, --version Print version
|
|
6165
|
+
|
|
6166
|
+
${agentUsage("warden")}
|
|
6167
|
+
A repository in a directory named after a command is given as ./<name>.
|
|
5960
6168
|
`;
|
|
5961
6169
|
}
|
|
5962
6170
|
async function openBrowser(url) {
|
|
@@ -5999,6 +6207,8 @@ function resolveWebDir() {
|
|
|
5999
6207
|
return void 0;
|
|
6000
6208
|
}
|
|
6001
6209
|
async function main() {
|
|
6210
|
+
const code = await runAgentCommand(process.argv.slice(2), selfCommand);
|
|
6211
|
+
if (code !== void 0) process.exit(code);
|
|
6002
6212
|
let args;
|
|
6003
6213
|
try {
|
|
6004
6214
|
args = parseArgs(process.argv.slice(2));
|
|
@@ -6019,12 +6229,11 @@ async function main() {
|
|
|
6019
6229
|
if (!webDir) {
|
|
6020
6230
|
console.error("warning: built web assets not found (dist/web). Only the /api endpoints will be served.");
|
|
6021
6231
|
}
|
|
6022
|
-
const viaNpx = fileURLToPath(import.meta.url).includes(`${path11.sep}_npx${path11.sep}`);
|
|
6023
6232
|
const checking = args.updateCheck && VERSION !== "dev" && updateCheckEnabled();
|
|
6024
6233
|
const update = checking ? checkForUpdate({ current: VERSION, npx: viaNpx }) : Promise.resolve(null);
|
|
6025
6234
|
let server;
|
|
6026
6235
|
try {
|
|
6027
|
-
server = await startServer({ repoPath: args.repoPath, port: args.port, webDir, update });
|
|
6236
|
+
server = await startServer({ repoPath: args.repoPath, port: args.port, webDir, update, replyCommand: selfCommand });
|
|
6028
6237
|
} catch (e) {
|
|
6029
6238
|
console.error(`warden: ${e.message}`);
|
|
6030
6239
|
process.exit(1);
|