syntaur 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dashboard/dist/assets/{_basePickBy-DXzhD14q.js → _basePickBy-eih-KlEh.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-gxypqvP5.js → _baseUniq-M21wg9ZQ.js} +1 -1
  3. package/dashboard/dist/assets/{arc-Ce7nYKSm.js → arc-uKZMelpQ.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-zX4f4_Mf.js → architectureDiagram-2XIMDMQ5-CpMG5exj.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-auOdy7nH.js → blockDiagram-WCTKOSBZ-BHnCCKl_.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-C2kkjPbW.js → c4Diagram-IC4MRINW-B-n3zU9i.js} +1 -1
  7. package/dashboard/dist/assets/channel-DVBgSlOI.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-B7dfpnbG.js → chunk-4BX2VUAB-ChD9Iuih.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-r1_jHZYp.js → chunk-55IACEB6-B3vP9Psg.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-5mMONjMK.js → chunk-FMBD7UC4-CIhWgxPS.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-CwKj-Es4.js → chunk-JSJVCQXG-DiGIV_cB.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-ByoW-HgN.js → chunk-KX2RTZJC-DnGsx5jo.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-D1olOovd.js → chunk-NQ4KR5QH-BFBu1fmg.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-CB8_FC8w.js → chunk-QZHKN3VN-DYtumHth.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-CFEqRrE1.js → chunk-WL4C6EOR-BzCrQPuw.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-B7dxBacd.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-B7dxBacd.js +1 -0
  18. package/dashboard/dist/assets/clone-DAOrHcCC.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-D6dGVXEI.js → cose-bilkent-S5V4N54A-Bl8mb5eY.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-Cvg9CgP-.js → dagre-KLK3FWXG-BHffcOgo.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-iCBudhZD.js → diagram-E7M64L7V-Ib83qzT_.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-BGniy7VQ.js → diagram-IFDJBPK2-hOdh63_T.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-B6Ie044E.js → diagram-P4PSJMXO-D4ocLmc5.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-BHvFRNhJ.js → erDiagram-INFDFZHY-CHJ6zqnJ.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-CN86Zu3Q.js → flowDiagram-PKNHOUZH-DEz5g2Ye.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-D-1fKFjW.js → ganttDiagram-A5KZAMGK-BSftxDHA.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-Dtf1A6KL.js → gitGraphDiagram-K3NZZRJ6-Cr3vGf07.js} +1 -1
  28. package/dashboard/dist/assets/{graph-B6H_kXSs.js → graph-D4us8trI.js} +1 -1
  29. package/dashboard/dist/assets/index-AXntWS_w.css +1 -0
  30. package/dashboard/dist/assets/index-CEMjexkj.js +460 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-R9wJj4JF.js → infoDiagram-LFFYTUFH-CH_jVfru.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CJmeR-bX.js → ishikawaDiagram-PHBUUO56-BdKLa5GC.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-FcUhyu8I.js → journeyDiagram-4ABVD52K-C_SMzNGF.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-C8UcTIwW.js → kanban-definition-K7BYSVSG-BeA-egRW.js} +1 -1
  35. package/dashboard/dist/assets/{layout-DzBy6alw.js → layout-B8tDmL4j.js} +1 -1
  36. package/dashboard/dist/assets/{linear-CZJCNOB9.js → linear-CeGJyrHS.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-fMQRe9Gq.js → mermaid.core-DyEs-LPd.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-BFwp-LS-.js → mindmap-definition-YRQLILUH-DCxzAr8m.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CQLmPkkd.js → pieDiagram-SKSYHLDU-CEj5dRDi.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-DAmi-dmD.js → quadrantDiagram-337W2JSQ-CKfvAEQg.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-Dcdts4kX.js → requirementDiagram-Z7DCOOCP-CTRqKPtJ.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-By8LRvM0.js → sankeyDiagram-WA2Y5GQK-BlYbz8UR.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BsvqgtTz.js → sequenceDiagram-2WXFIKYE-PT2t7ryQ.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DFNOD7cx.js → stateDiagram-RAJIS63D-eDX7IUuV.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO--yuSBnLh.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-CMcgJGjn.js → timeline-definition-YZTLITO2-By11B1Ow.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-BWsRNHwq.js → treemap-KZPCXAKY-rvdLeWWV.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-io7-2Tod.js → vennDiagram-LZ73GAT5-Br_oZ1wv.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-AVnh4fDS.js → xychartDiagram-JWTSCODW-D-MWVqrT.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +624 -139
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +658 -124
  54. package/dist/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/dashboard/dist/assets/channel-PMR2DuGi.js +0 -1
  57. package/dashboard/dist/assets/classDiagram-VBA2DB6C-DmESf_RL.js +0 -1
  58. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-DmESf_RL.js +0 -1
  59. package/dashboard/dist/assets/clone-WlIeyha4.js +0 -1
  60. package/dashboard/dist/assets/index-BhuXD-Q5.js +0 -445
  61. package/dashboard/dist/assets/index-BnqH-RIk.css +0 -1
  62. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DVO-Epiz.js +0 -1
package/dist/index.js CHANGED
@@ -93,10 +93,11 @@ onboarding:
93
93
  agentDefaults:
94
94
  trustLevel: medium
95
95
  autoApprove: false
96
- sync:
97
- enabled: false
98
- endpoint: null
99
- interval: 300
96
+ backup:
97
+ repo: null
98
+ categories: missions, playbooks, todos, servers, config
99
+ lastBackup: null
100
+ lastRestore: null
100
101
  ---
101
102
 
102
103
  # Syntaur Configuration
@@ -516,6 +517,14 @@ function serializeIntegrationConfig(integrations) {
516
517
  function serializeOnboardingConfig(onboarding) {
517
518
  return ["onboarding:", ` completed: ${onboarding.completed ? "true" : "false"}`].join("\n");
518
519
  }
520
+ function serializeBackupConfig(backup) {
521
+ const lines = ["backup:"];
522
+ lines.push(` repo: ${backup.repo ?? "null"}`);
523
+ lines.push(` categories: ${backup.categories}`);
524
+ lines.push(` lastBackup: ${backup.lastBackup ?? "null"}`);
525
+ lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
526
+ return lines.join("\n");
527
+ }
519
528
  function stripTopLevelBlock(fmBlock, key) {
520
529
  const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
521
530
  if (!blockStart) {
@@ -680,6 +689,40 @@ ${normalizedFm}
680
689
  ---${afterFrontmatter}`;
681
690
  await writeFileForce(configPath, newContent);
682
691
  }
692
+ async function updateBackupConfig(backup) {
693
+ const configPath = resolve4(syntaurRoot(), "config.md");
694
+ const current = (await readConfig()).backup;
695
+ const nextBackup = {
696
+ repo: current?.repo ?? null,
697
+ categories: current?.categories ?? "missions, playbooks, todos, servers, config",
698
+ lastBackup: current?.lastBackup ?? null,
699
+ lastRestore: current?.lastRestore ?? null,
700
+ ...backup
701
+ };
702
+ const backupBlock = serializeBackupConfig(nextBackup);
703
+ const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultMissionDir: defaultMissionDir() });
704
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
705
+ if (!fmMatch) {
706
+ const content = `---
707
+ version: "1.0"
708
+ defaultMissionDir: ${defaultMissionDir()}
709
+ ${backupBlock}
710
+ ---
711
+ ${existing}`;
712
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
713
+ return;
714
+ }
715
+ const fmBlock = fmMatch[2];
716
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
717
+ const cleanedFm = stripTopLevelBlock(fmBlock, "backup");
718
+ const newFm = `${cleanedFm}
719
+ ${backupBlock}`.replace(/^\n+/, "");
720
+ const normalizedFm = newFm.replace(/\n+$/, "");
721
+ const newContent = `---
722
+ ${normalizedFm}
723
+ ---${afterFrontmatter}`;
724
+ await writeFileForce(configPath, newContent);
725
+ }
683
726
  async function readConfig() {
684
727
  const configPath = resolve4(syntaurRoot(), "config.md");
685
728
  if (!await fileExists(configPath)) {
@@ -722,6 +765,12 @@ async function readConfig() {
722
765
  "integrations.codexMarketplacePath"
723
766
  )
724
767
  },
768
+ backup: fm["backup.repo"] || fm["backup.categories"] ? {
769
+ repo: fm["backup.repo"] && fm["backup.repo"] !== "null" ? fm["backup.repo"] : null,
770
+ categories: fm["backup.categories"] || "missions, playbooks, todos, servers, config",
771
+ lastBackup: fm["backup.lastBackup"] && fm["backup.lastBackup"] !== "null" ? fm["backup.lastBackup"] : null,
772
+ lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
773
+ } : null,
725
774
  statuses: parseStatusConfig(content)
726
775
  };
727
776
  }
@@ -747,6 +796,7 @@ var init_config2 = __esm({
747
796
  codexPluginDir: null,
748
797
  codexMarketplacePath: null
749
798
  },
799
+ backup: null,
750
800
  statuses: null
751
801
  };
752
802
  }
@@ -3768,8 +3818,8 @@ __export(launch_exports, {
3768
3818
  launchAgent: () => launchAgent
3769
3819
  });
3770
3820
  import { spawn as spawn2 } from "child_process";
3771
- import { mkdir as mkdir2, writeFile as writeFile5 } from "fs/promises";
3772
- import { resolve as resolve32 } from "path";
3821
+ import { mkdir as mkdir2, writeFile as writeFile6 } from "fs/promises";
3822
+ import { resolve as resolve33 } from "path";
3773
3823
  async function launchAgent(options) {
3774
3824
  const { missionsDir, missionSlug, assignmentSlug, agent } = options;
3775
3825
  const command = AGENT_COMMANDS[agent];
@@ -3779,9 +3829,9 @@ async function launchAgent(options) {
3779
3829
  process.exit(1);
3780
3830
  }
3781
3831
  const workspaceDir = detail.workspace.worktreePath ?? (detail.workspace.repository?.startsWith("/") ? detail.workspace.repository : null) ?? process.cwd();
3782
- const missionDir = resolve32(missionsDir, missionSlug);
3783
- const assignmentDir = resolve32(missionDir, "assignments", assignmentSlug);
3784
- const contextDir = resolve32(workspaceDir, ".syntaur");
3832
+ const missionDir = resolve33(missionsDir, missionSlug);
3833
+ const assignmentDir = resolve33(missionDir, "assignments", assignmentSlug);
3834
+ const contextDir = resolve33(workspaceDir, ".syntaur");
3785
3835
  await mkdir2(contextDir, { recursive: true });
3786
3836
  const context = {
3787
3837
  missionSlug,
@@ -3793,8 +3843,8 @@ async function launchAgent(options) {
3793
3843
  branch: detail.workspace.branch ?? null,
3794
3844
  grabbedAt: (/* @__PURE__ */ new Date()).toISOString()
3795
3845
  };
3796
- await writeFile5(
3797
- resolve32(contextDir, "context.json"),
3846
+ await writeFile6(
3847
+ resolve33(contextDir, "context.json"),
3798
3848
  JSON.stringify(context, null, 2) + "\n"
3799
3849
  );
3800
3850
  return new Promise((resolvePromise, reject) => {
@@ -3829,7 +3879,7 @@ var init_launch = __esm({
3829
3879
  });
3830
3880
 
3831
3881
  // src/index.ts
3832
- import { Command as Command2 } from "commander";
3882
+ import { Command as Command3 } from "commander";
3833
3883
 
3834
3884
  // src/commands/init.ts
3835
3885
  init_paths();
@@ -4907,16 +4957,16 @@ Use --slug to specify a different slug.`
4907
4957
  init_config2();
4908
4958
  import { spawn } from "child_process";
4909
4959
  import { createServer as createNetServer } from "net";
4910
- import { resolve as resolve18, dirname as dirname4 } from "path";
4960
+ import { resolve as resolve19, dirname as dirname4 } from "path";
4911
4961
  import { fileURLToPath as fileURLToPath2 } from "url";
4912
4962
 
4913
4963
  // src/dashboard/server.ts
4914
4964
  init_api();
4915
4965
  import express from "express";
4916
4966
  import { createServer } from "http";
4917
- import { resolve as resolve17 } from "path";
4967
+ import { resolve as resolve18 } from "path";
4918
4968
  import { homedir as homedir2 } from "os";
4919
- import { writeFile as writeFile3, unlink as unlink3 } from "fs/promises";
4969
+ import { writeFile as writeFile4, unlink as unlink4 } from "fs/promises";
4920
4970
  import { WebSocketServer, WebSocket } from "ws";
4921
4971
 
4922
4972
  // src/dashboard/watcher.ts
@@ -6096,8 +6146,8 @@ async function migrateFromMarkdown(missionsDir) {
6096
6146
  return allSessions.length;
6097
6147
  }
6098
6148
  async function parseMarkdownSessionsIndex(filePath, missionSlug) {
6099
- const { readFile: readFile15 } = await import("fs/promises");
6100
- const raw = await readFile15(filePath, "utf-8");
6149
+ const { readFile: readFile16 } = await import("fs/promises");
6150
+ const raw = await readFile16(filePath, "utf-8");
6101
6151
  const sessions = [];
6102
6152
  const lines = raw.split("\n");
6103
6153
  let inTable = false;
@@ -6604,8 +6654,8 @@ function createTodosRouter(todosDir2, broadcast) {
6604
6654
  router.post("/:workspace/archive", async (req, res) => {
6605
6655
  try {
6606
6656
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
6607
- const { resolve: resolve36 } = await import("path");
6608
- const { readFile: readFile15 } = await import("fs/promises");
6657
+ const { resolve: resolve37 } = await import("path");
6658
+ const { readFile: readFile16 } = await import("fs/promises");
6609
6659
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
6610
6660
  const workspace = getWorkspaceParam(req.params.workspace);
6611
6661
  const checklist = await readChecklist(todosDir2, workspace);
@@ -6621,10 +6671,10 @@ function createTodosRouter(todosDir2, broadcast) {
6621
6671
  (e) => e.itemIds.every((id) => completedIds.has(id))
6622
6672
  );
6623
6673
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
6624
- await ensureDir(resolve36(todosDir2, "archive"));
6674
+ await ensureDir(resolve37(todosDir2, "archive"));
6625
6675
  let archContent = "";
6626
6676
  if (await fileExists(archFile)) {
6627
- archContent = await readFile15(archFile, "utf-8");
6677
+ archContent = await readFile16(archFile, "utf-8");
6628
6678
  archContent = archContent.trimEnd() + "\n\n";
6629
6679
  } else {
6630
6680
  archContent = `---
@@ -6874,6 +6924,411 @@ workspace: ${workspace}
6874
6924
  return router;
6875
6925
  }
6876
6926
 
6927
+ // src/dashboard/api-backup.ts
6928
+ init_config2();
6929
+ import { Router as Router6 } from "express";
6930
+
6931
+ // src/utils/github-backup.ts
6932
+ init_paths();
6933
+ init_fs();
6934
+ init_config2();
6935
+ import { execFile as execFile2 } from "child_process";
6936
+ import { promisify as promisify2 } from "util";
6937
+ import { cp, mkdtemp, rm as rm2, readFile as readFile12, writeFile as writeFile3, unlink as unlink3, stat, open, rename as rename2 } from "fs/promises";
6938
+ import { resolve as resolve17, join as join2 } from "path";
6939
+ import { tmpdir } from "os";
6940
+ var exec2 = promisify2(execFile2);
6941
+ var VALID_CATEGORIES = ["missions", "playbooks", "todos", "servers", "config"];
6942
+ var LOCK_FILE_NAME = ".backup-lock";
6943
+ function parseCategoriesStrict(cats) {
6944
+ const unknown = [];
6945
+ const valid = [];
6946
+ for (const cat of cats) {
6947
+ if (VALID_CATEGORIES.includes(cat)) {
6948
+ valid.push(cat);
6949
+ } else {
6950
+ unknown.push(cat);
6951
+ }
6952
+ }
6953
+ if (unknown.length > 0) {
6954
+ throw new Error(
6955
+ `Unknown categor${unknown.length === 1 ? "y" : "ies"}: ${unknown.map((c) => `"${c}"`).join(", ")}. Valid: ${VALID_CATEGORIES.join(", ")}`
6956
+ );
6957
+ }
6958
+ return valid;
6959
+ }
6960
+ function validateRepoUrl(url) {
6961
+ if (!url || typeof url !== "string") return false;
6962
+ const trimmed = url.trim();
6963
+ return trimmed.startsWith("https://") || trimmed.startsWith("git@");
6964
+ }
6965
+ async function resolveCategoryPath(category) {
6966
+ switch (category) {
6967
+ case "missions": {
6968
+ const config = await readConfig();
6969
+ return { sourcePath: config.defaultMissionDir, repoPath: "missions", isFile: false };
6970
+ }
6971
+ case "playbooks":
6972
+ return { sourcePath: playbooksDir(), repoPath: "playbooks", isFile: false };
6973
+ case "todos":
6974
+ return { sourcePath: todosDir(), repoPath: "todos", isFile: false };
6975
+ case "servers":
6976
+ return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
6977
+ case "config":
6978
+ return { sourcePath: resolve17(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
6979
+ }
6980
+ }
6981
+ async function checkGitInstalled() {
6982
+ try {
6983
+ await exec2("git", ["--version"]);
6984
+ } catch {
6985
+ throw new Error("git is not installed or not on PATH. Install git and try again.");
6986
+ }
6987
+ }
6988
+ async function acquireLock() {
6989
+ const lockPath = resolve17(syntaurRoot(), LOCK_FILE_NAME);
6990
+ await ensureDir(syntaurRoot());
6991
+ try {
6992
+ const handle = await open(lockPath, "wx");
6993
+ await handle.write(String(process.pid));
6994
+ await handle.close();
6995
+ return lockPath;
6996
+ } catch (err) {
6997
+ if (err.code === "EEXIST") {
6998
+ const pid = await readFile12(lockPath, "utf-8").catch(() => "");
6999
+ throw new Error(
7000
+ `Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
7001
+ );
7002
+ }
7003
+ throw err;
7004
+ }
7005
+ }
7006
+ async function releaseLock(lockPath) {
7007
+ try {
7008
+ await unlink3(lockPath);
7009
+ } catch {
7010
+ }
7011
+ }
7012
+ async function runGit(args, cwd) {
7013
+ return exec2("git", args, { cwd });
7014
+ }
7015
+ async function cloneOrInit(repoUrl, destDir) {
7016
+ try {
7017
+ await exec2("git", ["clone", "--depth", "1", repoUrl, destDir]);
7018
+ } catch (error) {
7019
+ const message = error instanceof Error ? error.message : String(error);
7020
+ if (message.includes("Repository not found") || message.includes("does not appear to be a git repository")) {
7021
+ throw new Error(`Repository not found or inaccessible: ${repoUrl}. Check URL and credentials.`);
7022
+ }
7023
+ if (message.includes("Authentication failed") || message.includes("could not read Username")) {
7024
+ throw new Error(`Authentication failed for ${repoUrl}. Check SSH keys or credentials.`);
7025
+ }
7026
+ throw new Error(`git clone failed: ${message}`);
7027
+ }
7028
+ }
7029
+ async function copyRecursive(src, dest) {
7030
+ if (!await fileExists(src)) return;
7031
+ const s = await stat(src);
7032
+ if (s.isDirectory()) {
7033
+ await ensureDir(dest);
7034
+ await cp(src, dest, { recursive: true, force: true });
7035
+ } else {
7036
+ await ensureDir(resolve17(dest, ".."));
7037
+ await cp(src, dest, { force: true });
7038
+ }
7039
+ }
7040
+ function resolveCategoriesStrict(csv) {
7041
+ const parts = csv.split(",").map((s) => s.trim()).filter(Boolean);
7042
+ return parseCategoriesStrict(parts);
7043
+ }
7044
+ async function readSanitizedConfig(configPath) {
7045
+ const content = await readFile12(configPath, "utf-8");
7046
+ return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
7047
+ }
7048
+ async function backupToGithub(overrides) {
7049
+ await checkGitInstalled();
7050
+ const config = await readConfig();
7051
+ const rawRepo = overrides?.repo ?? config.backup?.repo ?? null;
7052
+ if (!rawRepo) {
7053
+ throw new Error("No backup repo configured. Set it via `syntaur backup config --repo <url>` or the dashboard.");
7054
+ }
7055
+ const repo = rawRepo.trim();
7056
+ if (!validateRepoUrl(repo)) {
7057
+ throw new Error(`Invalid repo URL: "${rawRepo}". Must start with https:// or git@.`);
7058
+ }
7059
+ const categoriesCsv = config.backup?.categories ?? "missions, playbooks, todos, servers, config";
7060
+ const categories = overrides?.categories ?? resolveCategoriesStrict(categoriesCsv);
7061
+ if (categories.length === 0) {
7062
+ throw new Error("No valid backup categories selected.");
7063
+ }
7064
+ const lockPath = await acquireLock();
7065
+ let tmpDir = null;
7066
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
7067
+ try {
7068
+ tmpDir = await mkdtemp(join2(tmpdir(), "syntaur-backup-"));
7069
+ await cloneOrInit(repo, tmpDir);
7070
+ for (const category of categories) {
7071
+ const { sourcePath, repoPath, isFile } = await resolveCategoryPath(category);
7072
+ const destPath = join2(tmpDir, repoPath);
7073
+ if (isFile) {
7074
+ await rm2(destPath, { force: true });
7075
+ } else {
7076
+ await rm2(destPath, { recursive: true, force: true });
7077
+ }
7078
+ if (!await fileExists(sourcePath)) {
7079
+ console.warn(`Category "${category}": no local data at ${sourcePath}; backup will reflect deletion.`);
7080
+ continue;
7081
+ }
7082
+ if (category === "config") {
7083
+ const sanitized = await readSanitizedConfig(sourcePath);
7084
+ await ensureDir(resolve17(destPath, ".."));
7085
+ await writeFile3(destPath, sanitized, "utf-8");
7086
+ } else {
7087
+ await copyRecursive(sourcePath, destPath);
7088
+ }
7089
+ }
7090
+ await runGit(["add", "-A"], tmpDir);
7091
+ const { stdout: status } = await runGit(["status", "--porcelain"], tmpDir);
7092
+ if (!status.trim()) {
7093
+ await updateBackupConfig({ lastBackup: timestamp }).catch(() => {
7094
+ });
7095
+ return {
7096
+ success: true,
7097
+ timestamp,
7098
+ message: "No changes to back up.",
7099
+ committed: false
7100
+ };
7101
+ }
7102
+ try {
7103
+ await runGit(["config", "user.email", "syntaur@local"], tmpDir);
7104
+ await runGit(["config", "user.name", "Syntaur Backup"], tmpDir);
7105
+ } catch {
7106
+ }
7107
+ await runGit(["commit", "-m", `Syntaur backup ${timestamp}`], tmpDir);
7108
+ try {
7109
+ await runGit(["push"], tmpDir);
7110
+ } catch (error) {
7111
+ const message = error instanceof Error ? error.message : String(error);
7112
+ if (message.includes("non-fast-forward") || message.includes("rejected")) {
7113
+ throw new Error("Push rejected (non-fast-forward). Pull and resolve manually, or delete remote contents.");
7114
+ }
7115
+ if (message.includes("Authentication") || message.includes("could not read Username")) {
7116
+ throw new Error("Push authentication failed. Check SSH keys or credentials.");
7117
+ }
7118
+ throw new Error(`git push failed: ${message}`);
7119
+ }
7120
+ await updateBackupConfig({ lastBackup: timestamp }).catch(() => {
7121
+ });
7122
+ return {
7123
+ success: true,
7124
+ timestamp,
7125
+ message: `Backed up ${categories.length} categor${categories.length === 1 ? "y" : "ies"} to ${repo}.`,
7126
+ committed: true
7127
+ };
7128
+ } finally {
7129
+ if (tmpDir) {
7130
+ await rm2(tmpDir, { recursive: true, force: true }).catch(() => {
7131
+ });
7132
+ }
7133
+ await releaseLock(lockPath);
7134
+ }
7135
+ }
7136
+ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
7137
+ if (isFile) {
7138
+ await ensureDir(resolve17(localPath, ".."));
7139
+ await cp(repoSrcPath, localPath, { force: true });
7140
+ return;
7141
+ }
7142
+ const stagingPath = `${localPath}.syntaur-restore-staging`;
7143
+ const backupPath = `${localPath}.syntaur-restore-backup`;
7144
+ await rm2(stagingPath, { recursive: true, force: true });
7145
+ const backupExistsBefore = await fileExists(backupPath);
7146
+ const localExistsBefore = await fileExists(localPath);
7147
+ if (backupExistsBefore) {
7148
+ if (!localExistsBefore) {
7149
+ await rename2(backupPath, localPath);
7150
+ } else {
7151
+ throw new Error(
7152
+ `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.`
7153
+ );
7154
+ }
7155
+ }
7156
+ let localMovedAside = false;
7157
+ try {
7158
+ await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
7159
+ const localExists = await fileExists(localPath);
7160
+ if (localExists) {
7161
+ await rename2(localPath, backupPath);
7162
+ localMovedAside = true;
7163
+ }
7164
+ await rename2(stagingPath, localPath);
7165
+ await rm2(backupPath, { recursive: true, force: true }).catch(() => {
7166
+ });
7167
+ } catch (err) {
7168
+ if (localMovedAside && await fileExists(backupPath)) {
7169
+ await rename2(backupPath, localPath).catch(() => {
7170
+ });
7171
+ }
7172
+ await rm2(stagingPath, { recursive: true, force: true }).catch(() => {
7173
+ });
7174
+ throw err;
7175
+ }
7176
+ }
7177
+ async function restoreFromGithub(overrides) {
7178
+ await checkGitInstalled();
7179
+ const config = await readConfig();
7180
+ const rawRepo = overrides?.repo ?? config.backup?.repo ?? null;
7181
+ if (!rawRepo) {
7182
+ throw new Error("No backup repo configured.");
7183
+ }
7184
+ const repo = rawRepo.trim();
7185
+ if (!validateRepoUrl(repo)) {
7186
+ throw new Error(`Invalid repo URL: "${rawRepo}".`);
7187
+ }
7188
+ const categoriesCsv = config.backup?.categories ?? "missions, playbooks, todos, servers, config";
7189
+ const categories = overrides?.categories ?? resolveCategoriesStrict(categoriesCsv);
7190
+ if (categories.length === 0) {
7191
+ throw new Error("No valid restore categories selected.");
7192
+ }
7193
+ const lockPath = await acquireLock();
7194
+ let tmpDir = null;
7195
+ const restored = [];
7196
+ const failed = [];
7197
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
7198
+ try {
7199
+ await updateBackupConfig({ lastRestore: timestamp });
7200
+ tmpDir = await mkdtemp(join2(tmpdir(), "syntaur-restore-"));
7201
+ await cloneOrInit(repo, tmpDir);
7202
+ for (const category of categories) {
7203
+ if (category === "config") {
7204
+ console.warn('Skipping "config" on restore (would overwrite local backup settings).');
7205
+ continue;
7206
+ }
7207
+ try {
7208
+ const { sourcePath: localPath, repoPath, isFile } = await resolveCategoryPath(category);
7209
+ const repoSrcPath = join2(tmpDir, repoPath);
7210
+ if (!await fileExists(repoSrcPath)) {
7211
+ console.warn(`Category "${category}" not found in backup repo, skipping.`);
7212
+ continue;
7213
+ }
7214
+ await safeRestoreCategory(localPath, repoSrcPath, isFile);
7215
+ restored.push(category);
7216
+ } catch (error) {
7217
+ const msg = error instanceof Error ? error.message : String(error);
7218
+ console.error(`Failed to restore "${category}": ${msg}`);
7219
+ failed.push(category);
7220
+ }
7221
+ }
7222
+ const success = failed.length === 0;
7223
+ return {
7224
+ success,
7225
+ timestamp,
7226
+ message: success ? `Restored ${restored.length} categor${restored.length === 1 ? "y" : "ies"} from ${repo}.` : `Partial restore: ${restored.length} succeeded, ${failed.length} failed (${failed.join(", ")}).`,
7227
+ committed: false
7228
+ };
7229
+ } finally {
7230
+ if (tmpDir) {
7231
+ await rm2(tmpDir, { recursive: true, force: true }).catch(() => {
7232
+ });
7233
+ }
7234
+ await releaseLock(lockPath);
7235
+ }
7236
+ }
7237
+ async function getBackupStatus() {
7238
+ const config = await readConfig();
7239
+ const lockPath = resolve17(syntaurRoot(), LOCK_FILE_NAME);
7240
+ const locked = await fileExists(lockPath);
7241
+ return {
7242
+ repo: config.backup?.repo ?? null,
7243
+ categories: config.backup?.categories ?? "missions, playbooks, todos, servers, config",
7244
+ lastBackup: config.backup?.lastBackup ?? null,
7245
+ lastRestore: config.backup?.lastRestore ?? null,
7246
+ locked
7247
+ };
7248
+ }
7249
+
7250
+ // src/dashboard/api-backup.ts
7251
+ function createBackupRouter() {
7252
+ const router = Router6();
7253
+ router.get("/", async (_req, res) => {
7254
+ try {
7255
+ const status = await getBackupStatus();
7256
+ res.json(status);
7257
+ } catch (error) {
7258
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
7259
+ }
7260
+ });
7261
+ router.put("/config", async (req, res) => {
7262
+ try {
7263
+ const body = req.body ?? {};
7264
+ const updates = {};
7265
+ if (body.repo !== void 0) {
7266
+ const trimmed = typeof body.repo === "string" ? body.repo.trim() : body.repo;
7267
+ if (trimmed !== null && trimmed !== "" && !validateRepoUrl(trimmed)) {
7268
+ return res.status(400).json({
7269
+ error: `Invalid repo URL. Must start with https:// or git@.`
7270
+ });
7271
+ }
7272
+ updates.repo = trimmed || null;
7273
+ }
7274
+ if (body.categories !== void 0) {
7275
+ let cats;
7276
+ if (Array.isArray(body.categories)) {
7277
+ cats = body.categories.map((s) => String(s).trim()).filter(Boolean);
7278
+ } else if (typeof body.categories === "string") {
7279
+ cats = body.categories.split(",").map((s) => s.trim()).filter(Boolean);
7280
+ } else {
7281
+ return res.status(400).json({ error: "categories must be a string or array" });
7282
+ }
7283
+ if (cats.length === 0) {
7284
+ return res.status(400).json({
7285
+ error: `No categories provided. Valid: ${VALID_CATEGORIES.join(", ")}`
7286
+ });
7287
+ }
7288
+ try {
7289
+ const valid = parseCategoriesStrict(cats);
7290
+ updates.categories = valid.join(", ");
7291
+ } catch (err) {
7292
+ return res.status(400).json({
7293
+ error: err instanceof Error ? err.message : String(err)
7294
+ });
7295
+ }
7296
+ }
7297
+ if (Object.keys(updates).length === 0) {
7298
+ return res.status(400).json({ error: "No fields to update" });
7299
+ }
7300
+ await updateBackupConfig(updates);
7301
+ const status = await getBackupStatus();
7302
+ res.json(status);
7303
+ } catch (error) {
7304
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
7305
+ }
7306
+ });
7307
+ router.post("/push", async (_req, res) => {
7308
+ try {
7309
+ const result = await backupToGithub();
7310
+ res.json(result);
7311
+ } catch (error) {
7312
+ res.status(500).json({
7313
+ success: false,
7314
+ error: error instanceof Error ? error.message : String(error)
7315
+ });
7316
+ }
7317
+ });
7318
+ router.post("/pull", async (_req, res) => {
7319
+ try {
7320
+ const result = await restoreFromGithub();
7321
+ res.json(result);
7322
+ } catch (error) {
7323
+ res.status(500).json({
7324
+ success: false,
7325
+ error: error instanceof Error ? error.message : String(error)
7326
+ });
7327
+ }
7328
+ });
7329
+ return router;
7330
+ }
7331
+
6877
7332
  // src/dashboard/autodiscovery.ts
6878
7333
  init_scanner();
6879
7334
  init_servers();
@@ -7299,10 +7754,11 @@ function createDashboardServer(options) {
7299
7754
  app.use("/api/agent-sessions", createAgentSessionsRouter(missionsDir, broadcast));
7300
7755
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
7301
7756
  app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
7757
+ app.use("/api/backup", createBackupRouter());
7302
7758
  if (serveStaticUi && dashboardDistPath) {
7303
7759
  app.use(express.static(dashboardDistPath));
7304
7760
  app.get("{*path}", async (_req, res) => {
7305
- const indexPath = resolve17(dashboardDistPath, "index.html");
7761
+ const indexPath = resolve18(dashboardDistPath, "index.html");
7306
7762
  if (await fileExists(indexPath)) {
7307
7763
  res.sendFile(indexPath);
7308
7764
  } else {
@@ -7334,8 +7790,8 @@ function createDashboardServer(options) {
7334
7790
  }
7335
7791
  });
7336
7792
  server.listen(port, () => {
7337
- const portFile = resolve17(homedir2(), ".syntaur", "dashboard-port");
7338
- writeFile3(portFile, String(port), "utf-8").catch(() => {
7793
+ const portFile = resolve18(homedir2(), ".syntaur", "dashboard-port");
7794
+ writeFile4(portFile, String(port), "utf-8").catch(() => {
7339
7795
  });
7340
7796
  resolvePromise();
7341
7797
  });
@@ -7351,8 +7807,8 @@ function createDashboardServer(options) {
7351
7807
  client.close();
7352
7808
  }
7353
7809
  clients.clear();
7354
- const portFile = resolve17(homedir2(), ".syntaur", "dashboard-port");
7355
- await unlink3(portFile).catch(() => {
7810
+ const portFile = resolve18(homedir2(), ".syntaur", "dashboard-port");
7811
+ await unlink4(portFile).catch(() => {
7356
7812
  });
7357
7813
  return new Promise((resolvePromise) => {
7358
7814
  server.close(() => resolvePromise());
@@ -7430,8 +7886,8 @@ async function dashboardCommand(options) {
7430
7886
  port = availablePort;
7431
7887
  }
7432
7888
  const thisFile = fileURLToPath2(import.meta.url);
7433
- const packageRoot = resolve18(dirname4(thisFile), "..");
7434
- const dashboardDist = resolve18(packageRoot, "dashboard", "dist");
7889
+ const packageRoot = resolve19(dirname4(thisFile), "..");
7890
+ const dashboardDist = resolve19(packageRoot, "dashboard", "dist");
7435
7891
  const server = createDashboardServer({
7436
7892
  port,
7437
7893
  missionsDir,
@@ -7444,8 +7900,8 @@ async function dashboardCommand(options) {
7444
7900
  await server.start();
7445
7901
  let viteProcess = null;
7446
7902
  if (mode === "dev") {
7447
- const dashboardDir = resolve18(packageRoot, "dashboard");
7448
- const viteBin = resolve18(dashboardDir, "node_modules", ".bin", "vite");
7903
+ const dashboardDir = resolve19(packageRoot, "dashboard");
7904
+ const viteBin = resolve19(dashboardDir, "node_modules", ".bin", "vite");
7449
7905
  if (!await fileExists(viteBin)) {
7450
7906
  console.error(
7451
7907
  'Vite not found. Run "npm ci --prefix dashboard" first, or use the default bundled dashboard mode.'
@@ -7497,7 +7953,7 @@ async function dashboardCommand(options) {
7497
7953
  init_paths();
7498
7954
  init_fs();
7499
7955
  init_config2();
7500
- import { resolve as resolve19 } from "path";
7956
+ import { resolve as resolve20 } from "path";
7501
7957
  init_lifecycle();
7502
7958
  async function assignCommand(assignment, options) {
7503
7959
  if (!options.mission) {
@@ -7518,8 +7974,8 @@ async function assignCommand(assignment, options) {
7518
7974
  }
7519
7975
  const config = await readConfig();
7520
7976
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultMissionDir;
7521
- const missionDir = resolve19(baseDir, options.mission);
7522
- const missionMdPath = resolve19(missionDir, "mission.md");
7977
+ const missionDir = resolve20(baseDir, options.mission);
7978
+ const missionMdPath = resolve20(missionDir, "mission.md");
7523
7979
  if (!await fileExists(missionDir) || !await fileExists(missionMdPath)) {
7524
7980
  throw new Error(
7525
7981
  `Mission "${options.mission}" not found at ${missionDir}.`
@@ -7536,7 +7992,7 @@ async function assignCommand(assignment, options) {
7536
7992
  init_paths();
7537
7993
  init_fs();
7538
7994
  init_config2();
7539
- import { resolve as resolve20 } from "path";
7995
+ import { resolve as resolve21 } from "path";
7540
7996
  init_lifecycle();
7541
7997
  async function startCommand(assignment, options) {
7542
7998
  if (!options.mission) {
@@ -7554,8 +8010,8 @@ async function startCommand(assignment, options) {
7554
8010
  }
7555
8011
  const config = await readConfig();
7556
8012
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultMissionDir;
7557
- const missionDir = resolve20(baseDir, options.mission);
7558
- const missionMdPath = resolve20(missionDir, "mission.md");
8013
+ const missionDir = resolve21(baseDir, options.mission);
8014
+ const missionMdPath = resolve21(missionDir, "mission.md");
7559
8015
  if (!await fileExists(missionDir) || !await fileExists(missionMdPath)) {
7560
8016
  throw new Error(
7561
8017
  `Mission "${options.mission}" not found at ${missionDir}.`
@@ -7579,7 +8035,7 @@ async function startCommand(assignment, options) {
7579
8035
  init_paths();
7580
8036
  init_fs();
7581
8037
  init_config2();
7582
- import { resolve as resolve21 } from "path";
8038
+ import { resolve as resolve22 } from "path";
7583
8039
  init_lifecycle();
7584
8040
  async function completeCommand(assignment, options) {
7585
8041
  if (!options.mission) {
@@ -7597,8 +8053,8 @@ async function completeCommand(assignment, options) {
7597
8053
  }
7598
8054
  const config = await readConfig();
7599
8055
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultMissionDir;
7600
- const missionDir = resolve21(baseDir, options.mission);
7601
- const missionMdPath = resolve21(missionDir, "mission.md");
8056
+ const missionDir = resolve22(baseDir, options.mission);
8057
+ const missionMdPath = resolve22(missionDir, "mission.md");
7602
8058
  if (!await fileExists(missionDir) || !await fileExists(missionMdPath)) {
7603
8059
  throw new Error(
7604
8060
  `Mission "${options.mission}" not found at ${missionDir}.`
@@ -7615,7 +8071,7 @@ async function completeCommand(assignment, options) {
7615
8071
  init_paths();
7616
8072
  init_fs();
7617
8073
  init_config2();
7618
- import { resolve as resolve22 } from "path";
8074
+ import { resolve as resolve23 } from "path";
7619
8075
  init_lifecycle();
7620
8076
  async function blockCommand(assignment, options) {
7621
8077
  if (!options.mission) {
@@ -7633,8 +8089,8 @@ async function blockCommand(assignment, options) {
7633
8089
  }
7634
8090
  const config = await readConfig();
7635
8091
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultMissionDir;
7636
- const missionDir = resolve22(baseDir, options.mission);
7637
- const missionMdPath = resolve22(missionDir, "mission.md");
8092
+ const missionDir = resolve23(baseDir, options.mission);
8093
+ const missionMdPath = resolve23(missionDir, "mission.md");
7638
8094
  if (!await fileExists(missionDir) || !await fileExists(missionMdPath)) {
7639
8095
  throw new Error(
7640
8096
  `Mission "${options.mission}" not found at ${missionDir}.`
@@ -7653,7 +8109,7 @@ async function blockCommand(assignment, options) {
7653
8109
  init_paths();
7654
8110
  init_fs();
7655
8111
  init_config2();
7656
- import { resolve as resolve23 } from "path";
8112
+ import { resolve as resolve24 } from "path";
7657
8113
  init_lifecycle();
7658
8114
  async function unblockCommand(assignment, options) {
7659
8115
  if (!options.mission) {
@@ -7671,8 +8127,8 @@ async function unblockCommand(assignment, options) {
7671
8127
  }
7672
8128
  const config = await readConfig();
7673
8129
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultMissionDir;
7674
- const missionDir = resolve23(baseDir, options.mission);
7675
- const missionMdPath = resolve23(missionDir, "mission.md");
8130
+ const missionDir = resolve24(baseDir, options.mission);
8131
+ const missionMdPath = resolve24(missionDir, "mission.md");
7676
8132
  if (!await fileExists(missionDir) || !await fileExists(missionMdPath)) {
7677
8133
  throw new Error(
7678
8134
  `Mission "${options.mission}" not found at ${missionDir}.`
@@ -7689,7 +8145,7 @@ async function unblockCommand(assignment, options) {
7689
8145
  init_paths();
7690
8146
  init_fs();
7691
8147
  init_config2();
7692
- import { resolve as resolve24 } from "path";
8148
+ import { resolve as resolve25 } from "path";
7693
8149
  init_lifecycle();
7694
8150
  async function reviewCommand(assignment, options) {
7695
8151
  if (!options.mission) {
@@ -7707,8 +8163,8 @@ async function reviewCommand(assignment, options) {
7707
8163
  }
7708
8164
  const config = await readConfig();
7709
8165
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultMissionDir;
7710
- const missionDir = resolve24(baseDir, options.mission);
7711
- const missionMdPath = resolve24(missionDir, "mission.md");
8166
+ const missionDir = resolve25(baseDir, options.mission);
8167
+ const missionMdPath = resolve25(missionDir, "mission.md");
7712
8168
  if (!await fileExists(missionDir) || !await fileExists(missionMdPath)) {
7713
8169
  throw new Error(
7714
8170
  `Mission "${options.mission}" not found at ${missionDir}.`
@@ -7725,7 +8181,7 @@ async function reviewCommand(assignment, options) {
7725
8181
  init_paths();
7726
8182
  init_fs();
7727
8183
  init_config2();
7728
- import { resolve as resolve25 } from "path";
8184
+ import { resolve as resolve26 } from "path";
7729
8185
  init_lifecycle();
7730
8186
  async function failCommand(assignment, options) {
7731
8187
  if (!options.mission) {
@@ -7743,8 +8199,8 @@ async function failCommand(assignment, options) {
7743
8199
  }
7744
8200
  const config = await readConfig();
7745
8201
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultMissionDir;
7746
- const missionDir = resolve25(baseDir, options.mission);
7747
- const missionMdPath = resolve25(missionDir, "mission.md");
8202
+ const missionDir = resolve26(baseDir, options.mission);
8203
+ const missionMdPath = resolve26(missionDir, "mission.md");
7748
8204
  if (!await fileExists(missionDir) || !await fileExists(missionMdPath)) {
7749
8205
  throw new Error(
7750
8206
  `Mission "${options.mission}" not found at ${missionDir}.`
@@ -7761,7 +8217,7 @@ async function failCommand(assignment, options) {
7761
8217
  init_paths();
7762
8218
  init_fs();
7763
8219
  init_config2();
7764
- import { resolve as resolve26 } from "path";
8220
+ import { resolve as resolve27 } from "path";
7765
8221
  init_lifecycle();
7766
8222
  async function reopenCommand(assignment, options) {
7767
8223
  if (!options.mission) {
@@ -7779,8 +8235,8 @@ async function reopenCommand(assignment, options) {
7779
8235
  }
7780
8236
  const config = await readConfig();
7781
8237
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultMissionDir;
7782
- const missionDir = resolve26(baseDir, options.mission);
7783
- const missionMdPath = resolve26(missionDir, "mission.md");
8238
+ const missionDir = resolve27(baseDir, options.mission);
8239
+ const missionMdPath = resolve27(missionDir, "mission.md");
7784
8240
  if (!await fileExists(missionDir) || !await fileExists(missionMdPath)) {
7785
8241
  throw new Error(
7786
8242
  `Mission "${options.mission}" not found at ${missionDir}.`
@@ -7800,32 +8256,32 @@ init_config2();
7800
8256
  init_config2();
7801
8257
  init_fs();
7802
8258
  import {
7803
- cp,
8259
+ cp as cp2,
7804
8260
  readdir as readdir8,
7805
8261
  symlink,
7806
8262
  lstat,
7807
- readFile as readFile12,
8263
+ readFile as readFile13,
7808
8264
  readlink,
7809
- rm as rm2,
7810
- unlink as unlink4,
7811
- writeFile as writeFile4
8265
+ rm as rm3,
8266
+ unlink as unlink5,
8267
+ writeFile as writeFile5
7812
8268
  } from "fs/promises";
7813
8269
  import { existsSync } from "fs";
7814
8270
  import { homedir as homedir3 } from "os";
7815
- import { basename, dirname as dirname6, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve28 } from "path";
8271
+ import { basename, dirname as dirname6, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve29 } from "path";
7816
8272
 
7817
8273
  // src/utils/package-root.ts
7818
8274
  init_fs();
7819
- import { dirname as dirname5, resolve as resolve27 } from "path";
8275
+ import { dirname as dirname5, resolve as resolve28 } from "path";
7820
8276
  import { fileURLToPath as fileURLToPath3 } from "url";
7821
8277
  async function findPackageRoot(expectedRelativePath) {
7822
8278
  let currentDir = dirname5(fileURLToPath3(import.meta.url));
7823
8279
  while (true) {
7824
- const candidate = resolve27(currentDir, expectedRelativePath);
8280
+ const candidate = resolve28(currentDir, expectedRelativePath);
7825
8281
  if (await fileExists(candidate)) {
7826
8282
  return currentDir;
7827
8283
  }
7828
- const parentDir = resolve27(currentDir, "..");
8284
+ const parentDir = resolve28(currentDir, "..");
7829
8285
  if (parentDir === currentDir) {
7830
8286
  throw new Error(
7831
8287
  `Could not locate package root containing ${expectedRelativePath}.`
@@ -7846,25 +8302,25 @@ function getPluginManifestRelativePath(pluginKind) {
7846
8302
  }
7847
8303
  function getDefaultPluginTargetDir(pluginKind) {
7848
8304
  const home = homedir3();
7849
- return pluginKind === "claude" ? resolve28(home, ".claude", "plugins", "syntaur") : resolve28(home, "plugins", "syntaur");
8305
+ return pluginKind === "claude" ? resolve29(home, ".claude", "plugins", "syntaur") : resolve29(home, "plugins", "syntaur");
7850
8306
  }
7851
8307
  function getDefaultMarketplacePath() {
7852
- return resolve28(homedir3(), ".agents", "plugins", "marketplace.json");
8308
+ return resolve29(homedir3(), ".agents", "plugins", "marketplace.json");
7853
8309
  }
7854
8310
  function getClaudeMarketplacesRoot() {
7855
- return resolve28(homedir3(), ".claude", "plugins", "marketplaces");
8311
+ return resolve29(homedir3(), ".claude", "plugins", "marketplaces");
7856
8312
  }
7857
8313
  function getClaudeKnownMarketplacesPath() {
7858
- return resolve28(homedir3(), ".claude", "plugins", "known_marketplaces.json");
8314
+ return resolve29(homedir3(), ".claude", "plugins", "known_marketplaces.json");
7859
8315
  }
7860
8316
  function getClaudeInstalledPluginsPath() {
7861
- return resolve28(homedir3(), ".claude", "plugins", "installed_plugins.json");
8317
+ return resolve29(homedir3(), ".claude", "plugins", "installed_plugins.json");
7862
8318
  }
7863
8319
  function getInstallMarkerPath(targetDir) {
7864
- return resolve28(targetDir, INSTALL_MARKER_FILENAME);
8320
+ return resolve29(targetDir, INSTALL_MARKER_FILENAME);
7865
8321
  }
7866
8322
  async function readPackageManifest(packageRoot) {
7867
- const raw = await readFile12(resolve28(packageRoot, "package.json"), "utf-8");
8323
+ const raw = await readFile13(resolve29(packageRoot, "package.json"), "utf-8");
7868
8324
  return JSON.parse(raw);
7869
8325
  }
7870
8326
  async function readJsonFileIfExists(pathValue) {
@@ -7872,7 +8328,7 @@ async function readJsonFileIfExists(pathValue) {
7872
8328
  return null;
7873
8329
  }
7874
8330
  try {
7875
- const raw = await readFile12(pathValue, "utf-8");
8331
+ const raw = await readFile13(pathValue, "utf-8");
7876
8332
  return JSON.parse(raw);
7877
8333
  } catch {
7878
8334
  return null;
@@ -7880,15 +8336,15 @@ async function readJsonFileIfExists(pathValue) {
7880
8336
  }
7881
8337
  async function readClaudePluginManifest(pluginDir) {
7882
8338
  return await readJsonFileIfExists(
7883
- resolve28(pluginDir, ".claude-plugin", "plugin.json")
8339
+ resolve29(pluginDir, ".claude-plugin", "plugin.json")
7884
8340
  ) ?? {};
7885
8341
  }
7886
8342
  async function readPluginManifestName(targetDir, pluginKind) {
7887
- const manifestPath = resolve28(targetDir, getPluginManifestRelativePath(pluginKind));
8343
+ const manifestPath = resolve29(targetDir, getPluginManifestRelativePath(pluginKind));
7888
8344
  if (!await fileExists(manifestPath)) {
7889
8345
  return void 0;
7890
8346
  }
7891
- const raw = await readFile12(manifestPath, "utf-8");
8347
+ const raw = await readFile13(manifestPath, "utf-8");
7892
8348
  const parsed = JSON.parse(raw);
7893
8349
  return parsed.name;
7894
8350
  }
@@ -7898,7 +8354,7 @@ async function readInstallMetadata(targetDir) {
7898
8354
  return null;
7899
8355
  }
7900
8356
  try {
7901
- const raw = await readFile12(markerPath, "utf-8");
8357
+ const raw = await readFile13(markerPath, "utf-8");
7902
8358
  return JSON.parse(raw);
7903
8359
  } catch {
7904
8360
  return null;
@@ -7911,7 +8367,7 @@ async function getInstallStatus(targetDir, pluginKind) {
7911
8367
  const info = await lstat(targetDir);
7912
8368
  if (info.isSymbolicLink()) {
7913
8369
  const symlinkTarget = await readlink(targetDir);
7914
- const resolvedTarget = resolve28(dirname6(targetDir), symlinkTarget);
8370
+ const resolvedTarget = resolve29(dirname6(targetDir), symlinkTarget);
7915
8371
  const manifestName2 = await readPluginManifestName(resolvedTarget, pluginKind);
7916
8372
  return {
7917
8373
  exists: true,
@@ -7940,7 +8396,7 @@ async function writeInstallMetadata(targetDir, pluginKind, installMode, packageM
7940
8396
  installMode,
7941
8397
  installedAt: (/* @__PURE__ */ new Date()).toISOString()
7942
8398
  };
7943
- await writeFile4(
8399
+ await writeFile5(
7944
8400
  getInstallMarkerPath(targetDir),
7945
8401
  `${JSON.stringify(metadata, null, 2)}
7946
8402
  `,
@@ -7949,20 +8405,20 @@ async function writeInstallMetadata(targetDir, pluginKind, installMode, packageM
7949
8405
  }
7950
8406
  async function installCopy(paths, pluginKind) {
7951
8407
  await ensureDir(dirname6(paths.targetDir));
7952
- await cp(paths.sourceDir, paths.targetDir, { recursive: true });
8408
+ await cp2(paths.sourceDir, paths.targetDir, { recursive: true });
7953
8409
  const packageManifest = await readPackageManifest(paths.packageRoot);
7954
8410
  await writeInstallMetadata(paths.targetDir, pluginKind, "copy", packageManifest);
7955
8411
  }
7956
8412
  async function installLink(paths) {
7957
8413
  await ensureDir(dirname6(paths.targetDir));
7958
- await rm2(paths.targetDir, { recursive: true, force: true });
8414
+ await rm3(paths.targetDir, { recursive: true, force: true });
7959
8415
  await ensureDir(dirname6(paths.targetDir));
7960
- await symlink(resolve28(paths.sourceDir), paths.targetDir, "dir");
8416
+ await symlink(resolve29(paths.sourceDir), paths.targetDir, "dir");
7961
8417
  }
7962
8418
  async function removeInstallMarker(targetDir) {
7963
8419
  const markerPath = getInstallMarkerPath(targetDir);
7964
8420
  if (await fileExists(markerPath)) {
7965
- await unlink4(markerPath).catch(() => {
8421
+ await unlink5(markerPath).catch(() => {
7966
8422
  });
7967
8423
  }
7968
8424
  }
@@ -7971,13 +8427,13 @@ function normalizeAbsoluteInstallPath(pathValue, label) {
7971
8427
  if (!isAbsolute2(expanded)) {
7972
8428
  throw new Error(`${label} must be an absolute path.`);
7973
8429
  }
7974
- return resolve28(expanded);
8430
+ return resolve29(expanded);
7975
8431
  }
7976
8432
  async function resolvePluginPaths(pluginKind, targetDir) {
7977
8433
  const packageRoot = await findPackageRoot(getPluginRelativePath(pluginKind));
7978
8434
  return {
7979
8435
  packageRoot,
7980
- sourceDir: resolve28(packageRoot, getPluginRelativePath(pluginKind)),
8436
+ sourceDir: resolve29(packageRoot, getPluginRelativePath(pluginKind)),
7981
8437
  targetDir: targetDir ?? getDefaultPluginTargetDir(pluginKind)
7982
8438
  };
7983
8439
  }
@@ -8012,7 +8468,7 @@ async function readClaudeMarketplaceFile(manifestPath) {
8012
8468
  }
8013
8469
  async function writeClaudeMarketplaceFile(manifestPath, marketplace) {
8014
8470
  await ensureDir(dirname6(manifestPath));
8015
- await writeFile4(manifestPath, `${JSON.stringify(marketplace, null, 2)}
8471
+ await writeFile5(manifestPath, `${JSON.stringify(marketplace, null, 2)}
8016
8472
  `, "utf-8");
8017
8473
  }
8018
8474
  function buildClaudeMarketplaceSourcePath(pluginTargetDir, marketplaceRootDir) {
@@ -8081,8 +8537,8 @@ async function listClaudeMarketplaceCandidates() {
8081
8537
  if (!entry.isDirectory()) {
8082
8538
  continue;
8083
8539
  }
8084
- const candidateRoot = resolve28(rootDir, entry.name);
8085
- const manifestPath = resolve28(candidateRoot, ".claude-plugin", "marketplace.json");
8540
+ const candidateRoot = resolve29(rootDir, entry.name);
8541
+ const manifestPath = resolve29(candidateRoot, ".claude-plugin", "marketplace.json");
8086
8542
  if (!await fileExists(manifestPath)) {
8087
8543
  continue;
8088
8544
  }
@@ -8109,11 +8565,11 @@ async function listClaudeMarketplaceCandidates() {
8109
8565
  if (!installLocation) {
8110
8566
  continue;
8111
8567
  }
8112
- const candidateRoot = resolve28(expandHome(installLocation));
8568
+ const candidateRoot = resolve29(expandHome(installLocation));
8113
8569
  if (seen.has(candidateRoot)) {
8114
8570
  continue;
8115
8571
  }
8116
- const manifestPath = resolve28(candidateRoot, ".claude-plugin", "marketplace.json");
8572
+ const manifestPath = resolve29(candidateRoot, ".claude-plugin", "marketplace.json");
8117
8573
  if (!await fileExists(manifestPath)) {
8118
8574
  continue;
8119
8575
  }
@@ -8141,7 +8597,7 @@ async function getPreferredClaudeMarketplace() {
8141
8597
  name: candidate.name,
8142
8598
  rootDir: candidate.rootDir,
8143
8599
  manifestPath: candidate.manifestPath,
8144
- targetDir: resolve28(candidate.rootDir, "plugins", "syntaur")
8600
+ targetDir: resolve29(candidate.rootDir, "plugins", "syntaur")
8145
8601
  };
8146
8602
  }
8147
8603
  async function detectClaudeMarketplaceForTarget(targetDir) {
@@ -8151,7 +8607,7 @@ async function detectClaudeMarketplaceForTarget(targetDir) {
8151
8607
  return null;
8152
8608
  }
8153
8609
  const rootDir = dirname6(pluginsDir);
8154
- const manifestPath = resolve28(rootDir, ".claude-plugin", "marketplace.json");
8610
+ const manifestPath = resolve29(rootDir, ".claude-plugin", "marketplace.json");
8155
8611
  if (!await fileExists(manifestPath)) {
8156
8612
  return null;
8157
8613
  }
@@ -8167,7 +8623,7 @@ async function detectClaudeMarketplaceForTarget(targetDir) {
8167
8623
  async function findManagedClaudeMarketplacePluginDir() {
8168
8624
  const marketplaces = await listClaudeMarketplaceCandidates();
8169
8625
  for (const marketplace of marketplaces) {
8170
- const targetDir = resolve28(marketplace.rootDir, "plugins", "syntaur");
8626
+ const targetDir = resolve29(marketplace.rootDir, "plugins", "syntaur");
8171
8627
  const status = await getInstallStatus(targetDir, "claude");
8172
8628
  if (status.exists && status.managed) {
8173
8629
  return targetDir;
@@ -8292,7 +8748,7 @@ async function installManagedPlugin(options) {
8292
8748
  `${paths.targetDir} already exists and is not a Syntaur-managed install. Remove it manually before installing Syntaur there.`
8293
8749
  );
8294
8750
  }
8295
- if (desiredMode === "link" && existing.exists && existing.installMode === "link" && existing.symlinkTarget === resolve28(paths.sourceDir) && !force) {
8751
+ if (desiredMode === "link" && existing.exists && existing.installMode === "link" && existing.symlinkTarget === resolve29(paths.sourceDir) && !force) {
8296
8752
  return {
8297
8753
  targetDir: paths.targetDir,
8298
8754
  sourceDir: paths.sourceDir,
@@ -8301,7 +8757,7 @@ async function installManagedPlugin(options) {
8301
8757
  };
8302
8758
  }
8303
8759
  if (existing.exists) {
8304
- await rm2(paths.targetDir, { recursive: true, force: true });
8760
+ await rm3(paths.targetDir, { recursive: true, force: true });
8305
8761
  }
8306
8762
  if (desiredMode === "link") {
8307
8763
  await installLink(paths);
@@ -8344,7 +8800,7 @@ async function readMarketplaceFile(marketplacePath) {
8344
8800
  plugins: []
8345
8801
  };
8346
8802
  }
8347
- const raw = await readFile12(marketplacePath, "utf-8");
8803
+ const raw = await readFile13(marketplacePath, "utf-8");
8348
8804
  const parsed = JSON.parse(raw);
8349
8805
  return {
8350
8806
  name: parsed.name ?? "local",
@@ -8354,7 +8810,7 @@ async function readMarketplaceFile(marketplacePath) {
8354
8810
  }
8355
8811
  async function writeMarketplaceFile(marketplacePath, marketplace) {
8356
8812
  await ensureDir(dirname6(marketplacePath));
8357
- await writeFile4(marketplacePath, `${JSON.stringify(marketplace, null, 2)}
8813
+ await writeFile5(marketplacePath, `${JSON.stringify(marketplace, null, 2)}
8358
8814
  `, "utf-8");
8359
8815
  }
8360
8816
  async function hasAnySyntaurMarketplaceEntry(marketplacePath) {
@@ -8440,7 +8896,7 @@ async function removeMarketplaceEntry(options) {
8440
8896
  return { marketplacePath, removed: false };
8441
8897
  }
8442
8898
  if (marketplace.plugins.length === 0 && isDefaultMarketplaceShell(marketplace)) {
8443
- await rm2(marketplacePath, { force: true });
8899
+ await rm3(marketplacePath, { force: true });
8444
8900
  return { marketplacePath, removed: true };
8445
8901
  }
8446
8902
  await writeMarketplaceFile(marketplacePath, marketplace);
@@ -8461,7 +8917,7 @@ async function uninstallManagedPlugin(pluginKind, targetDir = getDefaultPluginTa
8461
8917
  );
8462
8918
  }
8463
8919
  await removeInstallMarker(normalizedTarget);
8464
- await rm2(normalizedTarget, { recursive: true, force: true });
8920
+ await rm3(normalizedTarget, { recursive: true, force: true });
8465
8921
  return { removed: true, targetDir: normalizedTarget };
8466
8922
  }
8467
8923
  async function getConfiguredOrLegacyManagedPluginDir(pluginKind) {
@@ -8505,13 +8961,13 @@ async function recommendMarketplacePath() {
8505
8961
  return configuredOrManaged ?? getDefaultMarketplacePath();
8506
8962
  }
8507
8963
  async function isSyntaurDataInstalled() {
8508
- return fileExists(resolve28(syntaurRoot(), "config.md"));
8964
+ return fileExists(resolve29(syntaurRoot(), "config.md"));
8509
8965
  }
8510
8966
  async function removeSyntaurData() {
8511
- await rm2(syntaurRoot(), { recursive: true, force: true });
8967
+ await rm3(syntaurRoot(), { recursive: true, force: true });
8512
8968
  }
8513
8969
  async function getConfiguredMissionDir() {
8514
- if (!await fileExists(resolve28(syntaurRoot(), "config.md"))) {
8970
+ if (!await fileExists(resolve29(syntaurRoot(), "config.md"))) {
8515
8971
  return null;
8516
8972
  }
8517
8973
  return (await readConfig()).defaultMissionDir;
@@ -8857,7 +9313,7 @@ async function setupCommand(options) {
8857
9313
  }
8858
9314
 
8859
9315
  // src/commands/uninstall.ts
8860
- import { resolve as resolve29 } from "path";
9316
+ import { resolve as resolve30 } from "path";
8861
9317
  init_paths();
8862
9318
  function expandTargets(options) {
8863
9319
  if (options.all) {
@@ -8937,7 +9393,7 @@ async function uninstallCommand(options) {
8937
9393
  const configuredMissionDir = await getConfiguredMissionDir();
8938
9394
  await removeSyntaurData();
8939
9395
  console.log(`Removed ${syntaurRoot()}`);
8940
- if (configuredMissionDir && resolve29(configuredMissionDir) !== resolve29(syntaurRoot(), "missions")) {
9396
+ if (configuredMissionDir && resolve30(configuredMissionDir) !== resolve30(syntaurRoot(), "missions")) {
8941
9397
  console.warn(
8942
9398
  `Warning: config.md pointed to an external mission directory (${configuredMissionDir}). That directory was not removed automatically.`
8943
9399
  );
@@ -8952,7 +9408,7 @@ async function uninstallCommand(options) {
8952
9408
  init_paths();
8953
9409
  init_fs();
8954
9410
  init_config2();
8955
- import { resolve as resolve30 } from "path";
9411
+ import { resolve as resolve31 } from "path";
8956
9412
  var SUPPORTED_FRAMEWORKS = ["cursor", "codex", "opencode"];
8957
9413
  async function setupAdapterCommand(framework, options) {
8958
9414
  if (!SUPPORTED_FRAMEWORKS.includes(framework)) {
@@ -8978,19 +9434,19 @@ async function setupAdapterCommand(framework, options) {
8978
9434
  }
8979
9435
  const config = await readConfig();
8980
9436
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultMissionDir;
8981
- const missionDir = resolve30(baseDir, options.mission);
8982
- const assignmentDir = resolve30(
9437
+ const missionDir = resolve31(baseDir, options.mission);
9438
+ const assignmentDir = resolve31(
8983
9439
  missionDir,
8984
9440
  "assignments",
8985
9441
  options.assignment
8986
9442
  );
8987
- const missionMdPath = resolve30(missionDir, "mission.md");
9443
+ const missionMdPath = resolve31(missionDir, "mission.md");
8988
9444
  if (!await fileExists(missionDir) || !await fileExists(missionMdPath)) {
8989
9445
  throw new Error(
8990
9446
  `Mission "${options.mission}" not found at ${missionDir}.`
8991
9447
  );
8992
9448
  }
8993
- const assignmentMdPath = resolve30(assignmentDir, "assignment.md");
9449
+ const assignmentMdPath = resolve31(assignmentDir, "assignment.md");
8994
9450
  if (!await fileExists(assignmentDir) || !await fileExists(assignmentMdPath)) {
8995
9451
  throw new Error(
8996
9452
  `Assignment "${options.assignment}" not found at ${assignmentDir}.`
@@ -9018,15 +9474,15 @@ async function setupAdapterCommand(framework, options) {
9018
9474
  }
9019
9475
  }
9020
9476
  if (framework === "cursor") {
9021
- const protocolPath = resolve30(cwd, ".cursor", "rules", "syntaur-protocol.mdc");
9022
- const assignmentPath = resolve30(cwd, ".cursor", "rules", "syntaur-assignment.mdc");
9477
+ const protocolPath = resolve31(cwd, ".cursor", "rules", "syntaur-protocol.mdc");
9478
+ const assignmentPath = resolve31(cwd, ".cursor", "rules", "syntaur-assignment.mdc");
9023
9479
  await writeAdapterFile(protocolPath, renderCursorProtocol());
9024
9480
  await writeAdapterFile(assignmentPath, renderCursorAssignment(rendererParams));
9025
9481
  } else if (framework === "codex" || framework === "opencode") {
9026
- const agentsPath = resolve30(cwd, "AGENTS.md");
9482
+ const agentsPath = resolve31(cwd, "AGENTS.md");
9027
9483
  await writeAdapterFile(agentsPath, renderCodexAgents(rendererParams));
9028
9484
  if (framework === "opencode") {
9029
- const configPath = resolve30(cwd, "opencode.json");
9485
+ const configPath = resolve31(cwd, "opencode.json");
9030
9486
  await writeAdapterFile(configPath, renderOpenCodeConfig({ missionDir }));
9031
9487
  }
9032
9488
  }
@@ -9051,7 +9507,7 @@ async function setupAdapterCommand(framework, options) {
9051
9507
  init_paths();
9052
9508
  init_fs();
9053
9509
  init_config2();
9054
- import { resolve as resolve31 } from "path";
9510
+ import { resolve as resolve32 } from "path";
9055
9511
  import { randomUUID as randomUUID3 } from "crypto";
9056
9512
  async function trackSessionCommand(options) {
9057
9513
  if (!options.agent) {
@@ -9060,7 +9516,7 @@ async function trackSessionCommand(options) {
9060
9516
  if (options.mission) {
9061
9517
  const config = await readConfig();
9062
9518
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultMissionDir;
9063
- const missionDir = resolve31(baseDir, options.mission);
9519
+ const missionDir = resolve32(baseDir, options.mission);
9064
9520
  if (!await fileExists(missionDir)) {
9065
9521
  throw new Error(
9066
9522
  `Mission "${options.mission}" not found at ${missionDir}.`
@@ -9112,7 +9568,7 @@ async function browseCommand(options) {
9112
9568
  }
9113
9569
 
9114
9570
  // src/commands/create-playbook.ts
9115
- import { resolve as resolve33 } from "path";
9571
+ import { resolve as resolve34 } from "path";
9116
9572
  init_timestamp();
9117
9573
  init_paths();
9118
9574
  init_fs();
@@ -9128,7 +9584,7 @@ async function createPlaybookCommand(name, options) {
9128
9584
  }
9129
9585
  const dir = playbooksDir();
9130
9586
  await ensureDir(dir);
9131
- const filePath = resolve33(dir, `${slug}.md`);
9587
+ const filePath = resolve34(dir, `${slug}.md`);
9132
9588
  if (await fileExists(filePath)) {
9133
9589
  throw new Error(
9134
9590
  `Playbook "${slug}" already exists at ${filePath}
@@ -9149,8 +9605,8 @@ Use --slug to specify a different slug.`
9149
9605
  init_paths();
9150
9606
  init_fs();
9151
9607
  init_parser();
9152
- import { readdir as readdir9, readFile as readFile13 } from "fs/promises";
9153
- import { resolve as resolve34 } from "path";
9608
+ import { readdir as readdir9, readFile as readFile14 } from "fs/promises";
9609
+ import { resolve as resolve35 } from "path";
9154
9610
  async function listPlaybooksCommand() {
9155
9611
  const dir = playbooksDir();
9156
9612
  if (!await fileExists(dir)) {
@@ -9168,8 +9624,8 @@ async function listPlaybooksCommand() {
9168
9624
  console.log(`${"Slug".padEnd(30)} ${"Name".padEnd(30)} Description`);
9169
9625
  console.log(`${"\u2500".repeat(30)} ${"\u2500".repeat(30)} ${"\u2500".repeat(40)}`);
9170
9626
  for (const entry of mdFiles) {
9171
- const filePath = resolve34(dir, entry.name);
9172
- const raw = await readFile13(filePath, "utf-8");
9627
+ const filePath = resolve35(dir, entry.name);
9628
+ const raw = await readFile14(filePath, "utf-8");
9173
9629
  const parsed = parsePlaybook(raw);
9174
9630
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
9175
9631
  const name = parsed.name || slug;
@@ -9183,8 +9639,8 @@ init_paths();
9183
9639
  init_parser2();
9184
9640
  init_fs();
9185
9641
  import { Command } from "commander";
9186
- import { readFile as readFile14 } from "fs/promises";
9187
- import { resolve as resolve35 } from "path";
9642
+ import { readFile as readFile15 } from "fs/promises";
9643
+ import { resolve as resolve36 } from "path";
9188
9644
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
9189
9645
  function resolveWorkspace(options) {
9190
9646
  if (options.global) return "_global";
@@ -9465,10 +9921,10 @@ todoCommand.command("archive").description("Archive completed todos and their lo
9465
9921
  (e) => e.itemIds.every((id) => completedIds.has(id))
9466
9922
  );
9467
9923
  const archFile = archivePath(todosPath, workspace, checklist.archiveInterval);
9468
- await ensureDir(resolve35(todosPath, "archive"));
9924
+ await ensureDir(resolve36(todosPath, "archive"));
9469
9925
  let archContent = "";
9470
9926
  if (await fileExists(archFile)) {
9471
- archContent = await readFile14(archFile, "utf-8");
9927
+ archContent = await readFile15(archFile, "utf-8");
9472
9928
  archContent = archContent.trimEnd() + "\n\n";
9473
9929
  } else {
9474
9930
  archContent = `---
@@ -9575,6 +10031,83 @@ todoCommand.command("promote").description("Promote a todo to a full assignment"
9575
10031
  }
9576
10032
  });
9577
10033
 
10034
+ // src/commands/backup.ts
10035
+ init_config2();
10036
+ import { Command as Command2 } from "commander";
10037
+ function parseCategoryOption(csv) {
10038
+ if (!csv) return void 0;
10039
+ const parts = csv.split(",").map((s) => s.trim()).filter(Boolean);
10040
+ if (parts.length === 0) {
10041
+ throw new Error(`No categories provided. Valid: ${VALID_CATEGORIES.join(", ")}`);
10042
+ }
10043
+ return parseCategoriesStrict(parts);
10044
+ }
10045
+ var backupCommand = new Command2("backup").description("Back up Syntaur files to a GitHub repository");
10046
+ backupCommand.command("push").description("Push a backup to the configured GitHub repo").option("--repo <url>", "Override the configured repo URL").option("--categories <list>", "Comma-separated categories to back up (missions, playbooks, todos, servers, config)").action(async (options) => {
10047
+ try {
10048
+ const result = await backupToGithub({
10049
+ repo: options.repo,
10050
+ categories: parseCategoryOption(options.categories)
10051
+ });
10052
+ console.log(result.message);
10053
+ if (result.committed) {
10054
+ console.log(` timestamp: ${result.timestamp}`);
10055
+ }
10056
+ } catch (error) {
10057
+ console.error("Error:", error instanceof Error ? error.message : String(error));
10058
+ process.exit(1);
10059
+ }
10060
+ });
10061
+ backupCommand.command("pull").description("Restore Syntaur files from the configured GitHub repo").option("--repo <url>", "Override the configured repo URL").option("--categories <list>", "Comma-separated categories to restore").action(async (options) => {
10062
+ try {
10063
+ const result = await restoreFromGithub({
10064
+ repo: options.repo,
10065
+ categories: parseCategoryOption(options.categories)
10066
+ });
10067
+ console.log(result.message);
10068
+ console.log(` timestamp: ${result.timestamp}`);
10069
+ } catch (error) {
10070
+ console.error("Error:", error instanceof Error ? error.message : String(error));
10071
+ process.exit(1);
10072
+ }
10073
+ });
10074
+ backupCommand.command("config").description("Show or update backup configuration").option("--repo <url>", "Set the backup repo URL").option("--categories <list>", "Set the default categories (comma-separated)").action(async (options) => {
10075
+ try {
10076
+ const updates = {};
10077
+ if (options.repo !== void 0) {
10078
+ const trimmed = typeof options.repo === "string" ? options.repo.trim() : options.repo;
10079
+ if (!validateRepoUrl(trimmed)) {
10080
+ throw new Error(`Invalid repo URL: "${options.repo}". Must start with https:// or git@.`);
10081
+ }
10082
+ updates.repo = trimmed;
10083
+ }
10084
+ if (options.categories !== void 0) {
10085
+ const parts = options.categories.split(",").map((s) => s.trim()).filter(Boolean);
10086
+ if (parts.length === 0) {
10087
+ throw new Error(`No categories provided. Valid: ${VALID_CATEGORIES.join(", ")}`);
10088
+ }
10089
+ const valid = parseCategoriesStrict(parts);
10090
+ updates.categories = valid.join(", ");
10091
+ }
10092
+ if (Object.keys(updates).length > 0) {
10093
+ await updateBackupConfig(updates);
10094
+ console.log("Backup configuration updated.");
10095
+ }
10096
+ const status = await getBackupStatus();
10097
+ console.log("\nBackup configuration:");
10098
+ console.log(` repo: ${status.repo ?? "(not set)"}`);
10099
+ console.log(` categories: ${status.categories}`);
10100
+ console.log(` lastBackup: ${status.lastBackup ?? "(never)"}`);
10101
+ console.log(` lastRestore: ${status.lastRestore ?? "(never)"}`);
10102
+ if (status.locked) {
10103
+ console.log(" \u26A0 locked: a backup operation is in progress or the lock is stale");
10104
+ }
10105
+ } catch (error) {
10106
+ console.error("Error:", error instanceof Error ? error.message : String(error));
10107
+ process.exit(1);
10108
+ }
10109
+ });
10110
+
9578
10111
  // src/cli-default-command.ts
9579
10112
  init_config2();
9580
10113
  import { readdir as readdir10 } from "fs/promises";
@@ -9614,7 +10147,7 @@ async function getDefaultCommandName() {
9614
10147
  }
9615
10148
 
9616
10149
  // src/index.ts
9617
- var program = new Command2();
10150
+ var program = new Command3();
9618
10151
  program.name("syntaur").description("CLI scaffolding tool for the Syntaur protocol").version("0.1.0");
9619
10152
  program.command("init").description("Initialize ~/.syntaur/ directory structure and config").option("--force", "Overwrite existing config file").action(async (options) => {
9620
10153
  try {
@@ -9856,6 +10389,7 @@ program.command("list-playbooks").description("List all playbooks").action(async
9856
10389
  }
9857
10390
  });
9858
10391
  program.addCommand(todoCommand);
10392
+ program.addCommand(backupCommand);
9859
10393
  if (process.argv.length <= 2) {
9860
10394
  process.argv.push(await getDefaultCommandName());
9861
10395
  }