diffing 0.1.2 → 0.2.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 +103 -7
- package/dist/cli-agent-BLPX3oOP.mjs +443 -0
- package/dist/cli-agent-BNwY5lZE.mjs +446 -0
- package/dist/cli-agent-CbzXDSSF.mjs +446 -0
- package/dist/cli-agent-DsqbS3n7.mjs +443 -0
- package/dist/cli-agent-Vbs-ePHv.mjs +443 -0
- package/dist/cli-agent-ZAkixf4c.mjs +446 -0
- package/dist/cli.mjs +954 -36
- package/dist/client/assets/{angular-html-BABKYppC.js → angular-html-DsssUIMZ.js} +1 -1
- package/dist/client/assets/{angular-ts-CtXLuOQb.js → angular-ts-dTg-sJ77.js} +1 -1
- package/dist/client/assets/{apl-DOi69evI.js → apl-C1yUKbqz.js} +1 -1
- package/dist/client/assets/{astro-J8Em2tfW.js → astro-BI07ha2y.js} +1 -1
- package/dist/client/assets/{blade-B_tOnqRD.js → blade-DkzUGffS.js} +1 -1
- package/dist/client/assets/{c-C0tRZWSi.js → c-KkuqqOvW.js} +1 -1
- package/dist/client/assets/{cobol-Dj_rkgIY.js → cobol-DRh7-jLi.js} +1 -1
- package/dist/client/assets/{coffee-C6Vq4BFF.js → coffee-wR_ZAh9a.js} +1 -1
- package/dist/client/assets/{cpp-Cno48fgF.js → cpp-OD_qyywn.js} +1 -1
- package/dist/client/assets/{crystal-C7tmDnU1.js → crystal-E4ahAddE.js} +1 -1
- package/dist/client/assets/{css-0v4o-iNQ.js → css-2NgE_uxz.js} +1 -1
- package/dist/client/assets/{edge-CvULxRem.js → edge-8mbscxxD.js} +1 -1
- package/dist/client/assets/{elixir-vRTauEw-.js → elixir-CJv1b3Q6.js} +1 -1
- package/dist/client/assets/{elm-fLFTu1xn.js → elm-DUPzqZ9H.js} +1 -1
- package/dist/client/assets/{erb-CtItcikB.js → erb-ssvEF26z.js} +1 -1
- package/dist/client/assets/{git-rebase-5MeRBXZD.js → git-rebase-DiWIK1Oa.js} +1 -1
- package/dist/client/assets/{glimmer-js-B7yA-u8_.js → glimmer-js-DrHg6AuA.js} +1 -1
- package/dist/client/assets/{glimmer-ts-B2g1OaHc.js → glimmer-ts-wQSGOa8A.js} +1 -1
- package/dist/client/assets/{glsl-CqNPWp5Y.js → glsl-OY27QGY0.js} +1 -1
- package/dist/client/assets/{graphql-CF78KbfF.js → graphql-CSWSqe6f.js} +1 -1
- package/dist/client/assets/{hack-Dz8fQ8O9.js → hack-Bi1v624Q.js} +1 -1
- package/dist/client/assets/{haml-3Gbl257r.js → haml-CX94zV9m.js} +1 -1
- package/dist/client/assets/{handlebars-5UZnh5L7.js → handlebars-C1j6wnjG.js} +1 -1
- package/dist/client/assets/{html-JOaQiIkL.js → html-BcAjMlB7.js} +1 -1
- package/dist/client/assets/{html-derivative-GyX6oYy3.js → html-derivative-DcEpOzdx.js} +1 -1
- package/dist/client/assets/{http-t6uVAxgS.js → http-BBxxSc6S.js} +1 -1
- package/dist/client/assets/{hurl-OLQZpg0E.js → hurl-CpKRNTz4.js} +1 -1
- package/dist/client/assets/{index-CghVgrAF.js → index-Bwv7BWUp.js} +181 -78
- package/dist/client/assets/index-CoZyJYkw.css +1 -0
- package/dist/client/assets/{java-BqL_yaya.js → java-CFUlLvAA.js} +1 -1
- package/dist/client/assets/{javascript-CxBNAxR5.js → javascript-C-HnT3sE.js} +1 -1
- package/dist/client/assets/{jinja-CuaK033Z.js → jinja-CPU8RwNP.js} +1 -1
- package/dist/client/assets/{jison-aL_w2vvo.js → jison-CW0TBQOs.js} +1 -1
- package/dist/client/assets/{json-Byruj-EC.js → json-qUjlNuEJ.js} +1 -1
- package/dist/client/assets/{jsx-CosY06uG.js → jsx-KWeo60kf.js} +1 -1
- package/dist/client/assets/{julia-u1y7fV63.js → julia-CnJB8y2e.js} +1 -1
- package/dist/client/assets/{just-DS4T9P_K.js → just-Do22d9PP.js} +1 -1
- package/dist/client/assets/{latex-C9c2IWW4.js → latex-CCPgzvKn.js} +1 -1
- package/dist/client/assets/{liquid-DcXQgkEV.js → liquid-B9btdZoI.js} +1 -1
- package/dist/client/assets/{lua-DWe0hUn1.js → lua-IFRifurW.js} +1 -1
- package/dist/client/assets/{marko-CIW7o2up.js → marko-CCaVBFq1.js} +1 -1
- package/dist/client/assets/{mdc-DKjGzb4k.js → mdc-BtbnOLUc.js} +1 -1
- package/dist/client/assets/{nginx-CBXzDlkk.js → nginx-BkbFZHVt.js} +1 -1
- package/dist/client/assets/{nim-CBf_vdee.js → nim-2uCd5j65.js} +1 -1
- package/dist/client/assets/{perl-xC9qc39I.js → perl-C4ixqSNT.js} +1 -1
- package/dist/client/assets/{php-n4vnn5oz.js → php-DJ8WOtcI.js} +1 -1
- package/dist/client/assets/{pug-Db536lA_.js → pug-BOaafhT5.js} +1 -1
- package/dist/client/assets/{qml-CrATLnNN.js → qml-CURE-1y4.js} +1 -1
- package/dist/client/assets/{r-DBQ-dydl.js → r-CPWmgzo3.js} +1 -1
- package/dist/client/assets/{razor-C_wvK1iP.js → razor-DH83VP_d.js} +1 -1
- package/dist/client/assets/{regexp-DowFj0Xd.js → regexp-BXVWi-t_.js} +1 -1
- package/dist/client/assets/{rst-rY4UPiwe.js → rst-Bbxxs6E2.js} +1 -1
- package/dist/client/assets/{ruby-C4cISY4u.js → ruby-C-TuWES4.js} +1 -1
- package/dist/client/assets/{sas-ui1PNNkD.js → sas-njzNYNOq.js} +1 -1
- package/dist/client/assets/{scss-BZrs95TC.js → scss-D2i6rHB8.js} +1 -1
- package/dist/client/assets/{shellscript-D9YVFS67.js → shellscript-D6Bxi40C.js} +1 -1
- package/dist/client/assets/{shellsession-DK7LMzYg.js → shellsession-Defxq-I6.js} +1 -1
- package/dist/client/assets/{soy-CNkc-Oji.js → soy-D0GKXrtT.js} +1 -1
- package/dist/client/assets/{sql-BvRdqBMa.js → sql-BRW_ADkU.js} +1 -1
- package/dist/client/assets/{stata-Bawjgtiz.js → stata-w7w59F48.js} +1 -1
- package/dist/client/assets/{surrealql-7v7lT5jp.js → surrealql-O-lLuqz7.js} +1 -1
- package/dist/client/assets/{svelte-p7w_kluv.js → svelte-DN3DR5-t.js} +1 -1
- package/dist/client/assets/{templ-0Gi_yZZr.js → templ-CJ2JFoia.js} +1 -1
- package/dist/client/assets/{tex-nZ_J3lks.js → tex-CpWrf5g9.js} +1 -1
- package/dist/client/assets/{ts-tags-CuG-Aj5j.js → ts-tags-CZ6Af6Pe.js} +1 -1
- package/dist/client/assets/{tsx-Cc7GNb_V.js → tsx-gIUSpRih.js} +1 -1
- package/dist/client/assets/{twig-BBlmihVM.js → twig-DwZINhmw.js} +1 -1
- package/dist/client/assets/{typescript-aiXwsZhm.js → typescript-Dj2aMECR.js} +1 -1
- package/dist/client/assets/{vue-Cs5tk2kn.js → vue-B5K0aWrP.js} +1 -1
- package/dist/client/assets/{vue-html-BEI_B8Ml.js → vue-html-DKGgrypd.js} +1 -1
- package/dist/client/assets/{vue-vine-CpWdOkP4.js → vue-vine-B5iDYTCh.js} +1 -1
- package/dist/client/assets/{xml-madLQSkS.js → xml-CbTzxfc-.js} +1 -1
- package/dist/client/assets/{xsl-CqZR37V9.js → xsl-Dj-Be6nx.js} +1 -1
- package/dist/client/assets/{yaml-DHr9ndbK.js → yaml-rTK8uEjh.js} +1 -1
- package/dist/client/index.html +2 -5
- package/dist/mcp-CE77qfjx.mjs +254 -0
- package/dist/mcp-CFeE13n-.mjs +254 -0
- package/dist/mcp-DKlThflu.mjs +253 -0
- package/dist/mcp-SGCrfYGo.mjs +254 -0
- package/dist/server-lock-B4crz9-_.mjs +708 -0
- package/dist/server-lock-CAdqFSfl.mjs +691 -0
- package/dist/server-lock-DpCrvL-Z.mjs +714 -0
- package/dist/update-check-DJuFE9w6.mjs +128 -0
- package/dist/update-check-DSC02MWQ.mjs +129 -0
- package/dist/update-check-KL-2sGxE.mjs +129 -0
- package/package.json +12 -14
- package/dist/client/assets/index-CmfB2R-C.css +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { _ as
|
|
2
|
+
import { C as revertHunk, S as listRepoFiles, T as toSafeRelativePath, _ as getRepoRoot, a as extractPlanLines, b as gitAddFile, c as formatComments, d as getFileContent, f as getGitDiffAsync, g as getRepoName, h as getProjectStorageDir, i as writeServerLock, l as getBranchName, m as getMergeStatus, o as formatPlanReview, p as getHunkHistory, r as removeServerLock, s as sectionTitleForLine, u as getCustomGitDiffAsync, v as getTabSizeForFiles, w as isSafePath, x as isGitRepo, y as getUntrackedFilePathsAsync } from "./server-lock-DpCrvL-Z.mjs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, extname, join, resolve } from "node:path";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
|
|
@@ -11,6 +11,7 @@ import { homedir } from "node:os";
|
|
|
11
11
|
import { Hono } from "hono";
|
|
12
12
|
import { streamSSE } from "hono/streaming";
|
|
13
13
|
import { serve } from "@hono/node-server";
|
|
14
|
+
import { stdout } from "node:process";
|
|
14
15
|
//#region src/lib/diff-options.ts
|
|
15
16
|
const DEFAULTS$1 = {
|
|
16
17
|
revisions: [],
|
|
@@ -1112,7 +1113,10 @@ const DEFAULTS = {
|
|
|
1112
1113
|
expandContextByDefault: false,
|
|
1113
1114
|
collapsedContextThreshold: 10,
|
|
1114
1115
|
expansionLineCount: 20,
|
|
1115
|
-
haptics: true
|
|
1116
|
+
haptics: true,
|
|
1117
|
+
sounds: true,
|
|
1118
|
+
uiFont: null,
|
|
1119
|
+
monoFont: null
|
|
1116
1120
|
};
|
|
1117
1121
|
function loadSettings() {
|
|
1118
1122
|
try {
|
|
@@ -1139,9 +1143,15 @@ function saveSettings(settings) {
|
|
|
1139
1143
|
var FileCommentStore = class {
|
|
1140
1144
|
dirPath;
|
|
1141
1145
|
filePath;
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1146
|
+
/**
|
|
1147
|
+
* @param storageDir Absolute directory to persist `comments.json` in.
|
|
1148
|
+
* Defaults to the per-repo storage dir under `~/.diffing` — comments are
|
|
1149
|
+
* NEVER written inside the reviewed (consumer) repo, so a consumer project
|
|
1150
|
+
* stays free of any diffing-specific artifacts. The override exists only so
|
|
1151
|
+
* tests can point at a throwaway temp dir.
|
|
1152
|
+
*/
|
|
1153
|
+
constructor(storageDir) {
|
|
1154
|
+
this.dirPath = storageDir ?? getProjectStorageDir();
|
|
1145
1155
|
this.filePath = join(this.dirPath, "comments.json");
|
|
1146
1156
|
}
|
|
1147
1157
|
async getAll() {
|
|
@@ -1222,6 +1232,212 @@ var FileCommentStore = class {
|
|
|
1222
1232
|
}
|
|
1223
1233
|
};
|
|
1224
1234
|
//#endregion
|
|
1235
|
+
//#region src/lib/plans.ts
|
|
1236
|
+
function newId() {
|
|
1237
|
+
return crypto.randomUUID();
|
|
1238
|
+
}
|
|
1239
|
+
/** Shared mutation helpers so both stores behave identically. */
|
|
1240
|
+
function applyUpsert(plans, input, now) {
|
|
1241
|
+
if (input.id) {
|
|
1242
|
+
const existing = plans.find((p) => p.id === input.id);
|
|
1243
|
+
if (existing) {
|
|
1244
|
+
existing.title = input.title;
|
|
1245
|
+
existing.body = input.body;
|
|
1246
|
+
if (input.source !== void 0) existing.source = input.source;
|
|
1247
|
+
if (input.model !== void 0) existing.model = input.model;
|
|
1248
|
+
existing.version += 1;
|
|
1249
|
+
existing.updatedAt = now;
|
|
1250
|
+
existing.decision = "pending";
|
|
1251
|
+
existing.decisionComment = void 0;
|
|
1252
|
+
existing.decidedAt = void 0;
|
|
1253
|
+
return existing;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const plan = {
|
|
1257
|
+
id: input.id || newId(),
|
|
1258
|
+
title: input.title,
|
|
1259
|
+
body: input.body,
|
|
1260
|
+
source: input.source,
|
|
1261
|
+
model: input.model,
|
|
1262
|
+
createdAt: now,
|
|
1263
|
+
updatedAt: now,
|
|
1264
|
+
version: 1,
|
|
1265
|
+
decision: "pending",
|
|
1266
|
+
comments: []
|
|
1267
|
+
};
|
|
1268
|
+
plans.push(plan);
|
|
1269
|
+
return plan;
|
|
1270
|
+
}
|
|
1271
|
+
var FilePlanStore = class {
|
|
1272
|
+
dirPath;
|
|
1273
|
+
filePath;
|
|
1274
|
+
/**
|
|
1275
|
+
* @param storageDir Absolute directory to persist `plans.json` in. Defaults
|
|
1276
|
+
* to the per-repo storage dir under `~/.diffing`, the same place comments
|
|
1277
|
+
* and media live — plans are NEVER written inside the reviewed (consumer)
|
|
1278
|
+
* repo, so a consumer project stays free of any diffing-specific artifacts.
|
|
1279
|
+
* The override exists only so tests can point at a throwaway temp dir.
|
|
1280
|
+
*/
|
|
1281
|
+
constructor(storageDir) {
|
|
1282
|
+
this.dirPath = storageDir ?? getProjectStorageDir();
|
|
1283
|
+
this.filePath = join(this.dirPath, "plans.json");
|
|
1284
|
+
}
|
|
1285
|
+
async getAll() {
|
|
1286
|
+
try {
|
|
1287
|
+
const data = await readFile(this.filePath, "utf-8");
|
|
1288
|
+
return JSON.parse(data);
|
|
1289
|
+
} catch {
|
|
1290
|
+
return [];
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async get(id) {
|
|
1294
|
+
return (await this.getAll()).find((p) => p.id === id) ?? null;
|
|
1295
|
+
}
|
|
1296
|
+
async save(plans) {
|
|
1297
|
+
try {
|
|
1298
|
+
await mkdir(this.dirPath, { recursive: true });
|
|
1299
|
+
try {
|
|
1300
|
+
const repoRoot = getRepoRoot();
|
|
1301
|
+
await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
|
|
1302
|
+
} catch {}
|
|
1303
|
+
await writeFile(this.filePath, JSON.stringify(plans, null, 2), "utf-8");
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
console.error("Failed to save plans to file:", err);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
async upsert(input) {
|
|
1309
|
+
const plans = await this.getAll();
|
|
1310
|
+
const plan = applyUpsert(plans, input, Date.now());
|
|
1311
|
+
await this.save(plans);
|
|
1312
|
+
return plan;
|
|
1313
|
+
}
|
|
1314
|
+
async update(id, fields) {
|
|
1315
|
+
const plans = await this.getAll();
|
|
1316
|
+
const plan = plans.find((p) => p.id === id);
|
|
1317
|
+
if (!plan) return null;
|
|
1318
|
+
if (fields.title !== void 0) plan.title = fields.title;
|
|
1319
|
+
if (fields.body !== void 0) plan.body = fields.body;
|
|
1320
|
+
if (fields.source !== void 0) plan.source = fields.source;
|
|
1321
|
+
if (fields.model !== void 0) plan.model = fields.model;
|
|
1322
|
+
plan.updatedAt = Date.now();
|
|
1323
|
+
await this.save(plans);
|
|
1324
|
+
return plan;
|
|
1325
|
+
}
|
|
1326
|
+
async remove(id) {
|
|
1327
|
+
const plans = await this.getAll();
|
|
1328
|
+
const idx = plans.findIndex((p) => p.id === id);
|
|
1329
|
+
if (idx === -1) return false;
|
|
1330
|
+
plans.splice(idx, 1);
|
|
1331
|
+
await this.save(plans);
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
async setDecision(id, decision, decisionComment) {
|
|
1335
|
+
const plans = await this.getAll();
|
|
1336
|
+
const plan = plans.find((p) => p.id === id);
|
|
1337
|
+
if (!plan) return null;
|
|
1338
|
+
plan.decision = decision;
|
|
1339
|
+
plan.decisionComment = decisionComment?.trim() || void 0;
|
|
1340
|
+
plan.decidedAt = Date.now();
|
|
1341
|
+
plan.updatedAt = plan.decidedAt;
|
|
1342
|
+
await this.save(plans);
|
|
1343
|
+
return plan;
|
|
1344
|
+
}
|
|
1345
|
+
async mutate(planId, fn) {
|
|
1346
|
+
const plans = await this.getAll();
|
|
1347
|
+
const plan = plans.find((p) => p.id === planId);
|
|
1348
|
+
if (!plan) return null;
|
|
1349
|
+
if (!plan.comments) plan.comments = [];
|
|
1350
|
+
if (!fn(plan)) return null;
|
|
1351
|
+
await this.save(plans);
|
|
1352
|
+
return plan;
|
|
1353
|
+
}
|
|
1354
|
+
async addComment(planId, comment) {
|
|
1355
|
+
return this.mutate(planId, (plan) => {
|
|
1356
|
+
plan.comments.push(comment);
|
|
1357
|
+
return true;
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
async updateComment(planId, commentId, fields) {
|
|
1361
|
+
return this.mutate(planId, (plan) => {
|
|
1362
|
+
const c = plan.comments.find((x) => x.id === commentId);
|
|
1363
|
+
if (!c) return false;
|
|
1364
|
+
if (fields.body !== void 0) c.body = fields.body;
|
|
1365
|
+
if (fields.status !== void 0) c.status = fields.status;
|
|
1366
|
+
return true;
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
async removeComment(planId, commentId) {
|
|
1370
|
+
return this.mutate(planId, (plan) => {
|
|
1371
|
+
const idx = plan.comments.findIndex((x) => x.id === commentId);
|
|
1372
|
+
if (idx === -1) return false;
|
|
1373
|
+
plan.comments.splice(idx, 1);
|
|
1374
|
+
return true;
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
async addReply(planId, commentId, reply) {
|
|
1378
|
+
return this.mutate(planId, (plan) => {
|
|
1379
|
+
const c = plan.comments.find((x) => x.id === commentId);
|
|
1380
|
+
if (!c) return false;
|
|
1381
|
+
if (!c.replies) c.replies = [];
|
|
1382
|
+
c.replies.push(reply);
|
|
1383
|
+
return true;
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
async removeReply(planId, commentId, replyId) {
|
|
1387
|
+
return this.mutate(planId, (plan) => {
|
|
1388
|
+
const c = plan.comments.find((x) => x.id === commentId);
|
|
1389
|
+
if (!c) return false;
|
|
1390
|
+
const idx = c.replies.findIndex((r) => r.id === replyId);
|
|
1391
|
+
if (idx === -1) return false;
|
|
1392
|
+
c.replies.splice(idx, 1);
|
|
1393
|
+
return true;
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
async updateReply(planId, commentId, replyId, body) {
|
|
1397
|
+
return this.mutate(planId, (plan) => {
|
|
1398
|
+
const c = plan.comments.find((x) => x.id === commentId);
|
|
1399
|
+
if (!c) return false;
|
|
1400
|
+
const reply = c.replies.find((r) => r.id === replyId);
|
|
1401
|
+
if (!reply) return false;
|
|
1402
|
+
reply.body = body;
|
|
1403
|
+
return true;
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
//#endregion
|
|
1408
|
+
//#region src/lib/state.ts
|
|
1409
|
+
var FileUiStateStore = class {
|
|
1410
|
+
dirPath;
|
|
1411
|
+
filePath;
|
|
1412
|
+
constructor(storageDir) {
|
|
1413
|
+
this.dirPath = storageDir ?? getProjectStorageDir();
|
|
1414
|
+
this.filePath = join(this.dirPath, "ui-state.json");
|
|
1415
|
+
}
|
|
1416
|
+
async getAll() {
|
|
1417
|
+
try {
|
|
1418
|
+
const data = await readFile(this.filePath, "utf-8");
|
|
1419
|
+
return JSON.parse(data);
|
|
1420
|
+
} catch {
|
|
1421
|
+
return {};
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
async setAll(state) {
|
|
1425
|
+
await this.save(state);
|
|
1426
|
+
}
|
|
1427
|
+
async save(state) {
|
|
1428
|
+
try {
|
|
1429
|
+
await mkdir(this.dirPath, { recursive: true });
|
|
1430
|
+
try {
|
|
1431
|
+
const repoRoot = getRepoRoot();
|
|
1432
|
+
await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
|
|
1433
|
+
} catch {}
|
|
1434
|
+
await writeFile(this.filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
1435
|
+
} catch (err) {
|
|
1436
|
+
console.error("Failed to save UI state to file:", err);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
//#endregion
|
|
1225
1441
|
//#region src/lib/review-session.ts
|
|
1226
1442
|
var ReviewSession = class {
|
|
1227
1443
|
round = 0;
|
|
@@ -1285,7 +1501,8 @@ var ReviewSession = class {
|
|
|
1285
1501
|
sentAt: input.sentAt,
|
|
1286
1502
|
commentXml: input.commentXml,
|
|
1287
1503
|
openCount: input.openCount,
|
|
1288
|
-
comments: input.comments
|
|
1504
|
+
comments: input.comments,
|
|
1505
|
+
decision: input.decision
|
|
1289
1506
|
};
|
|
1290
1507
|
this.lastPayload = payload;
|
|
1291
1508
|
const waiters = [...this.waiters];
|
|
@@ -1319,6 +1536,100 @@ var ReviewSession = class {
|
|
|
1319
1536
|
}
|
|
1320
1537
|
};
|
|
1321
1538
|
//#endregion
|
|
1539
|
+
//#region src/lib/plan-review-session.ts
|
|
1540
|
+
var PlanReviewSession = class {
|
|
1541
|
+
round = 0;
|
|
1542
|
+
lastDecidedAt = null;
|
|
1543
|
+
lastPayload = null;
|
|
1544
|
+
waiters = /* @__PURE__ */ new Set();
|
|
1545
|
+
onStatusChange;
|
|
1546
|
+
constructor(onStatusChange) {
|
|
1547
|
+
this.onStatusChange = onStatusChange;
|
|
1548
|
+
}
|
|
1549
|
+
/** Block until the next `decide()` or until `timeoutMs` elapses. */
|
|
1550
|
+
await(opts) {
|
|
1551
|
+
const { sinceRound, timeoutMs, signal } = opts;
|
|
1552
|
+
if (sinceRound !== void 0 && sinceRound < this.round && this.lastPayload) return Promise.resolve({
|
|
1553
|
+
status: "released",
|
|
1554
|
+
payload: this.lastPayload
|
|
1555
|
+
});
|
|
1556
|
+
if (signal?.aborted) return Promise.resolve({
|
|
1557
|
+
status: "keep-waiting",
|
|
1558
|
+
round: this.round
|
|
1559
|
+
});
|
|
1560
|
+
return new Promise((resolve) => {
|
|
1561
|
+
const waiter = {
|
|
1562
|
+
resolve,
|
|
1563
|
+
timer: setTimeout(() => {
|
|
1564
|
+
this.removeWaiter(waiter);
|
|
1565
|
+
resolve({
|
|
1566
|
+
status: "keep-waiting",
|
|
1567
|
+
round: this.round
|
|
1568
|
+
});
|
|
1569
|
+
}, timeoutMs),
|
|
1570
|
+
cleanup: () => {}
|
|
1571
|
+
};
|
|
1572
|
+
if (signal) {
|
|
1573
|
+
const onAbort = () => {
|
|
1574
|
+
this.removeWaiter(waiter);
|
|
1575
|
+
resolve({
|
|
1576
|
+
status: "keep-waiting",
|
|
1577
|
+
round: this.round
|
|
1578
|
+
});
|
|
1579
|
+
};
|
|
1580
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1581
|
+
waiter.cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
1582
|
+
}
|
|
1583
|
+
this.waiters.add(waiter);
|
|
1584
|
+
this.emitStatus();
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
/** Capture a decision and release every blocked waiter. */
|
|
1588
|
+
decide(input) {
|
|
1589
|
+
this.round += 1;
|
|
1590
|
+
this.lastDecidedAt = input.sentAt;
|
|
1591
|
+
const payload = {
|
|
1592
|
+
round: this.round,
|
|
1593
|
+
sentAt: input.sentAt,
|
|
1594
|
+
planId: input.planId,
|
|
1595
|
+
decision: input.decision,
|
|
1596
|
+
decisionComment: input.decisionComment,
|
|
1597
|
+
reviewXml: input.reviewXml,
|
|
1598
|
+
openCommentCount: input.openCommentCount,
|
|
1599
|
+
plan: input.plan
|
|
1600
|
+
};
|
|
1601
|
+
this.lastPayload = payload;
|
|
1602
|
+
const waiters = [...this.waiters];
|
|
1603
|
+
this.waiters.clear();
|
|
1604
|
+
for (const waiter of waiters) {
|
|
1605
|
+
clearTimeout(waiter.timer);
|
|
1606
|
+
waiter.cleanup();
|
|
1607
|
+
waiter.resolve({
|
|
1608
|
+
status: "released",
|
|
1609
|
+
payload
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
this.emitStatus();
|
|
1613
|
+
return payload;
|
|
1614
|
+
}
|
|
1615
|
+
snapshot() {
|
|
1616
|
+
return {
|
|
1617
|
+
round: this.round,
|
|
1618
|
+
waiters: this.waiters.size,
|
|
1619
|
+
lastDecidedAt: this.lastDecidedAt
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
removeWaiter(waiter) {
|
|
1623
|
+
if (!this.waiters.delete(waiter)) return;
|
|
1624
|
+
clearTimeout(waiter.timer);
|
|
1625
|
+
waiter.cleanup();
|
|
1626
|
+
this.emitStatus();
|
|
1627
|
+
}
|
|
1628
|
+
emitStatus() {
|
|
1629
|
+
this.onStatusChange?.(this.snapshot());
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
//#endregion
|
|
1322
1633
|
//#region src/server.ts
|
|
1323
1634
|
const MIME_TYPES = {
|
|
1324
1635
|
".html": "text/html",
|
|
@@ -1338,10 +1649,12 @@ const MIME_TYPES = {
|
|
|
1338
1649
|
function isCustomMode(opts) {
|
|
1339
1650
|
return opts.revisions.length > 0 || opts.pathspecs.length > 0;
|
|
1340
1651
|
}
|
|
1341
|
-
function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
1652
|
+
function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
|
|
1342
1653
|
const app = new Hono();
|
|
1343
1654
|
const customMode = isCustomMode(diffOpts);
|
|
1344
1655
|
const store = commentStore ?? new FileCommentStore();
|
|
1656
|
+
const plans = planStore ?? new FilePlanStore();
|
|
1657
|
+
const uiStateStore = new FileUiStateStore();
|
|
1345
1658
|
const viewedFiles = /* @__PURE__ */ new Set();
|
|
1346
1659
|
const activeClients = /* @__PURE__ */ new Set();
|
|
1347
1660
|
const broadcast = (event, data) => {
|
|
@@ -1350,23 +1663,30 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
|
1350
1663
|
} catch {}
|
|
1351
1664
|
};
|
|
1352
1665
|
const reviewSession = new ReviewSession((snapshot) => broadcast("agent-status", JSON.stringify(snapshot)));
|
|
1666
|
+
const planReviewSession = new PlanReviewSession((snapshot) => broadcast("plan-review-status", JSON.stringify(snapshot)));
|
|
1353
1667
|
let repoRoot;
|
|
1354
1668
|
try {
|
|
1355
1669
|
repoRoot = getRepoRoot();
|
|
1356
1670
|
} catch {
|
|
1357
1671
|
repoRoot = process.cwd();
|
|
1358
1672
|
}
|
|
1359
|
-
if (!commentStore) try {
|
|
1673
|
+
if (!commentStore && !planStore) try {
|
|
1360
1674
|
const storageDir = getProjectStorageDir();
|
|
1361
1675
|
mkdirSync(storageDir, { recursive: true });
|
|
1362
1676
|
let commentsDebounce = null;
|
|
1677
|
+
let plansDebounce = null;
|
|
1363
1678
|
watch(storageDir, (_eventType, filename) => {
|
|
1364
|
-
if (
|
|
1365
|
-
if (
|
|
1366
|
-
|
|
1679
|
+
if (!filename) return;
|
|
1680
|
+
if (filename.startsWith("comments.json")) {
|
|
1681
|
+
if (commentsDebounce) clearTimeout(commentsDebounce);
|
|
1682
|
+
commentsDebounce = setTimeout(() => broadcast("comments", Date.now().toString()), 120);
|
|
1683
|
+
} else if (filename.startsWith("plans.json")) {
|
|
1684
|
+
if (plansDebounce) clearTimeout(plansDebounce);
|
|
1685
|
+
plansDebounce = setTimeout(() => broadcast("plans", Date.now().toString()), 120);
|
|
1686
|
+
}
|
|
1367
1687
|
}).unref();
|
|
1368
1688
|
} catch (err) {
|
|
1369
|
-
console.warn("Failed to initialize
|
|
1689
|
+
console.warn("Failed to initialize storage watcher:", err);
|
|
1370
1690
|
}
|
|
1371
1691
|
let debounceTimeout = null;
|
|
1372
1692
|
try {
|
|
@@ -1468,8 +1788,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
|
1468
1788
|
if (!filePath) return c.json({ error: "filePath is required" }, 400);
|
|
1469
1789
|
try {
|
|
1470
1790
|
const root = getRepoRoot();
|
|
1471
|
-
|
|
1472
|
-
|
|
1791
|
+
const relPath = toSafeRelativePath(filePath, root);
|
|
1792
|
+
if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
|
|
1793
|
+
const absolutePath = resolve(root, relPath);
|
|
1473
1794
|
const { exec } = await import("node:child_process");
|
|
1474
1795
|
if (editor === "vscode") exec(`code "${absolutePath}"`);
|
|
1475
1796
|
else if (editor === "zed") exec(`zed "${absolutePath}"`);
|
|
@@ -1543,8 +1864,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
|
1543
1864
|
const { filePath, hunkIndex } = await c.req.json();
|
|
1544
1865
|
if (!filePath || typeof hunkIndex !== "number") return c.json({ error: "Missing filePath or hunkIndex" }, 400);
|
|
1545
1866
|
try {
|
|
1546
|
-
|
|
1547
|
-
|
|
1867
|
+
const relPath = toSafeRelativePath(filePath, getRepoRoot());
|
|
1868
|
+
if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
|
|
1869
|
+
revertHunk(relPath, hunkIndex);
|
|
1548
1870
|
return c.json({ ok: true });
|
|
1549
1871
|
} catch (err) {
|
|
1550
1872
|
const stderr = typeof err?.stderr === "string" ? err.stderr : err?.stderr instanceof Buffer ? err.stderr.toString("utf-8") : "";
|
|
@@ -1557,8 +1879,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
|
1557
1879
|
const deletionCount = Number(c.req.query("deletionCount"));
|
|
1558
1880
|
if (!filePath || Number.isNaN(deletionStart) || Number.isNaN(deletionCount)) return c.json({ error: "Missing or invalid parameters" }, 400);
|
|
1559
1881
|
try {
|
|
1560
|
-
|
|
1561
|
-
|
|
1882
|
+
const relPath = toSafeRelativePath(filePath, getRepoRoot());
|
|
1883
|
+
if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
|
|
1884
|
+
const history = getHunkHistory(relPath, deletionStart, deletionCount, diffOpts.revisions[0] || "HEAD");
|
|
1562
1885
|
return c.json(history);
|
|
1563
1886
|
} catch (err) {
|
|
1564
1887
|
return c.json({ error: err.message || "Failed to fetch hunk history" }, 500);
|
|
@@ -1569,10 +1892,11 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
|
1569
1892
|
if (!filePath || typeof content !== "string") return c.json({ error: "Missing filePath or content" }, 400);
|
|
1570
1893
|
try {
|
|
1571
1894
|
const root = getRepoRoot();
|
|
1572
|
-
|
|
1573
|
-
|
|
1895
|
+
const relPath = toSafeRelativePath(filePath, root);
|
|
1896
|
+
if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
|
|
1897
|
+
await writeFile(resolve(root, relPath), content, "utf-8");
|
|
1574
1898
|
if (gitAdd) try {
|
|
1575
|
-
gitAddFile(
|
|
1899
|
+
gitAddFile(relPath);
|
|
1576
1900
|
} catch (err) {
|
|
1577
1901
|
return c.json({
|
|
1578
1902
|
ok: true,
|
|
@@ -1591,6 +1915,19 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
|
1591
1915
|
const settings = saveSettings(await c.req.json());
|
|
1592
1916
|
return c.json(settings);
|
|
1593
1917
|
});
|
|
1918
|
+
app.get("/api/ui-state", async (c) => {
|
|
1919
|
+
return c.json(await uiStateStore.getAll());
|
|
1920
|
+
});
|
|
1921
|
+
app.put("/api/ui-state", async (c) => {
|
|
1922
|
+
const body = await c.req.json();
|
|
1923
|
+
const merged = {
|
|
1924
|
+
...await uiStateStore.getAll(),
|
|
1925
|
+
...body
|
|
1926
|
+
};
|
|
1927
|
+
for (const key of Object.keys(body)) if (body[key] === null || body[key] === void 0) delete merged[key];
|
|
1928
|
+
await uiStateStore.setAll(merged);
|
|
1929
|
+
return c.json(merged);
|
|
1930
|
+
});
|
|
1594
1931
|
app.get("/api/viewed", (c) => {
|
|
1595
1932
|
return c.json([...viewedFiles]);
|
|
1596
1933
|
});
|
|
@@ -1671,8 +2008,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
|
1671
2008
|
const suggestion = match[1];
|
|
1672
2009
|
try {
|
|
1673
2010
|
const root = getRepoRoot();
|
|
1674
|
-
|
|
1675
|
-
|
|
2011
|
+
const relPath = toSafeRelativePath(comment.filePath, root);
|
|
2012
|
+
if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
|
|
2013
|
+
const absolutePath = resolve(root, relPath);
|
|
1676
2014
|
const lines = (await readFile(absolutePath, "utf-8")).split(/\r?\n/);
|
|
1677
2015
|
const lineIdx = comment.lineNumber - 1;
|
|
1678
2016
|
if (lineIdx < 0 || lineIdx >= lines.length) return c.json({ error: "Comment line number out of range" }, 400);
|
|
@@ -1692,18 +2030,21 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
|
1692
2030
|
app.post("/api/review/send", async (c) => {
|
|
1693
2031
|
const body = await c.req.json().catch(() => ({}));
|
|
1694
2032
|
const generalComment = typeof body?.generalComment === "string" ? body.generalComment : void 0;
|
|
2033
|
+
const decision = body?.decision === "approved" || body?.decision === "changes-requested" || body?.decision === "rejected" ? body.decision : void 0;
|
|
1695
2034
|
const all = await store.getAll();
|
|
1696
2035
|
const openCount = all.filter((x) => x.status === "open").length;
|
|
1697
2036
|
const payload = reviewSession.send({
|
|
1698
2037
|
sentAt: Date.now(),
|
|
1699
|
-
commentXml: formatComments(all, generalComment),
|
|
2038
|
+
commentXml: formatComments(all, generalComment, decision),
|
|
1700
2039
|
openCount,
|
|
1701
|
-
comments: all
|
|
2040
|
+
comments: all,
|
|
2041
|
+
decision
|
|
1702
2042
|
});
|
|
1703
2043
|
return c.json({
|
|
1704
2044
|
ok: true,
|
|
1705
2045
|
round: payload.round,
|
|
1706
2046
|
openCount: payload.openCount,
|
|
2047
|
+
decision: payload.decision,
|
|
1707
2048
|
waiters: reviewSession.snapshot().waiters
|
|
1708
2049
|
});
|
|
1709
2050
|
});
|
|
@@ -1722,6 +2063,148 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
|
1722
2063
|
app.get("/api/review/status", (c) => {
|
|
1723
2064
|
return c.json(reviewSession.snapshot());
|
|
1724
2065
|
});
|
|
2066
|
+
app.get("/api/plans", async (c) => {
|
|
2067
|
+
return c.json(await plans.getAll());
|
|
2068
|
+
});
|
|
2069
|
+
app.get("/api/plans/:id", async (c) => {
|
|
2070
|
+
const plan = await plans.get(c.req.param("id"));
|
|
2071
|
+
if (!plan) return c.json({ error: "Plan not found" }, 404);
|
|
2072
|
+
return c.json(plan);
|
|
2073
|
+
});
|
|
2074
|
+
app.post("/api/plans", async (c) => {
|
|
2075
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2076
|
+
if (typeof body.body !== "string" || !body.body.trim()) return c.json({ error: "A plan body (markdown) is required" }, 400);
|
|
2077
|
+
const title = typeof body.title === "string" && body.title.trim() ? body.title.trim() : "Untitled plan";
|
|
2078
|
+
const plan = await plans.upsert({
|
|
2079
|
+
id: typeof body.id === "string" && body.id ? body.id : void 0,
|
|
2080
|
+
title,
|
|
2081
|
+
body: body.body,
|
|
2082
|
+
source: typeof body.source === "string" ? body.source : void 0,
|
|
2083
|
+
model: typeof body.model === "string" ? body.model : void 0
|
|
2084
|
+
});
|
|
2085
|
+
return c.json(plan, 201);
|
|
2086
|
+
});
|
|
2087
|
+
app.put("/api/plans/:id", async (c) => {
|
|
2088
|
+
const id = c.req.param("id");
|
|
2089
|
+
const { title, body, source, model } = await c.req.json().catch(() => ({}));
|
|
2090
|
+
const updated = await plans.update(id, {
|
|
2091
|
+
title: typeof title === "string" ? title : void 0,
|
|
2092
|
+
body: typeof body === "string" ? body : void 0,
|
|
2093
|
+
source: typeof source === "string" ? source : void 0,
|
|
2094
|
+
model: typeof model === "string" ? model : void 0
|
|
2095
|
+
});
|
|
2096
|
+
if (!updated) return c.json({ error: "Plan not found" }, 404);
|
|
2097
|
+
return c.json(updated);
|
|
2098
|
+
});
|
|
2099
|
+
app.delete("/api/plans/:id", async (c) => {
|
|
2100
|
+
if (!await plans.remove(c.req.param("id"))) return c.json({ error: "Plan not found" }, 404);
|
|
2101
|
+
return c.json({ ok: true });
|
|
2102
|
+
});
|
|
2103
|
+
app.post("/api/plans/:id/comments", async (c) => {
|
|
2104
|
+
const planId = c.req.param("id");
|
|
2105
|
+
const plan = await plans.get(planId);
|
|
2106
|
+
if (!plan) return c.json({ error: "Plan not found" }, 404);
|
|
2107
|
+
const body = await c.req.json();
|
|
2108
|
+
const lineNumber = Number.isFinite(body.lineNumber) ? Number(body.lineNumber) : 0;
|
|
2109
|
+
const startLineNumber = Number.isFinite(body.startLineNumber) ? Number(body.startLineNumber) : void 0;
|
|
2110
|
+
const anchorStart = startLineNumber ?? lineNumber;
|
|
2111
|
+
const lineContent = typeof body.lineContent === "string" && body.lineContent ? body.lineContent : lineNumber > 0 ? extractPlanLines(plan.body, anchorStart, lineNumber) : "";
|
|
2112
|
+
const sectionTitle = typeof body.sectionTitle === "string" && body.sectionTitle ? body.sectionTitle : lineNumber > 0 ? sectionTitleForLine(plan.body, anchorStart) : void 0;
|
|
2113
|
+
const comment = {
|
|
2114
|
+
id: crypto.randomUUID(),
|
|
2115
|
+
lineNumber,
|
|
2116
|
+
startLineNumber,
|
|
2117
|
+
lineContent,
|
|
2118
|
+
sectionTitle,
|
|
2119
|
+
body: body.body,
|
|
2120
|
+
status: "open",
|
|
2121
|
+
createdAt: Date.now(),
|
|
2122
|
+
replies: []
|
|
2123
|
+
};
|
|
2124
|
+
const updated = await plans.addComment(planId, comment);
|
|
2125
|
+
if (!updated) return c.json({ error: "Plan not found" }, 404);
|
|
2126
|
+
return c.json(updated, 201);
|
|
2127
|
+
});
|
|
2128
|
+
app.put("/api/plans/:id/comments/:commentId", async (c) => {
|
|
2129
|
+
const { body, status } = await c.req.json();
|
|
2130
|
+
const updated = await plans.updateComment(c.req.param("id"), c.req.param("commentId"), {
|
|
2131
|
+
body,
|
|
2132
|
+
status
|
|
2133
|
+
});
|
|
2134
|
+
if (!updated) return c.json({ error: "Plan or comment not found" }, 404);
|
|
2135
|
+
return c.json(updated);
|
|
2136
|
+
});
|
|
2137
|
+
app.delete("/api/plans/:id/comments/:commentId", async (c) => {
|
|
2138
|
+
const updated = await plans.removeComment(c.req.param("id"), c.req.param("commentId"));
|
|
2139
|
+
if (!updated) return c.json({ error: "Plan or comment not found" }, 404);
|
|
2140
|
+
return c.json(updated);
|
|
2141
|
+
});
|
|
2142
|
+
app.post("/api/plans/:id/comments/:commentId/replies", async (c) => {
|
|
2143
|
+
const { body, role, model } = await c.req.json();
|
|
2144
|
+
const reply = {
|
|
2145
|
+
id: crypto.randomUUID(),
|
|
2146
|
+
body,
|
|
2147
|
+
createdAt: Date.now(),
|
|
2148
|
+
role: role || (model ? "agent" : "user"),
|
|
2149
|
+
model: model || void 0
|
|
2150
|
+
};
|
|
2151
|
+
const updated = await plans.addReply(c.req.param("id"), c.req.param("commentId"), reply);
|
|
2152
|
+
if (!updated) return c.json({ error: "Plan or comment not found" }, 404);
|
|
2153
|
+
return c.json(updated);
|
|
2154
|
+
});
|
|
2155
|
+
app.put("/api/plans/:id/comments/:commentId/replies/:replyId", async (c) => {
|
|
2156
|
+
const { body } = await c.req.json();
|
|
2157
|
+
if (!body) return c.json({ error: "Body is required" }, 400);
|
|
2158
|
+
const updated = await plans.updateReply(c.req.param("id"), c.req.param("commentId"), c.req.param("replyId"), body);
|
|
2159
|
+
if (!updated) return c.json({ error: "Plan, comment, or reply not found" }, 404);
|
|
2160
|
+
return c.json(updated);
|
|
2161
|
+
});
|
|
2162
|
+
app.delete("/api/plans/:id/comments/:commentId/replies/:replyId", async (c) => {
|
|
2163
|
+
const updated = await plans.removeReply(c.req.param("id"), c.req.param("commentId"), c.req.param("replyId"));
|
|
2164
|
+
if (!updated) return c.json({ error: "Plan, comment, or reply not found" }, 404);
|
|
2165
|
+
return c.json(updated);
|
|
2166
|
+
});
|
|
2167
|
+
app.post("/api/plans/:id/decision", async (c) => {
|
|
2168
|
+
const planId = c.req.param("id");
|
|
2169
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2170
|
+
const decision = body.decision;
|
|
2171
|
+
if (decision !== "approved" && decision !== "rejected" && decision !== "changes-requested") return c.json({ error: "decision must be one of: approved, rejected, changes-requested" }, 400);
|
|
2172
|
+
const decisionComment = typeof body.decisionComment === "string" ? body.decisionComment : void 0;
|
|
2173
|
+
const plan = await plans.setDecision(planId, decision, decisionComment);
|
|
2174
|
+
if (!plan) return c.json({ error: "Plan not found" }, 404);
|
|
2175
|
+
const openCommentCount = (plan.comments ?? []).filter((x) => x.status === "open").length;
|
|
2176
|
+
const payload = planReviewSession.decide({
|
|
2177
|
+
sentAt: Date.now(),
|
|
2178
|
+
planId,
|
|
2179
|
+
decision,
|
|
2180
|
+
decisionComment: plan.decisionComment,
|
|
2181
|
+
reviewXml: formatPlanReview(plan),
|
|
2182
|
+
openCommentCount,
|
|
2183
|
+
plan
|
|
2184
|
+
});
|
|
2185
|
+
return c.json({
|
|
2186
|
+
ok: true,
|
|
2187
|
+
round: payload.round,
|
|
2188
|
+
decision,
|
|
2189
|
+
openCommentCount,
|
|
2190
|
+
waiters: planReviewSession.snapshot().waiters
|
|
2191
|
+
});
|
|
2192
|
+
});
|
|
2193
|
+
app.get("/api/plan-review/await", async (c) => {
|
|
2194
|
+
const sinceRaw = c.req.query("sinceRound");
|
|
2195
|
+
const sinceRound = sinceRaw !== void 0 && sinceRaw !== "" ? Number(sinceRaw) : void 0;
|
|
2196
|
+
const requested = Number(c.req.query("timeoutMs")) || 25e3;
|
|
2197
|
+
const timeoutMs = Math.min(Math.max(requested, 1e3), 5e4);
|
|
2198
|
+
const result = await planReviewSession.await({
|
|
2199
|
+
sinceRound: Number.isNaN(sinceRound) ? void 0 : sinceRound,
|
|
2200
|
+
timeoutMs,
|
|
2201
|
+
signal: c.req.raw.signal
|
|
2202
|
+
});
|
|
2203
|
+
return c.json(result);
|
|
2204
|
+
});
|
|
2205
|
+
app.get("/api/plan-review/status", (c) => {
|
|
2206
|
+
return c.json(planReviewSession.snapshot());
|
|
2207
|
+
});
|
|
1725
2208
|
app.post("/api/attachments", async (c) => {
|
|
1726
2209
|
const file = (await c.req.parseBody())["file"];
|
|
1727
2210
|
if (!file || !(file instanceof File)) return c.json({ error: "No file uploaded" }, 400);
|
|
@@ -1771,6 +2254,40 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore) {
|
|
|
1771
2254
|
});
|
|
1772
2255
|
return app;
|
|
1773
2256
|
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Newest mtime (epoch ms) across everything that counts as project activity:
|
|
2259
|
+
* comments.json, plans.json, and any attachment (media) file. Plans and media
|
|
2260
|
+
* extend a project's life exactly like comments do, so retention is uniform
|
|
2261
|
+
* across all three. Returns null when none of them exist yet.
|
|
2262
|
+
*/
|
|
2263
|
+
async function newestActivityMs(projectDir) {
|
|
2264
|
+
const candidates = [];
|
|
2265
|
+
const safeStat = async (p) => {
|
|
2266
|
+
try {
|
|
2267
|
+
return (await stat(p)).mtimeMs;
|
|
2268
|
+
} catch {
|
|
2269
|
+
return null;
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
for (const name of ["comments.json", "plans.json"]) {
|
|
2273
|
+
const file = join(projectDir, name);
|
|
2274
|
+
if (!existsSync(file)) continue;
|
|
2275
|
+
const m = await safeStat(file);
|
|
2276
|
+
if (m !== null) candidates.push(m);
|
|
2277
|
+
}
|
|
2278
|
+
const attachmentsDir = join(projectDir, "attachments");
|
|
2279
|
+
if (existsSync(attachmentsDir)) {
|
|
2280
|
+
const dirM = await safeStat(attachmentsDir);
|
|
2281
|
+
if (dirM !== null) candidates.push(dirM);
|
|
2282
|
+
try {
|
|
2283
|
+
for (const file of await readdir(attachmentsDir)) {
|
|
2284
|
+
const m = await safeStat(join(attachmentsDir, file));
|
|
2285
|
+
if (m !== null) candidates.push(m);
|
|
2286
|
+
}
|
|
2287
|
+
} catch {}
|
|
2288
|
+
}
|
|
2289
|
+
return candidates.length ? Math.max(...candidates) : null;
|
|
2290
|
+
}
|
|
1774
2291
|
async function cleanupStaleProjects() {
|
|
1775
2292
|
const baseDir = join(homedir(), ".diffing");
|
|
1776
2293
|
if (!existsSync(baseDir)) return;
|
|
@@ -1781,15 +2298,15 @@ async function cleanupStaleProjects() {
|
|
|
1781
2298
|
for (const entry of entries) if (entry.isDirectory()) {
|
|
1782
2299
|
const projectDir = join(baseDir, entry.name);
|
|
1783
2300
|
const repoPathFile = join(projectDir, "repo_path.txt");
|
|
1784
|
-
const commentsFile = join(projectDir, "comments.json");
|
|
1785
2301
|
let shouldDelete = false;
|
|
1786
2302
|
if (existsSync(repoPathFile)) try {
|
|
1787
2303
|
const repoPath = (await readFile(repoPathFile, "utf-8")).trim();
|
|
1788
2304
|
if (!repoPath || !existsSync(repoPath)) shouldDelete = true;
|
|
1789
2305
|
} catch {}
|
|
1790
|
-
if (!shouldDelete
|
|
1791
|
-
|
|
1792
|
-
|
|
2306
|
+
if (!shouldDelete) {
|
|
2307
|
+
const newest = await newestActivityMs(projectDir);
|
|
2308
|
+
if (newest !== null && now - newest > STALE_TIME) shouldDelete = true;
|
|
2309
|
+
}
|
|
1793
2310
|
if (shouldDelete) await rm(projectDir, {
|
|
1794
2311
|
recursive: true,
|
|
1795
2312
|
force: true
|
|
@@ -1815,6 +2332,389 @@ function startServer(options) {
|
|
|
1815
2332
|
});
|
|
1816
2333
|
}
|
|
1817
2334
|
//#endregion
|
|
2335
|
+
//#region src/lib/startup-display.ts
|
|
2336
|
+
const R = "\x1B[0m";
|
|
2337
|
+
const B = "\x1B[1m";
|
|
2338
|
+
const HIDE = "\x1B[?25l";
|
|
2339
|
+
const SHOW = "\x1B[?25h";
|
|
2340
|
+
const CLR = "\x1B[2K\r";
|
|
2341
|
+
const GLITCH_CHARS = "░▒▓█▄▀■□▸▹◆◇";
|
|
2342
|
+
const c = (n) => `\x1b[38;5;${n}m`;
|
|
2343
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2344
|
+
const up = (n) => `\x1b[${n}A`;
|
|
2345
|
+
const PALETTES = [
|
|
2346
|
+
{
|
|
2347
|
+
base: c(51),
|
|
2348
|
+
text: c(159),
|
|
2349
|
+
faint: c(30),
|
|
2350
|
+
glow: B + c(87),
|
|
2351
|
+
rain: c(37)
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
base: c(82),
|
|
2355
|
+
text: c(155),
|
|
2356
|
+
faint: c(22),
|
|
2357
|
+
glow: B + c(118),
|
|
2358
|
+
rain: c(34)
|
|
2359
|
+
},
|
|
2360
|
+
{
|
|
2361
|
+
base: c(207),
|
|
2362
|
+
text: c(219),
|
|
2363
|
+
faint: c(96),
|
|
2364
|
+
glow: B + c(213),
|
|
2365
|
+
rain: c(128)
|
|
2366
|
+
},
|
|
2367
|
+
{
|
|
2368
|
+
base: c(220),
|
|
2369
|
+
text: c(229),
|
|
2370
|
+
faint: c(136),
|
|
2371
|
+
glow: B + c(226),
|
|
2372
|
+
rain: c(178)
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
base: c(75),
|
|
2376
|
+
text: c(153),
|
|
2377
|
+
faint: c(25),
|
|
2378
|
+
glow: B + c(81),
|
|
2379
|
+
rain: c(39)
|
|
2380
|
+
},
|
|
2381
|
+
{
|
|
2382
|
+
base: c(214),
|
|
2383
|
+
text: c(223),
|
|
2384
|
+
faint: c(130),
|
|
2385
|
+
glow: B + c(208),
|
|
2386
|
+
rain: c(172)
|
|
2387
|
+
}
|
|
2388
|
+
];
|
|
2389
|
+
const QUOTES = [
|
|
2390
|
+
{
|
|
2391
|
+
text: "The best code is no code at all.",
|
|
2392
|
+
author: "Jeff Atwood"
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
text: "Programs must be written for people to read, and only incidentally for machines to execute.",
|
|
2396
|
+
author: "Abelson & Sussman"
|
|
2397
|
+
},
|
|
2398
|
+
{
|
|
2399
|
+
text: "Make it work, make it right, make it fast.",
|
|
2400
|
+
author: "Kent Beck"
|
|
2401
|
+
},
|
|
2402
|
+
{
|
|
2403
|
+
text: "Simplicity is the soul of efficiency.",
|
|
2404
|
+
author: "Austin Freeman"
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
text: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
|
|
2408
|
+
author: "Martin Fowler"
|
|
2409
|
+
},
|
|
2410
|
+
{
|
|
2411
|
+
text: "First, solve the problem. Then, write the code.",
|
|
2412
|
+
author: "John Johnson"
|
|
2413
|
+
},
|
|
2414
|
+
{
|
|
2415
|
+
text: "Code is like humor. When you have to explain it, it's bad.",
|
|
2416
|
+
author: "Cory House"
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
text: "Before software can be reusable it first has to be usable.",
|
|
2420
|
+
author: "Ralph Johnson"
|
|
2421
|
+
},
|
|
2422
|
+
{
|
|
2423
|
+
text: "Clean code always looks like it was written by someone who cares.",
|
|
2424
|
+
author: "Robert C. Martin"
|
|
2425
|
+
},
|
|
2426
|
+
{
|
|
2427
|
+
text: "The most dangerous phrase is \"we've always done it this way\".",
|
|
2428
|
+
author: "Grace Hopper"
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
text: "Architecture is the decisions you wish you could get right early in a project.",
|
|
2432
|
+
author: "Martin Fowler"
|
|
2433
|
+
},
|
|
2434
|
+
{
|
|
2435
|
+
text: "The best error message is the one that never shows up.",
|
|
2436
|
+
author: "Thomas Fuchs"
|
|
2437
|
+
},
|
|
2438
|
+
{
|
|
2439
|
+
text: "It works on my machine.",
|
|
2440
|
+
author: "Every developer, always"
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
text: "git blame yourself.",
|
|
2444
|
+
author: "The terminal"
|
|
2445
|
+
},
|
|
2446
|
+
{
|
|
2447
|
+
text: "There are only two hard things in CS: cache invalidation, naming things, and off-by-one errors.",
|
|
2448
|
+
author: "Unknown"
|
|
2449
|
+
},
|
|
2450
|
+
{
|
|
2451
|
+
text: "Debugging: being the detective in a crime movie where you're also the murderer.",
|
|
2452
|
+
author: "Unknown"
|
|
2453
|
+
},
|
|
2454
|
+
{
|
|
2455
|
+
text: "The code you wrote six months ago was written by an idiot.",
|
|
2456
|
+
author: "Unknown"
|
|
2457
|
+
},
|
|
2458
|
+
{
|
|
2459
|
+
text: "sudo make me a sandwich.",
|
|
2460
|
+
author: "xkcd"
|
|
2461
|
+
},
|
|
2462
|
+
{
|
|
2463
|
+
text: "I don't always test my code, but when I do, I do it in production.",
|
|
2464
|
+
author: "Unknown"
|
|
2465
|
+
},
|
|
2466
|
+
{
|
|
2467
|
+
text: "A QA engineer walks into a bar. Orders 0 beers. Orders 999999 beers. Orders NULL beers. Walks in through the window.",
|
|
2468
|
+
author: "Unknown"
|
|
2469
|
+
},
|
|
2470
|
+
{
|
|
2471
|
+
text: "To understand recursion, you must first understand recursion.",
|
|
2472
|
+
author: "Unknown"
|
|
2473
|
+
},
|
|
2474
|
+
{
|
|
2475
|
+
text: "The code review is the product.",
|
|
2476
|
+
author: "diffing"
|
|
2477
|
+
},
|
|
2478
|
+
{
|
|
2479
|
+
text: "Your abstractions are just someone else's bugs.",
|
|
2480
|
+
author: "Unknown"
|
|
2481
|
+
},
|
|
2482
|
+
{
|
|
2483
|
+
text: "Every rewrite is a confession that you didn't understand the problem the first time.",
|
|
2484
|
+
author: "Unknown"
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
text: "The best PR is no PR — ship it in the design.",
|
|
2488
|
+
author: "Unknown"
|
|
2489
|
+
},
|
|
2490
|
+
{
|
|
2491
|
+
text: "You don't own your code. Your code owns you.",
|
|
2492
|
+
author: "Unknown"
|
|
2493
|
+
},
|
|
2494
|
+
{
|
|
2495
|
+
text: "A diff is a conversation. What story does yours tell?",
|
|
2496
|
+
author: "Unknown"
|
|
2497
|
+
},
|
|
2498
|
+
{
|
|
2499
|
+
text: "The senior engineer's superpower: knowing which shortcuts will haunt you.",
|
|
2500
|
+
author: "Unknown"
|
|
2501
|
+
},
|
|
2502
|
+
{
|
|
2503
|
+
text: "Production is the only real test environment.",
|
|
2504
|
+
author: "Site Reliability truth"
|
|
2505
|
+
},
|
|
2506
|
+
{
|
|
2507
|
+
text: "Tech debt is just deferred thinking.",
|
|
2508
|
+
author: "Unknown"
|
|
2509
|
+
},
|
|
2510
|
+
{
|
|
2511
|
+
text: "Comments lie. Code never does.",
|
|
2512
|
+
author: "Ron Jeffries"
|
|
2513
|
+
}
|
|
2514
|
+
];
|
|
2515
|
+
function wrapText(text, width) {
|
|
2516
|
+
const words = text.split(" ");
|
|
2517
|
+
const lines = [];
|
|
2518
|
+
let cur = "";
|
|
2519
|
+
for (const word of words) {
|
|
2520
|
+
if (!cur) {
|
|
2521
|
+
cur = word;
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
if (cur.length + 1 + word.length <= width) cur += " " + word;
|
|
2525
|
+
else {
|
|
2526
|
+
lines.push(cur);
|
|
2527
|
+
cur = word;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
if (cur) lines.push(cur);
|
|
2531
|
+
return lines;
|
|
2532
|
+
}
|
|
2533
|
+
function makeBox(quote, pal) {
|
|
2534
|
+
const termWidth = stdout.columns ?? 80;
|
|
2535
|
+
const maxContent = Math.min(64, termWidth - 8);
|
|
2536
|
+
const fullText = `"${quote.text}"`;
|
|
2537
|
+
const authorStr = `— ${quote.author}`;
|
|
2538
|
+
const textLines = wrapText(fullText, maxContent);
|
|
2539
|
+
return {
|
|
2540
|
+
textLines,
|
|
2541
|
+
authorStr,
|
|
2542
|
+
innerWidth: Math.max(...textLines.map((l) => l.length), authorStr.length, 28) + 4,
|
|
2543
|
+
pal
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
function topBorder(b, borderCol = b.pal.base) {
|
|
2547
|
+
return `${borderCol}╭${"─".repeat(b.innerWidth)}╮${R}`;
|
|
2548
|
+
}
|
|
2549
|
+
function bottomBorder(b, borderCol = b.pal.base) {
|
|
2550
|
+
return `${borderCol}╰${"─".repeat(b.innerWidth)}╯${R}`;
|
|
2551
|
+
}
|
|
2552
|
+
function contentRow(b, text, borderCol = b.pal.base, textCol = b.pal.text) {
|
|
2553
|
+
return `${borderCol}│${R} ${textCol}${text}${R}${" ".repeat(b.innerWidth - 4 - text.length)} ${borderCol}│${R}`;
|
|
2554
|
+
}
|
|
2555
|
+
function authorRow(b, borderCol = b.pal.base) {
|
|
2556
|
+
const leading = " ".repeat(b.innerWidth - 4 - b.authorStr.length);
|
|
2557
|
+
return `${borderCol}│${R} ${b.pal.faint}${leading}${b.authorStr}${R} ${borderCol}│${R}`;
|
|
2558
|
+
}
|
|
2559
|
+
function allLines(b, borderCol = b.pal.base) {
|
|
2560
|
+
return [
|
|
2561
|
+
topBorder(b, borderCol),
|
|
2562
|
+
...b.textLines.map((l) => contentRow(b, l, borderCol)),
|
|
2563
|
+
authorRow(b, borderCol),
|
|
2564
|
+
bottomBorder(b, borderCol)
|
|
2565
|
+
];
|
|
2566
|
+
}
|
|
2567
|
+
function printBox(b) {
|
|
2568
|
+
for (const line of allLines(b)) stdout.write(line + "\n");
|
|
2569
|
+
}
|
|
2570
|
+
const animTypewriter = async (b) => {
|
|
2571
|
+
stdout.write(topBorder(b) + "\n");
|
|
2572
|
+
for (const line of b.textLines) {
|
|
2573
|
+
stdout.write(`${b.pal.base}│${R} `);
|
|
2574
|
+
for (let i = 0; i < line.length; i++) {
|
|
2575
|
+
const ch = line[i];
|
|
2576
|
+
stdout.write(`${b.pal.text}${ch}${b.pal.glow}█${R}`);
|
|
2577
|
+
await sleep(25);
|
|
2578
|
+
stdout.write("\b \b");
|
|
2579
|
+
}
|
|
2580
|
+
for (let blink = 0; blink < 2; blink++) {
|
|
2581
|
+
stdout.write(`${b.pal.glow}█${R}`);
|
|
2582
|
+
await sleep(100);
|
|
2583
|
+
stdout.write("\b \b");
|
|
2584
|
+
await sleep(100);
|
|
2585
|
+
}
|
|
2586
|
+
stdout.write(" ".repeat(b.innerWidth - 4 - line.length) + ` ${b.pal.base}│${R}\n`);
|
|
2587
|
+
}
|
|
2588
|
+
stdout.write(authorRow(b) + "\n");
|
|
2589
|
+
stdout.write(bottomBorder(b) + "\n");
|
|
2590
|
+
};
|
|
2591
|
+
const animWaveReveal = async (b) => {
|
|
2592
|
+
stdout.write(topBorder(b) + "\n");
|
|
2593
|
+
for (const line of b.textLines) {
|
|
2594
|
+
const half = Math.ceil(line.length / 2);
|
|
2595
|
+
stdout.write(contentRow(b, line.slice(0, half).padEnd(line.length), b.pal.base, b.pal.faint) + "\n");
|
|
2596
|
+
await sleep(130);
|
|
2597
|
+
stdout.write(up(1) + CLR + contentRow(b, line) + "\n");
|
|
2598
|
+
await sleep(60);
|
|
2599
|
+
}
|
|
2600
|
+
stdout.write(authorRow(b) + "\n");
|
|
2601
|
+
stdout.write(bottomBorder(b) + "\n");
|
|
2602
|
+
};
|
|
2603
|
+
const animSlide = async (b) => {
|
|
2604
|
+
const lines = allLines(b);
|
|
2605
|
+
for (let i = 0; i < lines.length; i++) stdout.write("\n");
|
|
2606
|
+
stdout.write(up(lines.length));
|
|
2607
|
+
for (let step = 0; step < 5; step++) {
|
|
2608
|
+
const pad = " ".repeat(Math.floor(12 * (1 - step / 4)));
|
|
2609
|
+
for (const line of lines) stdout.write(CLR + pad + line + "\n");
|
|
2610
|
+
if (step < 4) {
|
|
2611
|
+
stdout.write(up(lines.length));
|
|
2612
|
+
await sleep(80);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
const animPulseBorder = async (b) => {
|
|
2617
|
+
const stages = [
|
|
2618
|
+
allLines(b, b.pal.faint),
|
|
2619
|
+
allLines(b, b.pal.base),
|
|
2620
|
+
allLines(b, b.pal.glow),
|
|
2621
|
+
allLines(b, b.pal.base)
|
|
2622
|
+
];
|
|
2623
|
+
for (let s = 0; s < stages.length; s++) {
|
|
2624
|
+
if (s > 0) stdout.write(up(stages[0].length));
|
|
2625
|
+
for (const line of stages[s]) stdout.write((s > 0 ? CLR : "") + line + "\n");
|
|
2626
|
+
if (s < stages.length - 1) await sleep(180);
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
const animGlitch = async (b) => {
|
|
2630
|
+
const noiseRates = [
|
|
2631
|
+
.25,
|
|
2632
|
+
.12,
|
|
2633
|
+
.04,
|
|
2634
|
+
0
|
|
2635
|
+
];
|
|
2636
|
+
const totalLines = b.textLines.length + 3;
|
|
2637
|
+
for (let f = 0; f < noiseRates.length; f++) {
|
|
2638
|
+
if (f > 0) stdout.write(up(totalLines));
|
|
2639
|
+
const rate = noiseRates[f];
|
|
2640
|
+
const clr = f > 0 ? CLR : "";
|
|
2641
|
+
const jitter = rate > 0 ? Math.floor(Math.random() * 3) : 0;
|
|
2642
|
+
const pad = " ".repeat(jitter);
|
|
2643
|
+
stdout.write(clr + pad + topBorder(b) + "\n");
|
|
2644
|
+
for (const line of b.textLines) if (rate === 0) stdout.write(clr + contentRow(b, line) + "\n");
|
|
2645
|
+
else {
|
|
2646
|
+
let inner = "";
|
|
2647
|
+
for (const ch of line) if (Math.random() < rate) {
|
|
2648
|
+
const charColor = Math.random() < .3 ? b.pal.glow : b.pal.base;
|
|
2649
|
+
inner += `${charColor}${GLITCH_CHARS[Math.floor(Math.random() * 12)]}${b.pal.text}`;
|
|
2650
|
+
} else inner += ch;
|
|
2651
|
+
const fill = " ".repeat(b.innerWidth - 4 - line.length);
|
|
2652
|
+
stdout.write(clr + pad + `${b.pal.base}│${R} ${b.pal.text}${inner}${R}${fill} ${b.pal.base}│${R}\n`);
|
|
2653
|
+
}
|
|
2654
|
+
stdout.write(clr + pad + authorRow(b) + "\n");
|
|
2655
|
+
stdout.write(clr + pad + bottomBorder(b) + "\n");
|
|
2656
|
+
if (f < noiseRates.length - 1) await sleep(120);
|
|
2657
|
+
}
|
|
2658
|
+
};
|
|
2659
|
+
const animMatrixRain = async (b) => {
|
|
2660
|
+
const width = b.innerWidth + 2;
|
|
2661
|
+
const RAIN_HEIGHT = 6;
|
|
2662
|
+
const streams = Array.from({ length: RAIN_HEIGHT }, () => Array(width).fill(" "));
|
|
2663
|
+
const chars = "日ハミヒーウシナモニサワツオリアホマケメエカキムユラセネスタヌヘ0123456789";
|
|
2664
|
+
for (let frame = 0; frame < 9; frame++) {
|
|
2665
|
+
for (let y = RAIN_HEIGHT - 1; y > 0; y--) streams[y] = [...streams[y - 1]];
|
|
2666
|
+
streams[0] = Array(width).fill(" ").map(() => {
|
|
2667
|
+
if (Math.random() < .25) return { char: chars[Math.floor(Math.random() * 42)] };
|
|
2668
|
+
return " ";
|
|
2669
|
+
});
|
|
2670
|
+
if (frame > 0) stdout.write(up(RAIN_HEIGHT));
|
|
2671
|
+
for (let y = 0; y < RAIN_HEIGHT; y++) {
|
|
2672
|
+
let line = "";
|
|
2673
|
+
for (let x = 0; x < width; x++) {
|
|
2674
|
+
const cell = streams[y][x];
|
|
2675
|
+
if (cell === " ") line += " ";
|
|
2676
|
+
else {
|
|
2677
|
+
const typedCell = cell;
|
|
2678
|
+
const color = y === 0 ? b.pal.glow : y <= 2 ? b.pal.text : y <= 4 ? b.pal.base : b.pal.faint;
|
|
2679
|
+
line += `${color}${typedCell.char}${R}`;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
stdout.write((frame > 0 ? CLR : "") + line + "\n");
|
|
2683
|
+
}
|
|
2684
|
+
await sleep(90);
|
|
2685
|
+
}
|
|
2686
|
+
stdout.write(up(RAIN_HEIGHT));
|
|
2687
|
+
for (let y = 0; y < RAIN_HEIGHT; y++) stdout.write(CLR + "\n");
|
|
2688
|
+
stdout.write(up(RAIN_HEIGHT));
|
|
2689
|
+
printBox(b);
|
|
2690
|
+
};
|
|
2691
|
+
const ANIMS = [
|
|
2692
|
+
animTypewriter,
|
|
2693
|
+
animWaveReveal,
|
|
2694
|
+
animSlide,
|
|
2695
|
+
animPulseBorder,
|
|
2696
|
+
animGlitch,
|
|
2697
|
+
animMatrixRain
|
|
2698
|
+
];
|
|
2699
|
+
function playStartupDisplay() {
|
|
2700
|
+
if (!stdout.isTTY) return Promise.resolve();
|
|
2701
|
+
return (async () => {
|
|
2702
|
+
try {
|
|
2703
|
+
const quote = QUOTES[Math.floor(Math.random() * QUOTES.length)];
|
|
2704
|
+
const pal = PALETTES[Math.floor(Math.random() * PALETTES.length)];
|
|
2705
|
+
const b = makeBox(quote, pal);
|
|
2706
|
+
const anim = ANIMS[Math.floor(Math.random() * ANIMS.length)];
|
|
2707
|
+
stdout.write("\n" + HIDE);
|
|
2708
|
+
await anim(b);
|
|
2709
|
+
stdout.write(SHOW);
|
|
2710
|
+
} catch {
|
|
2711
|
+
try {
|
|
2712
|
+
stdout.write(SHOW);
|
|
2713
|
+
} catch {}
|
|
2714
|
+
}
|
|
2715
|
+
})();
|
|
2716
|
+
}
|
|
2717
|
+
//#endregion
|
|
1818
2718
|
//#region src/cli.ts
|
|
1819
2719
|
const args = process.argv.slice(2);
|
|
1820
2720
|
if (new Set([
|
|
@@ -1823,14 +2723,16 @@ if (new Set([
|
|
|
1823
2723
|
"resolve",
|
|
1824
2724
|
"comments",
|
|
1825
2725
|
"url",
|
|
1826
|
-
"mcp"
|
|
2726
|
+
"mcp",
|
|
2727
|
+
"plan",
|
|
2728
|
+
"update"
|
|
1827
2729
|
]).has(args[0])) {
|
|
1828
2730
|
if (args[0] === "mcp") {
|
|
1829
|
-
const { startMcpServer } = await import("./mcp-
|
|
2731
|
+
const { startMcpServer } = await import("./mcp-CFeE13n-.mjs");
|
|
1830
2732
|
await startMcpServer();
|
|
1831
2733
|
await new Promise(() => {});
|
|
1832
2734
|
}
|
|
1833
|
-
const { runSubcommand } = await import("./cli-agent-
|
|
2735
|
+
const { runSubcommand } = await import("./cli-agent-BNwY5lZE.mjs");
|
|
1834
2736
|
process.exit(await runSubcommand(args[0], args.slice(1)));
|
|
1835
2737
|
}
|
|
1836
2738
|
const opts = parseDiffOptions(args);
|
|
@@ -1853,9 +2755,19 @@ if (opts.outputMode === "terminal") {
|
|
|
1853
2755
|
const exitCode = runTerminalDiff(opts);
|
|
1854
2756
|
process.exit(exitCode);
|
|
1855
2757
|
}
|
|
2758
|
+
const __pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
2759
|
+
const currentVersion = JSON.parse(readFileSync(resolve(__pkgDir, "..", "package.json"), "utf-8")).version;
|
|
2760
|
+
const updateCheckPromise = (async () => {
|
|
2761
|
+
try {
|
|
2762
|
+
const { checkForUpdates } = await import("./update-check-KL-2sGxE.mjs");
|
|
2763
|
+
return await checkForUpdates(currentVersion);
|
|
2764
|
+
} catch {
|
|
2765
|
+
return null;
|
|
2766
|
+
}
|
|
2767
|
+
})();
|
|
1856
2768
|
const port = await getPort(opts.port ? { port: opts.port } : void 0);
|
|
1857
2769
|
const host = opts.host;
|
|
1858
|
-
const clientDir = resolve(
|
|
2770
|
+
const clientDir = resolve(__pkgDir, "client");
|
|
1859
2771
|
const { existsSync: existsSync$1 } = await import("node:fs");
|
|
1860
2772
|
const { port: actualPort } = await startServer({
|
|
1861
2773
|
port,
|
|
@@ -1871,18 +2783,24 @@ try {
|
|
|
1871
2783
|
} catch {
|
|
1872
2784
|
repoRoot = process.cwd();
|
|
1873
2785
|
}
|
|
1874
|
-
const __pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
1875
|
-
const version = JSON.parse(readFileSync(resolve(__pkgDir, "..", "package.json"), "utf-8")).version;
|
|
1876
2786
|
writeServerLock({
|
|
1877
2787
|
port: actualPort,
|
|
1878
2788
|
host,
|
|
1879
2789
|
pid: process.pid,
|
|
1880
2790
|
repoRoot,
|
|
1881
2791
|
startedAt: Date.now(),
|
|
1882
|
-
version
|
|
2792
|
+
version: currentVersion
|
|
1883
2793
|
});
|
|
1884
2794
|
} catch {}
|
|
1885
2795
|
console.log(`diffing server running at ${localUrl}`);
|
|
2796
|
+
await playStartupDisplay();
|
|
2797
|
+
try {
|
|
2798
|
+
const updateInfo = await updateCheckPromise;
|
|
2799
|
+
if (updateInfo?.hasUpdate) {
|
|
2800
|
+
const { printUpdateDisclaimer } = await import("./update-check-KL-2sGxE.mjs");
|
|
2801
|
+
printUpdateDisclaimer(currentVersion, updateInfo.latestVersion);
|
|
2802
|
+
}
|
|
2803
|
+
} catch {}
|
|
1886
2804
|
if (!opts.noOpen) {
|
|
1887
2805
|
const settings = loadSettings();
|
|
1888
2806
|
const openUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}`;
|