@unbrained/pm-web 2026.7.26 → 2026.7.28
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 +55 -1
- package/README.md +16 -8
- package/dist/app.js +2 -0
- package/dist/app.js.map +1 -1
- package/dist/index.js +10 -7
- package/dist/index.js.map +1 -1
- package/dist/routes/extensions.d.ts +16 -0
- package/dist/routes/extensions.js +214 -0
- package/dist/routes/extensions.js.map +1 -0
- package/dist/routes/pm.js +152 -47
- package/dist/routes/pm.js.map +1 -1
- package/dist/services/package-catalog.d.ts +84 -0
- package/dist/services/package-catalog.js +194 -0
- package/dist/services/package-catalog.js.map +1 -0
- package/dist/services/pm-runner.d.ts +84 -2
- package/dist/services/pm-runner.js +354 -50
- package/dist/services/pm-runner.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +6 -8
- package/public/index.html +4 -0
- package/public/src/app.js +7 -0
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +8 -0
- package/public/src/constants.js +1 -1
- package/public/src/constants.js.map +1 -1
- package/public/src/constants.ts +1 -1
- package/public/src/i18n/de.json +24 -1
- package/public/src/i18n/en.json +24 -1
- package/public/src/i18n/es.json +24 -1
- package/public/src/i18n/zh.json +25 -2
- package/public/src/sw.ts +1 -0
- package/public/src/views/packages.js +221 -0
- package/public/src/views/packages.js.map +1 -0
- package/public/src/views/packages.ts +238 -0
- package/public/src/views/router.js +5 -0
- package/public/src/views/router.js.map +1 -1
- package/public/src/views/router.ts +3 -0
- package/public/sw.js +1 -0
package/dist/routes/pm.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import { requireAuth } from "../middleware/auth.js";
|
|
3
|
-
import { ensureGraphExtension, runPm, runGetItemAt, PmCliError, EXIT_CODE } from "../services/pm-runner.js";
|
|
3
|
+
import { ensureGraphExtension, runPm, runGetItemAt, readPmSettings, PmCliError, EXIT_CODE } from "../services/pm-runner.js";
|
|
4
|
+
// The search-tuning resolvers live only on the narrow sdk/query entrypoint — the
|
|
5
|
+
// aggregate sdk barrel documents itself as re-exporting every supported export but
|
|
6
|
+
// omits 45 of them, these three included (upstream: unbraind/pm-cli#740).
|
|
7
|
+
import { resolveSearchMaxResults, resolveSearchScoreThreshold, resolveHybridSemanticWeight, } from "@unbrained/pm-cli/sdk/query";
|
|
8
|
+
import { QUERY_CURSOR_CONTRACT } from "@unbrained/pm-cli/sdk";
|
|
4
9
|
import { boardColumns, filterItemsByQuery } from "../board.js";
|
|
5
10
|
import { buildIcsCalendar } from "../ical.js";
|
|
6
11
|
import { verifyProjectAccess } from "./projects.js";
|
|
@@ -28,6 +33,53 @@ function getNeo4jDriver() {
|
|
|
28
33
|
}
|
|
29
34
|
const router = Router({ mergeParams: true });
|
|
30
35
|
router.use(requireAuth);
|
|
36
|
+
const BASE64URL_CURSOR_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
37
|
+
/**
|
|
38
|
+
* Validate an incoming opaque pagination cursor. Returns the original cursor or
|
|
39
|
+
* `undefined` when none was supplied. Rejects cursors that exceed the SDK
|
|
40
|
+
* cursor contract's maximum length with a 400 so callers can map the failure
|
|
41
|
+
* to a proper client error instead of forwarding an oversized token to the SDK.
|
|
42
|
+
*/
|
|
43
|
+
function validateCursor(raw) {
|
|
44
|
+
if (raw === undefined || raw === null || raw === "")
|
|
45
|
+
return {};
|
|
46
|
+
const cursor = String(raw);
|
|
47
|
+
if (cursor.length > QUERY_CURSOR_CONTRACT.max_length) {
|
|
48
|
+
return {
|
|
49
|
+
error: `Pagination cursor exceeds the maximum length of ${QUERY_CURSOR_CONTRACT.max_length} characters.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (!BASE64URL_CURSOR_PATTERN.test(cursor)) {
|
|
53
|
+
return { error: "Pagination cursor must be a valid base64url token." };
|
|
54
|
+
}
|
|
55
|
+
return { cursor };
|
|
56
|
+
}
|
|
57
|
+
/** Coerce an optional request number into a finite, bounded value. */
|
|
58
|
+
function boundedNumber(raw, fallback, minimum, maximum, integer = false) {
|
|
59
|
+
const parsed = typeof raw === "number" ? raw : Number(raw);
|
|
60
|
+
if (!Number.isFinite(parsed))
|
|
61
|
+
return fallback;
|
|
62
|
+
const bounded = Math.min(maximum, Math.max(minimum, parsed));
|
|
63
|
+
return integer ? Math.trunc(bounded) : bounded;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Map a {@link PmRunResult} failure to the correct HTTP status using the pm CLI
|
|
67
|
+
* exit code surfaced by the in-process dispatcher. Expected validation failures
|
|
68
|
+
* (USAGE) and not-found (NOT_FOUND) become 4xx; anything without a recognised
|
|
69
|
+
* exit code is an unexpected runtime error and becomes 500 so it is not
|
|
70
|
+
* silently swallowed as a client error.
|
|
71
|
+
*/
|
|
72
|
+
function pmErrorStatus(result) {
|
|
73
|
+
if (result.exitCode === EXIT_CODE.NOT_FOUND)
|
|
74
|
+
return 404;
|
|
75
|
+
if (result.exitCode === EXIT_CODE.USAGE)
|
|
76
|
+
return 400;
|
|
77
|
+
if (result.exitCode === EXIT_CODE.CONFLICT)
|
|
78
|
+
return 409;
|
|
79
|
+
if (result.exitCode === EXIT_CODE.DEPENDENCY_FAILED)
|
|
80
|
+
return 424;
|
|
81
|
+
return 500;
|
|
82
|
+
}
|
|
31
83
|
const pendingGraphSyncs = new Map();
|
|
32
84
|
router.use(async (req, res, next) => {
|
|
33
85
|
if (["GET", "HEAD", "OPTIONS"].includes(req.method) || (req.method === "PATCH" && req.path.startsWith("/presence/"))) {
|
|
@@ -360,7 +412,7 @@ router.get("/list", async (req, res) => {
|
|
|
360
412
|
res.status(404).json({ error: "Project not found" });
|
|
361
413
|
return;
|
|
362
414
|
}
|
|
363
|
-
const { status, type, limit, priority, sprint, release, assignee } = req.query;
|
|
415
|
+
const { status, type, limit, priority, sprint, release, assignee, after } = req.query;
|
|
364
416
|
const args = ["list"];
|
|
365
417
|
if (status)
|
|
366
418
|
args.push("--status", status);
|
|
@@ -376,7 +428,18 @@ router.get("/list", async (req, res) => {
|
|
|
376
428
|
args.push("--release", release);
|
|
377
429
|
if (assignee)
|
|
378
430
|
args.push("--assignee", assignee);
|
|
431
|
+
const cursorResult = validateCursor(after);
|
|
432
|
+
if (cursorResult.error) {
|
|
433
|
+
res.status(400).json({ error: cursorResult.error, items: [] });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (cursorResult.cursor)
|
|
437
|
+
args.push("--after", cursorResult.cursor);
|
|
379
438
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
439
|
+
if (!result.ok && result.exitCode === EXIT_CODE.USAGE) {
|
|
440
|
+
res.status(400).json({ error: result.stderr, items: [] });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
380
443
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr, items: [] });
|
|
381
444
|
});
|
|
382
445
|
// GET /api/projects/:projectId/pm/list-all
|
|
@@ -386,13 +449,24 @@ router.get("/list-all", async (req, res) => {
|
|
|
386
449
|
res.status(404).json({ error: "Project not found" });
|
|
387
450
|
return;
|
|
388
451
|
}
|
|
389
|
-
const { type, limit } = req.query;
|
|
452
|
+
const { type, limit, after } = req.query;
|
|
390
453
|
const args = ["list-all"];
|
|
391
454
|
if (type)
|
|
392
455
|
args.push("--type", type);
|
|
393
456
|
if (limit)
|
|
394
457
|
args.push("--limit", limit);
|
|
458
|
+
const cursorResult = validateCursor(after);
|
|
459
|
+
if (cursorResult.error) {
|
|
460
|
+
res.status(400).json({ error: cursorResult.error, items: [] });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (cursorResult.cursor)
|
|
464
|
+
args.push("--after", cursorResult.cursor);
|
|
395
465
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
466
|
+
if (!result.ok && result.exitCode === EXIT_CODE.USAGE) {
|
|
467
|
+
res.status(400).json({ error: result.stderr, items: [] });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
396
470
|
res.json(result.ok ? (result.parsed || {}) : { error: result.stderr, items: [] });
|
|
397
471
|
});
|
|
398
472
|
// GET /api/projects/:projectId/pm/board
|
|
@@ -511,7 +585,7 @@ router.post("/create", async (req, res) => {
|
|
|
511
585
|
args.push("--definition-of-ready", definitionOfReady);
|
|
512
586
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
513
587
|
if (!result.ok) {
|
|
514
|
-
res.status(
|
|
588
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to create item" });
|
|
515
589
|
return;
|
|
516
590
|
}
|
|
517
591
|
// Broadcast SSE create event
|
|
@@ -628,7 +702,7 @@ router.patch("/update/:itemId", async (req, res) => {
|
|
|
628
702
|
args.push("--type", body.type);
|
|
629
703
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
630
704
|
if (!result.ok) {
|
|
631
|
-
res.status(
|
|
705
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to update item" });
|
|
632
706
|
return;
|
|
633
707
|
}
|
|
634
708
|
// Broadcast SSE update event
|
|
@@ -658,7 +732,7 @@ router.post("/close/:itemId", async (req, res) => {
|
|
|
658
732
|
jsonOutput: true,
|
|
659
733
|
});
|
|
660
734
|
if (!result.ok) {
|
|
661
|
-
res.status(
|
|
735
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to close item" });
|
|
662
736
|
return;
|
|
663
737
|
}
|
|
664
738
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -681,7 +755,7 @@ router.delete("/delete/:itemId", async (req, res) => {
|
|
|
681
755
|
slug: project.slug,
|
|
682
756
|
});
|
|
683
757
|
if (!result.ok) {
|
|
684
|
-
res.status(
|
|
758
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to delete item" });
|
|
685
759
|
return;
|
|
686
760
|
}
|
|
687
761
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -710,7 +784,7 @@ router.post("/comments/:itemId", async (req, res) => {
|
|
|
710
784
|
jsonOutput: true,
|
|
711
785
|
});
|
|
712
786
|
if (!result.ok) {
|
|
713
|
-
res.status(
|
|
787
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add comment" });
|
|
714
788
|
return;
|
|
715
789
|
}
|
|
716
790
|
res.status(201).json(result.parsed || { ok: true });
|
|
@@ -764,7 +838,7 @@ router.post("/notes/:itemId", async (req, res) => {
|
|
|
764
838
|
jsonOutput: true,
|
|
765
839
|
});
|
|
766
840
|
if (!result.ok) {
|
|
767
|
-
res.status(
|
|
841
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add note" });
|
|
768
842
|
return;
|
|
769
843
|
}
|
|
770
844
|
res.status(201).json(result.parsed || { ok: true });
|
|
@@ -838,21 +912,41 @@ router.post("/search", async (req, res) => {
|
|
|
838
912
|
res.status(404).json({ error: "Project not found" });
|
|
839
913
|
return;
|
|
840
914
|
}
|
|
841
|
-
const
|
|
915
|
+
const body = req.body;
|
|
916
|
+
const query = typeof body["query"] === "string" ? body["query"] : "";
|
|
917
|
+
const mode = typeof body["mode"] === "string" ? body["mode"] : "";
|
|
842
918
|
if (!query?.trim()) {
|
|
843
919
|
res.status(400).json({ error: "Search query is required" });
|
|
844
920
|
return;
|
|
845
921
|
}
|
|
846
922
|
const validModes = ["keyword", "semantic", "hybrid"];
|
|
847
|
-
const safeMode = validModes.includes(mode
|
|
923
|
+
const safeMode = validModes.includes(mode) ? mode : "hybrid";
|
|
924
|
+
// Adopt the sdk/query search-tuning resolvers: read the workspace settings and
|
|
925
|
+
// resolve the bounded max-results, score threshold, and hybrid semantic weight
|
|
926
|
+
// from the same defaults the pm CLI applies. Explicit per-request overrides
|
|
927
|
+
// (limit / minScore / semanticWeight) win over the workspace defaults so the
|
|
928
|
+
// browser can still narrow a page.
|
|
929
|
+
const settings = readPmSettings(project.ownerUserId, project.slug);
|
|
930
|
+
const resolvedLimit = boundedNumber(body["limit"], resolveSearchMaxResults(settings), 1, 500, true);
|
|
931
|
+
const resolvedMinScore = boundedNumber(body["minScore"], resolveSearchScoreThreshold(settings), 0, 1_000_000);
|
|
932
|
+
const resolvedSemanticWeight = boundedNumber(body["semanticWeight"], resolveHybridSemanticWeight(settings), 0, 1);
|
|
933
|
+
const cursorResult = validateCursor(body["after"]);
|
|
934
|
+
if (cursorResult.error) {
|
|
935
|
+
res.status(400).json({ error: cursorResult.error, results: [] });
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const args = ["search", "--mode", safeMode, "--limit", String(resolvedLimit), "--min-score", String(resolvedMinScore), "--semantic-weight", String(resolvedSemanticWeight)];
|
|
939
|
+
if (cursorResult.cursor)
|
|
940
|
+
args.push("--after", cursorResult.cursor);
|
|
941
|
+
args.push("--", ...query.trim().split(/\s+/));
|
|
848
942
|
const result = await runPm({
|
|
849
|
-
args
|
|
943
|
+
args,
|
|
850
944
|
userId: project.ownerUserId,
|
|
851
945
|
slug: project.slug,
|
|
852
946
|
jsonOutput: true,
|
|
853
947
|
});
|
|
854
948
|
if (!result.ok) {
|
|
855
|
-
res.status(
|
|
949
|
+
res.status(pmErrorStatus(result)).json({
|
|
856
950
|
error: result.stderr || "Search failed. Check that Ollama is reachable and the configured embedding model is available.",
|
|
857
951
|
results: [],
|
|
858
952
|
});
|
|
@@ -951,7 +1045,7 @@ router.post("/append/:itemId", async (req, res) => {
|
|
|
951
1045
|
jsonOutput: true,
|
|
952
1046
|
});
|
|
953
1047
|
if (!result.ok) {
|
|
954
|
-
res.status(
|
|
1048
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to append" });
|
|
955
1049
|
return;
|
|
956
1050
|
}
|
|
957
1051
|
scheduleGraphSync(routeParam(req, "projectId"), project, "item-appended");
|
|
@@ -1007,7 +1101,7 @@ router.post("/deps/:itemId", async (req, res) => {
|
|
|
1007
1101
|
jsonOutput: true,
|
|
1008
1102
|
});
|
|
1009
1103
|
if (!result.ok) {
|
|
1010
|
-
res.status(
|
|
1104
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add dependency" });
|
|
1011
1105
|
return;
|
|
1012
1106
|
}
|
|
1013
1107
|
scheduleGraphSync(routeParam(req, "projectId"), project, "dependency-added");
|
|
@@ -1040,7 +1134,7 @@ router.delete("/deps/:itemId", async (req, res) => {
|
|
|
1040
1134
|
jsonOutput: true,
|
|
1041
1135
|
});
|
|
1042
1136
|
if (!result.ok) {
|
|
1043
|
-
res.status(
|
|
1137
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to remove dependency" });
|
|
1044
1138
|
return;
|
|
1045
1139
|
}
|
|
1046
1140
|
scheduleGraphSync(routeParam(req, "projectId"), project, "dependency-removed");
|
|
@@ -1072,7 +1166,7 @@ router.post("/rel", async (req, res) => {
|
|
|
1072
1166
|
jsonOutput: true,
|
|
1073
1167
|
});
|
|
1074
1168
|
if (!result.ok) {
|
|
1075
|
-
res.status(
|
|
1169
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to create relationship" });
|
|
1076
1170
|
return;
|
|
1077
1171
|
}
|
|
1078
1172
|
scheduleGraphSync(routeParam(req, "projectId"), project, "rel-created");
|
|
@@ -1105,7 +1199,7 @@ router.delete("/rel", async (req, res) => {
|
|
|
1105
1199
|
jsonOutput: true,
|
|
1106
1200
|
});
|
|
1107
1201
|
if (!result.ok) {
|
|
1108
|
-
res.status(
|
|
1202
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to remove relationship" });
|
|
1109
1203
|
return;
|
|
1110
1204
|
}
|
|
1111
1205
|
scheduleGraphSync(routeParam(req, "projectId"), project, "rel-removed");
|
|
@@ -1229,7 +1323,7 @@ router.post("/graph/query", async (req, res) => {
|
|
|
1229
1323
|
jsonOutput: false,
|
|
1230
1324
|
});
|
|
1231
1325
|
if (!result.ok) {
|
|
1232
|
-
res.status(
|
|
1326
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "pm-graph query failed — ensure Neo4j is configured and pm-graph extension is installed" });
|
|
1233
1327
|
return;
|
|
1234
1328
|
}
|
|
1235
1329
|
try {
|
|
@@ -1274,7 +1368,7 @@ router.post("/learnings/:itemId", async (req, res) => {
|
|
|
1274
1368
|
jsonOutput: true,
|
|
1275
1369
|
});
|
|
1276
1370
|
if (!result.ok) {
|
|
1277
|
-
res.status(
|
|
1371
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add learning" });
|
|
1278
1372
|
return;
|
|
1279
1373
|
}
|
|
1280
1374
|
res.status(201).json(result.parsed || { ok: true });
|
|
@@ -1293,7 +1387,7 @@ router.post("/claim/:itemId", async (req, res) => {
|
|
|
1293
1387
|
jsonOutput: true,
|
|
1294
1388
|
});
|
|
1295
1389
|
if (!result.ok) {
|
|
1296
|
-
res.status(
|
|
1390
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to claim item" });
|
|
1297
1391
|
return;
|
|
1298
1392
|
}
|
|
1299
1393
|
scheduleGraphSync(routeParam(req, "projectId"), project, "item-claimed");
|
|
@@ -1313,7 +1407,7 @@ router.post("/release/:itemId", async (req, res) => {
|
|
|
1313
1407
|
jsonOutput: true,
|
|
1314
1408
|
});
|
|
1315
1409
|
if (!result.ok) {
|
|
1316
|
-
res.status(
|
|
1410
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to release item" });
|
|
1317
1411
|
return;
|
|
1318
1412
|
}
|
|
1319
1413
|
scheduleGraphSync(routeParam(req, "projectId"), project, "item-released");
|
|
@@ -1333,7 +1427,7 @@ router.post("/start-task/:itemId", async (req, res) => {
|
|
|
1333
1427
|
jsonOutput: true,
|
|
1334
1428
|
});
|
|
1335
1429
|
if (!result.ok) {
|
|
1336
|
-
res.status(
|
|
1430
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to start task" });
|
|
1337
1431
|
return;
|
|
1338
1432
|
}
|
|
1339
1433
|
scheduleGraphSync(routeParam(req, "projectId"), project, "task-started");
|
|
@@ -1353,7 +1447,7 @@ router.post("/pause-task/:itemId", async (req, res) => {
|
|
|
1353
1447
|
jsonOutput: true,
|
|
1354
1448
|
});
|
|
1355
1449
|
if (!result.ok) {
|
|
1356
|
-
res.status(
|
|
1450
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to pause task" });
|
|
1357
1451
|
return;
|
|
1358
1452
|
}
|
|
1359
1453
|
scheduleGraphSync(routeParam(req, "projectId"), project, "task-paused");
|
|
@@ -1396,7 +1490,7 @@ router.post("/tests/:itemId", async (req, res) => {
|
|
|
1396
1490
|
jsonOutput: true,
|
|
1397
1491
|
});
|
|
1398
1492
|
if (!result.ok) {
|
|
1399
|
-
res.status(
|
|
1493
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add test" });
|
|
1400
1494
|
return;
|
|
1401
1495
|
}
|
|
1402
1496
|
res.status(201).json(result.parsed || { ok: true });
|
|
@@ -1450,7 +1544,7 @@ router.post("/restore/:itemId", async (req, res) => {
|
|
|
1450
1544
|
jsonOutput: true,
|
|
1451
1545
|
});
|
|
1452
1546
|
if (!result.ok) {
|
|
1453
|
-
res.status(
|
|
1547
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to restore item" });
|
|
1454
1548
|
return;
|
|
1455
1549
|
}
|
|
1456
1550
|
scheduleGraphSync(routeParam(req, "projectId"), project, "item-restored");
|
|
@@ -1475,7 +1569,7 @@ router.post("/close-task/:itemId", async (req, res) => {
|
|
|
1475
1569
|
jsonOutput: true,
|
|
1476
1570
|
});
|
|
1477
1571
|
if (!result.ok) {
|
|
1478
|
-
res.status(
|
|
1572
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to close task" });
|
|
1479
1573
|
return;
|
|
1480
1574
|
}
|
|
1481
1575
|
scheduleGraphSync(routeParam(req, "projectId"), project, "task-closed");
|
|
@@ -1557,7 +1651,7 @@ router.post("/files/:itemId", async (req, res) => {
|
|
|
1557
1651
|
jsonOutput: true,
|
|
1558
1652
|
});
|
|
1559
1653
|
if (!result.ok) {
|
|
1560
|
-
res.status(
|
|
1654
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to link file" });
|
|
1561
1655
|
return;
|
|
1562
1656
|
}
|
|
1563
1657
|
scheduleGraphSync(routeParam(req, "projectId"), project, "file-linked");
|
|
@@ -1825,7 +1919,7 @@ router.post("/update-many", async (req, res) => {
|
|
|
1825
1919
|
}
|
|
1826
1920
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1827
1921
|
if (!result.ok) {
|
|
1828
|
-
res.status(
|
|
1922
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "update-many failed" });
|
|
1829
1923
|
return;
|
|
1830
1924
|
}
|
|
1831
1925
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -1867,7 +1961,7 @@ router.post("/close-many", async (req, res) => {
|
|
|
1867
1961
|
}
|
|
1868
1962
|
const listResult = await runPm({ args: listArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1869
1963
|
if (!listResult.ok) {
|
|
1870
|
-
res.status(
|
|
1964
|
+
res.status(pmErrorStatus(listResult)).json({ error: listResult.stderr || "Failed to list items for close-many" });
|
|
1871
1965
|
return;
|
|
1872
1966
|
}
|
|
1873
1967
|
const parsed = listResult.parsed;
|
|
@@ -1954,7 +2048,7 @@ router.post("/docs/:itemId", async (req, res) => {
|
|
|
1954
2048
|
}
|
|
1955
2049
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1956
2050
|
if (!result.ok) {
|
|
1957
|
-
res.status(
|
|
2051
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to update docs" });
|
|
1958
2052
|
return;
|
|
1959
2053
|
}
|
|
1960
2054
|
scheduleGraphSync(routeParam(req, "projectId"), project, "docs-updated");
|
|
@@ -1977,7 +2071,7 @@ router.post("/test-all", async (req, res) => {
|
|
|
1977
2071
|
args.push("--timeout", body.timeout);
|
|
1978
2072
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
1979
2073
|
if (!result.ok) {
|
|
1980
|
-
res.status(
|
|
2074
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "test-all failed" });
|
|
1981
2075
|
return;
|
|
1982
2076
|
}
|
|
1983
2077
|
res.json(result.parsed || {});
|
|
@@ -2077,7 +2171,7 @@ function buildListShortcutRoute(pmCommand) {
|
|
|
2077
2171
|
res.status(404).json({ error: "Project not found" });
|
|
2078
2172
|
return;
|
|
2079
2173
|
}
|
|
2080
|
-
const { type, limit, offset, tag, priority, assignee, sprint, release } = req.query;
|
|
2174
|
+
const { type, limit, offset, tag, priority, assignee, sprint, release, after } = req.query;
|
|
2081
2175
|
const args = [pmCommand];
|
|
2082
2176
|
if (type)
|
|
2083
2177
|
args.push("--type", type);
|
|
@@ -2095,7 +2189,18 @@ function buildListShortcutRoute(pmCommand) {
|
|
|
2095
2189
|
args.push("--sprint", sprint);
|
|
2096
2190
|
if (release)
|
|
2097
2191
|
args.push("--release", release);
|
|
2192
|
+
const cursorResult = validateCursor(after);
|
|
2193
|
+
if (cursorResult.error) {
|
|
2194
|
+
res.status(400).json({ error: cursorResult.error, items: [] });
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
if (cursorResult.cursor)
|
|
2198
|
+
args.push("--after", cursorResult.cursor);
|
|
2098
2199
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2200
|
+
if (!result.ok && result.exitCode === EXIT_CODE.USAGE) {
|
|
2201
|
+
res.status(400).json({ error: result.stderr, items: [] });
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2099
2204
|
res.json(result.ok ? (result.parsed || {}) : { items: [] });
|
|
2100
2205
|
};
|
|
2101
2206
|
}
|
|
@@ -2133,7 +2238,7 @@ router.post("/plan", async (req, res) => {
|
|
|
2133
2238
|
args.push("--body", body);
|
|
2134
2239
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2135
2240
|
if (!result.ok) {
|
|
2136
|
-
res.status(
|
|
2241
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to create plan" });
|
|
2137
2242
|
return;
|
|
2138
2243
|
}
|
|
2139
2244
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2176,7 +2281,7 @@ router.patch("/plan/:planId", async (req, res) => {
|
|
|
2176
2281
|
args.push("--description", description);
|
|
2177
2282
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2178
2283
|
if (!result.ok) {
|
|
2179
|
-
res.status(
|
|
2284
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to update plan" });
|
|
2180
2285
|
return;
|
|
2181
2286
|
}
|
|
2182
2287
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2199,7 +2304,7 @@ router.delete("/plan/:planId", async (req, res) => {
|
|
|
2199
2304
|
jsonOutput: true,
|
|
2200
2305
|
});
|
|
2201
2306
|
if (!result.ok) {
|
|
2202
|
-
res.status(
|
|
2307
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to delete plan" });
|
|
2203
2308
|
return;
|
|
2204
2309
|
}
|
|
2205
2310
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2227,7 +2332,7 @@ router.post("/plan/:planId/steps", async (req, res) => {
|
|
|
2227
2332
|
args.push("--depends-on", dependsOn);
|
|
2228
2333
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2229
2334
|
if (!result.ok) {
|
|
2230
|
-
res.status(
|
|
2335
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to add step" });
|
|
2231
2336
|
return;
|
|
2232
2337
|
}
|
|
2233
2338
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2251,7 +2356,7 @@ router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
|
2251
2356
|
args.push("--description", description);
|
|
2252
2357
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2253
2358
|
if (!result.ok) {
|
|
2254
|
-
res.status(
|
|
2359
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to update step" });
|
|
2255
2360
|
return;
|
|
2256
2361
|
}
|
|
2257
2362
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2274,7 +2379,7 @@ router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
|
|
|
2274
2379
|
jsonOutput: true,
|
|
2275
2380
|
});
|
|
2276
2381
|
if (!result.ok) {
|
|
2277
|
-
res.status(
|
|
2382
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to complete step" });
|
|
2278
2383
|
return;
|
|
2279
2384
|
}
|
|
2280
2385
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2302,7 +2407,7 @@ router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
|
|
|
2302
2407
|
jsonOutput: true,
|
|
2303
2408
|
});
|
|
2304
2409
|
if (!result.ok) {
|
|
2305
|
-
res.status(
|
|
2410
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to block step" });
|
|
2306
2411
|
return;
|
|
2307
2412
|
}
|
|
2308
2413
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2325,7 +2430,7 @@ router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
|
|
|
2325
2430
|
jsonOutput: true,
|
|
2326
2431
|
});
|
|
2327
2432
|
if (!result.ok) {
|
|
2328
|
-
res.status(
|
|
2433
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to remove step" });
|
|
2329
2434
|
return;
|
|
2330
2435
|
}
|
|
2331
2436
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2348,7 +2453,7 @@ router.post("/plan/:planId/approve", async (req, res) => {
|
|
|
2348
2453
|
jsonOutput: true,
|
|
2349
2454
|
});
|
|
2350
2455
|
if (!result.ok) {
|
|
2351
|
-
res.status(
|
|
2456
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to approve plan" });
|
|
2352
2457
|
return;
|
|
2353
2458
|
}
|
|
2354
2459
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2374,7 +2479,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
|
|
|
2374
2479
|
args.push("--steps", steps);
|
|
2375
2480
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2376
2481
|
if (!result.ok) {
|
|
2377
|
-
res.status(
|
|
2482
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to materialize plan" });
|
|
2378
2483
|
return;
|
|
2379
2484
|
}
|
|
2380
2485
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2402,7 +2507,7 @@ router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
|
|
|
2402
2507
|
jsonOutput: true,
|
|
2403
2508
|
});
|
|
2404
2509
|
if (!result.ok) {
|
|
2405
|
-
res.status(
|
|
2510
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to reorder step" });
|
|
2406
2511
|
return;
|
|
2407
2512
|
}
|
|
2408
2513
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2432,7 +2537,7 @@ router.post("/plan/:planId/link", async (req, res) => {
|
|
|
2432
2537
|
args.push("--promote-to-item-dep");
|
|
2433
2538
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2434
2539
|
if (!result.ok) {
|
|
2435
|
-
res.status(
|
|
2540
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to link plan" });
|
|
2436
2541
|
return;
|
|
2437
2542
|
}
|
|
2438
2543
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2458,7 +2563,7 @@ router.delete("/plan/:planId/link", async (req, res) => {
|
|
|
2458
2563
|
args.push("--link-kind", linkKind);
|
|
2459
2564
|
const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
|
|
2460
2565
|
if (!result.ok) {
|
|
2461
|
-
res.status(
|
|
2566
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Failed to unlink plan" });
|
|
2462
2567
|
return;
|
|
2463
2568
|
}
|
|
2464
2569
|
broadcastProjectEvent(routeParam(req, "projectId"), {
|
|
@@ -2499,7 +2604,7 @@ router.post("/upgrade", async (req, res) => {
|
|
|
2499
2604
|
jsonOutput: true,
|
|
2500
2605
|
});
|
|
2501
2606
|
if (!result.ok) {
|
|
2502
|
-
res.status(
|
|
2607
|
+
res.status(pmErrorStatus(result)).json({ error: result.stderr || "Upgrade failed" });
|
|
2503
2608
|
return;
|
|
2504
2609
|
}
|
|
2505
2610
|
res.json(result.parsed || { ok: true });
|