titan-agent 6.2.1 → 6.2.2

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 (148) hide show
  1. package/dist/gateway/routes/update.js +100 -0
  2. package/dist/gateway/routes/update.js.map +1 -0
  3. package/dist/gateway/server.js +2 -65
  4. package/dist/gateway/server.js.map +1 -1
  5. package/dist/utils/constants.js +1 -1
  6. package/dist/utils/constants.js.map +1 -1
  7. package/package.json +1 -1
  8. package/ui/dist/assets/{AuditPanel-DuxwAH5U.js → AuditPanel-BmFAKA7p.js} +1 -1
  9. package/ui/dist/assets/{AutonomyPanel-VWBgurnv.js → AutonomyPanel-BzuCEsiG.js} +1 -1
  10. package/ui/dist/assets/{AutopilotPanel-Dw8MYOtj.js → AutopilotPanel-DA1vFEl4.js} +1 -1
  11. package/ui/dist/assets/{AutoresearchPanel-CpEX0Zub.js → AutoresearchPanel-BeUCXYtX.js} +1 -1
  12. package/ui/dist/assets/{BackupPanel-n41_6i6x.js → BackupPanel-B6N1pfMT.js} +1 -1
  13. package/ui/dist/assets/{BrowserPanel-DEBTIt6h.js → BrowserPanel-BGHaPi3O.js} +1 -1
  14. package/ui/dist/assets/{CPActivity-em82PTiQ.js → CPActivity-BUSwjFj2.js} +1 -1
  15. package/ui/dist/assets/{CPAgentDetail-Bd8lKqcL.js → CPAgentDetail-DlUEVujH.js} +1 -1
  16. package/ui/dist/assets/{CPAgents-GRBrDd6c.js → CPAgents-CnRmsSMo.js} +1 -1
  17. package/ui/dist/assets/{CPApprovals-DdLaCgJZ.js → CPApprovals-CXdkzebd.js} +1 -1
  18. package/ui/dist/assets/{CPCosts-Cm8Hv9fv.js → CPCosts-CfcyIUZg.js} +1 -1
  19. package/ui/dist/assets/{CPDashboard-Dm9vUKLu.js → CPDashboard-DHlfE1mT.js} +1 -1
  20. package/ui/dist/assets/{CPFiles-DxMn1x2m.js → CPFiles-DyV9vsjE.js} +1 -1
  21. package/ui/dist/assets/{CPGoals-Wr1tCn6y.js → CPGoals-C17G2F0e.js} +1 -1
  22. package/ui/dist/assets/{CPInbox-CGYpK1rG.js → CPInbox-D5A3ja_Q.js} +1 -1
  23. package/ui/dist/assets/{CPIssueDetail-dTU59sgQ.js → CPIssueDetail-CoPv27Ll.js} +1 -1
  24. package/ui/dist/assets/{CPIssues-wgjI2eqS.js → CPIssues-C9EdfW1J.js} +1 -1
  25. package/ui/dist/assets/CPLayout-CHrlktMs.js +2 -0
  26. package/ui/dist/assets/{CPOrg-eT6LNmyy.js → CPOrg-DzoVDLKI.js} +1 -1
  27. package/ui/dist/assets/{CPRuns-B3sdo5g2.js → CPRuns-BHAyFv0K.js} +1 -1
  28. package/ui/dist/assets/{CPSocial-B99Mfee_.js → CPSocial-CjHD8dKM.js} +1 -1
  29. package/ui/dist/assets/{CPVoice-D4J6yFWu.js → CPVoice-aybMotU5.js} +1 -1
  30. package/ui/dist/assets/{ChannelsPanel--lhZqYLD.js → ChannelsPanel-8KfvAEYM.js} +1 -1
  31. package/ui/dist/assets/{CheckpointsPanel-DaOIUcEi.js → CheckpointsPanel-BCyLRLcg.js} +1 -1
  32. package/ui/dist/assets/{CommandPostHub-Pq9lQMW5.js → CommandPostHub-Ce-mWsu2.js} +3 -3
  33. package/ui/dist/assets/{CronPanel-CywjWM7O.js → CronPanel-A9JHvU_O.js} +1 -1
  34. package/ui/dist/assets/{DataTable-Bt0cxehB.js → DataTable-ol6IZMRj.js} +1 -1
  35. package/ui/dist/assets/{DreamPanel-Ct7MuLzD.js → DreamPanel-ZiOF8ybh.js} +1 -1
  36. package/ui/dist/assets/{EmptyState-iLOrbdU7.js → EmptyState-B1i1ZK0x.js} +1 -1
  37. package/ui/dist/assets/{EvalHarnessPanel-7KKDzeXw.js → EvalHarnessPanel-DRGc1zbp.js} +1 -1
  38. package/ui/dist/assets/{EvalPanel-70LzkLDO.js → EvalPanel-B_fiFImG.js} +1 -1
  39. package/ui/dist/assets/{FileViewer-h6A7YYTm.js → FileViewer-D14G6MQq.js} +1 -1
  40. package/ui/dist/assets/{FilesPanel-l8qC6eLD.js → FilesPanel-BIsMoSfg.js} +1 -1
  41. package/ui/dist/assets/{FleetPanel-Z3ResHIb.js → FleetPanel-CSjOOsbX.js} +1 -1
  42. package/ui/dist/assets/{HomelabPanel-9QhrXzo1.js → HomelabPanel-XHMxqiHu.js} +1 -1
  43. package/ui/dist/assets/InfraView-DNYgRXkB.js +2 -0
  44. package/ui/dist/assets/{InlineEditableField-DsVfGRH2.js → InlineEditableField-AOS-LBh_.js} +1 -1
  45. package/ui/dist/assets/{Input-Bzh0-cXU.js → Input-CTK9oH9g.js} +1 -1
  46. package/ui/dist/assets/{IntegrationsPanel-DBOdLYJ8.js → IntegrationsPanel-BKuC_5El.js} +1 -1
  47. package/ui/dist/assets/IntelligenceView-BUKpVpDQ.js +2 -0
  48. package/ui/dist/assets/{LearningPanel-CiSLVxUX.js → LearningPanel-RpDaPvCj.js} +1 -1
  49. package/ui/dist/assets/{LogsPanel-CPAAItsq.js → LogsPanel-9zIJP1Xv.js} +1 -1
  50. package/ui/dist/assets/{McpPanel-MYWE2Zqs.js → McpPanel-CNE7MhDu.js} +1 -1
  51. package/ui/dist/assets/{MemoryGraphPanel-BFr51D_1.js → MemoryGraphPanel-9DEsbOZF.js} +1 -1
  52. package/ui/dist/assets/{MemoryWikiPanel-BA7GUxhg.js → MemoryWikiPanel-CqNen9A6.js} +1 -1
  53. package/ui/dist/assets/{MeshPanel-5BDbwBVR.js → MeshPanel-Bq3pHHIT.js} +1 -1
  54. package/ui/dist/assets/{MissionCanvas-CrDZIQX6.js → MissionCanvas-Ci0hbAF0.js} +1 -1
  55. package/ui/dist/assets/{MissionChat-BDTWMQjj.js → MissionChat-BDqlCplQ.js} +1 -1
  56. package/ui/dist/assets/{MissionLibrary-DEPH2eEu.js → MissionLibrary-9SMdJnfa.js} +1 -1
  57. package/ui/dist/assets/{MissionStart-COLFZLHB.js → MissionStart-Cm7C3kyo.js} +1 -1
  58. package/ui/dist/assets/{Modal-nvni-LGR.js → Modal-B9KCAwCK.js} +1 -1
  59. package/ui/dist/assets/{NvidiaPanel-BCgVrdWt.js → NvidiaPanel-CpGBj1sn.js} +1 -1
  60. package/ui/dist/assets/{OrganismPanel-Cab4MV9i.js → OrganismPanel-DBHyylxC.js} +1 -1
  61. package/ui/dist/assets/{OverviewPanel-isNUxBki.js → OverviewPanel-DII4H_R7.js} +1 -1
  62. package/ui/dist/assets/{PageHeader-BJ1jHtOy.js → PageHeader-Bzb_WXw0.js} +1 -1
  63. package/ui/dist/assets/{PersonaProfilesPanel-BGiDuDPj.js → PersonaProfilesPanel-CXZTZhjo.js} +1 -1
  64. package/ui/dist/assets/{PersonasPanel-4-USk8fm.js → PersonasPanel-BYbD8XiV.js} +1 -1
  65. package/ui/dist/assets/{PhoneDeskPanel-CUz5YVUY.js → PhoneDeskPanel-Bol7fCCN.js} +1 -1
  66. package/ui/dist/assets/{RecipesPanel-DJd6Otdy.js → RecipesPanel-BRbXRcEn.js} +1 -1
  67. package/ui/dist/assets/{SecurityPanel-CKqTnhNY.js → SecurityPanel-BR3BT7Rb.js} +1 -1
  68. package/ui/dist/assets/{SelfImprovePanel-BiCjKpTM.js → SelfImprovePanel-BX9xYBxl.js} +1 -1
  69. package/ui/dist/assets/{SelfProposalsPanel-BQCwEHdF.js → SelfProposalsPanel-BOoDEAYp.js} +1 -1
  70. package/ui/dist/assets/{SessionsPanel-MTMSODP7.js → SessionsPanel-BICnNmn0.js} +1 -1
  71. package/ui/dist/assets/{SessionsTab-CEFFZWMK.js → SessionsTab-X10FeDWA.js} +1 -1
  72. package/ui/dist/assets/{SettingsPanel-CniESkQQ.js → SettingsPanel-MuC32AXw.js} +1 -1
  73. package/ui/dist/assets/SettingsView-Cv9IZqiU.js +2 -0
  74. package/ui/dist/assets/{SkeletonLoader-IpWkLmoy.js → SkeletonLoader-fwI4Z-f0.js} +1 -1
  75. package/ui/dist/assets/{SkillsPanel-CCP-gCR4.js → SkillsPanel-CKFndekU.js} +1 -1
  76. package/ui/dist/assets/{SomaView-BK_thK9H.js → SomaView-Ch7WlmBy.js} +1 -1
  77. package/ui/dist/assets/{StatCard-I0fVw2CH.js → StatCard-Bj2Q_4WS.js} +1 -1
  78. package/ui/dist/assets/{StatusBadge-e3tFf05B.js → StatusBadge-NtdTQ8_L.js} +1 -1
  79. package/ui/dist/assets/{Tabs-vGpPiBzr.js → Tabs-CC9aTVuL.js} +1 -1
  80. package/ui/dist/assets/{TeamsPanel-AFIQBzRK.js → TeamsPanel-DjFcLsdR.js} +1 -1
  81. package/ui/dist/assets/{TelemetryPanel-DKZGVmcp.js → TelemetryPanel-k7gJbZyS.js} +1 -1
  82. package/ui/dist/assets/{TitanCanvas-CqvuuIjd.js → TitanCanvas-MzLkpZhp.js} +25 -25
  83. package/ui/dist/assets/{ToolsView-DnIkrUvP.js → ToolsView-DcxwuM2l.js} +2 -2
  84. package/ui/dist/assets/{Tooltip-DNc5AWi0.js → Tooltip-CFYHfpgQ.js} +1 -1
  85. package/ui/dist/assets/{TraceViewer-MD9YRFVc.js → TraceViewer-BfLO9vwI.js} +1 -1
  86. package/ui/dist/assets/{TrainingPanel-BfXoR7_N.js → TrainingPanel-DUl6qChY.js} +1 -1
  87. package/ui/dist/assets/{VoiceOverlay-Bl81c4yf.js → VoiceOverlay-Dj_cR5_Y.js} +1 -1
  88. package/ui/dist/assets/{VramPanel-Ba8H8Qs2.js → VramPanel-th0_Eo8o.js} +1 -1
  89. package/ui/dist/assets/{WatchView-CoLHPGd7.js → WatchView-DyGhpKbv.js} +1 -1
  90. package/ui/dist/assets/{WorkTab-UppfR_AF.js → WorkTab-BG5VBljJ.js} +1 -1
  91. package/ui/dist/assets/{WorkflowsPanel-CdN7Epxr.js → WorkflowsPanel-C1hVHqAc.js} +1 -1
  92. package/ui/dist/assets/{archive-DWgNklp0.js → archive-C2_hAJXj.js} +1 -1
  93. package/ui/dist/assets/{arrow-left-CW5NV7E0.js → arrow-left-DFCBXvKS.js} +1 -1
  94. package/ui/dist/assets/{book-open-DRI_Y6cW.js → book-open-BgDKDGcH.js} +1 -1
  95. package/ui/dist/assets/{bot-DSGZRM5O.js → bot-C4ICYImG.js} +1 -1
  96. package/ui/dist/assets/{chart-column-D6yK9tts.js → chart-column-BIFJOG_f.js} +1 -1
  97. package/ui/dist/assets/{check-Cxp-6q4k.js → check-CmCL8yzl.js} +1 -1
  98. package/ui/dist/assets/{chevron-right-DkIMxdwH.js → chevron-right-U0Ujye5i.js} +1 -1
  99. package/ui/dist/assets/{chevron-up-BbDu8x1d.js → chevron-up-B6uSBxbj.js} +1 -1
  100. package/ui/dist/assets/{circle-check-big-DnJPBAGU.js → circle-check-big-BFw1nom4.js} +1 -1
  101. package/ui/dist/assets/{circle-stop-DOe0OacY.js → circle-stop-DbtGpdaw.js} +1 -1
  102. package/ui/dist/assets/{clipboard-list-Dj5RQbT8.js → clipboard-list-Dd3_U8cK.js} +1 -1
  103. package/ui/dist/assets/{clock-BS8fh7Rt.js → clock-DpJIc30t.js} +1 -1
  104. package/ui/dist/assets/{dollar-sign-DMXBeaTj.js → dollar-sign-DlrOHVzA.js} +1 -1
  105. package/ui/dist/assets/{download-BykARpbG.js → download-CZ5kTONh.js} +1 -1
  106. package/ui/dist/assets/{external-link-BfXhMi9L.js → external-link-CX-GvMky.js} +1 -1
  107. package/ui/dist/assets/{file-text-CUkSytWL.js → file-text-BtTsNJbB.js} +1 -1
  108. package/ui/dist/assets/{flask-conical-BS4cuENu.js → flask-conical-BaPYppKH.js} +1 -1
  109. package/ui/dist/assets/{folder-BDwoUobN.js → folder-BOnpha2-.js} +1 -1
  110. package/ui/dist/assets/{funnel-xr6Dd1ub.js → funnel-DaNi6kwp.js} +1 -1
  111. package/ui/dist/assets/{git-branch-C0Qk52W8.js → git-branch-gs6g9dSe.js} +1 -1
  112. package/ui/dist/assets/{index-CmaGnHzR.js → index-Dbv5oKsp.js} +2 -2
  113. package/ui/dist/assets/{layers-DPHYdeRk.js → layers-BKZz8iD1.js} +1 -1
  114. package/ui/dist/assets/{legacy-DzagVk5C.js → legacy-DbwFsW2A.js} +1 -1
  115. package/ui/dist/assets/{lightbulb-Bj6cj6Xl.js → lightbulb-DX6YLtPe.js} +1 -1
  116. package/ui/dist/assets/{list-todo-Cm4jNFHV.js → list-todo-qdMNm4-Q.js} +1 -1
  117. package/ui/dist/assets/{message-square-BrWLAovN.js → message-square-Cwo5FOho.js} +1 -1
  118. package/ui/dist/assets/{mic-BXNK4d7X.js → mic-BzvwK4IS.js} +1 -1
  119. package/ui/dist/assets/{missions-DEjMYSre.js → missions-CYlXuJED.js} +1 -1
  120. package/ui/dist/assets/{network-CrQn1GKv.js → network-Dt-RBtO_.js} +1 -1
  121. package/ui/dist/assets/{pause-DS5xgDMT.js → pause-Do3XNLtx.js} +1 -1
  122. package/ui/dist/assets/{pencil-DAI6FtFQ.js → pencil-DAGs9RFs.js} +1 -1
  123. package/ui/dist/assets/{play-DxBy837b.js → play-CNElkzEq.js} +1 -1
  124. package/ui/dist/assets/{plug-D0Bstfm0.js → plug-DCgrVnB8.js} +1 -1
  125. package/ui/dist/assets/{plus-DxgNIt8h.js → plus-BnsXhqXc.js} +1 -1
  126. package/ui/dist/assets/{proxy-KYomRnLW.js → proxy-BXHdgwrk.js} +1 -1
  127. package/ui/dist/assets/{radio-2vniRFTO.js → radio-BPz7sHXW.js} +1 -1
  128. package/ui/dist/assets/{refresh-cw-BFBnnWj8.js → refresh-cw-Cs4oDMNE.js} +1 -1
  129. package/ui/dist/assets/{rotate-ccw-B9wWlKRc.js → rotate-ccw-DSpsA3Wr.js} +1 -1
  130. package/ui/dist/assets/{save--wCYre_u.js → save-BhGg2pZh.js} +1 -1
  131. package/ui/dist/assets/{search-DOD1aOm8.js → search-BtxlIgqN.js} +1 -1
  132. package/ui/dist/assets/{send-Bb57UxT7.js → send-BzKTXI3q.js} +1 -1
  133. package/ui/dist/assets/{server-CBsCfofY.js → server-B1gIgixS.js} +1 -1
  134. package/ui/dist/assets/{shield-check-B24N03VU.js → shield-check-C39B-Wi3.js} +1 -1
  135. package/ui/dist/assets/{terminal-CZ77Swtc.js → terminal-CubaeYlJ.js} +1 -1
  136. package/ui/dist/assets/{test-tube-CkkvIu5j.js → test-tube-CYYLfFIz.js} +1 -1
  137. package/ui/dist/assets/{toggle-right-DgphAedS.js → toggle-right-BQ3iDgLG.js} +1 -1
  138. package/ui/dist/assets/{trash-2-C5KNXS-O.js → trash-2-CSxfLiVB.js} +1 -1
  139. package/ui/dist/assets/{trending-up-B3H8xrGD.js → trending-up-DXxKT18Q.js} +1 -1
  140. package/ui/dist/assets/{trophy-C-xSgHD2.js → trophy-osEOm2Vi.js} +1 -1
  141. package/ui/dist/assets/{volume-2-BXwRjeve.js → volume-2-C1OkTurz.js} +1 -1
  142. package/ui/dist/assets/{wifi-off-DOm__Zn6.js → wifi-off-DAhvsjnD.js} +1 -1
  143. package/ui/dist/index.html +1 -1
  144. package/ui/dist/sw.js +1 -1
  145. package/ui/dist/assets/CPLayout-CBtGEYSs.js +0 -2
  146. package/ui/dist/assets/InfraView-G1q6vF0Q.js +0 -2
  147. package/ui/dist/assets/IntelligenceView-J8Nbh4zs.js +0 -2
  148. package/ui/dist/assets/SettingsView-2AwXqlvc.js +0 -2
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import { exec as nodeExec } from "child_process";
3
+ import nodeFs from "fs";
4
+ import { join } from "path";
5
+ import { Router } from "express";
6
+ import logger from "../../utils/logger.js";
7
+ import { getUpdateInfo as defaultGetUpdateInfo } from "../../utils/updater.js";
8
+ const COMPONENT = "update-route";
9
+ const RESTART_DELAY_MS = 1e3;
10
+ const EXPECTED_DOWN_MS = 2500;
11
+ function detectUpdateMode(cwd, existsSyncFn) {
12
+ if (existsSyncFn(join(cwd, ".git"))) return "dev-git";
13
+ if (existsSyncFn("/run/systemd/system") || existsSyncFn(join(cwd, ".systemd-service"))) {
14
+ return "systemd";
15
+ }
16
+ return "npm-global";
17
+ }
18
+ function createUpdateRouter(deps = {}) {
19
+ const router = Router();
20
+ const killSelf = deps.killSelf ?? (() => process.kill(process.pid, "SIGTERM"));
21
+ const existsSyncFn = deps.existsSyncFn ?? nodeFs.existsSync;
22
+ const execFn = deps.execFn ?? nodeExec;
23
+ const getUpdateInfoFn = deps.getUpdateInfoFn ?? defaultGetUpdateInfo;
24
+ const cwdFn = deps.cwdFn ?? (() => process.cwd());
25
+ const envFn = deps.envFn ?? ((key) => process.env[key]);
26
+ router.get("/update", async (_req, res) => {
27
+ const info = await getUpdateInfoFn();
28
+ const mode = detectUpdateMode(cwdFn(), existsSyncFn);
29
+ res.json({ ...info, mode });
30
+ });
31
+ router.post("/update", (req, res) => {
32
+ const restart = req.body?.restart === true;
33
+ const mode = detectUpdateMode(cwdFn(), existsSyncFn);
34
+ const serviceName = envFn("TITAN_SERVICE_NAME") || "titan";
35
+ let command = null;
36
+ if (mode === "dev-git") command = "git pull && npm run build";
37
+ else if (mode === "npm-global") command = "npm update -g titan-agent";
38
+ logger.info(
39
+ COMPONENT,
40
+ `Update requested: mode=${mode} restart=${restart} service=${serviceName} command=${command ?? "(none, restart-only)"}`
41
+ );
42
+ const scheduleRestart = () => {
43
+ setTimeout(() => {
44
+ logger.info(COMPONENT, `Sending SIGTERM to self (pid=${process.pid}) for restart`);
45
+ try {
46
+ killSelf();
47
+ } catch (err) {
48
+ logger.error(COMPONENT, `SIGTERM failed: ${err.message}`);
49
+ }
50
+ }, RESTART_DELAY_MS);
51
+ };
52
+ if (command === null) {
53
+ const result = {
54
+ ok: true,
55
+ mode,
56
+ willRestart: restart,
57
+ expectedDownMs: restart ? EXPECTED_DOWN_MS : void 0,
58
+ message: restart ? `Restarting ${serviceName}. systemd will revive the process on the on-disk files.` : 'No-op: no command to run for systemd mode. Pass {"restart": true} to reload the process.'
59
+ };
60
+ res.json(result);
61
+ if (restart) scheduleRestart();
62
+ return;
63
+ }
64
+ execFn(command, { timeout: 18e4 }, (error, stdout) => {
65
+ if (error) {
66
+ logger.error(COMPONENT, `Update command failed (${mode}): ${error.message}`);
67
+ if (!res.headersSent) {
68
+ const result = {
69
+ ok: false,
70
+ mode,
71
+ willRestart: false,
72
+ error: error.message,
73
+ output: stdout?.slice(-500)
74
+ };
75
+ res.json(result);
76
+ }
77
+ return;
78
+ }
79
+ logger.info(COMPONENT, `Update command succeeded (${mode})`);
80
+ if (!res.headersSent) {
81
+ const result = {
82
+ ok: true,
83
+ mode,
84
+ willRestart: restart,
85
+ expectedDownMs: restart ? EXPECTED_DOWN_MS : void 0,
86
+ message: restart ? "Update complete. Restarting." : "Update complete.",
87
+ output: stdout?.slice(-500)
88
+ };
89
+ res.json(result);
90
+ }
91
+ if (restart) scheduleRestart();
92
+ });
93
+ });
94
+ return router;
95
+ }
96
+ export {
97
+ createUpdateRouter,
98
+ detectUpdateMode
99
+ };
100
+ //# sourceMappingURL=update.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/gateway/routes/update.ts"],"sourcesContent":["/**\n * Tests live in tests/gateway/updateRoute.test.ts. Keep DI seams intact so\n * those tests can mock killSelf/existsSyncFn/execFn/getUpdateInfoFn without\n * touching real disk, real npm, or real process signals.\n *\n * /api/update route — extracted from server.ts during the update-button fix.\n *\n * Why this exists as its own module:\n * The inline POST /api/update handler in server.ts had four shipping bugs\n * that broke the in-app \"Update to vX.Y.Z\" button for systemd installs\n * (which is how Tony's box and the docs both deploy TITAN):\n *\n * 1. Hard-coded `sudo systemctl restart titan-gateway` — the real\n * service name is `titan` everywhere we ship.\n * 2. Required passwordless sudo from the service user (`dj`), which\n * is not configured by default → the detached restart script hung\n * on the password prompt forever and the service stayed dead.\n * 3. Assumed `/opt/TITAN` is a git working tree and ran `git pull` in\n * it. `/opt/TITAN` is an rsync target per the deploy procedure, not\n * a git checkout, so this either errored or no-op'd silently.\n * 4. Returned `{ok: true}` to the UI before the restart actually ran,\n * so users saw \"Update completed\" while the service was dying.\n *\n * The fix:\n * - For systemd installs: send SIGTERM to ourselves and let\n * `Restart=always` revive the process on whatever files are on disk.\n * This is exactly the manual recovery path we already use; the button\n * just automates it. No sudo, no service-name guessing, no fragile\n * detached scripts.\n * - For git-checkout dev installs: still run `git pull && npm run build`\n * first, then SIGTERM-to-self (if Restart=always is wired) or spawn\n * a fresh node child (if not).\n * - For global-npm installs (no systemd, no .git): keep the existing\n * `npm update -g titan-agent` path — works for users on nvm/volta/fnm.\n * Users who installed with `sudo npm i -g` will still need to update\n * from a terminal; that's outside the scope of an in-app button.\n * - Always return an honest `{ok, willRestart, expectedDownMs, mode}`\n * shape so the UI can show a real \"Reconnecting…\" state instead of\n * a fake \"Update completed\" toast.\n */\nimport { exec as nodeExec, type ExecException } from 'child_process';\nimport nodeFs from 'fs';\nimport { join } from 'path';\nimport { Router } from 'express';\nimport logger from '../../utils/logger.js';\nimport { getUpdateInfo as defaultGetUpdateInfo } from '../../utils/updater.js';\n\nconst COMPONENT = 'update-route';\n\nexport type UpdateMode = 'dev-git' | 'systemd' | 'npm-global';\n\n/**\n * Returned from POST /api/update. Older clients that only check `ok` keep\n * working; newer clients (StatusBar, TitanCanvas) can read `willRestart`\n * and `expectedDownMs` to drive a reconnect UX.\n */\nexport interface UpdateActionResult {\n ok: boolean;\n mode: UpdateMode;\n willRestart: boolean;\n expectedDownMs?: number;\n message?: string;\n output?: string;\n error?: string;\n}\n\nexport interface UpdateRouterDeps {\n /** Override SIGTERM target for tests. Default: send SIGTERM to current pid. */\n killSelf?: () => void;\n /** fs.existsSync override for tests. */\n existsSyncFn?: (path: string) => boolean;\n /** child_process.exec override for tests. */\n execFn?: typeof nodeExec;\n /** getUpdateInfo override for tests. */\n getUpdateInfoFn?: typeof defaultGetUpdateInfo;\n /** Override process.cwd() for tests. */\n cwdFn?: () => string;\n /** Override env lookup for tests. */\n envFn?: (key: string) => string | undefined;\n}\n\nconst RESTART_DELAY_MS = 1000;\nconst EXPECTED_DOWN_MS = 2500; // Conservative: systemd Restart=always usually revives within ~1s.\n\n/**\n * Decide which update path applies based on what's on disk.\n *\n * Order matters:\n * 1. `.git/` at cwd → developer checkout, do the full git-pull+build path.\n * We check this first because a dev who happens to be on a systemd box\n * (rare but real) should still get build behavior, not a blind restart.\n * 2. `/run/systemd/system` exists or `.systemd-service` marker at cwd →\n * production systemd install. Restart-only path; operator handles\n * file deployment via rsync/ansible/docker.\n * 3. Otherwise → global npm install. `npm update -g`.\n */\nexport function detectUpdateMode(\n cwd: string,\n existsSyncFn: (p: string) => boolean,\n): UpdateMode {\n if (existsSyncFn(join(cwd, '.git'))) return 'dev-git';\n if (existsSyncFn('/run/systemd/system') || existsSyncFn(join(cwd, '.systemd-service'))) {\n return 'systemd';\n }\n return 'npm-global';\n}\n\nexport function createUpdateRouter(deps: UpdateRouterDeps = {}): Router {\n const router = Router();\n const killSelf = deps.killSelf ?? (() => process.kill(process.pid, 'SIGTERM'));\n const existsSyncFn = deps.existsSyncFn ?? nodeFs.existsSync;\n const execFn = deps.execFn ?? nodeExec;\n const getUpdateInfoFn = deps.getUpdateInfoFn ?? defaultGetUpdateInfo;\n const cwdFn = deps.cwdFn ?? (() => process.cwd());\n const envFn = deps.envFn ?? ((key: string) => process.env[key]);\n\n router.get('/update', async (_req, res) => {\n const info = await getUpdateInfoFn();\n const mode = detectUpdateMode(cwdFn(), existsSyncFn);\n res.json({ ...info, mode });\n });\n\n router.post('/update', (req, res) => {\n const restart = req.body?.restart === true;\n const mode = detectUpdateMode(cwdFn(), existsSyncFn);\n const serviceName = envFn('TITAN_SERVICE_NAME') || 'titan';\n\n // Commands per mode.\n // Systemd: nothing to fetch — files are deployed externally (rsync/ansible/docker).\n // The button's only job is to reload the running process into them.\n // Dev-git: pull + build, then restart so the new dist/ is loaded.\n // npm-global: shell out to npm; if the user is on nvm/volta/fnm this works\n // without sudo. If they did `sudo npm i -g`, npm will EACCES and we\n // surface the error honestly instead of pretending.\n let command: string | null = null;\n if (mode === 'dev-git') command = 'git pull && npm run build';\n else if (mode === 'npm-global') command = 'npm update -g titan-agent';\n // mode === 'systemd' → command stays null, we skip exec entirely\n\n logger.info(\n COMPONENT,\n `Update requested: mode=${mode} restart=${restart} service=${serviceName} command=${command ?? '(none, restart-only)'}`,\n );\n\n const scheduleRestart = (): void => {\n // setTimeout-then-SIGTERM. With systemd Restart=always (verified on Titan PC)\n // this gives the HTTP response time to flush before we tear the process down.\n setTimeout(() => {\n logger.info(COMPONENT, `Sending SIGTERM to self (pid=${process.pid}) for restart`);\n try {\n killSelf();\n } catch (err) {\n logger.error(COMPONENT, `SIGTERM failed: ${(err as Error).message}`);\n }\n }, RESTART_DELAY_MS);\n };\n\n if (command === null) {\n // Pure-restart path. systemd will revive us.\n const result: UpdateActionResult = {\n ok: true,\n mode,\n willRestart: restart,\n expectedDownMs: restart ? EXPECTED_DOWN_MS : undefined,\n message: restart\n ? `Restarting ${serviceName}. systemd will revive the process on the on-disk files.`\n : 'No-op: no command to run for systemd mode. Pass {\"restart\": true} to reload the process.',\n };\n res.json(result);\n if (restart) scheduleRestart();\n return;\n }\n\n // dev-git or npm-global path: run command, then optionally restart.\n execFn(command, { timeout: 180_000 }, (error: ExecException | null, stdout: string) => {\n if (error) {\n logger.error(COMPONENT, `Update command failed (${mode}): ${error.message}`);\n if (!res.headersSent) {\n const result: UpdateActionResult = {\n ok: false,\n mode,\n willRestart: false,\n error: error.message,\n output: stdout?.slice(-500),\n };\n res.json(result);\n }\n return;\n }\n logger.info(COMPONENT, `Update command succeeded (${mode})`);\n if (!res.headersSent) {\n const result: UpdateActionResult = {\n ok: true,\n mode,\n willRestart: restart,\n expectedDownMs: restart ? EXPECTED_DOWN_MS : undefined,\n message: restart ? 'Update complete. Restarting.' : 'Update complete.',\n output: stdout?.slice(-500),\n };\n res.json(result);\n }\n if (restart) scheduleRestart();\n });\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
@@ -9,7 +9,6 @@ import { fileURLToPath } from "url";
9
9
  import { homedir, hostname as osHostname, cpus, loadavg, totalmem, freemem } from "os";
