@xbghc/warden 0.6.0 → 0.6.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/README.md +6 -2
- package/dist/cli.js +46 -36
- package/dist/web/assets/{index-Bi9N5cIP.js → index-DxPObMgS.js} +39 -39
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,10 @@ Several instances on the same repository can run at the same time.
|
|
|
57
57
|
|
|
58
58
|
Refs accept anything git can resolve (`main`, `v1.2`, `HEAD~3`, a sha). `@` is `HEAD`.
|
|
59
59
|
Worktrees are discovered with `git worktree list` and share the review state of the main repository.
|
|
60
|
+
One whose directory is gone (git lists it as *prunable*) is not offered, and a remembered target
|
|
61
|
+
inside a removed worktree falls back to the working tree on the next load. The comments and viewed
|
|
62
|
+
flags kept under that worktree's key stay in the state file and come back if a worktree is created
|
|
63
|
+
at the same path again.
|
|
60
64
|
|
|
61
65
|
There is no target switcher in the UI: the sidebar's two blocks *are* the working tree, a commit is
|
|
62
66
|
picked from the *Commits* panel, and the *Branch vs* and *Range* fields at the top of that panel open
|
|
@@ -154,8 +158,8 @@ bound to a hash of the file's diff; when the diff changes the flag is dropped an
|
|
|
154
158
|
|
|
155
159
|
## Auto-refresh
|
|
156
160
|
|
|
157
|
-
warden polls the worktree every 1.5 s while the page is open (`git status --porcelain
|
|
158
|
-
branch name, plus mtime and size of the paths status reports — enough to notice a second edit to an
|
|
161
|
+
warden polls the worktree every 1.5 s while the page is open (`git status --porcelain
|
|
162
|
+
--untracked-files=all`, HEAD, the branch name, plus mtime and size of the paths status reports — enough to notice a second edit to an
|
|
159
163
|
already-modified file) and pushes a change event over `GET /api/events` (SSE). The page then re-runs
|
|
160
164
|
the normal refresh: re-anchor, reload both file lists, reload the open diff. Your draft comment,
|
|
161
165
|
selection, current view and current file are left alone, and the diff is scrolled back to where you
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// bin/cli.ts
|
|
4
4
|
import path8 from "path";
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
|
-
import { existsSync } from "fs";
|
|
6
|
+
import { existsSync as existsSync2 } from "fs";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
|
|
9
9
|
// packages/server/src/index.ts
|
|
@@ -3025,6 +3025,7 @@ function parseCommitLog(stdout) {
|
|
|
3025
3025
|
|
|
3026
3026
|
// packages/server/src/git.ts
|
|
3027
3027
|
import { execFile } from "child_process";
|
|
3028
|
+
import { existsSync } from "fs";
|
|
3028
3029
|
var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set(["rev-parse", "diff", "show", "log", "worktree", "ls-files", "status", "merge-base"]);
|
|
3029
3030
|
var FORBIDDEN_OPTION_PREFIXES = ["--output", "--ext-diff", "--textconv", "-c", "--config-env", "--exec-path", "--git-dir", "--work-tree"];
|
|
3030
3031
|
var DEFAULT_GIT_TIMEOUT_MS = 3e4;
|
|
@@ -3087,6 +3088,10 @@ async function runGit(args, opts) {
|
|
|
3087
3088
|
return;
|
|
3088
3089
|
}
|
|
3089
3090
|
if (err.code === "ENOENT") {
|
|
3091
|
+
if (opts.cwd && !existsSync(opts.cwd)) {
|
|
3092
|
+
reject(new GitError(`working directory no longer exists: ${opts.cwd}`, null, "", 400, "unknown_worktree"));
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3090
3095
|
reject(new GitError("git executable not found in PATH", null, "", 500, "git_missing"));
|
|
3091
3096
|
return;
|
|
3092
3097
|
}
|
|
@@ -3159,7 +3164,7 @@ async function listWorktrees(ctx) {
|
|
|
3159
3164
|
const out = [];
|
|
3160
3165
|
let cur = null;
|
|
3161
3166
|
const flush = () => {
|
|
3162
|
-
if (cur && cur.path) {
|
|
3167
|
+
if (cur && cur.path && !cur.prunable) {
|
|
3163
3168
|
out.push({
|
|
3164
3169
|
path: cur.path,
|
|
3165
3170
|
head: cur.head ?? "",
|
|
@@ -3185,6 +3190,7 @@ async function listWorktrees(ctx) {
|
|
|
3185
3190
|
else if (line.startsWith("branch ")) cur.branch = line.slice(7).replace(/^refs\/heads\//, "");
|
|
3186
3191
|
else if (line === "detached") cur.detached = true;
|
|
3187
3192
|
else if (line === "bare") cur.bare = true;
|
|
3193
|
+
else if (line.startsWith("prunable")) cur.prunable = true;
|
|
3188
3194
|
}
|
|
3189
3195
|
flush();
|
|
3190
3196
|
for (const wt of out) {
|
|
@@ -3387,11 +3393,11 @@ function parseUnifiedDiff(text) {
|
|
|
3387
3393
|
continue;
|
|
3388
3394
|
}
|
|
3389
3395
|
if (line.startsWith("--- ")) {
|
|
3390
|
-
c.fromMinus = stripPrefix(line.slice(4), "a/");
|
|
3396
|
+
c.fromMinus = stripPrefix(line.slice(4).replace(/\t$/, ""), "a/");
|
|
3391
3397
|
continue;
|
|
3392
3398
|
}
|
|
3393
3399
|
if (line.startsWith("+++ ")) {
|
|
3394
|
-
c.fromPlus = stripPrefix(line.slice(4), "b/");
|
|
3400
|
+
c.fromPlus = stripPrefix(line.slice(4).replace(/\t$/, ""), "b/");
|
|
3395
3401
|
continue;
|
|
3396
3402
|
}
|
|
3397
3403
|
if (line.startsWith("new file mode ")) {
|
|
@@ -3464,6 +3470,10 @@ function resolveTargetContext(repo, worktrees, key) {
|
|
|
3464
3470
|
}
|
|
3465
3471
|
return { key, target, cwd };
|
|
3466
3472
|
}
|
|
3473
|
+
function assertRepoPath(p) {
|
|
3474
|
+
if (!p || p.startsWith("/") || p.split("/").includes("..")) throw badRequest(`invalid path: ${p}`, "invalid_path");
|
|
3475
|
+
}
|
|
3476
|
+
var literal = (p) => `:(literal)${p}`;
|
|
3467
3477
|
async function hasHead(cwd) {
|
|
3468
3478
|
return refExists(cwd, "HEAD");
|
|
3469
3479
|
}
|
|
@@ -3491,6 +3501,7 @@ async function diffArgs(ctx) {
|
|
|
3491
3501
|
case "range": {
|
|
3492
3502
|
if (!await refExists(ctx.cwd, t.base)) throw badRequest(`unknown ref: ${t.base}`, "unknown_ref");
|
|
3493
3503
|
if (!await refExists(ctx.cwd, t.head)) throw badRequest(`unknown ref: ${t.head}`, "unknown_ref");
|
|
3504
|
+
if (!await mergeBase(ctx.cwd, t.base, t.head)) throw badRequest(`${t.base} and ${t.head} share no history`, "no_merge_base");
|
|
3494
3505
|
return [...DIFF_BASE_ARGS, `${t.base}...${t.head}`];
|
|
3495
3506
|
}
|
|
3496
3507
|
case "base":
|
|
@@ -3505,7 +3516,7 @@ async function listUntracked(cwd) {
|
|
|
3505
3516
|
return r.stdout.split("\0").filter(Boolean);
|
|
3506
3517
|
}
|
|
3507
3518
|
async function isUntracked(cwd, file) {
|
|
3508
|
-
const r = await runGit(["ls-files", "--others", "--exclude-standard", "-z", "--", file], { cwd });
|
|
3519
|
+
const r = await runGit(["ls-files", "--others", "--exclude-standard", "-z", "--", literal(file)], { cwd });
|
|
3509
3520
|
return r.stdout.split("\0").filter(Boolean).includes(file);
|
|
3510
3521
|
}
|
|
3511
3522
|
async function untrackedDiff(cwd, file) {
|
|
@@ -3551,15 +3562,14 @@ function toSummary(f) {
|
|
|
3551
3562
|
return rest;
|
|
3552
3563
|
}
|
|
3553
3564
|
async function getFileDiff(ctx, filePath, hints = {}) {
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
}
|
|
3565
|
+
assertRepoPath(filePath);
|
|
3566
|
+
if (hints.oldPath) assertRepoPath(hints.oldPath);
|
|
3557
3567
|
if (includesUntracked(ctx.target) && (hints.untracked || await isUntracked(ctx.cwd, filePath))) {
|
|
3558
3568
|
return untrackedDiff(ctx.cwd, filePath);
|
|
3559
3569
|
}
|
|
3560
3570
|
const args = await diffArgs(ctx);
|
|
3561
|
-
const pathspec = [filePath];
|
|
3562
|
-
if (hints.oldPath && hints.oldPath !== filePath) pathspec.push(hints.oldPath);
|
|
3571
|
+
const pathspec = [literal(filePath)];
|
|
3572
|
+
if (hints.oldPath && hints.oldPath !== filePath) pathspec.push(literal(hints.oldPath));
|
|
3563
3573
|
const r = await runGit([...args, "--", ...pathspec], { cwd: ctx.cwd });
|
|
3564
3574
|
const files = parseUnifiedDiff(r.stdout);
|
|
3565
3575
|
return files.find((f) => f.path === filePath) ?? files[0];
|
|
@@ -3583,9 +3593,7 @@ async function refForSide(ctx, side) {
|
|
|
3583
3593
|
}
|
|
3584
3594
|
}
|
|
3585
3595
|
async function getFullFile(ctx, filePath, side) {
|
|
3586
|
-
|
|
3587
|
-
throw badRequest(`invalid path: ${filePath}`, "invalid_path");
|
|
3588
|
-
}
|
|
3596
|
+
assertRepoPath(filePath);
|
|
3589
3597
|
const ref = await refForSide(ctx, side);
|
|
3590
3598
|
if (ref === void 0) {
|
|
3591
3599
|
try {
|
|
@@ -3751,6 +3759,8 @@ function ensureTarget(state, key) {
|
|
|
3751
3759
|
}
|
|
3752
3760
|
return t;
|
|
3753
3761
|
}
|
|
3762
|
+
var usable = (v) => !!v && typeof v === "object" && typeof v.id === "string";
|
|
3763
|
+
var usableComment = (v) => usable(v) && typeof v.anchor === "object" && !!v.anchor;
|
|
3754
3764
|
function normalise(raw2, repoRoot) {
|
|
3755
3765
|
const base = defaultState(repoRoot);
|
|
3756
3766
|
if (!raw2 || typeof raw2 !== "object") return base;
|
|
@@ -3761,7 +3771,7 @@ function normalise(raw2, repoRoot) {
|
|
|
3761
3771
|
if (!v || typeof v !== "object") continue;
|
|
3762
3772
|
targets[k] = {
|
|
3763
3773
|
viewed: typeof v.viewed === "object" && v.viewed ? v.viewed : {},
|
|
3764
|
-
comments: Array.isArray(v.comments) ? v.comments : [],
|
|
3774
|
+
comments: Array.isArray(v.comments) ? v.comments.filter(usableComment) : [],
|
|
3765
3775
|
...typeof v.head === "string" ? { head: v.head } : {}
|
|
3766
3776
|
};
|
|
3767
3777
|
}
|
|
@@ -3770,8 +3780,8 @@ function normalise(raw2, repoRoot) {
|
|
|
3770
3780
|
schemaVersion: 1,
|
|
3771
3781
|
repoRoot: r.repoRoot ?? repoRoot,
|
|
3772
3782
|
targets,
|
|
3773
|
-
issues: Array.isArray(r.issues) ? r.issues : [],
|
|
3774
|
-
todos: Array.isArray(r.todos) ? r.todos : [],
|
|
3783
|
+
issues: Array.isArray(r.issues) ? r.issues.filter(usable) : [],
|
|
3784
|
+
todos: Array.isArray(r.todos) ? r.todos.filter(usable) : [],
|
|
3775
3785
|
prefs: {
|
|
3776
3786
|
...defaultPrefs(),
|
|
3777
3787
|
...r.prefs ?? {},
|
|
@@ -4166,7 +4176,9 @@ var RepoWatcher = class {
|
|
|
4166
4176
|
}
|
|
4167
4177
|
async snapshot() {
|
|
4168
4178
|
const [statusRes, headRes, branch] = await Promise.all([
|
|
4169
|
-
|
|
4179
|
+
// Untracked files one by one: folded into their directory, an edit inside a directory the
|
|
4180
|
+
// agent just created would leave the directory's own mtime and size untouched.
|
|
4181
|
+
runGit(["status", "--porcelain", "-z", "--untracked-files=all"], { cwd: this.root }),
|
|
4170
4182
|
runGit(["rev-parse", "HEAD"], { cwd: this.root }).catch(() => ({ stdout: "" })),
|
|
4171
4183
|
// The same label getRepoInfo reports, so the branch identity the client filters todos by
|
|
4172
4184
|
// never flips between `HEAD` and a short sha depending on which of the two spoke last.
|
|
@@ -4275,13 +4287,6 @@ function createApp(opts) {
|
|
|
4275
4287
|
throw e;
|
|
4276
4288
|
}
|
|
4277
4289
|
};
|
|
4278
|
-
const decodeKey = (raw2) => {
|
|
4279
|
-
try {
|
|
4280
|
-
return decodeURIComponent(raw2);
|
|
4281
|
-
} catch {
|
|
4282
|
-
throw badRequest("malformed target key", "invalid_target");
|
|
4283
|
-
}
|
|
4284
|
-
};
|
|
4285
4290
|
const fileDiffWithHints = async (ctx, filePath, explicit) => {
|
|
4286
4291
|
const cached = listings.get(ctx.key);
|
|
4287
4292
|
const hint = cached && Date.now() - cached.at < LISTING_HINT_TTL_MS ? cached.files.find((f) => f.path === filePath) : void 0;
|
|
@@ -4308,8 +4313,13 @@ function createApp(opts) {
|
|
|
4308
4313
|
api.get("/ping", (c) => c.text(opts.instanceToken ?? ""));
|
|
4309
4314
|
api.get("/repo", async (c) => {
|
|
4310
4315
|
const state = await store.load();
|
|
4311
|
-
const info = await getRepoInfo(repo,
|
|
4316
|
+
const info = await getRepoInfo(repo, "working");
|
|
4312
4317
|
worktreeCache = { at: Date.now(), list: info.worktrees };
|
|
4318
|
+
const last = state.prefs.lastTarget;
|
|
4319
|
+
const target = last ? tryParseTargetKey(last) : void 0;
|
|
4320
|
+
const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree));
|
|
4321
|
+
if (last && usable2) info.defaultTarget = last;
|
|
4322
|
+
else if (last) await store.update((s) => void (s.prefs.lastTarget = "working"));
|
|
4313
4323
|
return c.json(info);
|
|
4314
4324
|
});
|
|
4315
4325
|
api.get("/state", async (c) => c.json(await store.load()));
|
|
@@ -4327,7 +4337,7 @@ function createApp(opts) {
|
|
|
4327
4337
|
return c.json(prefs);
|
|
4328
4338
|
});
|
|
4329
4339
|
api.get("/targets/:key/files", async (c) => {
|
|
4330
|
-
const key =
|
|
4340
|
+
const key = c.req.param("key");
|
|
4331
4341
|
const ctx = await targetCtx(key);
|
|
4332
4342
|
const diffs = await listTargetDiffs(ctx);
|
|
4333
4343
|
listings.set(key, { at: Date.now(), files: diffs });
|
|
@@ -4363,7 +4373,7 @@ function createApp(opts) {
|
|
|
4363
4373
|
return c.json(res);
|
|
4364
4374
|
});
|
|
4365
4375
|
api.get("/targets/:key/file", async (c) => {
|
|
4366
|
-
const key =
|
|
4376
|
+
const key = c.req.param("key");
|
|
4367
4377
|
const filePath = c.req.query("path");
|
|
4368
4378
|
if (!filePath) throw badRequest("missing path");
|
|
4369
4379
|
const ctx = await targetCtx(key);
|
|
@@ -4374,7 +4384,7 @@ function createApp(opts) {
|
|
|
4374
4384
|
return c.json(diff);
|
|
4375
4385
|
});
|
|
4376
4386
|
api.get("/targets/:key/file/full", async (c) => {
|
|
4377
|
-
const key =
|
|
4387
|
+
const key = c.req.param("key");
|
|
4378
4388
|
const filePath = c.req.query("path");
|
|
4379
4389
|
if (!filePath) throw badRequest("missing path");
|
|
4380
4390
|
const side = c.req.query("side") === "old" ? "old" : "new";
|
|
@@ -4384,7 +4394,7 @@ function createApp(opts) {
|
|
|
4384
4394
|
return c.json(res);
|
|
4385
4395
|
});
|
|
4386
4396
|
api.put("/targets/:key/viewed", async (c) => {
|
|
4387
|
-
const key =
|
|
4397
|
+
const key = c.req.param("key");
|
|
4388
4398
|
await targetCtx(key);
|
|
4389
4399
|
const body = await c.req.json();
|
|
4390
4400
|
if (!body.path) throw badRequest("missing path");
|
|
@@ -4398,12 +4408,12 @@ function createApp(opts) {
|
|
|
4398
4408
|
return c.json({ viewed });
|
|
4399
4409
|
});
|
|
4400
4410
|
api.get("/targets/:key/comments", async (c) => {
|
|
4401
|
-
const key =
|
|
4411
|
+
const key = c.req.param("key");
|
|
4402
4412
|
const state = await store.load();
|
|
4403
4413
|
return c.json({ comments: state.targets[commentScopeKey(key)]?.comments ?? [] });
|
|
4404
4414
|
});
|
|
4405
4415
|
api.post("/targets/:key/comments", async (c) => {
|
|
4406
|
-
const key =
|
|
4416
|
+
const key = c.req.param("key");
|
|
4407
4417
|
const ctx = await targetCtx(key);
|
|
4408
4418
|
const body = await c.req.json();
|
|
4409
4419
|
if (!body.filePath || typeof body.body !== "string" || !body.body.trim()) throw badRequest("filePath and body are required");
|
|
@@ -4436,7 +4446,7 @@ function createApp(opts) {
|
|
|
4436
4446
|
return c.json(comment, 201);
|
|
4437
4447
|
});
|
|
4438
4448
|
api.patch("/targets/:key/comments/:id", async (c) => {
|
|
4439
|
-
const key =
|
|
4449
|
+
const key = c.req.param("key");
|
|
4440
4450
|
const id = c.req.param("id");
|
|
4441
4451
|
const ctx = await targetCtx(key);
|
|
4442
4452
|
const body = await c.req.json();
|
|
@@ -4481,7 +4491,7 @@ function createApp(opts) {
|
|
|
4481
4491
|
return c.json(updated);
|
|
4482
4492
|
});
|
|
4483
4493
|
api.delete("/targets/:key/comments/:id", async (c) => {
|
|
4484
|
-
const key =
|
|
4494
|
+
const key = c.req.param("key");
|
|
4485
4495
|
const id = c.req.param("id");
|
|
4486
4496
|
const removed = await store.update((s) => {
|
|
4487
4497
|
const t = s.targets[commentScopeKey(key)];
|
|
@@ -4495,7 +4505,7 @@ function createApp(opts) {
|
|
|
4495
4505
|
return c.json({ ok: true });
|
|
4496
4506
|
});
|
|
4497
4507
|
api.post("/targets/:key/comments/reanchor", async (c) => {
|
|
4498
|
-
const key =
|
|
4508
|
+
const key = c.req.param("key");
|
|
4499
4509
|
const ctx = await targetCtx(key);
|
|
4500
4510
|
const scope = commentScopeKey(key);
|
|
4501
4511
|
const local = isLocalTarget(ctx.target);
|
|
@@ -4959,7 +4969,7 @@ async function startServer(opts) {
|
|
|
4959
4969
|
}
|
|
4960
4970
|
|
|
4961
4971
|
// bin/cli.ts
|
|
4962
|
-
var VERSION = true ? "0.6.
|
|
4972
|
+
var VERSION = true ? "0.6.1" : "dev";
|
|
4963
4973
|
function parseArgs(argv) {
|
|
4964
4974
|
const args = { repoPath: process.cwd(), open: true, help: false, version: false };
|
|
4965
4975
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -5029,7 +5039,7 @@ function resolveWebDir() {
|
|
|
5029
5039
|
// bin/cli.ts (tsx dev) -> dist/web
|
|
5030
5040
|
].filter((p) => !!p);
|
|
5031
5041
|
for (const dir of candidates) {
|
|
5032
|
-
if (
|
|
5042
|
+
if (existsSync2(path8.join(dir, "index.html"))) return dir;
|
|
5033
5043
|
}
|
|
5034
5044
|
return void 0;
|
|
5035
5045
|
}
|