syntaur 0.26.0 → 0.31.0
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-jPItyrQO.js → _basePickBy-ku_fr_ud.js} +1 -1
- package/dashboard/dist/assets/{_baseUniq-pEwUwurC.js → _baseUniq-BfsrymUF.js} +1 -1
- package/dashboard/dist/assets/{arc-ZZtp507S.js → arc-CAIW_Q55.js} +1 -1
- package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BNUerPqd.js → architectureDiagram-2XIMDMQ5-BSe-tJ6X.js} +1 -1
- package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-CQyovXFv.js → blockDiagram-WCTKOSBZ-BUj6C0-C.js} +1 -1
- package/dashboard/dist/assets/{c4Diagram-IC4MRINW-wNQ6EHeF.js → c4Diagram-IC4MRINW-mV-acLl-.js} +1 -1
- package/dashboard/dist/assets/channel-DSNNpVLB.js +1 -0
- package/dashboard/dist/assets/{chunk-4BX2VUAB-ZaueC30R.js → chunk-4BX2VUAB-TStVwY56.js} +1 -1
- package/dashboard/dist/assets/{chunk-55IACEB6-BjsRB0t8.js → chunk-55IACEB6-XPX1tCrH.js} +1 -1
- package/dashboard/dist/assets/{chunk-FMBD7UC4-BHuSr-Tl.js → chunk-FMBD7UC4-Dm-0meIo.js} +1 -1
- package/dashboard/dist/assets/{chunk-JSJVCQXG-SHNJA0es.js → chunk-JSJVCQXG-BVhtvJFZ.js} +1 -1
- package/dashboard/dist/assets/{chunk-KX2RTZJC-JXFPjeo4.js → chunk-KX2RTZJC-C3r5FnmX.js} +1 -1
- package/dashboard/dist/assets/{chunk-NQ4KR5QH-BiJqWT0B.js → chunk-NQ4KR5QH-DYjOE2Lm.js} +1 -1
- package/dashboard/dist/assets/{chunk-QZHKN3VN-DoXWBqP2.js → chunk-QZHKN3VN-CWpi_905.js} +1 -1
- package/dashboard/dist/assets/{chunk-WL4C6EOR-Dqtf_5it.js → chunk-WL4C6EOR-BLePb5II.js} +1 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-CCzPEXaq.js +1 -0
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CCzPEXaq.js +1 -0
- package/dashboard/dist/assets/clone-D_RUHO8e.js +1 -0
- package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-Cr6bkSKq.js → cose-bilkent-S5V4N54A-Bqz-BOZR.js} +1 -1
- package/dashboard/dist/assets/{dagre-KLK3FWXG-oXpXFuJQ.js → dagre-KLK3FWXG-ObXz616p.js} +1 -1
- package/dashboard/dist/assets/{diagram-E7M64L7V-Bq_xdDbg.js → diagram-E7M64L7V-BmnMDa4P.js} +1 -1
- package/dashboard/dist/assets/{diagram-IFDJBPK2-N7Er4Dui.js → diagram-IFDJBPK2-DF97e8GZ.js} +1 -1
- package/dashboard/dist/assets/{diagram-P4PSJMXO-BU0Zm2Fn.js → diagram-P4PSJMXO-B89xgPXg.js} +1 -1
- package/dashboard/dist/assets/{erDiagram-INFDFZHY-BSgZb5me.js → erDiagram-INFDFZHY-DLsFlh-n.js} +1 -1
- package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-Bn7pEu0U.js → flowDiagram-PKNHOUZH-BTOf6WXa.js} +1 -1
- package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-B8Xq9tyM.js → ganttDiagram-A5KZAMGK-Bf3qSpfJ.js} +1 -1
- package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-BoLUjYDa.js → gitGraphDiagram-K3NZZRJ6-TXYSfb6n.js} +1 -1
- package/dashboard/dist/assets/{graph-Pde_ni_y.js → graph-CX9dAzVP.js} +1 -1
- package/dashboard/dist/assets/index-BohN_jjP.css +1 -0
- package/dashboard/dist/assets/index-jQ0-J3SI.js +556 -0
- package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-Brv2khjP.js → infoDiagram-LFFYTUFH-JqFsL_yV.js} +1 -1
- package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-D5hxQ0Ke.js → ishikawaDiagram-PHBUUO56-jA6-TFjI.js} +1 -1
- package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CUevv5jA.js → journeyDiagram-4ABVD52K-QvSViLCF.js} +1 -1
- package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Cf6XyrAC.js → kanban-definition-K7BYSVSG-CwD7Cohp.js} +1 -1
- package/dashboard/dist/assets/{layout-Bc8RP2w3.js → layout-upwYHmrE.js} +1 -1
- package/dashboard/dist/assets/{linear-Cd_XUbl7.js → linear-Caio4qd9.js} +1 -1
- package/dashboard/dist/assets/{mermaid.core-Bx8MuMEM.js → mermaid.core-GdFnPCS2.js} +4 -4
- package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-D_4Pl3Mu.js → mindmap-definition-YRQLILUH-gcJj2wQg.js} +1 -1
- package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-DRVbjwxO.js → pieDiagram-SKSYHLDU-D8HzHHsW.js} +1 -1
- package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BciLlBMH.js → quadrantDiagram-337W2JSQ-Dw7trxdd.js} +1 -1
- package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-Bprwe8Z2.js → requirementDiagram-Z7DCOOCP-4TeVfDt_.js} +1 -1
- package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DI0t8Uiu.js → sankeyDiagram-WA2Y5GQK-BoK6HCj7.js} +1 -1
- package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CpCLCs5J.js → sequenceDiagram-2WXFIKYE-Cfdz1JPT.js} +1 -1
- package/dashboard/dist/assets/{stateDiagram-RAJIS63D-V-1VCApT.js → stateDiagram-RAJIS63D-CTxzMaII.js} +1 -1
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BhvDlKAC.js +1 -0
- package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DCAo6tA7.js → timeline-definition-YZTLITO2-BQ6k-eeg.js} +1 -1
- package/dashboard/dist/assets/{treemap-KZPCXAKY-CKlbZ6Y_.js → treemap-KZPCXAKY-Dj58PW0M.js} +1 -1
- package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-CJSijre_.js → vennDiagram-LZ73GAT5-C_nMFV9L.js} +1 -1
- package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DXd1BBmK.js → xychartDiagram-JWTSCODW-BcsMJT15.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/dashboard/server.js +1061 -349
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +6894 -4678
- package/dist/index.js.map +1 -1
- package/dist/launch/index.js +1191 -1181
- package/dist/launch/index.js.map +1 -1
- package/package.json +2 -1
- package/platforms/README.md +21 -0
- package/platforms/claude-code/skills/clear-assignment/SKILL.md +2 -2
- package/platforms/claude-code/skills/log-progress/SKILL.md +29 -48
- package/platforms/claude-code/skills/plan-assignment/SKILL.md +20 -1
- package/platforms/claude-code/skills/save-session-summary/SKILL.md +28 -29
- package/platforms/claude-code/skills/set-workspace/SKILL.md +25 -41
- package/platforms/codex/skills/clear-assignment/SKILL.md +2 -2
- package/platforms/codex/skills/log-progress/SKILL.md +29 -48
- package/platforms/codex/skills/plan-assignment/SKILL.md +20 -1
- package/platforms/codex/skills/save-session-summary/SKILL.md +28 -29
- package/platforms/codex/skills/set-workspace/SKILL.md +25 -41
- package/skills/clear-assignment/SKILL.md +2 -2
- package/skills/log-progress/SKILL.md +29 -48
- package/skills/plan-assignment/SKILL.md +20 -1
- package/skills/save-session-summary/SKILL.md +28 -29
- package/skills/set-workspace/SKILL.md +25 -41
- package/dashboard/dist/assets/channel-BYnzdl2x.js +0 -1
- package/dashboard/dist/assets/classDiagram-VBA2DB6C-BnPZbM4g.js +0 -1
- package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BnPZbM4g.js +0 -1
- package/dashboard/dist/assets/clone-DYNFxLr3.js +0 -1
- package/dashboard/dist/assets/index-7rNWNKq7.css +0 -1
- package/dashboard/dist/assets/index-Nc9kfSW-.js +0 -550
- package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-B6S2ctrX.js +0 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -169,8 +169,8 @@ function extractFrontmatter(fileContent) {
|
|
|
169
169
|
const body = fileContent.slice(match[0].length);
|
|
170
170
|
return [frontmatterBlock, body];
|
|
171
171
|
}
|
|
172
|
-
function parseSimpleValue(
|
|
173
|
-
const trimmed =
|
|
172
|
+
function parseSimpleValue(raw2) {
|
|
173
|
+
const trimmed = raw2.trim();
|
|
174
174
|
if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
|
|
175
175
|
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
176
176
|
return trimmed.slice(1, -1);
|
|
@@ -468,8 +468,8 @@ function extractFrontmatter2(fileContent) {
|
|
|
468
468
|
const body = fileContent.slice(match[0].length).trim();
|
|
469
469
|
return [frontmatterBlock, body];
|
|
470
470
|
}
|
|
471
|
-
function parseSimpleValue2(
|
|
472
|
-
const trimmed =
|
|
471
|
+
function parseSimpleValue2(raw2) {
|
|
472
|
+
const trimmed = raw2.trim();
|
|
473
473
|
if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
|
|
474
474
|
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
475
475
|
return trimmed.slice(1, -1);
|
|
@@ -1500,8 +1500,8 @@ ${key}: ${formatted}${content.slice(closingIdx)}`;
|
|
|
1500
1500
|
function readFrontmatterField(content, key) {
|
|
1501
1501
|
const match = content.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
|
|
1502
1502
|
if (!match) return null;
|
|
1503
|
-
const
|
|
1504
|
-
return
|
|
1503
|
+
const raw2 = match[1].trim().replace(/^['"]|['"]$/g, "");
|
|
1504
|
+
return raw2 === "" || raw2 === "null" ? null : raw2;
|
|
1505
1505
|
}
|
|
1506
1506
|
async function migrateLegacyArchivedProjects(projectsDir) {
|
|
1507
1507
|
const result = { reconciled: [] };
|
|
@@ -1771,9 +1771,9 @@ function normalizeHiddenList(input) {
|
|
|
1771
1771
|
if (!Array.isArray(input)) return [];
|
|
1772
1772
|
const seen = /* @__PURE__ */ new Set();
|
|
1773
1773
|
const out = [];
|
|
1774
|
-
for (const
|
|
1775
|
-
if (typeof
|
|
1776
|
-
const name =
|
|
1774
|
+
for (const raw2 of input) {
|
|
1775
|
+
if (typeof raw2 !== "string") continue;
|
|
1776
|
+
const name = raw2.trim();
|
|
1777
1777
|
if (name.length === 0) continue;
|
|
1778
1778
|
if (name.length > MAX_WORKSPACE_NAME_LENGTH) continue;
|
|
1779
1779
|
if (/[\r\n]/.test(name)) continue;
|
|
@@ -1798,9 +1798,11 @@ __export(config_exports, {
|
|
|
1798
1798
|
AgentConfigError: () => AgentConfigError,
|
|
1799
1799
|
BUILTIN_AGENTS: () => BUILTIN_AGENTS,
|
|
1800
1800
|
DEFAULT_ASSIGNMENT_TYPES: () => DEFAULT_ASSIGNMENT_TYPES,
|
|
1801
|
+
DEFAULT_STATUS_COLORS: () => DEFAULT_STATUS_COLORS,
|
|
1801
1802
|
PROMPT_ARG_POSITIONS: () => PROMPT_ARG_POSITIONS,
|
|
1802
1803
|
TERMINAL_CHOICES: () => TERMINAL_CHOICES,
|
|
1803
1804
|
TerminalConfigError: () => TerminalConfigError,
|
|
1805
|
+
buildDefaultStatusConfig: () => buildDefaultStatusConfig,
|
|
1804
1806
|
deleteAgentsConfig: () => deleteAgentsConfig,
|
|
1805
1807
|
deleteHotkeyBindingsConfig: () => deleteHotkeyBindingsConfig,
|
|
1806
1808
|
deleteStatusConfig: () => deleteStatusConfig,
|
|
@@ -1811,8 +1813,11 @@ __export(config_exports, {
|
|
|
1811
1813
|
getAssignmentTypes: () => getAssignmentTypes,
|
|
1812
1814
|
getTerminal: () => getTerminal,
|
|
1813
1815
|
parseAgentCommand: () => parseAgentCommand,
|
|
1816
|
+
parseStatusConfig: () => parseStatusConfig,
|
|
1814
1817
|
parseTerminalConfig: () => parseTerminalConfig,
|
|
1815
1818
|
readConfig: () => readConfig,
|
|
1819
|
+
serializeStatusConfig: () => serializeStatusConfig,
|
|
1820
|
+
toTitleCase: () => toTitleCase,
|
|
1816
1821
|
updateAgentsConfig: () => updateAgentsConfig,
|
|
1817
1822
|
updateBackupConfig: () => updateBackupConfig,
|
|
1818
1823
|
updateIntegrationConfig: () => updateIntegrationConfig,
|
|
@@ -2058,6 +2063,24 @@ function parseStatusConfig(content) {
|
|
|
2058
2063
|
transitions
|
|
2059
2064
|
};
|
|
2060
2065
|
}
|
|
2066
|
+
function toTitleCase(s) {
|
|
2067
|
+
return s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2068
|
+
}
|
|
2069
|
+
function buildDefaultStatusConfig() {
|
|
2070
|
+
return {
|
|
2071
|
+
statuses: DEFAULT_STATUSES.map((id) => ({
|
|
2072
|
+
id,
|
|
2073
|
+
label: toTitleCase(id),
|
|
2074
|
+
color: DEFAULT_STATUS_COLORS[id] ?? "gray",
|
|
2075
|
+
terminal: id === "completed" || id === "failed"
|
|
2076
|
+
})),
|
|
2077
|
+
order: [...DEFAULT_STATUSES],
|
|
2078
|
+
transitions: Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {
|
|
2079
|
+
const [from, command] = key.split(":");
|
|
2080
|
+
return { from, command, to };
|
|
2081
|
+
})
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2061
2084
|
function serializeStatusConfig(statuses) {
|
|
2062
2085
|
const lines = [];
|
|
2063
2086
|
lines.push("statuses:");
|
|
@@ -2152,13 +2175,13 @@ function parsePlaybooksConfig(fmBlock) {
|
|
|
2152
2175
|
continue;
|
|
2153
2176
|
}
|
|
2154
2177
|
if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
|
|
2155
|
-
const
|
|
2156
|
-
if (
|
|
2157
|
-
if (/\s/.test(
|
|
2158
|
-
console.warn(`Warning: config.md playbooks.disabled entry "${
|
|
2178
|
+
const raw2 = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
|
|
2179
|
+
if (raw2.length === 0) continue;
|
|
2180
|
+
if (/\s/.test(raw2)) {
|
|
2181
|
+
console.warn(`Warning: config.md playbooks.disabled entry "${raw2}" contains whitespace, ignoring`);
|
|
2159
2182
|
continue;
|
|
2160
2183
|
}
|
|
2161
|
-
disabled.push(
|
|
2184
|
+
disabled.push(raw2);
|
|
2162
2185
|
continue;
|
|
2163
2186
|
}
|
|
2164
2187
|
}
|
|
@@ -2438,9 +2461,9 @@ function serializeHotkeyBindingsConfig(cfg) {
|
|
|
2438
2461
|
async function writeHotkeyBindingsConfig(cfg) {
|
|
2439
2462
|
const cleaned = {};
|
|
2440
2463
|
for (const kind of BINDABLE_ACTION_KINDS) {
|
|
2441
|
-
const
|
|
2442
|
-
if (typeof
|
|
2443
|
-
const canonical = canonicalizeCombo(
|
|
2464
|
+
const raw2 = cfg.bindings[kind];
|
|
2465
|
+
if (typeof raw2 !== "string" || raw2.trim() === "") continue;
|
|
2466
|
+
const canonical = canonicalizeCombo(raw2);
|
|
2444
2467
|
if (!canonical) continue;
|
|
2445
2468
|
if (isReservedCombo(canonical)) continue;
|
|
2446
2469
|
cleaned[kind] = canonical;
|
|
@@ -3127,7 +3150,7 @@ async function updateAgentsConfig(mutation, options = {}) {
|
|
|
3127
3150
|
await writeAgentsConfig(next);
|
|
3128
3151
|
return { previous, next, written: true };
|
|
3129
3152
|
}
|
|
3130
|
-
var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
|
|
3153
|
+
var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
|
|
3131
3154
|
var init_config2 = __esm({
|
|
3132
3155
|
"src/utils/config.ts"() {
|
|
3133
3156
|
"use strict";
|
|
@@ -3135,6 +3158,7 @@ var init_config2 = __esm({
|
|
|
3135
3158
|
init_fs();
|
|
3136
3159
|
init_config();
|
|
3137
3160
|
init_fs_migration();
|
|
3161
|
+
init_lifecycle();
|
|
3138
3162
|
init_hotkeysCatalog();
|
|
3139
3163
|
init_agents_schema();
|
|
3140
3164
|
init_terminal_schema();
|
|
@@ -3182,6 +3206,14 @@ var init_config2 = __esm({
|
|
|
3182
3206
|
AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
|
|
3183
3207
|
AgentConfigError = class extends Error {
|
|
3184
3208
|
};
|
|
3209
|
+
DEFAULT_STATUS_COLORS = {
|
|
3210
|
+
pending: "slate",
|
|
3211
|
+
in_progress: "teal",
|
|
3212
|
+
blocked: "amber",
|
|
3213
|
+
review: "violet",
|
|
3214
|
+
completed: "emerald",
|
|
3215
|
+
failed: "rose"
|
|
3216
|
+
};
|
|
3185
3217
|
KNOWN_AGENT_SCALAR_FIELDS = /* @__PURE__ */ new Set([
|
|
3186
3218
|
"id",
|
|
3187
3219
|
"label",
|
|
@@ -3237,8 +3269,8 @@ async function resolvePlaybookSlug(playbooksDir2, slug) {
|
|
|
3237
3269
|
for (const entry of entries) {
|
|
3238
3270
|
if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
|
|
3239
3271
|
const filePath = resolve7(playbooksDir2, entry.name);
|
|
3240
|
-
const
|
|
3241
|
-
const parsed = parsePlaybook(
|
|
3272
|
+
const raw2 = await readFile6(filePath, "utf-8");
|
|
3273
|
+
const parsed = parsePlaybook(raw2);
|
|
3242
3274
|
const canonical = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
3243
3275
|
if (canonical === slug) {
|
|
3244
3276
|
return { filename: entry.name, slug: canonical, parsed };
|
|
@@ -3285,8 +3317,8 @@ async function rebuildPlaybookManifest(playbooksDir2) {
|
|
|
3285
3317
|
const rows = [];
|
|
3286
3318
|
for (const entry of entries) {
|
|
3287
3319
|
if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
|
|
3288
|
-
const
|
|
3289
|
-
const parsed = parsePlaybook(
|
|
3320
|
+
const raw2 = await readFile6(resolve7(playbooksDir2, entry.name), "utf-8");
|
|
3321
|
+
const parsed = parsePlaybook(raw2);
|
|
3290
3322
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
3291
3323
|
if (disabledSet.has(slug)) continue;
|
|
3292
3324
|
rows.push({
|
|
@@ -3363,8 +3395,8 @@ async function renamePlaybook(playbooksDir2, oldSlug, newSlug) {
|
|
|
3363
3395
|
);
|
|
3364
3396
|
}
|
|
3365
3397
|
}
|
|
3366
|
-
const
|
|
3367
|
-
let next = setFrontmatterField2(
|
|
3398
|
+
const raw2 = await readFile6(oldPath, "utf-8");
|
|
3399
|
+
let next = setFrontmatterField2(raw2, "slug", newSlug);
|
|
3368
3400
|
next = setFrontmatterField2(next, "updated", `"${nowTimestamp()}"`);
|
|
3369
3401
|
await writeFileForce(newPath, next);
|
|
3370
3402
|
if (!renamedInPlace) {
|
|
@@ -4040,6 +4072,37 @@ function initSessionDb(dbPath) {
|
|
|
4040
4072
|
UPDATE meta SET value = '4' WHERE key = 'schema_version';
|
|
4041
4073
|
`);
|
|
4042
4074
|
}
|
|
4075
|
+
const vBeforeV5 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
|
|
4076
|
+
if (vBeforeV5 === "4") {
|
|
4077
|
+
database.exec(`
|
|
4078
|
+
CREATE TABLE sessions_v5 (
|
|
4079
|
+
session_id TEXT PRIMARY KEY,
|
|
4080
|
+
project_slug TEXT,
|
|
4081
|
+
assignment_slug TEXT,
|
|
4082
|
+
agent TEXT NOT NULL,
|
|
4083
|
+
started TEXT NOT NULL,
|
|
4084
|
+
ended TEXT,
|
|
4085
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
4086
|
+
path TEXT,
|
|
4087
|
+
description TEXT,
|
|
4088
|
+
transcript_path TEXT,
|
|
4089
|
+
pid INTEGER,
|
|
4090
|
+
pid_started_at TEXT,
|
|
4091
|
+
original_head_sha TEXT,
|
|
4092
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4093
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4094
|
+
);
|
|
4095
|
+
INSERT INTO sessions_v5
|
|
4096
|
+
SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, description, transcript_path, pid, pid_started_at, NULL, created_at, updated_at
|
|
4097
|
+
FROM sessions;
|
|
4098
|
+
DROP TABLE sessions;
|
|
4099
|
+
ALTER TABLE sessions_v5 RENAME TO sessions;
|
|
4100
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
|
|
4101
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
|
|
4102
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
4103
|
+
UPDATE meta SET value = '5' WHERE key = 'schema_version';
|
|
4104
|
+
`);
|
|
4105
|
+
}
|
|
4043
4106
|
});
|
|
4044
4107
|
runMigrations.exclusive();
|
|
4045
4108
|
db.exec(POST_MIGRATION_INDEXES_SQL);
|
|
@@ -4090,9 +4153,9 @@ async function migrateFromMarkdown(projectsDir) {
|
|
|
4090
4153
|
}
|
|
4091
4154
|
async function parseMarkdownSessionsIndex(filePath, projectSlug) {
|
|
4092
4155
|
const { readFile: readFile21 } = await import("fs/promises");
|
|
4093
|
-
const
|
|
4156
|
+
const raw2 = await readFile21(filePath, "utf-8");
|
|
4094
4157
|
const sessions = [];
|
|
4095
|
-
const lines =
|
|
4158
|
+
const lines = raw2.split("\n");
|
|
4096
4159
|
let inTable = false;
|
|
4097
4160
|
let headerSeen = false;
|
|
4098
4161
|
for (const line of lines) {
|
|
@@ -4131,7 +4194,7 @@ var init_session_db = __esm({
|
|
|
4131
4194
|
init_paths();
|
|
4132
4195
|
init_fs();
|
|
4133
4196
|
db = null;
|
|
4134
|
-
SCHEMA_VERSION = "
|
|
4197
|
+
SCHEMA_VERSION = "5";
|
|
4135
4198
|
SCHEMA_SQL = `
|
|
4136
4199
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
4137
4200
|
session_id TEXT PRIMARY KEY,
|
|
@@ -4146,6 +4209,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
4146
4209
|
transcript_path TEXT,
|
|
4147
4210
|
pid INTEGER,
|
|
4148
4211
|
pid_started_at TEXT,
|
|
4212
|
+
original_head_sha TEXT,
|
|
4149
4213
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4150
4214
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4151
4215
|
);
|
|
@@ -4175,25 +4239,27 @@ function rowToSession(row) {
|
|
|
4175
4239
|
description: row.description ?? null,
|
|
4176
4240
|
transcriptPath: row.transcript_path ?? null,
|
|
4177
4241
|
pid: row.pid ?? null,
|
|
4178
|
-
pidStartedAt: row.pid_started_at ?? null
|
|
4242
|
+
pidStartedAt: row.pid_started_at ?? null,
|
|
4243
|
+
originalHeadSha: row.original_head_sha ?? null
|
|
4179
4244
|
};
|
|
4180
4245
|
}
|
|
4181
4246
|
async function appendSession(_projectDir, session) {
|
|
4182
4247
|
const db4 = getSessionDb();
|
|
4183
4248
|
db4.prepare(`
|
|
4184
|
-
INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at)
|
|
4185
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4249
|
+
INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)
|
|
4250
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4186
4251
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
4187
|
-
project_slug
|
|
4188
|
-
assignment_slug
|
|
4189
|
-
agent
|
|
4190
|
-
status
|
|
4191
|
-
path
|
|
4192
|
-
description
|
|
4193
|
-
transcript_path
|
|
4194
|
-
pid
|
|
4195
|
-
pid_started_at
|
|
4196
|
-
|
|
4252
|
+
project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
|
|
4253
|
+
assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
|
|
4254
|
+
agent = excluded.agent,
|
|
4255
|
+
status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
|
|
4256
|
+
path = COALESCE(NULLIF(excluded.path, ''), path),
|
|
4257
|
+
description = COALESCE(NULLIF(excluded.description, ''), description),
|
|
4258
|
+
transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
|
|
4259
|
+
pid = COALESCE(excluded.pid, pid),
|
|
4260
|
+
pid_started_at = COALESCE(NULLIF(excluded.pid_started_at, ''), pid_started_at),
|
|
4261
|
+
original_head_sha = COALESCE(NULLIF(original_head_sha, ''), NULLIF(excluded.original_head_sha, '')),
|
|
4262
|
+
updated_at = datetime('now')
|
|
4197
4263
|
`).run(
|
|
4198
4264
|
session.sessionId,
|
|
4199
4265
|
session.projectSlug ?? null,
|
|
@@ -4205,7 +4271,8 @@ async function appendSession(_projectDir, session) {
|
|
|
4205
4271
|
session.description ?? null,
|
|
4206
4272
|
session.transcriptPath ?? null,
|
|
4207
4273
|
session.pid ?? null,
|
|
4208
|
-
session.pidStartedAt ?? null
|
|
4274
|
+
session.pidStartedAt ?? null,
|
|
4275
|
+
session.originalHeadSha ?? null
|
|
4209
4276
|
);
|
|
4210
4277
|
}
|
|
4211
4278
|
async function updateSessionStatus(_projectDir, sessionId, status) {
|
|
@@ -4248,8 +4315,8 @@ async function deleteSessions(sessionIds) {
|
|
|
4248
4315
|
}
|
|
4249
4316
|
async function readAssignmentStatusFromPath(assignmentMdPath) {
|
|
4250
4317
|
if (!await fileExists(assignmentMdPath)) return null;
|
|
4251
|
-
const
|
|
4252
|
-
const match =
|
|
4318
|
+
const raw2 = await readFile8(assignmentMdPath, "utf-8");
|
|
4319
|
+
const match = raw2.match(/^status:\s*(.+)$/m);
|
|
4253
4320
|
return match ? match[1].trim() : null;
|
|
4254
4321
|
}
|
|
4255
4322
|
async function readAssignmentStatus(projectDir, assignmentSlug) {
|
|
@@ -4393,8 +4460,8 @@ async function listSessionFiles(dir) {
|
|
|
4393
4460
|
async function readSessionFile(dir, name) {
|
|
4394
4461
|
const filePath = resolve11(dir, `${sanitizeSessionName(name)}.md`);
|
|
4395
4462
|
if (!await fileExists(filePath)) return null;
|
|
4396
|
-
const
|
|
4397
|
-
const [frontmatter] = extractFrontmatter2(
|
|
4463
|
+
const raw2 = await readFile9(filePath, "utf-8");
|
|
4464
|
+
const [frontmatter] = extractFrontmatter2(raw2);
|
|
4398
4465
|
if (!frontmatter) return null;
|
|
4399
4466
|
const session = getField(frontmatter, "session") ?? name;
|
|
4400
4467
|
const registered = getField(frontmatter, "registered") ?? "";
|
|
@@ -4522,8 +4589,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
|
|
|
4522
4589
|
return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
|
|
4523
4590
|
}
|
|
4524
4591
|
function delay(ms) {
|
|
4525
|
-
return new Promise((
|
|
4526
|
-
const timer2 = setTimeout(
|
|
4592
|
+
return new Promise((resolve30) => {
|
|
4593
|
+
const timer2 = setTimeout(resolve30, ms);
|
|
4527
4594
|
if (typeof timer2.unref === "function") {
|
|
4528
4595
|
timer2.unref();
|
|
4529
4596
|
}
|
|
@@ -5063,9 +5130,6 @@ async function computeStandaloneRecords(assignmentsDir2) {
|
|
|
5063
5130
|
records.sort((left, right) => compareTimestamps(right.record.updated, left.record.updated));
|
|
5064
5131
|
return records;
|
|
5065
5132
|
}
|
|
5066
|
-
function toTitleCase(s) {
|
|
5067
|
-
return s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
5068
|
-
}
|
|
5069
5133
|
function getTransitionDefinitions(config) {
|
|
5070
5134
|
if (!config.custom) return DEFAULT_TRANSITION_DEFINITIONS;
|
|
5071
5135
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -5101,19 +5165,12 @@ async function getStatusConfig() {
|
|
|
5101
5165
|
terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"])
|
|
5102
5166
|
};
|
|
5103
5167
|
} else {
|
|
5168
|
+
const def = buildDefaultStatusConfig();
|
|
5104
5169
|
_cachedConfig = {
|
|
5105
5170
|
custom: false,
|
|
5106
|
-
statuses:
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
color: DEFAULT_STATUS_COLORS[id] ?? "gray",
|
|
5110
|
-
terminal: id === "completed" || id === "failed"
|
|
5111
|
-
})),
|
|
5112
|
-
order: [...DEFAULT_STATUSES],
|
|
5113
|
-
transitions: Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {
|
|
5114
|
-
const [from, command] = key.split(":");
|
|
5115
|
-
return { from, command, to };
|
|
5116
|
-
}),
|
|
5171
|
+
statuses: def.statuses,
|
|
5172
|
+
order: def.order,
|
|
5173
|
+
transitions: def.transitions,
|
|
5117
5174
|
transitionTable: DEFAULT_TRANSITION_TABLE,
|
|
5118
5175
|
terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"])
|
|
5119
5176
|
};
|
|
@@ -5130,8 +5187,8 @@ async function listProjects(projectsDir) {
|
|
|
5130
5187
|
async function readWorkspaceRegistry(projectsDir) {
|
|
5131
5188
|
const registryPath = resolve13(dirname2(projectsDir), "workspaces.json");
|
|
5132
5189
|
try {
|
|
5133
|
-
const
|
|
5134
|
-
const parsed = JSON.parse(
|
|
5190
|
+
const raw2 = await readFile10(registryPath, "utf-8");
|
|
5191
|
+
const parsed = JSON.parse(raw2);
|
|
5135
5192
|
return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
|
|
5136
5193
|
} catch {
|
|
5137
5194
|
return [];
|
|
@@ -5219,8 +5276,8 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
|
|
|
5219
5276
|
const timestamp = nowTimestamp();
|
|
5220
5277
|
for (const slug of projectsReferencing) {
|
|
5221
5278
|
const path = resolve13(projectsDir, slug, "project.md");
|
|
5222
|
-
const
|
|
5223
|
-
let next = clearFrontmatterField(
|
|
5279
|
+
const raw2 = await readFile10(path, "utf-8");
|
|
5280
|
+
let next = clearFrontmatterField(raw2, "workspace");
|
|
5224
5281
|
next = setUpdatedField(next, timestamp);
|
|
5225
5282
|
await writeFileForce(path, next);
|
|
5226
5283
|
rewroteFiles = true;
|
|
@@ -5228,8 +5285,8 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
|
|
|
5228
5285
|
for (const id of standalonesReferencing) {
|
|
5229
5286
|
if (!opts.assignmentsDir) break;
|
|
5230
5287
|
const path = resolve13(opts.assignmentsDir, id, "assignment.md");
|
|
5231
|
-
const
|
|
5232
|
-
let next = clearFrontmatterField(
|
|
5288
|
+
const raw2 = await readFile10(path, "utf-8");
|
|
5289
|
+
let next = clearFrontmatterField(raw2, "workspaceGroup");
|
|
5233
5290
|
next = setUpdatedField(next, timestamp);
|
|
5234
5291
|
await writeFileForce(path, next);
|
|
5235
5292
|
rewroteFiles = true;
|
|
@@ -6679,8 +6736,8 @@ async function listPlaybooks(playbooksDir2) {
|
|
|
6679
6736
|
for (const entry of entries) {
|
|
6680
6737
|
if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
|
|
6681
6738
|
const filePath = resolve13(playbooksDir2, entry.name);
|
|
6682
|
-
const
|
|
6683
|
-
const parsed = parsePlaybook(
|
|
6739
|
+
const raw2 = await readFile10(filePath, "utf-8");
|
|
6740
|
+
const parsed = parsePlaybook(raw2);
|
|
6684
6741
|
const slug = parsed.slug || entry.name.replace(/\.md$/, "");
|
|
6685
6742
|
playbooks.push({
|
|
6686
6743
|
slug,
|
|
@@ -6713,7 +6770,7 @@ async function getPlaybookDetail(playbooksDir2, slug) {
|
|
|
6713
6770
|
enabled
|
|
6714
6771
|
};
|
|
6715
6772
|
}
|
|
6716
|
-
var WorkspaceBlockedError, STALE_ASSIGNMENT_MS, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, TERMINAL_STATUSES2, STATUS_TO_SEGMENT, HERO_PRIORITY, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS,
|
|
6773
|
+
var WorkspaceBlockedError, STALE_ASSIGNMENT_MS, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, TERMINAL_STATUSES2, STATUS_TO_SEGMENT, HERO_PRIORITY, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
|
|
6717
6774
|
var init_api = __esm({
|
|
6718
6775
|
"src/dashboard/api.ts"() {
|
|
6719
6776
|
"use strict";
|
|
@@ -6828,14 +6885,6 @@ var init_api = __esm({
|
|
|
6828
6885
|
requiresReason: false
|
|
6829
6886
|
}
|
|
6830
6887
|
];
|
|
6831
|
-
DEFAULT_STATUS_COLORS = {
|
|
6832
|
-
pending: "slate",
|
|
6833
|
-
in_progress: "teal",
|
|
6834
|
-
blocked: "amber",
|
|
6835
|
-
review: "violet",
|
|
6836
|
-
completed: "emerald",
|
|
6837
|
-
failed: "rose"
|
|
6838
|
-
};
|
|
6839
6888
|
_cachedConfig = null;
|
|
6840
6889
|
REFERENCED_BY_LIMIT = 50;
|
|
6841
6890
|
migratedProjectsDirs = /* @__PURE__ */ new Set();
|
|
@@ -6904,8 +6953,8 @@ init_assignment_resolver();
|
|
|
6904
6953
|
init_agent_sessions();
|
|
6905
6954
|
import express from "express";
|
|
6906
6955
|
import { createServer } from "http";
|
|
6907
|
-
import { resolve as
|
|
6908
|
-
import { writeFile as
|
|
6956
|
+
import { resolve as resolve29 } from "path";
|
|
6957
|
+
import { writeFile as writeFile7, unlink as unlink7 } from "fs/promises";
|
|
6909
6958
|
import { WebSocketServer, WebSocket } from "ws";
|
|
6910
6959
|
|
|
6911
6960
|
// src/dashboard/session-liveness.ts
|
|
@@ -6991,7 +7040,18 @@ function enrichSessions(sessions, agents, deps) {
|
|
|
6991
7040
|
|
|
6992
7041
|
// src/dashboard/watcher.ts
|
|
6993
7042
|
import { watch } from "chokidar";
|
|
6994
|
-
import { basename as basename2, dirname as dirname3, relative, sep } from "path";
|
|
7043
|
+
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, relative, sep } from "path";
|
|
7044
|
+
var defaultPathApi = { relative, isAbsolute: isAbsolute2 };
|
|
7045
|
+
function ignoreDotSegmentsBelow(root, pathApi = defaultPathApi) {
|
|
7046
|
+
return (p) => {
|
|
7047
|
+
const rel = pathApi.relative(root, p);
|
|
7048
|
+
if (!rel) return false;
|
|
7049
|
+
if (pathApi.isAbsolute(rel)) return false;
|
|
7050
|
+
const parts = rel.split(/[\\/]/);
|
|
7051
|
+
if (parts[0] === "..") return false;
|
|
7052
|
+
return parts.some((segment) => segment.startsWith("."));
|
|
7053
|
+
};
|
|
7054
|
+
}
|
|
6995
7055
|
function createWatcher(options) {
|
|
6996
7056
|
const { projectsDir, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, dbPath, onMessage, debounceMs = 300 } = options;
|
|
6997
7057
|
const pendingEvents = /* @__PURE__ */ new Map();
|
|
@@ -6999,7 +7059,7 @@ function createWatcher(options) {
|
|
|
6999
7059
|
ignoreInitial: true,
|
|
7000
7060
|
persistent: true,
|
|
7001
7061
|
depth: 10,
|
|
7002
|
-
ignored:
|
|
7062
|
+
ignored: ignoreDotSegmentsBelow(projectsDir)
|
|
7003
7063
|
});
|
|
7004
7064
|
function handleProjectChange(filePath) {
|
|
7005
7065
|
const rel = relative(projectsDir, filePath);
|
|
@@ -7068,7 +7128,7 @@ function createWatcher(options) {
|
|
|
7068
7128
|
ignoreInitial: true,
|
|
7069
7129
|
persistent: true,
|
|
7070
7130
|
depth: 5,
|
|
7071
|
-
ignored:
|
|
7131
|
+
ignored: ignoreDotSegmentsBelow(assignmentsDir2)
|
|
7072
7132
|
});
|
|
7073
7133
|
standaloneWatcher.on("change", handleStandaloneChange2);
|
|
7074
7134
|
standaloneWatcher.on("add", handleStandaloneChange2);
|
|
@@ -7097,7 +7157,7 @@ function createWatcher(options) {
|
|
|
7097
7157
|
ignoreInitial: true,
|
|
7098
7158
|
persistent: true,
|
|
7099
7159
|
depth: 1,
|
|
7100
|
-
ignored:
|
|
7160
|
+
ignored: ignoreDotSegmentsBelow(serversDir2)
|
|
7101
7161
|
});
|
|
7102
7162
|
serversWatcher.on("change", handleServerChange2);
|
|
7103
7163
|
serversWatcher.on("add", handleServerChange2);
|
|
@@ -7126,7 +7186,7 @@ function createWatcher(options) {
|
|
|
7126
7186
|
ignoreInitial: true,
|
|
7127
7187
|
persistent: true,
|
|
7128
7188
|
depth: 1,
|
|
7129
|
-
ignored:
|
|
7189
|
+
ignored: ignoreDotSegmentsBelow(playbooksDir2)
|
|
7130
7190
|
});
|
|
7131
7191
|
playbooksWatcher.on("change", handlePlaybookChange2);
|
|
7132
7192
|
playbooksWatcher.on("add", handlePlaybookChange2);
|
|
@@ -7155,7 +7215,7 @@ function createWatcher(options) {
|
|
|
7155
7215
|
ignoreInitial: true,
|
|
7156
7216
|
persistent: true,
|
|
7157
7217
|
depth: 1,
|
|
7158
|
-
ignored:
|
|
7218
|
+
ignored: ignoreDotSegmentsBelow(todosDir2)
|
|
7159
7219
|
});
|
|
7160
7220
|
todosWatcher.on("change", handleTodoChange2);
|
|
7161
7221
|
todosWatcher.on("add", handleTodoChange2);
|
|
@@ -7187,7 +7247,7 @@ function createWatcher(options) {
|
|
|
7187
7247
|
ignoreInitial: true,
|
|
7188
7248
|
persistent: true,
|
|
7189
7249
|
depth: 0,
|
|
7190
|
-
ignored:
|
|
7250
|
+
ignored: ignoreDotSegmentsBelow(dbDir)
|
|
7191
7251
|
});
|
|
7192
7252
|
leasesDbWatcher.on("change", handleDbChange2);
|
|
7193
7253
|
leasesDbWatcher.on("add", handleDbChange2);
|
|
@@ -7379,15 +7439,15 @@ async function readViewPrefsFile() {
|
|
|
7379
7439
|
if (!await fileExists(path)) {
|
|
7380
7440
|
return { ...DEFAULT_VIEW_PREFS_FILE };
|
|
7381
7441
|
}
|
|
7382
|
-
let
|
|
7442
|
+
let raw2;
|
|
7383
7443
|
try {
|
|
7384
|
-
|
|
7444
|
+
raw2 = await readFile11(path, "utf-8");
|
|
7385
7445
|
} catch {
|
|
7386
7446
|
return { ...DEFAULT_VIEW_PREFS_FILE };
|
|
7387
7447
|
}
|
|
7388
7448
|
let parsed;
|
|
7389
7449
|
try {
|
|
7390
|
-
parsed = JSON.parse(
|
|
7450
|
+
parsed = JSON.parse(raw2);
|
|
7391
7451
|
} catch {
|
|
7392
7452
|
await backupCorrupt(path);
|
|
7393
7453
|
return { ...DEFAULT_VIEW_PREFS_FILE };
|
|
@@ -7592,15 +7652,15 @@ async function readSavedViewsFile() {
|
|
|
7592
7652
|
if (!await fileExists(path)) {
|
|
7593
7653
|
return cloneDefault();
|
|
7594
7654
|
}
|
|
7595
|
-
let
|
|
7655
|
+
let raw2;
|
|
7596
7656
|
try {
|
|
7597
|
-
|
|
7657
|
+
raw2 = await readFile12(path, "utf-8");
|
|
7598
7658
|
} catch {
|
|
7599
7659
|
return cloneDefault();
|
|
7600
7660
|
}
|
|
7601
7661
|
let parsed;
|
|
7602
7662
|
try {
|
|
7603
|
-
parsed = JSON.parse(
|
|
7663
|
+
parsed = JSON.parse(raw2);
|
|
7604
7664
|
} catch {
|
|
7605
7665
|
await backupCorrupt2(path);
|
|
7606
7666
|
return cloneDefault();
|
|
@@ -7909,7 +7969,7 @@ function createDashboardLayoutRouter() {
|
|
|
7909
7969
|
init_lifecycle();
|
|
7910
7970
|
init_slug();
|
|
7911
7971
|
import { Router as Router2 } from "express";
|
|
7912
|
-
import { resolve as resolve18, basename as basename3, isAbsolute as
|
|
7972
|
+
import { resolve as resolve18, basename as basename3, isAbsolute as isAbsolute4 } from "path";
|
|
7913
7973
|
import { rm, readFile as readFile15, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
|
|
7914
7974
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
7915
7975
|
|
|
@@ -7928,6 +7988,20 @@ init_frontmatter();
|
|
|
7928
7988
|
init_fs();
|
|
7929
7989
|
import { spawn } from "child_process";
|
|
7930
7990
|
import { readFile as readFile13 } from "fs/promises";
|
|
7991
|
+
|
|
7992
|
+
// src/launch/cwd.ts
|
|
7993
|
+
import { existsSync, statSync as statSync2 } from "fs";
|
|
7994
|
+
import { isAbsolute as isAbsolute3 } from "path";
|
|
7995
|
+
function isExistingDir(p) {
|
|
7996
|
+
if (!p || !isAbsolute3(p)) return false;
|
|
7997
|
+
try {
|
|
7998
|
+
return existsSync(p) && statSync2(p).isDirectory();
|
|
7999
|
+
} catch {
|
|
8000
|
+
return false;
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
8003
|
+
|
|
8004
|
+
// src/utils/git-worktree.ts
|
|
7931
8005
|
function run(command, args, cwd) {
|
|
7932
8006
|
return new Promise((resolvePromise) => {
|
|
7933
8007
|
const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -7962,11 +8036,11 @@ async function createWorktree(opts) {
|
|
|
7962
8036
|
);
|
|
7963
8037
|
}
|
|
7964
8038
|
}
|
|
7965
|
-
async function removeWorktree(repository, worktreePath) {
|
|
7966
|
-
const
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
);
|
|
8039
|
+
async function removeWorktree(repository, worktreePath, opts = {}) {
|
|
8040
|
+
const args = ["-C", repository, "worktree", "remove"];
|
|
8041
|
+
if (opts.force) args.push("--force");
|
|
8042
|
+
args.push(worktreePath);
|
|
8043
|
+
const result = await run("git", args);
|
|
7970
8044
|
return { ok: result.code === 0, stderr: result.stderr };
|
|
7971
8045
|
}
|
|
7972
8046
|
async function deleteBranch(repository, branch) {
|
|
@@ -8007,6 +8081,82 @@ async function detectDefaultBranch(repository) {
|
|
|
8007
8081
|
}
|
|
8008
8082
|
return branches[0] ?? null;
|
|
8009
8083
|
}
|
|
8084
|
+
async function captureHeadSha(dir) {
|
|
8085
|
+
const result = await run("git", ["-C", dir, "rev-parse", "HEAD"]);
|
|
8086
|
+
if (result.code !== 0) return null;
|
|
8087
|
+
const sha = result.stdout.trim();
|
|
8088
|
+
return sha.length > 0 ? sha : null;
|
|
8089
|
+
}
|
|
8090
|
+
async function branchCheckedOutAt(repository, branch) {
|
|
8091
|
+
const result = await run("git", ["-C", repository, "worktree", "list", "--porcelain"]);
|
|
8092
|
+
if (result.code !== 0) return null;
|
|
8093
|
+
let currentPath = null;
|
|
8094
|
+
for (const line of result.stdout.split("\n")) {
|
|
8095
|
+
if (line.startsWith("worktree ")) {
|
|
8096
|
+
currentPath = line.slice("worktree ".length).trim();
|
|
8097
|
+
} else if (line.startsWith("branch ")) {
|
|
8098
|
+
const ref = line.slice("branch ".length).trim();
|
|
8099
|
+
if (ref === `refs/heads/${branch}` && currentPath) return currentPath;
|
|
8100
|
+
}
|
|
8101
|
+
}
|
|
8102
|
+
return null;
|
|
8103
|
+
}
|
|
8104
|
+
async function recreateWorktree(opts) {
|
|
8105
|
+
const { repository, worktreePath, branch } = opts;
|
|
8106
|
+
const originalHeadSha = opts.originalHeadSha ?? null;
|
|
8107
|
+
await run("git", ["-C", repository, "worktree", "prune"]);
|
|
8108
|
+
const add = async (args) => {
|
|
8109
|
+
const result = await run("git", ["-C", repository, "worktree", "add", ...args]);
|
|
8110
|
+
if (result.code !== 0) {
|
|
8111
|
+
throw new GitWorktreeError(
|
|
8112
|
+
`git worktree add failed (exit ${result.code}): ${result.stderr.trim() || "(no stderr)"}`,
|
|
8113
|
+
result.stderr
|
|
8114
|
+
);
|
|
8115
|
+
}
|
|
8116
|
+
};
|
|
8117
|
+
const refExists = async (ref) => {
|
|
8118
|
+
const result = await run("git", [
|
|
8119
|
+
"-C",
|
|
8120
|
+
repository,
|
|
8121
|
+
"rev-parse",
|
|
8122
|
+
"--verify",
|
|
8123
|
+
"--quiet",
|
|
8124
|
+
ref
|
|
8125
|
+
]);
|
|
8126
|
+
return result.code === 0;
|
|
8127
|
+
};
|
|
8128
|
+
if (branch && (await listBranches(repository)).includes(branch)) {
|
|
8129
|
+
const checkedOutAt = await branchCheckedOutAt(repository, branch);
|
|
8130
|
+
if (!checkedOutAt || checkedOutAt === worktreePath || !isExistingDir(checkedOutAt)) {
|
|
8131
|
+
await add([worktreePath, branch]);
|
|
8132
|
+
return { baseUsed: branch, exact: true, branch };
|
|
8133
|
+
}
|
|
8134
|
+
const detachBase = originalHeadSha && await refExists(`${originalHeadSha}^{commit}`) ? originalHeadSha : `refs/heads/${branch}`;
|
|
8135
|
+
await add(["--detach", worktreePath, detachBase]);
|
|
8136
|
+
return { baseUsed: detachBase, exact: detachBase === originalHeadSha, branch: null };
|
|
8137
|
+
}
|
|
8138
|
+
let baseUsed = null;
|
|
8139
|
+
if (originalHeadSha && await refExists(`${originalHeadSha}^{commit}`)) {
|
|
8140
|
+
baseUsed = originalHeadSha;
|
|
8141
|
+
} else if (branch && await refExists(`refs/remotes/origin/${branch}`)) {
|
|
8142
|
+
baseUsed = `refs/remotes/origin/${branch}`;
|
|
8143
|
+
} else {
|
|
8144
|
+
baseUsed = await detectDefaultBranch(repository);
|
|
8145
|
+
}
|
|
8146
|
+
if (!baseUsed) {
|
|
8147
|
+
throw new GitWorktreeError(
|
|
8148
|
+
`recreateWorktree: no base ref to recreate ${worktreePath} (no original sha, no origin/${branch ?? "<none>"}, no default branch)`,
|
|
8149
|
+
""
|
|
8150
|
+
);
|
|
8151
|
+
}
|
|
8152
|
+
const exact = baseUsed === originalHeadSha;
|
|
8153
|
+
if (branch) {
|
|
8154
|
+
await add(["-b", branch, worktreePath, baseUsed]);
|
|
8155
|
+
return { baseUsed, exact, branch };
|
|
8156
|
+
}
|
|
8157
|
+
await add(["--detach", worktreePath, baseUsed]);
|
|
8158
|
+
return { baseUsed, exact, branch: null };
|
|
8159
|
+
}
|
|
8010
8160
|
async function createWorktreeAndRecord(opts) {
|
|
8011
8161
|
const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;
|
|
8012
8162
|
await createWorktree({ repository, branch, worktreePath, parentBranch });
|
|
@@ -8020,7 +8170,7 @@ async function createWorktreeAndRecord(opts) {
|
|
|
8020
8170
|
});
|
|
8021
8171
|
await writeFileForce(assignmentPath, updated);
|
|
8022
8172
|
} catch (writeErr) {
|
|
8023
|
-
const cleanup = await removeWorktree(repository, worktreePath);
|
|
8173
|
+
const cleanup = await removeWorktree(repository, worktreePath, { force: true });
|
|
8024
8174
|
const branchCleanup = await deleteBranch(repository, branch);
|
|
8025
8175
|
const writeMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);
|
|
8026
8176
|
throw new Error(
|
|
@@ -8121,6 +8271,154 @@ function validateBranchName(name) {
|
|
|
8121
8271
|
return null;
|
|
8122
8272
|
}
|
|
8123
8273
|
|
|
8274
|
+
// src/dashboard/recreate-target.ts
|
|
8275
|
+
init_api();
|
|
8276
|
+
init_agent_sessions();
|
|
8277
|
+
async function resolveRecreateTarget(deps, target) {
|
|
8278
|
+
const { projectsDir, assignmentsDir: assignmentsDir2 } = deps;
|
|
8279
|
+
if (target.kind === "assignment") {
|
|
8280
|
+
const detail = "id" in target ? await getAssignmentDetailById(projectsDir, assignmentsDir2, target.id) : await getAssignmentDetail(
|
|
8281
|
+
projectsDir,
|
|
8282
|
+
target.projectSlug,
|
|
8283
|
+
target.assignmentSlug
|
|
8284
|
+
);
|
|
8285
|
+
if (!detail) return null;
|
|
8286
|
+
const worktreePath2 = detail.workspace.worktreePath ?? "";
|
|
8287
|
+
const repository2 = detail.workspace.repository ?? null;
|
|
8288
|
+
const branch2 = detail.workspace.branch ?? null;
|
|
8289
|
+
const missing2 = worktreePath2 !== "" && !isExistingDir(worktreePath2);
|
|
8290
|
+
return {
|
|
8291
|
+
kind: "assignment",
|
|
8292
|
+
id: detail.id,
|
|
8293
|
+
projectSlug: detail.projectSlug ?? null,
|
|
8294
|
+
assignmentSlug: detail.slug,
|
|
8295
|
+
worktreePath: worktreePath2,
|
|
8296
|
+
repository: repository2,
|
|
8297
|
+
branch: branch2,
|
|
8298
|
+
originalHeadSha: null,
|
|
8299
|
+
missing: missing2,
|
|
8300
|
+
recreatable: missing2 && isExistingDir(repository2)
|
|
8301
|
+
};
|
|
8302
|
+
}
|
|
8303
|
+
const session = getSessionById(target.id);
|
|
8304
|
+
if (!session) return null;
|
|
8305
|
+
let repository = null;
|
|
8306
|
+
let branch = null;
|
|
8307
|
+
let assignmentWorktreePath = "";
|
|
8308
|
+
if (session.projectSlug && session.assignmentSlug) {
|
|
8309
|
+
const detail = await getAssignmentDetail(
|
|
8310
|
+
projectsDir,
|
|
8311
|
+
session.projectSlug,
|
|
8312
|
+
session.assignmentSlug
|
|
8313
|
+
);
|
|
8314
|
+
if (detail) {
|
|
8315
|
+
repository = detail.workspace.repository ?? null;
|
|
8316
|
+
branch = detail.workspace.branch ?? null;
|
|
8317
|
+
assignmentWorktreePath = detail.workspace.worktreePath ?? "";
|
|
8318
|
+
}
|
|
8319
|
+
} else if (session.assignmentSlug) {
|
|
8320
|
+
const detail = await getAssignmentDetailById(
|
|
8321
|
+
projectsDir,
|
|
8322
|
+
assignmentsDir2,
|
|
8323
|
+
session.assignmentSlug
|
|
8324
|
+
);
|
|
8325
|
+
if (detail) {
|
|
8326
|
+
repository = detail.workspace.repository ?? null;
|
|
8327
|
+
branch = detail.workspace.branch ?? null;
|
|
8328
|
+
assignmentWorktreePath = detail.workspace.worktreePath ?? "";
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
const worktreePath = session.path || assignmentWorktreePath;
|
|
8332
|
+
const missing = worktreePath !== "" && !isExistingDir(worktreePath);
|
|
8333
|
+
return {
|
|
8334
|
+
kind: "session",
|
|
8335
|
+
id: session.sessionId,
|
|
8336
|
+
projectSlug: session.projectSlug ?? null,
|
|
8337
|
+
assignmentSlug: session.assignmentSlug ?? null,
|
|
8338
|
+
worktreePath,
|
|
8339
|
+
repository,
|
|
8340
|
+
branch,
|
|
8341
|
+
originalHeadSha: session.originalHeadSha ?? null,
|
|
8342
|
+
missing,
|
|
8343
|
+
recreatable: missing && isExistingDir(repository)
|
|
8344
|
+
};
|
|
8345
|
+
}
|
|
8346
|
+
|
|
8347
|
+
// src/dashboard/worktree-recreate.ts
|
|
8348
|
+
async function recreateForTarget(deps, target) {
|
|
8349
|
+
const t = await resolveRecreateTarget(deps, target);
|
|
8350
|
+
if (!t) return { status: "not-found" };
|
|
8351
|
+
if (t.worktreePath === "") return { status: "no-path" };
|
|
8352
|
+
if (isExistingDir(t.worktreePath)) {
|
|
8353
|
+
return { status: "already-exists", branch: t.branch };
|
|
8354
|
+
}
|
|
8355
|
+
if (!t.repository) return { status: "no-repo" };
|
|
8356
|
+
const repoCheck = await assertRepoRoot(t.repository);
|
|
8357
|
+
if (!repoCheck.ok) {
|
|
8358
|
+
return { status: "bad-repo", httpStatus: repoCheck.status, error: repoCheck.error };
|
|
8359
|
+
}
|
|
8360
|
+
const key = `recreate:${t.worktreePath}`;
|
|
8361
|
+
if (worktreeInFlight.has(key)) return { status: "in-flight" };
|
|
8362
|
+
worktreeInFlight.add(key);
|
|
8363
|
+
try {
|
|
8364
|
+
const r = await recreateWorktree({
|
|
8365
|
+
repository: repoCheck.repo,
|
|
8366
|
+
worktreePath: t.worktreePath,
|
|
8367
|
+
branch: t.branch,
|
|
8368
|
+
originalHeadSha: t.originalHeadSha
|
|
8369
|
+
});
|
|
8370
|
+
return { status: "recreated", baseUsed: r.baseUsed, exact: r.exact, branch: r.branch };
|
|
8371
|
+
} finally {
|
|
8372
|
+
worktreeInFlight.delete(key);
|
|
8373
|
+
}
|
|
8374
|
+
}
|
|
8375
|
+
function recreateOutcomeToHttp(outcome) {
|
|
8376
|
+
switch (outcome.status) {
|
|
8377
|
+
case "not-found":
|
|
8378
|
+
return { httpStatus: 404, body: { error: "Target not found." } };
|
|
8379
|
+
case "no-path":
|
|
8380
|
+
return {
|
|
8381
|
+
httpStatus: 422,
|
|
8382
|
+
body: { error: "No recorded worktree path to recreate." }
|
|
8383
|
+
};
|
|
8384
|
+
case "no-repo":
|
|
8385
|
+
return {
|
|
8386
|
+
httpStatus: 422,
|
|
8387
|
+
body: {
|
|
8388
|
+
error: "Cannot recreate: no repository on record for this worktree."
|
|
8389
|
+
}
|
|
8390
|
+
};
|
|
8391
|
+
case "bad-repo":
|
|
8392
|
+
return { httpStatus: outcome.httpStatus, body: { error: outcome.error } };
|
|
8393
|
+
case "in-flight":
|
|
8394
|
+
return {
|
|
8395
|
+
httpStatus: 409,
|
|
8396
|
+
body: { error: "A recreate is already in progress for this worktree." }
|
|
8397
|
+
};
|
|
8398
|
+
case "already-exists":
|
|
8399
|
+
return {
|
|
8400
|
+
httpStatus: 200,
|
|
8401
|
+
body: {
|
|
8402
|
+
ok: true,
|
|
8403
|
+
alreadyExisted: true,
|
|
8404
|
+
baseUsed: outcome.branch ?? "HEAD",
|
|
8405
|
+
exact: true,
|
|
8406
|
+
branch: outcome.branch
|
|
8407
|
+
}
|
|
8408
|
+
};
|
|
8409
|
+
case "recreated":
|
|
8410
|
+
return {
|
|
8411
|
+
httpStatus: 200,
|
|
8412
|
+
body: {
|
|
8413
|
+
ok: true,
|
|
8414
|
+
baseUsed: outcome.baseUsed,
|
|
8415
|
+
exact: outcome.exact,
|
|
8416
|
+
branch: outcome.branch
|
|
8417
|
+
}
|
|
8418
|
+
};
|
|
8419
|
+
}
|
|
8420
|
+
}
|
|
8421
|
+
|
|
8124
8422
|
// src/dashboard/repository-candidates.ts
|
|
8125
8423
|
init_fs();
|
|
8126
8424
|
init_parser();
|
|
@@ -8145,8 +8443,8 @@ async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
|
|
|
8145
8443
|
const projectPath = resolve17(projectsDir, projectSlug, "project.md");
|
|
8146
8444
|
if (await fileExists(projectPath)) {
|
|
8147
8445
|
const project = parseProject(await readFile14(projectPath, "utf-8"));
|
|
8148
|
-
for (const
|
|
8149
|
-
const path =
|
|
8446
|
+
for (const raw2 of project.repositories) {
|
|
8447
|
+
const path = raw2.trim();
|
|
8150
8448
|
if (!path) continue;
|
|
8151
8449
|
const abs = resolve17(path);
|
|
8152
8450
|
if (seen.has(abs)) continue;
|
|
@@ -8807,7 +9105,7 @@ async function assertRepoRoot(repoInput) {
|
|
|
8807
9105
|
return { ok: false, status: 400, error: "`repository` is required." };
|
|
8808
9106
|
}
|
|
8809
9107
|
const repo = repoInput.trim();
|
|
8810
|
-
if (!
|
|
9108
|
+
if (!isAbsolute4(repo)) {
|
|
8811
9109
|
return { ok: false, status: 400, error: "`repository` must be an absolute path." };
|
|
8812
9110
|
}
|
|
8813
9111
|
try {
|
|
@@ -10046,6 +10344,45 @@ ${entry}`;
|
|
|
10046
10344
|
}
|
|
10047
10345
|
}
|
|
10048
10346
|
);
|
|
10347
|
+
router.post(
|
|
10348
|
+
"/api/projects/:slug/assignments/:aslug/worktree/recreate",
|
|
10349
|
+
async (req, res) => {
|
|
10350
|
+
try {
|
|
10351
|
+
const projectSlug = getParam(req.params.slug);
|
|
10352
|
+
const assignmentSlug = getParam(req.params.aslug);
|
|
10353
|
+
const outcome = await recreateForTarget(
|
|
10354
|
+
{ projectsDir, assignmentsDir: assignmentsDir2 ?? "" },
|
|
10355
|
+
{ kind: "assignment", projectSlug, assignmentSlug }
|
|
10356
|
+
);
|
|
10357
|
+
const { httpStatus, body } = recreateOutcomeToHttp(outcome);
|
|
10358
|
+
res.status(httpStatus).json(body);
|
|
10359
|
+
} catch (error) {
|
|
10360
|
+
console.error("Error recreating worktree:", error);
|
|
10361
|
+
res.status(500).json({ error: `Failed to recreate worktree: ${error.message}` });
|
|
10362
|
+
}
|
|
10363
|
+
}
|
|
10364
|
+
);
|
|
10365
|
+
router.post(
|
|
10366
|
+
"/api/assignments/:id/worktree/recreate",
|
|
10367
|
+
async (req, res) => {
|
|
10368
|
+
try {
|
|
10369
|
+
if (!assignmentsDir2) {
|
|
10370
|
+
res.status(501).json({ error: "Standalone assignments not configured on this server" });
|
|
10371
|
+
return;
|
|
10372
|
+
}
|
|
10373
|
+
const id = getParam(req.params.id);
|
|
10374
|
+
const outcome = await recreateForTarget(
|
|
10375
|
+
{ projectsDir, assignmentsDir: assignmentsDir2 },
|
|
10376
|
+
{ kind: "assignment", id }
|
|
10377
|
+
);
|
|
10378
|
+
const { httpStatus, body } = recreateOutcomeToHttp(outcome);
|
|
10379
|
+
res.status(httpStatus).json(body);
|
|
10380
|
+
} catch (error) {
|
|
10381
|
+
console.error("Error recreating worktree:", error);
|
|
10382
|
+
res.status(500).json({ error: `Failed to recreate worktree: ${error.message}` });
|
|
10383
|
+
}
|
|
10384
|
+
}
|
|
10385
|
+
);
|
|
10049
10386
|
router.post("/api/projects/:slug/status-override", async (req, res) => {
|
|
10050
10387
|
try {
|
|
10051
10388
|
const projectSlug = getParam(req.params.slug);
|
|
@@ -11308,6 +11645,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
|
|
|
11308
11645
|
const recordedPath = derivedPath ?? path ?? "";
|
|
11309
11646
|
const pid = typeof rawPid === "number" && Number.isFinite(rawPid) && rawPid > 0 ? rawPid : null;
|
|
11310
11647
|
const pidStartedAt = pid !== null ? captureProcessStartedAt(pid) : null;
|
|
11648
|
+
const originalHeadSha = isExistingDir(recordedPath) ? await captureHeadSha(recordedPath) : null;
|
|
11311
11649
|
const session = {
|
|
11312
11650
|
projectSlug: projectSlug || null,
|
|
11313
11651
|
assignmentSlug: assignmentSlug || null,
|
|
@@ -11319,7 +11657,8 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
|
|
|
11319
11657
|
description: description || null,
|
|
11320
11658
|
transcriptPath: transcriptPath || null,
|
|
11321
11659
|
pid,
|
|
11322
|
-
pidStartedAt
|
|
11660
|
+
pidStartedAt,
|
|
11661
|
+
originalHeadSha
|
|
11323
11662
|
};
|
|
11324
11663
|
await appendSession("", session);
|
|
11325
11664
|
broadcast?.({ type: "agent-sessions-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -11328,6 +11667,21 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
|
|
|
11328
11667
|
res.status(500).json({ error: error instanceof Error ? error.message : "Registration failed" });
|
|
11329
11668
|
}
|
|
11330
11669
|
});
|
|
11670
|
+
router.post("/:sessionId/worktree/recreate", async (req, res) => {
|
|
11671
|
+
try {
|
|
11672
|
+
const { sessionId } = req.params;
|
|
11673
|
+
const outcome = await recreateForTarget(
|
|
11674
|
+
{ projectsDir, assignmentsDir: assignmentsDir2 ?? "" },
|
|
11675
|
+
{ kind: "session", id: sessionId }
|
|
11676
|
+
);
|
|
11677
|
+
const { httpStatus, body } = recreateOutcomeToHttp(outcome);
|
|
11678
|
+
res.status(httpStatus).json(body);
|
|
11679
|
+
} catch (error) {
|
|
11680
|
+
res.status(500).json({
|
|
11681
|
+
error: error instanceof Error ? error.message : "Failed to recreate worktree"
|
|
11682
|
+
});
|
|
11683
|
+
}
|
|
11684
|
+
});
|
|
11331
11685
|
router.patch("/:sessionId", async (req, res) => {
|
|
11332
11686
|
try {
|
|
11333
11687
|
const { sessionId } = req.params;
|
|
@@ -11459,8 +11813,8 @@ function mapAgentErrorToFieldErrors(err) {
|
|
|
11459
11813
|
}
|
|
11460
11814
|
return { error: message };
|
|
11461
11815
|
}
|
|
11462
|
-
function coerceAgentRow(
|
|
11463
|
-
if (!
|
|
11816
|
+
function coerceAgentRow(raw2, index) {
|
|
11817
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) {
|
|
11464
11818
|
return {
|
|
11465
11819
|
ok: false,
|
|
11466
11820
|
status: 400,
|
|
@@ -11470,7 +11824,7 @@ function coerceAgentRow(raw, index) {
|
|
|
11470
11824
|
}
|
|
11471
11825
|
};
|
|
11472
11826
|
}
|
|
11473
|
-
const entry =
|
|
11827
|
+
const entry = raw2;
|
|
11474
11828
|
if (typeof entry.id !== "string" || entry.id.length === 0) {
|
|
11475
11829
|
return {
|
|
11476
11830
|
ok: false,
|
|
@@ -11579,14 +11933,14 @@ function createAgentsRouter() {
|
|
|
11579
11933
|
});
|
|
11580
11934
|
router.put("/", async (req, res) => {
|
|
11581
11935
|
try {
|
|
11582
|
-
const
|
|
11583
|
-
if (!Array.isArray(
|
|
11936
|
+
const raw2 = req.body && typeof req.body === "object" ? req.body : {};
|
|
11937
|
+
if (!Array.isArray(raw2.agents)) {
|
|
11584
11938
|
res.status(400).json({ error: "agents must be an array" });
|
|
11585
11939
|
return;
|
|
11586
11940
|
}
|
|
11587
11941
|
const cleaned = [];
|
|
11588
|
-
for (let i = 0; i <
|
|
11589
|
-
const result = coerceAgentRow(
|
|
11942
|
+
for (let i = 0; i < raw2.agents.length; i++) {
|
|
11943
|
+
const result = coerceAgentRow(raw2.agents[i], i);
|
|
11590
11944
|
if (!result.ok) {
|
|
11591
11945
|
res.status(result.status).json(result.body);
|
|
11592
11946
|
return;
|
|
@@ -11628,7 +11982,7 @@ import { Router as Router6 } from "express";
|
|
|
11628
11982
|
|
|
11629
11983
|
// src/utils/terminal-probe.ts
|
|
11630
11984
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
11631
|
-
import { existsSync } from "fs";
|
|
11985
|
+
import { existsSync as existsSync2 } from "fs";
|
|
11632
11986
|
import { homedir as homedir2 } from "os";
|
|
11633
11987
|
import { join as join2 } from "path";
|
|
11634
11988
|
var APP_BUNDLE_IDS = {
|
|
@@ -11650,12 +12004,12 @@ function defaultApplicationsDirs() {
|
|
|
11650
12004
|
}
|
|
11651
12005
|
function findAppBundle(terminal, dirs = defaultApplicationsDirs()) {
|
|
11652
12006
|
const fixed = APP_FIXED_PATHS[terminal];
|
|
11653
|
-
if (fixed &&
|
|
12007
|
+
if (fixed && existsSync2(fixed)) return fixed;
|
|
11654
12008
|
const bundleName = APP_BUNDLE_NAMES[terminal];
|
|
11655
12009
|
if (bundleName) {
|
|
11656
12010
|
for (const dir of dirs) {
|
|
11657
12011
|
const candidate = join2(dir, bundleName);
|
|
11658
|
-
if (
|
|
12012
|
+
if (existsSync2(candidate)) return candidate;
|
|
11659
12013
|
}
|
|
11660
12014
|
}
|
|
11661
12015
|
return null;
|
|
@@ -11692,50 +12046,6 @@ function probeTerminalInstalled(terminal, opts = {}) {
|
|
|
11692
12046
|
return { ok: false, reason: "no-probe-available" };
|
|
11693
12047
|
}
|
|
11694
12048
|
|
|
11695
|
-
// src/dashboard/api-launch-preflight.ts
|
|
11696
|
-
init_api();
|
|
11697
|
-
|
|
11698
|
-
// src/launch/cwd.ts
|
|
11699
|
-
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
11700
|
-
import { isAbsolute as isAbsolute3 } from "path";
|
|
11701
|
-
function isExistingDir(p) {
|
|
11702
|
-
if (!p || !isAbsolute3(p)) return false;
|
|
11703
|
-
try {
|
|
11704
|
-
return existsSync2(p) && statSync2(p).isDirectory();
|
|
11705
|
-
} catch {
|
|
11706
|
-
return false;
|
|
11707
|
-
}
|
|
11708
|
-
}
|
|
11709
|
-
function resolveWorkspaceCwd(input) {
|
|
11710
|
-
const { worktreePath, repository, branch, assignmentSlug } = input;
|
|
11711
|
-
if (isExistingDir(worktreePath)) {
|
|
11712
|
-
return { cwd: worktreePath, fallbackWarning: null, invalidReason: null };
|
|
11713
|
-
}
|
|
11714
|
-
if (isExistingDir(repository)) {
|
|
11715
|
-
const fallbackWarning = worktreePath ? `syntaur: workspace.worktreePath ${worktreePath} is not an existing directory for ${assignmentSlug} \u2014 launching in ${repository}` : formatFallbackCwdWarning({
|
|
11716
|
-
assignmentSlug,
|
|
11717
|
-
workspaceDir: repository,
|
|
11718
|
-
worktreePath,
|
|
11719
|
-
branch
|
|
11720
|
-
});
|
|
11721
|
-
return { cwd: repository, fallbackWarning, invalidReason: null };
|
|
11722
|
-
}
|
|
11723
|
-
const shown = (p) => p && p.trim().length > 0 ? p : "(unset)";
|
|
11724
|
-
return {
|
|
11725
|
-
cwd: null,
|
|
11726
|
-
fallbackWarning: null,
|
|
11727
|
-
invalidReason: `workspace path invalid for ${assignmentSlug}: tried worktreePath ${shown(worktreePath)} and repository ${shown(repository)} \u2014 neither is an existing directory`
|
|
11728
|
-
};
|
|
11729
|
-
}
|
|
11730
|
-
function formatFallbackCwdWarning(opts) {
|
|
11731
|
-
const missing = [];
|
|
11732
|
-
if (!opts.worktreePath) missing.push("worktreePath");
|
|
11733
|
-
if (!opts.branch) missing.push("branch");
|
|
11734
|
-
if (missing.length === 0) return null;
|
|
11735
|
-
const fields = missing.map((m) => `workspace.${m}`).join(" and ");
|
|
11736
|
-
return `syntaur: ${fields} not set for ${opts.assignmentSlug} \u2014 launching in ${opts.workspaceDir}`;
|
|
11737
|
-
}
|
|
11738
|
-
|
|
11739
12049
|
// src/dashboard/api-launch-preflight.ts
|
|
11740
12050
|
function createLaunchPreflightRouter(projectsDir, assignmentsDir2) {
|
|
11741
12051
|
const router = Router6();
|
|
@@ -11763,29 +12073,44 @@ function createLaunchPreflightRouter(projectsDir, assignmentsDir2) {
|
|
|
11763
12073
|
return;
|
|
11764
12074
|
}
|
|
11765
12075
|
const target = body.target;
|
|
11766
|
-
if (target && target.kind === "assignment" && typeof target.id === "string") {
|
|
11767
|
-
const
|
|
11768
|
-
projectsDir,
|
|
11769
|
-
|
|
11770
|
-
target.id
|
|
12076
|
+
if (target && (target.kind === "assignment" || target.kind === "session") && typeof target.id === "string") {
|
|
12077
|
+
const resolved = await resolveRecreateTarget(
|
|
12078
|
+
{ projectsDir, assignmentsDir: assignmentsDir2 },
|
|
12079
|
+
target.kind === "assignment" ? { kind: "assignment", id: target.id } : { kind: "session", id: target.id }
|
|
11771
12080
|
);
|
|
11772
|
-
if (
|
|
11773
|
-
const
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
12081
|
+
if (resolved && resolved.missing) {
|
|
12082
|
+
const response2 = resolved.recreatable ? {
|
|
12083
|
+
ok: false,
|
|
12084
|
+
terminal,
|
|
12085
|
+
reason: "workspace-path-invalid",
|
|
12086
|
+
message: `Worktree ${resolved.worktreePath} was deleted.`,
|
|
12087
|
+
recreate: {
|
|
12088
|
+
kind: resolved.kind,
|
|
12089
|
+
id: resolved.id,
|
|
12090
|
+
projectSlug: resolved.projectSlug,
|
|
12091
|
+
assignmentSlug: resolved.assignmentSlug,
|
|
12092
|
+
deletedPath: resolved.worktreePath,
|
|
12093
|
+
repository: resolved.repository,
|
|
12094
|
+
branch: resolved.branch
|
|
12095
|
+
}
|
|
12096
|
+
} : {
|
|
12097
|
+
ok: false,
|
|
12098
|
+
terminal,
|
|
12099
|
+
reason: "workspace-path-invalid",
|
|
12100
|
+
message: `Worktree ${resolved.worktreePath} was deleted and can't be auto-recreated (no repository on record). Set a valid workspace for this assignment, then try again.`
|
|
12101
|
+
};
|
|
12102
|
+
res.json(response2);
|
|
12103
|
+
return;
|
|
12104
|
+
}
|
|
12105
|
+
if (resolved && resolved.kind === "assignment" && resolved.worktreePath === "" && !isExistingDir(resolved.repository)) {
|
|
12106
|
+
const response2 = {
|
|
12107
|
+
ok: false,
|
|
12108
|
+
terminal,
|
|
12109
|
+
reason: "workspace-path-invalid",
|
|
12110
|
+
message: "This assignment has no valid workspace directory. Set a repository or worktree, then try again."
|
|
12111
|
+
};
|
|
12112
|
+
res.json(response2);
|
|
12113
|
+
return;
|
|
11789
12114
|
}
|
|
11790
12115
|
}
|
|
11791
12116
|
const response = { ok: true, terminal };
|
|
@@ -12186,18 +12511,18 @@ function buildAffectedResponse(id, list) {
|
|
|
12186
12511
|
function isString(x) {
|
|
12187
12512
|
return typeof x === "string" && x.length > 0;
|
|
12188
12513
|
}
|
|
12189
|
-
function parseResolutions(
|
|
12190
|
-
if (
|
|
12514
|
+
function parseResolutions(raw2) {
|
|
12515
|
+
if (raw2 === void 0) {
|
|
12191
12516
|
return { resolutions: [], malformed: null, duplicateIds: null };
|
|
12192
12517
|
}
|
|
12193
|
-
if (!Array.isArray(
|
|
12518
|
+
if (!Array.isArray(raw2)) {
|
|
12194
12519
|
return { resolutions: [], malformed: "resolutions must be an array", duplicateIds: null };
|
|
12195
12520
|
}
|
|
12196
12521
|
const out = [];
|
|
12197
12522
|
const seen = /* @__PURE__ */ new Set();
|
|
12198
12523
|
const dups = /* @__PURE__ */ new Set();
|
|
12199
|
-
for (let i = 0; i <
|
|
12200
|
-
const r =
|
|
12524
|
+
for (let i = 0; i < raw2.length; i++) {
|
|
12525
|
+
const r = raw2[i];
|
|
12201
12526
|
if (!r || typeof r !== "object") {
|
|
12202
12527
|
return { resolutions: [], malformed: `resolutions[${i}] must be an object`, duplicateIds: null };
|
|
12203
12528
|
}
|
|
@@ -13173,10 +13498,10 @@ init_fs_migration();
|
|
|
13173
13498
|
init_parser2();
|
|
13174
13499
|
init_fs();
|
|
13175
13500
|
init_paths();
|
|
13176
|
-
import { Router as
|
|
13177
|
-
import { readdir as
|
|
13178
|
-
import { resolve as resolvePath, dirname as
|
|
13179
|
-
import { rename as
|
|
13501
|
+
import { Router as Router14 } from "express";
|
|
13502
|
+
import { readdir as readdir11 } from "fs/promises";
|
|
13503
|
+
import { resolve as resolvePath, dirname as dirname6 } from "path";
|
|
13504
|
+
import { rename as rename6, mkdir as mkdir3 } from "fs/promises";
|
|
13180
13505
|
init_slug();
|
|
13181
13506
|
|
|
13182
13507
|
// src/utils/promote-todos.ts
|
|
@@ -13515,6 +13840,331 @@ async function promoteTodosToNewAssignment(groups, options) {
|
|
|
13515
13840
|
|
|
13516
13841
|
// src/dashboard/api-todos.ts
|
|
13517
13842
|
init_api();
|
|
13843
|
+
|
|
13844
|
+
// src/dashboard/todo-attachments-routes.ts
|
|
13845
|
+
import { raw } from "express";
|
|
13846
|
+
|
|
13847
|
+
// src/todos/attachments.ts
|
|
13848
|
+
import { mkdir as mkdir2, readdir as readdir10, stat, rename as rename5, rm as rm3, unlink as unlink5, writeFile as writeFile5, cp } from "fs/promises";
|
|
13849
|
+
import { resolve as resolve25, basename as basename4, dirname as dirname5, extname } from "path";
|
|
13850
|
+
|
|
13851
|
+
// src/utils/proof-artifact-id.ts
|
|
13852
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
13853
|
+
function generateArtifactId() {
|
|
13854
|
+
const ts = Date.now().toString(36);
|
|
13855
|
+
const rand = randomBytes2(2).toString("hex");
|
|
13856
|
+
return `${ts}-${rand}`;
|
|
13857
|
+
}
|
|
13858
|
+
|
|
13859
|
+
// src/todos/attachments.ts
|
|
13860
|
+
var SCOPE_RE = /^[a-z0-9_][a-z0-9-]*$/;
|
|
13861
|
+
var TODO_ID_RE = /^[a-f0-9]{4}$/;
|
|
13862
|
+
var ATTACHMENT_ID_RE = /^[a-z0-9]+-[0-9a-f]{4}$/;
|
|
13863
|
+
var AttachmentValidationError = class extends Error {
|
|
13864
|
+
constructor(message) {
|
|
13865
|
+
super(message);
|
|
13866
|
+
this.name = "AttachmentValidationError";
|
|
13867
|
+
}
|
|
13868
|
+
};
|
|
13869
|
+
function assertScope(scopeId) {
|
|
13870
|
+
if (!SCOPE_RE.test(scopeId)) throw new AttachmentValidationError(`Invalid scope id: "${scopeId}"`);
|
|
13871
|
+
}
|
|
13872
|
+
function assertTodoId(todoId) {
|
|
13873
|
+
if (!TODO_ID_RE.test(todoId)) throw new AttachmentValidationError(`Invalid todo id: "${todoId}"`);
|
|
13874
|
+
}
|
|
13875
|
+
function assertAttachmentId(attachmentId) {
|
|
13876
|
+
if (!ATTACHMENT_ID_RE.test(attachmentId)) {
|
|
13877
|
+
throw new AttachmentValidationError(`Invalid attachment id: "${attachmentId}"`);
|
|
13878
|
+
}
|
|
13879
|
+
}
|
|
13880
|
+
var EXT_MIME = {
|
|
13881
|
+
png: "image/png",
|
|
13882
|
+
jpg: "image/jpeg",
|
|
13883
|
+
jpeg: "image/jpeg",
|
|
13884
|
+
gif: "image/gif",
|
|
13885
|
+
webp: "image/webp",
|
|
13886
|
+
bmp: "image/bmp",
|
|
13887
|
+
ico: "image/x-icon",
|
|
13888
|
+
svg: "image/svg+xml",
|
|
13889
|
+
pdf: "application/pdf",
|
|
13890
|
+
txt: "text/plain",
|
|
13891
|
+
log: "text/plain",
|
|
13892
|
+
md: "text/markdown",
|
|
13893
|
+
json: "application/json",
|
|
13894
|
+
csv: "text/csv",
|
|
13895
|
+
html: "text/html",
|
|
13896
|
+
htm: "text/html",
|
|
13897
|
+
xml: "application/xml",
|
|
13898
|
+
mp4: "video/mp4",
|
|
13899
|
+
mov: "video/quicktime",
|
|
13900
|
+
webm: "video/webm",
|
|
13901
|
+
mp3: "audio/mpeg",
|
|
13902
|
+
wav: "audio/wav",
|
|
13903
|
+
m4a: "audio/mp4",
|
|
13904
|
+
zip: "application/zip"
|
|
13905
|
+
};
|
|
13906
|
+
function mimeForName(name) {
|
|
13907
|
+
const ext = extname(name).slice(1).toLowerCase();
|
|
13908
|
+
return EXT_MIME[ext] ?? "application/octet-stream";
|
|
13909
|
+
}
|
|
13910
|
+
var SAFE_INLINE_MIME = /* @__PURE__ */ new Set([
|
|
13911
|
+
"image/png",
|
|
13912
|
+
"image/jpeg",
|
|
13913
|
+
"image/gif",
|
|
13914
|
+
"image/webp",
|
|
13915
|
+
"application/pdf",
|
|
13916
|
+
"text/plain"
|
|
13917
|
+
]);
|
|
13918
|
+
function isSafeInlineMime(mime) {
|
|
13919
|
+
return SAFE_INLINE_MIME.has(mime);
|
|
13920
|
+
}
|
|
13921
|
+
function sanitizeAttachmentName(name) {
|
|
13922
|
+
let n = basename4(name || "").replace(/["'\\/]/g, "_");
|
|
13923
|
+
n = Array.from(n, (ch) => {
|
|
13924
|
+
const code = ch.charCodeAt(0);
|
|
13925
|
+
return code < 32 || code === 127 ? "_" : ch;
|
|
13926
|
+
}).join("");
|
|
13927
|
+
n = n.trim();
|
|
13928
|
+
if (!n || n === "." || n === "..") n = "file";
|
|
13929
|
+
if (n.length > 120) {
|
|
13930
|
+
const ext = extname(n);
|
|
13931
|
+
n = n.slice(0, Math.max(1, 120 - ext.length)) + ext;
|
|
13932
|
+
}
|
|
13933
|
+
return n;
|
|
13934
|
+
}
|
|
13935
|
+
function attachmentsRootDir(todosDir2) {
|
|
13936
|
+
return resolve25(todosDir2, "attachments");
|
|
13937
|
+
}
|
|
13938
|
+
function attachmentDirFor(todosDir2, scopeId, todoId) {
|
|
13939
|
+
assertScope(scopeId);
|
|
13940
|
+
assertTodoId(todoId);
|
|
13941
|
+
return resolve25(attachmentsRootDir(todosDir2), scopeId, todoId);
|
|
13942
|
+
}
|
|
13943
|
+
async function dirExists(p) {
|
|
13944
|
+
try {
|
|
13945
|
+
return (await stat(p)).isDirectory();
|
|
13946
|
+
} catch {
|
|
13947
|
+
return false;
|
|
13948
|
+
}
|
|
13949
|
+
}
|
|
13950
|
+
async function writeAttachment(todosDir2, scopeId, todoId, originalName, bytes) {
|
|
13951
|
+
const dir = attachmentDirFor(todosDir2, scopeId, todoId);
|
|
13952
|
+
await mkdir2(dir, { recursive: true });
|
|
13953
|
+
const id = generateArtifactId();
|
|
13954
|
+
const filename = sanitizeAttachmentName(originalName);
|
|
13955
|
+
await writeFile5(resolve25(dir, `${id}__${filename}`), bytes);
|
|
13956
|
+
return {
|
|
13957
|
+
id,
|
|
13958
|
+
filename,
|
|
13959
|
+
mime: mimeForName(filename),
|
|
13960
|
+
size: bytes.length,
|
|
13961
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13962
|
+
};
|
|
13963
|
+
}
|
|
13964
|
+
async function listAttachments(todosDir2, scopeId, todoId) {
|
|
13965
|
+
const dir = attachmentDirFor(todosDir2, scopeId, todoId);
|
|
13966
|
+
let names;
|
|
13967
|
+
try {
|
|
13968
|
+
names = await readdir10(dir);
|
|
13969
|
+
} catch {
|
|
13970
|
+
return [];
|
|
13971
|
+
}
|
|
13972
|
+
const out = [];
|
|
13973
|
+
for (const stored of names) {
|
|
13974
|
+
const sep2 = stored.indexOf("__");
|
|
13975
|
+
if (sep2 <= 0) continue;
|
|
13976
|
+
const id = stored.slice(0, sep2);
|
|
13977
|
+
if (!ATTACHMENT_ID_RE.test(id)) continue;
|
|
13978
|
+
const filename = stored.slice(sep2 + 2);
|
|
13979
|
+
try {
|
|
13980
|
+
const st = await stat(resolve25(dir, stored));
|
|
13981
|
+
if (!st.isFile()) continue;
|
|
13982
|
+
out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
|
|
13983
|
+
} catch {
|
|
13984
|
+
}
|
|
13985
|
+
}
|
|
13986
|
+
out.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
13987
|
+
return out;
|
|
13988
|
+
}
|
|
13989
|
+
async function readScopeAttachments(todosDir2, scopeId) {
|
|
13990
|
+
assertScope(scopeId);
|
|
13991
|
+
const scopeDir = resolve25(attachmentsRootDir(todosDir2), scopeId);
|
|
13992
|
+
let todoIds;
|
|
13993
|
+
try {
|
|
13994
|
+
todoIds = await readdir10(scopeDir);
|
|
13995
|
+
} catch {
|
|
13996
|
+
return {};
|
|
13997
|
+
}
|
|
13998
|
+
const result = {};
|
|
13999
|
+
for (const todoId of todoIds) {
|
|
14000
|
+
if (!TODO_ID_RE.test(todoId)) continue;
|
|
14001
|
+
const list = await listAttachments(todosDir2, scopeId, todoId);
|
|
14002
|
+
if (list.length) result[todoId] = list;
|
|
14003
|
+
}
|
|
14004
|
+
return result;
|
|
14005
|
+
}
|
|
14006
|
+
async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
|
|
14007
|
+
assertAttachmentId(attachmentId);
|
|
14008
|
+
const dir = attachmentDirFor(todosDir2, scopeId, todoId);
|
|
14009
|
+
let names;
|
|
14010
|
+
try {
|
|
14011
|
+
names = await readdir10(dir);
|
|
14012
|
+
} catch {
|
|
14013
|
+
return null;
|
|
14014
|
+
}
|
|
14015
|
+
const prefix = `${attachmentId}__`;
|
|
14016
|
+
const stored = names.find((n) => n.startsWith(prefix));
|
|
14017
|
+
if (!stored) return null;
|
|
14018
|
+
const filename = stored.slice(prefix.length);
|
|
14019
|
+
return { path: resolve25(dir, stored), filename, mime: mimeForName(filename) };
|
|
14020
|
+
}
|
|
14021
|
+
async function deleteAttachment(todosDir2, scopeId, todoId, attachmentId) {
|
|
14022
|
+
const resolved = await resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId);
|
|
14023
|
+
if (!resolved) return false;
|
|
14024
|
+
await unlink5(resolved.path);
|
|
14025
|
+
return true;
|
|
14026
|
+
}
|
|
14027
|
+
async function deleteAllAttachments(todosDir2, scopeId, todoId) {
|
|
14028
|
+
await rm3(attachmentDirFor(todosDir2, scopeId, todoId), { recursive: true, force: true });
|
|
14029
|
+
}
|
|
14030
|
+
async function attachmentMoveConflict(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId, todoId) {
|
|
14031
|
+
const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
|
|
14032
|
+
const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
|
|
14033
|
+
return await dirExists(src) && await dirExists(dst);
|
|
14034
|
+
}
|
|
14035
|
+
async function moveAttachments(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId, todoId) {
|
|
14036
|
+
const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
|
|
14037
|
+
if (!await dirExists(src)) return;
|
|
14038
|
+
const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
|
|
14039
|
+
await mkdir2(dirname5(dst), { recursive: true });
|
|
14040
|
+
try {
|
|
14041
|
+
await rename5(src, dst);
|
|
14042
|
+
} catch (err) {
|
|
14043
|
+
if (err?.code === "EXDEV") {
|
|
14044
|
+
await cp(src, dst, { recursive: true });
|
|
14045
|
+
await rm3(src, { recursive: true, force: true });
|
|
14046
|
+
} else {
|
|
14047
|
+
throw err;
|
|
14048
|
+
}
|
|
14049
|
+
}
|
|
14050
|
+
}
|
|
14051
|
+
|
|
14052
|
+
// src/dashboard/todo-attachments-routes.ts
|
|
14053
|
+
var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
14054
|
+
function headerValue(req, name) {
|
|
14055
|
+
const v = req.headers[name];
|
|
14056
|
+
return Array.isArray(v) ? v[0] : v;
|
|
14057
|
+
}
|
|
14058
|
+
function paramStr(v) {
|
|
14059
|
+
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
14060
|
+
}
|
|
14061
|
+
function sendError(res, err) {
|
|
14062
|
+
if (err instanceof AttachmentValidationError) {
|
|
14063
|
+
res.status(400).json({ error: err.message });
|
|
14064
|
+
return;
|
|
14065
|
+
}
|
|
14066
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "Attachment operation failed" });
|
|
14067
|
+
}
|
|
14068
|
+
function contentDisposition(filename, inline) {
|
|
14069
|
+
const disp = inline ? "inline" : "attachment";
|
|
14070
|
+
const asciiFallback = Array.from(filename, (ch) => {
|
|
14071
|
+
const code = ch.charCodeAt(0);
|
|
14072
|
+
return code >= 32 && code <= 126 && ch !== '"' && ch !== "\\" ? ch : "_";
|
|
14073
|
+
}).join("");
|
|
14074
|
+
return `${disp}; filename="${asciiFallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
14075
|
+
}
|
|
14076
|
+
function installTodoAttachmentRoutes(router, prefix, opts) {
|
|
14077
|
+
router.post(
|
|
14078
|
+
`${prefix}/attachments`,
|
|
14079
|
+
raw({ type: () => true, limit: MAX_UPLOAD_BYTES }),
|
|
14080
|
+
async (req, res) => {
|
|
14081
|
+
try {
|
|
14082
|
+
const rawName = headerValue(req, "x-attachment-filename");
|
|
14083
|
+
let filename = "file";
|
|
14084
|
+
if (rawName) {
|
|
14085
|
+
try {
|
|
14086
|
+
filename = decodeURIComponent(rawName);
|
|
14087
|
+
} catch {
|
|
14088
|
+
res.status(400).json({ error: "Invalid x-attachment-filename header" });
|
|
14089
|
+
return;
|
|
14090
|
+
}
|
|
14091
|
+
}
|
|
14092
|
+
const body = req.body;
|
|
14093
|
+
if (!Buffer.isBuffer(body) || body.length === 0) {
|
|
14094
|
+
res.status(400).json({ error: "Empty upload body" });
|
|
14095
|
+
return;
|
|
14096
|
+
}
|
|
14097
|
+
const scope = opts.resolveScope(req);
|
|
14098
|
+
const result = await opts.withScopeLock(req, async () => {
|
|
14099
|
+
if (!await opts.todoExists(scope)) return null;
|
|
14100
|
+
return writeAttachment(scope.todosDir, scope.scopeId, scope.todoId, filename, body);
|
|
14101
|
+
});
|
|
14102
|
+
if (!result) {
|
|
14103
|
+
res.status(404).json({ error: `Todo "${scope.todoId}" not found` });
|
|
14104
|
+
return;
|
|
14105
|
+
}
|
|
14106
|
+
opts.onChange(req);
|
|
14107
|
+
res.status(201).json(result);
|
|
14108
|
+
} catch (err) {
|
|
14109
|
+
sendError(res, err);
|
|
14110
|
+
}
|
|
14111
|
+
}
|
|
14112
|
+
);
|
|
14113
|
+
router.get(`${prefix}/attachments/:attachmentId`, async (req, res) => {
|
|
14114
|
+
try {
|
|
14115
|
+
const scope = opts.resolveScope(req);
|
|
14116
|
+
const attachmentId = paramStr(req.params.attachmentId);
|
|
14117
|
+
const resolved = await opts.withScopeLock(req, async () => {
|
|
14118
|
+
if (!await opts.todoExists(scope)) return { notFound: true };
|
|
14119
|
+
return {
|
|
14120
|
+
file: await resolveAttachmentFile(scope.todosDir, scope.scopeId, scope.todoId, attachmentId)
|
|
14121
|
+
};
|
|
14122
|
+
});
|
|
14123
|
+
if ("notFound" in resolved) {
|
|
14124
|
+
res.status(404).json({ error: `Todo "${scope.todoId}" not found` });
|
|
14125
|
+
return;
|
|
14126
|
+
}
|
|
14127
|
+
if (!resolved.file) {
|
|
14128
|
+
res.status(404).json({ error: `Attachment "${attachmentId}" not found` });
|
|
14129
|
+
return;
|
|
14130
|
+
}
|
|
14131
|
+
const { path, filename, mime } = resolved.file;
|
|
14132
|
+
const inline = isSafeInlineMime(mime);
|
|
14133
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
14134
|
+
res.setHeader("Content-Type", inline ? mime : "application/octet-stream");
|
|
14135
|
+
res.setHeader("Content-Disposition", contentDisposition(filename, inline));
|
|
14136
|
+
res.sendFile(path, (err) => {
|
|
14137
|
+
if (err && !res.headersSent) res.status(500).end();
|
|
14138
|
+
});
|
|
14139
|
+
} catch (err) {
|
|
14140
|
+
sendError(res, err);
|
|
14141
|
+
}
|
|
14142
|
+
});
|
|
14143
|
+
router.delete(`${prefix}/attachments/:attachmentId`, async (req, res) => {
|
|
14144
|
+
try {
|
|
14145
|
+
const scope = opts.resolveScope(req);
|
|
14146
|
+
const attachmentId = paramStr(req.params.attachmentId);
|
|
14147
|
+
const result = await opts.withScopeLock(req, async () => {
|
|
14148
|
+
if (!await opts.todoExists(scope)) return { notFound: true };
|
|
14149
|
+
return { deleted: await deleteAttachment(scope.todosDir, scope.scopeId, scope.todoId, attachmentId) };
|
|
14150
|
+
});
|
|
14151
|
+
if ("notFound" in result) {
|
|
14152
|
+
res.status(404).json({ error: `Todo "${scope.todoId}" not found` });
|
|
14153
|
+
return;
|
|
14154
|
+
}
|
|
14155
|
+
if (!result.deleted) {
|
|
14156
|
+
res.status(404).json({ error: `Attachment "${attachmentId}" not found` });
|
|
14157
|
+
return;
|
|
14158
|
+
}
|
|
14159
|
+
opts.onChange(req);
|
|
14160
|
+
res.json({ deleted: attachmentId });
|
|
14161
|
+
} catch (err) {
|
|
14162
|
+
sendError(res, err);
|
|
14163
|
+
}
|
|
14164
|
+
});
|
|
14165
|
+
}
|
|
14166
|
+
|
|
14167
|
+
// src/dashboard/api-todos.ts
|
|
13518
14168
|
var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
|
|
13519
14169
|
function getWorkspaceParam(value) {
|
|
13520
14170
|
if (Array.isArray(value)) {
|
|
@@ -13528,7 +14178,7 @@ function touchItem3(item) {
|
|
|
13528
14178
|
item.updatedAt = now;
|
|
13529
14179
|
}
|
|
13530
14180
|
function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
13531
|
-
const router =
|
|
14181
|
+
const router = Router14();
|
|
13532
14182
|
installRecordsInvalidation(router);
|
|
13533
14183
|
function broadcastUpdate() {
|
|
13534
14184
|
broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -13545,6 +14195,19 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
13545
14195
|
next();
|
|
13546
14196
|
}
|
|
13547
14197
|
router.param("workspace", validateWorkspace);
|
|
14198
|
+
installTodoAttachmentRoutes(router, "/:workspace/:id", {
|
|
14199
|
+
resolveScope: (req) => ({
|
|
14200
|
+
todosDir: todosDir2,
|
|
14201
|
+
scopeId: getWorkspaceParam(req.params.workspace),
|
|
14202
|
+
todoId: getWorkspaceParam(req.params.id)
|
|
14203
|
+
}),
|
|
14204
|
+
withScopeLock: (req, fn) => wsLock(getWorkspaceParam(req.params.workspace), fn),
|
|
14205
|
+
todoExists: async (scope) => {
|
|
14206
|
+
const checklist = await readChecklist(scope.todosDir, scope.scopeId);
|
|
14207
|
+
return checklist.items.some((i) => i.id === scope.todoId);
|
|
14208
|
+
},
|
|
14209
|
+
onChange: () => broadcastUpdate()
|
|
14210
|
+
});
|
|
13548
14211
|
router.post("/promote-bulk", async (req, res) => {
|
|
13549
14212
|
try {
|
|
13550
14213
|
const { groups, mode, target, title, type, priority, keepSource } = req.body ?? {};
|
|
@@ -13649,17 +14312,18 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
13649
14312
|
router.get("/", async (_req, res) => {
|
|
13650
14313
|
try {
|
|
13651
14314
|
await ensureDir(todosDir2);
|
|
13652
|
-
const files = await
|
|
14315
|
+
const files = await readdir11(todosDir2).catch(() => []);
|
|
13653
14316
|
const workspaces = [];
|
|
13654
14317
|
for (const file of files) {
|
|
13655
14318
|
if (typeof file !== "string") continue;
|
|
13656
14319
|
if (!file.endsWith(".md") || file.endsWith("-log.md")) continue;
|
|
13657
14320
|
const workspace = file.replace(".md", "");
|
|
13658
14321
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
14322
|
+
const attachmentsByTodo = await readScopeAttachments(todosDir2, checklist.workspace);
|
|
13659
14323
|
workspaces.push({
|
|
13660
14324
|
workspace: checklist.workspace,
|
|
13661
14325
|
archiveInterval: checklist.archiveInterval,
|
|
13662
|
-
items: checklist.items,
|
|
14326
|
+
items: checklist.items.map((i) => ({ ...i, attachments: attachmentsByTodo[i.id] ?? [] })),
|
|
13663
14327
|
counts: computeCounts(checklist.items)
|
|
13664
14328
|
});
|
|
13665
14329
|
}
|
|
@@ -13672,10 +14336,11 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
13672
14336
|
try {
|
|
13673
14337
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
13674
14338
|
const checklist = await readChecklist(todosDir2, workspace);
|
|
14339
|
+
const attachmentsByTodo = await readScopeAttachments(todosDir2, workspace);
|
|
13675
14340
|
res.json({
|
|
13676
14341
|
workspace: checklist.workspace,
|
|
13677
14342
|
archiveInterval: checklist.archiveInterval,
|
|
13678
|
-
items: checklist.items,
|
|
14343
|
+
items: checklist.items.map((i) => ({ ...i, attachments: attachmentsByTodo[i.id] ?? [] })),
|
|
13679
14344
|
counts: computeCounts(checklist.items)
|
|
13680
14345
|
});
|
|
13681
14346
|
} catch (error) {
|
|
@@ -13763,63 +14428,68 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
|
|
|
13763
14428
|
router.post("/:workspace/archive", async (req, res) => {
|
|
13764
14429
|
try {
|
|
13765
14430
|
const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
|
|
13766
|
-
const { resolve:
|
|
14431
|
+
const { resolve: resolve30 } = await import("path");
|
|
13767
14432
|
const { readFile: readFile21 } = await import("fs/promises");
|
|
13768
14433
|
const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
13769
14434
|
const workspace = getWorkspaceParam(req.params.workspace);
|
|
13770
|
-
const
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
|
|
13778
|
-
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
|
|
14435
|
+
const outcome = await wsLock(workspace, async () => {
|
|
14436
|
+
const checklist = await readChecklist(todosDir2, workspace);
|
|
14437
|
+
const log = await readLog(todosDir2, workspace);
|
|
14438
|
+
const completedIds = new Set(
|
|
14439
|
+
checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
|
|
14440
|
+
);
|
|
14441
|
+
if (completedIds.size === 0) {
|
|
14442
|
+
return { archived: 0, message: "No completed items to archive" };
|
|
14443
|
+
}
|
|
14444
|
+
const toArchive = log.entries.filter(
|
|
14445
|
+
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
14446
|
+
);
|
|
14447
|
+
const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
|
|
14448
|
+
await ensureDir(resolve30(todosDir2, "archive"));
|
|
14449
|
+
let archContent = "";
|
|
14450
|
+
if (await fileExists(archFile)) {
|
|
14451
|
+
archContent = await readFile21(archFile, "utf-8");
|
|
14452
|
+
archContent = archContent.trimEnd() + "\n\n";
|
|
14453
|
+
} else {
|
|
14454
|
+
archContent = `---
|
|
13790
14455
|
workspace: ${workspace}
|
|
13791
14456
|
---
|
|
13792
14457
|
|
|
13793
14458
|
# Archive
|
|
13794
14459
|
|
|
13795
14460
|
`;
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
14461
|
+
}
|
|
14462
|
+
const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
|
|
14463
|
+
for (const item of completedItems) {
|
|
14464
|
+
archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
|
|
13800
14465
|
`;
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
14466
|
+
}
|
|
14467
|
+
archContent += "\n";
|
|
14468
|
+
for (const entry of toArchive) {
|
|
14469
|
+
archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
|
|
13805
14470
|
`;
|
|
13806
|
-
|
|
14471
|
+
if (entry.items) archContent += `**Items:** ${entry.items}
|
|
13807
14472
|
`;
|
|
13808
|
-
|
|
14473
|
+
if (entry.session) archContent += `**Session:** ${entry.session}
|
|
13809
14474
|
`;
|
|
13810
|
-
|
|
14475
|
+
if (entry.branch) archContent += `**Branch:** ${entry.branch}
|
|
13811
14476
|
`;
|
|
13812
|
-
|
|
14477
|
+
if (entry.summary) archContent += `**Summary:** ${entry.summary}
|
|
13813
14478
|
`;
|
|
13814
|
-
|
|
14479
|
+
if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
|
|
13815
14480
|
`;
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
13822
|
-
|
|
14481
|
+
archContent += "\n";
|
|
14482
|
+
}
|
|
14483
|
+
await writeFileForce2(archFile, archContent);
|
|
14484
|
+
checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
|
|
14485
|
+
await writeChecklist(todosDir2, checklist);
|
|
14486
|
+
for (const id of completedIds) {
|
|
14487
|
+
await deleteAllAttachments(todosDir2, workspace, id);
|
|
14488
|
+
}
|
|
14489
|
+
return { archived: completedIds.size, logEntries: toArchive.length };
|
|
14490
|
+
});
|
|
14491
|
+
if (outcome.archived > 0) broadcastUpdate();
|
|
14492
|
+
res.json(outcome);
|
|
13823
14493
|
} catch (error) {
|
|
13824
14494
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to archive" });
|
|
13825
14495
|
}
|
|
@@ -13844,7 +14514,8 @@ workspace: ${workspace}
|
|
|
13844
14514
|
}
|
|
13845
14515
|
const log = await readLog(todosDir2, workspace);
|
|
13846
14516
|
const logEntries = log.entries.filter((e) => e.itemIds.includes(req.params.id));
|
|
13847
|
-
|
|
14517
|
+
const attachments = await listAttachments(todosDir2, workspace, item.id);
|
|
14518
|
+
res.json({ ...item, attachments, log: logEntries });
|
|
13848
14519
|
} catch (error) {
|
|
13849
14520
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todo" });
|
|
13850
14521
|
}
|
|
@@ -13881,6 +14552,7 @@ workspace: ${workspace}
|
|
|
13881
14552
|
if (idx === -1) return false;
|
|
13882
14553
|
checklist.items.splice(idx, 1);
|
|
13883
14554
|
await writeChecklist(todosDir2, checklist);
|
|
14555
|
+
await deleteAllAttachments(todosDir2, workspace, req.params.id);
|
|
13884
14556
|
return true;
|
|
13885
14557
|
});
|
|
13886
14558
|
if (!deleted) {
|
|
@@ -14192,15 +14864,22 @@ workspace: ${workspace}
|
|
|
14192
14864
|
return { status: 409, error: "id already exists in target" };
|
|
14193
14865
|
}
|
|
14194
14866
|
const item = sourceChecklist.items[idx];
|
|
14867
|
+
let newPlanDir = null;
|
|
14195
14868
|
if (item.planDir) {
|
|
14196
|
-
|
|
14869
|
+
newPlanDir = todoPlanDir(target.todosPath, target.id, id);
|
|
14197
14870
|
if (await fileExists(newPlanDir)) {
|
|
14198
14871
|
return { status: 409, error: "plan dir already exists in target" };
|
|
14199
14872
|
}
|
|
14200
|
-
|
|
14201
|
-
|
|
14873
|
+
}
|
|
14874
|
+
if (await attachmentMoveConflict(todosDir2, sourceWs, target.todosPath, target.id, id)) {
|
|
14875
|
+
return { status: 409, error: "attachments already exist in target" };
|
|
14876
|
+
}
|
|
14877
|
+
if (item.planDir && newPlanDir) {
|
|
14878
|
+
await mkdir3(dirname6(newPlanDir), { recursive: true });
|
|
14879
|
+
await rename6(item.planDir, newPlanDir);
|
|
14202
14880
|
item.planDir = newPlanDir;
|
|
14203
14881
|
}
|
|
14882
|
+
await moveAttachments(todosDir2, sourceWs, target.todosPath, target.id, id);
|
|
14204
14883
|
sourceChecklist.items.splice(idx, 1);
|
|
14205
14884
|
targetChecklist.items.push(item);
|
|
14206
14885
|
await writeChecklist(todosDir2, sourceChecklist);
|
|
@@ -14273,9 +14952,9 @@ init_parser2();
|
|
|
14273
14952
|
init_fs();
|
|
14274
14953
|
init_paths();
|
|
14275
14954
|
init_slug();
|
|
14276
|
-
import { Router as
|
|
14277
|
-
import { mkdir as
|
|
14278
|
-
import { resolve as
|
|
14955
|
+
import { Router as Router15 } from "express";
|
|
14956
|
+
import { mkdir as mkdir4, readFile as readFile18, rename as rename7 } from "fs/promises";
|
|
14957
|
+
import { resolve as resolve26, dirname as dirname7 } from "path";
|
|
14279
14958
|
init_api();
|
|
14280
14959
|
var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
|
|
14281
14960
|
function touchItem4(item) {
|
|
@@ -14291,12 +14970,12 @@ function params(req) {
|
|
|
14291
14970
|
return req.params;
|
|
14292
14971
|
}
|
|
14293
14972
|
async function projectExists(projectsDir, slug) {
|
|
14294
|
-
return fileExists(
|
|
14973
|
+
return fileExists(resolve26(projectsDir, slug, "project.md"));
|
|
14295
14974
|
}
|
|
14296
14975
|
async function ensureProjectTodosDir(projectsDir, slug) {
|
|
14297
14976
|
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
14298
14977
|
try {
|
|
14299
|
-
await
|
|
14978
|
+
await mkdir4(todosDir2, { recursive: false });
|
|
14300
14979
|
} catch (err) {
|
|
14301
14980
|
const code = err.code;
|
|
14302
14981
|
if (code === "EEXIST") return;
|
|
@@ -14308,7 +14987,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
|
|
|
14308
14987
|
throw err;
|
|
14309
14988
|
}
|
|
14310
14989
|
try {
|
|
14311
|
-
await
|
|
14990
|
+
await mkdir4(resolve26(todosDir2, "archive"), { recursive: false });
|
|
14312
14991
|
} catch (err) {
|
|
14313
14992
|
const code = err.code;
|
|
14314
14993
|
if (code === "EEXIST") return;
|
|
@@ -14324,7 +15003,7 @@ function notFound(res, slug) {
|
|
|
14324
15003
|
res.status(404).json({ error: `Project "${slug}" not found` });
|
|
14325
15004
|
}
|
|
14326
15005
|
function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
|
|
14327
|
-
const router =
|
|
15006
|
+
const router = Router15({ mergeParams: true });
|
|
14328
15007
|
installRecordsInvalidation(router);
|
|
14329
15008
|
function broadcastUpdate(projectSlug) {
|
|
14330
15009
|
broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -14341,6 +15020,18 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
|
|
|
14341
15020
|
next();
|
|
14342
15021
|
}
|
|
14343
15022
|
router.use(validateProjectId);
|
|
15023
|
+
installTodoAttachmentRoutes(router, "/:id", {
|
|
15024
|
+
resolveScope: (req) => {
|
|
15025
|
+
const slug = getProjectIdParam(params(req).projectId);
|
|
15026
|
+
return { todosDir: projectTodosDir(projectsDir, slug), scopeId: slug, todoId: params(req).id ?? "" };
|
|
15027
|
+
},
|
|
15028
|
+
withScopeLock: (req, fn) => projLock(getProjectIdParam(params(req).projectId), fn),
|
|
15029
|
+
todoExists: async (scope) => {
|
|
15030
|
+
const checklist = await readChecklist(scope.todosDir, scope.scopeId);
|
|
15031
|
+
return checklist.items.some((i) => i.id === scope.todoId);
|
|
15032
|
+
},
|
|
15033
|
+
onChange: (req) => broadcastUpdate(getProjectIdParam(params(req).projectId))
|
|
15034
|
+
});
|
|
14344
15035
|
router.get("/", async (req, res) => {
|
|
14345
15036
|
try {
|
|
14346
15037
|
const slug = getProjectIdParam(params(req).projectId);
|
|
@@ -14350,10 +15041,11 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
|
|
|
14350
15041
|
}
|
|
14351
15042
|
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
14352
15043
|
const checklist = await readChecklist(todosDir2, slug);
|
|
15044
|
+
const attachmentsByTodo = await readScopeAttachments(todosDir2, slug);
|
|
14353
15045
|
res.json({
|
|
14354
15046
|
workspace: checklist.workspace,
|
|
14355
15047
|
archiveInterval: checklist.archiveInterval,
|
|
14356
|
-
items: checklist.items,
|
|
15048
|
+
items: checklist.items.map((i) => ({ ...i, attachments: attachmentsByTodo[i.id] ?? [] })),
|
|
14357
15049
|
counts: computeCounts(checklist.items)
|
|
14358
15050
|
});
|
|
14359
15051
|
} catch (error) {
|
|
@@ -14481,61 +15173,71 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
|
|
|
14481
15173
|
notFound(res, slug);
|
|
14482
15174
|
return;
|
|
14483
15175
|
}
|
|
14484
|
-
const
|
|
14485
|
-
|
|
14486
|
-
|
|
14487
|
-
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
15176
|
+
const outcome = await projLock(slug, async () => {
|
|
15177
|
+
if (!await projectExists(projectsDir, slug)) return "gone";
|
|
15178
|
+
await ensureProjectTodosDir(projectsDir, slug);
|
|
15179
|
+
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
15180
|
+
const checklist = await readChecklist(todosDir2, slug);
|
|
15181
|
+
const log = await readLog(todosDir2, slug);
|
|
15182
|
+
const completedIds = new Set(
|
|
15183
|
+
checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
|
|
15184
|
+
);
|
|
15185
|
+
if (completedIds.size === 0) {
|
|
15186
|
+
return { archived: 0, message: "No completed items to archive" };
|
|
15187
|
+
}
|
|
15188
|
+
const toArchive = log.entries.filter(
|
|
15189
|
+
(e) => e.itemIds.every((id) => completedIds.has(id))
|
|
15190
|
+
);
|
|
15191
|
+
const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
|
|
15192
|
+
let archContent = "";
|
|
15193
|
+
if (await fileExists(archFile)) {
|
|
15194
|
+
archContent = await readFile18(archFile, "utf-8");
|
|
15195
|
+
archContent = archContent.trimEnd() + "\n\n";
|
|
15196
|
+
} else {
|
|
15197
|
+
archContent = `---
|
|
14505
15198
|
workspace: ${slug}
|
|
14506
15199
|
---
|
|
14507
15200
|
|
|
14508
15201
|
# Archive
|
|
14509
15202
|
|
|
14510
15203
|
`;
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
15204
|
+
}
|
|
15205
|
+
const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
|
|
15206
|
+
for (const item of completedItems) {
|
|
15207
|
+
archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
|
|
14515
15208
|
`;
|
|
14516
|
-
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
15209
|
+
}
|
|
15210
|
+
archContent += "\n";
|
|
15211
|
+
for (const entry of toArchive) {
|
|
15212
|
+
archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
|
|
14520
15213
|
`;
|
|
14521
|
-
|
|
15214
|
+
if (entry.items) archContent += `**Items:** ${entry.items}
|
|
14522
15215
|
`;
|
|
14523
|
-
|
|
15216
|
+
if (entry.session) archContent += `**Session:** ${entry.session}
|
|
14524
15217
|
`;
|
|
14525
|
-
|
|
15218
|
+
if (entry.branch) archContent += `**Branch:** ${entry.branch}
|
|
14526
15219
|
`;
|
|
14527
|
-
|
|
15220
|
+
if (entry.summary) archContent += `**Summary:** ${entry.summary}
|
|
14528
15221
|
`;
|
|
14529
|
-
|
|
15222
|
+
if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
|
|
14530
15223
|
`;
|
|
14531
|
-
|
|
15224
|
+
archContent += "\n";
|
|
15225
|
+
}
|
|
15226
|
+
await writeFileForce(archFile, archContent);
|
|
15227
|
+
checklist.workspace = slug;
|
|
15228
|
+
checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
|
|
15229
|
+
await writeChecklist(todosDir2, checklist);
|
|
15230
|
+
for (const id of completedIds) {
|
|
15231
|
+
await deleteAllAttachments(todosDir2, slug, id);
|
|
15232
|
+
}
|
|
15233
|
+
return { archived: completedIds.size, logEntries: toArchive.length };
|
|
15234
|
+
});
|
|
15235
|
+
if (outcome === "gone") {
|
|
15236
|
+
notFound(res, slug);
|
|
15237
|
+
return;
|
|
14532
15238
|
}
|
|
14533
|
-
|
|
14534
|
-
|
|
14535
|
-
checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
|
|
14536
|
-
await writeChecklist(todosDir2, checklist);
|
|
14537
|
-
broadcastUpdate(slug);
|
|
14538
|
-
res.json({ archived: completedIds.size, logEntries: toArchive.length });
|
|
15239
|
+
if (outcome.archived > 0) broadcastUpdate(slug);
|
|
15240
|
+
res.json(outcome);
|
|
14539
15241
|
} catch (error) {
|
|
14540
15242
|
if (error.code === "PROJECT_GONE") {
|
|
14541
15243
|
notFound(res, getProjectIdParam(params(req).projectId));
|
|
@@ -14575,7 +15277,8 @@ workspace: ${slug}
|
|
|
14575
15277
|
}
|
|
14576
15278
|
const log = await readLog(todosDir2, slug);
|
|
14577
15279
|
const logEntries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
|
|
14578
|
-
|
|
15280
|
+
const attachments = await listAttachments(todosDir2, slug, item.id);
|
|
15281
|
+
res.json({ ...item, attachments, log: logEntries });
|
|
14579
15282
|
} catch (error) {
|
|
14580
15283
|
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todo" });
|
|
14581
15284
|
}
|
|
@@ -14631,11 +15334,13 @@ workspace: ${slug}
|
|
|
14631
15334
|
await ensureProjectTodosDir(projectsDir, slug);
|
|
14632
15335
|
const todosDir2 = projectTodosDir(projectsDir, slug);
|
|
14633
15336
|
const checklist = await readChecklist(todosDir2, slug);
|
|
14634
|
-
const
|
|
15337
|
+
const targetId = params(req).id ?? "";
|
|
15338
|
+
const idx = checklist.items.findIndex((i) => i.id === targetId);
|
|
14635
15339
|
if (idx === -1) return false;
|
|
14636
15340
|
checklist.items.splice(idx, 1);
|
|
14637
15341
|
checklist.workspace = slug;
|
|
14638
15342
|
await writeChecklist(todosDir2, checklist);
|
|
15343
|
+
await deleteAllAttachments(todosDir2, slug, targetId);
|
|
14639
15344
|
return true;
|
|
14640
15345
|
});
|
|
14641
15346
|
if (deleted === "gone") {
|
|
@@ -14945,15 +15650,15 @@ workspace: ${slug}
|
|
|
14945
15650
|
if (tg.includes("/")) {
|
|
14946
15651
|
const parts = tg.split("/");
|
|
14947
15652
|
if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
|
|
14948
|
-
assignmentDir =
|
|
15653
|
+
assignmentDir = resolve26(projectsDir, parts[0], "assignments", parts[1]);
|
|
14949
15654
|
assignmentRef = `${parts[0]}/${parts[1]}`;
|
|
14950
15655
|
} else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
|
|
14951
|
-
assignmentDir =
|
|
15656
|
+
assignmentDir = resolve26(assignmentsDirFn(), tg);
|
|
14952
15657
|
assignmentRef = tg;
|
|
14953
15658
|
} else {
|
|
14954
15659
|
return { error: `Invalid target.assignment "${tg}"` };
|
|
14955
15660
|
}
|
|
14956
|
-
const assignmentMdPath =
|
|
15661
|
+
const assignmentMdPath = resolve26(assignmentDir, "assignment.md");
|
|
14957
15662
|
if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
|
|
14958
15663
|
let content = await readFile18(assignmentMdPath, "utf-8");
|
|
14959
15664
|
content = appendTodosToAssignmentBody2(
|
|
@@ -15083,15 +15788,22 @@ workspace: ${slug}
|
|
|
15083
15788
|
return { status: 409, error: "id already exists in target" };
|
|
15084
15789
|
}
|
|
15085
15790
|
const item = sourceChecklist.items[idx];
|
|
15791
|
+
let newPlanDir = null;
|
|
15086
15792
|
if (item.planDir) {
|
|
15087
|
-
|
|
15793
|
+
newPlanDir = todoPlanDir(target.todosPath, target.id, id);
|
|
15088
15794
|
if (await fileExists(newPlanDir)) {
|
|
15089
15795
|
return { status: 409, error: "plan dir already exists in target" };
|
|
15090
15796
|
}
|
|
15091
|
-
|
|
15092
|
-
|
|
15797
|
+
}
|
|
15798
|
+
if (await attachmentMoveConflict(sourceTodosDir, sourceSlug, target.todosPath, target.id, id)) {
|
|
15799
|
+
return { status: 409, error: "attachments already exist in target" };
|
|
15800
|
+
}
|
|
15801
|
+
if (item.planDir && newPlanDir) {
|
|
15802
|
+
await mkdir4(dirname7(newPlanDir), { recursive: true });
|
|
15803
|
+
await rename7(item.planDir, newPlanDir);
|
|
15093
15804
|
item.planDir = newPlanDir;
|
|
15094
15805
|
}
|
|
15806
|
+
await moveAttachments(sourceTodosDir, sourceSlug, target.todosPath, target.id, id);
|
|
15095
15807
|
sourceChecklist.items.splice(idx, 1);
|
|
15096
15808
|
targetChecklist.items.push(item);
|
|
15097
15809
|
sourceChecklist.workspace = sourceSlug;
|
|
@@ -15150,33 +15862,33 @@ workspace: ${slug}
|
|
|
15150
15862
|
}
|
|
15151
15863
|
|
|
15152
15864
|
// src/dashboard/api-bundles.ts
|
|
15153
|
-
import { Router as
|
|
15154
|
-
import { readdir as
|
|
15865
|
+
import { Router as Router16 } from "express";
|
|
15866
|
+
import { readdir as readdir12 } from "fs/promises";
|
|
15155
15867
|
|
|
15156
15868
|
// src/todos/bundle-parser.ts
|
|
15157
15869
|
init_parser();
|
|
15158
15870
|
init_fs();
|
|
15159
15871
|
init_paths();
|
|
15160
15872
|
init_parser2();
|
|
15161
|
-
import { randomBytes as
|
|
15873
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
15162
15874
|
import { readFile as readFile19 } from "fs/promises";
|
|
15163
15875
|
var BUNDLE_ID_REGEX = /^[a-f0-9]{4}$/;
|
|
15164
15876
|
var SCOPE_VALUES = /* @__PURE__ */ new Set(["workspace", "project", "global"]);
|
|
15165
15877
|
var SCOPE_ID_REGEX = /^[a-z0-9_][a-z0-9_-]*$/;
|
|
15166
15878
|
var BUNDLE_LINE_REGEX = /^- b:([a-f0-9]{4})\s+<([^>]*)>\s*$/;
|
|
15167
|
-
function parseScopeToken(
|
|
15168
|
-
const idx =
|
|
15879
|
+
function parseScopeToken(raw2) {
|
|
15880
|
+
const idx = raw2.indexOf(":");
|
|
15169
15881
|
if (idx < 0) return null;
|
|
15170
|
-
const scopeRaw =
|
|
15171
|
-
const scopeId =
|
|
15882
|
+
const scopeRaw = raw2.slice(0, idx);
|
|
15883
|
+
const scopeId = raw2.slice(idx + 1);
|
|
15172
15884
|
if (!SCOPE_VALUES.has(scopeRaw)) return null;
|
|
15173
15885
|
if (!scopeId) return null;
|
|
15174
15886
|
if (!SCOPE_ID_REGEX.test(scopeId)) return null;
|
|
15175
15887
|
return { scope: scopeRaw, scopeId };
|
|
15176
15888
|
}
|
|
15177
|
-
function parseTodosToken(
|
|
15178
|
-
if (!
|
|
15179
|
-
return
|
|
15889
|
+
function parseTodosToken(raw2) {
|
|
15890
|
+
if (!raw2) return [];
|
|
15891
|
+
return raw2.split(",").map((s) => s.trim()).filter((s) => BUNDLE_ID_REGEX.test(s));
|
|
15180
15892
|
}
|
|
15181
15893
|
function parseBundleLine(line) {
|
|
15182
15894
|
const match = line.match(BUNDLE_LINE_REGEX);
|
|
@@ -15261,7 +15973,7 @@ function annotate(bundle, items) {
|
|
|
15261
15973
|
}
|
|
15262
15974
|
function createBundlesRouter(todosDir2, broadcast) {
|
|
15263
15975
|
void broadcast;
|
|
15264
|
-
const router =
|
|
15976
|
+
const router = Router16();
|
|
15265
15977
|
function validateWorkspace(req, res, next) {
|
|
15266
15978
|
const workspace = getWorkspaceParam2(req.params.workspace);
|
|
15267
15979
|
if (workspace && !WORKSPACE_REGEX3.test(workspace)) {
|
|
@@ -15275,7 +15987,7 @@ function createBundlesRouter(todosDir2, broadcast) {
|
|
|
15275
15987
|
try {
|
|
15276
15988
|
await ensureDir(todosDir2);
|
|
15277
15989
|
const bundles = await readBundles(todosDir2);
|
|
15278
|
-
const workspaceFiles = await
|
|
15990
|
+
const workspaceFiles = await readdir12(todosDir2).catch(() => []);
|
|
15279
15991
|
const itemsByKey = /* @__PURE__ */ new Map();
|
|
15280
15992
|
for (const f of workspaceFiles) {
|
|
15281
15993
|
if (typeof f !== "string") continue;
|
|
@@ -15327,8 +16039,8 @@ function createBundlesRouter(todosDir2, broadcast) {
|
|
|
15327
16039
|
init_fs();
|
|
15328
16040
|
init_paths();
|
|
15329
16041
|
init_slug();
|
|
15330
|
-
import { Router as
|
|
15331
|
-
import { resolve as
|
|
16042
|
+
import { Router as Router17 } from "express";
|
|
16043
|
+
import { resolve as resolve27 } from "path";
|
|
15332
16044
|
init_parser2();
|
|
15333
16045
|
function deriveStatus2(bundle, items) {
|
|
15334
16046
|
const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
|
|
@@ -15357,7 +16069,7 @@ function notFound2(res, slug) {
|
|
|
15357
16069
|
}
|
|
15358
16070
|
function createProjectBundlesRouter(projectsDir, broadcast) {
|
|
15359
16071
|
void broadcast;
|
|
15360
|
-
const router =
|
|
16072
|
+
const router = Router17({ mergeParams: true });
|
|
15361
16073
|
function validateProjectId(req, res, next) {
|
|
15362
16074
|
const slug = getProjectIdParam2(req.params.projectId);
|
|
15363
16075
|
if (!slug || !isValidSlug(slug)) {
|
|
@@ -15370,7 +16082,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
|
|
|
15370
16082
|
router.get("/", async (req, res) => {
|
|
15371
16083
|
try {
|
|
15372
16084
|
const slug = getProjectIdParam2(req.params.projectId);
|
|
15373
|
-
const projectMd =
|
|
16085
|
+
const projectMd = resolve27(projectsDir, slug, "project.md");
|
|
15374
16086
|
if (!await fileExists(projectMd)) {
|
|
15375
16087
|
notFound2(res, slug);
|
|
15376
16088
|
return;
|
|
@@ -15391,7 +16103,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
|
|
|
15391
16103
|
init_config2();
|
|
15392
16104
|
init_api();
|
|
15393
16105
|
init_scanner();
|
|
15394
|
-
import { Router as
|
|
16106
|
+
import { Router as Router18 } from "express";
|
|
15395
16107
|
|
|
15396
16108
|
// src/utils/github-backup.ts
|
|
15397
16109
|
init_paths();
|
|
@@ -15399,8 +16111,8 @@ init_fs();
|
|
|
15399
16111
|
init_config2();
|
|
15400
16112
|
import { execFile as execFile2 } from "child_process";
|
|
15401
16113
|
import { promisify as promisify2 } from "util";
|
|
15402
|
-
import { cp, mkdtemp, rm as
|
|
15403
|
-
import { resolve as
|
|
16114
|
+
import { cp as cp2, mkdtemp, rm as rm4, readFile as readFile20, writeFile as writeFile6, unlink as unlink6, stat as stat2, open as open2, rename as rename8 } from "fs/promises";
|
|
16115
|
+
import { resolve as resolve28, join as join3 } from "path";
|
|
15404
16116
|
import { tmpdir } from "os";
|
|
15405
16117
|
var exec2 = promisify2(execFile2);
|
|
15406
16118
|
var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
|
|
@@ -15440,7 +16152,7 @@ async function resolveCategoryPath(category) {
|
|
|
15440
16152
|
case "servers":
|
|
15441
16153
|
return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
|
|
15442
16154
|
case "config":
|
|
15443
|
-
return { sourcePath:
|
|
16155
|
+
return { sourcePath: resolve28(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
|
|
15444
16156
|
}
|
|
15445
16157
|
}
|
|
15446
16158
|
async function checkGitInstalled() {
|
|
@@ -15451,7 +16163,7 @@ async function checkGitInstalled() {
|
|
|
15451
16163
|
}
|
|
15452
16164
|
}
|
|
15453
16165
|
async function acquireLock() {
|
|
15454
|
-
const lockPath =
|
|
16166
|
+
const lockPath = resolve28(syntaurRoot(), LOCK_FILE_NAME);
|
|
15455
16167
|
await ensureDir(syntaurRoot());
|
|
15456
16168
|
try {
|
|
15457
16169
|
const handle = await open2(lockPath, "wx");
|
|
@@ -15470,7 +16182,7 @@ async function acquireLock() {
|
|
|
15470
16182
|
}
|
|
15471
16183
|
async function releaseLock(lockPath) {
|
|
15472
16184
|
try {
|
|
15473
|
-
await
|
|
16185
|
+
await unlink6(lockPath);
|
|
15474
16186
|
} catch {
|
|
15475
16187
|
}
|
|
15476
16188
|
}
|
|
@@ -15493,13 +16205,13 @@ async function cloneOrInit(repoUrl, destDir) {
|
|
|
15493
16205
|
}
|
|
15494
16206
|
async function copyRecursive(src, dest) {
|
|
15495
16207
|
if (!await fileExists(src)) return;
|
|
15496
|
-
const s = await
|
|
16208
|
+
const s = await stat2(src);
|
|
15497
16209
|
if (s.isDirectory()) {
|
|
15498
16210
|
await ensureDir(dest);
|
|
15499
|
-
await
|
|
16211
|
+
await cp2(src, dest, { recursive: true, force: true });
|
|
15500
16212
|
} else {
|
|
15501
|
-
await ensureDir(
|
|
15502
|
-
await
|
|
16213
|
+
await ensureDir(resolve28(dest, ".."));
|
|
16214
|
+
await cp2(src, dest, { force: true });
|
|
15503
16215
|
}
|
|
15504
16216
|
}
|
|
15505
16217
|
function resolveCategoriesStrict(csv) {
|
|
@@ -15536,9 +16248,9 @@ async function backupToGithub(overrides) {
|
|
|
15536
16248
|
const { sourcePath, repoPath, isFile } = await resolveCategoryPath(category);
|
|
15537
16249
|
const destPath = join3(tmpDir, repoPath);
|
|
15538
16250
|
if (isFile) {
|
|
15539
|
-
await
|
|
16251
|
+
await rm4(destPath, { force: true });
|
|
15540
16252
|
} else {
|
|
15541
|
-
await
|
|
16253
|
+
await rm4(destPath, { recursive: true, force: true });
|
|
15542
16254
|
}
|
|
15543
16255
|
if (!await fileExists(sourcePath)) {
|
|
15544
16256
|
console.warn(`Category "${category}": no local data at ${sourcePath}; backup will reflect deletion.`);
|
|
@@ -15546,8 +16258,8 @@ async function backupToGithub(overrides) {
|
|
|
15546
16258
|
}
|
|
15547
16259
|
if (category === "config") {
|
|
15548
16260
|
const sanitized = await readSanitizedConfig(sourcePath);
|
|
15549
|
-
await ensureDir(
|
|
15550
|
-
await
|
|
16261
|
+
await ensureDir(resolve28(destPath, ".."));
|
|
16262
|
+
await writeFile6(destPath, sanitized, "utf-8");
|
|
15551
16263
|
} else {
|
|
15552
16264
|
await copyRecursive(sourcePath, destPath);
|
|
15553
16265
|
}
|
|
@@ -15592,7 +16304,7 @@ async function backupToGithub(overrides) {
|
|
|
15592
16304
|
};
|
|
15593
16305
|
} finally {
|
|
15594
16306
|
if (tmpDir) {
|
|
15595
|
-
await
|
|
16307
|
+
await rm4(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
15596
16308
|
});
|
|
15597
16309
|
}
|
|
15598
16310
|
await releaseLock(lockPath);
|
|
@@ -15600,18 +16312,18 @@ async function backupToGithub(overrides) {
|
|
|
15600
16312
|
}
|
|
15601
16313
|
async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
15602
16314
|
if (isFile) {
|
|
15603
|
-
await ensureDir(
|
|
15604
|
-
await
|
|
16315
|
+
await ensureDir(resolve28(localPath, ".."));
|
|
16316
|
+
await cp2(repoSrcPath, localPath, { force: true });
|
|
15605
16317
|
return;
|
|
15606
16318
|
}
|
|
15607
16319
|
const stagingPath = `${localPath}.syntaur-restore-staging`;
|
|
15608
16320
|
const backupPath = `${localPath}.syntaur-restore-backup`;
|
|
15609
|
-
await
|
|
16321
|
+
await rm4(stagingPath, { recursive: true, force: true });
|
|
15610
16322
|
const backupExistsBefore = await fileExists(backupPath);
|
|
15611
16323
|
const localExistsBefore = await fileExists(localPath);
|
|
15612
16324
|
if (backupExistsBefore) {
|
|
15613
16325
|
if (!localExistsBefore) {
|
|
15614
|
-
await
|
|
16326
|
+
await rename8(backupPath, localPath);
|
|
15615
16327
|
} else {
|
|
15616
16328
|
throw new Error(
|
|
15617
16329
|
`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.`
|
|
@@ -15620,21 +16332,21 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
|
|
|
15620
16332
|
}
|
|
15621
16333
|
let localMovedAside = false;
|
|
15622
16334
|
try {
|
|
15623
|
-
await
|
|
16335
|
+
await cp2(repoSrcPath, stagingPath, { recursive: true, force: true });
|
|
15624
16336
|
const localExists = await fileExists(localPath);
|
|
15625
16337
|
if (localExists) {
|
|
15626
|
-
await
|
|
16338
|
+
await rename8(localPath, backupPath);
|
|
15627
16339
|
localMovedAside = true;
|
|
15628
16340
|
}
|
|
15629
|
-
await
|
|
15630
|
-
await
|
|
16341
|
+
await rename8(stagingPath, localPath);
|
|
16342
|
+
await rm4(backupPath, { recursive: true, force: true }).catch(() => {
|
|
15631
16343
|
});
|
|
15632
16344
|
} catch (err) {
|
|
15633
16345
|
if (localMovedAside && await fileExists(backupPath)) {
|
|
15634
|
-
await
|
|
16346
|
+
await rename8(backupPath, localPath).catch(() => {
|
|
15635
16347
|
});
|
|
15636
16348
|
}
|
|
15637
|
-
await
|
|
16349
|
+
await rm4(stagingPath, { recursive: true, force: true }).catch(() => {
|
|
15638
16350
|
});
|
|
15639
16351
|
throw err;
|
|
15640
16352
|
}
|
|
@@ -15693,7 +16405,7 @@ async function restoreFromGithub(overrides) {
|
|
|
15693
16405
|
};
|
|
15694
16406
|
} finally {
|
|
15695
16407
|
if (tmpDir) {
|
|
15696
|
-
await
|
|
16408
|
+
await rm4(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
15697
16409
|
});
|
|
15698
16410
|
}
|
|
15699
16411
|
await releaseLock(lockPath);
|
|
@@ -15701,7 +16413,7 @@ async function restoreFromGithub(overrides) {
|
|
|
15701
16413
|
}
|
|
15702
16414
|
async function getBackupStatus() {
|
|
15703
16415
|
const config = await readConfig();
|
|
15704
|
-
const lockPath =
|
|
16416
|
+
const lockPath = resolve28(syntaurRoot(), LOCK_FILE_NAME);
|
|
15705
16417
|
const locked = await fileExists(lockPath);
|
|
15706
16418
|
return {
|
|
15707
16419
|
repo: config.backup?.repo ?? null,
|
|
@@ -15714,7 +16426,7 @@ async function getBackupStatus() {
|
|
|
15714
16426
|
|
|
15715
16427
|
// src/dashboard/api-backup.ts
|
|
15716
16428
|
function createBackupRouter() {
|
|
15717
|
-
const router =
|
|
16429
|
+
const router = Router18();
|
|
15718
16430
|
router.get("/", async (_req, res) => {
|
|
15719
16431
|
try {
|
|
15720
16432
|
const status = await getBackupStatus();
|
|
@@ -16049,7 +16761,7 @@ function createDashboardServer(options) {
|
|
|
16049
16761
|
(async () => {
|
|
16050
16762
|
try {
|
|
16051
16763
|
const configResult = await migrateLegacyConfig(
|
|
16052
|
-
|
|
16764
|
+
resolve29(syntaurRoot(), "config.md")
|
|
16053
16765
|
);
|
|
16054
16766
|
const projectResult = await migrateLegacyProjectFiles(projectsDir);
|
|
16055
16767
|
const summary = summarizeMigration(projectResult, configResult);
|
|
@@ -16150,8 +16862,8 @@ function createDashboardServer(options) {
|
|
|
16150
16862
|
});
|
|
16151
16863
|
app.put("/api/config/hotkeys", async (req, res) => {
|
|
16152
16864
|
try {
|
|
16153
|
-
const
|
|
16154
|
-
const incoming =
|
|
16865
|
+
const raw2 = req.body && typeof req.body === "object" ? req.body : {};
|
|
16866
|
+
const incoming = raw2.bindings;
|
|
16155
16867
|
if (!incoming || typeof incoming !== "object" || Array.isArray(incoming)) {
|
|
16156
16868
|
res.status(400).json({ error: "bindings must be an object keyed by action kind" });
|
|
16157
16869
|
return;
|
|
@@ -16567,14 +17279,14 @@ function createDashboardServer(options) {
|
|
|
16567
17279
|
app.use("/api/backup", createBackupRouter());
|
|
16568
17280
|
if (serveStaticUi && dashboardDistPath) {
|
|
16569
17281
|
const sendOpts = { dotfiles: "allow" };
|
|
16570
|
-
app.use("/assets", express.static(
|
|
17282
|
+
app.use("/assets", express.static(resolve29(dashboardDistPath, "assets"), sendOpts));
|
|
16571
17283
|
app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
|
|
16572
17284
|
app.get("{*path}", async (req, res) => {
|
|
16573
17285
|
if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
|
|
16574
17286
|
res.status(404).json({ error: "Not Found" });
|
|
16575
17287
|
return;
|
|
16576
17288
|
}
|
|
16577
|
-
const indexPath =
|
|
17289
|
+
const indexPath = resolve29(dashboardDistPath, "index.html");
|
|
16578
17290
|
if (!await fileExists(indexPath)) {
|
|
16579
17291
|
res.status(503).send(
|
|
16580
17292
|
'Dashboard not built. Run "npm run build:dashboard" first.'
|
|
@@ -16598,7 +17310,7 @@ function createDashboardServer(options) {
|
|
|
16598
17310
|
serversDir: serversDir2,
|
|
16599
17311
|
playbooksDir: playbooksDir2,
|
|
16600
17312
|
todosDir: todosDir2,
|
|
16601
|
-
dbPath:
|
|
17313
|
+
dbPath: resolve29(syntaurRoot(), "syntaur.db"),
|
|
16602
17314
|
onMessage: broadcast
|
|
16603
17315
|
});
|
|
16604
17316
|
startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
|
|
@@ -16613,8 +17325,8 @@ function createDashboardServer(options) {
|
|
|
16613
17325
|
}
|
|
16614
17326
|
});
|
|
16615
17327
|
server.listen(port, () => {
|
|
16616
|
-
const portFile =
|
|
16617
|
-
|
|
17328
|
+
const portFile = resolve29(syntaurRoot(), "dashboard-port");
|
|
17329
|
+
writeFile7(portFile, String(port), "utf-8").catch(() => {
|
|
16618
17330
|
});
|
|
16619
17331
|
resolvePromise();
|
|
16620
17332
|
});
|
|
@@ -16632,8 +17344,8 @@ function createDashboardServer(options) {
|
|
|
16632
17344
|
client.terminate();
|
|
16633
17345
|
}
|
|
16634
17346
|
clients.clear();
|
|
16635
|
-
const portFile =
|
|
16636
|
-
await
|
|
17347
|
+
const portFile = resolve29(syntaurRoot(), "dashboard-port");
|
|
17348
|
+
await unlink7(portFile).catch(() => {
|
|
16637
17349
|
});
|
|
16638
17350
|
server.closeAllConnections?.();
|
|
16639
17351
|
return new Promise((resolvePromise) => {
|