10
10
  import { statfs } from "fs/promises";
11
11
  import { randomBytes, timingSafeEqual } from "crypto";
12
- import { exec, spawn } from "child_process";
13
12
  import fs from "fs";
14
13
  import { loadConfig, updateConfig } from "../config/config.js";
15
14
  import { loadProfile, saveProfile } from "../memory/relationship.js";
@@ -42,7 +41,6 @@ import logger, { initFileLogger } from "../utils/logger.js";
42
41
  import { TITAN_VERSION, TITAN_NAME, TITAN_LOGS_DIR, TITAN_HOME } from "../utils/constants.js";
43
42
  import { getRestartStats as getRestartStatsSync } from "../utils/restartTracker.js";
44
43
  import { recordStartupAnalytics, startHeartbeatAnalytics } from "../analytics/collector.js";
45
- import { getUpdateInfo } from "../utils/updater.js";
46
44
  import { getMissionControlHTML } from "./dashboard.js";
47
45
  import { serializePrometheus, titanRequestsTotal, titanRequestDuration, titanErrorsTotal, titanActiveSessions, titanToolCallsTotal, titanTokensTotal, titanModelRequestsTotal } from "./metrics.js";
48
46
  import { createHardwareRouter } from "./routes/hardwareRouter.js";
@@ -88,6 +86,7 @@ import { createTestsRouter } from "./routes/tests.js";
88
86
  import { createSystemRouter } from "./routes/systemRouter.js";
