tide-commander 1.98.0 → 1.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/assets/{BossLogsModal-P6MiZVuY.js → BossLogsModal-CDel834o.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-DZJ1Ngsz.js → BossSpawnModal-BB9wL5VV.js} +1 -1
  3. package/dist/assets/{ControlsModal-BjV0a1kc.js → ControlsModal-D5RE5MvT.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-DtddubKa.js → DockerLogsModal-B27P1JpZ.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-ByRepTwR.js → EmbeddedEditor-DP1jqsT_.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-DS6NmxUi.js → GmailOAuthSetup-DvuL5G8Q.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-kQyKInRW.js → GoogleOAuthSetup-CG6bSCjv.js} +1 -1
  8. package/dist/assets/{IframeModal-BuTPDTx1.js → IframeModal-ClnUGmJV.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-Bm19ZAd6.js → IntegrationsPanel-0vdfxZRq.js} +2 -2
  10. package/dist/assets/{LogViewerModal-DFlUtkDD.js → LogViewerModal-DLQlrZ4O.js} +1 -1
  11. package/dist/assets/{MonitoringModal-BEEPwGu5.js → MonitoringModal-DiC9TNCy.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-Di5MjyxZ.js → PM2LogsModal-BgPrnaP5.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-CNjmR7BX.js → RestoreArchivedAreaModal-CIN1OrOW.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-CA29sh9R.js → Scene2DCanvas-Bap6brvv.js} +1 -1
  15. package/dist/assets/{SceneManager-duyCA_TD.js → SceneManager-CidCW0PR.js} +1 -1
  16. package/dist/assets/{SkillsPanel-CkV4ci4Q.js → SkillsPanel-DHQTPaP2.js} +1 -1
  17. package/dist/assets/{SlackMultiInstanceSetup-iHZNQX3B.js → SlackMultiInstanceSetup-CQK4D89W.js} +1 -1
  18. package/dist/assets/{SpawnModal-bzQlnSQy.js → SpawnModal-Cx_k3HHC.js} +1 -1
  19. package/dist/assets/{SubordinateAssignmentModal-D6Ak_XQ1.js → SubordinateAssignmentModal-C1DOv51H.js} +1 -1
  20. package/dist/assets/{TriggerManagerPanel-CT8aMQ5P.js → TriggerManagerPanel-jP5RBK2L.js} +1 -1
  21. package/dist/assets/{WorkflowEditorPanel-DcV9ErU4.js → WorkflowEditorPanel-Dh6mZ8M4.js} +1 -1
  22. package/dist/assets/{index-Cirz97EK.js → index-B-ttQFx4.js} +2 -2
  23. package/dist/assets/index-BXnThzaG.js +11 -0
  24. package/dist/assets/{index-C0fIrrTS.js → index-CYwFXTQZ.js} +1 -1
  25. package/dist/assets/{index-Cnj2pM08.js → index-Cwlm-Pqi.js} +3 -3
  26. package/dist/assets/{index-Ba8umpS_.js → index-D96LXKm4.js} +1 -1
  27. package/dist/assets/{index-BQQV5Xax.js → index-DP5sMNS9.js} +1 -1
  28. package/dist/assets/{index-Dww2MWLN.js → index-DfEbuBH8.js} +1 -1
  29. package/dist/assets/{index-D2L5oc5D.js → index-DsKaX6TJ.js} +1 -1
  30. package/dist/assets/{index-DR7uaDtK.js → index-enJvXAbe.js} +1 -1
  31. package/dist/assets/{main-XbhAPjbi.js → main-B7wf_xU_.js} +91 -91
  32. package/dist/assets/main-DLzFxLC1.css +1 -0
  33. package/dist/assets/{web-C4LpSGoH.js → web-BHmmnvF7.js} +1 -1
  34. package/dist/assets/{web-CaPUSaID.js → web-IGuhG0xr.js} +1 -1
  35. package/dist/assets/{web-C-JnApw7.js → web-SOehUGgT.js} +1 -1
  36. package/dist/index.html +2 -2
  37. package/dist/locales/en/config.json +16 -1
  38. package/dist/src/packages/server/routes/index.js +2 -0
  39. package/dist/src/packages/server/routes/system.js +156 -0
  40. package/dist/src/packages/server/services/self-update-service.js +191 -0
  41. package/package.json +1 -1
  42. package/dist/assets/index-CDHzSAgq.js +0 -1
  43. package/dist/assets/main-CD03IZnY.css +0 -1
