syntaur 0.1.13 → 0.1.14
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/dashboard/dist/assets/{_basePickBy-DXzhD14q.js → _basePickBy-eih-KlEh.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-gxypqvP5.js → _baseUniq-M21wg9ZQ.js} +1 -1
- package/dashboard/dist/assets/{arc-Ce7nYKSm.js → arc-uKZMelpQ.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-zX4f4_Mf.js → architectureDiagram-2XIMDMQ5-CpMG5exj.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-auOdy7nH.js → blockDiagram-WCTKOSBZ-BHnCCKl_.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-C2kkjPbW.js → c4Diagram-IC4MRINW-B-n3zU9i.js} +1 -1
- package/dashboard/dist/assets/channel-DVBgSlOI.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-B7dfpnbG.js → chunk-4BX2VUAB-ChD9Iuih.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-r1_jHZYp.js → chunk-55IACEB6-B3vP9Psg.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-5mMONjMK.js → chunk-FMBD7UC4-CIhWgxPS.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-CwKj-Es4.js → chunk-JSJVCQXG-DiGIV_cB.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-ByoW-HgN.js → chunk-KX2RTZJC-DnGsx5jo.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-D1olOovd.js → chunk-NQ4KR5QH-BFBu1fmg.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-CB8_FC8w.js → chunk-QZHKN3VN-DYtumHth.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-CFEqRrE1.js → chunk-WL4C6EOR-BzCrQPuw.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-B7dxBacd.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-B7dxBacd.js +1 -0
- package/dashboard/dist/assets/clone-DAOrHcCC.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-D6dGVXEI.js → cose-bilkent-S5V4N54A-Bl8mb5eY.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-Cvg9CgP-.js → dagre-KLK3FWXG-BHffcOgo.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-iCBudhZD.js → diagram-E7M64L7V-Ib83qzT_.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-BGniy7VQ.js → diagram-IFDJBPK2-hOdh63_T.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-B6Ie044E.js → diagram-P4PSJMXO-D4ocLmc5.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-BHvFRNhJ.js → erDiagram-INFDFZHY-CHJ6zqnJ.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-CN86Zu3Q.js → flowDiagram-PKNHOUZH-DEz5g2Ye.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-D-1fKFjW.js → ganttDiagram-A5KZAMGK-BSftxDHA.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-Dtf1A6KL.js → gitGraphDiagram-K3NZZRJ6-Cr3vGf07.js} +1 -1
- package/dashboard/dist/assets/{graph-B6H_kXSs.js → graph-D4us8trI.js} +1 -1
- package/dashboard/dist/assets/index-AXntWS_w.css +1 -0
- package/dashboard/dist/assets/index-CEMjexkj.js +460 -0
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-R9wJj4JF.js → infoDiagram-LFFYTUFH-CH_jVfru.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CJmeR-bX.js → ishikawaDiagram-PHBUUO56-BdKLa5GC.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-FcUhyu8I.js → journeyDiagram-4ABVD52K-C_SMzNGF.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-C8UcTIwW.js → kanban-definition-K7BYSVSG-BeA-egRW.js} +1 -1
- package/dashboard/dist/assets/{layout-DzBy6alw.js → layout-B8tDmL4j.js} +1 -1
- package/dashboard/dist/assets/{linear-CZJCNOB9.js → linear-CeGJyrHS.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-fMQRe9Gq.js → mermaid.core-DyEs-LPd.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-BFwp-LS-.js → mindmap-definition-YRQLILUH-DCxzAr8m.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CQLmPkkd.js → pieDiagram-SKSYHLDU-CEj5dRDi.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-DAmi-dmD.js → quadrantDiagram-337W2JSQ-CKfvAEQg.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-Dcdts4kX.js → requirementDiagram-Z7DCOOCP-CTRqKPtJ.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-By8LRvM0.js → sankeyDiagram-WA2Y5GQK-BlYbz8UR.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BsvqgtTz.js → sequenceDiagram-2WXFIKYE-PT2t7ryQ.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DFNOD7cx.js → stateDiagram-RAJIS63D-eDX7IUuV.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO--yuSBnLh.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-CMcgJGjn.js → timeline-definition-YZTLITO2-By11B1Ow.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-BWsRNHwq.js → treemap-KZPCXAKY-rvdLeWWV.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-io7-2Tod.js → vennDiagram-LZ73GAT5-Br_oZ1wv.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-AVnh4fDS.js → xychartDiagram-JWTSCODW-D-MWVqrT.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/dashboard/server.js +624 -139
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +658 -124
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/channel-PMR2DuGi.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-DmESf_RL.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-DmESf_RL.js +0 -1
- package/dashboard/dist/assets/clone-WlIeyha4.js +0 -1
- package/dashboard/dist/assets/index-BhuXD-Q5.js +0 -445
- package/dashboard/dist/assets/index-BnqH-RIk.css +0 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DVO-Epiz.js +0 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -389,6 +389,15 @@ function syntaurRoot() {
|
|
|
389
389
|
function defaultMissionDir() {
|
|
390
390
|
return resolve2(syntaurRoot(), "missions");
|
|
391
391
|
}
|
|
392
|
+
function serversDir() {
|
|
393
|
+
return resolve2(syntaurRoot(), "servers");
|
|
394
|
+
}
|
|
395
|
+
function playbooksDir() {
|
|
396
|
+
return resolve2(syntaurRoot(), "playbooks");
|
|
397
|
+
}
|
|
398
|
+
function todosDir() {
|
|
399
|
+
return resolve2(syntaurRoot(), "todos");
|
|
400
|
+
}
|
|
392
401
|
var init_paths = __esm({
|
|
393
402
|
"src/utils/paths.ts"() {
|
|
394
403
|
"use strict";
|
|
@@ -396,6 +405,27 @@ var init_paths = __esm({
|
|
|
396
405
|
});
|
|
397
406
|
|
|
398
407
|
// src/templates/config.ts
|
|
408
|
+
function renderConfig(params) {
|
|
409
|
+
return `---
|
|
410
|
+
version: "1.0"
|
|
411
|
+
defaultMissionDir: ${params.defaultMissionDir}
|
|
412
|
+
onboarding:
|
|
413
|
+
completed: false
|
|
414
|
+
agentDefaults:
|
|
415
|
+
trustLevel: medium
|
|
416
|
+
autoApprove: false
|
|
417
|
+
backup:
|
|
418
|
+
repo: null
|
|
419
|
+
categories: missions, playbooks, todos, servers, config
|
|
420
|
+
lastBackup: null
|
|
421
|
+
lastRestore: null
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
# Syntaur Configuration
|
|
425
|
+
|
|
426
|
+
Global configuration for the Syntaur CLI.
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
399
429
|
var init_config = __esm({
|
|
400
430
|
"src/templates/config.ts"() {
|
|
401
431
|
"use strict";
|
|
@@ -549,6 +579,14 @@ function serializeStatusConfig(statuses) {
|
|
|
549
579
|
}
|
|
550
580
|
return lines.join("\n");
|
|
551
581
|
}
|
|
582
|
+
function serializeBackupConfig(backup) {
|
|
583
|
+
const lines = ["backup:"];
|
|
584
|
+
lines.push(` repo: ${backup.repo ?? "null"}`);
|
|
585
|
+
lines.push(` categories: ${backup.categories}`);
|
|
586
|
+
lines.push(` lastBackup: ${backup.lastBackup ?? "null"}`);
|
|
587
|
+
lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
|
|
588
|
+
return lines.join("\n");
|
|
589
|
+
}
|
|
552
590
|
function stripTopLevelBlock(fmBlock, key) {
|
|
553
591
|
const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
|
|
554
592
|
if (!blockStart) {
|
|
@@ -653,6 +691,40 @@ ${cleanedFm}
|
|
|
653
691
|
---${afterFrontmatter}`;
|
|
654
692
|
await writeFileForce(configPath, newContent);
|
|
655
693
|
}
|
|
694
|
+
async function updateBackupConfig(backup) {
|
|
695
|
+
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
696
|
+
const current = (await readConfig()).backup;
|
|
697
|
+
const nextBackup = {
|
|
698
|
+
repo: current?.repo ?? null,
|
|
699
|
+
categories: current?.categories ?? "missions, playbooks, todos, servers, config",
|
|
700
|
+
lastBackup: current?.lastBackup ?? null,
|
|
701
|
+
lastRestore: current?.lastRestore ?? null,
|
|
702
|
+
...backup
|
|
703
|
+
};
|
|
704
|
+
const backupBlock = serializeBackupConfig(nextBackup);
|
|
705
|
+
const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultMissionDir: defaultMissionDir() });
|
|
706
|
+
const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
|
|
707
|
+
if (!fmMatch) {
|
|
708
|
+
const content = `---
|
|
709
|
+
version: "1.0"
|
|
710
|
+
defaultMissionDir: ${defaultMissionDir()}
|
|
711
|
+
${backupBlock}
|
|
712
|
+
---
|
|
713
|
+
${existing}`;
|
|
714
|
+
await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const fmBlock = fmMatch[2];
|
|
718
|
+
const afterFrontmatter = existing.slice(fmMatch[0].length);
|
|
719
|
+
const cleanedFm = stripTopLevelBlock(fmBlock, "backup");
|
|
720
|
+
const newFm = `${cleanedFm}
|
|
721
|
+
${backupBlock}`.replace(/^\n+/, "");
|
|
722
|
+
const normalizedFm = newFm.replace(/\n+$/, "");
|
|
723
|
+
const newContent = `---
|
|
724
|
+
${normalizedFm}
|
|
725
|
+
---${afterFrontmatter}`;
|
|
726
|
+
await writeFileForce(configPath, newContent);
|
|
727
|
+
}
|
|
656
728
|
async function readConfig() {
|
|
657
729
|
const configPath = resolve3(syntaurRoot(), "config.md");
|
|
658
730
|
if (!await fileExists(configPath)) {
|
|
@@ -695,6 +767,12 @@ async function readConfig() {
|
|
|
695
767
|
"integrations.codexMarketplacePath"
|
|
696
768
|
)
|
|
697
769
|
},
|
|
770
|
+
backup: fm["backup.repo"] || fm["backup.categories"] ? {
|
|
771
|
+
repo: fm["backup.repo"] && fm["backup.repo"] !== "null" ? fm["backup.repo"] : null,
|
|
772
|
+
categories: fm["backup.categories"] || "missions, playbooks, todos, servers, config",
|
|
773
|
+
lastBackup: fm["backup.lastBackup"] && fm["backup.lastBackup"] !== "null" ? fm["backup.lastBackup"] : null,
|
|
774
|
+
lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
|
|
775
|
+
} : null,
|
|
698
776
|
statuses: parseStatusConfig(content)
|
|
699
777
|
};
|
|
700
778
|
}
|
|
@@ -720,6 +798,7 @@ var init_config2 = __esm({
|
|
|
720
798
|
codexPluginDir: null,
|
|
721
799
|
codexMarketplacePath: null
|
|
722
800
|
},
|
|
801
|
+
backup: null,
|
|
723
802
|
statuses: null
|
|
724
803
|
};
|
|
725
804
|
}
|
|
@@ -1863,17 +1942,17 @@ async function scanProcessSession(sessionData, lsofOutput, workspaceRecords) {
|
|
|
1863
1942
|
windows: [{ index: 0, name: "process", panes: [pane] }]
|
|
1864
1943
|
};
|
|
1865
1944
|
}
|
|
1866
|
-
async function scanAllSessions(
|
|
1945
|
+
async function scanAllSessions(serversDir2, missionsDir, options) {
|
|
1867
1946
|
if (!options?.bypassCache && cache && Date.now() < cache.expiry) {
|
|
1868
1947
|
return cache.data;
|
|
1869
1948
|
}
|
|
1870
1949
|
const tmuxAvailable = await checkTmuxAvailable();
|
|
1871
|
-
const names = await listSessionFiles(
|
|
1950
|
+
const names = await listSessionFiles(serversDir2);
|
|
1872
1951
|
const lsofOutput = await getLsofOutput();
|
|
1873
1952
|
const workspaceRecords = await loadWorkspaceRecords(missionsDir);
|
|
1874
1953
|
const sessions = [];
|
|
1875
1954
|
for (const name of names) {
|
|
1876
|
-
const data = await readSessionFile(
|
|
1955
|
+
const data = await readSessionFile(serversDir2, name);
|
|
1877
1956
|
if (!data) continue;
|
|
1878
1957
|
if (data.kind === "process") {
|
|
1879
1958
|
sessions.push(await scanProcessSession(data, lsofOutput, workspaceRecords));
|
|
@@ -1885,8 +1964,8 @@ async function scanAllSessions(serversDir, missionsDir, options) {
|
|
|
1885
1964
|
cache = { data: result, expiry: Date.now() + CACHE_TTL_MS };
|
|
1886
1965
|
return result;
|
|
1887
1966
|
}
|
|
1888
|
-
async function scanSingleSession(
|
|
1889
|
-
const data = await readSessionFile(
|
|
1967
|
+
async function scanSingleSession(serversDir2, missionsDir, name) {
|
|
1968
|
+
const data = await readSessionFile(serversDir2, name);
|
|
1890
1969
|
if (!data) return null;
|
|
1891
1970
|
const lsofOutput = await getLsofOutput();
|
|
1892
1971
|
const workspaceRecords = await loadWorkspaceRecords(missionsDir);
|
|
@@ -2014,15 +2093,15 @@ async function deleteWorkspace(missionsDir, name) {
|
|
|
2014
2093
|
const filtered = registered.filter((w) => w !== name);
|
|
2015
2094
|
await writeWorkspaceRegistry(missionsDir, filtered);
|
|
2016
2095
|
}
|
|
2017
|
-
async function getOverview(missionsDir,
|
|
2096
|
+
async function getOverview(missionsDir, serversDir2) {
|
|
2018
2097
|
const missionRecords = await listMissionRecords(missionsDir);
|
|
2019
2098
|
const attention = buildAttentionItems(missionRecords);
|
|
2020
2099
|
const recentActivity = buildRecentActivity(missionRecords);
|
|
2021
2100
|
let serverStats;
|
|
2022
|
-
if (
|
|
2101
|
+
if (serversDir2) {
|
|
2023
2102
|
try {
|
|
2024
2103
|
const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|
|
2025
|
-
const servers = await scanAllSessions2(
|
|
2104
|
+
const servers = await scanAllSessions2(serversDir2, missionsDir);
|
|
2026
2105
|
if (servers.tmuxAvailable) {
|
|
2027
2106
|
const alive = servers.sessions.filter((s) => s.alive).length;
|
|
2028
2107
|
const totalPorts = servers.sessions.reduce((sum, s) => sum + s.windows.reduce((ws, w) => ws + w.panes.reduce((ps, p) => ps + p.ports.length, 0), 0), 0);
|
|
@@ -2068,13 +2147,13 @@ async function getOverview(missionsDir, serversDir) {
|
|
|
2068
2147
|
serverStats
|
|
2069
2148
|
};
|
|
2070
2149
|
}
|
|
2071
|
-
async function getAttention(missionsDir,
|
|
2150
|
+
async function getAttention(missionsDir, serversDir2) {
|
|
2072
2151
|
const missionRecords = await listMissionRecords(missionsDir);
|
|
2073
2152
|
const items = buildAttentionItems(missionRecords);
|
|
2074
|
-
if (
|
|
2153
|
+
if (serversDir2) {
|
|
2075
2154
|
try {
|
|
2076
2155
|
const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|
|
2077
|
-
const servers = await scanAllSessions2(
|
|
2156
|
+
const servers = await scanAllSessions2(serversDir2, missionsDir);
|
|
2078
2157
|
for (const session of servers.sessions) {
|
|
2079
2158
|
if (!session.alive) {
|
|
2080
2159
|
items.push({
|
|
@@ -2745,13 +2824,13 @@ function getEditableDocumentTitle(documentType, missionSlug, assignmentSlug) {
|
|
|
2745
2824
|
return missionSlug;
|
|
2746
2825
|
}
|
|
2747
2826
|
}
|
|
2748
|
-
async function listPlaybooks(
|
|
2749
|
-
if (!await fileExists(
|
|
2750
|
-
const entries = await readdir3(
|
|
2827
|
+
async function listPlaybooks(playbooksDir2) {
|
|
2828
|
+
if (!await fileExists(playbooksDir2)) return [];
|
|
2829
|
+
const entries = await readdir3(playbooksDir2, { withFileTypes: true });
|
|
2751
2830
|
const playbooks = [];
|
|
2752
2831
|
for (const entry of entries) {
|
|
2753
2832
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
2754
|
-
const filePath = resolve6(
|
|
2833
|
+
const filePath = resolve6(playbooksDir2, entry.name);
|
|
2755
2834
|
const raw = await readFile5(filePath, "utf-8");
|
|
2756
2835
|
const parsed = parsePlaybook(raw);
|
|
2757
2836
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
@@ -2767,8 +2846,8 @@ async function listPlaybooks(playbooksDir) {
|
|
|
2767
2846
|
}
|
|
2768
2847
|
return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
|
|
2769
2848
|
}
|
|
2770
|
-
async function getPlaybookDetail(
|
|
2771
|
-
const filePath = resolve6(
|
|
2849
|
+
async function getPlaybookDetail(playbooksDir2, slug) {
|
|
2850
|
+
const filePath = resolve6(playbooksDir2, `${slug}.md`);
|
|
2772
2851
|
if (!await fileExists(filePath)) return null;
|
|
2773
2852
|
const raw = await readFile5(filePath, "utf-8");
|
|
2774
2853
|
const parsed = parsePlaybook(raw);
|
|
@@ -3042,13 +3121,13 @@ function serializeLogEntry(entry) {
|
|
|
3042
3121
|
if (entry.status) lines.push(`**Status:** ${entry.status}`);
|
|
3043
3122
|
return lines.join("\n");
|
|
3044
3123
|
}
|
|
3045
|
-
function checklistPath(
|
|
3046
|
-
return resolve13(
|
|
3124
|
+
function checklistPath(todosDir2, workspace) {
|
|
3125
|
+
return resolve13(todosDir2, `${workspace}.md`);
|
|
3047
3126
|
}
|
|
3048
|
-
function logPath(
|
|
3049
|
-
return resolve13(
|
|
3127
|
+
function logPath(todosDir2, workspace) {
|
|
3128
|
+
return resolve13(todosDir2, `${workspace}-log.md`);
|
|
3050
3129
|
}
|
|
3051
|
-
function archivePath(
|
|
3130
|
+
function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new Date()) {
|
|
3052
3131
|
const year = now.getFullYear();
|
|
3053
3132
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
3054
3133
|
const day = String(now.getDate()).padStart(2, "0");
|
|
@@ -3070,32 +3149,32 @@ function archivePath(todosDir, workspace, interval, now = /* @__PURE__ */ new Da
|
|
|
3070
3149
|
default:
|
|
3071
3150
|
suffix = `${year}-${month}-${day}`;
|
|
3072
3151
|
}
|
|
3073
|
-
return resolve13(
|
|
3152
|
+
return resolve13(todosDir2, "archive", `${workspace}-${suffix}.md`);
|
|
3074
3153
|
}
|
|
3075
|
-
async function readChecklist(
|
|
3076
|
-
const path = checklistPath(
|
|
3154
|
+
async function readChecklist(todosDir2, workspace) {
|
|
3155
|
+
const path = checklistPath(todosDir2, workspace);
|
|
3077
3156
|
if (!await fileExists(path)) {
|
|
3078
3157
|
return { workspace, archiveInterval: "weekly", items: [] };
|
|
3079
3158
|
}
|
|
3080
3159
|
const content = await readFile10(path, "utf-8");
|
|
3081
3160
|
return parseChecklist(content);
|
|
3082
3161
|
}
|
|
3083
|
-
async function writeChecklist(
|
|
3084
|
-
await ensureDir(
|
|
3085
|
-
const path = checklistPath(
|
|
3162
|
+
async function writeChecklist(todosDir2, checklist) {
|
|
3163
|
+
await ensureDir(todosDir2);
|
|
3164
|
+
const path = checklistPath(todosDir2, checklist.workspace);
|
|
3086
3165
|
await writeFileForce(path, serializeChecklist(checklist));
|
|
3087
3166
|
}
|
|
3088
|
-
async function readLog(
|
|
3089
|
-
const path = logPath(
|
|
3167
|
+
async function readLog(todosDir2, workspace) {
|
|
3168
|
+
const path = logPath(todosDir2, workspace);
|
|
3090
3169
|
if (!await fileExists(path)) {
|
|
3091
3170
|
return { workspace, entries: [] };
|
|
3092
3171
|
}
|
|
3093
3172
|
const content = await readFile10(path, "utf-8");
|
|
3094
3173
|
return parseLog(content);
|
|
3095
3174
|
}
|
|
3096
|
-
async function appendLogEntry2(
|
|
3097
|
-
await ensureDir(
|
|
3098
|
-
const path = logPath(
|
|
3175
|
+
async function appendLogEntry2(todosDir2, workspace, entry) {
|
|
3176
|
+
await ensureDir(todosDir2);
|
|
3177
|
+
const path = logPath(todosDir2, workspace);
|
|
3099
3178
|
let content;
|
|
3100
3179
|
if (await fileExists(path)) {
|
|
3101
3180
|
content = await readFile10(path, "utf-8");
|
|
@@ -3135,16 +3214,16 @@ var init_parser2 = __esm({
|
|
|
3135
3214
|
init_api();
|
|
3136
3215
|
import express from "express";
|
|
3137
3216
|
import { createServer } from "http";
|
|
3138
|
-
import { resolve as
|
|
3217
|
+
import { resolve as resolve15 } from "path";
|
|
3139
3218
|
import { homedir as homedir2 } from "os";
|
|
3140
|
-
import { writeFile as
|
|
3219
|
+
import { writeFile as writeFile4, unlink as unlink4 } from "fs/promises";
|
|
3141
3220
|
import { WebSocketServer, WebSocket } from "ws";
|
|
3142
3221
|
|
|
3143
3222
|
// src/dashboard/watcher.ts
|
|
3144
3223
|
import { watch } from "chokidar";
|
|
3145
3224
|
import { relative, sep } from "path";
|
|
3146
3225
|
function createWatcher(options) {
|
|
3147
|
-
const { missionsDir, serversDir, playbooksDir, todosDir, onMessage, debounceMs = 300 } = options;
|
|
3226
|
+
const { missionsDir, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, onMessage, debounceMs = 300 } = options;
|
|
3148
3227
|
const pendingEvents = /* @__PURE__ */ new Map();
|
|
3149
3228
|
const missionsWatcher = watch(missionsDir, {
|
|
3150
3229
|
ignoreInitial: true,
|
|
@@ -3183,7 +3262,7 @@ function createWatcher(options) {
|
|
|
3183
3262
|
missionsWatcher.on("add", handleMissionChange);
|
|
3184
3263
|
missionsWatcher.on("unlink", handleMissionChange);
|
|
3185
3264
|
let serversWatcher = null;
|
|
3186
|
-
if (
|
|
3265
|
+
if (serversDir2) {
|
|
3187
3266
|
let handleServerChange2 = function() {
|
|
3188
3267
|
const debounceKey = "__servers__";
|
|
3189
3268
|
const existing = pendingEvents.get(debounceKey);
|
|
@@ -3201,7 +3280,7 @@ function createWatcher(options) {
|
|
|
3201
3280
|
);
|
|
3202
3281
|
};
|
|
3203
3282
|
var handleServerChange = handleServerChange2;
|
|
3204
|
-
serversWatcher = watch(
|
|
3283
|
+
serversWatcher = watch(serversDir2, {
|
|
3205
3284
|
ignoreInitial: true,
|
|
3206
3285
|
persistent: true,
|
|
3207
3286
|
depth: 1,
|
|
@@ -3212,7 +3291,7 @@ function createWatcher(options) {
|
|
|
3212
3291
|
serversWatcher.on("unlink", handleServerChange2);
|
|
3213
3292
|
}
|
|
3214
3293
|
let playbooksWatcher = null;
|
|
3215
|
-
if (
|
|
3294
|
+
if (playbooksDir2) {
|
|
3216
3295
|
let handlePlaybookChange2 = function() {
|
|
3217
3296
|
const debounceKey = "__playbooks__";
|
|
3218
3297
|
const existing = pendingEvents.get(debounceKey);
|
|
@@ -3230,7 +3309,7 @@ function createWatcher(options) {
|
|
|
3230
3309
|
);
|
|
3231
3310
|
};
|
|
3232
3311
|
var handlePlaybookChange = handlePlaybookChange2;
|
|
3233
|
-
playbooksWatcher = watch(
|
|
3312
|
+
playbooksWatcher = watch(playbooksDir2, {
|
|
3234
3313
|
ignoreInitial: true,
|
|
3235
3314
|
persistent: true,
|
|
3236
3315
|
depth: 1,
|
|
@@ -3241,7 +3320,7 @@ function createWatcher(options) {
|
|
|
3241
3320
|
playbooksWatcher.on("unlink", handlePlaybookChange2);
|
|
3242
3321
|
}
|
|
3243
3322
|
let todosWatcher = null;
|
|
3244
|
-
if (
|
|
3323
|
+
if (todosDir2) {
|
|
3245
3324
|
let handleTodoChange2 = function() {
|
|
3246
3325
|
const debounceKey = "__todos__";
|
|
3247
3326
|
const existing = pendingEvents.get(debounceKey);
|
|
@@ -3259,7 +3338,7 @@ function createWatcher(options) {
|
|
|
3259
3338
|
);
|
|
3260
3339
|
};
|
|
3261
3340
|
var handleTodoChange = handleTodoChange2;
|
|
3262
|
-
todosWatcher = watch(
|
|
3341
|
+
todosWatcher = watch(todosDir2, {
|
|
3263
3342
|
ignoreInitial: true,
|
|
3264
3343
|
persistent: true,
|
|
3265
3344
|
depth: 1,
|
|
@@ -4470,11 +4549,11 @@ function createWriteRouter(missionsDir) {
|
|
|
4470
4549
|
init_servers();
|
|
4471
4550
|
init_scanner();
|
|
4472
4551
|
import { Router as Router2 } from "express";
|
|
4473
|
-
function createServersRouter(
|
|
4552
|
+
function createServersRouter(serversDir2, missionsDir) {
|
|
4474
4553
|
const router = Router2();
|
|
4475
4554
|
router.get("/", async (_req, res) => {
|
|
4476
4555
|
try {
|
|
4477
|
-
const result = await scanAllSessions(
|
|
4556
|
+
const result = await scanAllSessions(serversDir2, missionsDir);
|
|
4478
4557
|
res.json(result);
|
|
4479
4558
|
} catch (error) {
|
|
4480
4559
|
res.status(500).json({ error: error instanceof Error ? error.message : "Scan failed" });
|
|
@@ -4482,7 +4561,7 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4482
4561
|
});
|
|
4483
4562
|
router.get("/:name", async (req, res) => {
|
|
4484
4563
|
try {
|
|
4485
|
-
const session = await scanSingleSession(
|
|
4564
|
+
const session = await scanSingleSession(serversDir2, missionsDir, req.params.name);
|
|
4486
4565
|
if (!session) {
|
|
4487
4566
|
res.status(404).json({ error: "Session not found" });
|
|
4488
4567
|
return;
|
|
@@ -4500,12 +4579,12 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4500
4579
|
return;
|
|
4501
4580
|
}
|
|
4502
4581
|
const sanitized = sanitizeSessionName(name);
|
|
4503
|
-
const existing = await readSessionFile(
|
|
4582
|
+
const existing = await readSessionFile(serversDir2, sanitized);
|
|
4504
4583
|
if (existing) {
|
|
4505
4584
|
res.status(409).json({ error: `Session "${sanitized}" already registered` });
|
|
4506
4585
|
return;
|
|
4507
4586
|
}
|
|
4508
|
-
await registerSession(
|
|
4587
|
+
await registerSession(serversDir2, name);
|
|
4509
4588
|
clearScanCache();
|
|
4510
4589
|
res.status(201).json({ name: sanitized });
|
|
4511
4590
|
} catch (error) {
|
|
@@ -4514,12 +4593,12 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4514
4593
|
});
|
|
4515
4594
|
router.delete("/:name", async (req, res) => {
|
|
4516
4595
|
try {
|
|
4517
|
-
const data = await readSessionFile(
|
|
4596
|
+
const data = await readSessionFile(serversDir2, req.params.name);
|
|
4518
4597
|
if (!data) {
|
|
4519
4598
|
res.status(404).json({ error: "Session not found" });
|
|
4520
4599
|
return;
|
|
4521
4600
|
}
|
|
4522
|
-
await removeSession(
|
|
4601
|
+
await removeSession(serversDir2, req.params.name);
|
|
4523
4602
|
clearScanCache();
|
|
4524
4603
|
res.json({ removed: req.params.name });
|
|
4525
4604
|
} catch (error) {
|
|
@@ -4528,12 +4607,12 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4528
4607
|
});
|
|
4529
4608
|
router.post("/refresh", async (_req, res) => {
|
|
4530
4609
|
try {
|
|
4531
|
-
const names = await listSessionFiles(
|
|
4610
|
+
const names = await listSessionFiles(serversDir2);
|
|
4532
4611
|
for (const name of names) {
|
|
4533
|
-
await updateLastRefreshed(
|
|
4612
|
+
await updateLastRefreshed(serversDir2, name);
|
|
4534
4613
|
}
|
|
4535
4614
|
clearScanCache();
|
|
4536
|
-
const result = await scanAllSessions(
|
|
4615
|
+
const result = await scanAllSessions(serversDir2, missionsDir, { bypassCache: true });
|
|
4537
4616
|
res.json(result);
|
|
4538
4617
|
} catch (error) {
|
|
4539
4618
|
res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
|
|
@@ -4541,14 +4620,14 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4541
4620
|
});
|
|
4542
4621
|
router.post("/:name/refresh", async (req, res) => {
|
|
4543
4622
|
try {
|
|
4544
|
-
const data = await readSessionFile(
|
|
4623
|
+
const data = await readSessionFile(serversDir2, req.params.name);
|
|
4545
4624
|
if (!data) {
|
|
4546
4625
|
res.status(404).json({ error: "Session not found" });
|
|
4547
4626
|
return;
|
|
4548
4627
|
}
|
|
4549
|
-
await updateLastRefreshed(
|
|
4628
|
+
await updateLastRefreshed(serversDir2, req.params.name);
|
|
4550
4629
|
clearScanCache();
|
|
4551
|
-
const session = await scanSingleSession(
|
|
4630
|
+
const session = await scanSingleSession(serversDir2, missionsDir, req.params.name);
|
|
4552
4631
|
res.json(session);
|
|
4553
4632
|
} catch (error) {
|
|
4554
4633
|
res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
|
|
@@ -4557,7 +4636,7 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4557
4636
|
router.patch("/:name/panes/:windowIndex/:paneIndex/assignment", async (req, res) => {
|
|
4558
4637
|
try {
|
|
4559
4638
|
const { name, windowIndex, paneIndex } = req.params;
|
|
4560
|
-
const data = await readSessionFile(
|
|
4639
|
+
const data = await readSessionFile(serversDir2, name);
|
|
4561
4640
|
if (!data) {
|
|
4562
4641
|
res.status(404).json({ error: "Session not found" });
|
|
4563
4642
|
return;
|
|
@@ -4565,7 +4644,7 @@ function createServersRouter(serversDir, missionsDir) {
|
|
|
4565
4644
|
const body = req.body;
|
|
4566
4645
|
if (body === null || body && body.mission && body.assignment) {
|
|
4567
4646
|
await setOverride(
|
|
4568
|
-
|
|
4647
|
+
serversDir2,
|
|
4569
4648
|
name,
|
|
4570
4649
|
parseInt(windowIndex, 10),
|
|
4571
4650
|
parseInt(paneIndex, 10),
|
|
@@ -4701,8 +4780,8 @@ async function migrateFromMarkdown(missionsDir) {
|
|
|
4701
4780
|
return allSessions.length;
|
|
4702
4781
|
}
|
|
4703
4782
|
async function parseMarkdownSessionsIndex(filePath, missionSlug) {
|
|
4704
|
-
const { readFile:
|
|
4705
|
-
const raw = await
|
|
4783
|
+
const { readFile: readFile12 } = await import("fs/promises");
|
|
4784
|
+
const raw = await readFile12(filePath, "utf-8");
|
|
4706
4785
|
const sessions = [];
|
|
4707
4786
|
const lines = raw.split("\n");
|
|
4708
4787
|
let inTable = false;
|
|
@@ -4954,13 +5033,13 @@ init_parser();
|
|
|
4954
5033
|
init_timestamp();
|
|
4955
5034
|
import { resolve as resolve11 } from "path";
|
|
4956
5035
|
import { readdir as readdir5, readFile as readFile8 } from "fs/promises";
|
|
4957
|
-
async function rebuildPlaybookManifest(
|
|
4958
|
-
if (!await fileExists(
|
|
4959
|
-
const entries = await readdir5(
|
|
5036
|
+
async function rebuildPlaybookManifest(playbooksDir2) {
|
|
5037
|
+
if (!await fileExists(playbooksDir2)) return;
|
|
5038
|
+
const entries = await readdir5(playbooksDir2, { withFileTypes: true });
|
|
4960
5039
|
const rows = [];
|
|
4961
5040
|
for (const entry of entries) {
|
|
4962
5041
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
4963
|
-
const raw = await readFile8(resolve11(
|
|
5042
|
+
const raw = await readFile8(resolve11(playbooksDir2, entry.name), "utf-8");
|
|
4964
5043
|
const parsed = parsePlaybook(raw);
|
|
4965
5044
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
4966
5045
|
rows.push({
|
|
@@ -4990,15 +5069,15 @@ async function rebuildPlaybookManifest(playbooksDir) {
|
|
|
4990
5069
|
}
|
|
4991
5070
|
}
|
|
4992
5071
|
lines.push("");
|
|
4993
|
-
await writeFileForce(resolve11(
|
|
5072
|
+
await writeFileForce(resolve11(playbooksDir2, "manifest.md"), lines.join("\n"));
|
|
4994
5073
|
}
|
|
4995
5074
|
|
|
4996
5075
|
// src/dashboard/api-playbooks.ts
|
|
4997
|
-
function createPlaybooksRouter(
|
|
5076
|
+
function createPlaybooksRouter(playbooksDir2) {
|
|
4998
5077
|
const router = Router4();
|
|
4999
5078
|
router.get("/", async (_req, res) => {
|
|
5000
5079
|
try {
|
|
5001
|
-
const playbooks = await listPlaybooks(
|
|
5080
|
+
const playbooks = await listPlaybooks(playbooksDir2);
|
|
5002
5081
|
res.json({ playbooks, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5003
5082
|
} catch (error) {
|
|
5004
5083
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to list playbooks" });
|
|
@@ -5019,7 +5098,7 @@ function createPlaybooksRouter(playbooksDir) {
|
|
|
5019
5098
|
});
|
|
5020
5099
|
router.get("/:slug", async (req, res) => {
|
|
5021
5100
|
try {
|
|
5022
|
-
const detail = await getPlaybookDetail(
|
|
5101
|
+
const detail = await getPlaybookDetail(playbooksDir2, req.params.slug);
|
|
5023
5102
|
if (!detail) {
|
|
5024
5103
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
5025
5104
|
return;
|
|
@@ -5031,7 +5110,7 @@ function createPlaybooksRouter(playbooksDir) {
|
|
|
5031
5110
|
});
|
|
5032
5111
|
router.get("/:slug/edit", async (req, res) => {
|
|
5033
5112
|
try {
|
|
5034
|
-
const filePath = resolve12(
|
|
5113
|
+
const filePath = resolve12(playbooksDir2, `${req.params.slug}.md`);
|
|
5035
5114
|
if (!await fileExists(filePath)) {
|
|
5036
5115
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
5037
5116
|
return;
|
|
@@ -5060,14 +5139,14 @@ function createPlaybooksRouter(playbooksDir) {
|
|
|
5060
5139
|
res.status(400).json({ error: `Invalid or missing slug: "${slug}"` });
|
|
5061
5140
|
return;
|
|
5062
5141
|
}
|
|
5063
|
-
await ensureDir(
|
|
5064
|
-
const filePath = resolve12(
|
|
5142
|
+
await ensureDir(playbooksDir2);
|
|
5143
|
+
const filePath = resolve12(playbooksDir2, `${slug}.md`);
|
|
5065
5144
|
if (await fileExists(filePath)) {
|
|
5066
5145
|
res.status(409).json({ error: `Playbook "${slug}" already exists` });
|
|
5067
5146
|
return;
|
|
5068
5147
|
}
|
|
5069
5148
|
await writeFileForce(filePath, content);
|
|
5070
|
-
await rebuildPlaybookManifest(
|
|
5149
|
+
await rebuildPlaybookManifest(playbooksDir2);
|
|
5071
5150
|
res.status(201).json({ slug, path: filePath });
|
|
5072
5151
|
} catch (error) {
|
|
5073
5152
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to create playbook" });
|
|
@@ -5080,13 +5159,13 @@ function createPlaybooksRouter(playbooksDir) {
|
|
|
5080
5159
|
res.status(400).json({ error: "content is required" });
|
|
5081
5160
|
return;
|
|
5082
5161
|
}
|
|
5083
|
-
const filePath = resolve12(
|
|
5162
|
+
const filePath = resolve12(playbooksDir2, `${req.params.slug}.md`);
|
|
5084
5163
|
if (!await fileExists(filePath)) {
|
|
5085
5164
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
5086
5165
|
return;
|
|
5087
5166
|
}
|
|
5088
5167
|
await writeFileForce(filePath, content);
|
|
5089
|
-
await rebuildPlaybookManifest(
|
|
5168
|
+
await rebuildPlaybookManifest(playbooksDir2);
|
|
5090
5169
|
res.json({ slug: req.params.slug, path: filePath });
|
|
5091
5170
|
} catch (error) {
|
|
5092
5171
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update playbook" });
|
|
@@ -5098,13 +5177,13 @@ function createPlaybooksRouter(playbooksDir) {
|
|
|
5098
5177
|
res.status(403).json({ error: "The playbook manifest cannot be deleted" });
|
|
5099
5178
|
return;
|
|
5100
5179
|
}
|
|
5101
|
-
const filePath = resolve12(
|
|
5180
|
+
const filePath = resolve12(playbooksDir2, `${req.params.slug}.md`);
|
|
5102
5181
|
if (!await fileExists(filePath)) {
|
|
5103
5182
|
res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
|
|
5104
5183
|
return;
|
|
5105
5184
|
}
|
|
5106
5185
|
await unlink2(filePath);
|
|
5107
|
-
await rebuildPlaybookManifest(
|
|
5186
|
+
await rebuildPlaybookManifest(playbooksDir2);
|
|
5108
5187
|
res.json({ deleted: req.params.slug });
|
|
5109
5188
|
} catch (error) {
|
|
5110
5189
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete playbook" });
|
|
@@ -5134,7 +5213,7 @@ function withLock(workspace, fn) {
|
|
|
5134
5213
|
}));
|
|
5135
5214
|
return next;
|
|
5136
5215
|
}
|
|
5137
|
-
function createTodosRouter(
|
|
5216
|
+
function createTodosRouter(todosDir2, broadcast) {
|
|
5138
5217
|
const router = Router5();
|
|
5139
5218
|
function broadcastUpdate() {
|
|
5140
5219
|
broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -5150,14 +5229,14 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5150
5229
|
router.param("workspace", validateWorkspace);
|
|
5151
5230
|
router.get("/", async (_req, res) => {
|
|
5152
5231
|
try {
|
|
5153
|
-
await ensureDir(
|
|
5154
|
-
const files = await readdir6(
|
|
5232
|
+
await ensureDir(todosDir2);
|
|
5233
|
+
const files = await readdir6(todosDir2).catch(() => []);
|
|
5155
5234
|
const workspaces = [];
|
|
5156
5235
|
for (const file of files) {
|
|
5157
5236
|
if (typeof file !== "string") continue;
|
|
5158
5237
|
if (!file.endsWith(".md") || file.endsWith("-log.md")) continue;
|
|
5159
5238
|
const workspace = file.replace(".md", "");
|
|
5160
|
-
const checklist = await readChecklist(
|
|
5239
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5161
5240
|
workspaces.push({
|
|
5162
5241
|
workspace: checklist.workspace,
|
|
5163
5242
|
archiveInterval: checklist.archiveInterval,
|
|
@@ -5173,7 +5252,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5173
5252
|
router.get("/:workspace", async (req, res) => {
|
|
5174
5253
|
try {
|
|
5175
5254
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5176
|
-
const checklist = await readChecklist(
|
|
5255
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5177
5256
|
res.json({
|
|
5178
5257
|
workspace: checklist.workspace,
|
|
5179
5258
|
archiveInterval: checklist.archiveInterval,
|
|
@@ -5193,7 +5272,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5193
5272
|
return;
|
|
5194
5273
|
}
|
|
5195
5274
|
const item = await withLock(workspace, async () => {
|
|
5196
|
-
const checklist = await readChecklist(
|
|
5275
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5197
5276
|
const existingIds = new Set(checklist.items.map((i) => i.id));
|
|
5198
5277
|
const id = generateUniqueId(existingIds);
|
|
5199
5278
|
const newItem = {
|
|
@@ -5204,7 +5283,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5204
5283
|
session: null
|
|
5205
5284
|
};
|
|
5206
5285
|
checklist.items.push(newItem);
|
|
5207
|
-
await writeChecklist(
|
|
5286
|
+
await writeChecklist(todosDir2, checklist);
|
|
5208
5287
|
return newItem;
|
|
5209
5288
|
});
|
|
5210
5289
|
broadcastUpdate();
|
|
@@ -5222,7 +5301,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5222
5301
|
return;
|
|
5223
5302
|
}
|
|
5224
5303
|
const items = await withLock(workspace, async () => {
|
|
5225
|
-
const checklist = await readChecklist(
|
|
5304
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5226
5305
|
const itemMap = new Map(checklist.items.map((i) => [i.id, i]));
|
|
5227
5306
|
const reordered = [];
|
|
5228
5307
|
for (const id of ids) {
|
|
@@ -5236,7 +5315,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5236
5315
|
reordered.push(item);
|
|
5237
5316
|
}
|
|
5238
5317
|
checklist.items = reordered;
|
|
5239
|
-
await writeChecklist(
|
|
5318
|
+
await writeChecklist(todosDir2, checklist);
|
|
5240
5319
|
return reordered;
|
|
5241
5320
|
});
|
|
5242
5321
|
broadcastUpdate();
|
|
@@ -5247,7 +5326,7 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5247
5326
|
});
|
|
5248
5327
|
router.get("/:workspace/log", async (req, res) => {
|
|
5249
5328
|
try {
|
|
5250
|
-
const log = await readLog(
|
|
5329
|
+
const log = await readLog(todosDir2, getWorkspaceParam(req.params.workspace));
|
|
5251
5330
|
res.json(log);
|
|
5252
5331
|
} catch (error) {
|
|
5253
5332
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get log" });
|
|
@@ -5256,12 +5335,12 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5256
5335
|
router.post("/:workspace/archive", async (req, res) => {
|
|
5257
5336
|
try {
|
|
5258
5337
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
5259
|
-
const { resolve:
|
|
5260
|
-
const { readFile:
|
|
5338
|
+
const { resolve: resolve16 } = await import("path");
|
|
5339
|
+
const { readFile: readFile12 } = await import("fs/promises");
|
|
5261
5340
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
5262
5341
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5263
|
-
const checklist = await readChecklist(
|
|
5264
|
-
const log = await readLog(
|
|
5342
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5343
|
+
const log = await readLog(todosDir2, workspace);
|
|
5265
5344
|
const completedIds = new Set(
|
|
5266
5345
|
checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
|
|
5267
5346
|
);
|
|
@@ -5272,11 +5351,11 @@ function createTodosRouter(todosDir, broadcast) {
|
|
|
5272
5351
|
const toArchive = log.entries.filter(
|
|
5273
5352
|
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
5274
5353
|
);
|
|
5275
|
-
const archFile = archivePath2(
|
|
5276
|
-
await ensureDir(
|
|
5354
|
+
const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
|
|
5355
|
+
await ensureDir(resolve16(todosDir2, "archive"));
|
|
5277
5356
|
let archContent = "";
|
|
5278
5357
|
if (await fileExists(archFile)) {
|
|
5279
|
-
archContent = await
|
|
5358
|
+
archContent = await readFile12(archFile, "utf-8");
|
|
5280
5359
|
archContent = archContent.trimEnd() + "\n\n";
|
|
5281
5360
|
} else {
|
|
5282
5361
|
archContent = `---
|
|
@@ -5310,7 +5389,7 @@ workspace: ${workspace}
|
|
|
5310
5389
|
}
|
|
5311
5390
|
await writeFileForce2(archFile, archContent);
|
|
5312
5391
|
checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
|
|
5313
|
-
await writeChecklist(
|
|
5392
|
+
await writeChecklist(todosDir2, checklist);
|
|
5314
5393
|
broadcastUpdate();
|
|
5315
5394
|
res.json({ archived: completedIds.size, logEntries: toArchive.length });
|
|
5316
5395
|
} catch (error) {
|
|
@@ -5319,7 +5398,7 @@ workspace: ${workspace}
|
|
|
5319
5398
|
});
|
|
5320
5399
|
router.get("/:workspace/log/:id", async (req, res) => {
|
|
5321
5400
|
try {
|
|
5322
|
-
const log = await readLog(
|
|
5401
|
+
const log = await readLog(todosDir2, getWorkspaceParam(req.params.workspace));
|
|
5323
5402
|
const entries = log.entries.filter((e) => e.itemIds.includes(req.params.id));
|
|
5324
5403
|
res.json({ workspace: log.workspace, entries });
|
|
5325
5404
|
} catch (error) {
|
|
@@ -5329,13 +5408,13 @@ workspace: ${workspace}
|
|
|
5329
5408
|
router.get("/:workspace/:id", async (req, res) => {
|
|
5330
5409
|
try {
|
|
5331
5410
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5332
|
-
const checklist = await readChecklist(
|
|
5411
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5333
5412
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5334
5413
|
if (!item) {
|
|
5335
5414
|
res.status(404).json({ error: `Todo "${req.params.id}" not found` });
|
|
5336
5415
|
return;
|
|
5337
5416
|
}
|
|
5338
|
-
const log = await readLog(
|
|
5417
|
+
const log = await readLog(todosDir2, workspace);
|
|
5339
5418
|
const logEntries = log.entries.filter((e) => e.itemIds.includes(req.params.id));
|
|
5340
5419
|
res.json({ ...item, log: logEntries });
|
|
5341
5420
|
} catch (error) {
|
|
@@ -5346,12 +5425,12 @@ workspace: ${workspace}
|
|
|
5346
5425
|
try {
|
|
5347
5426
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5348
5427
|
const result = await withLock(workspace, async () => {
|
|
5349
|
-
const checklist = await readChecklist(
|
|
5428
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5350
5429
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5351
5430
|
if (!item) return null;
|
|
5352
5431
|
if (req.body.description !== void 0) item.description = req.body.description;
|
|
5353
5432
|
if (Array.isArray(req.body.tags)) item.tags = req.body.tags;
|
|
5354
|
-
await writeChecklist(
|
|
5433
|
+
await writeChecklist(todosDir2, checklist);
|
|
5355
5434
|
return { ...item };
|
|
5356
5435
|
});
|
|
5357
5436
|
if (!result) {
|
|
@@ -5368,11 +5447,11 @@ workspace: ${workspace}
|
|
|
5368
5447
|
try {
|
|
5369
5448
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5370
5449
|
const deleted = await withLock(workspace, async () => {
|
|
5371
|
-
const checklist = await readChecklist(
|
|
5450
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5372
5451
|
const idx = checklist.items.findIndex((i) => i.id === req.params.id);
|
|
5373
5452
|
if (idx === -1) return false;
|
|
5374
5453
|
checklist.items.splice(idx, 1);
|
|
5375
|
-
await writeChecklist(
|
|
5454
|
+
await writeChecklist(todosDir2, checklist);
|
|
5376
5455
|
return true;
|
|
5377
5456
|
});
|
|
5378
5457
|
if (!deleted) {
|
|
@@ -5389,13 +5468,13 @@ workspace: ${workspace}
|
|
|
5389
5468
|
try {
|
|
5390
5469
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5391
5470
|
const result = await withLock(workspace, async () => {
|
|
5392
|
-
const checklist = await readChecklist(
|
|
5471
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5393
5472
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5394
5473
|
if (!item) return { error: "not_found" };
|
|
5395
5474
|
if (item.status === "in_progress") return { error: "conflict", session: item.session };
|
|
5396
5475
|
item.status = "in_progress";
|
|
5397
5476
|
item.session = req.body.session || null;
|
|
5398
|
-
await writeChecklist(
|
|
5477
|
+
await writeChecklist(todosDir2, checklist);
|
|
5399
5478
|
return { item: { ...item } };
|
|
5400
5479
|
});
|
|
5401
5480
|
if ("error" in result) {
|
|
@@ -5416,12 +5495,12 @@ workspace: ${workspace}
|
|
|
5416
5495
|
try {
|
|
5417
5496
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5418
5497
|
const result = await withLock(workspace, async () => {
|
|
5419
|
-
const checklist = await readChecklist(
|
|
5498
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5420
5499
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5421
5500
|
if (!item) return null;
|
|
5422
5501
|
item.status = "completed";
|
|
5423
5502
|
item.session = null;
|
|
5424
|
-
await writeChecklist(
|
|
5503
|
+
await writeChecklist(todosDir2, checklist);
|
|
5425
5504
|
const entry = {
|
|
5426
5505
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5427
5506
|
itemIds: [item.id],
|
|
@@ -5432,7 +5511,7 @@ workspace: ${workspace}
|
|
|
5432
5511
|
blockers: null,
|
|
5433
5512
|
status: null
|
|
5434
5513
|
};
|
|
5435
|
-
await appendLogEntry2(
|
|
5514
|
+
await appendLogEntry2(todosDir2, workspace, entry);
|
|
5436
5515
|
return { ...item };
|
|
5437
5516
|
});
|
|
5438
5517
|
if (!result) {
|
|
@@ -5450,12 +5529,12 @@ workspace: ${workspace}
|
|
|
5450
5529
|
const reason = req.body.reason || null;
|
|
5451
5530
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5452
5531
|
const result = await withLock(workspace, async () => {
|
|
5453
|
-
const checklist = await readChecklist(
|
|
5532
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5454
5533
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5455
5534
|
if (!item) return null;
|
|
5456
5535
|
item.status = "blocked";
|
|
5457
5536
|
item.session = null;
|
|
5458
|
-
await writeChecklist(
|
|
5537
|
+
await writeChecklist(todosDir2, checklist);
|
|
5459
5538
|
const entry = {
|
|
5460
5539
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5461
5540
|
itemIds: [item.id],
|
|
@@ -5466,7 +5545,7 @@ workspace: ${workspace}
|
|
|
5466
5545
|
blockers: reason,
|
|
5467
5546
|
status: "blocked"
|
|
5468
5547
|
};
|
|
5469
|
-
await appendLogEntry2(
|
|
5548
|
+
await appendLogEntry2(todosDir2, workspace, entry);
|
|
5470
5549
|
return { ...item };
|
|
5471
5550
|
});
|
|
5472
5551
|
if (!result) {
|
|
@@ -5483,12 +5562,12 @@ workspace: ${workspace}
|
|
|
5483
5562
|
try {
|
|
5484
5563
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5485
5564
|
const result = await withLock(workspace, async () => {
|
|
5486
|
-
const checklist = await readChecklist(
|
|
5565
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5487
5566
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5488
5567
|
if (!item) return null;
|
|
5489
5568
|
item.status = "open";
|
|
5490
5569
|
item.session = null;
|
|
5491
|
-
await writeChecklist(
|
|
5570
|
+
await writeChecklist(todosDir2, checklist);
|
|
5492
5571
|
return { ...item };
|
|
5493
5572
|
});
|
|
5494
5573
|
if (!result) {
|
|
@@ -5505,12 +5584,12 @@ workspace: ${workspace}
|
|
|
5505
5584
|
try {
|
|
5506
5585
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
5507
5586
|
const result = await withLock(workspace, async () => {
|
|
5508
|
-
const checklist = await readChecklist(
|
|
5587
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
5509
5588
|
const item = checklist.items.find((i) => i.id === req.params.id);
|
|
5510
5589
|
if (!item) return null;
|
|
5511
5590
|
item.status = "open";
|
|
5512
5591
|
item.session = null;
|
|
5513
|
-
await writeChecklist(
|
|
5592
|
+
await writeChecklist(todosDir2, checklist);
|
|
5514
5593
|
return { ...item };
|
|
5515
5594
|
});
|
|
5516
5595
|
if (!result) {
|
|
@@ -5526,6 +5605,411 @@ workspace: ${workspace}
|
|
|
5526
5605
|
return router;
|
|
5527
5606
|
}
|
|
5528
5607
|
|
|
5608
|
+
// src/dashboard/api-backup.ts
|
|
5609
|
+
init_config2();
|
|
5610
|
+
import { Router as Router6 } from "express";
|
|
5611
|
+
|
|
5612
|
+
// src/utils/github-backup.ts
|
|
5613
|
+
init_paths();
|
|
5614
|
+
init_fs();
|
|
5615
|
+
init_config2();
|
|
5616
|
+
import { execFile as execFile2 } from "child_process";
|
|
5617
|
+
import { promisify as promisify2 } from "util";
|
|
5618
|
+
import { cp, mkdtemp, rm as rm2, readFile as readFile11, writeFile as writeFile3, unlink as unlink3, stat, open, rename as rename2 } from "fs/promises";
|
|
5619
|
+
import { resolve as resolve14, join as join2 } from "path";
|
|
5620
|
+
import { tmpdir } from "os";
|
|
5621
|
+
var exec2 = promisify2(execFile2);
|
|
5622
|
+
var VALID_CATEGORIES = ["missions", "playbooks", "todos", "servers", "config"];
|
|
5623
|
+
var LOCK_FILE_NAME = ".backup-lock";
|
|
5624
|
+
function parseCategoriesStrict(cats) {
|
|
5625
|
+
const unknown = [];
|
|
5626
|
+
const valid = [];
|
|
5627
|
+
for (const cat of cats) {
|
|
5628
|
+
if (VALID_CATEGORIES.includes(cat)) {
|
|
5629
|
+
valid.push(cat);
|
|
5630
|
+
} else {
|
|
5631
|
+
unknown.push(cat);
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
if (unknown.length > 0) {
|
|
5635
|
+
throw new Error(
|
|
5636
|
+
`Unknown categor${unknown.length === 1 ? "y" : "ies"}: ${unknown.map((c) => `"${c}"`).join(", ")}. Valid: ${VALID_CATEGORIES.join(", ")}`
|
|
5637
|
+
);
|
|
5638
|
+
}
|
|
5639
|
+
return valid;
|
|
5640
|
+
}
|
|
5641
|
+
function validateRepoUrl(url) {
|
|
5642
|
+
if (!url || typeof url !== "string") return false;
|
|
5643
|
+
const trimmed = url.trim();
|
|
5644
|
+
return trimmed.startsWith("https://") || trimmed.startsWith("git@");
|
|
5645
|
+
}
|
|
5646
|
+
async function resolveCategoryPath(category) {
|
|
5647
|
+
switch (category) {
|
|
5648
|
+
case "missions": {
|
|
5649
|
+
const config = await readConfig();
|
|
5650
|
+
return { sourcePath: config.defaultMissionDir, repoPath: "missions", isFile: false };
|
|
5651
|
+
}
|
|
5652
|
+
case "playbooks":
|
|
5653
|
+
return { sourcePath: playbooksDir(), repoPath: "playbooks", isFile: false };
|
|
5654
|
+
case "todos":
|
|
5655
|
+
return { sourcePath: todosDir(), repoPath: "todos", isFile: false };
|
|
5656
|
+
case "servers":
|
|
5657
|
+
return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
|
|
5658
|
+
case "config":
|
|
5659
|
+
return { sourcePath: resolve14(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
async function checkGitInstalled() {
|
|
5663
|
+
try {
|
|
5664
|
+
await exec2("git", ["--version"]);
|
|
5665
|
+
} catch {
|
|
5666
|
+
throw new Error("git is not installed or not on PATH. Install git and try again.");
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
async function acquireLock() {
|
|
5670
|
+
const lockPath = resolve14(syntaurRoot(), LOCK_FILE_NAME);
|
|
5671
|
+
await ensureDir(syntaurRoot());
|
|
5672
|
+
try {
|
|
5673
|
+
const handle = await open(lockPath, "wx");
|
|
5674
|
+
await handle.write(String(process.pid));
|
|
5675
|
+
await handle.close();
|
|
5676
|
+
return lockPath;
|
|
5677
|
+
} catch (err) {
|
|
5678
|
+
if (err.code === "EEXIST") {
|
|
5679
|
+
const pid = await readFile11(lockPath, "utf-8").catch(() => "");
|
|
5680
|
+
throw new Error(
|
|
5681
|
+
`Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
|
|
5682
|
+
);
|
|
5683
|
+
}
|
|
5684
|
+
throw err;
|
|
5685
|
+
}
|
|
5686
|
+
}
|
|
5687
|
+
async function releaseLock(lockPath) {
|
|
5688
|
+
try {
|
|
5689
|
+
await unlink3(lockPath);
|
|
5690
|
+
} catch {
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
async function runGit(args, cwd) {
|
|
5694
|
+
return exec2("git", args, { cwd });
|
|
5695
|
+
}
|
|
5696
|
+
async function cloneOrInit(repoUrl, destDir) {
|
|
5697
|
+
try {
|
|
5698
|
+
await exec2("git", ["clone", "--depth", "1", repoUrl, destDir]);
|
|
5699
|
+
} catch (error) {
|
|
5700
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5701
|
+
if (message.includes("Repository not found") || message.includes("does not appear to be a git repository")) {
|
|
5702
|
+
throw new Error(`Repository not found or inaccessible: ${repoUrl}. Check URL and credentials.`);
|
|
5703
|
+
}
|
|
5704
|
+
if (message.includes("Authentication failed") || message.includes("could not read Username")) {
|
|
5705
|
+
throw new Error(`Authentication failed for ${repoUrl}. Check SSH keys or credentials.`);
|
|
5706
|
+
}
|
|
5707
|
+
throw new Error(`git clone failed: ${message}`);
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
async function copyRecursive(src, dest) {
|
|
5711
|
+
if (!await fileExists(src)) return;
|
|
5712
|
+
const s = await stat(src);
|
|
5713
|
+
if (s.isDirectory()) {
|
|
5714
|
+
await ensureDir(dest);
|
|
5715
|
+
await cp(src, dest, { recursive: true, force: true });
|
|
5716
|
+
} else {
|
|
5717
|
+
await ensureDir(resolve14(dest, ".."));
|
|
5718
|
+
await cp(src, dest, { force: true });
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
function resolveCategoriesStrict(csv) {
|
|
5722
|
+
const parts = csv.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5723
|
+
return parseCategoriesStrict(parts);
|
|
5724
|
+
}
|
|
5725
|
+
async function readSanitizedConfig(configPath) {
|
|
5726
|
+
const content = await readFile11(configPath, "utf-8");
|
|
5727
|
+
return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
|
|
5728
|
+
}
|
|
5729
|
+
async function backupToGithub(overrides) {
|
|
5730
|
+
await checkGitInstalled();
|
|
5731
|
+
const config = await readConfig();
|
|
5732
|
+
const rawRepo = overrides?.repo ?? config.backup?.repo ?? null;
|
|
5733
|
+
if (!rawRepo) {
|
|
5734
|
+
throw new Error("No backup repo configured. Set it via `syntaur backup config --repo <url>` or the dashboard.");
|
|
5735
|
+
}
|
|
5736
|
+
const repo = rawRepo.trim();
|
|
5737
|
+
if (!validateRepoUrl(repo)) {
|
|
5738
|
+
throw new Error(`Invalid repo URL: "${rawRepo}". Must start with https:// or git@.`);
|
|
5739
|
+
}
|
|
5740
|
+
const categoriesCsv = config.backup?.categories ?? "missions, playbooks, todos, servers, config";
|
|
5741
|
+
const categories = overrides?.categories ?? resolveCategoriesStrict(categoriesCsv);
|
|
5742
|
+
if (categories.length === 0) {
|
|
5743
|
+
throw new Error("No valid backup categories selected.");
|
|
5744
|
+
}
|
|
5745
|
+
const lockPath = await acquireLock();
|
|
5746
|
+
let tmpDir = null;
|
|
5747
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5748
|
+
try {
|
|
5749
|
+
tmpDir = await mkdtemp(join2(tmpdir(), "syntaur-backup-"));
|
|
5750
|
+
await cloneOrInit(repo, tmpDir);
|
|
5751
|
+
for (const category of categories) {
|
|
5752
|
+
const { sourcePath, repoPath, isFile } = await resolveCategoryPath(category);
|
|
5753
|
+
const destPath = join2(tmpDir, repoPath);
|
|
5754
|
+
if (isFile) {
|
|
5755
|
+
await rm2(destPath, { force: true });
|
|
5756
|
+
} else {
|
|
5757
|
+
await rm2(destPath, { recursive: true, force: true });
|
|
5758
|
+
}
|
|
5759
|
+
if (!await fileExists(sourcePath)) {
|
|
5760
|
+
console.warn(`Category "${category}": no local data at ${sourcePath}; backup will reflect deletion.`);
|
|
5761
|
+
continue;
|
|
5762
|
+
}
|
|
5763
|
+
if (category === "config") {
|
|
5764
|
+
const sanitized = await readSanitizedConfig(sourcePath);
|
|
5765
|
+
await ensureDir(resolve14(destPath, ".."));
|
|
5766
|
+
await writeFile3(destPath, sanitized, "utf-8");
|
|
5767
|
+
} else {
|
|
5768
|
+
await copyRecursive(sourcePath, destPath);
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
await runGit(["add", "-A"], tmpDir);
|
|
5772
|
+
const { stdout: status } = await runGit(["status", "--porcelain"], tmpDir);
|
|
5773
|
+
if (!status.trim()) {
|
|
5774
|
+
await updateBackupConfig({ lastBackup: timestamp }).catch(() => {
|
|
5775
|
+
});
|
|
5776
|
+
return {
|
|
5777
|
+
success: true,
|
|
5778
|
+
timestamp,
|
|
5779
|
+
message: "No changes to back up.",
|
|
5780
|
+
committed: false
|
|
5781
|
+
};
|
|
5782
|
+
}
|
|
5783
|
+
try {
|
|
5784
|
+
await runGit(["config", "user.email", "syntaur@local"], tmpDir);
|
|
5785
|
+
await runGit(["config", "user.name", "Syntaur Backup"], tmpDir);
|
|
5786
|
+
} catch {
|
|
5787
|
+
}
|
|
5788
|
+
await runGit(["commit", "-m", `Syntaur backup ${timestamp}`], tmpDir);
|
|
5789
|
+
try {
|
|
5790
|
+
await runGit(["push"], tmpDir);
|
|
5791
|
+
} catch (error) {
|
|
5792
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5793
|
+
if (message.includes("non-fast-forward") || message.includes("rejected")) {
|
|
5794
|
+
throw new Error("Push rejected (non-fast-forward). Pull and resolve manually, or delete remote contents.");
|
|
5795
|
+
}
|
|
5796
|
+
if (message.includes("Authentication") || message.includes("could not read Username")) {
|
|
5797
|
+
throw new Error("Push authentication failed. Check SSH keys or credentials.");
|
|
5798
|
+
}
|
|
5799
|
+
throw new Error(`git push failed: ${message}`);
|
|
5800
|
+
}
|
|
5801
|
+
await updateBackupConfig({ lastBackup: timestamp }).catch(() => {
|
|
5802
|
+
});
|
|
5803
|
+
return {
|
|
5804
|
+
success: true,
|
|
5805
|
+
timestamp,
|
|
5806
|
+
message: `Backed up ${categories.length} categor${categories.length === 1 ? "y" : "ies"} to ${repo}.`,
|
|
5807
|
+
committed: true
|
|
5808
|
+
};
|
|
5809
|
+
} finally {
|
|
5810
|
+
if (tmpDir) {
|
|
5811
|
+
await rm2(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
5812
|
+
});
|
|
5813
|
+
}
|
|
5814
|
+
await releaseLock(lockPath);
|
|
5815
|
+
}
|
|
5816
|
+
}
|
|
5817
|
+
async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
5818
|
+
if (isFile) {
|
|
5819
|
+
await ensureDir(resolve14(localPath, ".."));
|
|
5820
|
+
await cp(repoSrcPath, localPath, { force: true });
|
|
5821
|
+
return;
|
|
5822
|
+
}
|
|
5823
|
+
const stagingPath = `${localPath}.syntaur-restore-staging`;
|
|
5824
|
+
const backupPath = `${localPath}.syntaur-restore-backup`;
|
|
5825
|
+
await rm2(stagingPath, { recursive: true, force: true });
|
|
5826
|
+
const backupExistsBefore = await fileExists(backupPath);
|
|
5827
|
+
const localExistsBefore = await fileExists(localPath);
|
|
5828
|
+
if (backupExistsBefore) {
|
|
5829
|
+
if (!localExistsBefore) {
|
|
5830
|
+
await rename2(backupPath, localPath);
|
|
5831
|
+
} else {
|
|
5832
|
+
throw new Error(
|
|
5833
|
+
`Cannot restore "${localPath}": a stale crash-recovery backup exists at ${backupPath} while the current path also exists. Inspect both and remove the one you don't need, then retry.`
|
|
5834
|
+
);
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
let localMovedAside = false;
|
|
5838
|
+
try {
|
|
5839
|
+
await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
|
|
5840
|
+
const localExists = await fileExists(localPath);
|
|
5841
|
+
if (localExists) {
|
|
5842
|
+
await rename2(localPath, backupPath);
|
|
5843
|
+
localMovedAside = true;
|
|
5844
|
+
}
|
|
5845
|
+
await rename2(stagingPath, localPath);
|
|
5846
|
+
await rm2(backupPath, { recursive: true, force: true }).catch(() => {
|
|
5847
|
+
});
|
|
5848
|
+
} catch (err) {
|
|
5849
|
+
if (localMovedAside && await fileExists(backupPath)) {
|
|
5850
|
+
await rename2(backupPath, localPath).catch(() => {
|
|
5851
|
+
});
|
|
5852
|
+
}
|
|
5853
|
+
await rm2(stagingPath, { recursive: true, force: true }).catch(() => {
|
|
5854
|
+
});
|
|
5855
|
+
throw err;
|
|
5856
|
+
}
|
|
5857
|
+
}
|
|
5858
|
+
async function restoreFromGithub(overrides) {
|
|
5859
|
+
await checkGitInstalled();
|
|
5860
|
+
const config = await readConfig();
|
|
5861
|
+
const rawRepo = overrides?.repo ?? config.backup?.repo ?? null;
|
|
5862
|
+
if (!rawRepo) {
|
|
5863
|
+
throw new Error("No backup repo configured.");
|
|
5864
|
+
}
|
|
5865
|
+
const repo = rawRepo.trim();
|
|
5866
|
+
if (!validateRepoUrl(repo)) {
|
|
5867
|
+
throw new Error(`Invalid repo URL: "${rawRepo}".`);
|
|
5868
|
+
}
|
|
5869
|
+
const categoriesCsv = config.backup?.categories ?? "missions, playbooks, todos, servers, config";
|
|
5870
|
+
const categories = overrides?.categories ?? resolveCategoriesStrict(categoriesCsv);
|
|
5871
|
+
if (categories.length === 0) {
|
|
5872
|
+
throw new Error("No valid restore categories selected.");
|
|
5873
|
+
}
|
|
5874
|
+
const lockPath = await acquireLock();
|
|
5875
|
+
let tmpDir = null;
|
|
5876
|
+
const restored = [];
|
|
5877
|
+
const failed = [];
|
|
5878
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5879
|
+
try {
|
|
5880
|
+
await updateBackupConfig({ lastRestore: timestamp });
|
|
5881
|
+
tmpDir = await mkdtemp(join2(tmpdir(), "syntaur-restore-"));
|
|
5882
|
+
await cloneOrInit(repo, tmpDir);
|
|
5883
|
+
for (const category of categories) {
|
|
5884
|
+
if (category === "config") {
|
|
5885
|
+
console.warn('Skipping "config" on restore (would overwrite local backup settings).');
|
|
5886
|
+
continue;
|
|
5887
|
+
}
|
|
5888
|
+
try {
|
|
5889
|
+
const { sourcePath: localPath, repoPath, isFile } = await resolveCategoryPath(category);
|
|
5890
|
+
const repoSrcPath = join2(tmpDir, repoPath);
|
|
5891
|
+
if (!await fileExists(repoSrcPath)) {
|
|
5892
|
+
console.warn(`Category "${category}" not found in backup repo, skipping.`);
|
|
5893
|
+
continue;
|
|
5894
|
+
}
|
|
5895
|
+
await safeRestoreCategory(localPath, repoSrcPath, isFile);
|
|
5896
|
+
restored.push(category);
|
|
5897
|
+
} catch (error) {
|
|
5898
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
5899
|
+
console.error(`Failed to restore "${category}": ${msg}`);
|
|
5900
|
+
failed.push(category);
|
|
5901
|
+
}
|
|
5902
|
+
}
|
|
5903
|
+
const success = failed.length === 0;
|
|
5904
|
+
return {
|
|
5905
|
+
success,
|
|
5906
|
+
timestamp,
|
|
5907
|
+
message: success ? `Restored ${restored.length} categor${restored.length === 1 ? "y" : "ies"} from ${repo}.` : `Partial restore: ${restored.length} succeeded, ${failed.length} failed (${failed.join(", ")}).`,
|
|
5908
|
+
committed: false
|
|
5909
|
+
};
|
|
5910
|
+
} finally {
|
|
5911
|
+
if (tmpDir) {
|
|
5912
|
+
await rm2(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
5913
|
+
});
|
|
5914
|
+
}
|
|
5915
|
+
await releaseLock(lockPath);
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
async function getBackupStatus() {
|
|
5919
|
+
const config = await readConfig();
|
|
5920
|
+
const lockPath = resolve14(syntaurRoot(), LOCK_FILE_NAME);
|
|
5921
|
+
const locked = await fileExists(lockPath);
|
|
5922
|
+
return {
|
|
5923
|
+
repo: config.backup?.repo ?? null,
|
|
5924
|
+
categories: config.backup?.categories ?? "missions, playbooks, todos, servers, config",
|
|
5925
|
+
lastBackup: config.backup?.lastBackup ?? null,
|
|
5926
|
+
lastRestore: config.backup?.lastRestore ?? null,
|
|
5927
|
+
locked
|
|
5928
|
+
};
|
|
5929
|
+
}
|
|
5930
|
+
|
|
5931
|
+
// src/dashboard/api-backup.ts
|
|
5932
|
+
function createBackupRouter() {
|
|
5933
|
+
const router = Router6();
|
|
5934
|
+
router.get("/", async (_req, res) => {
|
|
5935
|
+
try {
|
|
5936
|
+
const status = await getBackupStatus();
|
|
5937
|
+
res.json(status);
|
|
5938
|
+
} catch (error) {
|
|
5939
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
5940
|
+
}
|
|
5941
|
+
});
|
|
5942
|
+
router.put("/config", async (req, res) => {
|
|
5943
|
+
try {
|
|
5944
|
+
const body = req.body ?? {};
|
|
5945
|
+
const updates = {};
|
|
5946
|
+
if (body.repo !== void 0) {
|
|
5947
|
+
const trimmed = typeof body.repo === "string" ? body.repo.trim() : body.repo;
|
|
5948
|
+
if (trimmed !== null && trimmed !== "" && !validateRepoUrl(trimmed)) {
|
|
5949
|
+
return res.status(400).json({
|
|
5950
|
+
error: `Invalid repo URL. Must start with https:// or git@.`
|
|
5951
|
+
});
|
|
5952
|
+
}
|
|
5953
|
+
updates.repo = trimmed || null;
|
|
5954
|
+
}
|
|
5955
|
+
if (body.categories !== void 0) {
|
|
5956
|
+
let cats;
|
|
5957
|
+
if (Array.isArray(body.categories)) {
|
|
5958
|
+
cats = body.categories.map((s) => String(s).trim()).filter(Boolean);
|
|
5959
|
+
} else if (typeof body.categories === "string") {
|
|
5960
|
+
cats = body.categories.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5961
|
+
} else {
|
|
5962
|
+
return res.status(400).json({ error: "categories must be a string or array" });
|
|
5963
|
+
}
|
|
5964
|
+
if (cats.length === 0) {
|
|
5965
|
+
return res.status(400).json({
|
|
5966
|
+
error: `No categories provided. Valid: ${VALID_CATEGORIES.join(", ")}`
|
|
5967
|
+
});
|
|
5968
|
+
}
|
|
5969
|
+
try {
|
|
5970
|
+
const valid = parseCategoriesStrict(cats);
|
|
5971
|
+
updates.categories = valid.join(", ");
|
|
5972
|
+
} catch (err) {
|
|
5973
|
+
return res.status(400).json({
|
|
5974
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5975
|
+
});
|
|
5976
|
+
}
|
|
5977
|
+
}
|
|
5978
|
+
if (Object.keys(updates).length === 0) {
|
|
5979
|
+
return res.status(400).json({ error: "No fields to update" });
|
|
5980
|
+
}
|
|
5981
|
+
await updateBackupConfig(updates);
|
|
5982
|
+
const status = await getBackupStatus();
|
|
5983
|
+
res.json(status);
|
|
5984
|
+
} catch (error) {
|
|
5985
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
5986
|
+
}
|
|
5987
|
+
});
|
|
5988
|
+
router.post("/push", async (_req, res) => {
|
|
5989
|
+
try {
|
|
5990
|
+
const result = await backupToGithub();
|
|
5991
|
+
res.json(result);
|
|
5992
|
+
} catch (error) {
|
|
5993
|
+
res.status(500).json({
|
|
5994
|
+
success: false,
|
|
5995
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5996
|
+
});
|
|
5997
|
+
}
|
|
5998
|
+
});
|
|
5999
|
+
router.post("/pull", async (_req, res) => {
|
|
6000
|
+
try {
|
|
6001
|
+
const result = await restoreFromGithub();
|
|
6002
|
+
res.json(result);
|
|
6003
|
+
} catch (error) {
|
|
6004
|
+
res.status(500).json({
|
|
6005
|
+
success: false,
|
|
6006
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6007
|
+
});
|
|
6008
|
+
}
|
|
6009
|
+
});
|
|
6010
|
+
return router;
|
|
6011
|
+
}
|
|
6012
|
+
|
|
5529
6013
|
// src/dashboard/autodiscovery.ts
|
|
5530
6014
|
init_scanner();
|
|
5531
6015
|
init_servers();
|
|
@@ -5599,7 +6083,7 @@ async function listAllTmuxSessions() {
|
|
|
5599
6083
|
if (!output) return [];
|
|
5600
6084
|
return output.split("\n").filter((line) => line.length > 0);
|
|
5601
6085
|
}
|
|
5602
|
-
async function discoverTmuxSessions(
|
|
6086
|
+
async function discoverTmuxSessions(serversDir2, missionsDir, existingNames) {
|
|
5603
6087
|
const tmuxAvailable = await checkTmuxAvailable();
|
|
5604
6088
|
if (!tmuxAvailable) return false;
|
|
5605
6089
|
const workspaceRecords = await loadWorkspaceRecords(missionsDir);
|
|
@@ -5627,7 +6111,7 @@ async function discoverTmuxSessions(serversDir, missionsDir, existingNames) {
|
|
|
5627
6111
|
}
|
|
5628
6112
|
}
|
|
5629
6113
|
if (matched) {
|
|
5630
|
-
await registerAutoSession(
|
|
6114
|
+
await registerAutoSession(serversDir2, sessionName, { kind: "tmux" });
|
|
5631
6115
|
changed = true;
|
|
5632
6116
|
}
|
|
5633
6117
|
}
|
|
@@ -5643,7 +6127,7 @@ async function getProcessCwd(pid) {
|
|
|
5643
6127
|
}
|
|
5644
6128
|
return null;
|
|
5645
6129
|
}
|
|
5646
|
-
async function discoverProcesses(
|
|
6130
|
+
async function discoverProcesses(serversDir2, missionsDir, existingFiles, excludePids) {
|
|
5647
6131
|
const workspaceRecords = await loadWorkspaceRecords(missionsDir);
|
|
5648
6132
|
if (workspaceRecords.length === 0) return false;
|
|
5649
6133
|
const lsofOutput = await getLsofOutput();
|
|
@@ -5668,7 +6152,7 @@ async function discoverProcesses(serversDir, missionsDir, existingFiles, exclude
|
|
|
5668
6152
|
const sanitized = sanitizeSessionName(sessionName);
|
|
5669
6153
|
if (existingFiles.has(sanitized)) continue;
|
|
5670
6154
|
const ports = parsePortsForPid(lsofOutput, proc.pid);
|
|
5671
|
-
await registerAutoSession(
|
|
6155
|
+
await registerAutoSession(serversDir2, sessionName, {
|
|
5672
6156
|
kind: "process",
|
|
5673
6157
|
pid: proc.pid,
|
|
5674
6158
|
ports: ports.length > 0 ? ports : [proc.port],
|
|
@@ -5678,7 +6162,7 @@ async function discoverProcesses(serversDir, missionsDir, existingFiles, exclude
|
|
|
5678
6162
|
}
|
|
5679
6163
|
return changed;
|
|
5680
6164
|
}
|
|
5681
|
-
async function cleanupDeadAutoSessions(
|
|
6165
|
+
async function cleanupDeadAutoSessions(serversDir2, existingFiles) {
|
|
5682
6166
|
let changed = false;
|
|
5683
6167
|
const removedNames = /* @__PURE__ */ new Set();
|
|
5684
6168
|
const tmuxAvailable = await checkTmuxAvailable();
|
|
@@ -5694,7 +6178,7 @@ async function cleanupDeadAutoSessions(serversDir, existingFiles) {
|
|
|
5694
6178
|
continue;
|
|
5695
6179
|
}
|
|
5696
6180
|
if (!alive) {
|
|
5697
|
-
await removeSession(
|
|
6181
|
+
await removeSession(serversDir2, name);
|
|
5698
6182
|
removedNames.add(name);
|
|
5699
6183
|
changed = true;
|
|
5700
6184
|
}
|
|
@@ -5709,20 +6193,20 @@ async function isProcessAlive(pid) {
|
|
|
5709
6193
|
return false;
|
|
5710
6194
|
}
|
|
5711
6195
|
}
|
|
5712
|
-
async function reconcile(
|
|
5713
|
-
const names = await listSessionFiles(
|
|
6196
|
+
async function reconcile(serversDir2, missionsDir, excludePids) {
|
|
6197
|
+
const names = await listSessionFiles(serversDir2);
|
|
5714
6198
|
const existingFiles = /* @__PURE__ */ new Map();
|
|
5715
6199
|
for (const name of names) {
|
|
5716
|
-
const data = await readSessionFile(
|
|
6200
|
+
const data = await readSessionFile(serversDir2, name);
|
|
5717
6201
|
if (data) existingFiles.set(name, data);
|
|
5718
6202
|
}
|
|
5719
|
-
const { changed: cleanupChanged, removedNames } = await cleanupDeadAutoSessions(
|
|
6203
|
+
const { changed: cleanupChanged, removedNames } = await cleanupDeadAutoSessions(serversDir2, existingFiles);
|
|
5720
6204
|
for (const name of removedNames) {
|
|
5721
6205
|
existingFiles.delete(name);
|
|
5722
6206
|
}
|
|
5723
6207
|
const existingNames = new Set(existingFiles.keys());
|
|
5724
|
-
const tmuxChanged = await discoverTmuxSessions(
|
|
5725
|
-
const processChanged = await discoverProcesses(
|
|
6208
|
+
const tmuxChanged = await discoverTmuxSessions(serversDir2, missionsDir, existingNames);
|
|
6209
|
+
const processChanged = await discoverProcesses(serversDir2, missionsDir, existingFiles, excludePids);
|
|
5726
6210
|
if (tmuxChanged || processChanged || cleanupChanged) {
|
|
5727
6211
|
clearScanCache();
|
|
5728
6212
|
}
|
|
@@ -5730,7 +6214,7 @@ async function reconcile(serversDir, missionsDir, excludePids) {
|
|
|
5730
6214
|
|
|
5731
6215
|
// src/dashboard/server.ts
|
|
5732
6216
|
function createDashboardServer(options) {
|
|
5733
|
-
const { port, missionsDir, serversDir, playbooksDir, todosDir, serveStaticUi, dashboardDistPath } = options;
|
|
6217
|
+
const { port, missionsDir, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, serveStaticUi, dashboardDistPath } = options;
|
|
5734
6218
|
const app = express();
|
|
5735
6219
|
const server = createServer(app);
|
|
5736
6220
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -5770,7 +6254,7 @@ function createDashboardServer(options) {
|
|
|
5770
6254
|
app.use(express.json());
|
|
5771
6255
|
app.get("/api/overview", async (_req, res) => {
|
|
5772
6256
|
try {
|
|
5773
|
-
const overview = await getOverview(missionsDir,
|
|
6257
|
+
const overview = await getOverview(missionsDir, serversDir2);
|
|
5774
6258
|
res.json(overview);
|
|
5775
6259
|
} catch (error) {
|
|
5776
6260
|
console.error("Error getting overview:", error);
|
|
@@ -5779,7 +6263,7 @@ function createDashboardServer(options) {
|
|
|
5779
6263
|
});
|
|
5780
6264
|
app.get("/api/attention", async (_req, res) => {
|
|
5781
6265
|
try {
|
|
5782
|
-
const attention = await getAttention(missionsDir,
|
|
6266
|
+
const attention = await getAttention(missionsDir, serversDir2);
|
|
5783
6267
|
res.json(attention);
|
|
5784
6268
|
} catch (error) {
|
|
5785
6269
|
console.error("Error getting attention queue:", error);
|
|
@@ -5947,14 +6431,15 @@ function createDashboardServer(options) {
|
|
|
5947
6431
|
}
|
|
5948
6432
|
});
|
|
5949
6433
|
app.use(createWriteRouter(missionsDir));
|
|
5950
|
-
app.use("/api/servers", createServersRouter(
|
|
6434
|
+
app.use("/api/servers", createServersRouter(serversDir2, missionsDir));
|
|
5951
6435
|
app.use("/api/agent-sessions", createAgentSessionsRouter(missionsDir, broadcast));
|
|
5952
|
-
app.use("/api/playbooks", createPlaybooksRouter(
|
|
5953
|
-
app.use("/api/todos", createTodosRouter(
|
|
6436
|
+
app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
|
|
6437
|
+
app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
|
|
6438
|
+
app.use("/api/backup", createBackupRouter());
|
|
5954
6439
|
if (serveStaticUi && dashboardDistPath) {
|
|
5955
6440
|
app.use(express.static(dashboardDistPath));
|
|
5956
6441
|
app.get("{*path}", async (_req, res) => {
|
|
5957
|
-
const indexPath =
|
|
6442
|
+
const indexPath = resolve15(dashboardDistPath, "index.html");
|
|
5958
6443
|
if (await fileExists(indexPath)) {
|
|
5959
6444
|
res.sendFile(indexPath);
|
|
5960
6445
|
} else {
|
|
@@ -5969,12 +6454,12 @@ function createDashboardServer(options) {
|
|
|
5969
6454
|
async start() {
|
|
5970
6455
|
watcherHandle = createWatcher({
|
|
5971
6456
|
missionsDir,
|
|
5972
|
-
serversDir,
|
|
5973
|
-
playbooksDir,
|
|
5974
|
-
todosDir,
|
|
6457
|
+
serversDir: serversDir2,
|
|
6458
|
+
playbooksDir: playbooksDir2,
|
|
6459
|
+
todosDir: todosDir2,
|
|
5975
6460
|
onMessage: broadcast
|
|
5976
6461
|
});
|
|
5977
|
-
startAutodiscovery({ serversDir, missionsDir, excludePids: /* @__PURE__ */ new Set([process.pid]) });
|
|
6462
|
+
startAutodiscovery({ serversDir: serversDir2, missionsDir, excludePids: /* @__PURE__ */ new Set([process.pid]) });
|
|
5978
6463
|
return new Promise((resolvePromise, reject) => {
|
|
5979
6464
|
server.on("error", (err) => {
|
|
5980
6465
|
if (err.code === "EADDRINUSE") {
|
|
@@ -5986,8 +6471,8 @@ function createDashboardServer(options) {
|
|
|
5986
6471
|
}
|
|
5987
6472
|
});
|
|
5988
6473
|
server.listen(port, () => {
|
|
5989
|
-
const portFile =
|
|
5990
|
-
|
|
6474
|
+
const portFile = resolve15(homedir2(), ".syntaur", "dashboard-port");
|
|
6475
|
+
writeFile4(portFile, String(port), "utf-8").catch(() => {
|
|
5991
6476
|
});
|
|
5992
6477
|
resolvePromise();
|
|
5993
6478
|
});
|
|
@@ -6003,8 +6488,8 @@ function createDashboardServer(options) {
|
|
|
6003
6488
|
client.close();
|
|
6004
6489
|
}
|
|
6005
6490
|
clients.clear();
|
|
6006
|
-
const portFile =
|
|
6007
|
-
await
|
|
6491
|
+
const portFile = resolve15(homedir2(), ".syntaur", "dashboard-port");
|
|
6492
|
+
await unlink4(portFile).catch(() => {
|
|
6008
6493
|
});
|
|
6009
6494
|
return new Promise((resolvePromise) => {
|
|
6010
6495
|
server.close(() => resolvePromise());
|