syntaur 0.17.0 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -1
- package/dashboard/dist/assets/{_basePickBy-Dek5-ACb.js → _basePickBy-DiPrdxAJ.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-BeLddDKU.js → _baseUniq-CChFfAnf.js} +1 -1
- package/dashboard/dist/assets/{arc-kotdj4Uh.js → arc-CfkGktFw.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BaqFXQ-q.js → architectureDiagram-2XIMDMQ5-hFcMu5eO.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BSyGne6h.js → blockDiagram-WCTKOSBZ-GZDth9sZ.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-iYjZFKOW.js → c4Diagram-IC4MRINW-BVajqiYz.js} +1 -1
- package/dashboard/dist/assets/channel-C3CI1Xiu.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-DvY9Y7jB.js → chunk-4BX2VUAB-L7ajwHyw.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-2vCDm2cJ.js → chunk-55IACEB6-BACmeqT4.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-BVPaoRtV.js → chunk-FMBD7UC4-BYQ2kuuH.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-B9uJpMzd.js → chunk-JSJVCQXG-akulVsXk.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-CHcjjGaq.js → chunk-KX2RTZJC-4ibFxLB_.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-B8n19pSs.js → chunk-NQ4KR5QH-DU4iJf0Y.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-CF9ZYFGK.js → chunk-QZHKN3VN-ouyGBhQB.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-mY_1Vmdf.js → chunk-WL4C6EOR-Pq-ySwIY.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-Ck2tXWRf.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-Ck2tXWRf.js +1 -0
- package/dashboard/dist/assets/clone-BEaO4u3F.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-DOYoG3gB.js → cose-bilkent-S5V4N54A-BfkMWgQb.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-CZFrp-Yq.js → dagre-KLK3FWXG-0yhY8ZsO.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-F3rF5YwM.js → diagram-E7M64L7V-DBkanpFi.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-BynWv-Vo.js → diagram-IFDJBPK2-CrucCpxV.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-NIyzhm_M.js → diagram-P4PSJMXO-BJrQ6lK3.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-Ca9NbihE.js → erDiagram-INFDFZHY--joDpxAm.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-YhS0MOu4.js → flowDiagram-PKNHOUZH-BUXiVwju.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-G7P3qq_2.js → ganttDiagram-A5KZAMGK-u2XuIMRA.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-BJAK7M7E.js → gitGraphDiagram-K3NZZRJ6-Ayf0-gwT.js} +1 -1
- package/dashboard/dist/assets/{graph-11geMy3f.js → graph-BUGVXq3c.js} +1 -1
- package/dashboard/dist/assets/index-Bv6vn4_x.js +535 -0
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-COcQYcLf.js → infoDiagram-LFFYTUFH-DYehx6g7.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-B7Jjyw8J.js → ishikawaDiagram-PHBUUO56-Bo8-BClf.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K--snU-QAX.js → journeyDiagram-4ABVD52K-DLm-bRYd.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-BucLoc89.js → kanban-definition-K7BYSVSG-stPdEvds.js} +1 -1
- package/dashboard/dist/assets/{layout-Bcgxdz8A.js → layout-B7aPbDvN.js} +1 -1
- package/dashboard/dist/assets/{linear-CRF8xwvk.js → linear-CtQ_RMAp.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-D5JD5uYg.js → mermaid.core-CcJoQ5At.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-BqGcTcbk.js → mindmap-definition-YRQLILUH-DahXO6Ui.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CnQ2EkyX.js → pieDiagram-SKSYHLDU-DXv973_0.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-CKCfmTRc.js → quadrantDiagram-337W2JSQ-5U0ryvjr.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-B9J4bvUK.js → requirementDiagram-Z7DCOOCP-CvGflSyl.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-CMSzI5rg.js → sankeyDiagram-WA2Y5GQK-AeetdpCi.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BFQDtxd0.js → sequenceDiagram-2WXFIKYE-BhRR4qzn.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DgWYKT02.js → stateDiagram-RAJIS63D-CxxF58nH.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DHCNHbs7.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-jANu0e7M.js → timeline-definition-YZTLITO2-BEQIeygp.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-BFjWMkyM.js → treemap-KZPCXAKY-BCWNTFfj.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-DGwrAJYj.js → vennDiagram-LZ73GAT5-BcuC_UIz.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BLCPfoFM.js → xychartDiagram-JWTSCODW-CWGez3_f.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dist/dashboard/server.js +379 -217
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +512 -349
- package/dist/index.js.map +1 -1
- package/dist/launch/index.js +20 -1
- package/dist/launch/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/claude-code/.claude-plugin/plugin.json +2 -1
- package/platforms/claude-code/commands/run-playbook/run-playbook.md +16 -0
- package/platforms/claude-code/skills/run-playbook/SKILL.md +87 -0
- package/platforms/codex/skills/run-playbook/SKILL.md +87 -0
- package/skills/run-playbook/SKILL.md +87 -0
- package/dashboard/dist/assets/channel-eXdj7ayV.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-B-bBQACr.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-B-bBQACr.js +0 -1
- package/dashboard/dist/assets/clone-CL04pv9J.js +0 -1
- package/dashboard/dist/assets/index--qwybK3W.js +0 -535
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Nmwft4cR.js +0 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -3871,8 +3871,8 @@ async function migrateFromMarkdown(projectsDir) {
|
|
|
3871
3871
|
return allSessions.length;
|
|
3872
3872
|
}
|
|
3873
3873
|
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
3874
|
-
const { readFile:
|
|
3875
|
-
const raw = await
|
|
3874
|
+
const { readFile: readFile20 } = await import("fs/promises");
|
|
3875
|
+
const raw = await readFile20(filePath, "utf-8");
|
|
3876
3876
|
const sessions = [];
|
|
3877
3877
|
const lines = raw.split("\n");
|
|
3878
3878
|
let inTable = false;
|
|
@@ -4270,13 +4270,16 @@ __export(scanner_exports, {
|
|
|
4270
4270
|
autoLinkPane: () => autoLinkPane,
|
|
4271
4271
|
checkTmuxAvailable: () => checkTmuxAvailable,
|
|
4272
4272
|
clearScanCache: () => clearScanCache,
|
|
4273
|
+
descendantPidsFromSnapshot: () => descendantPidsFromSnapshot,
|
|
4273
4274
|
execQuiet: () => execQuiet,
|
|
4274
4275
|
findListeningPorts: () => findListeningPorts,
|
|
4275
4276
|
getDescendantPids: () => getDescendantPids,
|
|
4276
4277
|
getGitInfo: () => getGitInfo,
|
|
4277
4278
|
getLsofOutput: () => getLsofOutput,
|
|
4279
|
+
getProcessSnapshot: () => getProcessSnapshot,
|
|
4278
4280
|
listTmuxPanes: () => listTmuxPanes,
|
|
4279
4281
|
loadWorkspaceRecords: () => loadWorkspaceRecords,
|
|
4282
|
+
parseProcessSnapshot: () => parseProcessSnapshot,
|
|
4280
4283
|
parseTmuxPaneOutput: () => parseTmuxPaneOutput,
|
|
4281
4284
|
resolveAndNormalize: () => resolveAndNormalize,
|
|
4282
4285
|
scanAllSessions: () => scanAllSessions,
|
|
@@ -4286,9 +4289,27 @@ __export(scanner_exports, {
|
|
|
4286
4289
|
import { execFile } from "child_process";
|
|
4287
4290
|
import { promisify } from "util";
|
|
4288
4291
|
import { resolve as resolve12 } from "path";
|
|
4289
|
-
import { realpath
|
|
4292
|
+
import { realpath } from "fs/promises";
|
|
4290
4293
|
function clearScanCache() {
|
|
4291
4294
|
cache = null;
|
|
4295
|
+
forceFreshNext = true;
|
|
4296
|
+
scanEpoch++;
|
|
4297
|
+
inFlight = null;
|
|
4298
|
+
tmuxAvailableCache = null;
|
|
4299
|
+
}
|
|
4300
|
+
function emptyScan() {
|
|
4301
|
+
return { sessions: [], tmuxAvailable: false };
|
|
4302
|
+
}
|
|
4303
|
+
function scanKey(serversDir2, projectsDir, assignmentsDir2) {
|
|
4304
|
+
return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
|
|
4305
|
+
}
|
|
4306
|
+
function delay(ms) {
|
|
4307
|
+
return new Promise((resolve29) => {
|
|
4308
|
+
const timer2 = setTimeout(resolve29, ms);
|
|
4309
|
+
if (typeof timer2.unref === "function") {
|
|
4310
|
+
timer2.unref();
|
|
4311
|
+
}
|
|
4312
|
+
});
|
|
4292
4313
|
}
|
|
4293
4314
|
function parseTmuxPaneOutput(output) {
|
|
4294
4315
|
return output.trim().split("\n").filter((line) => line.length > 0).map((line) => {
|
|
@@ -4322,15 +4343,20 @@ function findListeningPorts(lsofOutput, pids) {
|
|
|
4322
4343
|
}
|
|
4323
4344
|
async function execQuiet(cmd, args) {
|
|
4324
4345
|
try {
|
|
4325
|
-
const { stdout } = await exec(cmd, args
|
|
4346
|
+
const { stdout } = await exec(cmd, args, {
|
|
4347
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
4348
|
+
maxBuffer: PROBE_MAX_BUFFER
|
|
4349
|
+
});
|
|
4326
4350
|
return stdout.trim();
|
|
4327
4351
|
} catch {
|
|
4328
4352
|
return "";
|
|
4329
4353
|
}
|
|
4330
4354
|
}
|
|
4331
4355
|
async function checkTmuxAvailable() {
|
|
4332
|
-
|
|
4333
|
-
|
|
4356
|
+
if (!tmuxAvailableCache) {
|
|
4357
|
+
tmuxAvailableCache = execQuiet("which", ["tmux"]).then((result) => result.length > 0);
|
|
4358
|
+
}
|
|
4359
|
+
return tmuxAvailableCache;
|
|
4334
4360
|
}
|
|
4335
4361
|
async function sessionAlive(name) {
|
|
4336
4362
|
try {
|
|
@@ -4370,6 +4396,40 @@ async function getDescendantPids(rootPid, maxDepth = 4) {
|
|
|
4370
4396
|
}
|
|
4371
4397
|
return all;
|
|
4372
4398
|
}
|
|
4399
|
+
function parseProcessSnapshot(psOutput) {
|
|
4400
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
4401
|
+
for (const line of psOutput.split("\n")) {
|
|
4402
|
+
const parts = line.trim().split(/\s+/);
|
|
4403
|
+
if (parts.length < 2) continue;
|
|
4404
|
+
const pid = parseInt(parts[0], 10);
|
|
4405
|
+
const ppid = parseInt(parts[1], 10);
|
|
4406
|
+
if (isNaN(pid) || isNaN(ppid)) continue;
|
|
4407
|
+
const siblings = childrenOf.get(ppid);
|
|
4408
|
+
if (siblings) siblings.push(pid);
|
|
4409
|
+
else childrenOf.set(ppid, [pid]);
|
|
4410
|
+
}
|
|
4411
|
+
return { childrenOf };
|
|
4412
|
+
}
|
|
4413
|
+
async function getProcessSnapshot() {
|
|
4414
|
+
return parseProcessSnapshot(await execQuiet("ps", ["-axo", "pid=,ppid="]));
|
|
4415
|
+
}
|
|
4416
|
+
function descendantPidsFromSnapshot(rootPid, snapshot, maxDepth = 4) {
|
|
4417
|
+
const all = /* @__PURE__ */ new Set([rootPid]);
|
|
4418
|
+
let frontier = [rootPid];
|
|
4419
|
+
for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
|
|
4420
|
+
const nextFrontier = [];
|
|
4421
|
+
for (const pid of frontier) {
|
|
4422
|
+
for (const child of snapshot.childrenOf.get(pid) ?? []) {
|
|
4423
|
+
if (!all.has(child)) {
|
|
4424
|
+
all.add(child);
|
|
4425
|
+
nextFrontier.push(child);
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
frontier = nextFrontier;
|
|
4430
|
+
}
|
|
4431
|
+
return all;
|
|
4432
|
+
}
|
|
4373
4433
|
async function getLsofOutput() {
|
|
4374
4434
|
return execQuiet("lsof", ["-i", "-P", "-n", "-sTCP:LISTEN"]);
|
|
4375
4435
|
}
|
|
@@ -4391,62 +4451,11 @@ async function getGitInfo(cwd) {
|
|
|
4391
4451
|
return { branch: branch || null, worktree: isWorktree };
|
|
4392
4452
|
}
|
|
4393
4453
|
async function loadWorkspaceRecords(projectsDir, assignmentsDir2) {
|
|
4394
|
-
const records = [];
|
|
4395
4454
|
try {
|
|
4396
|
-
|
|
4397
|
-
for (const project of projects) {
|
|
4398
|
-
const projectAssignmentsDir = resolve12(projectsDir, project.slug, "assignments");
|
|
4399
|
-
let slugs;
|
|
4400
|
-
try {
|
|
4401
|
-
slugs = await readdir7(projectAssignmentsDir);
|
|
4402
|
-
} catch {
|
|
4403
|
-
continue;
|
|
4404
|
-
}
|
|
4405
|
-
for (const aslug of slugs) {
|
|
4406
|
-
const aFile = resolve12(projectAssignmentsDir, aslug, "assignment.md");
|
|
4407
|
-
try {
|
|
4408
|
-
const raw = await readFile9(aFile, "utf-8");
|
|
4409
|
-
const [fm] = extractFrontmatter2(raw);
|
|
4410
|
-
if (!fm) continue;
|
|
4411
|
-
records.push({
|
|
4412
|
-
projectSlug: project.slug,
|
|
4413
|
-
assignmentSlug: aslug,
|
|
4414
|
-
assignmentTitle: getField(fm, "title") ?? aslug,
|
|
4415
|
-
worktreePath: getNestedField(fm, "workspace", "worktreePath") ?? null,
|
|
4416
|
-
branch: getNestedField(fm, "workspace", "branch") ?? null
|
|
4417
|
-
});
|
|
4418
|
-
} catch {
|
|
4419
|
-
continue;
|
|
4420
|
-
}
|
|
4421
|
-
}
|
|
4422
|
-
}
|
|
4455
|
+
return await listWorkspaceRecords(projectsDir, assignmentsDir2);
|
|
4423
4456
|
} catch {
|
|
4457
|
+
return [];
|
|
4424
4458
|
}
|
|
4425
|
-
if (assignmentsDir2) {
|
|
4426
|
-
try {
|
|
4427
|
-
const entries = await readdir7(assignmentsDir2);
|
|
4428
|
-
for (const id of entries) {
|
|
4429
|
-
if (id.startsWith(".") || id.startsWith("_")) continue;
|
|
4430
|
-
const aFile = resolve12(assignmentsDir2, id, "assignment.md");
|
|
4431
|
-
try {
|
|
4432
|
-
const raw = await readFile9(aFile, "utf-8");
|
|
4433
|
-
const [fm] = extractFrontmatter2(raw);
|
|
4434
|
-
if (!fm) continue;
|
|
4435
|
-
records.push({
|
|
4436
|
-
projectSlug: null,
|
|
4437
|
-
assignmentSlug: id,
|
|
4438
|
-
assignmentTitle: getField(fm, "title") ?? id,
|
|
4439
|
-
worktreePath: getNestedField(fm, "workspace", "worktreePath") ?? null,
|
|
4440
|
-
branch: getNestedField(fm, "workspace", "branch") ?? null
|
|
4441
|
-
});
|
|
4442
|
-
} catch {
|
|
4443
|
-
continue;
|
|
4444
|
-
}
|
|
4445
|
-
}
|
|
4446
|
-
} catch {
|
|
4447
|
-
}
|
|
4448
|
-
}
|
|
4449
|
-
return records;
|
|
4450
4459
|
}
|
|
4451
4460
|
async function resolveAndNormalize(p) {
|
|
4452
4461
|
try {
|
|
@@ -4475,7 +4484,7 @@ async function autoLinkPane(cwd, branch, records) {
|
|
|
4475
4484
|
}
|
|
4476
4485
|
return null;
|
|
4477
4486
|
}
|
|
4478
|
-
async function scanSession(sessionData, lsofOutput, workspaceRecords) {
|
|
4487
|
+
async function scanSession(sessionData, lsofOutput, workspaceRecords, procSnapshot) {
|
|
4479
4488
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4480
4489
|
const alive = await sessionAlive(sessionData.session);
|
|
4481
4490
|
if (!alive) {
|
|
@@ -4498,13 +4507,13 @@ async function scanSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
4498
4507
|
windowMap.get(rp.windowIndex).panes.push(rp);
|
|
4499
4508
|
}
|
|
4500
4509
|
const cwdSet = new Set(rawPanes.map((p) => p.cwd));
|
|
4501
|
-
const
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4510
|
+
const gitInfoEntries = await Promise.all(
|
|
4511
|
+
[...cwdSet].map(async (cwd) => [cwd, await getGitInfo(cwd)])
|
|
4512
|
+
);
|
|
4513
|
+
const gitInfoCache = new Map(gitInfoEntries);
|
|
4505
4514
|
const pidToPaneKey = /* @__PURE__ */ new Map();
|
|
4506
4515
|
for (const rp of rawPanes) {
|
|
4507
|
-
const descendants =
|
|
4516
|
+
const descendants = descendantPidsFromSnapshot(rp.pid, procSnapshot);
|
|
4508
4517
|
const key = `${rp.windowIndex}:${rp.paneIndex}`;
|
|
4509
4518
|
for (const pid of descendants) {
|
|
4510
4519
|
pidToPaneKey.set(pid, key);
|
|
@@ -4529,37 +4538,38 @@ async function scanSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
4529
4538
|
}
|
|
4530
4539
|
const windows = [];
|
|
4531
4540
|
for (const [windowIndex, { name, panes: rawPanesInWindow }] of windowMap) {
|
|
4532
|
-
const panes =
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4541
|
+
const panes = await Promise.all(
|
|
4542
|
+
rawPanesInWindow.map(async (rp) => {
|
|
4543
|
+
const key = `${rp.windowIndex}:${rp.paneIndex}`;
|
|
4544
|
+
const gitInfo = gitInfoCache.get(rp.cwd) ?? { branch: null, worktree: false };
|
|
4545
|
+
const ports = panePorts.get(key) ?? [];
|
|
4546
|
+
const urls = ports.map((p) => `http://localhost:${p}`);
|
|
4547
|
+
const override = sessionData.overrides[key];
|
|
4548
|
+
let assignment = null;
|
|
4549
|
+
if (override) {
|
|
4550
|
+
const rec = workspaceRecords.find(
|
|
4551
|
+
(r) => r.projectSlug === override.project && r.assignmentSlug === override.assignment
|
|
4552
|
+
);
|
|
4553
|
+
assignment = {
|
|
4554
|
+
project: override.project,
|
|
4555
|
+
slug: override.assignment,
|
|
4556
|
+
title: rec?.assignmentTitle ?? override.assignment
|
|
4557
|
+
};
|
|
4558
|
+
} else {
|
|
4559
|
+
assignment = await autoLinkPane(rp.cwd, gitInfo.branch, workspaceRecords);
|
|
4560
|
+
}
|
|
4561
|
+
return {
|
|
4562
|
+
index: rp.paneIndex,
|
|
4563
|
+
command: rp.command,
|
|
4564
|
+
cwd: rp.cwd,
|
|
4565
|
+
branch: gitInfo.branch,
|
|
4566
|
+
worktree: gitInfo.worktree,
|
|
4567
|
+
ports,
|
|
4568
|
+
urls,
|
|
4569
|
+
assignment
|
|
4548
4570
|
};
|
|
4549
|
-
}
|
|
4550
|
-
|
|
4551
|
-
}
|
|
4552
|
-
panes.push({
|
|
4553
|
-
index: rp.paneIndex,
|
|
4554
|
-
command: rp.command,
|
|
4555
|
-
cwd: rp.cwd,
|
|
4556
|
-
branch: gitInfo.branch,
|
|
4557
|
-
worktree: gitInfo.worktree,
|
|
4558
|
-
ports,
|
|
4559
|
-
urls,
|
|
4560
|
-
assignment
|
|
4561
|
-
});
|
|
4562
|
-
}
|
|
4571
|
+
})
|
|
4572
|
+
);
|
|
4563
4573
|
windows.push({ index: windowIndex, name, panes });
|
|
4564
4574
|
}
|
|
4565
4575
|
windows.sort((a, b) => a.index - b.index);
|
|
@@ -4631,53 +4641,114 @@ async function scanProcessSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
4631
4641
|
windows: [{ index: 0, name: "process", panes: [pane] }]
|
|
4632
4642
|
};
|
|
4633
4643
|
}
|
|
4644
|
+
async function performScan(serversDir2, projectsDir, options) {
|
|
4645
|
+
const epoch = scanEpoch;
|
|
4646
|
+
const [tmuxAvailable, names, lsofOutput, workspaceRecords, procSnapshot] = await Promise.all([
|
|
4647
|
+
checkTmuxAvailable(),
|
|
4648
|
+
listSessionFiles(serversDir2),
|
|
4649
|
+
getLsofOutput(),
|
|
4650
|
+
loadWorkspaceRecords(projectsDir, options?.assignmentsDir),
|
|
4651
|
+
getProcessSnapshot()
|
|
4652
|
+
]);
|
|
4653
|
+
const datas = await Promise.all(names.map((name) => readSessionFile(serversDir2, name)));
|
|
4654
|
+
const scanned = await Promise.all(
|
|
4655
|
+
datas.map(async (data) => {
|
|
4656
|
+
if (!data) return null;
|
|
4657
|
+
if (data.kind === "process") {
|
|
4658
|
+
return scanProcessSession(data, lsofOutput, workspaceRecords);
|
|
4659
|
+
}
|
|
4660
|
+
if (tmuxAvailable) {
|
|
4661
|
+
return scanSession(data, lsofOutput, workspaceRecords, procSnapshot);
|
|
4662
|
+
}
|
|
4663
|
+
return null;
|
|
4664
|
+
})
|
|
4665
|
+
);
|
|
4666
|
+
const result = {
|
|
4667
|
+
sessions: scanned.filter((s) => s !== null),
|
|
4668
|
+
tmuxAvailable
|
|
4669
|
+
};
|
|
4670
|
+
if (epoch === scanEpoch) {
|
|
4671
|
+
cache = { data: result, expiry: Date.now() + CACHE_TTL_MS };
|
|
4672
|
+
lastKnown = result;
|
|
4673
|
+
forceFreshNext = false;
|
|
4674
|
+
}
|
|
4675
|
+
return result;
|
|
4676
|
+
}
|
|
4677
|
+
function refreshInBackground(serversDir2, projectsDir, options) {
|
|
4678
|
+
if (!inFlight) {
|
|
4679
|
+
const p = performScan(serversDir2, projectsDir, options);
|
|
4680
|
+
p.finally(() => {
|
|
4681
|
+
if (inFlight === p) inFlight = null;
|
|
4682
|
+
}).catch(() => {
|
|
4683
|
+
});
|
|
4684
|
+
inFlight = p;
|
|
4685
|
+
}
|
|
4686
|
+
return inFlight;
|
|
4687
|
+
}
|
|
4634
4688
|
async function scanAllSessions(serversDir2, projectsDir, options) {
|
|
4689
|
+
const key = scanKey(serversDir2, projectsDir, options?.assignmentsDir);
|
|
4690
|
+
if (key !== cacheKey) {
|
|
4691
|
+
cache = null;
|
|
4692
|
+
lastKnown = null;
|
|
4693
|
+
inFlight = null;
|
|
4694
|
+
forceFreshNext = false;
|
|
4695
|
+
scanEpoch++;
|
|
4696
|
+
cacheKey = key;
|
|
4697
|
+
}
|
|
4635
4698
|
if (!options?.bypassCache && cache && Date.now() < cache.expiry) {
|
|
4636
4699
|
return cache.data;
|
|
4637
4700
|
}
|
|
4638
|
-
const
|
|
4639
|
-
const
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
const data = await readSessionFile(serversDir2, name);
|
|
4645
|
-
if (!data) continue;
|
|
4646
|
-
if (data.kind === "process") {
|
|
4647
|
-
sessions.push(await scanProcessSession(data, lsofOutput, workspaceRecords));
|
|
4648
|
-
} else if (tmuxAvailable) {
|
|
4649
|
-
sessions.push(await scanSession(data, lsofOutput, workspaceRecords));
|
|
4650
|
-
}
|
|
4701
|
+
const refresh = refreshInBackground(serversDir2, projectsDir, options);
|
|
4702
|
+
const canServeStale = lastKnown !== null && !options?.bypassCache && !forceFreshNext;
|
|
4703
|
+
if (canServeStale) {
|
|
4704
|
+
void refresh.catch(() => {
|
|
4705
|
+
});
|
|
4706
|
+
return lastKnown;
|
|
4651
4707
|
}
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4708
|
+
if (options?.nonBlocking) {
|
|
4709
|
+
return Promise.race([
|
|
4710
|
+
refresh.catch(() => emptyScan()),
|
|
4711
|
+
delay(COLD_WAIT_BUDGET_MS).then(() => emptyScan())
|
|
4712
|
+
]);
|
|
4713
|
+
}
|
|
4714
|
+
return refresh;
|
|
4655
4715
|
}
|
|
4656
4716
|
async function scanSingleSession(serversDir2, projectsDir, name, options) {
|
|
4657
4717
|
const data = await readSessionFile(serversDir2, name);
|
|
4658
4718
|
if (!data) return null;
|
|
4659
|
-
const lsofOutput = await
|
|
4660
|
-
|
|
4719
|
+
const [lsofOutput, workspaceRecords, procSnapshot] = await Promise.all([
|
|
4720
|
+
getLsofOutput(),
|
|
4721
|
+
loadWorkspaceRecords(projectsDir, options?.assignmentsDir),
|
|
4722
|
+
getProcessSnapshot()
|
|
4723
|
+
]);
|
|
4661
4724
|
if (data.kind === "process") {
|
|
4662
4725
|
return scanProcessSession(data, lsofOutput, workspaceRecords);
|
|
4663
4726
|
}
|
|
4664
|
-
return scanSession(data, lsofOutput, workspaceRecords);
|
|
4727
|
+
return scanSession(data, lsofOutput, workspaceRecords, procSnapshot);
|
|
4665
4728
|
}
|
|
4666
|
-
var exec, cache, CACHE_TTL_MS;
|
|
4729
|
+
var exec, cache, lastKnown, inFlight, forceFreshNext, scanEpoch, cacheKey, CACHE_TTL_MS, COLD_WAIT_BUDGET_MS, PROBE_TIMEOUT_MS, PROBE_MAX_BUFFER, tmuxAvailableCache;
|
|
4667
4730
|
var init_scanner = __esm({
|
|
4668
4731
|
"src/dashboard/scanner.ts"() {
|
|
4669
4732
|
"use strict";
|
|
4670
4733
|
init_api();
|
|
4671
4734
|
init_servers();
|
|
4672
|
-
init_parser();
|
|
4673
4735
|
exec = promisify(execFile);
|
|
4674
4736
|
cache = null;
|
|
4737
|
+
lastKnown = null;
|
|
4738
|
+
inFlight = null;
|
|
4739
|
+
forceFreshNext = false;
|
|
4740
|
+
scanEpoch = 0;
|
|
4741
|
+
cacheKey = null;
|
|
4675
4742
|
CACHE_TTL_MS = 1e4;
|
|
4743
|
+
COLD_WAIT_BUDGET_MS = 2500;
|
|
4744
|
+
PROBE_TIMEOUT_MS = 15e3;
|
|
4745
|
+
PROBE_MAX_BUFFER = 32 * 1024 * 1024;
|
|
4746
|
+
tmuxAvailableCache = null;
|
|
4676
4747
|
}
|
|
4677
4748
|
});
|
|
4678
4749
|
|
|
4679
4750
|
// src/dashboard/api.ts
|
|
4680
|
-
import { readdir as
|
|
4751
|
+
import { readdir as readdir7, readFile as readFile9, writeFile as writeFile3 } from "fs/promises";
|
|
4681
4752
|
import { resolve as resolve13, dirname as dirname2, basename } from "path";
|
|
4682
4753
|
function clearFrontmatterField(content, key) {
|
|
4683
4754
|
const fieldRegex = new RegExp(`^(${escapeRegExp2(key)}:)\\s*.*$`, "m");
|
|
@@ -4717,10 +4788,41 @@ function emitTrace(traces, meta) {
|
|
|
4717
4788
|
JSON.stringify({ kind: "overview-trace", totalMs, phases: traces.entries, subPhases, ...meta })
|
|
4718
4789
|
);
|
|
4719
4790
|
}
|
|
4791
|
+
function invalidateRecordsCache() {
|
|
4792
|
+
projectRecordsCache.clear();
|
|
4793
|
+
standaloneRecordsCache.clear();
|
|
4794
|
+
}
|
|
4795
|
+
function installRecordsInvalidation(router) {
|
|
4796
|
+
for (const method of ["post", "put", "patch", "delete"]) {
|
|
4797
|
+
const original = router[method].bind(router);
|
|
4798
|
+
router[method] = (path, ...handlers) => {
|
|
4799
|
+
if (handlers.length > 0) {
|
|
4800
|
+
const last = handlers[handlers.length - 1];
|
|
4801
|
+
handlers[handlers.length - 1] = async (req, res, next) => {
|
|
4802
|
+
try {
|
|
4803
|
+
return await last(req, res, next);
|
|
4804
|
+
} finally {
|
|
4805
|
+
invalidateRecordsCache();
|
|
4806
|
+
}
|
|
4807
|
+
};
|
|
4808
|
+
}
|
|
4809
|
+
return original(path, ...handlers);
|
|
4810
|
+
};
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4720
4813
|
async function listStandaloneRecords(assignmentsDir2) {
|
|
4814
|
+
const key = assignmentsDir2 ?? "";
|
|
4815
|
+
const cached = standaloneRecordsCache.get(key);
|
|
4816
|
+
if (cached) return cached;
|
|
4817
|
+
const promise = computeStandaloneRecords(assignmentsDir2);
|
|
4818
|
+
standaloneRecordsCache.set(key, promise);
|
|
4819
|
+
promise.catch(() => standaloneRecordsCache.delete(key));
|
|
4820
|
+
return promise;
|
|
4821
|
+
}
|
|
4822
|
+
async function computeStandaloneRecords(assignmentsDir2) {
|
|
4721
4823
|
if (!assignmentsDir2) return [];
|
|
4722
4824
|
if (!await fileExists(assignmentsDir2)) return [];
|
|
4723
|
-
const entries = await
|
|
4825
|
+
const entries = await readdir7(assignmentsDir2, { withFileTypes: true });
|
|
4724
4826
|
const records = [];
|
|
4725
4827
|
for (const entry of entries) {
|
|
4726
4828
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
@@ -4728,7 +4830,7 @@ async function listStandaloneRecords(assignmentsDir2) {
|
|
|
4728
4830
|
const assignmentMdPath = resolve13(assignmentDir, "assignment.md");
|
|
4729
4831
|
if (!await fileExists(assignmentMdPath)) continue;
|
|
4730
4832
|
try {
|
|
4731
|
-
const content = await
|
|
4833
|
+
const content = await readFile9(assignmentMdPath, "utf-8");
|
|
4732
4834
|
const record = parseAssignmentFull(content);
|
|
4733
4835
|
records.push({ assignmentDir, id: entry.name, record });
|
|
4734
4836
|
} catch {
|
|
@@ -4804,7 +4906,7 @@ async function listProjects(projectsDir) {
|
|
|
4804
4906
|
async function readWorkspaceRegistry(projectsDir) {
|
|
4805
4907
|
const registryPath = resolve13(dirname2(projectsDir), "workspaces.json");
|
|
4806
4908
|
try {
|
|
4807
|
-
const raw = await
|
|
4909
|
+
const raw = await readFile9(registryPath, "utf-8");
|
|
4808
4910
|
const parsed = JSON.parse(raw);
|
|
4809
4911
|
return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
|
|
4810
4912
|
} catch {
|
|
@@ -4840,6 +4942,34 @@ async function listWorkspaces(projectsDir, assignmentsDir2) {
|
|
|
4840
4942
|
const workspaces = Array.from(workspaceSet).sort();
|
|
4841
4943
|
return { workspaces, hasUngrouped };
|
|
4842
4944
|
}
|
|
4945
|
+
async function listWorkspaceRecords(projectsDir, assignmentsDir2) {
|
|
4946
|
+
const [projectRecords, standaloneRecords] = await Promise.all([
|
|
4947
|
+
listProjectRecords(projectsDir),
|
|
4948
|
+
listStandaloneRecords(assignmentsDir2)
|
|
4949
|
+
]);
|
|
4950
|
+
const records = [];
|
|
4951
|
+
for (const project of projectRecords) {
|
|
4952
|
+
for (const assignment of project.assignments) {
|
|
4953
|
+
records.push({
|
|
4954
|
+
projectSlug: project.summary.slug,
|
|
4955
|
+
assignmentSlug: assignment.slug,
|
|
4956
|
+
assignmentTitle: assignment.title || assignment.slug,
|
|
4957
|
+
worktreePath: assignment.workspace.worktreePath ?? null,
|
|
4958
|
+
branch: assignment.workspace.branch ?? null
|
|
4959
|
+
});
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
for (const standalone of standaloneRecords) {
|
|
4963
|
+
records.push({
|
|
4964
|
+
projectSlug: null,
|
|
4965
|
+
assignmentSlug: standalone.id,
|
|
4966
|
+
assignmentTitle: standalone.record.title || standalone.id,
|
|
4967
|
+
worktreePath: standalone.record.workspace.worktreePath ?? null,
|
|
4968
|
+
branch: standalone.record.workspace.branch ?? null
|
|
4969
|
+
});
|
|
4970
|
+
}
|
|
4971
|
+
return records;
|
|
4972
|
+
}
|
|
4843
4973
|
async function createWorkspace(projectsDir, name) {
|
|
4844
4974
|
const registered = await readWorkspaceRegistry(projectsDir);
|
|
4845
4975
|
if (!registered.includes(name)) {
|
|
@@ -4865,7 +4995,7 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
|
|
|
4865
4995
|
const timestamp = nowTimestamp();
|
|
4866
4996
|
for (const slug of projectsReferencing) {
|
|
4867
4997
|
const path = resolve13(projectsDir, slug, "project.md");
|
|
4868
|
-
const raw = await
|
|
4998
|
+
const raw = await readFile9(path, "utf-8");
|
|
4869
4999
|
let next = clearFrontmatterField(raw, "workspace");
|
|
4870
5000
|
next = setUpdatedField(next, timestamp);
|
|
4871
5001
|
await writeFileForce(path, next);
|
|
@@ -4874,7 +5004,7 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
|
|
|
4874
5004
|
for (const id of standalonesReferencing) {
|
|
4875
5005
|
if (!opts.assignmentsDir) break;
|
|
4876
5006
|
const path = resolve13(opts.assignmentsDir, id, "assignment.md");
|
|
4877
|
-
const raw = await
|
|
5007
|
+
const raw = await readFile9(path, "utf-8");
|
|
4878
5008
|
let next = clearFrontmatterField(raw, "workspaceGroup");
|
|
4879
5009
|
next = setUpdatedField(next, timestamp);
|
|
4880
5010
|
await writeFileForce(path, next);
|
|
@@ -4884,6 +5014,9 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
|
|
|
4884
5014
|
const registered = await readWorkspaceRegistry(projectsDir);
|
|
4885
5015
|
const filtered = registered.filter((w) => w !== name);
|
|
4886
5016
|
await writeWorkspaceRegistry(projectsDir, filtered);
|
|
5017
|
+
if (rewroteFiles) {
|
|
5018
|
+
invalidateRecordsCache();
|
|
5019
|
+
}
|
|
4887
5020
|
return { rewroteFiles };
|
|
4888
5021
|
}
|
|
4889
5022
|
async function getOverview(projectsDir, serversDir2, assignmentsDir2, options = {}) {
|
|
@@ -4927,7 +5060,12 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
|
|
|
4927
5060
|
const servers = await timed(
|
|
4928
5061
|
traces,
|
|
4929
5062
|
"scan-tmux-sessions",
|
|
4930
|
-
() =>
|
|
5063
|
+
() => (
|
|
5064
|
+
// Overview only needs aggregate counts — never block its render on a
|
|
5065
|
+
// live scan; serve last-known stats and let the scan refresh in the
|
|
5066
|
+
// background (stale-while-revalidate).
|
|
5067
|
+
scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2, nonBlocking: true })
|
|
5068
|
+
)
|
|
4931
5069
|
);
|
|
4932
5070
|
if (servers.tmuxAvailable) {
|
|
4933
5071
|
const alive = servers.sessions.filter((s) => s.alive).length;
|
|
@@ -5046,7 +5184,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
|
|
|
5046
5184
|
if (!filePath || !await fileExists(filePath)) {
|
|
5047
5185
|
return null;
|
|
5048
5186
|
}
|
|
5049
|
-
const content = await
|
|
5187
|
+
const content = await readFile9(filePath, "utf-8");
|
|
5050
5188
|
const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
|
|
5051
5189
|
return {
|
|
5052
5190
|
documentType,
|
|
@@ -5072,7 +5210,7 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
|
|
|
5072
5210
|
if (!fileName) return null;
|
|
5073
5211
|
const filePath = resolve13(resolved.assignmentDir, fileName);
|
|
5074
5212
|
if (!await fileExists(filePath)) return null;
|
|
5075
|
-
const content = await
|
|
5213
|
+
const content = await readFile9(filePath, "utf-8");
|
|
5076
5214
|
const label = resolved.id;
|
|
5077
5215
|
const title = documentType === "assignment" ? `Edit Assignment: ${label}` : documentType === "plan" ? `Edit Plan: ${label}` : documentType === "scratchpad" ? `Edit Scratchpad: ${label}` : documentType === "handoff" ? `Append Handoff: ${label}` : `Append Decision: ${label}`;
|
|
5078
5216
|
return {
|
|
@@ -5091,7 +5229,7 @@ async function getProjectDetail(projectsDir, slug) {
|
|
|
5091
5229
|
if (!await fileExists(projectMdPath)) {
|
|
5092
5230
|
return null;
|
|
5093
5231
|
}
|
|
5094
|
-
const projectContent = await
|
|
5232
|
+
const projectContent = await readFile9(projectMdPath, "utf-8");
|
|
5095
5233
|
const project = parseProject(projectContent);
|
|
5096
5234
|
const assignments = await listAssignmentRecords(projectPath);
|
|
5097
5235
|
const rollup = await buildProjectRollup(projectPath, project, assignments);
|
|
@@ -5128,18 +5266,18 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5128
5266
|
if (!await fileExists(assignmentMdPath)) {
|
|
5129
5267
|
return null;
|
|
5130
5268
|
}
|
|
5131
|
-
const assignmentContent = await
|
|
5269
|
+
const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
|
|
5132
5270
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
5133
5271
|
let projectWorkspace = null;
|
|
5134
5272
|
const projectMdPath = resolve13(projectsDir, projectSlug, "project.md");
|
|
5135
5273
|
if (await fileExists(projectMdPath)) {
|
|
5136
|
-
const projectContent = await
|
|
5274
|
+
const projectContent = await readFile9(projectMdPath, "utf-8");
|
|
5137
5275
|
projectWorkspace = parseProject(projectContent).workspace;
|
|
5138
5276
|
}
|
|
5139
5277
|
let plan = null;
|
|
5140
5278
|
const planPath = resolve13(assignmentDir, "plan.md");
|
|
5141
5279
|
if (await fileExists(planPath)) {
|
|
5142
|
-
const planContent = await
|
|
5280
|
+
const planContent = await readFile9(planPath, "utf-8");
|
|
5143
5281
|
const parsed = parsePlan(planContent);
|
|
5144
5282
|
plan = {
|
|
5145
5283
|
status: parsed.status,
|
|
@@ -5150,7 +5288,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5150
5288
|
let scratchpad = null;
|
|
5151
5289
|
const scratchpadPath = resolve13(assignmentDir, "scratchpad.md");
|
|
5152
5290
|
if (await fileExists(scratchpadPath)) {
|
|
5153
|
-
const scratchpadContent = await
|
|
5291
|
+
const scratchpadContent = await readFile9(scratchpadPath, "utf-8");
|
|
5154
5292
|
const parsed = parseScratchpad(scratchpadContent);
|
|
5155
5293
|
scratchpad = {
|
|
5156
5294
|
updated: parsed.updated,
|
|
@@ -5160,7 +5298,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5160
5298
|
let handoff = null;
|
|
5161
5299
|
const handoffPath = resolve13(assignmentDir, "handoff.md");
|
|
5162
5300
|
if (await fileExists(handoffPath)) {
|
|
5163
|
-
const handoffContent = await
|
|
5301
|
+
const handoffContent = await readFile9(handoffPath, "utf-8");
|
|
5164
5302
|
const parsed = parseHandoff(handoffContent);
|
|
5165
5303
|
handoff = {
|
|
5166
5304
|
updated: parsed.updated,
|
|
@@ -5171,7 +5309,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5171
5309
|
let decisionRecord = null;
|
|
5172
5310
|
const decisionRecordPath = resolve13(assignmentDir, "decision-record.md");
|
|
5173
5311
|
if (await fileExists(decisionRecordPath)) {
|
|
5174
|
-
const decisionRecordContent = await
|
|
5312
|
+
const decisionRecordContent = await readFile9(decisionRecordPath, "utf-8");
|
|
5175
5313
|
const parsed = parseDecisionRecord(decisionRecordContent);
|
|
5176
5314
|
decisionRecord = {
|
|
5177
5315
|
updated: parsed.updated,
|
|
@@ -5182,7 +5320,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5182
5320
|
let progress = null;
|
|
5183
5321
|
const progressPath = resolve13(assignmentDir, "progress.md");
|
|
5184
5322
|
if (await fileExists(progressPath)) {
|
|
5185
|
-
const progressContent = await
|
|
5323
|
+
const progressContent = await readFile9(progressPath, "utf-8");
|
|
5186
5324
|
const parsed = parseProgress(progressContent);
|
|
5187
5325
|
progress = {
|
|
5188
5326
|
updated: parsed.updated,
|
|
@@ -5193,7 +5331,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
|
|
|
5193
5331
|
let comments = null;
|
|
5194
5332
|
const commentsPath = resolve13(assignmentDir, "comments.md");
|
|
5195
5333
|
if (await fileExists(commentsPath)) {
|
|
5196
|
-
const commentsContent = await
|
|
5334
|
+
const commentsContent = await readFile9(commentsPath, "utf-8");
|
|
5197
5335
|
const parsed = parseComments(commentsContent);
|
|
5198
5336
|
comments = {
|
|
5199
5337
|
updated: parsed.updated,
|
|
@@ -5344,7 +5482,7 @@ async function countMentionsInAssignment(sourceDir, target) {
|
|
|
5344
5482
|
const bodies = [];
|
|
5345
5483
|
const assignmentMd = resolve13(sourceDir, "assignment.md");
|
|
5346
5484
|
if (await fileExists(assignmentMd)) {
|
|
5347
|
-
const content = await
|
|
5485
|
+
const content = await readFile9(assignmentMd, "utf-8");
|
|
5348
5486
|
const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
|
|
5349
5487
|
if (todosMatch) bodies.push(todosMatch[1]);
|
|
5350
5488
|
}
|
|
@@ -5352,7 +5490,7 @@ async function countMentionsInAssignment(sourceDir, target) {
|
|
|
5352
5490
|
const path = resolve13(sourceDir, filename);
|
|
5353
5491
|
if (await fileExists(path)) {
|
|
5354
5492
|
try {
|
|
5355
|
-
bodies.push(await
|
|
5493
|
+
bodies.push(await readFile9(path, "utf-8"));
|
|
5356
5494
|
} catch {
|
|
5357
5495
|
}
|
|
5358
5496
|
}
|
|
@@ -5412,42 +5550,42 @@ async function buildStandaloneAssignmentDetail(resolved) {
|
|
|
5412
5550
|
const assignmentDir = resolved.assignmentDir;
|
|
5413
5551
|
const assignmentMdPath = resolve13(assignmentDir, "assignment.md");
|
|
5414
5552
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
5415
|
-
const assignmentContent = await
|
|
5553
|
+
const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
|
|
5416
5554
|
const assignment = parseAssignmentFull(assignmentContent);
|
|
5417
5555
|
let plan = null;
|
|
5418
5556
|
const planPath = resolve13(assignmentDir, "plan.md");
|
|
5419
5557
|
if (await fileExists(planPath)) {
|
|
5420
|
-
const parsed = parsePlan(await
|
|
5558
|
+
const parsed = parsePlan(await readFile9(planPath, "utf-8"));
|
|
5421
5559
|
plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
|
|
5422
5560
|
}
|
|
5423
5561
|
let scratchpad = null;
|
|
5424
5562
|
const scratchpadPath = resolve13(assignmentDir, "scratchpad.md");
|
|
5425
5563
|
if (await fileExists(scratchpadPath)) {
|
|
5426
|
-
const parsed = parseScratchpad(await
|
|
5564
|
+
const parsed = parseScratchpad(await readFile9(scratchpadPath, "utf-8"));
|
|
5427
5565
|
scratchpad = { updated: parsed.updated, body: parsed.body };
|
|
5428
5566
|
}
|
|
5429
5567
|
let handoff = null;
|
|
5430
5568
|
const handoffPath = resolve13(assignmentDir, "handoff.md");
|
|
5431
5569
|
if (await fileExists(handoffPath)) {
|
|
5432
|
-
const parsed = parseHandoff(await
|
|
5570
|
+
const parsed = parseHandoff(await readFile9(handoffPath, "utf-8"));
|
|
5433
5571
|
handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
|
|
5434
5572
|
}
|
|
5435
5573
|
let decisionRecord = null;
|
|
5436
5574
|
const decisionRecordPath = resolve13(assignmentDir, "decision-record.md");
|
|
5437
5575
|
if (await fileExists(decisionRecordPath)) {
|
|
5438
|
-
const parsed = parseDecisionRecord(await
|
|
5576
|
+
const parsed = parseDecisionRecord(await readFile9(decisionRecordPath, "utf-8"));
|
|
5439
5577
|
decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
|
|
5440
5578
|
}
|
|
5441
5579
|
let progress = null;
|
|
5442
5580
|
const progressPath = resolve13(assignmentDir, "progress.md");
|
|
5443
5581
|
if (await fileExists(progressPath)) {
|
|
5444
|
-
const parsed = parseProgress(await
|
|
5582
|
+
const parsed = parseProgress(await readFile9(progressPath, "utf-8"));
|
|
5445
5583
|
progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
5446
5584
|
}
|
|
5447
5585
|
let comments = null;
|
|
5448
5586
|
const commentsPath = resolve13(assignmentDir, "comments.md");
|
|
5449
5587
|
if (await fileExists(commentsPath)) {
|
|
5450
|
-
const parsed = parseComments(await
|
|
5588
|
+
const parsed = parseComments(await readFile9(commentsPath, "utf-8"));
|
|
5451
5589
|
comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
|
|
5452
5590
|
}
|
|
5453
5591
|
const detail = {
|
|
@@ -5484,6 +5622,14 @@ async function buildStandaloneAssignmentDetail(resolved) {
|
|
|
5484
5622
|
return detail;
|
|
5485
5623
|
}
|
|
5486
5624
|
async function listProjectRecords(projectsDir, traces) {
|
|
5625
|
+
const cached = projectRecordsCache.get(projectsDir);
|
|
5626
|
+
if (cached) return cached;
|
|
5627
|
+
const promise = computeProjectRecords(projectsDir, traces);
|
|
5628
|
+
projectRecordsCache.set(projectsDir, promise);
|
|
5629
|
+
promise.catch(() => projectRecordsCache.delete(projectsDir));
|
|
5630
|
+
return promise;
|
|
5631
|
+
}
|
|
5632
|
+
async function computeProjectRecords(projectsDir, traces) {
|
|
5487
5633
|
if (!await fileExists(projectsDir)) {
|
|
5488
5634
|
return [];
|
|
5489
5635
|
}
|
|
@@ -5491,7 +5637,7 @@ async function listProjectRecords(projectsDir, traces) {
|
|
|
5491
5637
|
migratedProjectsDirs.add(projectsDir);
|
|
5492
5638
|
await migrateLegacyProjectFiles(projectsDir);
|
|
5493
5639
|
}
|
|
5494
|
-
const entries = await
|
|
5640
|
+
const entries = await readdir7(projectsDir, { withFileTypes: true });
|
|
5495
5641
|
const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
|
|
5496
5642
|
const maybeRecords = await Promise.all(
|
|
5497
5643
|
projectDirs.map(async (entry) => {
|
|
@@ -5501,7 +5647,7 @@ async function listProjectRecords(projectsDir, traces) {
|
|
|
5501
5647
|
return null;
|
|
5502
5648
|
}
|
|
5503
5649
|
const t0 = traces ? performance.now() : 0;
|
|
5504
|
-
const projectContent = await
|
|
5650
|
+
const projectContent = await readFile9(projectMdPath, "utf-8");
|
|
5505
5651
|
const project = parseProject(projectContent);
|
|
5506
5652
|
if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
|
|
5507
5653
|
const t1 = traces ? performance.now() : 0;
|
|
@@ -5546,7 +5692,7 @@ async function listAssignmentRecords(projectPath, traces) {
|
|
|
5546
5692
|
if (!await fileExists(assignmentsDir2)) {
|
|
5547
5693
|
return [];
|
|
5548
5694
|
}
|
|
5549
|
-
const entries = await
|
|
5695
|
+
const entries = await readdir7(assignmentsDir2, { withFileTypes: true });
|
|
5550
5696
|
const dirEntries = entries.filter((entry) => entry.isDirectory());
|
|
5551
5697
|
const maybeRecords = await Promise.all(
|
|
5552
5698
|
dirEntries.map(async (entry) => {
|
|
@@ -5555,7 +5701,7 @@ async function listAssignmentRecords(projectPath, traces) {
|
|
|
5555
5701
|
return null;
|
|
5556
5702
|
}
|
|
5557
5703
|
const t0 = traces ? performance.now() : 0;
|
|
5558
|
-
const content = await
|
|
5704
|
+
const content = await readFile9(assignmentMd, "utf-8");
|
|
5559
5705
|
const parsed = parseAssignmentFull(content);
|
|
5560
5706
|
if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
|
|
5561
5707
|
return parsed;
|
|
@@ -5570,14 +5716,14 @@ async function listResources(projectPath) {
|
|
|
5570
5716
|
if (!await fileExists(resourcesDir)) {
|
|
5571
5717
|
return [];
|
|
5572
5718
|
}
|
|
5573
|
-
const entries = await
|
|
5719
|
+
const entries = await readdir7(resourcesDir, { withFileTypes: true });
|
|
5574
5720
|
const results = [];
|
|
5575
5721
|
for (const entry of entries) {
|
|
5576
5722
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
5577
5723
|
continue;
|
|
5578
5724
|
}
|
|
5579
5725
|
const filePath = resolve13(resourcesDir, entry.name);
|
|
5580
|
-
const content = await
|
|
5726
|
+
const content = await readFile9(filePath, "utf-8");
|
|
5581
5727
|
const parsed = parseResource(content);
|
|
5582
5728
|
results.push({
|
|
5583
5729
|
name: parsed.name,
|
|
@@ -5596,14 +5742,14 @@ async function listMemories(projectPath) {
|
|
|
5596
5742
|
if (!await fileExists(memoriesDir)) {
|
|
5597
5743
|
return [];
|
|
5598
5744
|
}
|
|
5599
|
-
const entries = await
|
|
5745
|
+
const entries = await readdir7(memoriesDir, { withFileTypes: true });
|
|
5600
5746
|
const results = [];
|
|
5601
5747
|
for (const entry of entries) {
|
|
5602
5748
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
|
|
5603
5749
|
continue;
|
|
5604
5750
|
}
|
|
5605
5751
|
const filePath = resolve13(memoriesDir, entry.name);
|
|
5606
|
-
const content = await
|
|
5752
|
+
const content = await readFile9(filePath, "utf-8");
|
|
5607
5753
|
const parsed = parseMemory(content);
|
|
5608
5754
|
results.push({
|
|
5609
5755
|
name: parsed.name,
|
|
@@ -5666,7 +5812,7 @@ async function getMemoryDetail(projectsDir, projectSlug, itemSlug) {
|
|
|
5666
5812
|
if (!projectRecord) return null;
|
|
5667
5813
|
const filePath = resolve13(projectRecord.projectPath, "memories", `${itemSlug}.md`);
|
|
5668
5814
|
if (!await fileExists(filePath)) return null;
|
|
5669
|
-
const content = await
|
|
5815
|
+
const content = await readFile9(filePath, "utf-8");
|
|
5670
5816
|
const parsed = parseMemory(content);
|
|
5671
5817
|
return {
|
|
5672
5818
|
name: parsed.name,
|
|
@@ -5692,7 +5838,7 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
|
|
|
5692
5838
|
if (!projectRecord) return null;
|
|
5693
5839
|
const filePath = resolve13(projectRecord.projectPath, "resources", `${itemSlug}.md`);
|
|
5694
5840
|
if (!await fileExists(filePath)) return null;
|
|
5695
|
-
const content = await
|
|
5841
|
+
const content = await readFile9(filePath, "utf-8");
|
|
5696
5842
|
const parsed = parseResource(content);
|
|
5697
5843
|
return {
|
|
5698
5844
|
name: parsed.name,
|
|
@@ -5710,7 +5856,7 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
|
|
|
5710
5856
|
async function loadDependencyGraph(projectPath, assignments) {
|
|
5711
5857
|
const statusPath = resolve13(projectPath, "_status.md");
|
|
5712
5858
|
if (await fileExists(statusPath)) {
|
|
5713
|
-
const statusContent = await
|
|
5859
|
+
const statusContent = await readFile9(statusPath, "utf-8");
|
|
5714
5860
|
const parsed = parseStatus(statusContent);
|
|
5715
5861
|
const derivedGraph = extractMermaidGraph(parsed.body);
|
|
5716
5862
|
if (derivedGraph) {
|
|
@@ -5871,7 +6017,7 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
|
|
|
5871
6017
|
unmet.push(`${dependency} (missing)`);
|
|
5872
6018
|
continue;
|
|
5873
6019
|
}
|
|
5874
|
-
const content = await
|
|
6020
|
+
const content = await readFile9(dependencyPath, "utf-8");
|
|
5875
6021
|
const parsed = parseAssignmentFull(content);
|
|
5876
6022
|
if (!terminals.has(parsed.status)) {
|
|
5877
6023
|
unmet.push(`${dependency} (${parsed.status})`);
|
|
@@ -6164,7 +6310,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
|
|
|
6164
6310
|
return 0;
|
|
6165
6311
|
}
|
|
6166
6312
|
try {
|
|
6167
|
-
const content = await
|
|
6313
|
+
const content = await readFile9(commentsPath, "utf-8");
|
|
6168
6314
|
const parsed = parseComments(content);
|
|
6169
6315
|
return parsed.entries.filter(
|
|
6170
6316
|
(e) => e.type === "question" && e.resolved !== true
|
|
@@ -6232,12 +6378,12 @@ async function listPlaybooks(playbooksDir2) {
|
|
|
6232
6378
|
if (!await fileExists(playbooksDir2)) return [];
|
|
6233
6379
|
const config = await readConfig();
|
|
6234
6380
|
const disabledSet = new Set(config.playbooks.disabled);
|
|
6235
|
-
const entries = await
|
|
6381
|
+
const entries = await readdir7(playbooksDir2, { withFileTypes: true });
|
|
6236
6382
|
const playbooks = [];
|
|
6237
6383
|
for (const entry of entries) {
|
|
6238
6384
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
6239
6385
|
const filePath = resolve13(playbooksDir2, entry.name);
|
|
6240
|
-
const raw = await
|
|
6386
|
+
const raw = await readFile9(filePath, "utf-8");
|
|
6241
6387
|
const parsed = parsePlaybook(raw);
|
|
6242
6388
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
6243
6389
|
playbooks.push({
|
|
@@ -6271,7 +6417,7 @@ async function getPlaybookDetail(playbooksDir2, slug) {
|
|
|
6271
6417
|
enabled
|
|
6272
6418
|
};
|
|
6273
6419
|
}
|
|
6274
|
-
var WorkspaceBlockedError, STALE_ASSIGNMENT_MS, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, TERMINAL_STATUSES2, STATUS_TO_SEGMENT, HERO_PRIORITY, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
|
|
6420
|
+
var WorkspaceBlockedError, STALE_ASSIGNMENT_MS, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, TERMINAL_STATUSES2, STATUS_TO_SEGMENT, HERO_PRIORITY, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
|
|
6275
6421
|
var init_api = __esm({
|
|
6276
6422
|
"src/dashboard/api.ts"() {
|
|
6277
6423
|
"use strict";
|
|
@@ -6322,6 +6468,8 @@ var init_api = __esm({
|
|
|
6322
6468
|
["blocked", "blocked"],
|
|
6323
6469
|
["stale", "stale"]
|
|
6324
6470
|
];
|
|
6471
|
+
projectRecordsCache = /* @__PURE__ */ new Map();
|
|
6472
|
+
standaloneRecordsCache = /* @__PURE__ */ new Map();
|
|
6325
6473
|
DEFAULT_TRANSITION_DEFINITIONS = [
|
|
6326
6474
|
{
|
|
6327
6475
|
command: "start",
|
|
@@ -6870,7 +7018,7 @@ function mergePatch(current, patch) {
|
|
|
6870
7018
|
// src/utils/view-prefs.ts
|
|
6871
7019
|
init_paths();
|
|
6872
7020
|
init_fs();
|
|
6873
|
-
import { readFile as
|
|
7021
|
+
import { readFile as readFile10, rename as rename3, unlink as unlink3 } from "fs/promises";
|
|
6874
7022
|
import { resolve as resolve14 } from "path";
|
|
6875
7023
|
function corruptFilePath() {
|
|
6876
7024
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -6893,7 +7041,7 @@ async function readViewPrefsFile() {
|
|
|
6893
7041
|
}
|
|
6894
7042
|
let raw;
|
|
6895
7043
|
try {
|
|
6896
|
-
raw = await
|
|
7044
|
+
raw = await readFile10(path, "utf-8");
|
|
6897
7045
|
} catch {
|
|
6898
7046
|
return { ...DEFAULT_VIEW_PREFS_FILE };
|
|
6899
7047
|
}
|
|
@@ -7076,7 +7224,7 @@ function isDashboardSlot(value) {
|
|
|
7076
7224
|
// src/utils/saved-views.ts
|
|
7077
7225
|
init_paths();
|
|
7078
7226
|
init_fs();
|
|
7079
|
-
import { readFile as
|
|
7227
|
+
import { readFile as readFile11, rename as rename4, unlink as unlink4 } from "fs/promises";
|
|
7080
7228
|
import { randomUUID } from "crypto";
|
|
7081
7229
|
import { resolve as resolve15 } from "path";
|
|
7082
7230
|
function corruptFilePath2() {
|
|
@@ -7106,7 +7254,7 @@ async function readSavedViewsFile() {
|
|
|
7106
7254
|
}
|
|
7107
7255
|
let raw;
|
|
7108
7256
|
try {
|
|
7109
|
-
raw = await
|
|
7257
|
+
raw = await readFile11(path, "utf-8");
|
|
7110
7258
|
} catch {
|
|
7111
7259
|
return cloneDefault();
|
|
7112
7260
|
}
|
|
@@ -7422,7 +7570,7 @@ init_lifecycle();
|
|
|
7422
7570
|
init_slug();
|
|
7423
7571
|
import { Router as Router2 } from "express";
|
|
7424
7572
|
import { resolve as resolve18, basename as basename3, isAbsolute as isAbsolute2 } from "path";
|
|
7425
|
-
import { rm, readFile as
|
|
7573
|
+
import { rm, readFile as readFile14, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
|
|
7426
7574
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
7427
7575
|
|
|
7428
7576
|
// src/utils/uuid.ts
|
|
@@ -7439,7 +7587,7 @@ init_fs();
|
|
|
7439
7587
|
init_frontmatter();
|
|
7440
7588
|
init_fs();
|
|
7441
7589
|
import { spawn } from "child_process";
|
|
7442
|
-
import { readFile as
|
|
7590
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
7443
7591
|
function run(command, args, cwd) {
|
|
7444
7592
|
return new Promise((resolvePromise) => {
|
|
7445
7593
|
const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -7489,7 +7637,7 @@ async function createWorktreeAndRecord(opts) {
|
|
|
7489
7637
|
const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;
|
|
7490
7638
|
await createWorktree({ repository, branch, worktreePath, parentBranch });
|
|
7491
7639
|
try {
|
|
7492
|
-
const content = await
|
|
7640
|
+
const content = await readFile12(assignmentPath, "utf-8");
|
|
7493
7641
|
const updated = updateAssignmentWorkspace(content, {
|
|
7494
7642
|
repository,
|
|
7495
7643
|
worktreePath,
|
|
@@ -7573,14 +7721,14 @@ function detectCurrentBranch(cwd) {
|
|
|
7573
7721
|
// src/dashboard/repository-candidates.ts
|
|
7574
7722
|
init_fs();
|
|
7575
7723
|
init_parser();
|
|
7576
|
-
import { readdir as
|
|
7724
|
+
import { readdir as readdir8, readFile as readFile13 } from "fs/promises";
|
|
7577
7725
|
import { resolve as resolve17 } from "path";
|
|
7578
7726
|
async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
|
|
7579
7727
|
const seen = /* @__PURE__ */ new Set();
|
|
7580
7728
|
const out = [];
|
|
7581
7729
|
const projectPath = resolve17(projectsDir, projectSlug, "project.md");
|
|
7582
7730
|
if (await fileExists(projectPath)) {
|
|
7583
|
-
const project = parseProject(await
|
|
7731
|
+
const project = parseProject(await readFile13(projectPath, "utf-8"));
|
|
7584
7732
|
for (const raw of project.repositories) {
|
|
7585
7733
|
const path = raw.trim();
|
|
7586
7734
|
if (!path) continue;
|
|
@@ -7592,12 +7740,12 @@ async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
|
|
|
7592
7740
|
}
|
|
7593
7741
|
const assignmentsDir2 = resolve17(projectsDir, projectSlug, "assignments");
|
|
7594
7742
|
if (await fileExists(assignmentsDir2)) {
|
|
7595
|
-
const entries = await
|
|
7743
|
+
const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
|
|
7596
7744
|
for (const entry of entries) {
|
|
7597
7745
|
if (!entry.isDirectory()) continue;
|
|
7598
7746
|
const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
|
|
7599
7747
|
if (!await fileExists(assignmentMd)) continue;
|
|
7600
|
-
const parsed = parseAssignmentFull(await
|
|
7748
|
+
const parsed = parseAssignmentFull(await readFile13(assignmentMd, "utf-8"));
|
|
7601
7749
|
const repo = parsed.workspace.repository?.trim();
|
|
7602
7750
|
if (!repo) continue;
|
|
7603
7751
|
const abs = resolve17(repo);
|
|
@@ -7614,13 +7762,13 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
|
|
|
7614
7762
|
}
|
|
7615
7763
|
const seen = /* @__PURE__ */ new Set();
|
|
7616
7764
|
const out = [];
|
|
7617
|
-
const entries = await
|
|
7765
|
+
const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
|
|
7618
7766
|
for (const entry of entries) {
|
|
7619
7767
|
if (!entry.isDirectory()) continue;
|
|
7620
7768
|
if (entry.name === excludeAssignmentId) continue;
|
|
7621
7769
|
const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
|
|
7622
7770
|
if (!await fileExists(assignmentMd)) continue;
|
|
7623
|
-
const parsed = parseAssignmentFull(await
|
|
7771
|
+
const parsed = parseAssignmentFull(await readFile13(assignmentMd, "utf-8"));
|
|
7624
7772
|
const repo = parsed.workspace.repository?.trim();
|
|
7625
7773
|
if (!repo) continue;
|
|
7626
7774
|
const abs = resolve17(repo);
|
|
@@ -8186,14 +8334,14 @@ async function readCurrentDocument(filePath) {
|
|
|
8186
8334
|
if (!await fileExists(filePath)) {
|
|
8187
8335
|
return null;
|
|
8188
8336
|
}
|
|
8189
|
-
return
|
|
8337
|
+
return readFile14(filePath, "utf-8");
|
|
8190
8338
|
}
|
|
8191
8339
|
async function handleWorktreeCreate(req, res, ctx) {
|
|
8192
8340
|
if (!await fileExists(ctx.assignmentPath)) {
|
|
8193
8341
|
res.status(404).json({ error: "Assignment not found" });
|
|
8194
8342
|
return;
|
|
8195
8343
|
}
|
|
8196
|
-
const parsed = parseAssignmentFull(await
|
|
8344
|
+
const parsed = parseAssignmentFull(await readFile14(ctx.assignmentPath, "utf-8"));
|
|
8197
8345
|
if (parsed.workspace.worktreePath) {
|
|
8198
8346
|
res.status(409).json({ error: "Worktree already configured for this assignment" });
|
|
8199
8347
|
return;
|
|
@@ -8285,6 +8433,7 @@ async function handleWorktreeCreate(req, res, ctx) {
|
|
|
8285
8433
|
function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
|
|
8286
8434
|
const linkedTodosLookup = todosDir2 ? { todosDir: todosDir2, projectsDir } : void 0;
|
|
8287
8435
|
const router = Router2();
|
|
8436
|
+
installRecordsInvalidation(router);
|
|
8288
8437
|
router.get("/api/templates/project", (_req, res) => {
|
|
8289
8438
|
const content = renderProject({
|
|
8290
8439
|
id: generateId(),
|
|
@@ -8562,7 +8711,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
|
|
|
8562
8711
|
}
|
|
8563
8712
|
const nextContentRaw = requireContent(req, res);
|
|
8564
8713
|
if (!nextContentRaw) return;
|
|
8565
|
-
const currentContent = await
|
|
8714
|
+
const currentContent = await readFile14(filePath, "utf-8");
|
|
8566
8715
|
const frontmatterBlock = extractFrontmatterBlock(currentContent);
|
|
8567
8716
|
if (!frontmatterBlock) {
|
|
8568
8717
|
res.status(500).json({ error: `${kind} file is malformed (no frontmatter)` });
|
|
@@ -9013,7 +9162,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
|
|
|
9013
9162
|
let currentContent;
|
|
9014
9163
|
let currentCount = 0;
|
|
9015
9164
|
if (await fileExists(commentsPath)) {
|
|
9016
|
-
currentContent = await
|
|
9165
|
+
currentContent = await readFile14(commentsPath, "utf-8");
|
|
9017
9166
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
9018
9167
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
9019
9168
|
} else {
|
|
@@ -9070,7 +9219,7 @@ ${entry}`;
|
|
|
9070
9219
|
res.status(400).json({ error: "resolved (boolean) is required" });
|
|
9071
9220
|
return;
|
|
9072
9221
|
}
|
|
9073
|
-
const content = await
|
|
9222
|
+
const content = await readFile14(commentsPath, "utf-8");
|
|
9074
9223
|
const parsed = parseComments(content);
|
|
9075
9224
|
const target = parsed.entries.find((e) => e.id === commentId);
|
|
9076
9225
|
if (!target) {
|
|
@@ -9117,7 +9266,7 @@ ${entry}`;
|
|
|
9117
9266
|
});
|
|
9118
9267
|
return;
|
|
9119
9268
|
}
|
|
9120
|
-
let content = await
|
|
9269
|
+
let content = await readFile14(projectPath, "utf-8");
|
|
9121
9270
|
content = setTopLevelField(content, "workspace", workspace ?? null);
|
|
9122
9271
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
9123
9272
|
await writeFileForce(projectPath, content);
|
|
@@ -9154,7 +9303,7 @@ ${entry}`;
|
|
|
9154
9303
|
return;
|
|
9155
9304
|
}
|
|
9156
9305
|
const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
|
|
9157
|
-
let content = await
|
|
9306
|
+
let content = await readFile14(assignmentPath, "utf-8");
|
|
9158
9307
|
content = setTopLevelField(content, "workspaceGroup", workspaceGroup ?? null);
|
|
9159
9308
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
9160
9309
|
await writeFileForce(assignmentPath, content);
|
|
@@ -9249,7 +9398,7 @@ ${entry}`;
|
|
|
9249
9398
|
return;
|
|
9250
9399
|
}
|
|
9251
9400
|
const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
|
|
9252
|
-
const parsedForSlug = parseAssignmentFull(await
|
|
9401
|
+
const parsedForSlug = parseAssignmentFull(await readFile14(assignmentPath, "utf-8"));
|
|
9253
9402
|
const assignmentSlugForBranch = parsedForSlug.slug || resolved.id;
|
|
9254
9403
|
await handleWorktreeCreate(req, res, {
|
|
9255
9404
|
assignmentPath,
|
|
@@ -9278,7 +9427,7 @@ ${entry}`;
|
|
|
9278
9427
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
|
|
9279
9428
|
return;
|
|
9280
9429
|
}
|
|
9281
|
-
let content = await
|
|
9430
|
+
let content = await readFile14(projectPath, "utf-8");
|
|
9282
9431
|
content = setTopLevelField(content, "statusOverride", status ?? null);
|
|
9283
9432
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
9284
9433
|
await writeFileForce(projectPath, content);
|
|
@@ -9311,7 +9460,7 @@ ${entry}`;
|
|
|
9311
9460
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
|
|
9312
9461
|
return;
|
|
9313
9462
|
}
|
|
9314
|
-
let content = await
|
|
9463
|
+
let content = await readFile14(assignmentPath, "utf-8");
|
|
9315
9464
|
content = setTopLevelField(content, "status", status);
|
|
9316
9465
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
9317
9466
|
if (status !== "blocked") {
|
|
@@ -9345,7 +9494,7 @@ ${entry}`;
|
|
|
9345
9494
|
res.status(400).json({ error: validation.error });
|
|
9346
9495
|
return;
|
|
9347
9496
|
}
|
|
9348
|
-
let content = await
|
|
9497
|
+
let content = await readFile14(assignmentPath, "utf-8");
|
|
9349
9498
|
content = setTopLevelField(content, "assignee", validation.value);
|
|
9350
9499
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
9351
9500
|
await writeFileForce(assignmentPath, content);
|
|
@@ -9376,7 +9525,7 @@ ${entry}`;
|
|
|
9376
9525
|
res.status(400).json({ error: validation.error });
|
|
9377
9526
|
return;
|
|
9378
9527
|
}
|
|
9379
|
-
let content = await
|
|
9528
|
+
let content = await readFile14(assignmentPath, "utf-8");
|
|
9380
9529
|
content = setTopLevelField(content, "title", validation.value);
|
|
9381
9530
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
9382
9531
|
await writeFileForce(assignmentPath, content);
|
|
@@ -9894,7 +10043,7 @@ ${entry}`;
|
|
|
9894
10043
|
res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
|
|
9895
10044
|
return;
|
|
9896
10045
|
}
|
|
9897
|
-
let content = await
|
|
10046
|
+
let content = await readFile14(assignmentPath, "utf-8");
|
|
9898
10047
|
content = setTopLevelField(content, "status", status);
|
|
9899
10048
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
9900
10049
|
if (status !== "blocked") {
|
|
@@ -9930,7 +10079,7 @@ ${entry}`;
|
|
|
9930
10079
|
res.status(400).json({ error: validation.error });
|
|
9931
10080
|
return;
|
|
9932
10081
|
}
|
|
9933
|
-
let content = await
|
|
10082
|
+
let content = await readFile14(assignmentPath, "utf-8");
|
|
9934
10083
|
content = setTopLevelField(content, "assignee", validation.value);
|
|
9935
10084
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
9936
10085
|
await writeFileForce(assignmentPath, content);
|
|
@@ -9963,7 +10112,7 @@ ${entry}`;
|
|
|
9963
10112
|
res.status(400).json({ error: validation.error });
|
|
9964
10113
|
return;
|
|
9965
10114
|
}
|
|
9966
|
-
let content = await
|
|
10115
|
+
let content = await readFile14(assignmentPath, "utf-8");
|
|
9967
10116
|
content = setTopLevelField(content, "title", validation.value);
|
|
9968
10117
|
content = setTopLevelField(content, "updated", nowTimestamp());
|
|
9969
10118
|
await writeFileForce(assignmentPath, content);
|
|
@@ -10032,7 +10181,7 @@ ${entry}`;
|
|
|
10032
10181
|
if (!await fileExists(assignmentPath)) {
|
|
10033
10182
|
throw new Error("assignment file not found");
|
|
10034
10183
|
}
|
|
10035
|
-
let content = await
|
|
10184
|
+
let content = await readFile14(assignmentPath, "utf-8");
|
|
10036
10185
|
content = setTopLevelField(content, "status", status);
|
|
10037
10186
|
content = setTopLevelField(content, "updated", timestamp);
|
|
10038
10187
|
if (status !== "blocked") {
|
|
@@ -10188,7 +10337,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
|
|
|
10188
10337
|
let currentContent;
|
|
10189
10338
|
let currentCount = 0;
|
|
10190
10339
|
if (await fileExists(commentsPath)) {
|
|
10191
|
-
currentContent = await
|
|
10340
|
+
currentContent = await readFile14(commentsPath, "utf-8");
|
|
10192
10341
|
const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
|
|
10193
10342
|
if (countMatch) currentCount = parseInt(countMatch[1], 10);
|
|
10194
10343
|
} else {
|
|
@@ -10228,7 +10377,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
|
|
|
10228
10377
|
res.status(400).json({ error: "resolved (boolean) is required" });
|
|
10229
10378
|
return;
|
|
10230
10379
|
}
|
|
10231
|
-
const content = await
|
|
10380
|
+
const content = await readFile14(commentsPath, "utf-8");
|
|
10232
10381
|
const parsed = parseComments(content);
|
|
10233
10382
|
const target = parsed.entries.find((e) => e.id === commentId);
|
|
10234
10383
|
if (!target) {
|
|
@@ -10956,26 +11105,26 @@ import { Router as Router8 } from "express";
|
|
|
10956
11105
|
|
|
10957
11106
|
// src/utils/status-config-resolution.ts
|
|
10958
11107
|
init_frontmatter();
|
|
10959
|
-
import { readFile as
|
|
11108
|
+
import { readFile as readFile15, writeFile as writeFile4, rm as rm2 } from "fs/promises";
|
|
10960
11109
|
import { dirname as dirname4 } from "path";
|
|
10961
11110
|
|
|
10962
11111
|
// src/utils/assignment-walk.ts
|
|
10963
11112
|
init_fs();
|
|
10964
11113
|
import { resolve as resolve20 } from "path";
|
|
10965
|
-
import { readdir as
|
|
11114
|
+
import { readdir as readdir9 } from "fs/promises";
|
|
10966
11115
|
async function listAssignmentsByProject(projectsDir, standaloneDir) {
|
|
10967
11116
|
const result = {
|
|
10968
11117
|
withAssignmentMd: [],
|
|
10969
11118
|
orphanFolders: []
|
|
10970
11119
|
};
|
|
10971
11120
|
if (await fileExists(projectsDir)) {
|
|
10972
|
-
const projects = await
|
|
11121
|
+
const projects = await readdir9(projectsDir, { withFileTypes: true });
|
|
10973
11122
|
for (const m of projects) {
|
|
10974
11123
|
if (!m.isDirectory()) continue;
|
|
10975
11124
|
if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
|
|
10976
11125
|
const assignmentsDir2 = resolve20(projectsDir, m.name, "assignments");
|
|
10977
11126
|
if (!await fileExists(assignmentsDir2)) continue;
|
|
10978
|
-
const entries = await
|
|
11127
|
+
const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
|
|
10979
11128
|
for (const a of entries) {
|
|
10980
11129
|
if (!a.isDirectory()) continue;
|
|
10981
11130
|
if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
|
|
@@ -10997,7 +11146,7 @@ async function listAssignmentsByProject(projectsDir, standaloneDir) {
|
|
|
10997
11146
|
}
|
|
10998
11147
|
}
|
|
10999
11148
|
if (standaloneDir !== null && await fileExists(standaloneDir)) {
|
|
11000
|
-
const entries = await
|
|
11149
|
+
const entries = await readdir9(standaloneDir, { withFileTypes: true });
|
|
11001
11150
|
for (const a of entries) {
|
|
11002
11151
|
if (!a.isDirectory()) continue;
|
|
11003
11152
|
if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
|
|
@@ -11039,7 +11188,7 @@ async function scanAssignmentsByStatus(projectsDir, standaloneDir, ids) {
|
|
|
11039
11188
|
const assignmentPath = `${entry.assignmentDir}/assignment.md`;
|
|
11040
11189
|
let content;
|
|
11041
11190
|
try {
|
|
11042
|
-
content = await
|
|
11191
|
+
content = await readFile15(assignmentPath, "utf-8");
|
|
11043
11192
|
} catch (err) {
|
|
11044
11193
|
const code = err?.code;
|
|
11045
11194
|
if (code === "ENOENT") {
|
|
@@ -11102,7 +11251,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
|
|
|
11102
11251
|
const list = affected.get(r.id) ?? [];
|
|
11103
11252
|
for (const a of list) {
|
|
11104
11253
|
try {
|
|
11105
|
-
const content = await
|
|
11254
|
+
const content = await readFile15(a.path, "utf-8");
|
|
11106
11255
|
buffer.set(a.path, content);
|
|
11107
11256
|
} catch (err) {
|
|
11108
11257
|
const code = err?.code;
|
|
@@ -11130,7 +11279,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
|
|
|
11130
11279
|
for (const a of list) {
|
|
11131
11280
|
let current;
|
|
11132
11281
|
try {
|
|
11133
|
-
current = await
|
|
11282
|
+
current = await readFile15(a.path, "utf-8");
|
|
11134
11283
|
} catch (err) {
|
|
11135
11284
|
const code = err?.code;
|
|
11136
11285
|
if (code === "ENOENT") {
|
|
@@ -11179,7 +11328,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
|
|
|
11179
11328
|
const list = affected.get(r.id) ?? [];
|
|
11180
11329
|
for (const a of list) {
|
|
11181
11330
|
try {
|
|
11182
|
-
const current = await
|
|
11331
|
+
const current = await readFile15(a.path, "utf-8");
|
|
11183
11332
|
const fm = parseAssignmentFrontmatter(current);
|
|
11184
11333
|
if (fm.status !== r.id) {
|
|
11185
11334
|
console.warn(
|
|
@@ -11315,6 +11464,7 @@ function mapResolutionErrorToHttp(err, applied) {
|
|
|
11315
11464
|
}
|
|
11316
11465
|
function createStatusConfigRouter(projectsDir, assignmentsDir2) {
|
|
11317
11466
|
const router = Router8();
|
|
11467
|
+
installRecordsInvalidation(router);
|
|
11318
11468
|
router.get("/", async (_req, res) => {
|
|
11319
11469
|
try {
|
|
11320
11470
|
const config = await getStatusConfig();
|
|
@@ -12025,7 +12175,7 @@ init_timestamp();
|
|
|
12025
12175
|
init_fs();
|
|
12026
12176
|
import { Router as Router11 } from "express";
|
|
12027
12177
|
import { resolve as resolve23 } from "path";
|
|
12028
|
-
import { readFile as
|
|
12178
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
12029
12179
|
init_playbooks();
|
|
12030
12180
|
function statusForPlaybookError(code) {
|
|
12031
12181
|
switch (code) {
|
|
@@ -12108,7 +12258,7 @@ function createPlaybooksRouter(playbooksDir2) {
|
|
|
12108
12258
|
return;
|
|
12109
12259
|
}
|
|
12110
12260
|
const filePath = resolve23(playbooksDir2, resolved.filename);
|
|
12111
|
-
const content = await
|
|
12261
|
+
const content = await readFile16(filePath, "utf-8");
|
|
12112
12262
|
res.json({
|
|
12113
12263
|
documentType: "playbook",
|
|
12114
12264
|
title: `Edit Playbook: ${resolved.slug}`,
|
|
@@ -12205,7 +12355,7 @@ init_parser2();
|
|
|
12205
12355
|
init_fs();
|
|
12206
12356
|
init_paths();
|
|
12207
12357
|
import { Router as Router12 } from "express";
|
|
12208
|
-
import { readdir as
|
|
12358
|
+
import { readdir as readdir10 } from "fs/promises";
|
|
12209
12359
|
import { resolve as resolvePath, dirname as dirname5 } from "path";
|
|
12210
12360
|
import { rename as rename5, mkdir as mkdir2 } from "fs/promises";
|
|
12211
12361
|
init_slug();
|
|
@@ -12545,6 +12695,7 @@ async function promoteTodosToNewAssignment(groups, options) {
|
|
|
12545
12695
|
}
|
|
12546
12696
|
|
|
12547
12697
|
// src/dashboard/api-todos.ts
|
|
12698
|
+
init_api();
|
|
12548
12699
|
var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
|
|
12549
12700
|
function getWorkspaceParam(value) {
|
|
12550
12701
|
if (Array.isArray(value)) {
|
|
@@ -12559,6 +12710,7 @@ function touchItem3(item) {
|
|
|
12559
12710
|
}
|
|
12560
12711
|
function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
12561
12712
|
const router = Router12();
|
|
12713
|
+
installRecordsInvalidation(router);
|
|
12562
12714
|
function broadcastUpdate() {
|
|
12563
12715
|
broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
12564
12716
|
}
|
|
@@ -12678,7 +12830,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
12678
12830
|
router.get("/", async (_req, res) => {
|
|
12679
12831
|
try {
|
|
12680
12832
|
await ensureDir(todosDir2);
|
|
12681
|
-
const files = await
|
|
12833
|
+
const files = await readdir10(todosDir2).catch(() => []);
|
|
12682
12834
|
const workspaces = [];
|
|
12683
12835
|
for (const file of files) {
|
|
12684
12836
|
if (typeof file !== "string") continue;
|
|
@@ -12793,7 +12945,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
12793
12945
|
try {
|
|
12794
12946
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
12795
12947
|
const { resolve: resolve29 } = await import("path");
|
|
12796
|
-
const { readFile:
|
|
12948
|
+
const { readFile: readFile20 } = await import("fs/promises");
|
|
12797
12949
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
12798
12950
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
12799
12951
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
@@ -12812,7 +12964,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
12812
12964
|
await ensureDir(resolve29(todosDir2, "archive"));
|
|
12813
12965
|
let archContent = "";
|
|
12814
12966
|
if (await fileExists(archFile)) {
|
|
12815
|
-
archContent = await
|
|
12967
|
+
archContent = await readFile20(archFile, "utf-8");
|
|
12816
12968
|
archContent = archContent.trimEnd() + "\n\n";
|
|
12817
12969
|
} else {
|
|
12818
12970
|
archContent = `---
|
|
@@ -13094,7 +13246,7 @@ workspace: ${workspace}
|
|
|
13094
13246
|
const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
13095
13247
|
const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
13096
13248
|
const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
13097
|
-
const { readFile:
|
|
13249
|
+
const { readFile: readFile20 } = await import("fs/promises");
|
|
13098
13250
|
const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
|
|
13099
13251
|
const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
|
|
13100
13252
|
let assignmentRef;
|
|
@@ -13115,7 +13267,7 @@ workspace: ${workspace}
|
|
|
13115
13267
|
}
|
|
13116
13268
|
const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
|
|
13117
13269
|
if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
|
|
13118
|
-
let content = await
|
|
13270
|
+
let content = await readFile20(assignmentMdPath, "utf-8");
|
|
13119
13271
|
content = appendTodosToAssignmentBody2(
|
|
13120
13272
|
content,
|
|
13121
13273
|
items.map((it) => ({
|
|
@@ -13303,8 +13455,9 @@ init_fs();
|
|
|
13303
13455
|
init_paths();
|
|
13304
13456
|
init_slug();
|
|
13305
13457
|
import { Router as Router13 } from "express";
|
|
13306
|
-
import { mkdir as mkdir3, readFile as
|
|
13458
|
+
import { mkdir as mkdir3, readFile as readFile17, rename as rename6 } from "fs/promises";
|
|
13307
13459
|
import { resolve as resolve25, dirname as dirname6 } from "path";
|
|
13460
|
+
init_api();
|
|
13308
13461
|
var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
|
|
13309
13462
|
function touchItem4(item) {
|
|
13310
13463
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -13353,6 +13506,7 @@ function notFound(res, slug) {
|
|
|
13353
13506
|
}
|
|
13354
13507
|
function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
|
|
13355
13508
|
const router = Router13({ mergeParams: true });
|
|
13509
|
+
installRecordsInvalidation(router);
|
|
13356
13510
|
function broadcastUpdate(projectSlug) {
|
|
13357
13511
|
broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
13358
13512
|
}
|
|
@@ -13525,7 +13679,7 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
|
|
|
13525
13679
|
const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
|
|
13526
13680
|
let archContent = "";
|
|
13527
13681
|
if (await fileExists(archFile)) {
|
|
13528
|
-
archContent = await
|
|
13682
|
+
archContent = await readFile17(archFile, "utf-8");
|
|
13529
13683
|
archContent = archContent.trimEnd() + "\n\n";
|
|
13530
13684
|
} else {
|
|
13531
13685
|
archContent = `---
|
|
@@ -13982,7 +14136,7 @@ workspace: ${slug}
|
|
|
13982
14136
|
}
|
|
13983
14137
|
const assignmentMdPath = resolve25(assignmentDir, "assignment.md");
|
|
13984
14138
|
if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
|
|
13985
|
-
let content = await
|
|
14139
|
+
let content = await readFile17(assignmentMdPath, "utf-8");
|
|
13986
14140
|
content = appendTodosToAssignmentBody2(
|
|
13987
14141
|
content,
|
|
13988
14142
|
items.map((it) => ({
|
|
@@ -14178,7 +14332,7 @@ workspace: ${slug}
|
|
|
14178
14332
|
|
|
14179
14333
|
// src/dashboard/api-bundles.ts
|
|
14180
14334
|
import { Router as Router14 } from "express";
|
|
14181
|
-
import { readdir as
|
|
14335
|
+
import { readdir as readdir11 } from "fs/promises";
|
|
14182
14336
|
|
|
14183
14337
|
// src/todos/bundle-parser.ts
|
|
14184
14338
|
init_parser();
|
|
@@ -14186,7 +14340,7 @@ init_fs();
|
|
|
14186
14340
|
init_paths();
|
|
14187
14341
|
init_parser2();
|
|
14188
14342
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
14189
|
-
import { readFile as
|
|
14343
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
14190
14344
|
var BUNDLE_ID_REGEX = /^[a-f0-9]{4}$/;
|
|
14191
14345
|
var SCOPE_VALUES = /* @__PURE__ */ new Set(["workspace", "project", "global"]);
|
|
14192
14346
|
var SCOPE_ID_REGEX = /^[a-z0-9_][a-z0-9_-]*$/;
|
|
@@ -14253,7 +14407,7 @@ function parseBundles(content) {
|
|
|
14253
14407
|
async function readBundles(todosDir2) {
|
|
14254
14408
|
const path = bundlesPath(todosDir2);
|
|
14255
14409
|
if (!await fileExists(path)) return [];
|
|
14256
|
-
const content = await
|
|
14410
|
+
const content = await readFile18(path, "utf-8");
|
|
14257
14411
|
return parseBundles(content).bundles;
|
|
14258
14412
|
}
|
|
14259
14413
|
|
|
@@ -14302,7 +14456,7 @@ function createBundlesRouter(todosDir2, broadcast) {
|
|
|
14302
14456
|
try {
|
|
14303
14457
|
await ensureDir(todosDir2);
|
|
14304
14458
|
const bundles = await readBundles(todosDir2);
|
|
14305
|
-
const workspaceFiles = await
|
|
14459
|
+
const workspaceFiles = await readdir11(todosDir2).catch(() => []);
|
|
14306
14460
|
const itemsByKey = /* @__PURE__ */ new Map();
|
|
14307
14461
|
for (const f of workspaceFiles) {
|
|
14308
14462
|
if (typeof f !== "string") continue;
|
|
@@ -14416,6 +14570,8 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
|
|
|
14416
14570
|
|
|
14417
14571
|
// src/dashboard/api-backup.ts
|
|
14418
14572
|
init_config2();
|
|
14573
|
+
init_api();
|
|
14574
|
+
init_scanner();
|
|
14419
14575
|
import { Router as Router16 } from "express";
|
|
14420
14576
|
|
|
14421
14577
|
// src/utils/github-backup.ts
|
|
@@ -14424,7 +14580,7 @@ init_fs();
|
|
|
14424
14580
|
init_config2();
|
|
14425
14581
|
import { execFile as execFile2 } from "child_process";
|
|
14426
14582
|
import { promisify as promisify2 } from "util";
|
|
14427
|
-
import { cp, mkdtemp, rm as rm3, readFile as
|
|
14583
|
+
import { cp, mkdtemp, rm as rm3, readFile as readFile19, writeFile as writeFile5, unlink as unlink5, stat, open as open2, rename as rename7 } from "fs/promises";
|
|
14428
14584
|
import { resolve as resolve27, join as join2 } from "path";
|
|
14429
14585
|
import { tmpdir } from "os";
|
|
14430
14586
|
var exec2 = promisify2(execFile2);
|
|
@@ -14485,7 +14641,7 @@ async function acquireLock() {
|
|
|
14485
14641
|
return lockPath;
|
|
14486
14642
|
} catch (err) {
|
|
14487
14643
|
if (err.code === "EEXIST") {
|
|
14488
|
-
const pid = await
|
|
14644
|
+
const pid = await readFile19(lockPath, "utf-8").catch(() => "");
|
|
14489
14645
|
throw new Error(
|
|
14490
14646
|
`Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
|
|
14491
14647
|
);
|
|
@@ -14532,7 +14688,7 @@ function resolveCategoriesStrict(csv) {
|
|
|
14532
14688
|
return parseCategoriesStrict(parts);
|
|
14533
14689
|
}
|
|
14534
14690
|
async function readSanitizedConfig(configPath) {
|
|
14535
|
-
const content = await
|
|
14691
|
+
const content = await readFile19(configPath, "utf-8");
|
|
14536
14692
|
return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
|
|
14537
14693
|
}
|
|
14538
14694
|
async function backupToGithub(overrides) {
|
|
@@ -14814,6 +14970,9 @@ function createBackupRouter() {
|
|
|
14814
14970
|
success: false,
|
|
14815
14971
|
error: error instanceof Error ? error.message : String(error)
|
|
14816
14972
|
});
|
|
14973
|
+
} finally {
|
|
14974
|
+
invalidateRecordsCache();
|
|
14975
|
+
clearScanCache();
|
|
14817
14976
|
}
|
|
14818
14977
|
});
|
|
14819
14978
|
return router;
|
|
@@ -15052,6 +15211,9 @@ function createDashboardServer(options) {
|
|
|
15052
15211
|
});
|
|
15053
15212
|
});
|
|
15054
15213
|
function broadcast(message) {
|
|
15214
|
+
if (message.type === "project-updated" || message.type === "assignment-updated") {
|
|
15215
|
+
invalidateRecordsCache();
|
|
15216
|
+
}
|
|
15055
15217
|
const data = JSON.stringify(message);
|
|
15056
15218
|
for (const client of clients) {
|
|
15057
15219
|
if (client.readyState === WebSocket.OPEN) {
|