89
87
  import { createVoiceRouter } from "./routes/voiceRouter.js";
90
88
  import { createTelephonyRouter } from "./routes/telephony.js";
89
+ import { createUpdateRouter } from "./routes/update.js";
91
90
  import { createSocialRouter } from "./routes/socialRouter.js";
92
91
  import { createWatchRouter } from "./routes/watchRouter.js";
93
92
  import { setupSSEFlush } from "../utils/sseFlush.js";
@@ -2567,69 +2566,7 @@ data: ${JSON.stringify(structured)}
2567
2566
  // Last 20 for detail view
2568
2567
  });
2569
2568
  });
2570
- app.get("/api/update", async (_req, res) => {
2571
- const info = await getUpdateInfo();
2572
- res.json(info);
2573
- });
2574
- app.post("/api/update", (req, res) => {
2575
- const isLocalDev = fs.existsSync(join(process.cwd(), ".git"));
2576
- const isSystemd = fs.existsSync("/run/systemd/system") || fs.existsSync(join(process.cwd(), ".systemd-service"));
2577
- const restart = req.body?.restart === true;
2578
- let command;
2579
- let postCommand = null;
2580
- if (isLocalDev) {
2581
- command = "git pull && npm run build";
2582
- if (restart) {
2583
- postCommand = "restart";
2584
- }
2585
- } else if (isSystemd) {
2586
- command = "git pull && npm run build";
2587
- if (restart) {
2588
- postCommand = "systemctl";
2589
- }
2590
- } else {
2591
- command = "npm update -g titan-agent";
2592
- }
2593
- logger.info(COMPONENT, `Triggering update: ${command} (isDev=${isLocalDev}, isSystemd=${isSystemd}, restart=${restart})`);
2594
- exec(command, { timeout: 18e4 }, (error, stdout, _stderr) => {
2595
- if (error) {
2596
- logger.error(COMPONENT, `Update failed: ${error.message}`);
2597
- if (!res.headersSent) res.json({ ok: false, error: error.message });
2598
- return;
2599
- }
2600
- logger.info(COMPONENT, `Update completed successfully.\\n${stdout}`);
2601
- if (!res.headersSent) {
2602
- res.json({ ok: true, message: "Update completed", restarting: restart, output: stdout.slice(-500) });
2603
- }
2604
- if (restart) {
2605
- logger.info(COMPONENT, "Scheduling restart in 2 seconds...");
2606
- const cwd = process.cwd();
2607
- if (postCommand === "systemctl") {
2608
- const scriptPath = "/tmp/titan-restart.sh";
2609
- fs.writeFileSync(scriptPath, [
2610
- "#!/bin/bash",
2611
- "sleep 2",
2612
- `cd "${cwd}"`,
2613
- "sudo systemctl restart titan-gateway"
2614
- ].join("\n"), { mode: 493 });
2615
- spawn("bash", [scriptPath], { detached: true, stdio: "ignore" }).unref();
2616
- } else {
2617
- const scriptPath = "/tmp/titan-restart.sh";
2618
- fs.writeFileSync(scriptPath, [
2619
- "#!/bin/bash",
2620
- "sleep 2",
2621
- `cd "${cwd}"`,
2622
- "nohup node dist/cli/index.js gateway >> /tmp/titan-gateway.log 2>&1 &"
2623
- ].join("\n"), { mode: 493 });
2624
- spawn("bash", [scriptPath], { detached: true, stdio: "ignore" }).unref();
2625
- }
2626
- setTimeout(() => {
2627
- logger.info(COMPONENT, "Exiting for restart...");
2628
- process.exit(0);
2629
- }, 1e3);
2630
- }
2631
- });
2632
- });
2569
+ app.use("/api", createUpdateRouter());
2633
2570
  app.get("/api/config", (_req, res) => {
2634
2571
  const cfg = loadConfig();
2635
2572
  res.json({