@unbrained/pm-web 2026.9.13 → 2026.9.23
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 +18 -1
- package/README.md +6 -4
- package/dist/app.d.ts +10 -0
- package/dist/app.js +19 -9
- package/dist/app.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/routes/admin.js +6 -6
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/auth.js +2 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/extensions.js +24 -37
- package/dist/routes/extensions.js.map +1 -1
- package/dist/routes/github.js +169 -107
- package/dist/routes/github.js.map +1 -1
- package/dist/routes/groups.js +9 -14
- package/dist/routes/groups.js.map +1 -1
- package/dist/routes/pm.js +532 -1015
- package/dist/routes/pm.js.map +1 -1
- package/dist/routes/projects.js +10 -6
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/route-helpers.d.ts +74 -0
- package/dist/routes/route-helpers.js +85 -0
- package/dist/routes/route-helpers.js.map +1 -0
- package/dist/routes/sharing.js +30 -18
- package/dist/routes/sharing.js.map +1 -1
- package/dist/server.js +18 -23
- package/dist/server.js.map +1 -1
- package/dist/services/mutation-event-watcher.js +3 -28
- package/dist/services/mutation-event-watcher.js.map +1 -1
- package/dist/services/pm-runner.js +3 -1
- package/dist/services/pm-runner.js.map +1 -1
- package/dist/services/project-watcher.js +14 -38
- package/dist/services/project-watcher.js.map +1 -1
- package/dist/services/sse.d.ts +7 -0
- package/dist/services/sse.js +22 -0
- package/dist/services/sse.js.map +1 -1
- package/dist/services/watcher-utils.d.ts +42 -0
- package/dist/services/watcher-utils.js +60 -0
- package/dist/services/watcher-utils.js.map +1 -0
- package/manifest.json +2 -2
- package/package.json +20 -9
- package/public/src/app.js +22 -52
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +18 -75
- package/public/src/browser-window.js +29 -0
- package/public/src/browser-window.js.map +1 -0
- package/public/src/browser-window.ts +56 -0
- package/public/src/components/modals.js +1 -1
- package/public/src/components/modals.js.map +1 -1
- package/public/src/components/modals.ts +1 -1
- package/public/src/i18n.js +14 -6
- package/public/src/i18n.js.map +1 -1
- package/public/src/i18n.ts +16 -6
- package/public/src/sw.ts +1 -0
- package/public/src/utils.js +58 -0
- package/public/src/utils.js.map +1 -1
- package/public/src/utils.ts +53 -0
- package/public/src/views/admin.js +2 -5
- package/public/src/views/admin.js.map +1 -1
- package/public/src/views/admin.ts +2 -2
- package/public/src/views/auth.js +5 -1
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +6 -1
- package/public/src/views/calendar.js +1 -2
- package/public/src/views/calendar.js.map +1 -1
- package/public/src/views/calendar.ts +1 -2
- package/public/src/views/config.js +14 -21
- package/public/src/views/config.js.map +1 -1
- package/public/src/views/config.ts +14 -19
- package/public/src/views/context.js +11 -12
- package/public/src/views/context.js.map +1 -1
- package/public/src/views/context.ts +13 -12
- package/public/src/views/create.js +3 -11
- package/public/src/views/create.js.map +1 -1
- package/public/src/views/create.ts +3 -3
- package/public/src/views/export.js +2 -3
- package/public/src/views/export.js.map +1 -1
- package/public/src/views/export.ts +2 -3
- package/public/src/views/github.js +33 -47
- package/public/src/views/github.js.map +1 -1
- package/public/src/views/github.ts +35 -28
- package/public/src/views/graph-canvas.js +92 -99
- package/public/src/views/graph-canvas.js.map +1 -1
- package/public/src/views/graph-canvas.ts +94 -93
- package/public/src/views/graph.js +88 -73
- package/public/src/views/graph.js.map +1 -1
- package/public/src/views/graph.ts +97 -63
- package/public/src/views/groups.js +2 -5
- package/public/src/views/groups.js.map +1 -1
- package/public/src/views/groups.ts +2 -2
- package/public/src/views/guide.js +11 -10
- package/public/src/views/guide.js.map +1 -1
- package/public/src/views/guide.ts +12 -10
- package/public/src/views/items.js +100 -130
- package/public/src/views/items.js.map +1 -1
- package/public/src/views/items.ts +99 -117
- package/public/src/views/normalize.js +2 -9
- package/public/src/views/normalize.js.map +1 -1
- package/public/src/views/normalize.ts +2 -9
- package/public/src/views/packages.js +7 -4
- package/public/src/views/packages.js.map +1 -1
- package/public/src/views/packages.ts +8 -4
- package/public/src/views/plan-execution.js.map +1 -1
- package/public/src/views/plan-execution.ts +5 -15
- package/public/src/views/plan.js +2 -1
- package/public/src/views/plan.js.map +1 -1
- package/public/src/views/plan.ts +6 -17
- package/public/src/views/projects.js +5 -6
- package/public/src/views/projects.js.map +1 -1
- package/public/src/views/projects.ts +5 -6
- package/public/src/views/search.js +5 -1
- package/public/src/views/search.js.map +1 -1
- package/public/src/views/search.ts +7 -1
- package/public/src/views/settings.js +30 -55
- package/public/src/views/settings.js.map +1 -1
- package/public/src/views/settings.ts +23 -17
- package/public/src/views/shared.js +2 -5
- package/public/src/views/shared.js.map +1 -1
- package/public/src/views/shared.ts +2 -5
- package/public/src/views/sharing.js +2 -5
- package/public/src/views/sharing.js.map +1 -1
- package/public/src/views/sharing.ts +2 -2
- package/public/src/views/templates.js +9 -16
- package/public/src/views/templates.js.map +1 -1
- package/public/src/views/templates.ts +9 -14
- package/public/src/views/validate.js +2 -9
- package/public/src/views/validate.js.map +1 -1
- package/public/src/views/validate.ts +2 -9
- package/public/sw.js +1 -0
package/dist/routes/pm.js
CHANGED
|
@@ -470,12 +470,7 @@ async function pmGraphExtensionGraphForProject(project) {
|
|
|
470
470
|
if (!provision.ok) {
|
|
471
471
|
return { error: provision.error };
|
|
472
472
|
}
|
|
473
|
-
const extensionResult = await
|
|
474
|
-
args: ["pm-graph", "export", "--json"],
|
|
475
|
-
userId: project.ownerUserId,
|
|
476
|
-
slug: project.slug,
|
|
477
|
-
jsonOutput: false,
|
|
478
|
-
});
|
|
473
|
+
const extensionResult = await projectPm(project, ["pm-graph", "export", "--json"], false);
|
|
479
474
|
let extensionData;
|
|
480
475
|
if (extensionResult.ok && extensionResult.stdout) {
|
|
481
476
|
try {
|
|
@@ -502,16 +497,223 @@ async function verifyProject(userId, projectId) {
|
|
|
502
497
|
return null;
|
|
503
498
|
return { slug: access.slug, prefix: access.prefix, ownerUserId: access.ownerUserId };
|
|
504
499
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
500
|
+
/**
|
|
501
|
+
* Load the caller's project or answer 404.
|
|
502
|
+
*
|
|
503
|
+
* The route layer repeats this access check before every pm invocation. One
|
|
504
|
+
* helper keeps the status and error body identical without copying the lookup.
|
|
505
|
+
*
|
|
506
|
+
* @param req - Authenticated request carrying the project id.
|
|
507
|
+
* @param res - Response used for the not-found body.
|
|
508
|
+
* @returns The project reference, or undefined after a 404 was sent.
|
|
509
|
+
*/
|
|
510
|
+
async function requireProject(req, res) {
|
|
509
511
|
const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
|
|
510
512
|
if (!project) {
|
|
511
513
|
res.status(404).json({ error: "Project not found" });
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
return project;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Run pm in the verified project's workspace.
|
|
520
|
+
*
|
|
521
|
+
* @param project - Owner and slug that locate the workspace.
|
|
522
|
+
* @param args - pm arguments, already validated by the route.
|
|
523
|
+
* @param jsonOutput - When set, request JSON output; omit to keep the CLI default.
|
|
524
|
+
* @returns The pm invocation result.
|
|
525
|
+
*/
|
|
526
|
+
function projectPm(project, args, jsonOutput) {
|
|
527
|
+
return runPm({
|
|
528
|
+
args,
|
|
529
|
+
userId: project.ownerUserId,
|
|
530
|
+
slug: project.slug,
|
|
531
|
+
...(jsonOutput === undefined ? {} : { jsonOutput }),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Shared body of the cursor-paginated list endpoints.
|
|
536
|
+
*
|
|
537
|
+
* Copies the base arguments, appends each filter flag whose query value is
|
|
538
|
+
* present, validates the `after` cursor (sending the shared 400 body when it
|
|
539
|
+
* is malformed), and maps a USAGE exit to a 400. Returns the command result
|
|
540
|
+
* so the caller can send its success or failure payload, or `undefined`
|
|
541
|
+
* after an error response has already been sent.
|
|
542
|
+
*
|
|
543
|
+
* @param res - Response used for the cursor and USAGE error bodies.
|
|
544
|
+
* @param project - Verified project that owns the workspace.
|
|
545
|
+
* @param baseArgs - Command and fixed arguments; copied, never mutated.
|
|
546
|
+
* @param flags - Ordered `[flag, value]` pairs appended when `value` is present.
|
|
547
|
+
* @param after - Raw `after` query value to validate as a pagination cursor.
|
|
548
|
+
* @returns The pm result, or `undefined` after an error response was sent.
|
|
549
|
+
*/
|
|
550
|
+
async function runCursorList(res, project, baseArgs, flags, after) {
|
|
551
|
+
const args = [...baseArgs];
|
|
552
|
+
for (const [flag, value] of flags) {
|
|
553
|
+
if (value)
|
|
554
|
+
args.push(flag, value);
|
|
555
|
+
}
|
|
556
|
+
const cursorResult = validateCursor(after);
|
|
557
|
+
if (cursorResult.error) {
|
|
558
|
+
res.status(400).json({ error: cursorResult.error, items: [] });
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
if (cursorResult.cursor)
|
|
562
|
+
args.push("--after", cursorResult.cursor);
|
|
563
|
+
const result = await projectPm(project, args, true);
|
|
564
|
+
if (!result.ok && result.exitCode === EXIT_CODE.USAGE) {
|
|
565
|
+
res.status(400).json({ error: result.stderr, items: [] });
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Run a mutating pm command and send the shared failure response.
|
|
572
|
+
*
|
|
573
|
+
* Every item and plan mutation maps a failed pm run to
|
|
574
|
+
* `pmErrorStatus(result)` with `result.stderr || fallback` as the body.
|
|
575
|
+
* Centralising it keeps the status mapping identical everywhere while the
|
|
576
|
+
* per-route fallback text stays at the call site.
|
|
577
|
+
*
|
|
578
|
+
* @param res - Response used for the failure body.
|
|
579
|
+
* @param project - Verified project that owns the workspace.
|
|
580
|
+
* @param args - pm arguments, already validated by the route.
|
|
581
|
+
* @param fallback - Error text used when pm wrote nothing to stderr.
|
|
582
|
+
* @param jsonOutput - Request JSON output (default); pass `false` to keep the
|
|
583
|
+
* CLI default output, as the raw delete invocation does.
|
|
584
|
+
* @returns The successful pm result, or `undefined` after the error response.
|
|
585
|
+
*/
|
|
586
|
+
async function runMutation(res, project, args, fallback, jsonOutput = true) {
|
|
587
|
+
const result = await projectPm(project, args, jsonOutput);
|
|
588
|
+
if (!result.ok) {
|
|
589
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || fallback });
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Broadcast an item lifecycle event to the project's SSE clients and queue a
|
|
596
|
+
* graph sync for the same project.
|
|
597
|
+
*
|
|
598
|
+
* @param req - Authenticated request carrying the project and user ids.
|
|
599
|
+
* @param project - Verified project that owns the workspace.
|
|
600
|
+
* @param type - Event name to broadcast and graph-sync reason.
|
|
601
|
+
* @param itemId - Item the event is about; defaults to the route's `itemId`.
|
|
602
|
+
*/
|
|
603
|
+
function emitItemEvent(req, project, type, itemId = routeParam(req, "itemId")) {
|
|
604
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
605
|
+
type,
|
|
606
|
+
data: { itemId, userId: req.user.userId },
|
|
607
|
+
});
|
|
608
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, type);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Queue a graph sync and broadcast a dependency event for one graph edge.
|
|
612
|
+
*
|
|
613
|
+
* The four dependency mutation routes (deps and rel, add and remove) all pair
|
|
614
|
+
* the same two side effects with the same payload shape; only the sync reason
|
|
615
|
+
* and the broadcast event name vary between them.
|
|
616
|
+
*
|
|
617
|
+
* @param req - Authenticated request carrying the project and user ids.
|
|
618
|
+
* @param project - Verified project that owns the workspace.
|
|
619
|
+
* @param syncReason - Graph-sync queue reason for this mutation.
|
|
620
|
+
* @param event - SSE dependency event name to broadcast.
|
|
621
|
+
* @param from - Source item id of the edge.
|
|
622
|
+
* @param to - Target item id of the edge.
|
|
623
|
+
* @param rel - Dependency kind of the edge.
|
|
624
|
+
*/
|
|
625
|
+
function emitDependencyEvent(req, project, syncReason, event, from, to, rel) {
|
|
626
|
+
scheduleGraphSync(routeParam(req, "projectId"), project, syncReason);
|
|
627
|
+
broadcastDependencyEvent(routeParam(req, "projectId"), event, {
|
|
628
|
+
from,
|
|
629
|
+
to,
|
|
630
|
+
rel,
|
|
631
|
+
userId: req.user.userId,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Run a plan mutation and complete the whole response.
|
|
636
|
+
*
|
|
637
|
+
* The plan step routes share one response shape end to end: map a failure via
|
|
638
|
+
* {@link pmErrorStatus} with the per-route fallback text, broadcast an
|
|
639
|
+
* `item-updated` event naming the plan id, and answer with the parsed result.
|
|
640
|
+
* This helper performs all of it; the caller supplies only the arguments and
|
|
641
|
+
* the failure text, and nothing after the call.
|
|
642
|
+
*
|
|
643
|
+
* @param req - Authenticated request carrying the project, plan and user ids.
|
|
644
|
+
* @param res - Response used for both the failure and success bodies.
|
|
645
|
+
* @param project - Verified project that owns the workspace.
|
|
646
|
+
* @param args - pm arguments, already validated by the route.
|
|
647
|
+
* @param fallback - Error text used when pm wrote nothing to stderr.
|
|
648
|
+
* @param successStatus - HTTP status for the success response (default 200).
|
|
649
|
+
*/
|
|
650
|
+
async function runPlanMutation(req, res, project, args, fallback, successStatus = 200) {
|
|
651
|
+
const result = await runMutation(res, project, args, fallback);
|
|
652
|
+
if (!result)
|
|
512
653
|
return;
|
|
654
|
+
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
655
|
+
type: "item-updated",
|
|
656
|
+
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
657
|
+
});
|
|
658
|
+
res.status(successStatus).json(result.parsed || {});
|
|
659
|
+
}
|
|
660
|
+
/** Validate a `text` body field and run a mutation with it. Used by the
|
|
661
|
+
* comments, notes, append and learnings POST routes that all share the same
|
|
662
|
+
* body shape and error-handling pattern. */
|
|
663
|
+
async function runTextBodyMutation(req, res, project, cmd, errorMsg, failedMsg) {
|
|
664
|
+
const { text } = req.body;
|
|
665
|
+
if (!text?.trim()) {
|
|
666
|
+
res.status(400).json({ error: errorMsg });
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
return runMutation(res, project, [cmd, routeParam(req, "itemId"), text.trim()], failedMsg);
|
|
670
|
+
}
|
|
671
|
+
/** Validate a `reason` body field and run a mutation with it. Used by the
|
|
672
|
+
* close and close-task POST routes that share the same body shape. */
|
|
673
|
+
async function runReasonBodyMutation(req, res, project, cmd, errorMsg, failedMsg) {
|
|
674
|
+
const { reason } = req.body;
|
|
675
|
+
if (!reason?.trim()) {
|
|
676
|
+
res.status(400).json({ error: errorMsg });
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
return runMutation(res, project, [cmd, routeParam(req, "itemId"), reason.trim()], failedMsg);
|
|
680
|
+
}
|
|
681
|
+
/** Validate the `targetId`/`rel` body fields for deps add/remove routes and
|
|
682
|
+
* return the normalized dependency kind. */
|
|
683
|
+
function parseDepBody(req, res, defaultRel) {
|
|
684
|
+
const { targetId, rel } = req.body;
|
|
685
|
+
if (!targetId?.trim()) {
|
|
686
|
+
res.status(400).json({ error: "targetId is required" });
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
return { targetId: targetId.trim(), depRel: normalizeDependencyKind(rel || defaultRel) };
|
|
690
|
+
}
|
|
691
|
+
/** Validate the `from`/`to`/`type` body fields for rel add/remove routes and
|
|
692
|
+
* return the normalized values. */
|
|
693
|
+
function parseRelBody(req, res) {
|
|
694
|
+
const { from, to, type: relType } = req.body;
|
|
695
|
+
if (!from?.trim() || !to?.trim()) {
|
|
696
|
+
res.status(400).json({ error: "from and to item IDs are required" });
|
|
697
|
+
return undefined;
|
|
513
698
|
}
|
|
514
|
-
|
|
699
|
+
return { from: from.trim(), to: to.trim(), depRel: normalizeDependencyKind(relType || "relates_to") };
|
|
700
|
+
}
|
|
701
|
+
/** Push `--title` and `--description` args from a plan/step PATCH body when
|
|
702
|
+
* present. Used by the plan and step update routes. */
|
|
703
|
+
function pushTitleDescArgs(args, body) {
|
|
704
|
+
if (body.title?.trim())
|
|
705
|
+
args.push("--title", body.title.trim());
|
|
706
|
+
if (body.description !== undefined)
|
|
707
|
+
args.push("--description", body.description);
|
|
708
|
+
}
|
|
709
|
+
// GET /api/projects/:projectId/pm/schema
|
|
710
|
+
// Returns runtime types/statuses from `pm contracts --json` so the frontend
|
|
711
|
+
// stays in sync with whatever pm CLI version + extensions are installed.
|
|
712
|
+
router.get("/schema", async (req, res) => {
|
|
713
|
+
const project = await requireProject(req, res);
|
|
714
|
+
if (!project)
|
|
715
|
+
return;
|
|
716
|
+
const result = await projectPm(project, ["contracts", "--json"], true);
|
|
515
717
|
const contracts = result.ok && result.parsed ? result.parsed : null;
|
|
516
718
|
const rt = contracts?.["runtime_schema"];
|
|
517
719
|
res.json({
|
|
@@ -568,11 +770,9 @@ function isSafeTypeFolder(folder) {
|
|
|
568
770
|
* collaborators with 403 before this handler runs.
|
|
569
771
|
*/
|
|
570
772
|
router.post("/schema/add-type", async (req, res) => {
|
|
571
|
-
const project = await
|
|
572
|
-
if (!project)
|
|
573
|
-
res.status(404).json({ error: "Project not found" });
|
|
773
|
+
const project = await requireProject(req, res);
|
|
774
|
+
if (!project)
|
|
574
775
|
return;
|
|
575
|
-
}
|
|
576
776
|
const body = req.body;
|
|
577
777
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
578
778
|
if (!name) {
|
|
@@ -607,7 +807,7 @@ router.post("/schema/add-type", async (req, res) => {
|
|
|
607
807
|
args.push("--alias", alias.trim());
|
|
608
808
|
}
|
|
609
809
|
}
|
|
610
|
-
const result = await
|
|
810
|
+
const result = await projectPm(project, args, true);
|
|
611
811
|
if (!result.ok) {
|
|
612
812
|
res.status(400).json({ error: result.stderr || "pm schema add-type failed" });
|
|
613
813
|
return;
|
|
@@ -623,11 +823,9 @@ router.post("/schema/add-type", async (req, res) => {
|
|
|
623
823
|
* Dry Run controls against a route that was never mounted.
|
|
624
824
|
*/
|
|
625
825
|
router.post("/items/:itemId/history-repair", async (req, res) => {
|
|
626
|
-
const project = await
|
|
627
|
-
if (!project)
|
|
628
|
-
res.status(404).json({ error: "Project not found" });
|
|
826
|
+
const project = await requireProject(req, res);
|
|
827
|
+
if (!project)
|
|
629
828
|
return;
|
|
630
|
-
}
|
|
631
829
|
const itemId = routeParam(req, "itemId").trim();
|
|
632
830
|
if (!itemId) {
|
|
633
831
|
res.status(400).json({ error: "itemId is required" });
|
|
@@ -640,7 +838,7 @@ router.post("/items/:itemId/history-repair", async (req, res) => {
|
|
|
640
838
|
const args = ["history-repair", itemId];
|
|
641
839
|
if (req.body?.dryRun === true)
|
|
642
840
|
args.push("--dry-run");
|
|
643
|
-
const result = await
|
|
841
|
+
const result = await projectPm(project, args, true);
|
|
644
842
|
if (!result.ok) {
|
|
645
843
|
res.status(400).json({ error: result.stderr || "pm history-repair failed" });
|
|
646
844
|
return;
|
|
@@ -649,81 +847,48 @@ router.post("/items/:itemId/history-repair", async (req, res) => {
|
|
|
649
847
|
});
|
|
650
848
|
// GET /api/projects/:projectId/pm/list
|
|
651
849
|
router.get("/list", async (req, res) => {
|
|
652
|
-
const project = await
|
|
653
|
-
if (!project)
|
|
654
|
-
res.status(404).json({ error: "Project not found" });
|
|
850
|
+
const project = await requireProject(req, res);
|
|
851
|
+
if (!project)
|
|
655
852
|
return;
|
|
656
|
-
}
|
|
657
853
|
const { status, type, limit, priority, sprint, release, assignee, after } = req.query;
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if (
|
|
668
|
-
args.push("--sprint", sprint);
|
|
669
|
-
if (release)
|
|
670
|
-
args.push("--release", release);
|
|
671
|
-
if (assignee)
|
|
672
|
-
args.push("--assignee", assignee);
|
|
673
|
-
const cursorResult = validateCursor(after);
|
|
674
|
-
if (cursorResult.error) {
|
|
675
|
-
res.status(400).json({ error: cursorResult.error, items: [] });
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
if (cursorResult.cursor)
|
|
679
|
-
args.push("--after", cursorResult.cursor);
|
|
680
|
-
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
681
|
-
if (!result.ok && result.exitCode === EXIT_CODE.USAGE) {
|
|
682
|
-
res.status(400).json({ error: result.stderr, items: [] });
|
|
854
|
+
const result = await runCursorList(res, project, ["list"], [
|
|
855
|
+
["--status", status],
|
|
856
|
+
["--type", type],
|
|
857
|
+
["--limit", limit],
|
|
858
|
+
["--priority", priority],
|
|
859
|
+
["--sprint", sprint],
|
|
860
|
+
["--release", release],
|
|
861
|
+
["--assignee", assignee],
|
|
862
|
+
], after);
|
|
863
|
+
if (!result)
|
|
683
864
|
return;
|
|
684
|
-
}
|
|
685
865
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr, items: [] });
|
|
686
866
|
});
|
|
687
867
|
// GET /api/projects/:projectId/pm/list-all
|
|
688
868
|
router.get("/list-all", async (req, res) => {
|
|
689
|
-
const project = await
|
|
690
|
-
if (!project)
|
|
691
|
-
res.status(404).json({ error: "Project not found" });
|
|
869
|
+
const project = await requireProject(req, res);
|
|
870
|
+
if (!project)
|
|
692
871
|
return;
|
|
693
|
-
}
|
|
694
872
|
const { type, limit, after } = req.query;
|
|
695
873
|
// Preserve the public HTTP compatibility route while invoking the canonical
|
|
696
874
|
// CLI/SDK command internally. This route is intentionally paginated and is
|
|
697
875
|
// therefore distinct from readCompletePmItems used by whole-corpus views.
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const cursorResult = validateCursor(after);
|
|
704
|
-
if (cursorResult.error) {
|
|
705
|
-
res.status(400).json({ error: cursorResult.error, items: [] });
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
if (cursorResult.cursor)
|
|
709
|
-
args.push("--after", cursorResult.cursor);
|
|
710
|
-
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
711
|
-
if (!result.ok && result.exitCode === EXIT_CODE.USAGE) {
|
|
712
|
-
res.status(400).json({ error: result.stderr, items: [] });
|
|
876
|
+
const result = await runCursorList(res, project, ["list", "--all"], [
|
|
877
|
+
["--type", type],
|
|
878
|
+
["--limit", limit],
|
|
879
|
+
], after);
|
|
880
|
+
if (!result)
|
|
713
881
|
return;
|
|
714
|
-
}
|
|
715
882
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr, items: [] });
|
|
716
883
|
});
|
|
717
884
|
// GET /api/projects/:projectId/pm/board
|
|
718
885
|
// Kanban board: items grouped into columns by the workspace's runtime statuses
|
|
719
886
|
// (read live from `pm contracts`) so the board matches the installed CLI.
|
|
720
887
|
router.get("/board", async (req, res) => {
|
|
721
|
-
const project = await
|
|
722
|
-
if (!project)
|
|
723
|
-
res.status(404).json({ error: "Project not found" });
|
|
888
|
+
const project = await requireProject(req, res);
|
|
889
|
+
if (!project)
|
|
724
890
|
return;
|
|
725
|
-
|
|
726
|
-
const contracts = await runPm({ args: ["contracts", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
891
|
+
const contracts = await projectPm(project, ["contracts", "--json"], true);
|
|
727
892
|
const rt = contracts.ok && contracts.parsed
|
|
728
893
|
? contracts.parsed["runtime_schema"]
|
|
729
894
|
: undefined;
|
|
@@ -739,11 +904,9 @@ router.get("/board", async (req, res) => {
|
|
|
739
904
|
// GET /api/projects/:projectId/pm/search?q=...
|
|
740
905
|
// Full-text search over id/title/tags/body via one certified complete read.
|
|
741
906
|
router.get("/search", async (req, res) => {
|
|
742
|
-
const project = await
|
|
743
|
-
if (!project)
|
|
744
|
-
res.status(404).json({ error: "Project not found" });
|
|
907
|
+
const project = await requireProject(req, res);
|
|
908
|
+
if (!project)
|
|
745
909
|
return;
|
|
746
|
-
}
|
|
747
910
|
const query = String(req.query["q"] ?? "");
|
|
748
911
|
const listed = await readCompletePmItems(project.ownerUserId, project.slug, true);
|
|
749
912
|
if (!listed.ok) {
|
|
@@ -755,11 +918,9 @@ router.get("/search", async (req, res) => {
|
|
|
755
918
|
});
|
|
756
919
|
// POST /api/projects/:projectId/pm/create
|
|
757
920
|
router.post("/create", async (req, res) => {
|
|
758
|
-
const project = await
|
|
759
|
-
if (!project)
|
|
760
|
-
res.status(404).json({ error: "Project not found" });
|
|
921
|
+
const project = await requireProject(req, res);
|
|
922
|
+
if (!project)
|
|
761
923
|
return;
|
|
762
|
-
}
|
|
763
924
|
const { title, type, priority, description, tags, parent, deadline, assignee, sprint, release, estimate, body, acceptanceCriteria, reporter, component, severity, risk, goal, objective, environment, "blocked-by": blockedBy, "blocked-reason": blockedReason, "repro-steps": reproSteps, "expected-result": expectedResult, "actual-result": actualResult, reviewer, confidence, "why-now": whyNow, value, impact, outcome, "definition-of-ready": definitionOfReady, } = req.body;
|
|
764
925
|
if (!title?.trim()) {
|
|
765
926
|
res.status(400).json({ error: "Title is required" });
|
|
@@ -828,7 +989,7 @@ router.post("/create", async (req, res) => {
|
|
|
828
989
|
args.push("--outcome", outcome);
|
|
829
990
|
if (definitionOfReady)
|
|
830
991
|
args.push("--definition-of-ready", definitionOfReady);
|
|
831
|
-
const result = await
|
|
992
|
+
const result = await projectPm(project, args, true);
|
|
832
993
|
if (!result.ok) {
|
|
833
994
|
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to create item" });
|
|
834
995
|
return;
|
|
@@ -843,17 +1004,10 @@ router.post("/create", async (req, res) => {
|
|
|
843
1004
|
});
|
|
844
1005
|
// GET /api/projects/:projectId/pm/get/:itemId
|
|
845
1006
|
router.get("/get/:itemId", async (req, res) => {
|
|
846
|
-
const project = await
|
|
847
|
-
if (!project)
|
|
848
|
-
res.status(404).json({ error: "Project not found" });
|
|
1007
|
+
const project = await requireProject(req, res);
|
|
1008
|
+
if (!project)
|
|
849
1009
|
return;
|
|
850
|
-
|
|
851
|
-
const result = await runPm({
|
|
852
|
-
args: ["get", routeParam(req, "itemId")],
|
|
853
|
-
userId: project.ownerUserId,
|
|
854
|
-
slug: project.slug,
|
|
855
|
-
jsonOutput: true,
|
|
856
|
-
});
|
|
1010
|
+
const result = await projectPm(project, ["get", routeParam(req, "itemId")], true);
|
|
857
1011
|
if (!result.ok) {
|
|
858
1012
|
res.status(404).json({ error: "Item not found" });
|
|
859
1013
|
return;
|
|
@@ -870,11 +1024,9 @@ router.get("/get/:itemId", async (req, res) => {
|
|
|
870
1024
|
// 400 — invalid ref, or version/timestamp outside the available history range
|
|
871
1025
|
// 404 — unknown item, or item with no recorded history
|
|
872
1026
|
router.get("/at/:itemId/:ref", async (req, res) => {
|
|
873
|
-
const project = await
|
|
874
|
-
if (!project)
|
|
875
|
-
res.status(404).json({ error: "Project not found" });
|
|
1027
|
+
const project = await requireProject(req, res);
|
|
1028
|
+
if (!project)
|
|
876
1029
|
return;
|
|
877
|
-
}
|
|
878
1030
|
const itemId = routeParam(req, "itemId");
|
|
879
1031
|
const ref = routeParam(req, "ref");
|
|
880
1032
|
try {
|
|
@@ -912,11 +1064,9 @@ router.get("/at/:itemId/:ref", async (req, res) => {
|
|
|
912
1064
|
});
|
|
913
1065
|
// PATCH /api/projects/:projectId/pm/update/:itemId
|
|
914
1066
|
router.patch("/update/:itemId", async (req, res) => {
|
|
915
|
-
const project = await
|
|
916
|
-
if (!project)
|
|
917
|
-
res.status(404).json({ error: "Project not found" });
|
|
1067
|
+
const project = await requireProject(req, res);
|
|
1068
|
+
if (!project)
|
|
918
1069
|
return;
|
|
919
|
-
}
|
|
920
1070
|
const body = req.body;
|
|
921
1071
|
const args = ["update", routeParam(req, "itemId")];
|
|
922
1072
|
// String options
|
|
@@ -945,218 +1095,115 @@ router.patch("/update/:itemId", async (req, res) => {
|
|
|
945
1095
|
// Type can be set but must use --type
|
|
946
1096
|
if (body.type)
|
|
947
1097
|
args.push("--type", body.type);
|
|
948
|
-
const result = await
|
|
949
|
-
if (!result
|
|
950
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to update item" });
|
|
1098
|
+
const result = await runMutation(res, project, args, "Failed to update item");
|
|
1099
|
+
if (!result)
|
|
951
1100
|
return;
|
|
952
|
-
}
|
|
953
1101
|
// Broadcast SSE update event
|
|
954
|
-
|
|
955
|
-
type: "item-updated",
|
|
956
|
-
data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
|
|
957
|
-
});
|
|
958
|
-
scheduleGraphSync(routeParam(req, "projectId"), project, "item-updated");
|
|
1102
|
+
emitItemEvent(req, project, "item-updated");
|
|
959
1103
|
res.json(result.parsed || {});
|
|
960
1104
|
});
|
|
961
1105
|
// POST /api/projects/:projectId/pm/close/:itemId
|
|
962
1106
|
router.post("/close/:itemId", async (req, res) => {
|
|
963
|
-
const project = await
|
|
964
|
-
if (!project)
|
|
965
|
-
res.status(404).json({ error: "Project not found" });
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
const { reason } = req.body;
|
|
969
|
-
if (!reason?.trim()) {
|
|
970
|
-
res.status(400).json({ error: "Close reason is required" });
|
|
1107
|
+
const project = await requireProject(req, res);
|
|
1108
|
+
if (!project)
|
|
971
1109
|
return;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
args: ["close", routeParam(req, "itemId"), reason.trim()],
|
|
975
|
-
userId: project.ownerUserId,
|
|
976
|
-
slug: project.slug,
|
|
977
|
-
jsonOutput: true,
|
|
978
|
-
});
|
|
979
|
-
if (!result.ok) {
|
|
980
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to close item" });
|
|
1110
|
+
const result = await runReasonBodyMutation(req, res, project, "close", "Close reason is required", "Failed to close item");
|
|
1111
|
+
if (!result)
|
|
981
1112
|
return;
|
|
982
|
-
|
|
983
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
984
|
-
type: "item-closed",
|
|
985
|
-
data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
|
|
986
|
-
});
|
|
987
|
-
scheduleGraphSync(routeParam(req, "projectId"), project, "item-closed");
|
|
1113
|
+
emitItemEvent(req, project, "item-closed");
|
|
988
1114
|
res.json(result.parsed || {});
|
|
989
1115
|
});
|
|
990
1116
|
// DELETE /api/projects/:projectId/pm/delete/:itemId
|
|
991
1117
|
router.delete("/delete/:itemId", async (req, res) => {
|
|
992
|
-
const project = await
|
|
993
|
-
if (!project)
|
|
994
|
-
res.status(404).json({ error: "Project not found" });
|
|
1118
|
+
const project = await requireProject(req, res);
|
|
1119
|
+
if (!project)
|
|
995
1120
|
return;
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
args: ["delete", routeParam(req, "itemId"), "--yes"],
|
|
999
|
-
userId: project.ownerUserId,
|
|
1000
|
-
slug: project.slug,
|
|
1001
|
-
});
|
|
1002
|
-
if (!result.ok) {
|
|
1003
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to delete item" });
|
|
1121
|
+
const result = await runMutation(res, project, ["delete", routeParam(req, "itemId"), "--yes"], "Failed to delete item", false);
|
|
1122
|
+
if (!result)
|
|
1004
1123
|
return;
|
|
1005
|
-
|
|
1006
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
1007
|
-
type: "item-deleted",
|
|
1008
|
-
data: { itemId: routeParam(req, "itemId"), userId: req.user.userId },
|
|
1009
|
-
});
|
|
1010
|
-
scheduleGraphSync(routeParam(req, "projectId"), project, "item-deleted");
|
|
1124
|
+
emitItemEvent(req, project, "item-deleted");
|
|
1011
1125
|
res.json({ ok: true });
|
|
1012
1126
|
});
|
|
1013
1127
|
// POST /api/projects/:projectId/pm/comments/:itemId
|
|
1014
1128
|
router.post("/comments/:itemId", async (req, res) => {
|
|
1015
|
-
const project = await
|
|
1016
|
-
if (!project)
|
|
1017
|
-
res.status(404).json({ error: "Project not found" });
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
const { text } = req.body;
|
|
1021
|
-
if (!text?.trim()) {
|
|
1022
|
-
res.status(400).json({ error: "Comment text is required" });
|
|
1129
|
+
const project = await requireProject(req, res);
|
|
1130
|
+
if (!project)
|
|
1023
1131
|
return;
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
args: ["comments", routeParam(req, "itemId"), text.trim()],
|
|
1027
|
-
userId: project.ownerUserId,
|
|
1028
|
-
slug: project.slug,
|
|
1029
|
-
jsonOutput: true,
|
|
1030
|
-
});
|
|
1031
|
-
if (!result.ok) {
|
|
1032
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add comment" });
|
|
1132
|
+
const result = await runTextBodyMutation(req, res, project, "comments", "Comment text is required", "Failed to add comment");
|
|
1133
|
+
if (!result)
|
|
1033
1134
|
return;
|
|
1034
|
-
}
|
|
1035
1135
|
res.status(201).json(result.parsed || { ok: true });
|
|
1036
1136
|
});
|
|
1037
1137
|
// GET /api/projects/:projectId/pm/comments/:itemId
|
|
1038
1138
|
router.get("/comments/:itemId", async (req, res) => {
|
|
1039
|
-
const project = await
|
|
1040
|
-
if (!project)
|
|
1041
|
-
res.status(404).json({ error: "Project not found" });
|
|
1139
|
+
const project = await requireProject(req, res);
|
|
1140
|
+
if (!project)
|
|
1042
1141
|
return;
|
|
1043
|
-
|
|
1044
|
-
const result = await runPm({
|
|
1045
|
-
args: ["comments", routeParam(req, "itemId")],
|
|
1046
|
-
userId: project.ownerUserId,
|
|
1047
|
-
slug: project.slug,
|
|
1048
|
-
jsonOutput: true,
|
|
1049
|
-
});
|
|
1142
|
+
const result = await projectPm(project, ["comments", routeParam(req, "itemId")], true);
|
|
1050
1143
|
res.json(result.ok ? (result.parsed || {}) : { comments: [] });
|
|
1051
1144
|
});
|
|
1052
1145
|
// GET /api/projects/:projectId/pm/notes/:itemId
|
|
1053
1146
|
router.get("/notes/:itemId", async (req, res) => {
|
|
1054
|
-
const project = await
|
|
1055
|
-
if (!project)
|
|
1056
|
-
res.status(404).json({ error: "Project not found" });
|
|
1147
|
+
const project = await requireProject(req, res);
|
|
1148
|
+
if (!project)
|
|
1057
1149
|
return;
|
|
1058
|
-
|
|
1059
|
-
const result = await runPm({
|
|
1060
|
-
args: ["notes", routeParam(req, "itemId")],
|
|
1061
|
-
userId: project.ownerUserId,
|
|
1062
|
-
slug: project.slug,
|
|
1063
|
-
jsonOutput: true,
|
|
1064
|
-
});
|
|
1150
|
+
const result = await projectPm(project, ["notes", routeParam(req, "itemId")], true);
|
|
1065
1151
|
res.json(result.ok ? (result.parsed || {}) : { notes: [] });
|
|
1066
1152
|
});
|
|
1067
1153
|
// POST /api/projects/:projectId/pm/notes/:itemId
|
|
1068
1154
|
router.post("/notes/:itemId", async (req, res) => {
|
|
1069
|
-
const project = await
|
|
1070
|
-
if (!project)
|
|
1071
|
-
res.status(404).json({ error: "Project not found" });
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
const { text } = req.body;
|
|
1075
|
-
if (!text?.trim()) {
|
|
1076
|
-
res.status(400).json({ error: "Note text is required" });
|
|
1155
|
+
const project = await requireProject(req, res);
|
|
1156
|
+
if (!project)
|
|
1077
1157
|
return;
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
args: ["notes", routeParam(req, "itemId"), text.trim()],
|
|
1081
|
-
userId: project.ownerUserId,
|
|
1082
|
-
slug: project.slug,
|
|
1083
|
-
jsonOutput: true,
|
|
1084
|
-
});
|
|
1085
|
-
if (!result.ok) {
|
|
1086
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add note" });
|
|
1158
|
+
const result = await runTextBodyMutation(req, res, project, "notes", "Note text is required", "Failed to add note");
|
|
1159
|
+
if (!result)
|
|
1087
1160
|
return;
|
|
1088
|
-
}
|
|
1089
1161
|
res.status(201).json(result.parsed || { ok: true });
|
|
1090
1162
|
});
|
|
1091
1163
|
// GET /api/projects/:projectId/pm/context
|
|
1092
1164
|
router.get("/context", async (req, res) => {
|
|
1093
|
-
const project = await
|
|
1094
|
-
if (!project)
|
|
1095
|
-
res.status(404).json({ error: "Project not found" });
|
|
1165
|
+
const project = await requireProject(req, res);
|
|
1166
|
+
if (!project)
|
|
1096
1167
|
return;
|
|
1097
|
-
}
|
|
1098
1168
|
const { depth } = req.query;
|
|
1099
1169
|
const validDepths = ["brief", "standard", "deep"];
|
|
1100
1170
|
const resolvedDepth = validDepths.includes(depth) ? depth : "standard";
|
|
1101
|
-
const result = await
|
|
1102
|
-
args: ["context", "--depth", resolvedDepth],
|
|
1103
|
-
userId: project.ownerUserId,
|
|
1104
|
-
slug: project.slug,
|
|
1105
|
-
jsonOutput: true,
|
|
1106
|
-
});
|
|
1171
|
+
const result = await projectPm(project, ["context", "--depth", resolvedDepth], true);
|
|
1107
1172
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1108
1173
|
});
|
|
1109
1174
|
// GET /api/projects/:projectId/pm/activity
|
|
1110
1175
|
router.get("/activity", async (req, res) => {
|
|
1111
|
-
const project = await
|
|
1112
|
-
if (!project)
|
|
1113
|
-
res.status(404).json({ error: "Project not found" });
|
|
1176
|
+
const project = await requireProject(req, res);
|
|
1177
|
+
if (!project)
|
|
1114
1178
|
return;
|
|
1115
|
-
}
|
|
1116
1179
|
const { limit } = req.query;
|
|
1117
1180
|
const args = ["activity"];
|
|
1118
1181
|
if (limit)
|
|
1119
1182
|
args.push("--limit", limit);
|
|
1120
|
-
const result = await
|
|
1183
|
+
const result = await projectPm(project, args, true);
|
|
1121
1184
|
res.json(result.ok ? (result.parsed || {}) : { activity: [] });
|
|
1122
1185
|
});
|
|
1123
1186
|
// GET /api/projects/:projectId/pm/stats
|
|
1124
1187
|
router.get("/stats", async (req, res) => {
|
|
1125
|
-
const project = await
|
|
1126
|
-
if (!project)
|
|
1127
|
-
res.status(404).json({ error: "Project not found" });
|
|
1188
|
+
const project = await requireProject(req, res);
|
|
1189
|
+
if (!project)
|
|
1128
1190
|
return;
|
|
1129
|
-
|
|
1130
|
-
const result = await runPm({
|
|
1131
|
-
args: ["stats"],
|
|
1132
|
-
userId: project.ownerUserId,
|
|
1133
|
-
slug: project.slug,
|
|
1134
|
-
jsonOutput: true,
|
|
1135
|
-
});
|
|
1191
|
+
const result = await projectPm(project, ["stats"], true);
|
|
1136
1192
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1137
1193
|
});
|
|
1138
1194
|
// GET /api/projects/:projectId/pm/aggregate
|
|
1139
1195
|
router.get("/aggregate", async (req, res) => {
|
|
1140
|
-
const project = await
|
|
1141
|
-
if (!project)
|
|
1142
|
-
res.status(404).json({ error: "Project not found" });
|
|
1196
|
+
const project = await requireProject(req, res);
|
|
1197
|
+
if (!project)
|
|
1143
1198
|
return;
|
|
1144
|
-
|
|
1145
|
-
const result = await runPm({
|
|
1146
|
-
args: ["aggregate"],
|
|
1147
|
-
userId: project.ownerUserId,
|
|
1148
|
-
slug: project.slug,
|
|
1149
|
-
jsonOutput: true,
|
|
1150
|
-
});
|
|
1199
|
+
const result = await projectPm(project, ["aggregate"], true);
|
|
1151
1200
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1152
1201
|
});
|
|
1153
1202
|
// POST /api/projects/:projectId/pm/search
|
|
1154
1203
|
router.post("/search", async (req, res) => {
|
|
1155
|
-
const project = await
|
|
1156
|
-
if (!project)
|
|
1157
|
-
res.status(404).json({ error: "Project not found" });
|
|
1204
|
+
const project = await requireProject(req, res);
|
|
1205
|
+
if (!project)
|
|
1158
1206
|
return;
|
|
1159
|
-
}
|
|
1160
1207
|
const body = req.body;
|
|
1161
1208
|
const query = typeof body["query"] === "string" ? body["query"] : "";
|
|
1162
1209
|
const mode = typeof body["mode"] === "string" ? body["mode"] : "";
|
|
@@ -1184,12 +1231,7 @@ router.post("/search", async (req, res) => {
|
|
|
1184
1231
|
if (cursorResult.cursor)
|
|
1185
1232
|
args.push("--after", cursorResult.cursor);
|
|
1186
1233
|
args.push("--", ...query.trim().split(/\s+/));
|
|
1187
|
-
const result = await
|
|
1188
|
-
args,
|
|
1189
|
-
userId: project.ownerUserId,
|
|
1190
|
-
slug: project.slug,
|
|
1191
|
-
jsonOutput: true,
|
|
1192
|
-
});
|
|
1234
|
+
const result = await projectPm(project, args, true);
|
|
1193
1235
|
if (!result.ok) {
|
|
1194
1236
|
res.status(pmErrorStatus(result)).json({
|
|
1195
1237
|
error: result.stderr || "Search failed. Check that Ollama is reachable and the configured embedding model is available.",
|
|
@@ -1201,17 +1243,10 @@ router.post("/search", async (req, res) => {
|
|
|
1201
1243
|
});
|
|
1202
1244
|
// GET /api/projects/:projectId/pm/calendar
|
|
1203
1245
|
router.get("/calendar", async (req, res) => {
|
|
1204
|
-
const project = await
|
|
1205
|
-
if (!project)
|
|
1206
|
-
res.status(404).json({ error: "Project not found" });
|
|
1246
|
+
const project = await requireProject(req, res);
|
|
1247
|
+
if (!project)
|
|
1207
1248
|
return;
|
|
1208
|
-
|
|
1209
|
-
const result = await runPm({
|
|
1210
|
-
args: ["calendar", "--view", "month"],
|
|
1211
|
-
userId: project.ownerUserId,
|
|
1212
|
-
slug: project.slug,
|
|
1213
|
-
jsonOutput: true,
|
|
1214
|
-
});
|
|
1249
|
+
const result = await projectPm(project, ["calendar", "--view", "month"], true);
|
|
1215
1250
|
res.json(result.ok ? (result.parsed || {}) : { events: [] });
|
|
1216
1251
|
});
|
|
1217
1252
|
// GET /api/projects/:projectId/pm/calendar.ics
|
|
@@ -1220,11 +1255,9 @@ router.get("/calendar", async (req, res) => {
|
|
|
1220
1255
|
// Auth works via the usual token (header/cookie) or a
|
|
1221
1256
|
// `?token=` query param, since calendar clients cannot send cookies.
|
|
1222
1257
|
router.get("/calendar.ics", async (req, res) => {
|
|
1223
|
-
const project = await
|
|
1224
|
-
if (!project)
|
|
1225
|
-
res.status(404).json({ error: "Project not found" });
|
|
1258
|
+
const project = await requireProject(req, res);
|
|
1259
|
+
if (!project)
|
|
1226
1260
|
return;
|
|
1227
|
-
}
|
|
1228
1261
|
const listed = await readCompletePmItems(project.ownerUserId, project.slug);
|
|
1229
1262
|
if (!listed.ok) {
|
|
1230
1263
|
res.status(502).json({ error: listed.stderr || "Failed to load items" });
|
|
@@ -1253,211 +1286,102 @@ router.get("/calendar.ics", async (req, res) => {
|
|
|
1253
1286
|
});
|
|
1254
1287
|
// GET /api/projects/:projectId/pm/health
|
|
1255
1288
|
router.get("/health", async (req, res) => {
|
|
1256
|
-
const project = await
|
|
1257
|
-
if (!project)
|
|
1258
|
-
res.status(404).json({ error: "Project not found" });
|
|
1289
|
+
const project = await requireProject(req, res);
|
|
1290
|
+
if (!project)
|
|
1259
1291
|
return;
|
|
1260
|
-
|
|
1261
|
-
const result = await runPm({
|
|
1262
|
-
args: ["health"],
|
|
1263
|
-
userId: project.ownerUserId,
|
|
1264
|
-
slug: project.slug,
|
|
1265
|
-
jsonOutput: true,
|
|
1266
|
-
});
|
|
1292
|
+
const result = await projectPm(project, ["health"], true);
|
|
1267
1293
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1268
1294
|
});
|
|
1269
1295
|
// POST /api/projects/:projectId/pm/append/:itemId
|
|
1270
1296
|
router.post("/append/:itemId", async (req, res) => {
|
|
1271
|
-
const project = await
|
|
1272
|
-
if (!project)
|
|
1273
|
-
res.status(404).json({ error: "Project not found" });
|
|
1297
|
+
const project = await requireProject(req, res);
|
|
1298
|
+
if (!project)
|
|
1274
1299
|
return;
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
if (!text?.trim()) {
|
|
1278
|
-
res.status(400).json({ error: "Text is required" });
|
|
1300
|
+
const result = await runTextBodyMutation(req, res, project, "append", "Text is required", "Failed to append");
|
|
1301
|
+
if (!result)
|
|
1279
1302
|
return;
|
|
1280
|
-
}
|
|
1281
|
-
const result = await runPm({
|
|
1282
|
-
args: ["append", routeParam(req, "itemId"), text.trim()],
|
|
1283
|
-
userId: project.ownerUserId,
|
|
1284
|
-
slug: project.slug,
|
|
1285
|
-
jsonOutput: true,
|
|
1286
|
-
});
|
|
1287
|
-
if (!result.ok) {
|
|
1288
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to append" });
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
1303
|
scheduleGraphSync(routeParam(req, "projectId"), project, "item-appended");
|
|
1292
1304
|
res.json(result.parsed || { ok: true });
|
|
1293
1305
|
});
|
|
1294
1306
|
// GET /api/projects/:projectId/pm/history/:itemId
|
|
1295
1307
|
router.get("/history/:itemId", async (req, res) => {
|
|
1296
|
-
const project = await
|
|
1297
|
-
if (!project)
|
|
1298
|
-
res.status(404).json({ error: "Project not found" });
|
|
1308
|
+
const project = await requireProject(req, res);
|
|
1309
|
+
if (!project)
|
|
1299
1310
|
return;
|
|
1300
|
-
|
|
1301
|
-
const result = await runPm({
|
|
1302
|
-
args: ["history", routeParam(req, "itemId")],
|
|
1303
|
-
userId: project.ownerUserId,
|
|
1304
|
-
slug: project.slug,
|
|
1305
|
-
jsonOutput: true,
|
|
1306
|
-
});
|
|
1311
|
+
const result = await projectPm(project, ["history", routeParam(req, "itemId")], true);
|
|
1307
1312
|
res.json(result.ok ? (result.parsed || {}) : { history: [] });
|
|
1308
1313
|
});
|
|
1309
1314
|
// GET /api/projects/:projectId/pm/deps/:itemId
|
|
1310
1315
|
router.get("/deps/:itemId", async (req, res) => {
|
|
1311
|
-
const project = await
|
|
1312
|
-
if (!project)
|
|
1313
|
-
res.status(404).json({ error: "Project not found" });
|
|
1316
|
+
const project = await requireProject(req, res);
|
|
1317
|
+
if (!project)
|
|
1314
1318
|
return;
|
|
1315
|
-
|
|
1316
|
-
const result = await runPm({
|
|
1317
|
-
args: ["deps", routeParam(req, "itemId")],
|
|
1318
|
-
userId: project.ownerUserId,
|
|
1319
|
-
slug: project.slug,
|
|
1320
|
-
jsonOutput: true,
|
|
1321
|
-
});
|
|
1319
|
+
const result = await projectPm(project, ["deps", routeParam(req, "itemId")], true);
|
|
1322
1320
|
res.json(result.ok ? (result.parsed || {}) : { deps: [] });
|
|
1323
1321
|
});
|
|
1324
1322
|
// POST /api/projects/:projectId/pm/deps/:itemId
|
|
1325
1323
|
router.post("/deps/:itemId", async (req, res) => {
|
|
1326
|
-
const project = await
|
|
1327
|
-
if (!project)
|
|
1328
|
-
res.status(404).json({ error: "Project not found" });
|
|
1324
|
+
const project = await requireProject(req, res);
|
|
1325
|
+
if (!project)
|
|
1329
1326
|
return;
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
if (!targetId?.trim()) {
|
|
1333
|
-
res.status(400).json({ error: "targetId is required" });
|
|
1327
|
+
const dep = parseDepBody(req, res);
|
|
1328
|
+
if (!dep)
|
|
1334
1329
|
return;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
const result = await runPm({
|
|
1338
|
-
args: ["update", routeParam(req, "itemId"), "--dep", `id=${targetId.trim()},kind=${depRel}`],
|
|
1339
|
-
userId: project.ownerUserId,
|
|
1340
|
-
slug: project.slug,
|
|
1341
|
-
jsonOutput: true,
|
|
1342
|
-
});
|
|
1343
|
-
if (!result.ok) {
|
|
1344
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add dependency" });
|
|
1330
|
+
const result = await runMutation(res, project, ["update", routeParam(req, "itemId"), "--dep", `id=${dep.targetId},kind=${dep.depRel}`], "Failed to add dependency");
|
|
1331
|
+
if (!result)
|
|
1345
1332
|
return;
|
|
1346
|
-
|
|
1347
|
-
scheduleGraphSync(routeParam(req, "projectId"), project, "dependency-added");
|
|
1348
|
-
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-added", {
|
|
1349
|
-
from: routeParam(req, "itemId"),
|
|
1350
|
-
to: targetId.trim(),
|
|
1351
|
-
rel: depRel,
|
|
1352
|
-
userId: req.user.userId,
|
|
1353
|
-
});
|
|
1333
|
+
emitDependencyEvent(req, project, "dependency-added", "dependency-added", routeParam(req, "itemId"), dep.targetId, dep.depRel);
|
|
1354
1334
|
res.status(201).json(result.parsed || { ok: true });
|
|
1355
1335
|
});
|
|
1356
1336
|
// DELETE /api/projects/:projectId/pm/deps/:itemId
|
|
1357
1337
|
router.delete("/deps/:itemId", async (req, res) => {
|
|
1358
|
-
const project = await
|
|
1359
|
-
if (!project)
|
|
1360
|
-
res.status(404).json({ error: "Project not found" });
|
|
1338
|
+
const project = await requireProject(req, res);
|
|
1339
|
+
if (!project)
|
|
1361
1340
|
return;
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
if (!targetId?.trim()) {
|
|
1365
|
-
res.status(400).json({ error: "targetId is required" });
|
|
1341
|
+
const dep = parseDepBody(req, res, "relates_to");
|
|
1342
|
+
if (!dep)
|
|
1366
1343
|
return;
|
|
1367
|
-
}
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
const result = await runPm({
|
|
1371
|
-
args: ["update", routeParam(req, "itemId"), "--dep-remove", selector],
|
|
1372
|
-
userId: project.ownerUserId,
|
|
1373
|
-
slug: project.slug,
|
|
1374
|
-
jsonOutput: true,
|
|
1375
|
-
});
|
|
1376
|
-
if (!result.ok) {
|
|
1377
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to remove dependency" });
|
|
1344
|
+
const selector = `id=${dep.targetId},kind=${dep.depRel}`;
|
|
1345
|
+
const result = await runMutation(res, project, ["update", routeParam(req, "itemId"), "--dep-remove", selector], "Failed to remove dependency");
|
|
1346
|
+
if (!result)
|
|
1378
1347
|
return;
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-removed", {
|
|
1382
|
-
from: routeParam(req, "itemId"),
|
|
1383
|
-
to: targetId.trim(),
|
|
1384
|
-
rel: depRel,
|
|
1385
|
-
userId: req.user.userId,
|
|
1386
|
-
});
|
|
1387
|
-
res.status(200).json({ ok: true, from: routeParam(req, "itemId"), to: targetId.trim(), type: depRel, result: result.parsed || null });
|
|
1348
|
+
emitDependencyEvent(req, project, "dependency-removed", "dependency-removed", routeParam(req, "itemId"), dep.targetId, dep.depRel);
|
|
1349
|
+
res.status(200).json({ ok: true, from: routeParam(req, "itemId"), to: dep.targetId, type: dep.depRel, result: result.parsed || null });
|
|
1388
1350
|
});
|
|
1389
1351
|
// POST /api/projects/:projectId/pm/rel — Create a relationship between two items
|
|
1390
1352
|
router.post("/rel", async (req, res) => {
|
|
1391
|
-
const project = await
|
|
1392
|
-
if (!project)
|
|
1393
|
-
res.status(404).json({ error: "Project not found" });
|
|
1353
|
+
const project = await requireProject(req, res);
|
|
1354
|
+
if (!project)
|
|
1394
1355
|
return;
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
if (!from?.trim() || !to?.trim()) {
|
|
1398
|
-
res.status(400).json({ error: "from and to item IDs are required" });
|
|
1356
|
+
const rel = parseRelBody(req, res);
|
|
1357
|
+
if (!rel)
|
|
1399
1358
|
return;
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
const result = await runPm({
|
|
1403
|
-
args: ["update", from.trim(), "--dep", `id=${to.trim()},kind=${depRel}`],
|
|
1404
|
-
userId: project.ownerUserId,
|
|
1405
|
-
slug: project.slug,
|
|
1406
|
-
jsonOutput: true,
|
|
1407
|
-
});
|
|
1408
|
-
if (!result.ok) {
|
|
1409
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to create relationship" });
|
|
1359
|
+
const result = await runMutation(res, project, ["update", rel.from, "--dep", `id=${rel.to},kind=${rel.depRel}`], "Failed to create relationship");
|
|
1360
|
+
if (!result)
|
|
1410
1361
|
return;
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-added", {
|
|
1414
|
-
from: from.trim(),
|
|
1415
|
-
to: to.trim(),
|
|
1416
|
-
rel: depRel,
|
|
1417
|
-
userId: req.user.userId,
|
|
1418
|
-
});
|
|
1419
|
-
res.status(201).json({ ok: true, from: from.trim(), to: to.trim(), type: depRel });
|
|
1362
|
+
emitDependencyEvent(req, project, "rel-created", "dependency-added", rel.from, rel.to, rel.depRel);
|
|
1363
|
+
res.status(201).json({ ok: true, from: rel.from, to: rel.to, type: rel.depRel });
|
|
1420
1364
|
});
|
|
1421
1365
|
// DELETE /api/projects/:projectId/pm/rel — Remove a relationship between two items
|
|
1422
1366
|
router.delete("/rel", async (req, res) => {
|
|
1423
|
-
const project = await
|
|
1424
|
-
if (!project)
|
|
1425
|
-
res.status(404).json({ error: "Project not found" });
|
|
1367
|
+
const project = await requireProject(req, res);
|
|
1368
|
+
if (!project)
|
|
1426
1369
|
return;
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
if (!from?.trim() || !to?.trim()) {
|
|
1430
|
-
res.status(400).json({ error: "from and to item IDs are required" });
|
|
1370
|
+
const rel = parseRelBody(req, res);
|
|
1371
|
+
if (!rel)
|
|
1431
1372
|
return;
|
|
1432
|
-
}
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
const result = await runPm({
|
|
1436
|
-
args: ["update", from.trim(), "--dep-remove", selector, "--message", `Remove ${depRel} dependency on ${to.trim()}`],
|
|
1437
|
-
userId: project.ownerUserId,
|
|
1438
|
-
slug: project.slug,
|
|
1439
|
-
jsonOutput: true,
|
|
1440
|
-
});
|
|
1441
|
-
if (!result.ok) {
|
|
1442
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to remove relationship" });
|
|
1373
|
+
const selector = `id=${rel.to},kind=${rel.depRel}`;
|
|
1374
|
+
const result = await runMutation(res, project, ["update", rel.from, "--dep-remove", selector, "--message", `Remove ${rel.depRel} dependency on ${rel.to}`], "Failed to remove relationship");
|
|
1375
|
+
if (!result)
|
|
1443
1376
|
return;
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
broadcastDependencyEvent(routeParam(req, "projectId"), "dependency-removed", {
|
|
1447
|
-
from: from.trim(),
|
|
1448
|
-
to: to.trim(),
|
|
1449
|
-
rel: depRel,
|
|
1450
|
-
userId: req.user.userId,
|
|
1451
|
-
});
|
|
1452
|
-
res.json({ ok: true, from: from.trim(), to: to.trim(), type: depRel, result: result.parsed || null });
|
|
1377
|
+
emitDependencyEvent(req, project, "rel-removed", "dependency-removed", rel.from, rel.to, rel.depRel);
|
|
1378
|
+
res.json({ ok: true, from: rel.from, to: rel.to, type: rel.depRel, result: result.parsed || null });
|
|
1453
1379
|
});
|
|
1454
1380
|
// GET /api/projects/:projectId/pm/graph
|
|
1455
1381
|
router.get("/graph", async (req, res) => {
|
|
1456
|
-
const project = await
|
|
1457
|
-
if (!project)
|
|
1458
|
-
res.status(404).json({ error: "Project not found" });
|
|
1382
|
+
const project = await requireProject(req, res);
|
|
1383
|
+
if (!project)
|
|
1459
1384
|
return;
|
|
1460
|
-
}
|
|
1461
1385
|
const extensionGraph = await pmGraphExtensionGraphForProject(project);
|
|
1462
1386
|
if (extensionGraph.graph) {
|
|
1463
1387
|
res.json({
|
|
@@ -1481,11 +1405,9 @@ router.get("/graph", async (req, res) => {
|
|
|
1481
1405
|
});
|
|
1482
1406
|
// POST /api/projects/:projectId/pm/graph/sync
|
|
1483
1407
|
router.post("/graph/sync", async (req, res) => {
|
|
1484
|
-
const project = await
|
|
1485
|
-
if (!project)
|
|
1486
|
-
res.status(404).json({ error: "Project not found" });
|
|
1408
|
+
const project = await requireProject(req, res);
|
|
1409
|
+
if (!project)
|
|
1487
1410
|
return;
|
|
1488
|
-
}
|
|
1489
1411
|
try {
|
|
1490
1412
|
const syncResult = await syncProjectGraph(project);
|
|
1491
1413
|
const payload = { reason: "manual-sync", ...syncResult, projectKey: graphProjectKey(project), source: "pm-web" };
|
|
@@ -1515,22 +1437,15 @@ router.post("/graph/sync", async (req, res) => {
|
|
|
1515
1437
|
});
|
|
1516
1438
|
// GET /api/projects/:projectId/pm/graph/neighbors/:nodeId
|
|
1517
1439
|
router.get("/graph/neighbors/:nodeId", async (req, res) => {
|
|
1518
|
-
const project = await
|
|
1519
|
-
if (!project)
|
|
1520
|
-
res.status(404).json({ error: "Project not found" });
|
|
1440
|
+
const project = await requireProject(req, res);
|
|
1441
|
+
if (!project)
|
|
1521
1442
|
return;
|
|
1522
|
-
}
|
|
1523
1443
|
const nodeId = routeParam(req, "nodeId");
|
|
1524
1444
|
if (!nodeId) {
|
|
1525
1445
|
res.status(400).json({ error: "nodeId is required" });
|
|
1526
1446
|
return;
|
|
1527
1447
|
}
|
|
1528
|
-
const result = await
|
|
1529
|
-
args: ["pm-graph", "neighbors", nodeId, "--json"],
|
|
1530
|
-
userId: project.ownerUserId,
|
|
1531
|
-
slug: project.slug,
|
|
1532
|
-
jsonOutput: false,
|
|
1533
|
-
});
|
|
1448
|
+
const result = await projectPm(project, ["pm-graph", "neighbors", nodeId, "--json"], false);
|
|
1534
1449
|
if (!result.ok) {
|
|
1535
1450
|
// Extension not available — return empty neighbors
|
|
1536
1451
|
res.json({ ok: true, center: null, neighbors: [], extensionAvailable: false, error: result.stderr || "pm-graph extension not available" });
|
|
@@ -1546,22 +1461,15 @@ router.get("/graph/neighbors/:nodeId", async (req, res) => {
|
|
|
1546
1461
|
});
|
|
1547
1462
|
// POST /api/projects/:projectId/pm/graph/query
|
|
1548
1463
|
router.post("/graph/query", async (req, res) => {
|
|
1549
|
-
const project = await
|
|
1550
|
-
if (!project)
|
|
1551
|
-
res.status(404).json({ error: "Project not found" });
|
|
1464
|
+
const project = await requireProject(req, res);
|
|
1465
|
+
if (!project)
|
|
1552
1466
|
return;
|
|
1553
|
-
}
|
|
1554
1467
|
const { cypher } = req.body;
|
|
1555
1468
|
if (!cypher?.trim()) {
|
|
1556
1469
|
res.status(400).json({ error: "cypher query is required" });
|
|
1557
1470
|
return;
|
|
1558
1471
|
}
|
|
1559
|
-
const result = await
|
|
1560
|
-
args: ["pm-graph", "query", cypher.trim(), "--json"],
|
|
1561
|
-
userId: project.ownerUserId,
|
|
1562
|
-
slug: project.slug,
|
|
1563
|
-
jsonOutput: false,
|
|
1564
|
-
});
|
|
1472
|
+
const result = await projectPm(project, ["pm-graph", "query", cypher.trim(), "--json"], false);
|
|
1565
1473
|
if (!result.ok) {
|
|
1566
1474
|
res.status(pmErrorStatus(result)).json({ error: result.stderr || "pm-graph query failed — ensure Neo4j is configured and pm-graph extension is installed" });
|
|
1567
1475
|
return;
|
|
@@ -1576,145 +1484,79 @@ router.post("/graph/query", async (req, res) => {
|
|
|
1576
1484
|
});
|
|
1577
1485
|
// GET /api/projects/:projectId/pm/learnings/:itemId
|
|
1578
1486
|
router.get("/learnings/:itemId", async (req, res) => {
|
|
1579
|
-
const project = await
|
|
1580
|
-
if (!project)
|
|
1581
|
-
res.status(404).json({ error: "Project not found" });
|
|
1487
|
+
const project = await requireProject(req, res);
|
|
1488
|
+
if (!project)
|
|
1582
1489
|
return;
|
|
1583
|
-
|
|
1584
|
-
const result = await runPm({
|
|
1585
|
-
args: ["learnings", routeParam(req, "itemId")],
|
|
1586
|
-
userId: project.ownerUserId,
|
|
1587
|
-
slug: project.slug,
|
|
1588
|
-
jsonOutput: true,
|
|
1589
|
-
});
|
|
1490
|
+
const result = await projectPm(project, ["learnings", routeParam(req, "itemId")], true);
|
|
1590
1491
|
res.json(result.ok ? (result.parsed || {}) : { learnings: [] });
|
|
1591
1492
|
});
|
|
1592
1493
|
// POST /api/projects/:projectId/pm/learnings/:itemId
|
|
1593
1494
|
router.post("/learnings/:itemId", async (req, res) => {
|
|
1594
|
-
const project = await
|
|
1595
|
-
if (!project)
|
|
1596
|
-
res.status(404).json({ error: "Project not found" });
|
|
1495
|
+
const project = await requireProject(req, res);
|
|
1496
|
+
if (!project)
|
|
1597
1497
|
return;
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
if (!text?.trim()) {
|
|
1601
|
-
res.status(400).json({ error: "Learning text is required" });
|
|
1498
|
+
const result = await runTextBodyMutation(req, res, project, "learnings", "Learning text is required", "Failed to add learning");
|
|
1499
|
+
if (!result)
|
|
1602
1500
|
return;
|
|
1603
|
-
}
|
|
1604
|
-
const result = await runPm({
|
|
1605
|
-
args: ["learnings", routeParam(req, "itemId"), text.trim()],
|
|
1606
|
-
userId: project.ownerUserId,
|
|
1607
|
-
slug: project.slug,
|
|
1608
|
-
jsonOutput: true,
|
|
1609
|
-
});
|
|
1610
|
-
if (!result.ok) {
|
|
1611
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add learning" });
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
1501
|
res.status(201).json(result.parsed || { ok: true });
|
|
1615
1502
|
});
|
|
1616
1503
|
// POST /api/projects/:projectId/pm/claim/:itemId
|
|
1617
1504
|
router.post("/claim/:itemId", async (req, res) => {
|
|
1618
|
-
const project = await
|
|
1619
|
-
if (!project)
|
|
1620
|
-
res.status(404).json({ error: "Project not found" });
|
|
1505
|
+
const project = await requireProject(req, res);
|
|
1506
|
+
if (!project)
|
|
1621
1507
|
return;
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
args: ["claim", routeParam(req, "itemId")],
|
|
1625
|
-
userId: project.ownerUserId,
|
|
1626
|
-
slug: project.slug,
|
|
1627
|
-
jsonOutput: true,
|
|
1628
|
-
});
|
|
1629
|
-
if (!result.ok) {
|
|
1630
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to claim item" });
|
|
1508
|
+
const result = await runMutation(res, project, ["claim", routeParam(req, "itemId")], "Failed to claim item");
|
|
1509
|
+
if (!result)
|
|
1631
1510
|
return;
|
|
1632
|
-
}
|
|
1633
1511
|
scheduleGraphSync(routeParam(req, "projectId"), project, "item-claimed");
|
|
1634
1512
|
res.json(result.parsed || { ok: true });
|
|
1635
1513
|
});
|
|
1636
1514
|
// POST /api/projects/:projectId/pm/release/:itemId
|
|
1637
1515
|
router.post("/release/:itemId", async (req, res) => {
|
|
1638
|
-
const project = await
|
|
1639
|
-
if (!project)
|
|
1640
|
-
res.status(404).json({ error: "Project not found" });
|
|
1516
|
+
const project = await requireProject(req, res);
|
|
1517
|
+
if (!project)
|
|
1641
1518
|
return;
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
args: ["release", routeParam(req, "itemId")],
|
|
1645
|
-
userId: project.ownerUserId,
|
|
1646
|
-
slug: project.slug,
|
|
1647
|
-
jsonOutput: true,
|
|
1648
|
-
});
|
|
1649
|
-
if (!result.ok) {
|
|
1650
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to release item" });
|
|
1519
|
+
const result = await runMutation(res, project, ["release", routeParam(req, "itemId")], "Failed to release item");
|
|
1520
|
+
if (!result)
|
|
1651
1521
|
return;
|
|
1652
|
-
}
|
|
1653
1522
|
scheduleGraphSync(routeParam(req, "projectId"), project, "item-released");
|
|
1654
1523
|
res.json(result.parsed || { ok: true });
|
|
1655
1524
|
});
|
|
1656
1525
|
// POST /api/projects/:projectId/pm/start-task/:itemId
|
|
1657
1526
|
router.post("/start-task/:itemId", async (req, res) => {
|
|
1658
|
-
const project = await
|
|
1659
|
-
if (!project)
|
|
1660
|
-
res.status(404).json({ error: "Project not found" });
|
|
1527
|
+
const project = await requireProject(req, res);
|
|
1528
|
+
if (!project)
|
|
1661
1529
|
return;
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
args: ["start-task", routeParam(req, "itemId")],
|
|
1665
|
-
userId: project.ownerUserId,
|
|
1666
|
-
slug: project.slug,
|
|
1667
|
-
jsonOutput: true,
|
|
1668
|
-
});
|
|
1669
|
-
if (!result.ok) {
|
|
1670
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to start task" });
|
|
1530
|
+
const result = await runMutation(res, project, ["start-task", routeParam(req, "itemId")], "Failed to start task");
|
|
1531
|
+
if (!result)
|
|
1671
1532
|
return;
|
|
1672
|
-
}
|
|
1673
1533
|
scheduleGraphSync(routeParam(req, "projectId"), project, "task-started");
|
|
1674
1534
|
res.json(result.parsed || { ok: true });
|
|
1675
1535
|
});
|
|
1676
1536
|
// POST /api/projects/:projectId/pm/pause-task/:itemId
|
|
1677
1537
|
router.post("/pause-task/:itemId", async (req, res) => {
|
|
1678
|
-
const project = await
|
|
1679
|
-
if (!project)
|
|
1680
|
-
res.status(404).json({ error: "Project not found" });
|
|
1538
|
+
const project = await requireProject(req, res);
|
|
1539
|
+
if (!project)
|
|
1681
1540
|
return;
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
args: ["pause-task", routeParam(req, "itemId")],
|
|
1685
|
-
userId: project.ownerUserId,
|
|
1686
|
-
slug: project.slug,
|
|
1687
|
-
jsonOutput: true,
|
|
1688
|
-
});
|
|
1689
|
-
if (!result.ok) {
|
|
1690
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to pause task" });
|
|
1541
|
+
const result = await runMutation(res, project, ["pause-task", routeParam(req, "itemId")], "Failed to pause task");
|
|
1542
|
+
if (!result)
|
|
1691
1543
|
return;
|
|
1692
|
-
}
|
|
1693
1544
|
scheduleGraphSync(routeParam(req, "projectId"), project, "task-paused");
|
|
1694
1545
|
res.json(result.parsed || { ok: true });
|
|
1695
1546
|
});
|
|
1696
1547
|
// GET /api/projects/:projectId/pm/tests/:itemId
|
|
1697
1548
|
router.get("/tests/:itemId", async (req, res) => {
|
|
1698
|
-
const project = await
|
|
1699
|
-
if (!project)
|
|
1700
|
-
res.status(404).json({ error: "Project not found" });
|
|
1549
|
+
const project = await requireProject(req, res);
|
|
1550
|
+
if (!project)
|
|
1701
1551
|
return;
|
|
1702
|
-
|
|
1703
|
-
const result = await runPm({
|
|
1704
|
-
args: ["test", routeParam(req, "itemId")],
|
|
1705
|
-
userId: project.ownerUserId,
|
|
1706
|
-
slug: project.slug,
|
|
1707
|
-
jsonOutput: true,
|
|
1708
|
-
});
|
|
1552
|
+
const result = await projectPm(project, ["test", routeParam(req, "itemId")], true);
|
|
1709
1553
|
res.json(result.ok ? (result.parsed || {}) : { tests: [] });
|
|
1710
1554
|
});
|
|
1711
1555
|
// POST /api/projects/:projectId/pm/tests/:itemId
|
|
1712
1556
|
router.post("/tests/:itemId", async (req, res) => {
|
|
1713
|
-
const project = await
|
|
1714
|
-
if (!project)
|
|
1715
|
-
res.status(404).json({ error: "Project not found" });
|
|
1557
|
+
const project = await requireProject(req, res);
|
|
1558
|
+
if (!project)
|
|
1716
1559
|
return;
|
|
1717
|
-
}
|
|
1718
1560
|
const { command, description } = req.body;
|
|
1719
1561
|
if (!command?.trim()) {
|
|
1720
1562
|
res.status(400).json({ error: "Test command is required" });
|
|
@@ -1723,113 +1565,63 @@ router.post("/tests/:itemId", async (req, res) => {
|
|
|
1723
1565
|
const args = ["test", routeParam(req, "itemId"), "--add", "--command", command.trim()];
|
|
1724
1566
|
if (description)
|
|
1725
1567
|
args.push("--description", description.trim());
|
|
1726
|
-
const result = await
|
|
1727
|
-
|
|
1728
|
-
userId: project.ownerUserId,
|
|
1729
|
-
slug: project.slug,
|
|
1730
|
-
jsonOutput: true,
|
|
1731
|
-
});
|
|
1732
|
-
if (!result.ok) {
|
|
1733
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add test" });
|
|
1568
|
+
const result = await runMutation(res, project, args, "Failed to add test");
|
|
1569
|
+
if (!result)
|
|
1734
1570
|
return;
|
|
1735
|
-
}
|
|
1736
1571
|
res.status(201).json(result.parsed || { ok: true });
|
|
1737
1572
|
});
|
|
1738
1573
|
// GET /api/projects/:projectId/pm/dedupe-audit
|
|
1739
1574
|
router.get("/dedupe-audit", async (req, res) => {
|
|
1740
|
-
const project = await
|
|
1741
|
-
if (!project)
|
|
1742
|
-
res.status(404).json({ error: "Project not found" });
|
|
1575
|
+
const project = await requireProject(req, res);
|
|
1576
|
+
if (!project)
|
|
1743
1577
|
return;
|
|
1744
|
-
|
|
1745
|
-
const result = await runPm({
|
|
1746
|
-
args: ["dedupe-audit"],
|
|
1747
|
-
userId: project.ownerUserId,
|
|
1748
|
-
slug: project.slug,
|
|
1749
|
-
jsonOutput: true,
|
|
1750
|
-
});
|
|
1578
|
+
const result = await projectPm(project, ["dedupe-audit"], true);
|
|
1751
1579
|
res.json(result.ok ? (result.parsed || {}) : { duplicates: [] });
|
|
1752
1580
|
});
|
|
1753
1581
|
// GET /api/projects/:projectId/pm/validate
|
|
1754
1582
|
router.get("/validate", async (req, res) => {
|
|
1755
|
-
const project = await
|
|
1756
|
-
if (!project)
|
|
1757
|
-
res.status(404).json({ error: "Project not found" });
|
|
1583
|
+
const project = await requireProject(req, res);
|
|
1584
|
+
if (!project)
|
|
1758
1585
|
return;
|
|
1759
|
-
|
|
1760
|
-
const result = await runPm({
|
|
1761
|
-
args: ["validate"],
|
|
1762
|
-
userId: project.ownerUserId,
|
|
1763
|
-
slug: project.slug,
|
|
1764
|
-
jsonOutput: true,
|
|
1765
|
-
});
|
|
1586
|
+
const result = await projectPm(project, ["validate"], true);
|
|
1766
1587
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1767
1588
|
});
|
|
1768
1589
|
// POST /api/projects/:projectId/pm/restore/:itemId
|
|
1769
1590
|
router.post("/restore/:itemId", async (req, res) => {
|
|
1770
|
-
const project = await
|
|
1771
|
-
if (!project)
|
|
1772
|
-
res.status(404).json({ error: "Project not found" });
|
|
1591
|
+
const project = await requireProject(req, res);
|
|
1592
|
+
if (!project)
|
|
1773
1593
|
return;
|
|
1774
|
-
}
|
|
1775
1594
|
const { target } = req.body;
|
|
1776
1595
|
if (!target?.trim()) {
|
|
1777
1596
|
res.status(400).json({ error: "Restore target (timestamp or version) is required" });
|
|
1778
1597
|
return;
|
|
1779
1598
|
}
|
|
1780
|
-
const result = await
|
|
1781
|
-
|
|
1782
|
-
userId: project.ownerUserId,
|
|
1783
|
-
slug: project.slug,
|
|
1784
|
-
jsonOutput: true,
|
|
1785
|
-
});
|
|
1786
|
-
if (!result.ok) {
|
|
1787
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to restore item" });
|
|
1599
|
+
const result = await runMutation(res, project, ["restore", routeParam(req, "itemId"), target.trim()], "Failed to restore item");
|
|
1600
|
+
if (!result)
|
|
1788
1601
|
return;
|
|
1789
|
-
}
|
|
1790
1602
|
scheduleGraphSync(routeParam(req, "projectId"), project, "item-restored");
|
|
1791
1603
|
res.json(result.parsed || { ok: true });
|
|
1792
1604
|
});
|
|
1793
1605
|
// POST /api/projects/:projectId/pm/close-task/:itemId
|
|
1794
1606
|
router.post("/close-task/:itemId", async (req, res) => {
|
|
1795
|
-
const project = await
|
|
1796
|
-
if (!project)
|
|
1797
|
-
res.status(404).json({ error: "Project not found" });
|
|
1607
|
+
const project = await requireProject(req, res);
|
|
1608
|
+
if (!project)
|
|
1798
1609
|
return;
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
if (!reason?.trim()) {
|
|
1802
|
-
res.status(400).json({ error: "Close reason is required" });
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
|
-
const result = await runPm({
|
|
1806
|
-
args: ["close-task", routeParam(req, "itemId"), reason.trim()],
|
|
1807
|
-
userId: project.ownerUserId,
|
|
1808
|
-
slug: project.slug,
|
|
1809
|
-
jsonOutput: true,
|
|
1810
|
-
});
|
|
1811
|
-
if (!result.ok) {
|
|
1812
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to close task" });
|
|
1610
|
+
const result = await runReasonBodyMutation(req, res, project, "close-task", "Close reason is required", "Failed to close task");
|
|
1611
|
+
if (!result)
|
|
1813
1612
|
return;
|
|
1814
|
-
}
|
|
1815
1613
|
scheduleGraphSync(routeParam(req, "projectId"), project, "task-closed");
|
|
1816
1614
|
res.json(result.parsed || { ok: true });
|
|
1817
1615
|
});
|
|
1818
1616
|
// POST /api/projects/:projectId/pm/reindex
|
|
1819
1617
|
router.post("/reindex", async (req, res) => {
|
|
1820
|
-
const project = await
|
|
1821
|
-
if (!project)
|
|
1822
|
-
res.status(404).json({ error: "Project not found" });
|
|
1618
|
+
const project = await requireProject(req, res);
|
|
1619
|
+
if (!project)
|
|
1823
1620
|
return;
|
|
1824
|
-
}
|
|
1825
1621
|
const { mode = "keyword" } = req.body;
|
|
1826
1622
|
const validModes = ["keyword", "semantic", "hybrid"];
|
|
1827
1623
|
const safeMode = validModes.includes(mode) ? mode : "keyword";
|
|
1828
|
-
const result = await
|
|
1829
|
-
args: ["reindex", "--mode", safeMode],
|
|
1830
|
-
userId: project.ownerUserId,
|
|
1831
|
-
slug: project.slug,
|
|
1832
|
-
});
|
|
1624
|
+
const result = await projectPm(project, ["reindex", "--mode", safeMode]);
|
|
1833
1625
|
if (!result.ok) {
|
|
1834
1626
|
res.status(400).json({
|
|
1835
1627
|
error: result.stderr || "Reindex failed. Check that Ollama is reachable and the configured embedding model is available.",
|
|
@@ -1840,41 +1632,25 @@ router.post("/reindex", async (req, res) => {
|
|
|
1840
1632
|
});
|
|
1841
1633
|
// POST /api/projects/:projectId/pm/normalize
|
|
1842
1634
|
router.post("/normalize", async (req, res) => {
|
|
1843
|
-
const project = await
|
|
1844
|
-
if (!project)
|
|
1845
|
-
res.status(404).json({ error: "Project not found" });
|
|
1635
|
+
const project = await requireProject(req, res);
|
|
1636
|
+
if (!project)
|
|
1846
1637
|
return;
|
|
1847
|
-
|
|
1848
|
-
const result = await runPm({
|
|
1849
|
-
args: ["normalize"],
|
|
1850
|
-
userId: project.ownerUserId,
|
|
1851
|
-
slug: project.slug,
|
|
1852
|
-
jsonOutput: true,
|
|
1853
|
-
});
|
|
1638
|
+
const result = await projectPm(project, ["normalize"], true);
|
|
1854
1639
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1855
1640
|
});
|
|
1856
1641
|
// GET /api/projects/:projectId/pm/comments-audit
|
|
1857
1642
|
router.get("/comments-audit", async (req, res) => {
|
|
1858
|
-
const project = await
|
|
1859
|
-
if (!project)
|
|
1860
|
-
res.status(404).json({ error: "Project not found" });
|
|
1643
|
+
const project = await requireProject(req, res);
|
|
1644
|
+
if (!project)
|
|
1861
1645
|
return;
|
|
1862
|
-
|
|
1863
|
-
const result = await runPm({
|
|
1864
|
-
args: ["comments-audit"],
|
|
1865
|
-
userId: project.ownerUserId,
|
|
1866
|
-
slug: project.slug,
|
|
1867
|
-
jsonOutput: true,
|
|
1868
|
-
});
|
|
1646
|
+
const result = await projectPm(project, ["comments-audit"], true);
|
|
1869
1647
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1870
1648
|
});
|
|
1871
1649
|
// POST /api/projects/:projectId/pm/files/:itemId
|
|
1872
1650
|
router.post("/files/:itemId", async (req, res) => {
|
|
1873
|
-
const project = await
|
|
1874
|
-
if (!project)
|
|
1875
|
-
res.status(404).json({ error: "Project not found" });
|
|
1651
|
+
const project = await requireProject(req, res);
|
|
1652
|
+
if (!project)
|
|
1876
1653
|
return;
|
|
1877
|
-
}
|
|
1878
1654
|
const { path: filePath, scope } = req.body;
|
|
1879
1655
|
if (!filePath?.trim()) {
|
|
1880
1656
|
res.status(400).json({ error: "File path is required" });
|
|
@@ -1884,62 +1660,34 @@ router.post("/files/:itemId", async (req, res) => {
|
|
|
1884
1660
|
if (scope)
|
|
1885
1661
|
addVal += `,scope=${scope}`;
|
|
1886
1662
|
const args = ["files", routeParam(req, "itemId"), "--add", addVal];
|
|
1887
|
-
const result = await
|
|
1888
|
-
|
|
1889
|
-
userId: project.ownerUserId,
|
|
1890
|
-
slug: project.slug,
|
|
1891
|
-
jsonOutput: true,
|
|
1892
|
-
});
|
|
1893
|
-
if (!result.ok) {
|
|
1894
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to link file" });
|
|
1663
|
+
const result = await runMutation(res, project, args, "Failed to link file");
|
|
1664
|
+
if (!result)
|
|
1895
1665
|
return;
|
|
1896
|
-
}
|
|
1897
1666
|
scheduleGraphSync(routeParam(req, "projectId"), project, "file-linked");
|
|
1898
1667
|
res.status(201).json(result.parsed || { ok: true });
|
|
1899
1668
|
});
|
|
1900
1669
|
// GET /api/projects/:projectId/pm/files/:itemId
|
|
1901
1670
|
router.get("/files/:itemId", async (req, res) => {
|
|
1902
|
-
const project = await
|
|
1903
|
-
if (!project)
|
|
1904
|
-
res.status(404).json({ error: "Project not found" });
|
|
1671
|
+
const project = await requireProject(req, res);
|
|
1672
|
+
if (!project)
|
|
1905
1673
|
return;
|
|
1906
|
-
|
|
1907
|
-
const result = await runPm({
|
|
1908
|
-
args: ["files", routeParam(req, "itemId")],
|
|
1909
|
-
userId: project.ownerUserId,
|
|
1910
|
-
slug: project.slug,
|
|
1911
|
-
jsonOutput: true,
|
|
1912
|
-
});
|
|
1674
|
+
const result = await projectPm(project, ["files", routeParam(req, "itemId")], true);
|
|
1913
1675
|
res.json(result.ok ? (result.parsed || {}) : { files: [] });
|
|
1914
1676
|
});
|
|
1915
1677
|
// GET /api/projects/:projectId/pm/guide — list guide topics
|
|
1916
|
-
router.get("/guide", async (req, res) => {
|
|
1917
|
-
const project = await
|
|
1918
|
-
if (!project)
|
|
1919
|
-
res.status(404).json({ error: "Project not found" });
|
|
1678
|
+
router.get("/guide", async (req, res) => {
|
|
1679
|
+
const project = await requireProject(req, res);
|
|
1680
|
+
if (!project)
|
|
1920
1681
|
return;
|
|
1921
|
-
|
|
1922
|
-
const result = await runPm({
|
|
1923
|
-
args: ["guide"],
|
|
1924
|
-
userId: project.ownerUserId,
|
|
1925
|
-
slug: project.slug,
|
|
1926
|
-
jsonOutput: true,
|
|
1927
|
-
});
|
|
1682
|
+
const result = await projectPm(project, ["guide"], true);
|
|
1928
1683
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
1929
1684
|
});
|
|
1930
1685
|
// GET /api/projects/:projectId/pm/guide/:topicId — get single guide topic
|
|
1931
1686
|
router.get("/guide/:topicId", async (req, res) => {
|
|
1932
|
-
const project = await
|
|
1933
|
-
if (!project)
|
|
1934
|
-
res.status(404).json({ error: "Project not found" });
|
|
1687
|
+
const project = await requireProject(req, res);
|
|
1688
|
+
if (!project)
|
|
1935
1689
|
return;
|
|
1936
|
-
|
|
1937
|
-
const result = await runPm({
|
|
1938
|
-
args: ["guide", routeParam(req, "topicId")],
|
|
1939
|
-
userId: project.ownerUserId,
|
|
1940
|
-
slug: project.slug,
|
|
1941
|
-
jsonOutput: true,
|
|
1942
|
-
});
|
|
1690
|
+
const result = await projectPm(project, ["guide", routeParam(req, "topicId")], true);
|
|
1943
1691
|
if (!result.ok) {
|
|
1944
1692
|
res.status(404).json({ error: result.stderr || "Topic not found" });
|
|
1945
1693
|
return;
|
|
@@ -1953,11 +1701,9 @@ router.get("/guide/:topicId", async (req, res) => {
|
|
|
1953
1701
|
// ─────────────────────────────────────────────────────────
|
|
1954
1702
|
// GET /api/projects/:projectId/pm/export?format=json|csv|yaml
|
|
1955
1703
|
router.get("/export", async (req, res) => {
|
|
1956
|
-
const project = await
|
|
1957
|
-
if (!project)
|
|
1958
|
-
res.status(404).json({ error: "Project not found" });
|
|
1704
|
+
const project = await requireProject(req, res);
|
|
1705
|
+
if (!project)
|
|
1959
1706
|
return;
|
|
1960
|
-
}
|
|
1961
1707
|
const format = req.query["format"] || "json";
|
|
1962
1708
|
const result = await readCompletePmItems(project.ownerUserId, project.slug, true);
|
|
1963
1709
|
if (!result.ok) {
|
|
@@ -2047,11 +1793,9 @@ router.get("/export", async (req, res) => {
|
|
|
2047
1793
|
});
|
|
2048
1794
|
// POST /api/projects/:projectId/pm/import — import JSON items
|
|
2049
1795
|
router.post("/import", async (req, res) => {
|
|
2050
|
-
const project = await
|
|
2051
|
-
if (!project)
|
|
2052
|
-
res.status(404).json({ error: "Project not found" });
|
|
1796
|
+
const project = await requireProject(req, res);
|
|
1797
|
+
if (!project)
|
|
2053
1798
|
return;
|
|
2054
|
-
}
|
|
2055
1799
|
const { items } = req.body;
|
|
2056
1800
|
if (!items || !Array.isArray(items) || items.length === 0) {
|
|
2057
1801
|
res.status(400).json({ error: "items array is required" });
|
|
@@ -2094,7 +1838,7 @@ router.post("/import", async (req, res) => {
|
|
|
2094
1838
|
args.push("--body", item.body);
|
|
2095
1839
|
if (item.parent)
|
|
2096
1840
|
args.push("--parent", item.parent);
|
|
2097
|
-
const result = await
|
|
1841
|
+
const result = await projectPm(project, args, true);
|
|
2098
1842
|
if (result.ok && result.parsed) {
|
|
2099
1843
|
// `pm create --json` (and the in-process SDK dispatcher) return the flat
|
|
2100
1844
|
// envelope { id, status, changed_field_count } — no `item` wrapper.
|
|
@@ -2115,11 +1859,9 @@ router.post("/import", async (req, res) => {
|
|
|
2115
1859
|
});
|
|
2116
1860
|
// POST /api/projects/:projectId/pm/update-many
|
|
2117
1861
|
router.post("/update-many", async (req, res) => {
|
|
2118
|
-
const project = await
|
|
2119
|
-
if (!project)
|
|
2120
|
-
res.status(404).json({ error: "Project not found" });
|
|
1862
|
+
const project = await requireProject(req, res);
|
|
1863
|
+
if (!project)
|
|
2121
1864
|
return;
|
|
2122
|
-
}
|
|
2123
1865
|
const body = req.body;
|
|
2124
1866
|
const args = ["update-many"];
|
|
2125
1867
|
// Filter options
|
|
@@ -2153,7 +1895,7 @@ router.post("/update-many", async (req, res) => {
|
|
|
2153
1895
|
if (body[key])
|
|
2154
1896
|
args.push(flag, body[key]);
|
|
2155
1897
|
}
|
|
2156
|
-
const result = await
|
|
1898
|
+
const result = await projectPm(project, args, true);
|
|
2157
1899
|
if (!result.ok) {
|
|
2158
1900
|
res.status(pmErrorStatus(result)).json({ error: result.stderr || "update-many failed" });
|
|
2159
1901
|
return;
|
|
@@ -2170,11 +1912,9 @@ router.post("/update-many", async (req, res) => {
|
|
|
2170
1912
|
// Accepts same filter options as update-many plus a required `reason` field.
|
|
2171
1913
|
// Returns { closed_count, failed_count, skipped_count, rows }.
|
|
2172
1914
|
router.post("/close-many", async (req, res) => {
|
|
2173
|
-
const project = await
|
|
2174
|
-
if (!project)
|
|
2175
|
-
res.status(404).json({ error: "Project not found" });
|
|
1915
|
+
const project = await requireProject(req, res);
|
|
1916
|
+
if (!project)
|
|
2176
1917
|
return;
|
|
2177
|
-
}
|
|
2178
1918
|
const body = req.body;
|
|
2179
1919
|
const reason = body.reason?.trim();
|
|
2180
1920
|
if (!reason) {
|
|
@@ -2195,7 +1935,7 @@ router.post("/close-many", async (req, res) => {
|
|
|
2195
1935
|
if (body[key])
|
|
2196
1936
|
listArgs.push(flag, body[key]);
|
|
2197
1937
|
}
|
|
2198
|
-
const listResult = await
|
|
1938
|
+
const listResult = await projectPm(project, listArgs, true);
|
|
2199
1939
|
if (!listResult.ok) {
|
|
2200
1940
|
res.status(pmErrorStatus(listResult)).json({ error: listResult.stderr || "Failed to list items for close-many" });
|
|
2201
1941
|
return;
|
|
@@ -2215,7 +1955,7 @@ router.post("/close-many", async (req, res) => {
|
|
|
2215
1955
|
const closeArgs = targetStatus === "canceled"
|
|
2216
1956
|
? ["update", itemId, "--status", "canceled"]
|
|
2217
1957
|
: ["close", itemId, reason];
|
|
2218
|
-
const closeResult = await
|
|
1958
|
+
const closeResult = await projectPm(project, closeArgs, true);
|
|
2219
1959
|
if (closeResult.ok) {
|
|
2220
1960
|
rows.push({ id: itemId, status: "ok" });
|
|
2221
1961
|
closedCount++;
|
|
@@ -2242,26 +1982,17 @@ router.post("/close-many", async (req, res) => {
|
|
|
2242
1982
|
});
|
|
2243
1983
|
// GET /api/projects/:projectId/pm/docs/:itemId
|
|
2244
1984
|
router.get("/docs/:itemId", async (req, res) => {
|
|
2245
|
-
const project = await
|
|
2246
|
-
if (!project)
|
|
2247
|
-
res.status(404).json({ error: "Project not found" });
|
|
1985
|
+
const project = await requireProject(req, res);
|
|
1986
|
+
if (!project)
|
|
2248
1987
|
return;
|
|
2249
|
-
|
|
2250
|
-
const result = await runPm({
|
|
2251
|
-
args: ["docs", routeParam(req, "itemId")],
|
|
2252
|
-
userId: project.ownerUserId,
|
|
2253
|
-
slug: project.slug,
|
|
2254
|
-
jsonOutput: true,
|
|
2255
|
-
});
|
|
1988
|
+
const result = await projectPm(project, ["docs", routeParam(req, "itemId")], true);
|
|
2256
1989
|
res.json(result.ok ? (result.parsed || {}) : { docs: [] });
|
|
2257
1990
|
});
|
|
2258
1991
|
// POST /api/projects/:projectId/pm/docs/:itemId
|
|
2259
1992
|
router.post("/docs/:itemId", async (req, res) => {
|
|
2260
|
-
const project = await
|
|
2261
|
-
if (!project)
|
|
2262
|
-
res.status(404).json({ error: "Project not found" });
|
|
1993
|
+
const project = await requireProject(req, res);
|
|
1994
|
+
if (!project)
|
|
2263
1995
|
return;
|
|
2264
|
-
}
|
|
2265
1996
|
const { path: docPath, scope, note, remove, validatePaths } = req.body;
|
|
2266
1997
|
const args = ["docs", routeParam(req, "itemId")];
|
|
2267
1998
|
if (remove) {
|
|
@@ -2282,7 +2013,7 @@ router.post("/docs/:itemId", async (req, res) => {
|
|
|
2282
2013
|
res.status(400).json({ error: "path, remove, or validatePaths is required" });
|
|
2283
2014
|
return;
|
|
2284
2015
|
}
|
|
2285
|
-
const result = await
|
|
2016
|
+
const result = await projectPm(project, args, true);
|
|
2286
2017
|
if (!result.ok) {
|
|
2287
2018
|
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to update docs" });
|
|
2288
2019
|
return;
|
|
@@ -2292,11 +2023,9 @@ router.post("/docs/:itemId", async (req, res) => {
|
|
|
2292
2023
|
});
|
|
2293
2024
|
// POST /api/projects/:projectId/pm/test-all
|
|
2294
2025
|
router.post("/test-all", async (req, res) => {
|
|
2295
|
-
const project = await
|
|
2296
|
-
if (!project)
|
|
2297
|
-
res.status(404).json({ error: "Project not found" });
|
|
2026
|
+
const project = await requireProject(req, res);
|
|
2027
|
+
if (!project)
|
|
2298
2028
|
return;
|
|
2299
|
-
}
|
|
2300
2029
|
const body = req.body;
|
|
2301
2030
|
const args = ["test-all"];
|
|
2302
2031
|
if (body.status)
|
|
@@ -2305,7 +2034,7 @@ router.post("/test-all", async (req, res) => {
|
|
|
2305
2034
|
args.push("--limit", body.limit);
|
|
2306
2035
|
if (body.timeout)
|
|
2307
2036
|
args.push("--timeout", body.timeout);
|
|
2308
|
-
const result = await
|
|
2037
|
+
const result = await projectPm(project, args, true);
|
|
2309
2038
|
if (!result.ok) {
|
|
2310
2039
|
res.status(pmErrorStatus(result)).json({ error: result.stderr || "test-all failed" });
|
|
2311
2040
|
return;
|
|
@@ -2314,78 +2043,64 @@ router.post("/test-all", async (req, res) => {
|
|
|
2314
2043
|
});
|
|
2315
2044
|
// GET /api/projects/:projectId/pm/test-runs
|
|
2316
2045
|
router.get("/test-runs", async (req, res) => {
|
|
2317
|
-
const project = await
|
|
2318
|
-
if (!project)
|
|
2319
|
-
res.status(404).json({ error: "Project not found" });
|
|
2046
|
+
const project = await requireProject(req, res);
|
|
2047
|
+
if (!project)
|
|
2320
2048
|
return;
|
|
2321
|
-
}
|
|
2322
2049
|
const { status, limit } = req.query;
|
|
2323
2050
|
const args = ["test-runs", "list"];
|
|
2324
2051
|
if (status)
|
|
2325
2052
|
args.push("--status", status);
|
|
2326
2053
|
if (limit)
|
|
2327
2054
|
args.push("--limit", limit);
|
|
2328
|
-
const result = await
|
|
2055
|
+
const result = await projectPm(project, args, true);
|
|
2329
2056
|
res.json(result.ok ? (result.parsed || {}) : { runs: [] });
|
|
2330
2057
|
});
|
|
2331
2058
|
// POST /api/projects/:projectId/pm/gc
|
|
2332
2059
|
router.post("/gc", async (req, res) => {
|
|
2333
|
-
const project = await
|
|
2334
|
-
if (!project)
|
|
2335
|
-
res.status(404).json({ error: "Project not found" });
|
|
2060
|
+
const project = await requireProject(req, res);
|
|
2061
|
+
if (!project)
|
|
2336
2062
|
return;
|
|
2337
|
-
|
|
2338
|
-
const result = await runPm({ args: ["gc"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2063
|
+
const result = await projectPm(project, ["gc"], true);
|
|
2339
2064
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
2340
2065
|
});
|
|
2341
2066
|
// GET /api/projects/:projectId/pm/templates
|
|
2342
2067
|
router.get("/templates", async (req, res) => {
|
|
2343
|
-
const project = await
|
|
2344
|
-
if (!project)
|
|
2345
|
-
res.status(404).json({ error: "Project not found" });
|
|
2068
|
+
const project = await requireProject(req, res);
|
|
2069
|
+
if (!project)
|
|
2346
2070
|
return;
|
|
2347
|
-
|
|
2348
|
-
const result = await runPm({ args: ["templates", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2071
|
+
const result = await projectPm(project, ["templates", "list"], true);
|
|
2349
2072
|
res.json(result.ok ? (result.parsed || {}) : { templates: [] });
|
|
2350
2073
|
});
|
|
2351
2074
|
// GET /api/projects/:projectId/pm/templates/:name
|
|
2352
2075
|
router.get("/templates/:name", async (req, res) => {
|
|
2353
|
-
const project = await
|
|
2354
|
-
if (!project)
|
|
2355
|
-
res.status(404).json({ error: "Project not found" });
|
|
2076
|
+
const project = await requireProject(req, res);
|
|
2077
|
+
if (!project)
|
|
2356
2078
|
return;
|
|
2357
|
-
|
|
2358
|
-
const result = await runPm({ args: ["templates", "show", routeParam(req, "name")], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2079
|
+
const result = await projectPm(project, ["templates", "show", routeParam(req, "name")], true);
|
|
2359
2080
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
2360
2081
|
});
|
|
2361
2082
|
// GET /api/projects/:projectId/pm/config
|
|
2362
2083
|
router.get("/config", async (req, res) => {
|
|
2363
|
-
const project = await
|
|
2364
|
-
if (!project)
|
|
2365
|
-
res.status(404).json({ error: "Project not found" });
|
|
2084
|
+
const project = await requireProject(req, res);
|
|
2085
|
+
if (!project)
|
|
2366
2086
|
return;
|
|
2367
|
-
|
|
2368
|
-
const result = await runPm({ args: ["config", "project", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2087
|
+
const result = await projectPm(project, ["config", "project", "list"], true);
|
|
2369
2088
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
2370
2089
|
});
|
|
2371
2090
|
// GET /api/projects/:projectId/pm/config/:key
|
|
2372
2091
|
router.get("/config/:key", async (req, res) => {
|
|
2373
|
-
const project = await
|
|
2374
|
-
if (!project)
|
|
2375
|
-
res.status(404).json({ error: "Project not found" });
|
|
2092
|
+
const project = await requireProject(req, res);
|
|
2093
|
+
if (!project)
|
|
2376
2094
|
return;
|
|
2377
|
-
}
|
|
2378
2095
|
const key = routeParam(req, "key");
|
|
2379
|
-
const result = await
|
|
2096
|
+
const result = await projectPm(project, ["config", "project", "get", key], true);
|
|
2380
2097
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
2381
2098
|
});
|
|
2382
2099
|
// PATCH /api/projects/:projectId/pm/config/:key
|
|
2383
2100
|
router.patch("/config/:key", async (req, res) => {
|
|
2384
|
-
const project = await
|
|
2385
|
-
if (!project)
|
|
2386
|
-
res.status(404).json({ error: "Project not found" });
|
|
2101
|
+
const project = await requireProject(req, res);
|
|
2102
|
+
if (!project)
|
|
2387
2103
|
return;
|
|
2388
|
-
}
|
|
2389
2104
|
const key = routeParam(req, "key");
|
|
2390
2105
|
const body = req.body;
|
|
2391
2106
|
const args = ["config", "project", "set", key];
|
|
@@ -2395,7 +2110,7 @@ router.patch("/config/:key", async (req, res) => {
|
|
|
2395
2110
|
args.push("--policy", body.policy);
|
|
2396
2111
|
if (body.format)
|
|
2397
2112
|
args.push("--format", body.format);
|
|
2398
|
-
const result = await
|
|
2113
|
+
const result = await projectPm(project, args, true);
|
|
2399
2114
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
|
|
2400
2115
|
});
|
|
2401
2116
|
// ─── List status shortcut routes ───
|
|
@@ -2414,41 +2129,22 @@ router.patch("/config/:key", async (req, res) => {
|
|
|
2414
2129
|
*/
|
|
2415
2130
|
function buildListShortcutRoute(pmCommand) {
|
|
2416
2131
|
return async (req, res) => {
|
|
2417
|
-
const project = await
|
|
2418
|
-
if (!project)
|
|
2419
|
-
res.status(404).json({ error: "Project not found" });
|
|
2132
|
+
const project = await requireProject(req, res);
|
|
2133
|
+
if (!project)
|
|
2420
2134
|
return;
|
|
2421
|
-
}
|
|
2422
2135
|
const { type, limit, offset, tag, priority, assignee, sprint, release, after } = req.query;
|
|
2423
|
-
const
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
if (assignee)
|
|
2435
|
-
args.push("--assignee", assignee);
|
|
2436
|
-
if (sprint)
|
|
2437
|
-
args.push("--sprint", sprint);
|
|
2438
|
-
if (release)
|
|
2439
|
-
args.push("--release", release);
|
|
2440
|
-
const cursorResult = validateCursor(after);
|
|
2441
|
-
if (cursorResult.error) {
|
|
2442
|
-
res.status(400).json({ error: cursorResult.error, items: [] });
|
|
2443
|
-
return;
|
|
2444
|
-
}
|
|
2445
|
-
if (cursorResult.cursor)
|
|
2446
|
-
args.push("--after", cursorResult.cursor);
|
|
2447
|
-
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2448
|
-
if (!result.ok && result.exitCode === EXIT_CODE.USAGE) {
|
|
2449
|
-
res.status(400).json({ error: result.stderr, items: [] });
|
|
2136
|
+
const result = await runCursorList(res, project, [pmCommand], [
|
|
2137
|
+
["--type", type],
|
|
2138
|
+
["--limit", limit],
|
|
2139
|
+
["--offset", offset],
|
|
2140
|
+
["--tag", tag],
|
|
2141
|
+
["--priority", priority],
|
|
2142
|
+
["--assignee", assignee],
|
|
2143
|
+
["--sprint", sprint],
|
|
2144
|
+
["--release", release],
|
|
2145
|
+
], after);
|
|
2146
|
+
if (!result)
|
|
2450
2147
|
return;
|
|
2451
|
-
}
|
|
2452
2148
|
res.json(result.ok ? (result.parsed || {}) : { items: [] });
|
|
2453
2149
|
};
|
|
2454
2150
|
}
|
|
@@ -2463,11 +2159,9 @@ router.get("/list-canceled", buildListShortcutRoute("list-canceled"));
|
|
|
2463
2159
|
// ─────────────────────────────────────────────────────────
|
|
2464
2160
|
// POST /api/projects/:projectId/pm/plan
|
|
2465
2161
|
router.post("/plan", async (req, res) => {
|
|
2466
|
-
const project = await
|
|
2467
|
-
if (!project)
|
|
2468
|
-
res.status(404).json({ error: "Project not found" });
|
|
2162
|
+
const project = await requireProject(req, res);
|
|
2163
|
+
if (!project)
|
|
2469
2164
|
return;
|
|
2470
|
-
}
|
|
2471
2165
|
const { title, description, scope, tags, priority, body } = req.body;
|
|
2472
2166
|
if (!title?.trim()) {
|
|
2473
2167
|
res.status(400).json({ error: "Title is required" });
|
|
@@ -2484,7 +2178,7 @@ router.post("/plan", async (req, res) => {
|
|
|
2484
2178
|
args.push("--priority", priority);
|
|
2485
2179
|
if (body)
|
|
2486
2180
|
args.push("--body", body);
|
|
2487
|
-
const result = await
|
|
2181
|
+
const result = await projectPm(project, args, true);
|
|
2488
2182
|
if (!result.ok) {
|
|
2489
2183
|
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to create plan" });
|
|
2490
2184
|
return;
|
|
@@ -2497,17 +2191,10 @@ router.post("/plan", async (req, res) => {
|
|
|
2497
2191
|
});
|
|
2498
2192
|
// GET /api/projects/:projectId/pm/plan/:planId
|
|
2499
2193
|
router.get("/plan/:planId", async (req, res) => {
|
|
2500
|
-
const project = await
|
|
2501
|
-
if (!project)
|
|
2502
|
-
res.status(404).json({ error: "Project not found" });
|
|
2194
|
+
const project = await requireProject(req, res);
|
|
2195
|
+
if (!project)
|
|
2503
2196
|
return;
|
|
2504
|
-
|
|
2505
|
-
const result = await runPm({
|
|
2506
|
-
args: ["plan", "show", routeParam(req, "planId"), "--depth", "standard"],
|
|
2507
|
-
userId: project.ownerUserId,
|
|
2508
|
-
slug: project.slug,
|
|
2509
|
-
jsonOutput: true,
|
|
2510
|
-
});
|
|
2197
|
+
const result = await projectPm(project, ["plan", "show", routeParam(req, "planId"), "--depth", "standard"], true);
|
|
2511
2198
|
if (!result.ok) {
|
|
2512
2199
|
res.status(404).json({ error: result.stderr || "Plan not found" });
|
|
2513
2200
|
return;
|
|
@@ -2516,41 +2203,19 @@ router.get("/plan/:planId", async (req, res) => {
|
|
|
2516
2203
|
});
|
|
2517
2204
|
// PATCH /api/projects/:projectId/pm/plan/:planId
|
|
2518
2205
|
router.patch("/plan/:planId", async (req, res) => {
|
|
2519
|
-
const project = await
|
|
2520
|
-
if (!project)
|
|
2521
|
-
res.status(404).json({ error: "Project not found" });
|
|
2206
|
+
const project = await requireProject(req, res);
|
|
2207
|
+
if (!project)
|
|
2522
2208
|
return;
|
|
2523
|
-
}
|
|
2524
|
-
const { title, description } = req.body;
|
|
2525
2209
|
const args = ["update", routeParam(req, "planId")];
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
if (description !== undefined)
|
|
2529
|
-
args.push("--description", description);
|
|
2530
|
-
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2531
|
-
if (!result.ok) {
|
|
2532
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to update plan" });
|
|
2533
|
-
return;
|
|
2534
|
-
}
|
|
2535
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2536
|
-
type: "item-updated",
|
|
2537
|
-
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2538
|
-
});
|
|
2539
|
-
res.json(result.parsed || {});
|
|
2210
|
+
pushTitleDescArgs(args, req.body);
|
|
2211
|
+
await runPlanMutation(req, res, project, args, "Failed to update plan");
|
|
2540
2212
|
});
|
|
2541
2213
|
// DELETE /api/projects/:projectId/pm/plan/:planId
|
|
2542
2214
|
router.delete("/plan/:planId", async (req, res) => {
|
|
2543
|
-
const project = await
|
|
2544
|
-
if (!project)
|
|
2545
|
-
res.status(404).json({ error: "Project not found" });
|
|
2215
|
+
const project = await requireProject(req, res);
|
|
2216
|
+
if (!project)
|
|
2546
2217
|
return;
|
|
2547
|
-
|
|
2548
|
-
const result = await runPm({
|
|
2549
|
-
args: ["delete", routeParam(req, "planId")],
|
|
2550
|
-
userId: project.ownerUserId,
|
|
2551
|
-
slug: project.slug,
|
|
2552
|
-
jsonOutput: true,
|
|
2553
|
-
});
|
|
2218
|
+
const result = await projectPm(project, ["delete", routeParam(req, "planId")], true);
|
|
2554
2219
|
if (!result.ok) {
|
|
2555
2220
|
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to delete plan" });
|
|
2556
2221
|
return;
|
|
@@ -2563,11 +2228,9 @@ router.delete("/plan/:planId", async (req, res) => {
|
|
|
2563
2228
|
});
|
|
2564
2229
|
// POST /api/projects/:projectId/pm/plan/:planId/steps
|
|
2565
2230
|
router.post("/plan/:planId/steps", async (req, res) => {
|
|
2566
|
-
const project = await
|
|
2567
|
-
if (!project)
|
|
2568
|
-
res.status(404).json({ error: "Project not found" });
|
|
2231
|
+
const project = await requireProject(req, res);
|
|
2232
|
+
if (!project)
|
|
2569
2233
|
return;
|
|
2570
|
-
}
|
|
2571
2234
|
const { title, description, dependsOn } = req.body;
|
|
2572
2235
|
if (!title?.trim()) {
|
|
2573
2236
|
res.status(400).json({ error: "Title is required" });
|
|
@@ -2578,145 +2241,55 @@ router.post("/plan/:planId/steps", async (req, res) => {
|
|
|
2578
2241
|
args.push("--description", description);
|
|
2579
2242
|
if (dependsOn)
|
|
2580
2243
|
args.push("--depends-on", dependsOn);
|
|
2581
|
-
|
|
2582
|
-
if (!result.ok) {
|
|
2583
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add step" });
|
|
2584
|
-
return;
|
|
2585
|
-
}
|
|
2586
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2587
|
-
type: "item-updated",
|
|
2588
|
-
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2589
|
-
});
|
|
2590
|
-
res.status(201).json(result.parsed || {});
|
|
2244
|
+
await runPlanMutation(req, res, project, args, "Failed to add step", 201);
|
|
2591
2245
|
});
|
|
2592
2246
|
// PATCH /api/projects/:projectId/pm/plan/:planId/steps/:stepRef
|
|
2593
2247
|
router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
2594
|
-
const project = await
|
|
2595
|
-
if (!project)
|
|
2596
|
-
res.status(404).json({ error: "Project not found" });
|
|
2248
|
+
const project = await requireProject(req, res);
|
|
2249
|
+
if (!project)
|
|
2597
2250
|
return;
|
|
2598
|
-
}
|
|
2599
|
-
const { title, description } = req.body;
|
|
2600
2251
|
const args = ["plan", "update-step", routeParam(req, "planId"), routeParam(req, "stepRef")];
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
if (description)
|
|
2604
|
-
args.push("--description", description);
|
|
2605
|
-
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2606
|
-
if (!result.ok) {
|
|
2607
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to update step" });
|
|
2608
|
-
return;
|
|
2609
|
-
}
|
|
2610
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2611
|
-
type: "item-updated",
|
|
2612
|
-
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2613
|
-
});
|
|
2614
|
-
res.json(result.parsed || {});
|
|
2252
|
+
pushTitleDescArgs(args, req.body);
|
|
2253
|
+
await runPlanMutation(req, res, project, args, "Failed to update step");
|
|
2615
2254
|
});
|
|
2616
2255
|
// POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/complete
|
|
2617
2256
|
router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
|
|
2618
|
-
const project = await
|
|
2619
|
-
if (!project)
|
|
2620
|
-
res.status(404).json({ error: "Project not found" });
|
|
2257
|
+
const project = await requireProject(req, res);
|
|
2258
|
+
if (!project)
|
|
2621
2259
|
return;
|
|
2622
|
-
|
|
2623
|
-
const result = await runPm({
|
|
2624
|
-
args: ["plan", "complete-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
|
|
2625
|
-
userId: project.ownerUserId,
|
|
2626
|
-
slug: project.slug,
|
|
2627
|
-
jsonOutput: true,
|
|
2628
|
-
});
|
|
2629
|
-
if (!result.ok) {
|
|
2630
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to complete step" });
|
|
2631
|
-
return;
|
|
2632
|
-
}
|
|
2633
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2634
|
-
type: "item-updated",
|
|
2635
|
-
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2636
|
-
});
|
|
2637
|
-
res.json(result.parsed || {});
|
|
2260
|
+
await runPlanMutation(req, res, project, ["plan", "complete-step", routeParam(req, "planId"), routeParam(req, "stepRef")], "Failed to complete step");
|
|
2638
2261
|
});
|
|
2639
2262
|
// POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/block
|
|
2640
2263
|
router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
|
|
2641
|
-
const project = await
|
|
2642
|
-
if (!project)
|
|
2643
|
-
res.status(404).json({ error: "Project not found" });
|
|
2264
|
+
const project = await requireProject(req, res);
|
|
2265
|
+
if (!project)
|
|
2644
2266
|
return;
|
|
2645
|
-
}
|
|
2646
2267
|
const { reason } = req.body;
|
|
2647
2268
|
if (!reason?.trim()) {
|
|
2648
2269
|
res.status(400).json({ error: "Block reason is required" });
|
|
2649
2270
|
return;
|
|
2650
2271
|
}
|
|
2651
|
-
|
|
2652
|
-
args: ["plan", "block-step", routeParam(req, "planId"), routeParam(req, "stepRef"), "--step-blocked-reason", reason.trim()],
|
|
2653
|
-
userId: project.ownerUserId,
|
|
2654
|
-
slug: project.slug,
|
|
2655
|
-
jsonOutput: true,
|
|
2656
|
-
});
|
|
2657
|
-
if (!result.ok) {
|
|
2658
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to block step" });
|
|
2659
|
-
return;
|
|
2660
|
-
}
|
|
2661
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2662
|
-
type: "item-updated",
|
|
2663
|
-
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2664
|
-
});
|
|
2665
|
-
res.json(result.parsed || {});
|
|
2272
|
+
await runPlanMutation(req, res, project, ["plan", "block-step", routeParam(req, "planId"), routeParam(req, "stepRef"), "--step-blocked-reason", reason.trim()], "Failed to block step");
|
|
2666
2273
|
});
|
|
2667
2274
|
// DELETE /api/projects/:projectId/pm/plan/:planId/steps/:stepRef
|
|
2668
2275
|
router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
2669
|
-
const project = await
|
|
2670
|
-
if (!project)
|
|
2671
|
-
res.status(404).json({ error: "Project not found" });
|
|
2672
|
-
return;
|
|
2673
|
-
}
|
|
2674
|
-
const result = await runPm({
|
|
2675
|
-
args: ["plan", "remove-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
|
|
2676
|
-
userId: project.ownerUserId,
|
|
2677
|
-
slug: project.slug,
|
|
2678
|
-
jsonOutput: true,
|
|
2679
|
-
});
|
|
2680
|
-
if (!result.ok) {
|
|
2681
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to remove step" });
|
|
2276
|
+
const project = await requireProject(req, res);
|
|
2277
|
+
if (!project)
|
|
2682
2278
|
return;
|
|
2683
|
-
|
|
2684
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2685
|
-
type: "item-updated",
|
|
2686
|
-
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2687
|
-
});
|
|
2688
|
-
res.json(result.parsed || {});
|
|
2279
|
+
await runPlanMutation(req, res, project, ["plan", "remove-step", routeParam(req, "planId"), routeParam(req, "stepRef")], "Failed to remove step");
|
|
2689
2280
|
});
|
|
2690
2281
|
// POST /api/projects/:projectId/pm/plan/:planId/approve
|
|
2691
2282
|
router.post("/plan/:planId/approve", async (req, res) => {
|
|
2692
|
-
const project = await
|
|
2693
|
-
if (!project)
|
|
2694
|
-
res.status(404).json({ error: "Project not found" });
|
|
2695
|
-
return;
|
|
2696
|
-
}
|
|
2697
|
-
const result = await runPm({
|
|
2698
|
-
args: ["plan", "approve", routeParam(req, "planId")],
|
|
2699
|
-
userId: project.ownerUserId,
|
|
2700
|
-
slug: project.slug,
|
|
2701
|
-
jsonOutput: true,
|
|
2702
|
-
});
|
|
2703
|
-
if (!result.ok) {
|
|
2704
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to approve plan" });
|
|
2283
|
+
const project = await requireProject(req, res);
|
|
2284
|
+
if (!project)
|
|
2705
2285
|
return;
|
|
2706
|
-
|
|
2707
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2708
|
-
type: "item-updated",
|
|
2709
|
-
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2710
|
-
});
|
|
2711
|
-
res.json(result.parsed || {});
|
|
2286
|
+
await runPlanMutation(req, res, project, ["plan", "approve", routeParam(req, "planId")], "Failed to approve plan");
|
|
2712
2287
|
});
|
|
2713
2288
|
// POST /api/projects/:projectId/pm/plan/:planId/materialize
|
|
2714
2289
|
router.post("/plan/:planId/materialize", async (req, res) => {
|
|
2715
|
-
const project = await
|
|
2716
|
-
if (!project)
|
|
2717
|
-
res.status(404).json({ error: "Project not found" });
|
|
2290
|
+
const project = await requireProject(req, res);
|
|
2291
|
+
if (!project)
|
|
2718
2292
|
return;
|
|
2719
|
-
}
|
|
2720
2293
|
const { materializeType, materializeParent, steps } = req.body;
|
|
2721
2294
|
const args = ["plan", "materialize", routeParam(req, "planId")];
|
|
2722
2295
|
if (materializeType)
|
|
@@ -2725,7 +2298,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
|
|
|
2725
2298
|
args.push("--materialize-parent", materializeParent);
|
|
2726
2299
|
if (steps)
|
|
2727
2300
|
args.push("--steps", steps);
|
|
2728
|
-
const result = await
|
|
2301
|
+
const result = await projectPm(project, args, true);
|
|
2729
2302
|
if (!result.ok) {
|
|
2730
2303
|
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to materialize plan" });
|
|
2731
2304
|
return;
|
|
@@ -2738,39 +2311,21 @@ router.post("/plan/:planId/materialize", async (req, res) => {
|
|
|
2738
2311
|
});
|
|
2739
2312
|
// POST /api/projects/:projectId/pm/plan/:planId/steps/:stepRef/reorder
|
|
2740
2313
|
router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
|
|
2741
|
-
const project = await
|
|
2742
|
-
if (!project)
|
|
2743
|
-
res.status(404).json({ error: "Project not found" });
|
|
2314
|
+
const project = await requireProject(req, res);
|
|
2315
|
+
if (!project)
|
|
2744
2316
|
return;
|
|
2745
|
-
}
|
|
2746
2317
|
const { reorderTo } = req.body;
|
|
2747
2318
|
if (reorderTo === undefined || reorderTo === null || reorderTo === "") {
|
|
2748
2319
|
res.status(400).json({ error: "reorderTo (new order integer) is required" });
|
|
2749
2320
|
return;
|
|
2750
2321
|
}
|
|
2751
|
-
|
|
2752
|
-
args: ["plan", "reorder-step", routeParam(req, "planId"), routeParam(req, "stepRef"), String(reorderTo)],
|
|
2753
|
-
userId: project.ownerUserId,
|
|
2754
|
-
slug: project.slug,
|
|
2755
|
-
jsonOutput: true,
|
|
2756
|
-
});
|
|
2757
|
-
if (!result.ok) {
|
|
2758
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to reorder step" });
|
|
2759
|
-
return;
|
|
2760
|
-
}
|
|
2761
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2762
|
-
type: "item-updated",
|
|
2763
|
-
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2764
|
-
});
|
|
2765
|
-
res.json(result.parsed || {});
|
|
2322
|
+
await runPlanMutation(req, res, project, ["plan", "reorder-step", routeParam(req, "planId"), routeParam(req, "stepRef"), String(reorderTo)], "Failed to reorder step");
|
|
2766
2323
|
});
|
|
2767
2324
|
// POST /api/projects/:projectId/pm/plan/:planId/link
|
|
2768
2325
|
router.post("/plan/:planId/link", async (req, res) => {
|
|
2769
|
-
const project = await
|
|
2770
|
-
if (!project)
|
|
2771
|
-
res.status(404).json({ error: "Project not found" });
|
|
2326
|
+
const project = await requireProject(req, res);
|
|
2327
|
+
if (!project)
|
|
2772
2328
|
return;
|
|
2773
|
-
}
|
|
2774
2329
|
const { link, linkKind, linkNote, promoteToItemDep } = req.body;
|
|
2775
2330
|
if (!link?.trim()) {
|
|
2776
2331
|
res.status(400).json({ error: "link (item id) is required" });
|
|
@@ -2783,24 +2338,13 @@ router.post("/plan/:planId/link", async (req, res) => {
|
|
|
2783
2338
|
args.push("--link-note", linkNote);
|
|
2784
2339
|
if (promoteToItemDep === "true")
|
|
2785
2340
|
args.push("--promote-to-item-dep");
|
|
2786
|
-
|
|
2787
|
-
if (!result.ok) {
|
|
2788
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to link plan" });
|
|
2789
|
-
return;
|
|
2790
|
-
}
|
|
2791
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2792
|
-
type: "item-updated",
|
|
2793
|
-
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2794
|
-
});
|
|
2795
|
-
res.status(201).json(result.parsed || {});
|
|
2341
|
+
await runPlanMutation(req, res, project, args, "Failed to link plan", 201);
|
|
2796
2342
|
});
|
|
2797
2343
|
// DELETE /api/projects/:projectId/pm/plan/:planId/link
|
|
2798
2344
|
router.delete("/plan/:planId/link", async (req, res) => {
|
|
2799
|
-
const project = await
|
|
2800
|
-
if (!project)
|
|
2801
|
-
res.status(404).json({ error: "Project not found" });
|
|
2345
|
+
const project = await requireProject(req, res);
|
|
2346
|
+
if (!project)
|
|
2802
2347
|
return;
|
|
2803
|
-
}
|
|
2804
2348
|
const { link, linkKind } = req.body;
|
|
2805
2349
|
if (!link?.trim()) {
|
|
2806
2350
|
res.status(400).json({ error: "link (item id) is required" });
|
|
@@ -2809,48 +2353,25 @@ router.delete("/plan/:planId/link", async (req, res) => {
|
|
|
2809
2353
|
const args = ["plan", "unlink", routeParam(req, "planId"), "--link", link.trim()];
|
|
2810
2354
|
if (linkKind)
|
|
2811
2355
|
args.push("--link-kind", linkKind);
|
|
2812
|
-
|
|
2813
|
-
if (!result.ok) {
|
|
2814
|
-
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to unlink plan" });
|
|
2815
|
-
return;
|
|
2816
|
-
}
|
|
2817
|
-
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
2818
|
-
type: "item-updated",
|
|
2819
|
-
data: { itemId: routeParam(req, "planId"), userId: req.user.userId },
|
|
2820
|
-
});
|
|
2821
|
-
res.json(result.parsed || {});
|
|
2356
|
+
await runPlanMutation(req, res, project, args, "Failed to unlink plan");
|
|
2822
2357
|
});
|
|
2823
2358
|
// GET /api/projects/:projectId/pm/upgrade
|
|
2824
2359
|
// Returns a dry-run preview of what upgrade would do (safe, read-only).
|
|
2825
2360
|
// POST /api/projects/:projectId/pm/upgrade
|
|
2826
2361
|
// Runs pm upgrade --packages-only (never upgrades the CLI itself via the web UI).
|
|
2827
2362
|
router.get("/upgrade", async (req, res) => {
|
|
2828
|
-
const project = await
|
|
2829
|
-
if (!project)
|
|
2830
|
-
res.status(404).json({ error: "Project not found" });
|
|
2363
|
+
const project = await requireProject(req, res);
|
|
2364
|
+
if (!project)
|
|
2831
2365
|
return;
|
|
2832
|
-
|
|
2833
|
-
const result = await runPm({
|
|
2834
|
-
args: ["upgrade", "--dry-run", "--packages-only"],
|
|
2835
|
-
userId: project.ownerUserId,
|
|
2836
|
-
slug: project.slug,
|
|
2837
|
-
jsonOutput: true,
|
|
2838
|
-
});
|
|
2366
|
+
const result = await projectPm(project, ["upgrade", "--dry-run", "--packages-only"], true);
|
|
2839
2367
|
res.json(result.ok ? (result.parsed || { dryRun: true }) : { error: result.stderr });
|
|
2840
2368
|
});
|
|
2841
2369
|
router.post("/upgrade", async (req, res) => {
|
|
2842
|
-
const project = await
|
|
2843
|
-
if (!project)
|
|
2844
|
-
res.status(404).json({ error: "Project not found" });
|
|
2370
|
+
const project = await requireProject(req, res);
|
|
2371
|
+
if (!project)
|
|
2845
2372
|
return;
|
|
2846
|
-
}
|
|
2847
2373
|
// Only allow package upgrades from the web UI — never self-upgrade the CLI binary.
|
|
2848
|
-
const result = await
|
|
2849
|
-
args: ["upgrade", "--packages-only"],
|
|
2850
|
-
userId: project.ownerUserId,
|
|
2851
|
-
slug: project.slug,
|
|
2852
|
-
jsonOutput: true,
|
|
2853
|
-
});
|
|
2374
|
+
const result = await projectPm(project, ["upgrade", "--packages-only"], true);
|
|
2854
2375
|
if (!result.ok) {
|
|
2855
2376
|
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Upgrade failed" });
|
|
2856
2377
|
return;
|
|
@@ -2882,11 +2403,9 @@ router.patch("/presence/:clientId", async (req, res) => {
|
|
|
2882
2403
|
// GET /api/projects/:projectId/pm/events
|
|
2883
2404
|
router.get("/events", async (req, res) => {
|
|
2884
2405
|
try {
|
|
2885
|
-
const project = await
|
|
2886
|
-
if (!project)
|
|
2887
|
-
res.status(404).json({ error: "Project not found" });
|
|
2406
|
+
const project = await requireProject(req, res);
|
|
2407
|
+
if (!project)
|
|
2888
2408
|
return;
|
|
2889
|
-
}
|
|
2890
2409
|
}
|
|
2891
2410
|
catch (err) {
|
|
2892
2411
|
console.error("SSE project verification failed:", err);
|
|
@@ -2935,11 +2454,9 @@ router.get("/events", async (req, res) => {
|
|
|
2935
2454
|
// ─── Presence endpoint ───
|
|
2936
2455
|
// GET /api/projects/:projectId/pm/presence
|
|
2937
2456
|
router.get("/presence", async (req, res) => {
|
|
2938
|
-
const project = await
|
|
2939
|
-
if (!project)
|
|
2940
|
-
res.status(404).json({ error: "Project not found" });
|
|
2457
|
+
const project = await requireProject(req, res);
|
|
2458
|
+
if (!project)
|
|
2941
2459
|
return;
|
|
2942
|
-
}
|
|
2943
2460
|
const users = getProjectPresence(routeParam(req, "projectId"));
|
|
2944
2461
|
res.json({ users });
|
|
2945
2462
|
});
|