syntaur 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/{_basePickBy-BQIP1Ca7.js → _basePickBy-DTYUlCEg.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-BnBWRwT7.js → _baseUniq-C0Y4HRd5.js} +1 -1
- package/dashboard/dist/assets/{arc-BYWL4eq0.js → arc-BFx2eqN9.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CD_SWPSa.js → architectureDiagram-2XIMDMQ5-Erol1JD6.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BS1ZbFBU.js → blockDiagram-WCTKOSBZ-kSkh6VkS.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-D99yg-l2.js → c4Diagram-IC4MRINW-C04oKzvX.js} +1 -1
- package/dashboard/dist/assets/channel-C82tBKZ7.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-BkN9IORC.js → chunk-4BX2VUAB-C3t0tXt-.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-BQPHWefV.js → chunk-55IACEB6-2cnyEL0b.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-CNcExMdx.js → chunk-FMBD7UC4-DIY9MTNi.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-LXBmftkC.js → chunk-JSJVCQXG-Cw8fpqpE.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-Tqi7zNqq.js → chunk-KX2RTZJC-BAhd66XV.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-DkMbx-rW.js → chunk-NQ4KR5QH-RzXwoxk3.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-BlrRCfkJ.js → chunk-QZHKN3VN-Dgri4sGz.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-of3XBzMu.js → chunk-WL4C6EOR-DYLj9JRa.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-STOZ51tg.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-STOZ51tg.js +1 -0
- package/dashboard/dist/assets/clone-TzhWk-Bj.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-BlIiyO76.js → cose-bilkent-S5V4N54A-DfY_Fnfu.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-CYQjSI9N.js → dagre-KLK3FWXG-CyTKIVSK.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-BZHzTKct.js → diagram-E7M64L7V-Krub7Xxo.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-kMP3WqBV.js → diagram-IFDJBPK2-giUl9uHz.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-BWSHyFOv.js → diagram-P4PSJMXO-oAtnO3C9.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-B5HrvsPP.js → erDiagram-INFDFZHY-eYaVjXqo.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-Dm4ewP7w.js → flowDiagram-PKNHOUZH-or5S0_Sb.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DB3k27zu.js → ganttDiagram-A5KZAMGK-C9R1lsme.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-G7y6Ey-m.js → gitGraphDiagram-K3NZZRJ6-BQwsDzvp.js} +1 -1
- package/dashboard/dist/assets/{graph-CaM4i6vq.js → graph-EQOX1wg8.js} +1 -1
- package/dashboard/dist/assets/index-Cy7yjuqO.js +500 -0
- package/dashboard/dist/assets/index-u80fISp0.css +1 -0
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-JNTUbTjg.js → infoDiagram-LFFYTUFH-BjLlQWxk.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-BZJt1ht8.js → ishikawaDiagram-PHBUUO56-BNjydh4j.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-DPcqvl9A.js → journeyDiagram-4ABVD52K-DNzE7TgQ.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-D1D7AuOV.js → kanban-definition-K7BYSVSG-FbNvGx6i.js} +1 -1
- package/dashboard/dist/assets/{layout-BTOh3EDT.js → layout-B2yZvlWs.js} +1 -1
- package/dashboard/dist/assets/{linear-MbCpC_Cg.js → linear-p68yY_14.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-CYbhqlNy.js → mermaid.core-D558akcW.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-CwYCISFH.js → mindmap-definition-YRQLILUH-CZPgesSK.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-5qfZ73SG.js → pieDiagram-SKSYHLDU-CdXMWspp.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-WI8y1sQ_.js → quadrantDiagram-337W2JSQ-D7tq22ZY.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BFlD0ZTS.js → requirementDiagram-Z7DCOOCP-ByZxUSmd.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-Bdckv1Se.js → sankeyDiagram-WA2Y5GQK-CZon9rRY.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-DgzxKAlZ.js → sequenceDiagram-2WXFIKYE-nbELB6rb.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DO4OXahC.js → stateDiagram-RAJIS63D-D_OPKr5B.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-4pLM5B3m.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BBB01JWw.js → timeline-definition-YZTLITO2-Cxuvk1D2.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-Dr0jb8op.js → treemap-KZPCXAKY-C0CRpL92.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-D40KFl2o.js → vennDiagram-LZ73GAT5-DijCj6M3.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DBUmWQfT.js → xychartDiagram-JWTSCODW-CdpE0oRi.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/dashboard/server.js +419 -38
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +577 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/channel-Df6VrFK5.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-CyfzumTY.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CyfzumTY.js +0 -1
- package/dashboard/dist/assets/clone-CMs4Aqrx.js +0 -1
- package/dashboard/dist/assets/index-B4QMu-Oq.css +0 -1
- package/dashboard/dist/assets/index-BBWZjPBC.js +0 -495
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-o8bgX-J3.js +0 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -8172,37 +8172,62 @@ init_fs_migration();
|
|
|
8172
8172
|
// src/dashboard/api-todos.ts
|
|
8173
8173
|
init_parser2();
|
|
8174
8174
|
init_fs();
|
|
8175
|
+
init_paths();
|
|
8175
8176
|
import { Router as Router5 } from "express";
|
|
8176
8177
|
import { readdir as readdir8 } from "fs/promises";
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
}
|
|
8182
|
-
return value ?? "";
|
|
8183
|
-
}
|
|
8178
|
+
import { resolve as resolvePath, dirname as dirname3 } from "path";
|
|
8179
|
+
import { rename as rename3, mkdir as mkdir2 } from "fs/promises";
|
|
8180
|
+
|
|
8181
|
+
// src/dashboard/todos-locks.ts
|
|
8184
8182
|
var writeLocks = /* @__PURE__ */ new Map();
|
|
8185
8183
|
function withLock(lockKey, fn) {
|
|
8186
8184
|
const prev = writeLocks.get(lockKey) ?? Promise.resolve();
|
|
8187
8185
|
const next = prev.then(fn);
|
|
8188
|
-
writeLocks.set(
|
|
8189
|
-
|
|
8190
|
-
|
|
8186
|
+
writeLocks.set(
|
|
8187
|
+
lockKey,
|
|
8188
|
+
next.then(
|
|
8189
|
+
() => {
|
|
8190
|
+
},
|
|
8191
|
+
() => {
|
|
8192
|
+
}
|
|
8193
|
+
)
|
|
8194
|
+
);
|
|
8191
8195
|
return next;
|
|
8192
8196
|
}
|
|
8193
8197
|
function wsLock(workspace, fn) {
|
|
8194
8198
|
return withLock(`ws:${workspace}`, fn);
|
|
8195
8199
|
}
|
|
8200
|
+
function projLock(slug, fn) {
|
|
8201
|
+
return withLock(`proj:${slug}`, fn);
|
|
8202
|
+
}
|
|
8203
|
+
function withTwoLocks(keyA, keyB, fn) {
|
|
8204
|
+
if (keyA === keyB) return withLock(keyA, fn);
|
|
8205
|
+
const [first, second] = keyA < keyB ? [keyA, keyB] : [keyB, keyA];
|
|
8206
|
+
return withLock(first, () => withLock(second, fn));
|
|
8207
|
+
}
|
|
8208
|
+
|
|
8209
|
+
// src/dashboard/api-todos.ts
|
|
8210
|
+
init_slug();
|
|
8211
|
+
var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
|
|
8212
|
+
function getWorkspaceParam(value) {
|
|
8213
|
+
if (Array.isArray(value)) {
|
|
8214
|
+
return value[0] ?? "";
|
|
8215
|
+
}
|
|
8216
|
+
return value ?? "";
|
|
8217
|
+
}
|
|
8196
8218
|
function touchItem(item) {
|
|
8197
8219
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8198
8220
|
if (item.createdAt === null) item.createdAt = now;
|
|
8199
8221
|
item.updatedAt = now;
|
|
8200
8222
|
}
|
|
8201
|
-
function createTodosRouter(todosDir2, broadcast) {
|
|
8223
|
+
function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
8202
8224
|
const router = Router5();
|
|
8203
8225
|
function broadcastUpdate() {
|
|
8204
8226
|
broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8205
8227
|
}
|
|
8228
|
+
function broadcastProject(slug) {
|
|
8229
|
+
broadcast({ type: "todos-updated", projectSlug: slug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8230
|
+
}
|
|
8206
8231
|
function validateWorkspace(req, res, next) {
|
|
8207
8232
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
8208
8233
|
if (workspace && !WORKSPACE_REGEX.test(workspace)) {
|
|
@@ -8601,7 +8626,7 @@ workspace: ${workspace}
|
|
|
8601
8626
|
items.push(item);
|
|
8602
8627
|
}
|
|
8603
8628
|
const scopeLabel = workspace === "_global" ? "_global" : `workspace:${workspace}`;
|
|
8604
|
-
const { resolve:
|
|
8629
|
+
const { resolve: resolvePath2 } = await import("path");
|
|
8605
8630
|
const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|
|
8606
8631
|
const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
8607
8632
|
const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
@@ -8631,18 +8656,18 @@ workspace: ${workspace}
|
|
|
8631
8656
|
const parts = tg.split("/");
|
|
8632
8657
|
if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
|
|
8633
8658
|
const config = await readConfig2();
|
|
8634
|
-
assignmentDir =
|
|
8659
|
+
assignmentDir = resolvePath2(config.defaultProjectDir, parts[0], "assignments", parts[1]);
|
|
8635
8660
|
assignmentRef = `${parts[0]}/${parts[1]}`;
|
|
8636
8661
|
} else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
|
|
8637
|
-
assignmentDir =
|
|
8662
|
+
assignmentDir = resolvePath2(assignmentsDirFn(), tg);
|
|
8638
8663
|
assignmentRef = tg;
|
|
8639
8664
|
} else {
|
|
8640
8665
|
return { error: `Invalid target.assignment "${tg}"` };
|
|
8641
8666
|
}
|
|
8642
|
-
const assignmentMdPath2 =
|
|
8667
|
+
const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
|
|
8643
8668
|
if (!await fileExists2(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
|
|
8644
8669
|
}
|
|
8645
|
-
const assignmentMdPath =
|
|
8670
|
+
const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
|
|
8646
8671
|
let content = await readFile15(assignmentMdPath, "utf-8");
|
|
8647
8672
|
content = appendTodosToAssignmentBody2(
|
|
8648
8673
|
content,
|
|
@@ -8686,6 +8711,115 @@ workspace: ${workspace}
|
|
|
8686
8711
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
|
|
8687
8712
|
}
|
|
8688
8713
|
});
|
|
8714
|
+
router.post("/:workspace/:id/move", async (req, res) => {
|
|
8715
|
+
try {
|
|
8716
|
+
const sourceWs = getWorkspaceParam(req.params.workspace);
|
|
8717
|
+
const id = req.params.id;
|
|
8718
|
+
const to = req.body?.to;
|
|
8719
|
+
if (!to || typeof to !== "object") {
|
|
8720
|
+
res.status(400).json({ error: "body.to is required" });
|
|
8721
|
+
return;
|
|
8722
|
+
}
|
|
8723
|
+
const targetCount = [Boolean(to.workspace), Boolean(to.project), Boolean(to.global)].filter(Boolean).length;
|
|
8724
|
+
if (targetCount !== 1) {
|
|
8725
|
+
res.status(400).json({ error: "body.to must specify exactly one of workspace, project, or global" });
|
|
8726
|
+
return;
|
|
8727
|
+
}
|
|
8728
|
+
if (to.project && !isValidSlug(to.project)) {
|
|
8729
|
+
res.status(400).json({ error: `Invalid target project slug: "${to.project}"` });
|
|
8730
|
+
return;
|
|
8731
|
+
}
|
|
8732
|
+
if (to.workspace && !WORKSPACE_REGEX.test(to.workspace)) {
|
|
8733
|
+
res.status(400).json({ error: `Invalid target workspace name: "${to.workspace}"` });
|
|
8734
|
+
return;
|
|
8735
|
+
}
|
|
8736
|
+
let target;
|
|
8737
|
+
if (to.global) {
|
|
8738
|
+
target = { kind: "workspace", id: "_global", todosPath: todosDir2, lockKey: "ws:_global" };
|
|
8739
|
+
} else if (to.workspace) {
|
|
8740
|
+
target = { kind: "workspace", id: to.workspace, todosPath: todosDir2, lockKey: `ws:${to.workspace}` };
|
|
8741
|
+
} else {
|
|
8742
|
+
if (!projectsDir) {
|
|
8743
|
+
res.status(500).json({ error: "Server not configured with projectsDir; cannot move to project scope" });
|
|
8744
|
+
return;
|
|
8745
|
+
}
|
|
8746
|
+
const slug = to.project;
|
|
8747
|
+
const projectMd = resolvePath(projectsDir, slug, "project.md");
|
|
8748
|
+
if (!await fileExists(projectMd)) {
|
|
8749
|
+
res.status(404).json({ error: `Target project "${slug}" not found` });
|
|
8750
|
+
return;
|
|
8751
|
+
}
|
|
8752
|
+
target = {
|
|
8753
|
+
kind: "project",
|
|
8754
|
+
id: slug,
|
|
8755
|
+
todosPath: projectTodosDir(projectsDir, slug),
|
|
8756
|
+
lockKey: `proj:${slug}`
|
|
8757
|
+
};
|
|
8758
|
+
}
|
|
8759
|
+
const sourceLockKey = `ws:${sourceWs}`;
|
|
8760
|
+
if (sourceLockKey === target.lockKey) {
|
|
8761
|
+
res.status(400).json({ error: "cannot move to the same scope" });
|
|
8762
|
+
return;
|
|
8763
|
+
}
|
|
8764
|
+
const result = await withTwoLocks(sourceLockKey, target.lockKey, async () => {
|
|
8765
|
+
const sourceChecklist = await readChecklist(todosDir2, sourceWs);
|
|
8766
|
+
const targetChecklist = await readChecklist(target.todosPath, target.id);
|
|
8767
|
+
const idx = sourceChecklist.items.findIndex((i) => i.id === id);
|
|
8768
|
+
if (idx === -1) return { status: 404, error: `Todo "${id}" not found` };
|
|
8769
|
+
if (targetChecklist.items.some((i) => i.id === id)) {
|
|
8770
|
+
return { status: 409, error: "id already exists in target" };
|
|
8771
|
+
}
|
|
8772
|
+
const item = sourceChecklist.items[idx];
|
|
8773
|
+
if (item.planDir) {
|
|
8774
|
+
const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
|
|
8775
|
+
if (await fileExists(newPlanDir)) {
|
|
8776
|
+
return { status: 409, error: "plan dir already exists in target" };
|
|
8777
|
+
}
|
|
8778
|
+
await mkdir2(dirname3(newPlanDir), { recursive: true });
|
|
8779
|
+
await rename3(item.planDir, newPlanDir);
|
|
8780
|
+
item.planDir = newPlanDir;
|
|
8781
|
+
}
|
|
8782
|
+
sourceChecklist.items.splice(idx, 1);
|
|
8783
|
+
targetChecklist.items.push(item);
|
|
8784
|
+
await writeChecklist(todosDir2, sourceChecklist);
|
|
8785
|
+
await writeChecklist(target.todosPath, targetChecklist);
|
|
8786
|
+
const sourceLabel = sourceWs === "_global" ? "_global" : `workspace:${sourceWs}`;
|
|
8787
|
+
const targetLabel = target.kind === "project" ? `project:${target.id}` : target.id === "_global" ? "_global" : `workspace:${target.id}`;
|
|
8788
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
8789
|
+
await appendLogEntry2(todosDir2, sourceWs, {
|
|
8790
|
+
timestamp: ts,
|
|
8791
|
+
itemIds: [id],
|
|
8792
|
+
items: item.description,
|
|
8793
|
+
session: null,
|
|
8794
|
+
branch: item.branch || null,
|
|
8795
|
+
summary: `Moved to ${targetLabel}`,
|
|
8796
|
+
blockers: null,
|
|
8797
|
+
status: null
|
|
8798
|
+
});
|
|
8799
|
+
await appendLogEntry2(target.todosPath, target.id, {
|
|
8800
|
+
timestamp: ts,
|
|
8801
|
+
itemIds: [id],
|
|
8802
|
+
items: item.description,
|
|
8803
|
+
session: null,
|
|
8804
|
+
branch: item.branch || null,
|
|
8805
|
+
summary: `Moved from ${sourceLabel}`,
|
|
8806
|
+
blockers: null,
|
|
8807
|
+
status: null
|
|
8808
|
+
});
|
|
8809
|
+
return { status: 200, item };
|
|
8810
|
+
});
|
|
8811
|
+
if (result.status !== 200) {
|
|
8812
|
+
res.status(result.status).json({ error: result.error });
|
|
8813
|
+
return;
|
|
8814
|
+
}
|
|
8815
|
+
broadcastUpdate();
|
|
8816
|
+
if (target.kind === "project") broadcastProject(target.id);
|
|
8817
|
+
else broadcastUpdate();
|
|
8818
|
+
res.json({ moved: id, to: target });
|
|
8819
|
+
} catch (error) {
|
|
8820
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to move todo" });
|
|
8821
|
+
}
|
|
8822
|
+
});
|
|
8689
8823
|
router.post("/:workspace/:id/unblock", async (req, res) => {
|
|
8690
8824
|
try {
|
|
8691
8825
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
@@ -8718,18 +8852,9 @@ init_fs();
|
|
|
8718
8852
|
init_paths();
|
|
8719
8853
|
init_slug();
|
|
8720
8854
|
import { Router as Router6 } from "express";
|
|
8721
|
-
import { mkdir as
|
|
8722
|
-
import { resolve as resolve17 } from "path";
|
|
8723
|
-
var
|
|
8724
|
-
function projLock(slug, fn) {
|
|
8725
|
-
const key = `proj:${slug}`;
|
|
8726
|
-
const prev = writeLocks2.get(key) ?? Promise.resolve();
|
|
8727
|
-
const next = prev.then(fn);
|
|
8728
|
-
writeLocks2.set(key, next.then(() => {
|
|
8729
|
-
}, () => {
|
|
8730
|
-
}));
|
|
8731
|
-
return next;
|
|
8732
|
-
}
|
|
8855
|
+
import { mkdir as mkdir3, readFile as readFile13, rename as rename4 } from "fs/promises";
|
|
8856
|
+
import { resolve as resolve17, dirname as dirname4 } from "path";
|
|
8857
|
+
var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
|
|
8733
8858
|
function touchItem2(item) {
|
|
8734
8859
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8735
8860
|
if (item.createdAt === null) item.createdAt = now;
|
|
@@ -8748,7 +8873,7 @@ async function projectExists(projectsDir, slug) {
|
|
|
8748
8873
|
async function ensureProjectTodosDir(projectsDir, slug) {
|
|
8749
8874
|
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
8750
8875
|
try {
|
|
8751
|
-
await
|
|
8876
|
+
await mkdir3(todosDir2, { recursive: false });
|
|
8752
8877
|
} catch (err) {
|
|
8753
8878
|
const code = err.code;
|
|
8754
8879
|
if (code === "EEXIST") return;
|
|
@@ -8760,7 +8885,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
|
|
|
8760
8885
|
throw err;
|
|
8761
8886
|
}
|
|
8762
8887
|
try {
|
|
8763
|
-
await
|
|
8888
|
+
await mkdir3(resolve17(todosDir2, "archive"), { recursive: false });
|
|
8764
8889
|
} catch (err) {
|
|
8765
8890
|
const code = err.code;
|
|
8766
8891
|
if (code === "EEXIST") return;
|
|
@@ -8775,11 +8900,14 @@ async function ensureProjectTodosDir(projectsDir, slug) {
|
|
|
8775
8900
|
function notFound(res, slug) {
|
|
8776
8901
|
res.status(404).json({ error: `Project "${slug}" not found` });
|
|
8777
8902
|
}
|
|
8778
|
-
function createProjectTodosRouter(projectsDir, broadcast) {
|
|
8903
|
+
function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
|
|
8779
8904
|
const router = Router6({ mergeParams: true });
|
|
8780
8905
|
function broadcastUpdate(projectSlug) {
|
|
8781
8906
|
broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8782
8907
|
}
|
|
8908
|
+
function broadcastWorkspace() {
|
|
8909
|
+
broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8910
|
+
}
|
|
8783
8911
|
function validateProjectId(req, res, next) {
|
|
8784
8912
|
const slug = getProjectIdParam(params(req).projectId);
|
|
8785
8913
|
if (!slug || !isValidSlug(slug)) {
|
|
@@ -9327,6 +9455,259 @@ workspace: ${slug}
|
|
|
9327
9455
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to unblock todo" });
|
|
9328
9456
|
}
|
|
9329
9457
|
});
|
|
9458
|
+
router.post("/promote", async (req, res) => {
|
|
9459
|
+
try {
|
|
9460
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
9461
|
+
if (!await projectExists(projectsDir, slug)) {
|
|
9462
|
+
notFound(res, slug);
|
|
9463
|
+
return;
|
|
9464
|
+
}
|
|
9465
|
+
const { todoIds, mode, target, title, type, priority, keepSource } = req.body ?? {};
|
|
9466
|
+
if (!Array.isArray(todoIds) || todoIds.length === 0) {
|
|
9467
|
+
res.status(400).json({ error: "todoIds (non-empty array of strings) is required" });
|
|
9468
|
+
return;
|
|
9469
|
+
}
|
|
9470
|
+
if (mode !== "new-assignment" && mode !== "to-assignment") {
|
|
9471
|
+
res.status(400).json({ error: 'mode must be "new-assignment" or "to-assignment"' });
|
|
9472
|
+
return;
|
|
9473
|
+
}
|
|
9474
|
+
const result = await projLock(slug, async () => {
|
|
9475
|
+
if (!await projectExists(projectsDir, slug)) return { gone: true };
|
|
9476
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
9477
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
9478
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
9479
|
+
const items = [];
|
|
9480
|
+
for (const id of todoIds) {
|
|
9481
|
+
const item = checklist.items.find((i) => i.id === id);
|
|
9482
|
+
if (!item) return { error: `Todo "${id}" not found` };
|
|
9483
|
+
if (item.status === "completed") return { error: `Todo "${id}" is already completed` };
|
|
9484
|
+
items.push(item);
|
|
9485
|
+
}
|
|
9486
|
+
const scopeLabel = `project:${slug}`;
|
|
9487
|
+
const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
9488
|
+
const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
|
|
9489
|
+
const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
|
|
9490
|
+
let assignmentRef;
|
|
9491
|
+
let assignmentDir;
|
|
9492
|
+
if (mode === "new-assignment") {
|
|
9493
|
+
const targetProject = target?.project ?? slug;
|
|
9494
|
+
if (!targetProject) return { error: "target.project is required for new-assignment mode" };
|
|
9495
|
+
if (items.length > 1 && !title) return { error: "title is required when promoting multiple todos" };
|
|
9496
|
+
const { createAssignmentCommand: createAssignmentCommand2 } = await Promise.resolve().then(() => (init_create_assignment(), create_assignment_exports));
|
|
9497
|
+
const created = await createAssignmentCommand2(title || items[0].description, {
|
|
9498
|
+
project: targetProject,
|
|
9499
|
+
type,
|
|
9500
|
+
priority,
|
|
9501
|
+
withTodos: true,
|
|
9502
|
+
silent: true
|
|
9503
|
+
});
|
|
9504
|
+
assignmentDir = created.assignmentDir;
|
|
9505
|
+
assignmentRef = `${created.projectSlug}/${created.slug}`;
|
|
9506
|
+
} else {
|
|
9507
|
+
const tg = target?.assignment || "";
|
|
9508
|
+
if (!tg) return { error: "target.assignment is required for to-assignment mode" };
|
|
9509
|
+
if (tg.includes("/")) {
|
|
9510
|
+
const parts = tg.split("/");
|
|
9511
|
+
if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
|
|
9512
|
+
assignmentDir = resolve17(projectsDir, parts[0], "assignments", parts[1]);
|
|
9513
|
+
assignmentRef = `${parts[0]}/${parts[1]}`;
|
|
9514
|
+
} else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
|
|
9515
|
+
assignmentDir = resolve17(assignmentsDirFn(), tg);
|
|
9516
|
+
assignmentRef = tg;
|
|
9517
|
+
} else {
|
|
9518
|
+
return { error: `Invalid target.assignment "${tg}"` };
|
|
9519
|
+
}
|
|
9520
|
+
const assignmentMdPath2 = resolve17(assignmentDir, "assignment.md");
|
|
9521
|
+
if (!await fileExists(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
|
|
9522
|
+
}
|
|
9523
|
+
const assignmentMdPath = resolve17(assignmentDir, "assignment.md");
|
|
9524
|
+
let content = await readFile13(assignmentMdPath, "utf-8");
|
|
9525
|
+
content = appendTodosToAssignmentBody2(
|
|
9526
|
+
content,
|
|
9527
|
+
items.map((it) => ({
|
|
9528
|
+
description: it.description,
|
|
9529
|
+
trace: `promoted from t:${it.id} in ${scopeLabel}`
|
|
9530
|
+
}))
|
|
9531
|
+
);
|
|
9532
|
+
content = touchAssignmentUpdated2(content, nowTimestamp3());
|
|
9533
|
+
await writeFileForce(assignmentMdPath, content);
|
|
9534
|
+
if (!keepSource) {
|
|
9535
|
+
for (const item of items) {
|
|
9536
|
+
item.status = "completed";
|
|
9537
|
+
item.session = null;
|
|
9538
|
+
touchItem2(item);
|
|
9539
|
+
}
|
|
9540
|
+
checklist.workspace = slug;
|
|
9541
|
+
await writeChecklist(todosDir2, checklist);
|
|
9542
|
+
for (const item of items) {
|
|
9543
|
+
const entry = {
|
|
9544
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9545
|
+
itemIds: [item.id],
|
|
9546
|
+
items: item.description,
|
|
9547
|
+
session: null,
|
|
9548
|
+
branch: item.branch || null,
|
|
9549
|
+
summary: `Promoted to assignment ${assignmentRef}`,
|
|
9550
|
+
blockers: null,
|
|
9551
|
+
status: null
|
|
9552
|
+
};
|
|
9553
|
+
await appendLogEntry2(todosDir2, slug, entry);
|
|
9554
|
+
}
|
|
9555
|
+
}
|
|
9556
|
+
return { assignmentRef, assignmentDir, promoted: items.map((i) => i.id) };
|
|
9557
|
+
});
|
|
9558
|
+
if ("gone" in result) {
|
|
9559
|
+
notFound(res, slug);
|
|
9560
|
+
return;
|
|
9561
|
+
}
|
|
9562
|
+
if ("error" in result) {
|
|
9563
|
+
res.status(400).json({ error: result.error });
|
|
9564
|
+
return;
|
|
9565
|
+
}
|
|
9566
|
+
broadcastUpdate(slug);
|
|
9567
|
+
res.json(result);
|
|
9568
|
+
} catch (error) {
|
|
9569
|
+
if (error.code === "PROJECT_GONE") {
|
|
9570
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9571
|
+
return;
|
|
9572
|
+
}
|
|
9573
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
|
|
9574
|
+
}
|
|
9575
|
+
});
|
|
9576
|
+
router.post("/:id/move", async (req, res) => {
|
|
9577
|
+
try {
|
|
9578
|
+
const sourceSlug = getProjectIdParam(params(req).projectId);
|
|
9579
|
+
const id = params(req).id ?? "";
|
|
9580
|
+
if (!await projectExists(projectsDir, sourceSlug)) {
|
|
9581
|
+
notFound(res, sourceSlug);
|
|
9582
|
+
return;
|
|
9583
|
+
}
|
|
9584
|
+
const to = req.body?.to;
|
|
9585
|
+
if (!to || typeof to !== "object") {
|
|
9586
|
+
res.status(400).json({ error: "body.to is required" });
|
|
9587
|
+
return;
|
|
9588
|
+
}
|
|
9589
|
+
const targetCount = [Boolean(to.workspace), Boolean(to.project), Boolean(to.global)].filter(Boolean).length;
|
|
9590
|
+
if (targetCount !== 1) {
|
|
9591
|
+
res.status(400).json({ error: "body.to must specify exactly one of workspace, project, or global" });
|
|
9592
|
+
return;
|
|
9593
|
+
}
|
|
9594
|
+
if (to.project && !isValidSlug(to.project)) {
|
|
9595
|
+
res.status(400).json({ error: `Invalid target project slug: "${to.project}"` });
|
|
9596
|
+
return;
|
|
9597
|
+
}
|
|
9598
|
+
if (to.workspace && !WORKSPACE_REGEX2.test(to.workspace)) {
|
|
9599
|
+
res.status(400).json({ error: `Invalid target workspace name: "${to.workspace}"` });
|
|
9600
|
+
return;
|
|
9601
|
+
}
|
|
9602
|
+
let target;
|
|
9603
|
+
if (to.global) {
|
|
9604
|
+
if (!workspaceTodosDir) {
|
|
9605
|
+
res.status(500).json({ error: "Server not configured with workspaceTodosDir; cannot move to global scope" });
|
|
9606
|
+
return;
|
|
9607
|
+
}
|
|
9608
|
+
target = { kind: "workspace", id: "_global", todosPath: workspaceTodosDir, lockKey: "ws:_global" };
|
|
9609
|
+
} else if (to.workspace) {
|
|
9610
|
+
if (!workspaceTodosDir) {
|
|
9611
|
+
res.status(500).json({ error: "Server not configured with workspaceTodosDir; cannot move to workspace scope" });
|
|
9612
|
+
return;
|
|
9613
|
+
}
|
|
9614
|
+
target = { kind: "workspace", id: to.workspace, todosPath: workspaceTodosDir, lockKey: `ws:${to.workspace}` };
|
|
9615
|
+
} else {
|
|
9616
|
+
const tslug = to.project;
|
|
9617
|
+
if (!await projectExists(projectsDir, tslug)) {
|
|
9618
|
+
res.status(404).json({ error: `Target project "${tslug}" not found` });
|
|
9619
|
+
return;
|
|
9620
|
+
}
|
|
9621
|
+
target = {
|
|
9622
|
+
kind: "project",
|
|
9623
|
+
id: tslug,
|
|
9624
|
+
todosPath: projectTodosDir(projectsDir, tslug),
|
|
9625
|
+
lockKey: `proj:${tslug}`
|
|
9626
|
+
};
|
|
9627
|
+
}
|
|
9628
|
+
const sourceLockKey = `proj:${sourceSlug}`;
|
|
9629
|
+
if (sourceLockKey === target.lockKey) {
|
|
9630
|
+
res.status(400).json({ error: "cannot move to the same scope" });
|
|
9631
|
+
return;
|
|
9632
|
+
}
|
|
9633
|
+
const result = await withTwoLocks(sourceLockKey, target.lockKey, async () => {
|
|
9634
|
+
if (!await projectExists(projectsDir, sourceSlug)) return { status: "gone" };
|
|
9635
|
+
if (target.kind === "project" && !await projectExists(projectsDir, target.id)) {
|
|
9636
|
+
return { status: "targetGone" };
|
|
9637
|
+
}
|
|
9638
|
+
await ensureProjectTodosDir(projectsDir, sourceSlug);
|
|
9639
|
+
const sourceTodosDir = projectTodosDir(projectsDir, sourceSlug);
|
|
9640
|
+
const sourceChecklist = await readChecklist(sourceTodosDir, sourceSlug);
|
|
9641
|
+
const targetChecklist = await readChecklist(target.todosPath, target.id);
|
|
9642
|
+
const idx = sourceChecklist.items.findIndex((i) => i.id === id);
|
|
9643
|
+
if (idx === -1) return { status: 404, error: `Todo "${id}" not found` };
|
|
9644
|
+
if (targetChecklist.items.some((i) => i.id === id)) {
|
|
9645
|
+
return { status: 409, error: "id already exists in target" };
|
|
9646
|
+
}
|
|
9647
|
+
const item = sourceChecklist.items[idx];
|
|
9648
|
+
if (item.planDir) {
|
|
9649
|
+
const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
|
|
9650
|
+
if (await fileExists(newPlanDir)) {
|
|
9651
|
+
return { status: 409, error: "plan dir already exists in target" };
|
|
9652
|
+
}
|
|
9653
|
+
await mkdir3(dirname4(newPlanDir), { recursive: true });
|
|
9654
|
+
await rename4(item.planDir, newPlanDir);
|
|
9655
|
+
item.planDir = newPlanDir;
|
|
9656
|
+
}
|
|
9657
|
+
sourceChecklist.items.splice(idx, 1);
|
|
9658
|
+
targetChecklist.items.push(item);
|
|
9659
|
+
sourceChecklist.workspace = sourceSlug;
|
|
9660
|
+
await writeChecklist(sourceTodosDir, sourceChecklist);
|
|
9661
|
+
await writeChecklist(target.todosPath, targetChecklist);
|
|
9662
|
+
const sourceLabel = `project:${sourceSlug}`;
|
|
9663
|
+
const targetLabel = target.kind === "project" ? `project:${target.id}` : target.id === "_global" ? "_global" : `workspace:${target.id}`;
|
|
9664
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
9665
|
+
await appendLogEntry2(sourceTodosDir, sourceSlug, {
|
|
9666
|
+
timestamp: ts,
|
|
9667
|
+
itemIds: [id],
|
|
9668
|
+
items: item.description,
|
|
9669
|
+
session: null,
|
|
9670
|
+
branch: item.branch || null,
|
|
9671
|
+
summary: `Moved to ${targetLabel}`,
|
|
9672
|
+
blockers: null,
|
|
9673
|
+
status: null
|
|
9674
|
+
});
|
|
9675
|
+
await appendLogEntry2(target.todosPath, target.id, {
|
|
9676
|
+
timestamp: ts,
|
|
9677
|
+
itemIds: [id],
|
|
9678
|
+
items: item.description,
|
|
9679
|
+
session: null,
|
|
9680
|
+
branch: item.branch || null,
|
|
9681
|
+
summary: `Moved from ${sourceLabel}`,
|
|
9682
|
+
blockers: null,
|
|
9683
|
+
status: null
|
|
9684
|
+
});
|
|
9685
|
+
return { status: 200, item };
|
|
9686
|
+
});
|
|
9687
|
+
if (result.status === "gone") {
|
|
9688
|
+
notFound(res, sourceSlug);
|
|
9689
|
+
return;
|
|
9690
|
+
}
|
|
9691
|
+
if (result.status === "targetGone") {
|
|
9692
|
+
res.status(404).json({ error: `Target project "${target.id}" not found` });
|
|
9693
|
+
return;
|
|
9694
|
+
}
|
|
9695
|
+
if (result.status !== 200) {
|
|
9696
|
+
res.status(result.status).json({ error: result.error });
|
|
9697
|
+
return;
|
|
9698
|
+
}
|
|
9699
|
+
broadcastUpdate(sourceSlug);
|
|
9700
|
+
if (target.kind === "project") broadcastUpdate(target.id);
|
|
9701
|
+
else broadcastWorkspace();
|
|
9702
|
+
res.json({ moved: id, to: target });
|
|
9703
|
+
} catch (error) {
|
|
9704
|
+
if (error.code === "PROJECT_GONE") {
|
|
9705
|
+
notFound(res, getProjectIdParam(params(req).projectId));
|
|
9706
|
+
return;
|
|
9707
|
+
}
|
|
9708
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to move todo" });
|
|
9709
|
+
}
|
|
9710
|
+
});
|
|
9330
9711
|
return router;
|
|
9331
9712
|
}
|
|
9332
9713
|
|
|
@@ -9340,7 +9721,7 @@ init_fs();
|
|
|
9340
9721
|
init_config2();
|
|
9341
9722
|
import { execFile as execFile2 } from "child_process";
|
|
9342
9723
|
import { promisify as promisify2 } from "util";
|
|
9343
|
-
import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as
|
|
9724
|
+
import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename5 } from "fs/promises";
|
|
9344
9725
|
import { resolve as resolve18, join as join2 } from "path";
|
|
9345
9726
|
import { tmpdir } from "os";
|
|
9346
9727
|
var exec2 = promisify2(execFile2);
|
|
@@ -9552,7 +9933,7 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
|
9552
9933
|
const localExistsBefore = await fileExists(localPath);
|
|
9553
9934
|
if (backupExistsBefore) {
|
|
9554
9935
|
if (!localExistsBefore) {
|
|
9555
|
-
await
|
|
9936
|
+
await rename5(backupPath, localPath);
|
|
9556
9937
|
} else {
|
|
9557
9938
|
throw new Error(
|
|
9558
9939
|
`Cannot restore "${localPath}": a stale crash-recovery backup exists at ${backupPath} while the current path also exists. Inspect both and remove the one you don't need, then retry.`
|
|
@@ -9564,15 +9945,15 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
|
9564
9945
|
await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
|
|
9565
9946
|
const localExists = await fileExists(localPath);
|
|
9566
9947
|
if (localExists) {
|
|
9567
|
-
await
|
|
9948
|
+
await rename5(localPath, backupPath);
|
|
9568
9949
|
localMovedAside = true;
|
|
9569
9950
|
}
|
|
9570
|
-
await
|
|
9951
|
+
await rename5(stagingPath, localPath);
|
|
9571
9952
|
await rm2(backupPath, { recursive: true, force: true }).catch(() => {
|
|
9572
9953
|
});
|
|
9573
9954
|
} catch (err) {
|
|
9574
9955
|
if (localMovedAside && await fileExists(backupPath)) {
|
|
9575
|
-
await
|
|
9956
|
+
await rename5(backupPath, localPath).catch(() => {
|
|
9576
9957
|
});
|
|
9577
9958
|
}
|
|
9578
9959
|
await rm2(stagingPath, { recursive: true, force: true }).catch(() => {
|
|
@@ -10311,8 +10692,8 @@ function createDashboardServer(options) {
|
|
|
10311
10692
|
app.use("/api/servers", createServersRouter(serversDir2, projectsDir, assignmentsDir2));
|
|
10312
10693
|
app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2));
|
|
10313
10694
|
app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
|
|
10314
|
-
app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
|
|
10315
|
-
app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast));
|
|
10695
|
+
app.use("/api/todos", createTodosRouter(todosDir2, broadcast, projectsDir));
|
|
10696
|
+
app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast, todosDir2));
|
|
10316
10697
|
app.use("/api/backup", createBackupRouter());
|
|
10317
10698
|
if (serveStaticUi && dashboardDistPath) {
|
|
10318
10699
|
const sendOpts = { dotfiles: "allow" };
|