@@ -1 +1 @@
1
- import{ck as a}from"./main-XbhAPjbi.js";import{ImpactStyle as i,NotificationType as r}from"./index-Cirz97EK.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
1
+ import{ck as a}from"./main-B7wf_xU_.js";import{ImpactStyle as i,NotificationType as r}from"./index-B-ttQFx4.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
@@ -1 +1 @@
1
- import{ck as s}from"./main-XbhAPjbi.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
1
+ import{ck as s}from"./main-B7wf_xU_.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
@@ -1 +1 @@
1
- import{ck as t}from"./main-XbhAPjbi.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
1
+ import{ck as t}from"./main-B7wf_xU_.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
package/dist/index.html CHANGED
@@ -22,10 +22,10 @@
22
22
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
23
23
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
24
24
  <title>Tide Commander</title>
25
- <script type="module" crossorigin src="/assets/main-XbhAPjbi.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-B7wf_xU_.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
28
- <link rel="stylesheet" crossorigin href="/assets/main-CD03IZnY.css">
28
+ <link rel="stylesheet" crossorigin href="/assets/main-DLzFxLC1.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="app"></div>
@@ -541,7 +541,22 @@
541
541
  "principle4": "Transparent agent communication",
542
542
  "specialThanks": "Special Thanks",
543
543
  "kenneyCredit": "for the agent character models",
544
- "claudeCodeCredit": "by Anthropic, the AI backbone"
544
+ "claudeCodeCredit": "by Anthropic, the AI backbone",
545
+ "autoUpdateTitle": "Auto-update",
546
+ "autoUpdateButton": "Update now",
547
+ "autoUpdateRunning": "Installing update...",
548
+ "autoUpdateConfirmTitle": "Update Tide Commander?",
549
+ "autoUpdateConfirmBody": "This will run npm install -g tide-commander@latest and then close the server. You will need to relaunch tide-commander from your terminal.",
550
+ "autoUpdateConfirm": "Yes, update",
551
+ "autoUpdateCancel": "Cancel",
552
+ "autoUpdateClose": "Close",
553
+ "autoUpdateOutput": "Install log",
554
+ "autoUpdateSuccess": "Update installed.",
555
+ "autoUpdateRestartHint": "Please restart Tide Commander from your terminal (run: tide-commander).",
556
+ "autoUpdateFailed": "Update failed.",
557
+ "autoUpdateDevMode": "Auto-update is unavailable in dev mode.",
558
+ "autoUpdateManualCommand": "Run this in your terminal to update manually:",
559
+ "autoUpdatePackageManagerNotice": "Auto-update only supports npm-installed globals. Detected: {{pm}}."
545
560
  },
