@unbrained/pm-web 2026.7.11 → 2026.7.17-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/CHANGELOG.md +19 -0
- package/README.md +26 -0
- package/dist/app.d.ts +16 -1
- package/dist/app.js +53 -3
- 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 +172 -108
- 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 +27 -3
- package/dist/services/pm-runner.js +188 -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 +32 -26
- package/public/legal-notice.html +10 -12
- package/public/privacy-policy.html +14 -24
- package/public/src/app.js +18 -4
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +19 -4
- package/public/src/components/modals.js +5 -3
- package/public/src/components/modals.js.map +1 -1
- package/public/src/components/modals.ts +6 -3
- package/public/src/components/toast.js.map +1 -1
- package/public/src/cookie-consent.ts +68 -0
- package/public/src/i18n/de.json +117 -0
- package/public/src/i18n/en.json +117 -0
- package/public/src/i18n/es.json +117 -0
- package/public/src/i18n/zh.json +117 -0
- package/public/src/i18n.js +287 -0
- package/public/src/i18n.js.map +1 -0
- package/public/src/i18n.ts +306 -0
- package/public/src/sw.ts +446 -0
- package/public/src/views/auth.js +29 -6
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +27 -6
- package/public/src/views/graph.js.map +1 -1
- package/public/src/views/settings.js +73 -48
- package/public/src/views/settings.js.map +1 -1
- package/public/src/views/settings.ts +77 -48
- package/public/styles.css +2 -0
- package/public/sw.js +323 -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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import { requireAuth } from "../middleware/auth.js";
|
|
3
|
-
import { ensureGraphExtension, runPm } from "../services/pm-runner.js";
|
|
3
|
+
import { ensureGraphExtension, runPm, runGetItemAt, PmCliError, EXIT_CODE } 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,
|
|
@@ -540,6 +541,56 @@ router.get("/get/:itemId", async (req, res) => {
|
|
|
540
541
|
}
|
|
541
542
|
res.json(result.parsed || {});
|
|
542
543
|
});
|
|
544
|
+
// GET /api/projects/:projectId/pm/at/:itemId/:ref
|
|
545
|
+
// Point-in-time item view: reconstruct a verified historical item state at a
|
|
546
|
+
// one-based version number or ISO timestamp via the pm CLI SDK `getItemAt`
|
|
547
|
+
// projection (the same replay kernel that powers `pm get --at`). The read is
|
|
548
|
+
// mutation-free and lock-free, so it never interferes with concurrent writers.
|
|
549
|
+
//
|
|
550
|
+
// 200 — reconstructed item state at the requested ref
|
|
551
|
+
// 400 — invalid ref, or version/timestamp outside the available history range
|
|
552
|
+
// 404 — unknown item, or item with no recorded history
|
|
553
|
+
router.get("/at/:itemId/:ref", async (req, res) => {
|
|
554
|
+
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
555
|
+
if (!project) {
|
|
556
|
+
res.status(404).json({ error: "Project not found" });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const itemId = routeParam(req, "itemId");
|
|
560
|
+
const ref = routeParam(req, "ref");
|
|
561
|
+
try {
|
|
562
|
+
const result = await runGetItemAt(project.ownerUserId, project.slug, itemId, ref);
|
|
563
|
+
// Mirror the `pm get` envelope so existing clients can reuse the same shape:
|
|
564
|
+
// the reconstructed metadata is exposed as `item` (with `body` inlined),
|
|
565
|
+
// alongside the provenance fields `reconstructed` / `as_of_version` /
|
|
566
|
+
// `as_of_timestamp` / `history_length` and the resolved `target`.
|
|
567
|
+
const item = { ...result.document.metadata, body: result.document.body };
|
|
568
|
+
res.json({
|
|
569
|
+
item,
|
|
570
|
+
reconstructed: result.reconstructed,
|
|
571
|
+
as_of_version: result.as_of_version,
|
|
572
|
+
as_of_timestamp: result.as_of_timestamp,
|
|
573
|
+
history_length: result.history_length,
|
|
574
|
+
target: result.target,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
if (err instanceof PmCliError) {
|
|
579
|
+
if (err.exitCode === EXIT_CODE.NOT_FOUND) {
|
|
580
|
+
res.status(404).json({ error: err.message });
|
|
581
|
+
}
|
|
582
|
+
else if (err.exitCode === EXIT_CODE.USAGE) {
|
|
583
|
+
res.status(400).json({ error: err.message });
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
res.status(500).json({ error: err.message });
|
|
587
|
+
}
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
console.error("Point-in-time item read failed:", err);
|
|
591
|
+
res.status(500).json({ error: "Failed to reconstruct historical item state" });
|
|
592
|
+
}
|
|
593
|
+
});
|
|
543
594
|
// PATCH /api/projects/:projectId/pm/update/:itemId
|
|
544
595
|
router.patch("/update/:itemId", async (req, res) => {
|
|
545
596
|
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
@@ -575,7 +626,7 @@ router.patch("/update/:itemId", async (req, res) => {
|
|
|
575
626
|
// Type can be set but must use --type
|
|
576
627
|
if (body.type)
|
|
577
628
|
args.push("--type", body.type);
|
|
578
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
629
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
579
630
|
if (!result.ok) {
|
|
580
631
|
res.status(400).json({ error: result.stderr || "Failed to update item" });
|
|
581
632
|
return;
|
|
@@ -600,7 +651,7 @@ router.post("/close/:itemId", async (req, res) => {
|
|
|
600
651
|
res.status(400).json({ error: "Close reason is required" });
|
|
601
652
|
return;
|
|
602
653
|
}
|
|
603
|
-
const result = runPm({
|
|
654
|
+
const result = await runPm({
|
|
604
655
|
args: ["close", routeParam(req, "itemId"), reason.trim()],
|
|
605
656
|
userId: project.ownerUserId,
|
|
606
657
|
slug: project.slug,
|
|
@@ -624,7 +675,7 @@ router.delete("/delete/:itemId", async (req, res) => {
|
|
|
624
675
|
res.status(404).json({ error: "Project not found" });
|
|
625
676
|
return;
|
|
626
677
|
}
|
|
627
|
-
const result = runPm({
|
|
678
|
+
const result = await runPm({
|
|
628
679
|
args: ["delete", routeParam(req, "itemId"), "--yes"],
|
|
629
680
|
userId: project.ownerUserId,
|
|
630
681
|
slug: project.slug,
|
|
@@ -652,7 +703,7 @@ router.post("/comments/:itemId", async (req, res) => {
|
|
|
652
703
|
res.status(400).json({ error: "Comment text is required" });
|
|
653
704
|
return;
|
|
654
705
|
}
|
|
655
|
-
const result = runPm({
|
|
706
|
+
const result = await runPm({
|
|
656
707
|
args: ["comments", routeParam(req, "itemId"), text.trim()],
|
|
657
708
|
userId: project.ownerUserId,
|
|
658
709
|
slug: project.slug,
|
|
@@ -671,7 +722,7 @@ router.get("/comments/:itemId", async (req, res) => {
|
|
|
671
722
|
res.status(404).json({ error: "Project not found" });
|
|
672
723
|
return;
|
|
673
724
|
}
|
|
674
|
-
const result = runPm({
|
|
725
|
+
const result = await runPm({
|
|
675
726
|
args: ["comments", routeParam(req, "itemId")],
|
|
676
727
|
userId: project.ownerUserId,
|
|
677
728
|
slug: project.slug,
|
|
@@ -686,7 +737,7 @@ router.get("/notes/:itemId", async (req, res) => {
|
|
|
686
737
|
res.status(404).json({ error: "Project not found" });
|
|
687
738
|
return;
|
|
688
739
|
}
|
|
689
|
-
const result = runPm({
|
|
740
|
+
const result = await runPm({
|
|
690
741
|
args: ["notes", routeParam(req, "itemId")],
|
|
691
742
|
userId: project.ownerUserId,
|
|
692
743
|
slug: project.slug,
|
|
@@ -706,7 +757,7 @@ router.post("/notes/:itemId", async (req, res) => {
|
|
|
706
757
|
res.status(400).json({ error: "Note text is required" });
|
|
707
758
|
return;
|
|
708
759
|
}
|
|
709
|
-
const result = runPm({
|
|
760
|
+
const result = await runPm({
|
|
710
761
|
args: ["notes", routeParam(req, "itemId"), text.trim()],
|
|
711
762
|
userId: project.ownerUserId,
|
|
712
763
|
slug: project.slug,
|
|
@@ -728,7 +779,7 @@ router.get("/context", async (req, res) => {
|
|
|
728
779
|
const { depth } = req.query;
|
|
729
780
|
const validDepths = ["brief", "standard", "deep"];
|
|
730
781
|
const resolvedDepth = validDepths.includes(depth) ? depth : "standard";
|
|
731
|
-
const result = runPm({
|
|
782
|
+
const result = await runPm({
|
|
732
783
|
args: ["context", "--depth", resolvedDepth],
|
|
733
784
|
userId: project.ownerUserId,
|
|
734
785
|
slug: project.slug,
|
|
@@ -747,7 +798,7 @@ router.get("/activity", async (req, res) => {
|
|
|
747
798
|
const args = ["activity"];
|
|
748
799
|
if (limit)
|
|
749
800
|
args.push("--limit", limit);
|
|
750
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
801
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
751
802
|
res.json(result.ok ? (result.parsed || {}) : { activity: [] });
|
|
752
803
|
});
|
|
753
804
|
// GET /api/projects/:projectId/pm/stats
|
|
@@ -757,7 +808,7 @@ router.get("/stats", async (req, res) => {
|
|
|
757
808
|
res.status(404).json({ error: "Project not found" });
|
|
758
809
|
return;
|
|
759
810
|
}
|
|
760
|
-
const result = runPm({
|
|
811
|
+
const result = await runPm({
|
|
761
812
|
args: ["stats"],
|
|
762
813
|
userId: project.ownerUserId,
|
|
763
814
|
slug: project.slug,
|
|
@@ -772,7 +823,7 @@ router.get("/aggregate", async (req, res) => {
|
|
|
772
823
|
res.status(404).json({ error: "Project not found" });
|
|
773
824
|
return;
|
|
774
825
|
}
|
|
775
|
-
const result = runPm({
|
|
826
|
+
const result = await runPm({
|
|
776
827
|
args: ["aggregate"],
|
|
777
828
|
userId: project.ownerUserId,
|
|
778
829
|
slug: project.slug,
|
|
@@ -794,7 +845,7 @@ router.post("/search", async (req, res) => {
|
|
|
794
845
|
}
|
|
795
846
|
const validModes = ["keyword", "semantic", "hybrid"];
|
|
796
847
|
const safeMode = validModes.includes(mode || "") ? mode : "hybrid";
|
|
797
|
-
const result = runPm({
|
|
848
|
+
const result = await runPm({
|
|
798
849
|
args: ["search", "--mode", safeMode, ...query.trim().split(/\s+/)],
|
|
799
850
|
userId: project.ownerUserId,
|
|
800
851
|
slug: project.slug,
|
|
@@ -816,7 +867,7 @@ router.get("/calendar", async (req, res) => {
|
|
|
816
867
|
res.status(404).json({ error: "Project not found" });
|
|
817
868
|
return;
|
|
818
869
|
}
|
|
819
|
-
const result = runPm({
|
|
870
|
+
const result = await runPm({
|
|
820
871
|
args: ["calendar", "--view", "month"],
|
|
821
872
|
userId: project.ownerUserId,
|
|
822
873
|
slug: project.slug,
|
|
@@ -835,7 +886,7 @@ router.get("/calendar.ics", async (req, res) => {
|
|
|
835
886
|
res.status(404).json({ error: "Project not found" });
|
|
836
887
|
return;
|
|
837
888
|
}
|
|
838
|
-
const listed = runPm({
|
|
889
|
+
const listed = await runPm({
|
|
839
890
|
args: ["list-all", "--json"],
|
|
840
891
|
userId: project.ownerUserId,
|
|
841
892
|
slug: project.slug,
|
|
@@ -873,7 +924,7 @@ router.get("/health", async (req, res) => {
|
|
|
873
924
|
res.status(404).json({ error: "Project not found" });
|
|
874
925
|
return;
|
|
875
926
|
}
|
|
876
|
-
const result = runPm({
|
|
927
|
+
const result = await runPm({
|
|
877
928
|
args: ["health"],
|
|
878
929
|
userId: project.ownerUserId,
|
|
879
930
|
slug: project.slug,
|
|
@@ -893,7 +944,7 @@ router.post("/append/:itemId", async (req, res) => {
|
|
|
893
944
|
res.status(400).json({ error: "Text is required" });
|
|
894
945
|
return;
|
|
895
946
|
}
|
|
896
|
-
const result = runPm({
|
|
947
|
+
const result = await runPm({
|
|
897
948
|
args: ["append", routeParam(req, "itemId"), text.trim()],
|
|
898
949
|
userId: project.ownerUserId,
|
|
899
950
|
slug: project.slug,
|
|
@@ -913,7 +964,7 @@ router.get("/history/:itemId", async (req, res) => {
|
|
|
913
964
|
res.status(404).json({ error: "Project not found" });
|
|
914
965
|
return;
|
|
915
966
|
}
|
|
916
|
-
const result = runPm({
|
|
967
|
+
const result = await runPm({
|
|
917
968
|
args: ["history", routeParam(req, "itemId")],
|
|
918
969
|
userId: project.ownerUserId,
|
|
919
970
|
slug: project.slug,
|
|
@@ -928,7 +979,7 @@ router.get("/deps/:itemId", async (req, res) => {
|
|
|
928
979
|
res.status(404).json({ error: "Project not found" });
|
|
929
980
|
return;
|
|
930
981
|
}
|
|
931
|
-
const result = runPm({
|
|
982
|
+
const result = await runPm({
|
|
932
983
|
args: ["deps", routeParam(req, "itemId")],
|
|
933
984
|
userId: project.ownerUserId,
|
|
934
985
|
slug: project.slug,
|
|
@@ -949,7 +1000,7 @@ router.post("/deps/:itemId", async (req, res) => {
|
|
|
949
1000
|
return;
|
|
950
1001
|
}
|
|
951
1002
|
const depRel = normalizeDependencyKind(rel);
|
|
952
|
-
const result = runPm({
|
|
1003
|
+
const result = await runPm({
|
|
953
1004
|
args: ["update", routeParam(req, "itemId"), "--dep", `id=${targetId.trim()},kind=${depRel}`],
|
|
954
1005
|
userId: project.ownerUserId,
|
|
955
1006
|
slug: project.slug,
|
|
@@ -982,7 +1033,7 @@ router.delete("/deps/:itemId", async (req, res) => {
|
|
|
982
1033
|
}
|
|
983
1034
|
const depRel = normalizeDependencyKind(rel || "relates_to");
|
|
984
1035
|
const selector = `id=${targetId.trim()},kind=${depRel}`;
|
|
985
|
-
const result = runPm({
|
|
1036
|
+
const result = await runPm({
|
|
986
1037
|
args: ["update", routeParam(req, "itemId"), "--dep-remove", selector],
|
|
987
1038
|
userId: project.ownerUserId,
|
|
988
1039
|
slug: project.slug,
|
|
@@ -1014,7 +1065,7 @@ router.post("/rel", async (req, res) => {
|
|
|
1014
1065
|
return;
|
|
1015
1066
|
}
|
|
1016
1067
|
const depRel = normalizeDependencyKind(relType || "relates_to");
|
|
1017
|
-
const result = runPm({
|
|
1068
|
+
const result = await runPm({
|
|
1018
1069
|
args: ["update", from.trim(), "--dep", `id=${to.trim()},kind=${depRel}`],
|
|
1019
1070
|
userId: project.ownerUserId,
|
|
1020
1071
|
slug: project.slug,
|
|
@@ -1047,7 +1098,7 @@ router.delete("/rel", async (req, res) => {
|
|
|
1047
1098
|
}
|
|
1048
1099
|
const depRel = normalizeDependencyKind(relType || "relates_to");
|
|
1049
1100
|
const selector = `id=${to.trim()},kind=${depRel}`;
|
|
1050
|
-
const result = runPm({
|
|
1101
|
+
const result = await runPm({
|
|
1051
1102
|
args: ["update", from.trim(), "--dep-remove", selector, "--message", `Remove ${depRel} dependency on ${to.trim()}`],
|
|
1052
1103
|
userId: project.ownerUserId,
|
|
1053
1104
|
slug: project.slug,
|
|
@@ -1073,7 +1124,7 @@ router.get("/graph", async (req, res) => {
|
|
|
1073
1124
|
res.status(404).json({ error: "Project not found" });
|
|
1074
1125
|
return;
|
|
1075
1126
|
}
|
|
1076
|
-
const extensionGraph = pmGraphExtensionGraphForProject(project);
|
|
1127
|
+
const extensionGraph = await pmGraphExtensionGraphForProject(project);
|
|
1077
1128
|
if (extensionGraph.graph) {
|
|
1078
1129
|
res.json({
|
|
1079
1130
|
ok: true,
|
|
@@ -1085,7 +1136,7 @@ router.get("/graph", async (req, res) => {
|
|
|
1085
1136
|
try {
|
|
1086
1137
|
res.json({
|
|
1087
1138
|
ok: true,
|
|
1088
|
-
graph: fallbackGraphForProject(project.ownerUserId, project.slug),
|
|
1139
|
+
graph: await fallbackGraphForProject(project.ownerUserId, project.slug),
|
|
1089
1140
|
extensionAvailable: false,
|
|
1090
1141
|
extensionError: extensionGraph.error,
|
|
1091
1142
|
});
|
|
@@ -1140,7 +1191,7 @@ router.get("/graph/neighbors/:nodeId", async (req, res) => {
|
|
|
1140
1191
|
res.status(400).json({ error: "nodeId is required" });
|
|
1141
1192
|
return;
|
|
1142
1193
|
}
|
|
1143
|
-
const result = runPm({
|
|
1194
|
+
const result = await runPm({
|
|
1144
1195
|
args: ["pm-graph", "neighbors", nodeId, "--json"],
|
|
1145
1196
|
userId: project.ownerUserId,
|
|
1146
1197
|
slug: project.slug,
|
|
@@ -1171,7 +1222,7 @@ router.post("/graph/query", async (req, res) => {
|
|
|
1171
1222
|
res.status(400).json({ error: "cypher query is required" });
|
|
1172
1223
|
return;
|
|
1173
1224
|
}
|
|
1174
|
-
const result = runPm({
|
|
1225
|
+
const result = await runPm({
|
|
1175
1226
|
args: ["pm-graph", "query", cypher.trim(), "--json"],
|
|
1176
1227
|
userId: project.ownerUserId,
|
|
1177
1228
|
slug: project.slug,
|
|
@@ -1196,7 +1247,7 @@ router.get("/learnings/:itemId", async (req, res) => {
|
|
|
1196
1247
|
res.status(404).json({ error: "Project not found" });
|
|
1197
1248
|
return;
|
|
1198
1249
|
}
|
|
1199
|
-
const result = runPm({
|
|
1250
|
+
const result = await runPm({
|
|
1200
1251
|
args: ["learnings", routeParam(req, "itemId")],
|
|
1201
1252
|
userId: project.ownerUserId,
|
|
1202
1253
|
slug: project.slug,
|
|
@@ -1216,7 +1267,7 @@ router.post("/learnings/:itemId", async (req, res) => {
|
|
|
1216
1267
|
res.status(400).json({ error: "Learning text is required" });
|
|
1217
1268
|
return;
|
|
1218
1269
|
}
|
|
1219
|
-
const result = runPm({
|
|
1270
|
+
const result = await runPm({
|
|
1220
1271
|
args: ["learnings", routeParam(req, "itemId"), text.trim()],
|
|
1221
1272
|
userId: project.ownerUserId,
|
|
1222
1273
|
slug: project.slug,
|
|
@@ -1235,7 +1286,7 @@ router.post("/claim/:itemId", async (req, res) => {
|
|
|
1235
1286
|
res.status(404).json({ error: "Project not found" });
|
|
1236
1287
|
return;
|
|
1237
1288
|
}
|
|
1238
|
-
const result = runPm({
|
|
1289
|
+
const result = await runPm({
|
|
1239
1290
|
args: ["claim", routeParam(req, "itemId")],
|
|
1240
1291
|
userId: project.ownerUserId,
|
|
1241
1292
|
slug: project.slug,
|
|
@@ -1255,7 +1306,7 @@ router.post("/release/:itemId", async (req, res) => {
|
|
|
1255
1306
|
res.status(404).json({ error: "Project not found" });
|
|
1256
1307
|
return;
|
|
1257
1308
|
}
|
|
1258
|
-
const result = runPm({
|
|
1309
|
+
const result = await runPm({
|
|
1259
1310
|
args: ["release", routeParam(req, "itemId")],
|
|
1260
1311
|
userId: project.ownerUserId,
|
|
1261
1312
|
slug: project.slug,
|
|
@@ -1275,7 +1326,7 @@ router.post("/start-task/:itemId", async (req, res) => {
|
|
|
1275
1326
|
res.status(404).json({ error: "Project not found" });
|
|
1276
1327
|
return;
|
|
1277
1328
|
}
|
|
1278
|
-
const result = runPm({
|
|
1329
|
+
const result = await runPm({
|
|
1279
1330
|
args: ["start-task", routeParam(req, "itemId")],
|
|
1280
1331
|
userId: project.ownerUserId,
|
|
1281
1332
|
slug: project.slug,
|
|
@@ -1295,7 +1346,7 @@ router.post("/pause-task/:itemId", async (req, res) => {
|
|
|
1295
1346
|
res.status(404).json({ error: "Project not found" });
|
|
1296
1347
|
return;
|
|
1297
1348
|
}
|
|
1298
|
-
const result = runPm({
|
|
1349
|
+
const result = await runPm({
|
|
1299
1350
|
args: ["pause-task", routeParam(req, "itemId")],
|
|
1300
1351
|
userId: project.ownerUserId,
|
|
1301
1352
|
slug: project.slug,
|
|
@@ -1315,7 +1366,7 @@ router.get("/tests/:itemId", async (req, res) => {
|
|
|
1315
1366
|
res.status(404).json({ error: "Project not found" });
|
|
1316
1367
|
return;
|
|
1317
1368
|
}
|
|
1318
|
-
const result = runPm({
|
|
1369
|
+
const result = await runPm({
|
|
1319
1370
|
args: ["test", routeParam(req, "itemId")],
|
|
1320
1371
|
userId: project.ownerUserId,
|
|
1321
1372
|
slug: project.slug,
|
|
@@ -1338,7 +1389,7 @@ router.post("/tests/:itemId", async (req, res) => {
|
|
|
1338
1389
|
const args = ["test", routeParam(req, "itemId"), "--add", "--command", command.trim()];
|
|
1339
1390
|
if (description)
|
|
1340
1391
|
args.push("--description", description.trim());
|
|
1341
|
-
const result = runPm({
|
|
1392
|
+
const result = await runPm({
|
|
1342
1393
|
args,
|
|
1343
1394
|
userId: project.ownerUserId,
|
|
1344
1395
|
slug: project.slug,
|
|
@@ -1357,7 +1408,7 @@ router.get("/dedupe-audit", async (req, res) => {
|
|
|
1357
1408
|
res.status(404).json({ error: "Project not found" });
|
|
1358
1409
|
return;
|
|
1359
1410
|
}
|
|
1360
|
-
const result = runPm({
|
|
1411
|
+
const result = await runPm({
|
|
1361
1412
|
args: ["dedupe-audit"],
|
|
1362
1413
|
userId: project.ownerUserId,
|
|
1363
1414
|
slug: project.slug,
|
|
@@ -1372,7 +1423,7 @@ router.get("/validate", async (req, res) => {
|
|
|
1372
1423
|
res.status(404).json({ error: "Project not found" });
|
|
1373
1424
|
return;
|
|
1374
1425
|
}
|
|
1375
|
-
const result = runPm({
|
|
1426
|
+
const result = await runPm({
|
|
1376
1427
|
args: ["validate"],
|
|
1377
1428
|
userId: project.ownerUserId,
|
|
1378
1429
|
slug: project.slug,
|
|
@@ -1392,7 +1443,7 @@ router.post("/restore/:itemId", async (req, res) => {
|
|
|
1392
1443
|
res.status(400).json({ error: "Restore target (timestamp or version) is required" });
|
|
1393
1444
|
return;
|
|
1394
1445
|
}
|
|
1395
|
-
const result = runPm({
|
|
1446
|
+
const result = await runPm({
|
|
1396
1447
|
args: ["restore", routeParam(req, "itemId"), target.trim()],
|
|
1397
1448
|
userId: project.ownerUserId,
|
|
1398
1449
|
slug: project.slug,
|
|
@@ -1417,7 +1468,7 @@ router.post("/close-task/:itemId", async (req, res) => {
|
|
|
1417
1468
|
res.status(400).json({ error: "Close reason is required" });
|
|
1418
1469
|
return;
|
|
1419
1470
|
}
|
|
1420
|
-
const result = runPm({
|
|
1471
|
+
const result = await runPm({
|
|
1421
1472
|
args: ["close-task", routeParam(req, "itemId"), reason.trim()],
|
|
1422
1473
|
userId: project.ownerUserId,
|
|
1423
1474
|
slug: project.slug,
|
|
@@ -1440,7 +1491,7 @@ router.post("/reindex", async (req, res) => {
|
|
|
1440
1491
|
const { mode = "keyword" } = req.body;
|
|
1441
1492
|
const validModes = ["keyword", "semantic", "hybrid"];
|
|
1442
1493
|
const safeMode = validModes.includes(mode) ? mode : "keyword";
|
|
1443
|
-
const result = runPm({
|
|
1494
|
+
const result = await runPm({
|
|
1444
1495
|
args: ["reindex", "--mode", safeMode],
|
|
1445
1496
|
userId: project.ownerUserId,
|
|
1446
1497
|
slug: project.slug,
|
|
@@ -1460,7 +1511,7 @@ router.post("/normalize", async (req, res) => {
|
|
|
1460
1511
|
res.status(404).json({ error: "Project not found" });
|
|
1461
1512
|
return;
|
|
1462
1513
|
}
|
|
1463
|
-
const result = runPm({
|
|
1514
|
+
const result = await runPm({
|
|
1464
1515
|
args: ["normalize"],
|
|
1465
1516
|
userId: project.ownerUserId,
|
|
1466
1517
|
slug: project.slug,
|
|
@@ -1475,7 +1526,7 @@ router.get("/comments-audit", async (req, res) => {
|
|
|
1475
1526
|
res.status(404).json({ error: "Project not found" });
|
|
1476
1527
|
return;
|
|
1477
1528
|
}
|
|
1478
|
-
const result = runPm({
|
|
1529
|
+
const result = await runPm({
|
|
1479
1530
|
args: ["comments-audit"],
|
|
1480
1531
|
userId: project.ownerUserId,
|
|
1481
1532
|
slug: project.slug,
|
|
@@ -1499,7 +1550,7 @@ router.post("/files/:itemId", async (req, res) => {
|
|
|
1499
1550
|
if (scope)
|
|
1500
1551
|
addVal += `,scope=${scope}`;
|
|
1501
1552
|
const args = ["files", routeParam(req, "itemId"), "--add", addVal];
|
|
1502
|
-
const result = runPm({
|
|
1553
|
+
const result = await runPm({
|
|
1503
1554
|
args,
|
|
1504
1555
|
userId: project.ownerUserId,
|
|
1505
1556
|
slug: project.slug,
|
|
@@ -1519,7 +1570,7 @@ router.get("/files/:itemId", async (req, res) => {
|
|
|
1519
1570
|
res.status(404).json({ error: "Project not found" });
|
|
1520
1571
|
return;
|
|
1521
1572
|
}
|
|
1522
|
-
const result = runPm({
|
|
1573
|
+
const result = await runPm({
|
|
1523
1574
|
args: ["files", routeParam(req, "itemId")],
|
|
1524
1575
|
userId: project.ownerUserId,
|
|
1525
1576
|
slug: project.slug,
|
|
@@ -1534,7 +1585,7 @@ router.get("/guide", async (req, res) => {
|
|
|
1534
1585
|
res.status(404).json({ error: "Project not found" });
|
|
1535
1586
|
return;
|
|
1536
1587
|
}
|
|
1537
|
-
const result = runPm({
|
|
1588
|
+
const result = await runPm({
|
|
1538
1589
|
args: ["guide"],
|
|
1539
1590
|
userId: project.ownerUserId,
|
|
1540
1591
|
slug: project.slug,
|
|
@@ -1549,7 +1600,7 @@ router.get("/guide/:topicId", async (req, res) => {
|
|
|
1549
1600
|
res.status(404).json({ error: "Project not found" });
|
|
1550
1601
|
return;
|
|
1551
1602
|
}
|
|
1552
|
-
const result = runPm({
|
|
1603
|
+
const result = await runPm({
|
|
1553
1604
|
args: ["guide", routeParam(req, "topicId")],
|
|
1554
1605
|
userId: project.ownerUserId,
|
|
1555
1606
|
slug: project.slug,
|
|
@@ -1575,7 +1626,7 @@ router.get("/export", async (req, res) => {
|
|
|
1575
1626
|
}
|
|
1576
1627
|
const format = req.query["format"] || "json";
|
|
1577
1628
|
// Use --full --include-body to get the richest available list-level metadata
|
|
1578
|
-
const result = runPm({
|
|
1629
|
+
const result = await runPm({
|
|
1579
1630
|
args: ["list-all", "--limit", "10000", "--full", "--include-body"],
|
|
1580
1631
|
userId: project.ownerUserId,
|
|
1581
1632
|
slug: project.slug,
|
|
@@ -1715,7 +1766,7 @@ router.post("/import", async (req, res) => {
|
|
|
1715
1766
|
args.push("--body", item.body);
|
|
1716
1767
|
if (item.parent)
|
|
1717
1768
|
args.push("--parent", item.parent);
|
|
1718
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1769
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1719
1770
|
if (result.ok && result.parsed) {
|
|
1720
1771
|
const parsed = result.parsed;
|
|
1721
1772
|
created.push(parsed.item?.id || `item[${i}]`);
|
|
@@ -1772,7 +1823,7 @@ router.post("/update-many", async (req, res) => {
|
|
|
1772
1823
|
if (body[key])
|
|
1773
1824
|
args.push(flag, body[key]);
|
|
1774
1825
|
}
|
|
1775
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1826
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1776
1827
|
if (!result.ok) {
|
|
1777
1828
|
res.status(400).json({ error: result.stderr || "update-many failed" });
|
|
1778
1829
|
return;
|
|
@@ -1814,7 +1865,7 @@ router.post("/close-many", async (req, res) => {
|
|
|
1814
1865
|
if (body[key])
|
|
1815
1866
|
listArgs.push(flag, body[key]);
|
|
1816
1867
|
}
|
|
1817
|
-
const listResult = runPm({ args: listArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1868
|
+
const listResult = await runPm({ args: listArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1818
1869
|
if (!listResult.ok) {
|
|
1819
1870
|
res.status(400).json({ error: listResult.stderr || "Failed to list items for close-many" });
|
|
1820
1871
|
return;
|
|
@@ -1834,7 +1885,7 @@ router.post("/close-many", async (req, res) => {
|
|
|
1834
1885
|
const closeArgs = targetStatus === "canceled"
|
|
1835
1886
|
? ["update", itemId, "--status", "canceled"]
|
|
1836
1887
|
: ["close", itemId, reason];
|
|
1837
|
-
const closeResult = runPm({ args: closeArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1888
|
+
const closeResult = await runPm({ args: closeArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1838
1889
|
if (closeResult.ok) {
|
|
1839
1890
|
rows.push({ id: itemId, status: "ok" });
|
|
1840
1891
|
closedCount++;
|
|
@@ -1866,7 +1917,7 @@ router.get("/docs/:itemId", async (req, res) => {
|
|
|
1866
1917
|
res.status(404).json({ error: "Project not found" });
|
|
1867
1918
|
return;
|
|
1868
1919
|
}
|
|
1869
|
-
const result = runPm({
|
|
1920
|
+
const result = await runPm({
|
|
1870
1921
|
args: ["docs", routeParam(req, "itemId")],
|
|
1871
1922
|
userId: project.ownerUserId,
|
|
1872
1923
|
slug: project.slug,
|
|
@@ -1901,7 +1952,7 @@ router.post("/docs/:itemId", async (req, res) => {
|
|
|
1901
1952
|
res.status(400).json({ error: "path, remove, or validatePaths is required" });
|
|
1902
1953
|
return;
|
|
1903
1954
|
}
|
|
1904
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1955
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1905
1956
|
if (!result.ok) {
|
|
1906
1957
|
res.status(400).json({ error: result.stderr || "Failed to update docs" });
|
|
1907
1958
|
return;
|
|
@@ -1924,7 +1975,7 @@ router.post("/test-all", async (req, res) => {
|
|
|
1924
1975
|
args.push("--limit", body.limit);
|
|
1925
1976
|
if (body.timeout)
|
|
1926
1977
|
args.push("--timeout", body.timeout);
|
|
1927
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1978
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1928
1979
|
if (!result.ok) {
|
|
1929
1980
|
res.status(400).json({ error: result.stderr || "test-all failed" });
|
|
1930
1981
|
return;
|
|
@@ -1944,7 +1995,7 @@ router.get("/test-runs", async (req, res) => {
|
|
|
1944
1995
|
args.push("--status", status);
|
|
1945
1996
|
if (limit)
|
|
1946
1997
|
args.push("--limit", limit);
|
|
1947
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1998
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1948
1999
|
res.json(result.ok ? (result.parsed || {}) : { runs: [] });
|
|
1949
2000
|
});
|
|
1950
2001
|
// POST /api/projects/:projectId/pm/gc
|
|
@@ -1954,7 +2005,7 @@ router.post("/gc", async (req, res) => {
|
|
|
1954
2005
|
res.status(404).json({ error: "Project not found" });
|
|
1955
2006
|
return;
|
|
1956
2007
|
}
|
|
1957
|
-
const result = runPm({ args: ["gc"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2008
|
+
const result = await runPm({ args: ["gc"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1958
2009
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1959
2010
|
});
|
|
1960
2011
|
// GET /api/projects/:projectId/pm/templates
|
|
@@ -1964,7 +2015,7 @@ router.get("/templates", async (req, res) => {
|
|
|
1964
2015
|
res.status(404).json({ error: "Project not found" });
|
|
1965
2016
|
return;
|
|
1966
2017
|
}
|
|
1967
|
-
const result = runPm({ args: ["templates", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2018
|
+
const result = await runPm({ args: ["templates", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1968
2019
|
res.json(result.ok ? (result.parsed || {}) : { templates: [] });
|
|
1969
2020
|
});
|
|
1970
2021
|
// GET /api/projects/:projectId/pm/templates/:name
|
|
@@ -1974,7 +2025,7 @@ router.get("/templates/:name", async (req, res) => {
|
|
|
1974
2025
|
res.status(404).json({ error: "Project not found" });
|
|
1975
2026
|
return;
|
|
1976
2027
|
}
|
|
1977
|
-
const result = runPm({ args: ["templates", "show", routeParam(req, "name")], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2028
|
+
const result = await runPm({ args: ["templates", "show", routeParam(req, "name")], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1978
2029
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1979
2030
|
});
|
|
1980
2031
|
// GET /api/projects/:projectId/pm/config
|
|
@@ -1984,7 +2035,7 @@ router.get("/config", async (req, res) => {
|
|
|
1984
2035
|
res.status(404).json({ error: "Project not found" });
|
|
1985
2036
|
return;
|
|
1986
2037
|
}
|
|
1987
|
-
const result = runPm({ args: ["config", "project", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2038
|
+
const result = await runPm({ args: ["config", "project", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1988
2039
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1989
2040
|
});
|
|
1990
2041
|
// GET /api/projects/:projectId/pm/config/:key
|
|
@@ -1995,7 +2046,7 @@ router.get("/config/:key", async (req, res) => {
|
|
|
1995
2046
|
return;
|
|
1996
2047
|
}
|
|
1997
2048
|
const key = routeParam(req, "key");
|
|
1998
|
-
const result = runPm({ args: ["config", "project", "get", key], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2049
|
+
const result = await runPm({ args: ["config", "project", "get", key], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1999
2050
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
2000
2051
|
});
|
|
2001
2052
|
// PATCH /api/projects/:projectId/pm/config/:key
|
|
@@ -2014,7 +2065,7 @@ router.patch("/config/:key", async (req, res) => {
|
|
|
2014
2065
|
args.push("--policy", body.policy);
|
|
2015
2066
|
if (body.format)
|
|
2016
2067
|
args.push("--format", body.format);
|
|
2017
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2068
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2018
2069
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
2019
2070
|
});
|
|
2020
2071
|
// ─── List status shortcut routes ───
|
|
@@ -2044,7 +2095,7 @@ function buildListShortcutRoute(pmCommand) {
|
|
|
2044
2095
|
args.push("--sprint", sprint);
|
|
2045
2096
|
if (release)
|
|
2046
2097
|
args.push("--release", release);
|
|
2047
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2098
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2048
2099
|
res.json(result.ok ? (result.parsed || {}) : { items: [] });
|
|
2049
2100
|
};
|
|
2050
2101
|
}
|
|
@@ -2080,7 +2131,7 @@ router.post("/plan", async (req, res) => {
|
|
|
2080
2131
|
args.push("--priority", priority);
|
|
2081
2132
|
if (body)
|
|
2082
2133
|
args.push("--body", body);
|
|
2083
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2134
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2084
2135
|
if (!result.ok) {
|
|
2085
2136
|
res.status(400).json({ error: result.stderr || "Failed to create plan" });
|
|
2086
2137
|
return;
|
|
@@ -2098,7 +2149,7 @@ router.get("/plan/:planId", async (req, res) => {
|
|
|
2098
2149
|
res.status(404).json({ error: "Project not found" });
|
|
2099
2150
|
return;
|
|
2100
2151
|
}
|
|
2101
|
-
const result = runPm({
|
|
2152
|
+
const result = await runPm({
|
|
2102
2153
|
args: ["plan", "show", routeParam(req, "planId"), "--depth", "standard"],
|
|
2103
2154
|
userId: project.ownerUserId,
|
|
2104
2155
|
slug: project.slug,
|
|
@@ -2123,7 +2174,7 @@ router.patch("/plan/:planId", async (req, res) => {
|
|
|
2123
2174
|
args.push("--title", title.trim());
|
|
2124
2175
|
if (description !== undefined)
|
|
2125
2176
|
args.push("--description", description);
|
|
2126
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2177
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2127
2178
|
if (!result.ok) {
|
|
2128
2179
|
res.status(400).json({ error: result.stderr || "Failed to update plan" });
|
|
2129
2180
|
return;
|
|
@@ -2141,7 +2192,7 @@ router.delete("/plan/:planId", async (req, res) => {
|
|
|
2141
2192
|
res.status(404).json({ error: "Project not found" });
|
|
2142
2193
|
return;
|
|
2143
2194
|
}
|
|
2144
|
-
const result = runPm({
|
|
2195
|
+
const result = await runPm({
|
|
2145
2196
|
args: ["delete", routeParam(req, "planId")],
|
|
2146
2197
|
userId: project.ownerUserId,
|
|
2147
2198
|
slug: project.slug,
|
|
@@ -2174,7 +2225,7 @@ router.post("/plan/:planId/steps", async (req, res) => {
|
|
|
2174
2225
|
args.push("--description", description);
|
|
2175
2226
|
if (dependsOn)
|
|
2176
2227
|
args.push("--depends-on", dependsOn);
|
|
2177
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2228
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2178
2229
|
if (!result.ok) {
|
|
2179
2230
|
res.status(400).json({ error: result.stderr || "Failed to add step" });
|
|
2180
2231
|
return;
|
|
@@ -2198,7 +2249,7 @@ router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
|
2198
2249
|
args.push("--title", title);
|
|
2199
2250
|
if (description)
|
|
2200
2251
|
args.push("--description", description);
|
|
2201
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2252
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2202
2253
|
if (!result.ok) {
|
|
2203
2254
|
res.status(400).json({ error: result.stderr || "Failed to update step" });
|
|
2204
2255
|
return;
|
|
@@ -2216,7 +2267,7 @@ router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
|
|
|
2216
2267
|
res.status(404).json({ error: "Project not found" });
|
|
2217
2268
|
return;
|
|
2218
2269
|
}
|
|
2219
|
-
const result = runPm({
|
|
2270
|
+
const result = await runPm({
|
|
2220
2271
|
args: ["plan", "complete-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
|
|
2221
2272
|
userId: project.ownerUserId,
|
|
2222
2273
|
slug: project.slug,
|
|
@@ -2244,7 +2295,7 @@ router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
|
|
|
2244
2295
|
res.status(400).json({ error: "Block reason is required" });
|
|
2245
2296
|
return;
|
|
2246
2297
|
}
|
|
2247
|
-
const result = runPm({
|
|
2298
|
+
const result = await runPm({
|
|
2248
2299
|
args: ["plan", "block-step", routeParam(req, "planId"), routeParam(req, "stepRef"), "--step-blocked-reason", reason.trim()],
|
|
2249
2300
|
userId: project.ownerUserId,
|
|
2250
2301
|
slug: project.slug,
|
|
@@ -2267,7 +2318,7 @@ router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
|
2267
2318
|
res.status(404).json({ error: "Project not found" });
|
|
2268
2319
|
return;
|
|
2269
2320
|
}
|
|
2270
|
-
const result = runPm({
|
|
2321
|
+
const result = await runPm({
|
|
2271
2322
|
args: ["plan", "remove-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
|
|
2272
2323
|
userId: project.ownerUserId,
|
|
2273
2324
|
slug: project.slug,
|
|
@@ -2290,7 +2341,7 @@ router.post("/plan/:planId/approve", async (req, res) => {
|
|
|
2290
2341
|
res.status(404).json({ error: "Project not found" });
|
|
2291
2342
|
return;
|
|
2292
2343
|
}
|
|
2293
|
-
const result = runPm({
|
|
2344
|
+
const result = await runPm({
|
|
2294
2345
|
args: ["plan", "approve", routeParam(req, "planId")],
|
|
2295
2346
|
userId: project.ownerUserId,
|
|
2296
2347
|
slug: project.slug,
|
|
@@ -2321,7 +2372,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
|
|
|
2321
2372
|
args.push("--materialize-parent", materializeParent);
|
|
2322
2373
|
if (steps)
|
|
2323
2374
|
args.push("--steps", steps);
|
|
2324
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2375
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2325
2376
|
if (!result.ok) {
|
|
2326
2377
|
res.status(400).json({ error: result.stderr || "Failed to materialize plan" });
|
|
2327
2378
|
return;
|
|
@@ -2344,7 +2395,7 @@ router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
|
|
|
2344
2395
|
res.status(400).json({ error: "reorderTo (new order integer) is required" });
|
|
2345
2396
|
return;
|
|
2346
2397
|
}
|
|
2347
|
-
const result = runPm({
|
|
2398
|
+
const result = await runPm({
|
|
2348
2399
|
args: ["plan", "reorder-step", routeParam(req, "planId"), routeParam(req, "stepRef"), String(reorderTo)],
|
|
2349
2400
|
userId: project.ownerUserId,
|
|
2350
2401
|
slug: project.slug,
|
|
@@ -2379,7 +2430,7 @@ router.post("/plan/:planId/link", async (req, res) => {
|
|
|
2379
2430
|
args.push("--link-note", linkNote);
|
|
2380
2431
|
if (promoteToItemDep === "true")
|
|
2381
2432
|
args.push("--promote-to-item-dep");
|
|
2382
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2433
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2383
2434
|
if (!result.ok) {
|
|
2384
2435
|
res.status(400).json({ error: result.stderr || "Failed to link plan" });
|
|
2385
2436
|
return;
|
|
@@ -2405,7 +2456,7 @@ router.delete("/plan/:planId/link", async (req, res) => {
|
|
|
2405
2456
|
const args = ["plan", "unlink", routeParam(req, "planId"), "--link", link.trim()];
|
|
2406
2457
|
if (linkKind)
|
|
2407
2458
|
args.push("--link-kind", linkKind);
|
|
2408
|
-
const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2459
|
+
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2409
2460
|
if (!result.ok) {
|
|
2410
2461
|
res.status(400).json({ error: result.stderr || "Failed to unlink plan" });
|
|
2411
2462
|
return;
|
|
@@ -2426,7 +2477,7 @@ router.get("/upgrade", async (req, res) => {
|
|
|
2426
2477
|
res.status(404).json({ error: "Project not found" });
|
|
2427
2478
|
return;
|
|
2428
2479
|
}
|
|
2429
|
-
const result = runPm({
|
|
2480
|
+
const result = await runPm({
|
|
2430
2481
|
args: ["upgrade", "--dry-run", "--packages-only"],
|
|
2431
2482
|
userId: project.ownerUserId,
|
|
2432
2483
|
slug: project.slug,
|
|
@@ -2441,7 +2492,7 @@ router.post("/upgrade", async (req, res) => {
|
|
|
2441
2492
|
return;
|
|
2442
2493
|
}
|
|
2443
2494
|
// Only allow package upgrades from the web UI — never self-upgrade the CLI binary.
|
|
2444
|
-
const result = runPm({
|
|
2495
|
+
const result = await runPm({
|
|
2445
2496
|
args: ["upgrade", "--packages-only"],
|
|
2446
2497
|
userId: project.ownerUserId,
|
|
2447
2498
|
slug: project.slug,
|
|
@@ -2453,18 +2504,25 @@ router.post("/upgrade", async (req, res) => {
|
|
|
2453
2504
|
}
|
|
2454
2505
|
res.json(result.parsed || { ok: true });
|
|
2455
2506
|
});
|
|
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
2507
|
// PATCH /api/projects/:projectId/pm/presence/:clientId
|
|
2463
2508
|
router.patch("/presence/:clientId", async (req, res) => {
|
|
2509
|
+
const projectId = routeParam(req, "projectId");
|
|
2510
|
+
const access = await verifyProjectAccess(req.user.userId, projectId);
|
|
2511
|
+
if (!access) {
|
|
2512
|
+
res.status(404).json({ error: "Project not found" });
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2464
2515
|
const { clientId } = req.params;
|
|
2465
2516
|
const { view } = req.body;
|
|
2466
|
-
if (view)
|
|
2467
|
-
|
|
2517
|
+
if (!view || !/^[a-z][a-z0-9-]{0,63}$/.test(view)) {
|
|
2518
|
+
res.status(400).json({ error: "Invalid view" });
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
const updated = updateClientView(clientId, req.user.userId, projectId, view);
|
|
2522
|
+
if (!updated) {
|
|
2523
|
+
res.status(404).json({ error: "Presence session not found" });
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2468
2526
|
res.json({ ok: true });
|
|
2469
2527
|
});
|
|
2470
2528
|
// ─── SSE endpoint ───
|
|
@@ -2482,13 +2540,21 @@ router.get("/events", async (req, res) => {
|
|
|
2482
2540
|
res.status(500).json({ error: "Failed to verify project for real-time sync" });
|
|
2483
2541
|
return;
|
|
2484
2542
|
}
|
|
2485
|
-
setupSSEHeaders(res);
|
|
2486
2543
|
const clientId = uuidv4();
|
|
2487
2544
|
const projectId = routeParam(req, "projectId");
|
|
2488
2545
|
const userId = req.user.userId;
|
|
2489
|
-
|
|
2490
|
-
|
|
2546
|
+
let displayName;
|
|
2547
|
+
try {
|
|
2548
|
+
const userResult = await pool.query("SELECT display_name FROM pm_users WHERE id = $1", [userId]);
|
|
2549
|
+
displayName = userResult.rows[0]?.display_name?.trim() || "Project member";
|
|
2550
|
+
}
|
|
2551
|
+
catch (error) {
|
|
2552
|
+
console.error("SSE presence lookup failed", error instanceof Error ? error.name : typeof error);
|
|
2553
|
+
res.status(500).json({ error: "Failed to initialize real-time presence" });
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2491
2556
|
const currentView = String(req.query["view"] ?? "items");
|
|
2557
|
+
setupSSEHeaders(res);
|
|
2492
2558
|
const unsubscribe = addSSEClient({
|
|
2493
2559
|
id: clientId,
|
|
2494
2560
|
projectId,
|
|
@@ -2502,8 +2568,6 @@ router.get("/events", async (req, res) => {
|
|
|
2502
2568
|
const heartbeat = setInterval(() => {
|
|
2503
2569
|
try {
|
|
2504
2570
|
res.write(": heartbeat\n\n");
|
|
2505
|
-
// Re-broadcast presence on heartbeat to keep list fresh
|
|
2506
|
-
broadcastPresence(projectId);
|
|
2507
2571
|
}
|
|
2508
2572
|
catch {
|
|
2509
2573
|
clearInterval(heartbeat);
|