bosun 0.40.1 → 0.40.3
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/cli.mjs +86 -67
- package/infra/monitor.mjs +8 -3
- package/infra/update-check.mjs +1 -3
- package/package.json +1 -1
- package/server/ui-server.mjs +126 -3
- package/task/task-executor.mjs +690 -6
- package/task/task-store.mjs +116 -1
- package/ui/demo.html +26 -1
- package/ui/styles/components.css +43 -3
- package/ui/tabs/tasks.js +391 -99
- package/workflow/workflow-engine.mjs +30 -5
- package/workflow/workflow-nodes.mjs +102 -2
- package/workspace/workspace-manager.mjs +14 -0
- package/workspace/worktree-manager.mjs +2 -2
package/cli.mjs
CHANGED
|
@@ -864,6 +864,46 @@ function findAllBosunProcessPids() {
|
|
|
864
864
|
}
|
|
865
865
|
}
|
|
866
866
|
|
|
867
|
+
function getRunningDaemonPids() {
|
|
868
|
+
const trackedDaemonPid = getDaemonPid();
|
|
869
|
+
if (trackedDaemonPid) return [trackedDaemonPid];
|
|
870
|
+
const ghostPids = findGhostDaemonPids();
|
|
871
|
+
return ghostPids.length > 0 ? ghostPids : [];
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function stopBosunProcesses(
|
|
875
|
+
pids,
|
|
876
|
+
{ reason = null, timeoutMs = 5000 } = {},
|
|
877
|
+
) {
|
|
878
|
+
const targets = Array.from(
|
|
879
|
+
new Set(
|
|
880
|
+
(Array.isArray(pids) ? pids : []).filter(
|
|
881
|
+
(pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid,
|
|
882
|
+
),
|
|
883
|
+
),
|
|
884
|
+
);
|
|
885
|
+
if (targets.length === 0) return [];
|
|
886
|
+
if (reason) {
|
|
887
|
+
console.log(` ${reason}: ${targets.join(", ")}`);
|
|
888
|
+
}
|
|
889
|
+
for (const pid of targets) {
|
|
890
|
+
try {
|
|
891
|
+
process.kill(pid, "SIGTERM");
|
|
892
|
+
} catch {
|
|
893
|
+
/* already dead */
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const alive = waitForPidsToExit(targets, timeoutMs);
|
|
897
|
+
for (const pid of alive) {
|
|
898
|
+
try {
|
|
899
|
+
process.kill(pid, "SIGKILL");
|
|
900
|
+
} catch {
|
|
901
|
+
/* already dead */
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return targets;
|
|
905
|
+
}
|
|
906
|
+
|
|
867
907
|
function removeKnownPidFiles(extraCacheDirs = []) {
|
|
868
908
|
const pidFiles = uniqueResolvedPaths([
|
|
869
909
|
...getPidFileCandidates("bosun-daemon.pid", extraCacheDirs),
|
|
@@ -1506,78 +1546,57 @@ async function main() {
|
|
|
1506
1546
|
|
|
1507
1547
|
// Handle --update (force update)
|
|
1508
1548
|
if (args.includes("--update")) {
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1549
|
+
const runningDaemonPids = getRunningDaemonPids();
|
|
1550
|
+
const shouldRestartDaemon = runningDaemonPids.length > 0;
|
|
1551
|
+
const runningSiblingPids = findAllBosunProcessPids().filter(
|
|
1552
|
+
(pid) => pid !== process.pid,
|
|
1553
|
+
);
|
|
1554
|
+
const runningNonDaemonPids = runningSiblingPids.filter(
|
|
1555
|
+
(pid) => !runningDaemonPids.includes(pid),
|
|
1556
|
+
);
|
|
1557
|
+
if (shouldRestartDaemon) {
|
|
1513
1558
|
console.log(
|
|
1514
|
-
|
|
1559
|
+
" Note: a successful update will restart the bosun daemon automatically.",
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
if (runningNonDaemonPids.length > 0) {
|
|
1563
|
+
console.log(
|
|
1564
|
+
` Note: ${runningNonDaemonPids.length} other running bosun process(es) will be stopped after a successful update.`,
|
|
1515
1565
|
);
|
|
1516
|
-
for (const pid of siblingPids) {
|
|
1517
|
-
try {
|
|
1518
|
-
process.kill(pid, "SIGTERM");
|
|
1519
|
-
} catch {
|
|
1520
|
-
/* already dead */
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
const deadline = Date.now() + 5000;
|
|
1524
|
-
let alive = siblingPids.filter((pid) => isProcessAlive(pid));
|
|
1525
|
-
while (alive.length > 0 && Date.now() < deadline) {
|
|
1526
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
1527
|
-
alive = alive.filter((pid) => isProcessAlive(pid));
|
|
1528
|
-
}
|
|
1529
|
-
for (const pid of alive) {
|
|
1530
|
-
try {
|
|
1531
|
-
process.kill(pid, "SIGKILL");
|
|
1532
|
-
} catch {
|
|
1533
|
-
/* already dead */
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
1566
|
}
|
|
1537
1567
|
|
|
1538
1568
|
const { forceUpdate } = await import("./infra/update-check.mjs");
|
|
1539
1569
|
const updated = await forceUpdate(VERSION);
|
|
1540
|
-
if (updated) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1570
|
+
if (!updated) {
|
|
1571
|
+
process.exit(0);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Only stop sibling processes once npm has actually installed the update.
|
|
1575
|
+
const daemonPids = getRunningDaemonPids();
|
|
1576
|
+
const siblingPids = Array.from(
|
|
1577
|
+
new Set([
|
|
1578
|
+
...findAllBosunProcessPids().filter((pid) => pid !== process.pid),
|
|
1579
|
+
...daemonPids,
|
|
1580
|
+
]),
|
|
1581
|
+
);
|
|
1582
|
+
stopBosunProcesses(siblingPids, {
|
|
1583
|
+
reason: `Stopping ${siblingPids.length} running bosun process(es) to finish update`,
|
|
1584
|
+
timeoutMs: 5000,
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
if (shouldRestartDaemon) {
|
|
1550
1588
|
if (daemonPids.length > 0) {
|
|
1551
|
-
console.log(
|
|
1552
|
-
` Daemon running (PID ${daemonPids.join(", ")}). Stopping for restart...`,
|
|
1553
|
-
);
|
|
1554
|
-
for (const p of daemonPids) {
|
|
1555
|
-
try {
|
|
1556
|
-
process.kill(p, "SIGTERM");
|
|
1557
|
-
} catch {
|
|
1558
|
-
/* already dead */
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
// Wait up to 4 s for graceful exit
|
|
1562
|
-
const deadline = Date.now() + 4000;
|
|
1563
|
-
let alive = daemonPids.filter((p) => isProcessAlive(p));
|
|
1564
|
-
while (alive.length > 0 && Date.now() < deadline) {
|
|
1565
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
1566
|
-
alive = alive.filter((p) => isProcessAlive(p));
|
|
1567
|
-
}
|
|
1568
|
-
for (const p of alive) {
|
|
1569
|
-
try {
|
|
1570
|
-
process.kill(p, "SIGKILL");
|
|
1571
|
-
} catch {
|
|
1572
|
-
/* ok */
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
1589
|
removePidFile();
|
|
1576
|
-
console.log(" Restarting daemon with updated version...");
|
|
1577
|
-
startDaemon(); // spawns new daemon-child and calls process.exit(0)
|
|
1578
|
-
return; // unreachable — startDaemon exits
|
|
1579
1590
|
}
|
|
1591
|
+
console.log(" Restarting daemon with updated version...");
|
|
1592
|
+
startDaemon(); // spawns new daemon-child and calls process.exit(0)
|
|
1593
|
+
return; // unreachable — startDaemon exits
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
if (siblingPids.length > 0) {
|
|
1597
|
+
console.log(" Restart stopped bosun sessions manually if you still need them.");
|
|
1580
1598
|
}
|
|
1599
|
+
console.log(" Restart bosun to use the new version.\n");
|
|
1581
1600
|
process.exit(0);
|
|
1582
1601
|
}
|
|
1583
1602
|
|
|
@@ -1610,7 +1629,7 @@ async function main() {
|
|
|
1610
1629
|
if (args.includes("--workspace-list") || args.includes("workspace-list")) {
|
|
1611
1630
|
const { listWorkspaces, getActiveWorkspace } = await import("./workspace/workspace-manager.mjs");
|
|
1612
1631
|
const configDirArg = getArgValue("--config-dir");
|
|
1613
|
-
const configDir = configDirArg || process.env.BOSUN_DIR ||
|
|
1632
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1614
1633
|
const workspaces = listWorkspaces(configDir);
|
|
1615
1634
|
const active = getActiveWorkspace(configDir);
|
|
1616
1635
|
if (workspaces.length === 0) {
|
|
@@ -1634,7 +1653,7 @@ async function main() {
|
|
|
1634
1653
|
if (args.includes("--workspace-add")) {
|
|
1635
1654
|
const { createWorkspace } = await import("./workspace/workspace-manager.mjs");
|
|
1636
1655
|
const configDirArg = getArgValue("--config-dir");
|
|
1637
|
-
const configDir = configDirArg || process.env.BOSUN_DIR ||
|
|
1656
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1638
1657
|
const name = getArgValue("--workspace-add");
|
|
1639
1658
|
if (!name) {
|
|
1640
1659
|
console.error(" Error: workspace name is required. Usage: bosun --workspace-add <name>");
|
|
@@ -1653,7 +1672,7 @@ async function main() {
|
|
|
1653
1672
|
if (args.includes("--workspace-switch")) {
|
|
1654
1673
|
const { setActiveWorkspace, getWorkspace } = await import("./workspace/workspace-manager.mjs");
|
|
1655
1674
|
const configDirArg = getArgValue("--config-dir");
|
|
1656
|
-
const configDir = configDirArg || process.env.BOSUN_DIR ||
|
|
1675
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1657
1676
|
const wsId = getArgValue("--workspace-switch");
|
|
1658
1677
|
if (!wsId) {
|
|
1659
1678
|
console.error(" Error: workspace ID required. Usage: bosun --workspace-switch <id>");
|
|
@@ -1673,7 +1692,7 @@ async function main() {
|
|
|
1673
1692
|
if (args.includes("--workspace-add-repo")) {
|
|
1674
1693
|
const { addRepoToWorkspace, getActiveWorkspace, listWorkspaces } = await import("./workspace/workspace-manager.mjs");
|
|
1675
1694
|
const configDirArg = getArgValue("--config-dir");
|
|
1676
|
-
const configDir = configDirArg || process.env.BOSUN_DIR ||
|
|
1695
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1677
1696
|
const active = getActiveWorkspace(configDir);
|
|
1678
1697
|
if (!active) {
|
|
1679
1698
|
console.error(" No active workspace. Create one first: bosun --workspace-add <name>");
|
|
@@ -1702,7 +1721,7 @@ async function main() {
|
|
|
1702
1721
|
const { runWorkspaceHealthCheck, formatWorkspaceHealthReport } =
|
|
1703
1722
|
await import("./config/config-doctor.mjs");
|
|
1704
1723
|
const configDirArg = getArgValue("--config-dir");
|
|
1705
|
-
const configDir = configDirArg || process.env.BOSUN_DIR ||
|
|
1724
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1706
1725
|
const result = runWorkspaceHealthCheck({ configDir });
|
|
1707
1726
|
console.log(formatWorkspaceHealthReport(result));
|
|
1708
1727
|
process.exit(result.ok ? 0 : 1);
|
package/infra/monitor.mjs
CHANGED
|
@@ -201,6 +201,7 @@ import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
|
|
|
201
201
|
// ── Task management subsystem imports ──────────────────────────────────────
|
|
202
202
|
import {
|
|
203
203
|
configureTaskStore,
|
|
204
|
+
canStartTask,
|
|
204
205
|
getTask as getInternalTask,
|
|
205
206
|
getTasksByStatus as getInternalTasksByStatus,
|
|
206
207
|
updateTask as updateInternalTask,
|
|
@@ -548,6 +549,9 @@ async function ensureWorkflowAutomationEngine() {
|
|
|
548
549
|
const services = {
|
|
549
550
|
telegram: telegramService,
|
|
550
551
|
kanban: kanbanService,
|
|
552
|
+
taskStore: {
|
|
553
|
+
canStartTask,
|
|
554
|
+
},
|
|
551
555
|
agentPool: agentPoolService,
|
|
552
556
|
meeting: meetingService,
|
|
553
557
|
prompts: Object.keys(promptServices).length > 0 ? promptServices : null,
|
|
@@ -13509,9 +13513,10 @@ safeSetInterval("flush-error-queue", () => flushErrorQueue(), 60 * 1000);
|
|
|
13509
13513
|
// Legacy periodic maintenance sweep removed (workflow-only control).
|
|
13510
13514
|
|
|
13511
13515
|
// ── Workflow schedule trigger polling ───────────────────────────────────────
|
|
13512
|
-
// Check all installed workflows
|
|
13513
|
-
//
|
|
13514
|
-
//
|
|
13516
|
+
// Check all installed polling workflows (trigger.schedule, trigger.scheduled_once,
|
|
13517
|
+
// trigger.task_available, trigger.task_low) and fire any whose interval has elapsed.
|
|
13518
|
+
// This keeps scheduled and task-poll lifecycle templates executing without hardcoded
|
|
13519
|
+
// per-workflow timers.
|
|
13515
13520
|
const scheduleCheckIntervalMs = 60 * 1000; // check every 60s
|
|
13516
13521
|
safeSetInterval("workflow-schedule-check", async () => {
|
|
13517
13522
|
try {
|
package/infra/update-check.mjs
CHANGED
|
@@ -463,9 +463,7 @@ export async function forceUpdate(currentVersion) {
|
|
|
463
463
|
stdio: "inherit",
|
|
464
464
|
timeout: 120000,
|
|
465
465
|
});
|
|
466
|
-
console.log(
|
|
467
|
-
`\n :check: Updated to v${latest}. Restart bosun to use the new version.\n`,
|
|
468
|
-
);
|
|
466
|
+
console.log(`\n :check: Updated to v${latest}.\n`);
|
|
469
467
|
return true; // update was installed
|
|
470
468
|
} catch (err) {
|
|
471
469
|
console.error(`\n :close: Update failed: ${err.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.3",
|
|
4
4
|
"description": "Bosun Autonomous Engineering — manages AI agent executors with failover, extremely powerful workflow builder, and a massive amount of included default workflow templates for autonomous engineering, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/server/ui-server.mjs
CHANGED
|
@@ -188,6 +188,12 @@ const TASK_STORE_DEPENDENCY_EXPORTS = {
|
|
|
188
188
|
update: ["updateTask"],
|
|
189
189
|
};
|
|
190
190
|
const TASK_STORE_ASSIGN_SPRINT_EXPORTS = ["assignTaskToSprint", "setTaskSprint"];
|
|
191
|
+
const TASK_STORE_EPIC_DEPENDENCY_EXPORTS = Object.freeze({
|
|
192
|
+
list: ["getEpicDependencies", "listEpicDependencies"],
|
|
193
|
+
set: ["setEpicDependencies", "updateEpicDependencies"],
|
|
194
|
+
add: ["addEpicDependency"],
|
|
195
|
+
remove: ["removeEpicDependency"],
|
|
196
|
+
});
|
|
191
197
|
let taskStoreApi = null;
|
|
192
198
|
let taskStoreApiPromise = null;
|
|
193
199
|
let didLogTaskStoreLoadFailure = false;
|
|
@@ -278,6 +284,7 @@ const repoRoot = resolveRepoRoot();
|
|
|
278
284
|
const uiRootPreferred = resolve(__dirname, "..", "ui");
|
|
279
285
|
const uiRootFallback = resolve(__dirname, "..", "site", "ui");
|
|
280
286
|
const uiRoot = existsSync(uiRootPreferred) ? uiRootPreferred : uiRootFallback;
|
|
287
|
+
const sharedLibRoot = resolve(__dirname, "..", "lib");
|
|
281
288
|
const libraryInitAttemptedRoots = new Set();
|
|
282
289
|
const MAX_VISION_FRAME_BYTES = Math.max(
|
|
283
290
|
128_000,
|
|
@@ -6153,6 +6160,68 @@ async function setTaskDependenciesForApi({
|
|
|
6153
6160
|
};
|
|
6154
6161
|
}
|
|
6155
6162
|
|
|
6163
|
+
async function listEpicDependenciesForApi() {
|
|
6164
|
+
const listed = await callTaskStoreFunction(TASK_STORE_EPIC_DEPENDENCY_EXPORTS.list, []);
|
|
6165
|
+
if (listed.found && Array.isArray(listed.value)) {
|
|
6166
|
+
return {
|
|
6167
|
+
ok: true,
|
|
6168
|
+
source: `task-store.${listed.found}`,
|
|
6169
|
+
data: listed.value
|
|
6170
|
+
.map((entry) => ({
|
|
6171
|
+
epicId: String(entry?.epicId || entry?.id || "").trim(),
|
|
6172
|
+
dependencies: normalizeTaskIdList(entry?.dependencies || entry?.dependsOn || []),
|
|
6173
|
+
}))
|
|
6174
|
+
.filter((entry) => entry.epicId),
|
|
6175
|
+
};
|
|
6176
|
+
}
|
|
6177
|
+
return { ok: false, source: null, data: [] };
|
|
6178
|
+
}
|
|
6179
|
+
|
|
6180
|
+
async function setEpicDependenciesForApi({ epicId, dependencies }) {
|
|
6181
|
+
const normalizedEpicId = String(epicId || "").trim();
|
|
6182
|
+
if (!normalizedEpicId) return { ok: false, status: 400, error: "epicId required" };
|
|
6183
|
+
const normalizedDependencies = normalizeTaskIdList(dependencies, { exclude: normalizedEpicId });
|
|
6184
|
+
|
|
6185
|
+
const setResult = await callTaskStoreFunction(
|
|
6186
|
+
TASK_STORE_EPIC_DEPENDENCY_EXPORTS.set,
|
|
6187
|
+
[normalizedEpicId, normalizedDependencies],
|
|
6188
|
+
);
|
|
6189
|
+
if (setResult.found) {
|
|
6190
|
+
return {
|
|
6191
|
+
ok: true,
|
|
6192
|
+
source: `task-store.${setResult.found}`,
|
|
6193
|
+
data: {
|
|
6194
|
+
epicId: normalizedEpicId,
|
|
6195
|
+
dependencies: normalizeTaskIdList(setResult.value?.dependencies || normalizedDependencies),
|
|
6196
|
+
},
|
|
6197
|
+
};
|
|
6198
|
+
}
|
|
6199
|
+
|
|
6200
|
+
const listed = await listEpicDependenciesForApi();
|
|
6201
|
+
const current = listed.ok
|
|
6202
|
+
? normalizeTaskIdList((listed.data.find((entry) => entry.epicId === normalizedEpicId) || {}).dependencies || [])
|
|
6203
|
+
: [];
|
|
6204
|
+
|
|
6205
|
+
const toRemove = current.filter((entry) => !normalizedDependencies.includes(entry));
|
|
6206
|
+
const toAdd = normalizedDependencies.filter((entry) => !current.includes(entry));
|
|
6207
|
+
|
|
6208
|
+
for (const dep of toRemove) {
|
|
6209
|
+
await callTaskStoreFunction(TASK_STORE_EPIC_DEPENDENCY_EXPORTS.remove, [normalizedEpicId, dep]);
|
|
6210
|
+
}
|
|
6211
|
+
for (const dep of toAdd) {
|
|
6212
|
+
await callTaskStoreFunction(TASK_STORE_EPIC_DEPENDENCY_EXPORTS.add, [normalizedEpicId, dep]);
|
|
6213
|
+
}
|
|
6214
|
+
|
|
6215
|
+
const refreshed = await listEpicDependenciesForApi();
|
|
6216
|
+
const row = refreshed.ok
|
|
6217
|
+
? refreshed.data.find((entry) => entry.epicId === normalizedEpicId)
|
|
6218
|
+
: null;
|
|
6219
|
+
return {
|
|
6220
|
+
ok: true,
|
|
6221
|
+
source: refreshed.source || "task-store.fallback",
|
|
6222
|
+
data: { epicId: normalizedEpicId, dependencies: normalizeTaskIdList(row?.dependencies || []) },
|
|
6223
|
+
};
|
|
6224
|
+
}
|
|
6156
6225
|
async function getTaskCommentsForApi(taskId, adapter = null) {
|
|
6157
6226
|
const storeComments = await callTaskStoreFunction(TASK_STORE_COMMENT_EXPORTS, [taskId]);
|
|
6158
6227
|
if (storeComments.found && Array.isArray(storeComments.value)) {
|
|
@@ -9052,6 +9121,55 @@ async function handleApi(req, res, url) {
|
|
|
9052
9121
|
}
|
|
9053
9122
|
return;
|
|
9054
9123
|
}
|
|
9124
|
+
if (path === "/api/tasks/epic-dependencies" && req.method === "GET") {
|
|
9125
|
+
try {
|
|
9126
|
+
const listed = await listEpicDependenciesForApi();
|
|
9127
|
+
if (!listed.ok) {
|
|
9128
|
+
jsonResponse(res, 501, { ok: false, error: "Epic dependency APIs are unavailable." });
|
|
9129
|
+
return;
|
|
9130
|
+
}
|
|
9131
|
+
jsonResponse(res, 200, { ok: true, source: listed.source, data: listed.data });
|
|
9132
|
+
} catch (err) {
|
|
9133
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
9134
|
+
}
|
|
9135
|
+
return;
|
|
9136
|
+
}
|
|
9137
|
+
|
|
9138
|
+
if (path === "/api/tasks/epic-dependencies" && req.method === "PUT") {
|
|
9139
|
+
try {
|
|
9140
|
+
const body = await readJsonBody(req);
|
|
9141
|
+
const epicId = String(body?.epicId || body?.id || "").trim();
|
|
9142
|
+
const dependencies = Array.isArray(body?.dependencies)
|
|
9143
|
+
? body.dependencies
|
|
9144
|
+
: Array.isArray(body?.dependsOn)
|
|
9145
|
+
? body.dependsOn
|
|
9146
|
+
: [];
|
|
9147
|
+
if (!epicId) {
|
|
9148
|
+
jsonResponse(res, 400, { ok: false, error: "epicId required" });
|
|
9149
|
+
return;
|
|
9150
|
+
}
|
|
9151
|
+
const updated = await setEpicDependenciesForApi({ epicId, dependencies });
|
|
9152
|
+
if (!updated.ok) {
|
|
9153
|
+
jsonResponse(res, updated.status || 400, { ok: false, error: updated.error || "Failed to update epic dependencies" });
|
|
9154
|
+
return;
|
|
9155
|
+
}
|
|
9156
|
+
const globalDag = await getGlobalDagData();
|
|
9157
|
+
jsonResponse(res, 200, {
|
|
9158
|
+
ok: true,
|
|
9159
|
+
source: updated.source,
|
|
9160
|
+
data: updated.data,
|
|
9161
|
+
dag: globalDag?.data || null,
|
|
9162
|
+
});
|
|
9163
|
+
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
9164
|
+
reason: "epic-dependencies-updated",
|
|
9165
|
+
epicId,
|
|
9166
|
+
});
|
|
9167
|
+
} catch (err) {
|
|
9168
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
9169
|
+
}
|
|
9170
|
+
return;
|
|
9171
|
+
}
|
|
9172
|
+
|
|
9055
9173
|
if (path === "/api/tasks/start") {
|
|
9056
9174
|
try {
|
|
9057
9175
|
const body = await readJsonBody(req);
|
|
@@ -14739,9 +14857,14 @@ async function handleStatic(req, res, url) {
|
|
|
14739
14857
|
return;
|
|
14740
14858
|
}
|
|
14741
14859
|
const pathname = rawPathname === "/" ? "/index.html" : rawPathname;
|
|
14742
|
-
const
|
|
14743
|
-
|
|
14744
|
-
|
|
14860
|
+
const isSharedLibRequest = pathname === "/lib" || pathname.startsWith("/lib/");
|
|
14861
|
+
const staticRoot = isSharedLibRequest ? sharedLibRoot : uiRoot;
|
|
14862
|
+
const staticPathname = isSharedLibRequest
|
|
14863
|
+
? pathname.slice(4) || "/"
|
|
14864
|
+
: pathname;
|
|
14865
|
+
const filePath = resolve(staticRoot, `.${staticPathname}`);
|
|
14866
|
+
|
|
14867
|
+
if (!filePath.startsWith(staticRoot)) {
|
|
14745
14868
|
textResponse(res, 403, "Forbidden");
|
|
14746
14869
|
return;
|
|
14747
14870
|
}
|