546
561
  "shortcuts": {
547
562
  "pressKeys": "Press keys...",
@@ -27,6 +27,7 @@ import sessionsRouter from './sessions.js';
27
27
  import databaseRouter from './database.js';
28
28
  import buildingsRouter, { setBroadcast as setBuildingsBroadcast } from './buildings.js';
29
29
  import skillsRouter, { setBroadcast as setSkillsBroadcast } from './skills.js';
30
+ import systemRouter from './system.js';
30
31
  import { getPlugins } from '../integrations/integration-registry.js';
31
32
  const router = Router();
32
33
  // Health check
@@ -55,6 +56,7 @@ router.use('/sessions', sessionsRouter);
55
56
  router.use('/database', databaseRouter);
56
57
  router.use('/buildings', buildingsRouter);
57
58
  router.use('/skills', skillsRouter);
59
+ router.use('/system', systemRouter);
58
60
  // Integration plugin routes (e.g. /api/slack/*, /api/documents/*, /api/jira/*)
59
61
  // Uses lazy lookup so plugins can be registered after route setup
60
62
  router.use((req, res, next) => {
@@ -0,0 +1,156 @@
1
+ /**
2
+ * System Routes
3
+ * Endpoints for inspecting / updating the running Tide Commander install.
4
+ */
5
+ import { Router } from 'express';
6
+ import { createLogger } from '../utils/logger.js';
7
+ import { fetchLatestNpmVersion, getVersionRelation } from '../../shared/version.js';
8
+ import { getInstallInfo, isAutoUpdateSupported, runNpmGlobalUpdate, } from '../services/self-update-service.js';
9
+ const log = createLogger('SystemRoutes');
10
+ const router = Router();
11
+ const PACKAGE_NAME = 'tide-commander';
12
+ // Guard: only one self-update may run at a time
13
+ let updateInProgress = false;
14
+ /**
15
+ * GET /api/system/install-info
16
+ *
17
+ * Returns enough info for the UI to decide whether to render the "Update now"
18
+ * button and which command to suggest if auto-update isn't supported.
19
+ */
20
+ router.get('/install-info', async (_req, res) => {
21
+ try {
22
+ const info = getInstallInfo();
23
+ const latestVersion = await fetchLatestNpmVersion(PACKAGE_NAME);
24
+ const relation = latestVersion
25
+ ? getVersionRelation(info.currentVersion, latestVersion)
26
+ : 'unknown';
27
+ const updateAvailable = relation === 'behind';
28
+ res.json({
29
+ isGlobalInstall: info.isGlobalInstall,
30
+ packageManager: info.packageManager,
31
+ installRoot: info.installRoot,
32
+ currentVersion: info.currentVersion,
33
+ latestVersion,
34
+ updateAvailable,
35
+ autoUpdateSupported: isAutoUpdateSupported(info),
36
+ suggestedManualCommand: info.suggestedManualCommand,
37
+ reason: info.reason,
38
+ updateInProgress,
39
+ });
40
+ }
41
+ catch (err) {
42
+ const message = err.message;
43
+ log.error(`Failed to get install info: ${message}`);
44
+ res.status(500).json({ error: message });
45
+ }
46
+ });
47
+ /**
48
+ * POST /api/system/self-update
49
+ *
50
+ * Streams the output of `npm install -g tide-commander@latest` via SSE.
51
+ *
52
+ * SSE events:
53
+ * - start { message }
54
+ * - stdout { chunk }
55
+ * - stderr { chunk }
56
+ * - done { success, exitCode, newVersion, requiresRestart }
57
+ * - error { message, permissionDenied, suggestedManualCommand }
58
+ *
59
+ * On success the server schedules its own exit so the next launch picks up
60
+ * the new binary. The user must relaunch tide-commander manually.
61
+ */
62
+ router.post('/self-update', async (_req, res) => {
63
+ const info = getInstallInfo();
64
+ if (!info.isGlobalInstall) {
65
+ res.status(400).json({
66
+ error: 'Auto-update is only available when running from a global install.',
67
+ reason: info.reason,
68
+ });
69
+ return;
70
+ }
71
+ if (!isAutoUpdateSupported(info)) {
72
+ res.status(400).json({
73
+ error: `Auto-update is not supported for package manager: ${info.packageManager}`,
74
+ suggestedManualCommand: info.suggestedManualCommand,
75
+ });
76
+ return;
77
+ }
78
+ if (updateInProgress) {
79
+ res.status(409).json({ error: 'An update is already in progress.' });
80
+ return;
81
+ }
82
+ updateInProgress = true;
83
+ res.setHeader('Content-Type', 'text/event-stream');
84
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
85
+ res.setHeader('Connection', 'keep-alive');
86
+ res.setHeader('X-Accel-Buffering', 'no');
87
+ res.flushHeaders?.();
88
+ const send = (event, data) => {
89
+ res.write(`event: ${event}\n`);
90
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
91
+ };
92
+ // Keepalive comment every 15s in case the install takes a while
93
+ const keepalive = setInterval(() => {
94
+ try {
95
+ res.write(`: keepalive ${Date.now()}\n\n`);
96
+ }
97
+ catch {
98
+ // socket closed
99
+ }
100
+ }, 15000);
101
+ send('start', { message: `Running npm install -g ${PACKAGE_NAME}@latest...` });
102
+ try {
103
+ const result = await runNpmGlobalUpdate({
104
+ onStdout: (chunk) => send('stdout', { chunk }),
105
+ onStderr: (chunk) => send('stderr', { chunk }),
106
+ });
107
+ clearInterval(keepalive);
108
+ if (result.exitCode === 0) {
109
+ const newVersion = (await fetchLatestNpmVersion(PACKAGE_NAME)) ?? null;
110
+ send('done', {
111
+ success: true,
112
+ exitCode: 0,
113
+ newVersion,
114
+ requiresRestart: true,
115
+ message: 'Update installed. Please restart Tide Commander from your terminal.',
116
+ });
117
+ res.end();
118
+ // Give the client ~1.5s to render the success message before we exit.
119
+ // The user must manually restart with `tide-commander` from the terminal.
120
+ log.log('Update succeeded — scheduling server exit in 1500ms to release the old binary.');
121
+ setTimeout(() => {
122
+ log.log('Exiting after successful update. Restart manually with: tide-commander');
123
+ process.exit(0);
124
+ }, 1500);
125
+ }
126
+ else {
127
+ const errMsg = result.permissionDenied
128
+ ? 'Permission denied while installing globally. Re-run from a terminal with the appropriate permissions (e.g. sudo npm install -g tide-commander@latest), or fix your npm prefix to a user-owned directory.'
129
+ : `npm install exited with code ${result.exitCode}.`;
130
+ send('error', {
131
+ message: errMsg,
132
+ exitCode: result.exitCode,
133
+ permissionDenied: result.permissionDenied,
134
+ suggestedManualCommand: info.suggestedManualCommand,
135
+ });
136
+ send('done', {
137
+ success: false,
138
+ exitCode: result.exitCode,
139
+ newVersion: null,
140
+ requiresRestart: false,
141
+ });
142
+ res.end();
143
+ updateInProgress = false;
144
+ }
145
+ }
146
+ catch (err) {
147
+ clearInterval(keepalive);
148
+ const message = err.message;
149
+ log.error(`Self-update failed: ${message}`);
150
+ send('error', { message, permissionDenied: false, suggestedManualCommand: info.suggestedManualCommand });
151
+ send('done', { success: false, exitCode: -1, newVersion: null, requiresRestart: false });
152
+ res.end();
153
+ updateInProgress = false;
154
+ }
155
+ });
156
+ export default router;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Self-Update Service
3
+ *
4
+ * Detects how Tide Commander was installed (global npm/pnpm/yarn/bun vs dev mode)
5
+ * and runs `npm install -g tide-commander@latest` when invoked from the UI.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { spawn } from 'child_process';
10
+ import { fileURLToPath } from 'url';
11
+ import { createLogger } from '../utils/logger.js';
12
+ const log = createLogger('SelfUpdate');
13
+ function readPackageJson(dir) {
14
+ try {
15
+ const raw = fs.readFileSync(path.join(dir, 'package.json'), 'utf8');
16
+ return JSON.parse(raw);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ function detectPackageManager(installRoot) {
23
+ const lower = installRoot.toLowerCase();
24
+ // Bun: ~/.bun/install/global/node_modules/tide-commander/
25
+ if (lower.includes(`${path.sep}.bun${path.sep}install${path.sep}global${path.sep}`) || lower.includes(`${path.sep}.bun${path.sep}`)) {
26
+ return { pm: 'bun', suggested: 'bun add -g tide-commander@latest' };
27
+ }
28
+ // Pnpm:
29
+ // ~/.local/share/pnpm/global/<N>/node_modules/tide-commander/
30
+ // ~/Library/pnpm/global/...
31
+ // ~/.pnpm/global/...
32
+ if (lower.includes(`${path.sep}pnpm${path.sep}global${path.sep}`) || lower.includes(`${path.sep}.pnpm${path.sep}`)) {
33
+ return { pm: 'pnpm', suggested: 'pnpm add -g tide-commander@latest' };
34
+ }
35
+ // Yarn:
36
+ // ~/.config/yarn/global/node_modules/tide-commander/
37
+ // ~/.yarn/global/...
38
+ if (lower.includes(`${path.sep}yarn${path.sep}global${path.sep}`) || lower.includes(`${path.sep}.yarn${path.sep}`)) {
39
+ return { pm: 'yarn', suggested: 'yarn global add tide-commander@latest' };
40
+ }
41
+ // npm — global prefix typically ends in /lib/node_modules/tide-commander
42
+ // Works for system npm, nvm, asdf, volta, fnm, npm config prefix overrides.
43
+ if (lower.includes(`${path.sep}lib${path.sep}node_modules${path.sep}`)) {
44
+ return { pm: 'npm', suggested: 'npm install -g tide-commander@latest' };
45
+ }
46
+ // Some Windows / non-standard prefixes don't have /lib — fall back to npm
47
+ return { pm: 'npm', suggested: 'npm install -g tide-commander@latest' };
48
+ }
49
+ /**
50
+ * Locate the tide-commander package root on disk (the dir whose package.json
51
+ * has name === "tide-commander"). Returns null if it can't be determined.
52
+ */
53
+ export function locateInstallRoot() {
54
+ try {
55
+ const here = path.dirname(fileURLToPath(import.meta.url));
56
+ // Walk up looking for the nearest package.json named "tide-commander"
57
+ let current = here;
58
+ while (current !== path.dirname(current)) {
59
+ const pkgPath = path.join(current, 'package.json');
60
+ if (fs.existsSync(pkgPath)) {
61
+ const pkg = readPackageJson(current);
62
+ if (pkg?.name === 'tide-commander') {
63
+ return current;
64
+ }
65
+ }
66
+ current = path.dirname(current);
67
+ }
68
+ }
69
+ catch (err) {
70
+ log.error(`Failed to locate install root: ${err.message}`);
71
+ }
72
+ return null;
73
+ }
74
+ /**
75
+ * Returns true when the install root looks like a global npm-style install
76
+ * (lives under a node_modules directory) AND is not the dev/repo checkout
77
+ * (no .git/ at the package root).
78
+ */
79
+ function isGlobalInstallPath(installRoot) {
80
+ const parent = path.dirname(installRoot);
81
+ const insideNodeModules = path.basename(parent) === 'node_modules';
82
+ const hasGitDir = fs.existsSync(path.join(installRoot, '.git'));
83
+ if (hasGitDir) {
84
+ return { isGlobal: false, reason: 'Install root contains .git/ — running from a repo checkout (dev mode).' };
85
+ }
86
+ if (!insideNodeModules) {
87
+ return { isGlobal: false, reason: 'Install root is not inside a node_modules directory — running from source.' };
88
+ }
89
+ return { isGlobal: true, reason: 'Install root lives under node_modules — looks like a global install.' };
90
+ }
91
+ /**
92
+ * Build the full install info snapshot used by the UI.
93
+ */
94
+ export function getInstallInfo() {
95
+ const installRoot = locateInstallRoot();
96
+ if (!installRoot) {
97
+ return {
98
+ isGlobalInstall: false,
99
+ packageManager: 'unknown',
100
+ installRoot: null,
101
+ currentVersion: 'unknown',
102
+ suggestedManualCommand: null,
103
+ reason: 'Could not locate tide-commander package root on disk.',
104
+ };
105
+ }
106
+ const pkg = readPackageJson(installRoot);
107
+ const currentVersion = pkg?.version ?? 'unknown';
108
+ const { isGlobal, reason } = isGlobalInstallPath(installRoot);
109
+ if (!isGlobal) {
110
+ return {
111
+ isGlobalInstall: false,
112
+ packageManager: 'unknown',
113
+ installRoot,
114
+ currentVersion,
115
+ suggestedManualCommand: null,
116
+ reason,
117
+ };
118
+ }
119
+ const { pm, suggested } = detectPackageManager(installRoot);
120
+ return {
121
+ isGlobalInstall: true,
122
+ packageManager: pm,
123
+ installRoot,
124
+ currentVersion,
125
+ suggestedManualCommand: suggested,
126
+ reason,
127
+ };
128
+ }
129
+ /**
130
+ * Whether the current install can be auto-updated by this server.
131
+ * Right now only npm-style globals are supported.
132
+ */
133
+ export function isAutoUpdateSupported(info = getInstallInfo()) {
134
+ return info.isGlobalInstall && info.packageManager === 'npm';
135
+ }
136
+ /**
137
+ * Run `npm install -g tide-commander@latest`. Streams stdout/stderr via callbacks.
138
+ * Never throws; always resolves with the captured output and exit code.
139
+ */
140
+ export function runNpmGlobalUpdate(callbacks = {}) {
141
+ return new Promise((resolve) => {
142
+ const args = ['install', '-g', 'tide-commander@latest'];
143
+ // Disable npm fund/audit output noise to keep the log readable
144
+ const env = {
145
+ ...process.env,
146
+ npm_config_fund: 'false',
147
+ npm_config_audit: 'false',
148
+ npm_config_progress: 'false',
149
+ };
150
+ log.log(`Running: npm ${args.join(' ')}`);
151
+ const child = spawn('npm', args, {
152
+ env,
153
+ stdio: ['ignore', 'pipe', 'pipe'],
154
+ });
155
+ let stdout = '';
156
+ let stderr = '';
157
+ child.stdout?.on('data', (buf) => {
158
+ const text = buf.toString('utf8');
159
+ stdout += text;
160
+ callbacks.onStdout?.(text);
161
+ });
162
+ child.stderr?.on('data', (buf) => {
163
+ const text = buf.toString('utf8');
164
+ stderr += text;
165
+ callbacks.onStderr?.(text);
166
+ });
167
+ child.on('error', (err) => {
168
+ const message = `Failed to spawn npm: ${err.message}\n`;
169
+ stderr += message;
170
+ callbacks.onStderr?.(message);
171
+ resolve({
172
+ exitCode: -1,
173
+ signal: null,
174
+ stdout,
175
+ stderr,
176
+ permissionDenied: /eacces|eperm|permission/i.test(message),
177
+ });
178
+ });
179
+ child.on('close', (code, signal) => {
180
+ const combined = `${stdout}\n${stderr}`;
181
+ const permissionDenied = /EACCES|EPERM|permission denied/i.test(combined);
182
+ resolve({
183
+ exitCode: code,
184
+ signal,
185
+ stdout,
186
+ stderr,
187
+ permissionDenied,
188
+ });
189
+ });
190
+ });
191
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.98.0",
3
+ "version": "1.99.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",