@unbrained/pm-web 2026.7.11 → 2026.7.13
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/CHANGELOG.md +13 -0
- package/README.md +26 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +38 -2
- package/dist/app.js.map +1 -1
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +10 -0
- package/dist/auth.js.map +1 -1
- package/dist/db.js +17 -1
- package/dist/db.js.map +1 -1
- package/dist/ical.js.map +1 -1
- package/dist/index.d.ts +1 -22
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/oidc.d.ts +89 -0
- package/dist/oidc.js +305 -0
- package/dist/oidc.js.map +1 -0
- package/dist/routes/auth.js +3 -15
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/github.js +3 -3
- package/dist/routes/github.js.map +1 -1
- package/dist/routes/oidc.d.ts +4 -0
- package/dist/routes/oidc.js +147 -0
- package/dist/routes/oidc.js.map +1 -0
- package/dist/routes/pm.js +121 -107
- package/dist/routes/pm.js.map +1 -1
- package/dist/routes/projects.js +9 -2
- package/dist/routes/projects.js.map +1 -1
- package/dist/server.js +7 -2
- package/dist/server.js.map +1 -1
- package/dist/services/pm-runner.d.ts +11 -3
- package/dist/services/pm-runner.js +167 -54
- package/dist/services/pm-runner.js.map +1 -1
- package/dist/services/realtime-bus.d.ts +1 -0
- package/dist/services/realtime-bus.js +148 -0
- package/dist/services/realtime-bus.js.map +1 -0
- package/dist/services/sse.d.ts +3 -1
- package/dist/services/sse.js +83 -48
- package/dist/services/sse.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +11 -11
- package/public/cookie-consent.js +54 -39
- package/public/cookie-settings.html +7 -7
- package/public/index.html +2 -0
- package/public/legal-notice.html +10 -12
- package/public/privacy-policy.html +14 -24
- package/public/src/app.js +4 -4
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +4 -4
- package/public/src/components/toast.js.map +1 -1
- package/public/src/cookie-consent.ts +68 -0
- package/public/src/sw.ts +443 -0
- package/public/src/views/auth.js +22 -0
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +20 -0
- package/public/src/views/graph.js.map +1 -1
- package/public/styles.css +2 -0
- package/public/sw.js +320 -256
- package/public/terms.html +7 -9
- package/public/tsconfig.json +1 -1
- package/public/tsconfig.scripts.json +18 -0
- package/public/tsconfig.sw.json +18 -0
- package/sql/schema.sql +16 -0
package/dist/routes/pm.js
CHANGED
|
@@ -4,10 +4,11 @@ import { ensureGraphExtension, runPm } from "../services/pm-runner.js";
|
|
|
4
4
|
import { boardColumns, filterItemsByQuery } from "../board.js";
|
|
5
5
|
import { buildIcsCalendar } from "../ical.js";
|
|
6
6
|
import { verifyProjectAccess } from "./projects.js";
|
|
7
|
-
import { addSSEClient, broadcastProjectEvent, setupSSEHeaders,
|
|
7
|
+
import { addSSEClient, broadcastProjectEvent, setupSSEHeaders, updateClientView, getProjectPresence } from "../services/sse.js";
|
|
8
8
|
import { v4 as uuidv4 } from "uuid";
|
|
9
9
|
import neo4j from "neo4j-driver";
|
|
10
10
|
import { routeParam } from "./route-params.js";
|
|
11
|
+
import { pool } from "../db.js";
|
|
11
12
|
// Singleton Neo4j driver — reused across sync calls to avoid per-call connection overhead.
|
|
12
13
|
let _neo4jDriver = null;
|
|
13
14
|
let _neo4jDriverKey = "";
|
|
@@ -29,7 +30,7 @@ const router = Router({ mergeParams: true });
|
|
|
29
30
|
router.use(requireAuth);
|
|
30
31
|
const pendingGraphSyncs = new Map();
|
|
31
32
|
router.use(async (req, res, next) => {
|
|
32
|
-
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
|
|
33
|
+
if (["GET", "HEAD", "OPTIONS"].includes(req.method) || (req.method === "PATCH" && req.path.startsWith("/presence/"))) {
|
|
33
34
|
next();
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
@@ -232,8 +233,8 @@ async function syncGraphToNeo4j(graph, projectKey) {
|
|
|
232
233
|
return { syncedNodes: graph.nodes.length, syncedRelationships: graph.relationships.length };
|
|
233
234
|
}
|
|
234
235
|
async function syncProjectGraph(project) {
|
|
235
|
-
const extensionGraph = pmGraphExtensionGraphForProject(project);
|
|
236
|
-
const graph = extensionGraph.graph ?? fallbackGraphForProject(project.ownerUserId, project.slug);
|
|
236
|
+
const extensionGraph = await pmGraphExtensionGraphForProject(project);
|
|
237
|
+
const graph = extensionGraph.graph ?? await fallbackGraphForProject(project.ownerUserId, project.slug);
|
|
237
238
|
return syncGraphToNeo4j(graph, graphProjectKey(project));
|
|
238
239
|
}
|
|
239
240
|
function scheduleGraphSync(projectId, project, reason) {
|
|
@@ -281,8 +282,8 @@ function broadcastDependencyEvent(projectId, kind, data) {
|
|
|
281
282
|
function itemsFromListAll(parsed) {
|
|
282
283
|
return ((parsed?.items) ?? []);
|
|
283
284
|
}
|
|
284
|
-
function fallbackGraphForProject(ownerUserId, slug) {
|
|
285
|
-
const itemsResult = runPm({
|
|
285
|
+
async function fallbackGraphForProject(ownerUserId, slug) {
|
|
286
|
+
const itemsResult = await runPm({
|
|
286
287
|
args: ["list-all"],
|
|
287
288
|
userId: ownerUserId,
|
|
288
289
|
slug,
|
|
@@ -295,12 +296,12 @@ function fallbackGraphForProject(ownerUserId, slug) {
|
|
|
295
296
|
// Avoid N+1 subprocess calls by using only the embedded data.
|
|
296
297
|
return graphFromItems(items, new Map());
|
|
297
298
|
}
|
|
298
|
-
function pmGraphExtensionGraphForProject(project) {
|
|
299
|
-
const provision = ensureGraphExtension(project.ownerUserId, project.slug);
|
|
299
|
+
async function pmGraphExtensionGraphForProject(project) {
|
|
300
|
+
const provision = await ensureGraphExtension(project.ownerUserId, project.slug);
|
|
300
301
|
if (!provision.ok) {
|
|
301
302
|
return { error: provision.error };
|
|
302
303
|
}
|
|
303
|
-
const extensionResult = runPm({
|
|
304
|
+
const extensionResult = await runPm({
|
|
304
305
|
args: ["pm-graph", "export", "--json"],
|
|
305
306
|
userId: project.ownerUserId,
|
|
306
307
|
slug: project.slug,
|
|
@@ -341,7 +342,7 @@ router.get("/schema", async (req, res) => {
|
|
|
341
342
|
res.status(404).json({ error: "Project not found" });
|
|
342
343
|
return;
|
|
343
344
|
}
|
|
344
|
-
const result = runPm({ args: ["contracts", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
345
|
+
const result = await runPm({ args: ["contracts", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
345
346
|
const contracts = result.ok && result.parsed ? result.parsed : null;
|
|
346
347
|
const rt = contracts?.["runtime_schema"];
|
|
347
348
|
res.json({
|
|
@@ -375,7 +376,7 @@ router.get("/list", async (req, res) => {
|
|
|
375
376
|
args.push("--release", release);
|
|
376
377
|
if (assignee)
|
|
377
378
|
args.push("--assignee", assignee);
|
|
378
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
379
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
379
380
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr, items: [] });
|
|
380
381
|
});
|
|
381
382
|
// GET /api/projects/:projectId/pm/list-all
|
|
@@ -391,7 +392,7 @@ router.get("/list-all", async (req, res) => {
|
|
|
391
392
|
args.push("--type", type);
|
|
392
393
|
if (limit)
|
|
393
394
|
args.push("--limit", limit);
|
|
394
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
395
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
395
396
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr, items: [] });
|
|
396
397
|
});
|
|
397
398
|
// GET /api/projects/:projectId/pm/board
|
|
@@ -403,12 +404,12 @@ router.get("/board", async (req, res) => {
|
|
|
403
404
|
res.status(404).json({ error: "Project not found" });
|
|
404
405
|
return;
|
|
405
406
|
}
|
|
406
|
-
const contracts = runPm({ args: ["contracts", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
407
|
+
const contracts = await runPm({ args: ["contracts", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
407
408
|
const rt = contracts.ok && contracts.parsed
|
|
408
409
|
? contracts.parsed["runtime_schema"]
|
|
409
410
|
: undefined;
|
|
410
411
|
const statuses = Array.isArray(rt?.["statuses"]) ? rt["statuses"] : [];
|
|
411
|
-
const listed = runPm({ args: ["list-all", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
412
|
+
const listed = await runPm({ args: ["list-all", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
412
413
|
if (!listed.ok) {
|
|
413
414
|
res.json({ error: listed.stderr, columns: [] });
|
|
414
415
|
return;
|
|
@@ -425,7 +426,7 @@ router.get("/search", async (req, res) => {
|
|
|
425
426
|
return;
|
|
426
427
|
}
|
|
427
428
|
const query = String(req.query["q"] ?? "");
|
|
428
|
-
const listed = runPm({ args: ["list-all", "--json", "--include-body"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
429
|
+
const listed = await runPm({ args: ["list-all", "--json", "--include-body"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
429
430
|
if (!listed.ok) {
|
|
430
431
|
res.json({ error: listed.stderr, items: [] });
|
|
431
432
|
return;
|
|
@@ -508,7 +509,7 @@ router.post("/create", async (req, res) => {
|
|
|
508
509
|
args.push("--outcome", outcome);
|
|
509
510
|
if (definitionOfReady)
|
|
510
511
|
args.push("--definition-of-ready", definitionOfReady);
|
|
511
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
512
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
512
513
|
if (!result.ok) {
|
|
513
514
|
res.status(400).json({ error: result.stderr || "Failed to create item" });
|
|
514
515
|
return;
|
|
@@ -528,7 +529,7 @@ router.get("/get/:itemId", async (req, res) => {
|
|
|
528
529
|
res.status(404).json({ error: "Project not found" });
|
|
529
530
|
return;
|
|
530
531
|
}
|
|
531
|
-
const result = runPm({
|
|
532
|
+
const result = await runPm({
|
|
532
533
|
args: ["get", routeParam(req, "itemId")],
|
|
533
534
|
userId: project.ownerUserId,
|
|
534
535
|
slug: project.slug,
|
|
@@ -575,7 +576,7 @@ router.patch("/update/:itemId", async (req, res) => {
|
|
|
575
576
|
// Type can be set but must use --type
|
|
576
577
|
if (body.type)
|
|
577
578
|
args.push("--type", body.type);
|
|
578
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
579
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
579
580
|
if (!result.ok) {
|
|
580
581
|
res.status(400).json({ error: result.stderr || "Failed to update item" });
|
|
581
582
|
return;
|
|
@@ -600,7 +601,7 @@ router.post("/close/:itemId", async (req, res) => {
|
|
|
600
601
|
res.status(400).json({ error: "Close reason is required" });
|
|
601
602
|
return;
|
|
602
603
|
}
|
|
603
|
-
const result = runPm({
|
|
604
|
+
const result = await runPm({
|
|
604
605
|
args: ["close", routeParam(req, "itemId"), reason.trim()],
|
|
605
606
|
userId: project.ownerUserId,
|
|
606
607
|
slug: project.slug,
|
|
@@ -624,7 +625,7 @@ router.delete("/delete/:itemId", async (req, res) => {
|
|
|
624
625
|
res.status(404).json({ error: "Project not found" });
|
|
625
626
|
return;
|
|
626
627
|
}
|
|
627
|
-
const result = runPm({
|
|
628
|
+
const result = await runPm({
|
|
628
629
|
args: ["delete", routeParam(req, "itemId"), "--yes"],
|
|
629
630
|
userId: project.ownerUserId,
|
|
630
631
|
slug: project.slug,
|
|
@@ -652,7 +653,7 @@ router.post("/comments/:itemId", async (req, res) => {
|
|
|
652
653
|
res.status(400).json({ error: "Comment text is required" });
|
|
653
654
|
return;
|
|
654
655
|
}
|
|
655
|
-
const result = runPm({
|
|
656
|
+
const result = await runPm({
|
|
656
657
|
args: ["comments", routeParam(req, "itemId"), text.trim()],
|
|
657
658
|
userId: project.ownerUserId,
|
|
658
659
|
slug: project.slug,
|
|
@@ -671,7 +672,7 @@ router.get("/comments/:itemId", async (req, res) => {
|
|
|
671
672
|
res.status(404).json({ error: "Project not found" });
|
|
672
673
|
return;
|
|
673
674
|
}
|
|
674
|
-
const result = runPm({
|
|
675
|
+
const result = await runPm({
|
|
675
676
|
args: ["comments", routeParam(req, "itemId")],
|
|
676
677
|
userId: project.ownerUserId,
|
|
677
678
|
slug: project.slug,
|
|
@@ -686,7 +687,7 @@ router.get("/notes/:itemId", async (req, res) => {
|
|
|
686
687
|
res.status(404).json({ error: "Project not found" });
|
|
687
688
|
return;
|
|
688
689
|
}
|
|
689
|
-
const result = runPm({
|
|
690
|
+
const result = await runPm({
|
|
690
691
|
args: ["notes", routeParam(req, "itemId")],
|
|
691
692
|
userId: project.ownerUserId,
|
|
692
693
|
slug: project.slug,
|
|
@@ -706,7 +707,7 @@ router.post("/notes/:itemId", async (req, res) => {
|
|
|
706
707
|
res.status(400).json({ error: "Note text is required" });
|
|
707
708
|
return;
|
|
708
709
|
}
|
|
709
|
-
const result = runPm({
|
|
710
|
+
const result = await runPm({
|
|
710
711
|
args: ["notes", routeParam(req, "itemId"), text.trim()],
|
|
711
712
|
userId: project.ownerUserId,
|
|
712
713
|
slug: project.slug,
|
|
@@ -728,7 +729,7 @@ router.get("/context", async (req, res) => {
|
|
|
728
729
|
const { depth } = req.query;
|
|
729
730
|
const validDepths = ["brief", "standard", "deep"];
|
|
730
731
|
const resolvedDepth = validDepths.includes(depth) ? depth : "standard";
|
|
731
|
-
const result = runPm({
|
|
732
|
+
const result = await runPm({
|
|
732
733
|
args: ["context", "--depth", resolvedDepth],
|
|
733
734
|
userId: project.ownerUserId,
|
|
734
735
|
slug: project.slug,
|
|
@@ -747,7 +748,7 @@ router.get("/activity", async (req, res) => {
|
|
|
747
748
|
const args = ["activity"];
|
|
748
749
|
if (limit)
|
|
749
750
|
args.push("--limit", limit);
|
|
750
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
751
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
751
752
|
res.json(result.ok ? (result.parsed || {}) : { activity: [] });
|
|
752
753
|
});
|
|
753
754
|
// GET /api/projects/:projectId/pm/stats
|
|
@@ -757,7 +758,7 @@ router.get("/stats", async (req, res) => {
|
|
|
757
758
|
res.status(404).json({ error: "Project not found" });
|
|
758
759
|
return;
|
|
759
760
|
}
|
|
760
|
-
const result = runPm({
|
|
761
|
+
const result = await runPm({
|
|
761
762
|
args: ["stats"],
|
|
762
763
|
userId: project.ownerUserId,
|
|
763
764
|
slug: project.slug,
|
|
@@ -772,7 +773,7 @@ router.get("/aggregate", async (req, res) => {
|
|
|
772
773
|
res.status(404).json({ error: "Project not found" });
|
|
773
774
|
return;
|
|
774
775
|
}
|
|
775
|
-
const result = runPm({
|
|
776
|
+
const result = await runPm({
|
|
776
777
|
args: ["aggregate"],
|
|
777
778
|
userId: project.ownerUserId,
|
|
778
779
|
slug: project.slug,
|
|
@@ -794,7 +795,7 @@ router.post("/search", async (req, res) => {
|
|
|
794
795
|
}
|
|
795
796
|
const validModes = ["keyword", "semantic", "hybrid"];
|
|
796
797
|
const safeMode = validModes.includes(mode || "") ? mode : "hybrid";
|
|
797
|
-
const result = runPm({
|
|
798
|
+
const result = await runPm({
|
|
798
799
|
args: ["search", "--mode", safeMode, ...query.trim().split(/\s+/)],
|
|
799
800
|
userId: project.ownerUserId,
|
|
800
801
|
slug: project.slug,
|
|
@@ -816,7 +817,7 @@ router.get("/calendar", async (req, res) => {
|
|
|
816
817
|
res.status(404).json({ error: "Project not found" });
|
|
817
818
|
return;
|
|
818
819
|
}
|
|
819
|
-
const result = runPm({
|
|
820
|
+
const result = await runPm({
|
|
820
821
|
args: ["calendar", "--view", "month"],
|
|
821
822
|
userId: project.ownerUserId,
|
|
822
823
|
slug: project.slug,
|
|
@@ -835,7 +836,7 @@ router.get("/calendar.ics", async (req, res) => {
|
|
|
835
836
|
res.status(404).json({ error: "Project not found" });
|
|
836
837
|
return;
|
|
837
838
|
}
|
|
838
|
-
const listed = runPm({
|
|
839
|
+
const listed = await runPm({
|
|
839
840
|
args: ["list-all", "--json"],
|
|
840
841
|
userId: project.ownerUserId,
|
|
841
842
|
slug: project.slug,
|
|
@@ -873,7 +874,7 @@ router.get("/health", async (req, res) => {
|
|
|
873
874
|
res.status(404).json({ error: "Project not found" });
|
|
874
875
|
return;
|
|
875
876
|
}
|
|
876
|
-
const result = runPm({
|
|
877
|
+
const result = await runPm({
|
|
877
878
|
args: ["health"],
|
|
878
879
|
userId: project.ownerUserId,
|
|
879
880
|
slug: project.slug,
|
|
@@ -893,7 +894,7 @@ router.post("/append/:itemId", async (req, res) => {
|
|
|
893
894
|
res.status(400).json({ error: "Text is required" });
|
|
894
895
|
return;
|
|
895
896
|
}
|
|
896
|
-
const result = runPm({
|
|
897
|
+
const result = await runPm({
|
|
897
898
|
args: ["append", routeParam(req, "itemId"), text.trim()],
|
|
898
899
|
userId: project.ownerUserId,
|
|
899
900
|
slug: project.slug,
|
|
@@ -913,7 +914,7 @@ router.get("/history/:itemId", async (req, res) => {
|
|
|
913
914
|
res.status(404).json({ error: "Project not found" });
|
|
914
915
|
return;
|
|
915
916
|
}
|
|
916
|
-
const result = runPm({
|
|
917
|
+
const result = await runPm({
|
|
917
918
|
args: ["history", routeParam(req, "itemId")],
|
|
918
919
|
userId: project.ownerUserId,
|
|
919
920
|
slug: project.slug,
|
|
@@ -928,7 +929,7 @@ router.get("/deps/:itemId", async (req, res) => {
|
|
|
928
929
|
res.status(404).json({ error: "Project not found" });
|
|
929
930
|
return;
|
|
930
931
|
}
|
|
931
|
-
const result = runPm({
|
|
932
|
+
const result = await runPm({
|
|
932
933
|
args: ["deps", routeParam(req, "itemId")],
|
|
933
934
|
userId: project.ownerUserId,
|
|
934
935
|
slug: project.slug,
|
|
@@ -949,7 +950,7 @@ router.post("/deps/:itemId", async (req, res) => {
|
|
|
949
950
|
return;
|
|
950
951
|
}
|
|
951
952
|
const depRel = normalizeDependencyKind(rel);
|
|
952
|
-
const result = runPm({
|
|
953
|
+
const result = await runPm({
|
|
953
954
|
args: ["update", routeParam(req, "itemId"), "--dep", `id=${targetId.trim()},kind=${depRel}`],
|
|
954
955
|
userId: project.ownerUserId,
|
|
955
956
|
slug: project.slug,
|
|
@@ -982,7 +983,7 @@ router.delete("/deps/:itemId", async (req, res) => {
|
|
|
982
983
|
}
|
|
983
984
|
const depRel = normalizeDependencyKind(rel || "relates_to");
|
|
984
985
|
const selector = `id=${targetId.trim()},kind=${depRel}`;
|
|
985
|
-
const result = runPm({
|
|
986
|
+
const result = await runPm({
|
|
986
987
|
args: ["update", routeParam(req, "itemId"), "--dep-remove", selector],
|
|
987
988
|
userId: project.ownerUserId,
|
|
988
989
|
slug: project.slug,
|
|
@@ -1014,7 +1015,7 @@ router.post("/rel", async (req, res) => {
|
|
|
1014
1015
|
return;
|
|
1015
1016
|
}
|
|
1016
1017
|
const depRel = normalizeDependencyKind(relType || "relates_to");
|
|
1017
|
-
const result = runPm({
|
|
1018
|
+
const result = await runPm({
|
|
1018
1019
|
args: ["update", from.trim(), "--dep", `id=${to.trim()},kind=${depRel}`],
|
|
1019
1020
|
userId: project.ownerUserId,
|
|
1020
1021
|
slug: project.slug,
|
|
@@ -1047,7 +1048,7 @@ router.delete("/rel", async (req, res) => {
|
|
|
1047
1048
|
}
|
|
1048
1049
|
const depRel = normalizeDependencyKind(relType || "relates_to");
|
|
1049
1050
|
const selector = `id=${to.trim()},kind=${depRel}`;
|
|
1050
|
-
const result = runPm({
|
|
1051
|
+
const result = await runPm({
|
|
1051
1052
|
args: ["update", from.trim(), "--dep-remove", selector, "--message", `Remove ${depRel} dependency on ${to.trim()}`],
|
|
1052
1053
|
userId: project.ownerUserId,
|
|
1053
1054
|
slug: project.slug,
|
|
@@ -1073,7 +1074,7 @@ router.get("/graph", async (req, res) => {
|
|
|
1073
1074
|
res.status(404).json({ error: "Project not found" });
|
|
1074
1075
|
return;
|
|
1075
1076
|
}
|
|
1076
|
-
const extensionGraph = pmGraphExtensionGraphForProject(project);
|
|
1077
|
+
const extensionGraph = await pmGraphExtensionGraphForProject(project);
|
|
1077
1078
|
if (extensionGraph.graph) {
|
|
1078
1079
|
res.json({
|
|
1079
1080
|
ok: true,
|
|
@@ -1085,7 +1086,7 @@ router.get("/graph", async (req, res) => {
|
|
|
1085
1086
|
try {
|
|
1086
1087
|
res.json({
|
|
1087
1088
|
ok: true,
|
|
1088
|
-
graph: fallbackGraphForProject(project.ownerUserId, project.slug),
|
|
1089
|
+
graph: await fallbackGraphForProject(project.ownerUserId, project.slug),
|
|
1089
1090
|
extensionAvailable: false,
|
|
1090
1091
|
extensionError: extensionGraph.error,
|
|
1091
1092
|
});
|
|
@@ -1140,7 +1141,7 @@ router.get("/graph/neighbors/:nodeId", async (req, res) => {
|
|
|
1140
1141
|
res.status(400).json({ error: "nodeId is required" });
|
|
1141
1142
|
return;
|
|
1142
1143
|
}
|
|
1143
|
-
const result = runPm({
|
|
1144
|
+
const result = await runPm({
|
|
1144
1145
|
args: ["pm-graph", "neighbors", nodeId, "--json"],
|
|
1145
1146
|
userId: project.ownerUserId,
|
|
1146
1147
|
slug: project.slug,
|
|
@@ -1171,7 +1172,7 @@ router.post("/graph/query", async (req, res) => {
|
|
|
1171
1172
|
res.status(400).json({ error: "cypher query is required" });
|
|
1172
1173
|
return;
|
|
1173
1174
|
}
|
|
1174
|
-
const result = runPm({
|
|
1175
|
+
const result = await runPm({
|
|
1175
1176
|
args: ["pm-graph", "query", cypher.trim(), "--json"],
|
|
1176
1177
|
userId: project.ownerUserId,
|
|
1177
1178
|
slug: project.slug,
|
|
@@ -1196,7 +1197,7 @@ router.get("/learnings/:itemId", async (req, res) => {
|
|
|
1196
1197
|
res.status(404).json({ error: "Project not found" });
|
|
1197
1198
|
return;
|
|
1198
1199
|
}
|
|
1199
|
-
const result = runPm({
|
|
1200
|
+
const result = await runPm({
|
|
1200
1201
|
args: ["learnings", routeParam(req, "itemId")],
|
|
1201
1202
|
userId: project.ownerUserId,
|
|
1202
1203
|
slug: project.slug,
|
|
@@ -1216,7 +1217,7 @@ router.post("/learnings/:itemId", async (req, res) => {
|
|
|
1216
1217
|
res.status(400).json({ error: "Learning text is required" });
|
|
1217
1218
|
return;
|
|
1218
1219
|
}
|
|
1219
|
-
const result = runPm({
|
|
1220
|
+
const result = await runPm({
|
|
1220
1221
|
args: ["learnings", routeParam(req, "itemId"), text.trim()],
|
|
1221
1222
|
userId: project.ownerUserId,
|
|
1222
1223
|
slug: project.slug,
|
|
@@ -1235,7 +1236,7 @@ router.post("/claim/:itemId", async (req, res) => {
|
|
|
1235
1236
|
res.status(404).json({ error: "Project not found" });
|
|
1236
1237
|
return;
|
|
1237
1238
|
}
|
|
1238
|
-
const result = runPm({
|
|
1239
|
+
const result = await runPm({
|
|
1239
1240
|
args: ["claim", routeParam(req, "itemId")],
|
|
1240
1241
|
userId: project.ownerUserId,
|
|
1241
1242
|
slug: project.slug,
|
|
@@ -1255,7 +1256,7 @@ router.post("/release/:itemId", async (req, res) => {
|
|
|
1255
1256
|
res.status(404).json({ error: "Project not found" });
|
|
1256
1257
|
return;
|
|
1257
1258
|
}
|
|
1258
|
-
const result = runPm({
|
|
1259
|
+
const result = await runPm({
|
|
1259
1260
|
args: ["release", routeParam(req, "itemId")],
|
|
1260
1261
|
userId: project.ownerUserId,
|
|
1261
1262
|
slug: project.slug,
|
|
@@ -1275,7 +1276,7 @@ router.post("/start-task/:itemId", async (req, res) => {
|
|
|
1275
1276
|
res.status(404).json({ error: "Project not found" });
|
|
1276
1277
|
return;
|
|
1277
1278
|
}
|
|
1278
|
-
const result = runPm({
|
|
1279
|
+
const result = await runPm({
|
|
1279
1280
|
args: ["start-task", routeParam(req, "itemId")],
|
|
1280
1281
|
userId: project.ownerUserId,
|
|
1281
1282
|
slug: project.slug,
|
|
@@ -1295,7 +1296,7 @@ router.post("/pause-task/:itemId", async (req, res) => {
|
|
|
1295
1296
|
res.status(404).json({ error: "Project not found" });
|
|
1296
1297
|
return;
|
|
1297
1298
|
}
|
|
1298
|
-
const result = runPm({
|
|
1299
|
+
const result = await runPm({
|
|
1299
1300
|
args: ["pause-task", routeParam(req, "itemId")],
|
|
1300
1301
|
userId: project.ownerUserId,
|
|
1301
1302
|
slug: project.slug,
|
|
@@ -1315,7 +1316,7 @@ router.get("/tests/:itemId", async (req, res) => {
|
|
|
1315
1316
|
res.status(404).json({ error: "Project not found" });
|
|
1316
1317
|
return;
|
|
1317
1318
|
}
|
|
1318
|
-
const result = runPm({
|
|
1319
|
+
const result = await runPm({
|
|
1319
1320
|
args: ["test", routeParam(req, "itemId")],
|
|
1320
1321
|
userId: project.ownerUserId,
|
|
1321
1322
|
slug: project.slug,
|
|
@@ -1338,7 +1339,7 @@ router.post("/tests/:itemId", async (req, res) => {
|
|
|
1338
1339
|
const args = ["test", routeParam(req, "itemId"), "--add", "--command", command.trim()];
|
|
1339
1340
|
if (description)
|
|
1340
1341
|
args.push("--description", description.trim());
|
|
1341
|
-
const result = runPm({
|
|
1342
|
+
const result = await runPm({
|
|
1342
1343
|
args,
|
|
1343
1344
|
userId: project.ownerUserId,
|
|
1344
1345
|
slug: project.slug,
|
|
@@ -1357,7 +1358,7 @@ router.get("/dedupe-audit", async (req, res) => {
|
|
|
1357
1358
|
res.status(404).json({ error: "Project not found" });
|
|
1358
1359
|
return;
|
|
1359
1360
|
}
|
|
1360
|
-
const result = runPm({
|
|
1361
|
+
const result = await runPm({
|
|
1361
1362
|
args: ["dedupe-audit"],
|
|
1362
1363
|
userId: project.ownerUserId,
|
|
1363
1364
|
slug: project.slug,
|
|
@@ -1372,7 +1373,7 @@ router.get("/validate", async (req, res) => {
|
|
|
1372
1373
|
res.status(404).json({ error: "Project not found" });
|
|
1373
1374
|
return;
|
|
1374
1375
|
}
|
|
1375
|
-
const result = runPm({
|
|
1376
|
+
const result = await runPm({
|
|
1376
1377
|
args: ["validate"],
|
|
1377
1378
|
userId: project.ownerUserId,
|
|
1378
1379
|
slug: project.slug,
|
|
@@ -1392,7 +1393,7 @@ router.post("/restore/:itemId", async (req, res) => {
|
|
|
1392
1393
|
res.status(400).json({ error: "Restore target (timestamp or version) is required" });
|
|
1393
1394
|
return;
|
|
1394
1395
|
}
|
|
1395
|
-
const result = runPm({
|
|
1396
|
+
const result = await runPm({
|
|
1396
1397
|
args: ["restore", routeParam(req, "itemId"), target.trim()],
|
|
1397
1398
|
userId: project.ownerUserId,
|
|
1398
1399
|
slug: project.slug,
|
|
@@ -1417,7 +1418,7 @@ router.post("/close-task/:itemId", async (req, res) => {
|
|
|
1417
1418
|
res.status(400).json({ error: "Close reason is required" });
|
|
1418
1419
|
return;
|
|
1419
1420
|
}
|
|
1420
|
-
const result = runPm({
|
|
1421
|
+
const result = await runPm({
|
|
1421
1422
|
args: ["close-task", routeParam(req, "itemId"), reason.trim()],
|
|
1422
1423
|
userId: project.ownerUserId,
|
|
1423
1424
|
slug: project.slug,
|
|
@@ -1440,7 +1441,7 @@ router.post("/reindex", async (req, res) => {
|
|
|
1440
1441
|
const { mode = "keyword" } = req.body;
|
|
1441
1442
|
const validModes = ["keyword", "semantic", "hybrid"];
|
|
1442
1443
|
const safeMode = validModes.includes(mode) ? mode : "keyword";
|
|
1443
|
-
const result = runPm({
|
|
1444
|
+
const result = await runPm({
|
|
1444
1445
|
args: ["reindex", "--mode", safeMode],
|
|
1445
1446
|
userId: project.ownerUserId,
|
|
1446
1447
|
slug: project.slug,
|
|
@@ -1460,7 +1461,7 @@ router.post("/normalize", async (req, res) => {
|
|
|
1460
1461
|
res.status(404).json({ error: "Project not found" });
|
|
1461
1462
|
return;
|
|
1462
1463
|
}
|
|
1463
|
-
const result = runPm({
|
|
1464
|
+
const result = await runPm({
|
|
1464
1465
|
args: ["normalize"],
|
|
1465
1466
|
userId: project.ownerUserId,
|
|
1466
1467
|
slug: project.slug,
|
|
@@ -1475,7 +1476,7 @@ router.get("/comments-audit", async (req, res) => {
|
|
|
1475
1476
|
res.status(404).json({ error: "Project not found" });
|
|
1476
1477
|
return;
|
|
1477
1478
|
}
|
|
1478
|
-
const result = runPm({
|
|
1479
|
+
const result = await runPm({
|
|
1479
1480
|
args: ["comments-audit"],
|
|
1480
1481
|
userId: project.ownerUserId,
|
|
1481
1482
|
slug: project.slug,
|
|
@@ -1499,7 +1500,7 @@ router.post("/files/:itemId", async (req, res) => {
|
|
|
1499
1500
|
if (scope)
|
|
1500
1501
|
addVal += `,scope=${scope}`;
|
|
1501
1502
|
const args = ["files", routeParam(req, "itemId"), "--add", addVal];
|
|
1502
|
-
const result = runPm({
|
|
1503
|
+
const result = await runPm({
|
|
1503
1504
|
args,
|
|
1504
1505
|
userId: project.ownerUserId,
|
|
1505
1506
|
slug: project.slug,
|
|
@@ -1519,7 +1520,7 @@ router.get("/files/:itemId", async (req, res) => {
|
|
|
1519
1520
|
res.status(404).json({ error: "Project not found" });
|
|
1520
1521
|
return;
|
|
1521
1522
|
}
|
|
1522
|
-
const result = runPm({
|
|
1523
|
+
const result = await runPm({
|
|
1523
1524
|
args: ["files", routeParam(req, "itemId")],
|
|
1524
1525
|
userId: project.ownerUserId,
|
|
1525
1526
|
slug: project.slug,
|
|
@@ -1534,7 +1535,7 @@ router.get("/guide", async (req, res) => {
|
|
|
1534
1535
|
res.status(404).json({ error: "Project not found" });
|
|
1535
1536
|
return;
|
|
1536
1537
|
}
|
|
1537
|
-
const result = runPm({
|
|
1538
|
+
const result = await runPm({
|
|
1538
1539
|
args: ["guide"],
|
|
1539
1540
|
userId: project.ownerUserId,
|
|
1540
1541
|
slug: project.slug,
|
|
@@ -1549,7 +1550,7 @@ router.get("/guide/:topicId", async (req, res) => {
|
|
|
1549
1550
|
res.status(404).json({ error: "Project not found" });
|
|
1550
1551
|
return;
|
|
1551
1552
|
}
|
|
1552
|
-
const result = runPm({
|
|
1553
|
+
const result = await runPm({
|
|
1553
1554
|
args: ["guide", routeParam(req, "topicId")],
|
|
1554
1555
|
userId: project.ownerUserId,
|
|
1555
1556
|
slug: project.slug,
|
|
@@ -1575,7 +1576,7 @@ router.get("/export", async (req, res) => {
|
|
|
1575
1576
|
}
|
|
1576
1577
|
const format = req.query["format"] || "json";
|
|
1577
1578
|
// Use --full --include-body to get the richest available list-level metadata
|
|
1578
|
-
const result = runPm({
|
|
1579
|
+
const result = await runPm({
|
|
1579
1580
|
args: ["list-all", "--limit", "10000", "--full", "--include-body"],
|
|
1580
1581
|
userId: project.ownerUserId,
|
|
1581
1582
|
slug: project.slug,
|
|
@@ -1715,7 +1716,7 @@ router.post("/import", async (req, res) => {
|
|
|
1715
1716
|
args.push("--body", item.body);
|
|
1716
1717
|
if (item.parent)
|
|
1717
1718
|
args.push("--parent", item.parent);
|
|
1718
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1719
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1719
1720
|
if (result.ok && result.parsed) {
|
|
1720
1721
|
const parsed = result.parsed;
|
|
1721
1722
|
created.push(parsed.item?.id || `item[${i}]`);
|
|
@@ -1772,7 +1773,7 @@ router.post("/update-many", async (req, res) => {
|
|
|
1772
1773
|
if (body[key])
|
|
1773
1774
|
args.push(flag, body[key]);
|
|
1774
1775
|
}
|
|
1775
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1776
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1776
1777
|
if (!result.ok) {
|
|
1777
1778
|
res.status(400).json({ error: result.stderr || "update-many failed" });
|
|
1778
1779
|
return;
|
|
@@ -1814,7 +1815,7 @@ router.post("/close-many", async (req, res) => {
|
|
|
1814
1815
|
if (body[key])
|
|
1815
1816
|
listArgs.push(flag, body[key]);
|
|
1816
1817
|
}
|
|
1817
|
-
const listResult = runPm({ args: listArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1818
|
+
const listResult = await runPm({ args: listArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1818
1819
|
if (!listResult.ok) {
|
|
1819
1820
|
res.status(400).json({ error: listResult.stderr || "Failed to list items for close-many" });
|
|
1820
1821
|
return;
|
|
@@ -1834,7 +1835,7 @@ router.post("/close-many", async (req, res) => {
|
|
|
1834
1835
|
const closeArgs = targetStatus === "canceled"
|
|
1835
1836
|
? ["update", itemId, "--status", "canceled"]
|
|
1836
1837
|
: ["close", itemId, reason];
|
|
1837
|
-
const closeResult = runPm({ args: closeArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1838
|
+
const closeResult = await runPm({ args: closeArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1838
1839
|
if (closeResult.ok) {
|
|
1839
1840
|
rows.push({ id: itemId, status: "ok" });
|
|
1840
1841
|
closedCount++;
|
|
@@ -1866,7 +1867,7 @@ router.get("/docs/:itemId", async (req, res) => {
|
|
|
1866
1867
|
res.status(404).json({ error: "Project not found" });
|
|
1867
1868
|
return;
|
|
1868
1869
|
}
|
|
1869
|
-
const result = runPm({
|
|
1870
|
+
const result = await runPm({
|
|
1870
1871
|
args: ["docs", routeParam(req, "itemId")],
|
|
1871
1872
|
userId: project.ownerUserId,
|
|
1872
1873
|
slug: project.slug,
|
|
@@ -1901,7 +1902,7 @@ router.post("/docs/:itemId", async (req, res) => {
|
|
|
1901
1902
|
res.status(400).json({ error: "path, remove, or validatePaths is required" });
|
|
1902
1903
|
return;
|
|
1903
1904
|
}
|
|
1904
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1905
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1905
1906
|
if (!result.ok) {
|
|
1906
1907
|
res.status(400).json({ error: result.stderr || "Failed to update docs" });
|
|
1907
1908
|
return;
|
|
@@ -1924,7 +1925,7 @@ router.post("/test-all", async (req, res) => {
|
|
|
1924
1925
|
args.push("--limit", body.limit);
|
|
1925
1926
|
if (body.timeout)
|
|
1926
1927
|
args.push("--timeout", body.timeout);
|
|
1927
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1928
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1928
1929
|
if (!result.ok) {
|
|
1929
1930
|
res.status(400).json({ error: result.stderr || "test-all failed" });
|
|
1930
1931
|
return;
|
|
@@ -1944,7 +1945,7 @@ router.get("/test-runs", async (req, res) => {
|
|
|
1944
1945
|
args.push("--status", status);
|
|
1945
1946
|
if (limit)
|
|
1946
1947
|
args.push("--limit", limit);
|
|
1947
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1948
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1948
1949
|
res.json(result.ok ? (result.parsed || {}) : { runs: [] });
|
|
1949
1950
|
});
|
|
1950
1951
|
// POST /api/projects/:projectId/pm/gc
|
|
@@ -1954,7 +1955,7 @@ router.post("/gc", async (req, res) => {
|
|
|
1954
1955
|
res.status(404).json({ error: "Project not found" });
|
|
1955
1956
|
return;
|
|
1956
1957
|
}
|
|
1957
|
-
const result = runPm({ args: ["gc"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1958
|
+
const result = await runPm({ args: ["gc"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1958
1959
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1959
1960
|
});
|
|
1960
1961
|
// GET /api/projects/:projectId/pm/templates
|
|
@@ -1964,7 +1965,7 @@ router.get("/templates", async (req, res) => {
|
|
|
1964
1965
|
res.status(404).json({ error: "Project not found" });
|
|
1965
1966
|
return;
|
|
1966
1967
|
}
|
|
1967
|
-
const result = runPm({ args: ["templates", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1968
|
+
const result = await runPm({ args: ["templates", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1968
1969
|
res.json(result.ok ? (result.parsed || {}) : { templates: [] });
|
|
1969
1970
|
});
|
|
1970
1971
|
// GET /api/projects/:projectId/pm/templates/:name
|
|
@@ -1974,7 +1975,7 @@ router.get("/templates/:name", async (req, res) => {
|
|
|
1974
1975
|
res.status(404).json({ error: "Project not found" });
|
|
1975
1976
|
return;
|
|
1976
1977
|
}
|
|
1977
|
-
const result = runPm({ args: ["templates", "show", routeParam(req, "name")], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1978
|
+
const result = await runPm({ args: ["templates", "show", routeParam(req, "name")], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1978
1979
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1979
1980
|
});
|
|
1980
1981
|
// GET /api/projects/:projectId/pm/config
|
|
@@ -1984,7 +1985,7 @@ router.get("/config", async (req, res) => {
|
|
|
1984
1985
|
res.status(404).json({ error: "Project not found" });
|
|
1985
1986
|
return;
|
|
1986
1987
|
}
|
|
1987
|
-
const result = runPm({ args: ["config", "project", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1988
|
+
const result = await runPm({ args: ["config", "project", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1988
1989
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1989
1990
|
});
|
|
1990
1991
|
// GET /api/projects/:projectId/pm/config/:key
|
|
@@ -1995,7 +1996,7 @@ router.get("/config/:key", async (req, res) => {
|
|
|
1995
1996
|
return;
|
|
1996
1997
|
}
|
|
1997
1998
|
const key = routeParam(req, "key");
|
|
1998
|
-
const result = runPm({ args: ["config", "project", "get", key], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1999
|
+
const result = await runPm({ args: ["config", "project", "get", key], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1999
2000
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
2000
2001
|
});
|
|
2001
2002
|
// PATCH /api/projects/:projectId/pm/config/:key
|
|
@@ -2014,7 +2015,7 @@ router.patch("/config/:key", async (req, res) => {
|
|
|
2014
2015
|
args.push("--policy", body.policy);
|
|
2015
2016
|
if (body.format)
|
|
2016
2017
|
args.push("--format", body.format);
|
|
2017
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2018
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2018
2019
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
2019
2020
|
});
|
|
2020
2021
|
// ─── List status shortcut routes ───
|
|
@@ -2044,7 +2045,7 @@ function buildListShortcutRoute(pmCommand) {
|
|
|
2044
2045
|
args.push("--sprint", sprint);
|
|
2045
2046
|
if (release)
|
|
2046
2047
|
args.push("--release", release);
|
|
2047
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2048
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2048
2049
|
res.json(result.ok ? (result.parsed || {}) : { items: [] });
|
|
2049
2050
|
};
|
|
2050
2051
|
}
|
|
@@ -2080,7 +2081,7 @@ router.post("/plan", async (req, res) => {
|
|
|
2080
2081
|
args.push("--priority", priority);
|
|
2081
2082
|
if (body)
|
|
2082
2083
|
args.push("--body", body);
|
|
2083
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2084
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2084
2085
|
if (!result.ok) {
|
|
2085
2086
|
res.status(400).json({ error: result.stderr || "Failed to create plan" });
|
|
2086
2087
|
return;
|
|
@@ -2098,7 +2099,7 @@ router.get("/plan/:planId", async (req, res) => {
|
|
|
2098
2099
|
res.status(404).json({ error: "Project not found" });
|
|
2099
2100
|
return;
|
|
2100
2101
|
}
|
|
2101
|
-
const result = runPm({
|
|
2102
|
+
const result = await runPm({
|
|
2102
2103
|
args: ["plan", "show", routeParam(req, "planId"), "--depth", "standard"],
|
|
2103
2104
|
userId: project.ownerUserId,
|
|
2104
2105
|
slug: project.slug,
|
|
@@ -2123,7 +2124,7 @@ router.patch("/plan/:planId", async (req, res) => {
|
|
|
2123
2124
|
args.push("--title", title.trim());
|
|
2124
2125
|
if (description !== undefined)
|
|
2125
2126
|
args.push("--description", description);
|
|
2126
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2127
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2127
2128
|
if (!result.ok) {
|
|
2128
2129
|
res.status(400).json({ error: result.stderr || "Failed to update plan" });
|
|
2129
2130
|
return;
|
|
@@ -2141,7 +2142,7 @@ router.delete("/plan/:planId", async (req, res) => {
|
|
|
2141
2142
|
res.status(404).json({ error: "Project not found" });
|
|
2142
2143
|
return;
|
|
2143
2144
|
}
|
|
2144
|
-
const result = runPm({
|
|
2145
|
+
const result = await runPm({
|
|
2145
2146
|
args: ["delete", routeParam(req, "planId")],
|
|
2146
2147
|
userId: project.ownerUserId,
|
|
2147
2148
|
slug: project.slug,
|
|
@@ -2174,7 +2175,7 @@ router.post("/plan/:planId/steps", async (req, res) => {
|
|
|
2174
2175
|
args.push("--description", description);
|
|
2175
2176
|
if (dependsOn)
|
|
2176
2177
|
args.push("--depends-on", dependsOn);
|
|
2177
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2178
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2178
2179
|
if (!result.ok) {
|
|
2179
2180
|
res.status(400).json({ error: result.stderr || "Failed to add step" });
|
|
2180
2181
|
return;
|
|
@@ -2198,7 +2199,7 @@ router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
|
2198
2199
|
args.push("--title", title);
|
|
2199
2200
|
if (description)
|
|
2200
2201
|
args.push("--description", description);
|
|
2201
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2202
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2202
2203
|
if (!result.ok) {
|
|
2203
2204
|
res.status(400).json({ error: result.stderr || "Failed to update step" });
|
|
2204
2205
|
return;
|
|
@@ -2216,7 +2217,7 @@ router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
|
|
|
2216
2217
|
res.status(404).json({ error: "Project not found" });
|
|
2217
2218
|
return;
|
|
2218
2219
|
}
|
|
2219
|
-
const result = runPm({
|
|
2220
|
+
const result = await runPm({
|
|
2220
2221
|
args: ["plan", "complete-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
|
|
2221
2222
|
userId: project.ownerUserId,
|
|
2222
2223
|
slug: project.slug,
|
|
@@ -2244,7 +2245,7 @@ router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
|
|
|
2244
2245
|
res.status(400).json({ error: "Block reason is required" });
|
|
2245
2246
|
return;
|
|
2246
2247
|
}
|
|
2247
|
-
const result = runPm({
|
|
2248
|
+
const result = await runPm({
|
|
2248
2249
|
args: ["plan", "block-step", routeParam(req, "planId"), routeParam(req, "stepRef"), "--step-blocked-reason", reason.trim()],
|
|
2249
2250
|
userId: project.ownerUserId,
|
|
2250
2251
|
slug: project.slug,
|
|
@@ -2267,7 +2268,7 @@ router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
|
2267
2268
|
res.status(404).json({ error: "Project not found" });
|
|
2268
2269
|
return;
|
|
2269
2270
|
}
|
|
2270
|
-
const result = runPm({
|
|
2271
|
+
const result = await runPm({
|
|
2271
2272
|
args: ["plan", "remove-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
|
|
2272
2273
|
userId: project.ownerUserId,
|
|
2273
2274
|
slug: project.slug,
|
|
@@ -2290,7 +2291,7 @@ router.post("/plan/:planId/approve", async (req, res) => {
|
|
|
2290
2291
|
res.status(404).json({ error: "Project not found" });
|
|
2291
2292
|
return;
|
|
2292
2293
|
}
|
|
2293
|
-
const result = runPm({
|
|
2294
|
+
const result = await runPm({
|
|
2294
2295
|
args: ["plan", "approve", routeParam(req, "planId")],
|
|
2295
2296
|
userId: project.ownerUserId,
|
|
2296
2297
|
slug: project.slug,
|
|
@@ -2321,7 +2322,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
|
|
|
2321
2322
|
args.push("--materialize-parent", materializeParent);
|
|
2322
2323
|
if (steps)
|
|
2323
2324
|
args.push("--steps", steps);
|
|
2324
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2325
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2325
2326
|
if (!result.ok) {
|
|
2326
2327
|
res.status(400).json({ error: result.stderr || "Failed to materialize plan" });
|
|
2327
2328
|
return;
|
|
@@ -2344,7 +2345,7 @@ router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
|
|
|
2344
2345
|
res.status(400).json({ error: "reorderTo (new order integer) is required" });
|
|
2345
2346
|
return;
|
|
2346
2347
|
}
|
|
2347
|
-
const result = runPm({
|
|
2348
|
+
const result = await runPm({
|
|
2348
2349
|
args: ["plan", "reorder-step", routeParam(req, "planId"), routeParam(req, "stepRef"), String(reorderTo)],
|
|
2349
2350
|
userId: project.ownerUserId,
|
|
2350
2351
|
slug: project.slug,
|
|
@@ -2379,7 +2380,7 @@ router.post("/plan/:planId/link", async (req, res) => {
|
|
|
2379
2380
|
args.push("--link-note", linkNote);
|
|
2380
2381
|
if (promoteToItemDep === "true")
|
|
2381
2382
|
args.push("--promote-to-item-dep");
|
|
2382
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2383
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2383
2384
|
if (!result.ok) {
|
|
2384
2385
|
res.status(400).json({ error: result.stderr || "Failed to link plan" });
|
|
2385
2386
|
return;
|
|
@@ -2405,7 +2406,7 @@ router.delete("/plan/:planId/link", async (req, res) => {
|
|
|
2405
2406
|
const args = ["plan", "unlink", routeParam(req, "planId"), "--link", link.trim()];
|
|
2406
2407
|
if (linkKind)
|
|
2407
2408
|
args.push("--link-kind", linkKind);
|
|
2408
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2409
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2409
2410
|
if (!result.ok) {
|
|
2410
2411
|
res.status(400).json({ error: result.stderr || "Failed to unlink plan" });
|
|
2411
2412
|
return;
|
|
@@ -2426,7 +2427,7 @@ router.get("/upgrade", async (req, res) => {
|
|
|
2426
2427
|
res.status(404).json({ error: "Project not found" });
|
|
2427
2428
|
return;
|
|
2428
2429
|
}
|
|
2429
|
-
const result = runPm({
|
|
2430
|
+
const result = await runPm({
|
|
2430
2431
|
args: ["upgrade", "--dry-run", "--packages-only"],
|
|
2431
2432
|
userId: project.ownerUserId,
|
|
2432
2433
|
slug: project.slug,
|
|
@@ -2441,7 +2442,7 @@ router.post("/upgrade", async (req, res) => {
|
|
|
2441
2442
|
return;
|
|
2442
2443
|
}
|
|
2443
2444
|
// Only allow package upgrades from the web UI — never self-upgrade the CLI binary.
|
|
2444
|
-
const result = runPm({
|
|
2445
|
+
const result = await runPm({
|
|
2445
2446
|
args: ["upgrade", "--packages-only"],
|
|
2446
2447
|
userId: project.ownerUserId,
|
|
2447
2448
|
slug: project.slug,
|
|
@@ -2453,18 +2454,25 @@ router.post("/upgrade", async (req, res) => {
|
|
|
2453
2454
|
}
|
|
2454
2455
|
res.json(result.parsed || { ok: true });
|
|
2455
2456
|
});
|
|
2456
|
-
// ─── Presence endpoints ───
|
|
2457
|
-
// GET /api/projects/:projectId/pm/presence
|
|
2458
|
-
router.get("/presence", async (req, res) => {
|
|
2459
|
-
const projectId = routeParam(req, "projectId");
|
|
2460
|
-
res.json({ users: getProjectPresence(projectId) });
|
|
2461
|
-
});
|
|
2462
2457
|
// PATCH /api/projects/:projectId/pm/presence/:clientId
|
|
2463
2458
|
router.patch("/presence/:clientId", async (req, res) => {
|
|
2459
|
+
const projectId = routeParam(req, "projectId");
|
|
2460
|
+
const access = await verifyProjectAccess(req.user.userId, projectId);
|
|
2461
|
+
if (!access) {
|
|
2462
|
+
res.status(404).json({ error: "Project not found" });
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2464
2465
|
const { clientId } = req.params;
|
|
2465
2466
|
const { view } = req.body;
|
|
2466
|
-
if (view)
|
|
2467
|
-
|
|
2467
|
+
if (!view || !/^[a-z][a-z0-9-]{0,63}$/.test(view)) {
|
|
2468
|
+
res.status(400).json({ error: "Invalid view" });
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
const updated = updateClientView(clientId, req.user.userId, projectId, view);
|
|
2472
|
+
if (!updated) {
|
|
2473
|
+
res.status(404).json({ error: "Presence session not found" });
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2468
2476
|
res.json({ ok: true });
|
|
2469
2477
|
});
|
|
2470
2478
|
// ─── SSE endpoint ───
|
|
@@ -2482,13 +2490,21 @@ router.get("/events", async (req, res) => {
|
|
|
2482
2490
|
res.status(500).json({ error: "Failed to verify project for real-time sync" });
|
|
2483
2491
|
return;
|
|
2484
2492
|
}
|
|
2485
|
-
setupSSEHeaders(res);
|
|
2486
2493
|
const clientId = uuidv4();
|
|
2487
2494
|
const projectId = routeParam(req, "projectId");
|
|
2488
2495
|
const userId = req.user.userId;
|
|
2489
|
-
|
|
2490
|
-
|
|
2496
|
+
let displayName;
|
|
2497
|
+
try {
|
|
2498
|
+
const userResult = await pool.query("SELECT display_name FROM pm_users WHERE id = $1", [userId]);
|
|
2499
|
+
displayName = userResult.rows[0]?.display_name?.trim() || "Project member";
|
|
2500
|
+
}
|
|
2501
|
+
catch (error) {
|
|
2502
|
+
console.error("SSE presence lookup failed", error instanceof Error ? error.name : typeof error);
|
|
2503
|
+
res.status(500).json({ error: "Failed to initialize real-time presence" });
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2491
2506
|
const currentView = String(req.query["view"] ?? "items");
|
|
2507
|
+
setupSSEHeaders(res);
|
|
2492
2508
|
const unsubscribe = addSSEClient({
|
|
2493
2509
|
id: clientId,
|
|
2494
2510
|
projectId,
|
|
@@ -2502,8 +2518,6 @@ router.get("/events", async (req, res) => {
|
|
|
2502
2518
|
const heartbeat = setInterval(() => {
|
|
2503
2519
|
try {
|
|
2504
2520
|
res.write(": heartbeat\n\n");
|
|
2505
|
-
// Re-broadcast presence on heartbeat to keep list fresh
|
|
2506
|
-
broadcastPresence(projectId);
|
|
2507
2521
|
}
|
|
2508
2522
|
catch {
|
|
2509
2523
|
clearInterval(heartbeat);
|