tide-commander 1.95.0 → 1.96.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 (41) hide show
  1. package/dist/assets/{BossLogsModal-BNfB6g0E.js → BossLogsModal-BFy6Rxdn.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-BIcCbrjM.js → BossSpawnModal-KQyJYj_B.js} +1 -1
  3. package/dist/assets/{ControlsModal-CS5jOEdY.js → ControlsModal-W_ghuG_Q.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-B6sUNqY_.js → DockerLogsModal-valTcuRy.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-DiXmHZpX.js → EmbeddedEditor-CIsFWo6A.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-OPmwhJyE.js → GmailOAuthSetup-AcW1zAlk.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-BEc7lAua.js → GoogleOAuthSetup-C9uZ46Oe.js} +1 -1
  8. package/dist/assets/{IframeModal-VrlQUMlO.js → IframeModal-CmWv7GXl.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-ta1yp-s4.js → IntegrationsPanel-CNvzosgx.js} +2 -2
  10. package/dist/assets/{LogViewerModal-B_4ke-1p.js → LogViewerModal-imTE-619.js} +1 -1
  11. package/dist/assets/{MonitoringModal-BHAuVdYA.js → MonitoringModal-DLxBY738.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-BfKic3hD.js → PM2LogsModal-S4n0D11V.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-Bxk9GcPa.js → RestoreArchivedAreaModal-B32sqCxN.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-COVSkNbV.js → Scene2DCanvas-bEu0UPUm.js} +1 -1
  15. package/dist/assets/{SceneManager-DWVI2idg.js → SceneManager-Ce3X0awy.js} +1 -1
  16. package/dist/assets/{SkillsPanel-BnKqbJyg.js → SkillsPanel-Dj22nDU8.js} +1 -1
  17. package/dist/assets/{SlackMultiInstanceSetup-BuA87Vlm.js → SlackMultiInstanceSetup-J9NdRY0U.js} +1 -1
  18. package/dist/assets/{SpawnModal-ClZUpgWy.js → SpawnModal-F9nspdWd.js} +1 -1
  19. package/dist/assets/{SubordinateAssignmentModal-Dn8tejNU.js → SubordinateAssignmentModal-DT31CkPr.js} +1 -1
  20. package/dist/assets/{TriggerManagerPanel-oBoHJdcv.js → TriggerManagerPanel-CVJ9ozHz.js} +1 -1
  21. package/dist/assets/{WorkflowEditorPanel-Bwju9-46.js → WorkflowEditorPanel-BYRi0cU9.js} +1 -1
  22. package/dist/assets/{index-CBh6qNCb.js → index-B9G4hrFR.js} +1 -1
  23. package/dist/assets/{index-CXBrQLNP.js → index-BPy8NE5B.js} +3 -3
  24. package/dist/assets/{index-DMUs4kjY.js → index-BmjrPptb.js} +1 -1
  25. package/dist/assets/{index-Bu_n7vgB.js → index-ByfAbkqv.js} +1 -1
  26. package/dist/assets/{index-umTVv-4x.js → index-C5wC_Xis.js} +1 -1
  27. package/dist/assets/{index-DgwVJN80.js → index-CLr8DpLX.js} +2 -2
  28. package/dist/assets/{index-fIzifjgU.js → index-DJm7oMXk.js} +1 -1
  29. package/dist/assets/{index-CfliOGe8.js → index-DkjWoHhy.js} +1 -1
  30. package/dist/assets/{index-DvBhO5je.js → index-DqZhHROd.js} +1 -1
  31. package/dist/assets/{main-BaGMbjuZ.js → main-9uTEp9Lr.js} +4 -4
  32. package/dist/assets/{web-pfDqogx0.js → web-CLuvtqCm.js} +1 -1
  33. package/dist/assets/{web-Cp8n5FK3.js → web-D-a3phls.js} +1 -1
  34. package/dist/assets/{web-BD4VGICh.js → web-DM2bFWBN.js} +1 -1
  35. package/dist/index.html +1 -1
  36. package/dist/src/packages/server/data/builtin-skills/create-building.js +521 -484
  37. package/dist/src/packages/server/routes/buildings.js +298 -0
  38. package/dist/src/packages/server/routes/index.js +3 -1
  39. package/dist/src/packages/server/services/building-service.js +400 -85
  40. package/dist/src/packages/server/websocket/handler.js +2 -4
  41. package/package.json +1 -1
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Building Routes
3
+ * REST API endpoints for building lifecycle and control.
4
+ *
5
+ * This is the canonical surface for agents managing buildings. The
6
+ * underlying buildingService handles validation, ID generation,
7
+ * credential encryption, broadcast events, and PM2/Docker/Terminal
8
+ * reconciliation.
9
+ *
10
+ * Routes:
11
+ * GET /api/buildings - List buildings (secrets redacted)
12
+ * GET /api/buildings/:id - Get one building (secrets redacted)
13
+ * POST /api/buildings - Create a building
14
+ * PATCH /api/buildings/:id - Partial update
15
+ * DELETE /api/buildings/:id[?cleanup=false] - Delete (default: cleanup runtime artifacts)
16
+ *
17
+ * POST /api/buildings/:id/command - { command } start|stop|restart|healthCheck|logs|delete
18
+ * GET /api/buildings/:id/logs?lines=200&service=foo - Snapshot logs (PM2/Docker/custom)
19
+ * POST /api/buildings/:id/sync-status - Force PM2/Docker status refresh
20
+ * POST /api/buildings/:id/subordinates - Assign subordinates (boss only)
21
+ *
22
+ * POST /api/buildings/boss/:id/command - { command } start_all|stop_all|restart_all
23
+ *
24
+ * GET /api/buildings/docker/containers - List adoptable containers + compose projects
25
+ */
26
+ import { Router } from 'express';
27
+ import { buildingService } from '../services/index.js';
28
+ import * as dockerService from '../services/docker-service.js';
29
+ import * as terminalService from '../services/terminal-service.js';
30
+ import { broadcast } from '../websocket/handler.js';
31
+ import { createLogger } from '../utils/logger.js';
32
+ const log = createLogger('BuildingRoutes');
33
+ const router = Router();
34
+ let broadcastFn = null;
35
+ /**
36
+ * Wire the WebSocket broadcaster. Called from the websocket handler at startup.
37
+ * Falls back to the directly-imported broadcast() at call time if not wired,
38
+ * which lets these routes work in tests that don't initialise the WS layer.
39
+ */
40
+ export function setBroadcast(fn) {
41
+ broadcastFn = fn;
42
+ }
43
+ function emit(message) {
44
+ if (broadcastFn) {
45
+ broadcastFn(message);
46
+ }
47
+ else {
48
+ // In production setBroadcast() is always called from the WS handler init;
49
+ // this fallback keeps unit tests working without the full WS bootstrap.
50
+ try {
51
+ broadcast(message);
52
+ }
53
+ catch {
54
+ // No WS server up — broadcast is best-effort.
55
+ }
56
+ }
57
+ }
58
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
59
+ const VALID_COMMANDS = new Set([
60
+ 'start', 'stop', 'restart', 'healthCheck', 'logs', 'delete',
61
+ ]);
62
+ const VALID_BOSS_COMMANDS = new Set([
63
+ 'start_all', 'stop_all', 'restart_all',
64
+ ]);
65
+ function redactDatabaseConnection(c) {
66
+ return {
67
+ id: c.id,
68
+ name: c.name,
69
+ engine: c.engine,
70
+ host: c.host,
71
+ port: c.port,
72
+ username: c.username,
73
+ database: c.database,
74
+ filepath: c.filepath,
75
+ ssl: c.ssl,
76
+ hasPassword: Boolean(c.password),
77
+ ssh: c.ssh
78
+ ? {
79
+ enabled: c.ssh.enabled,
80
+ host: c.ssh.host,
81
+ port: c.ssh.port,
82
+ username: c.ssh.username,
83
+ authMethod: c.ssh.authMethod,
84
+ hasPassword: Boolean(c.ssh.password),
85
+ hasPrivateKey: Boolean(c.ssh.privateKey || c.ssh.privateKeyPath),
86
+ }
87
+ : undefined,
88
+ };
89
+ }
90
+ /**
91
+ * Strip secret fields before returning a building over HTTP.
92
+ * Plaintext credentials may sit in memory after the agent creates a building,
93
+ * but they must never come back out of GET responses.
94
+ */
95
+ function redactBuilding(building) {
96
+ const out = { ...building };
97
+ if (building.database?.connections) {
98
+ out.database = {
99
+ ...building.database,
100
+ connections: building.database.connections.map(redactDatabaseConnection),
101
+ };
102
+ }
103
+ return out;
104
+ }
105
+ // ─── CRUD ─────────────────────────────────────────────────────────────────────
106
+ // GET /api/buildings — list all buildings (redacted)
107
+ router.get('/', (_req, res) => {
108
+ const buildings = buildingService.getBuildings().map(redactBuilding);
109
+ res.json({ buildings });
110
+ });
111
+ // GET /api/buildings/docker/containers — adoptable containers + compose projects
112
+ // (Must come before /:id so the docker prefix doesn't match as an id.)
113
+ router.get('/docker/containers', async (_req, res) => {
114
+ try {
115
+ const [containers, composeProjects] = await Promise.all([
116
+ dockerService.listAllContainers(),
117
+ dockerService.listComposeProjects(),
118
+ ]);
119
+ res.json({ containers, composeProjects });
120
+ }
121
+ catch (err) {
122
+ log.error('Failed to list Docker resources:', err);
123
+ res.status(500).json({ error: err.message });
124
+ }
125
+ });
126
+ // POST /api/buildings/boss/:id/command — body { command }
127
+ // Must come before /:id/command so 'boss' isn't matched as an id.
128
+ router.post('/boss/:id/command', async (req, res) => {
129
+ const command = req.body?.command;
130
+ if (!command || !VALID_BOSS_COMMANDS.has(command)) {
131
+ res.status(400).json({
132
+ error: `command must be one of: ${[...VALID_BOSS_COMMANDS].join(', ')}`,
133
+ });
134
+ return;
135
+ }
136
+ try {
137
+ const result = await buildingService.executeBossBuildingCommand(req.params.id, command, emit);
138
+ res.json(result);
139
+ }
140
+ catch (err) {
141
+ log.error(`Boss command failed for ${req.params.id}:`, err);
142
+ res.status(500).json({ error: err.message });
143
+ }
144
+ });
145
+ // GET /api/buildings/:id — fetch one (redacted)
146
+ router.get('/:id', (req, res) => {
147
+ const building = buildingService.getBuilding(req.params.id);
148
+ if (!building) {
149
+ res.status(404).json({ error: `Building not found: ${req.params.id}` });
150
+ return;
151
+ }
152
+ res.json(redactBuilding(building));
153
+ });
154
+ // POST /api/buildings — create
155
+ router.post('/', async (req, res) => {
156
+ try {
157
+ const result = await buildingService.createBuilding(req.body, emit);
158
+ if (!result.ok) {
159
+ res.status(result.status).json({ error: 'Validation failed', errors: result.errors });
160
+ return;
161
+ }
162
+ res.status(201).json(redactBuilding(result.building));
163
+ }
164
+ catch (err) {
165
+ log.error('Failed to create building:', err);
166
+ res.status(500).json({ error: err.message });
167
+ }
168
+ });
169
+ // PATCH /api/buildings/:id — partial update
170
+ router.patch('/:id', async (req, res) => {
171
+ try {
172
+ const result = await buildingService.updateBuilding(req.params.id, req.body, emit);
173
+ if (!result.ok) {
174
+ res.status(result.status).json({
175
+ error: result.status === 404 ? 'Not found' : 'Validation failed',
176
+ errors: result.errors,
177
+ });
178
+ return;
179
+ }
180
+ res.json(redactBuilding(result.building));
181
+ }
182
+ catch (err) {
183
+ log.error(`Failed to update building ${req.params.id}:`, err);
184
+ res.status(500).json({ error: err.message });
185
+ }
186
+ });
187
+ // DELETE /api/buildings/:id?cleanup=false — delete (optionally skip runtime cleanup)
188
+ router.delete('/:id', async (req, res) => {
189
+ try {
190
+ const cleanup = req.query.cleanup !== 'false';
191
+ const result = await buildingService.deleteBuilding(req.params.id, emit, { cleanup });
192
+ if (!result.ok) {
193
+ res.status(result.status).json({ error: result.error });
194
+ return;
195
+ }
196
+ res.json({ deleted: true });
197
+ }
198
+ catch (err) {
199
+ log.error(`Failed to delete building ${req.params.id}:`, err);
200
+ res.status(500).json({ error: err.message });
201
+ }
202
+ });
203
+ // ─── Controls ─────────────────────────────────────────────────────────────────
204
+ // POST /api/buildings/:id/command — body { command }
205
+ router.post('/:id/command', async (req, res) => {
206
+ const command = req.body?.command;
207
+ if (!command || !VALID_COMMANDS.has(command)) {
208
+ res.status(400).json({
209
+ error: `command must be one of: ${[...VALID_COMMANDS].join(', ')}`,
210
+ });
211
+ return;
212
+ }
213
+ try {
214
+ const result = await buildingService.executeCommand(req.params.id, command, emit);
215
+ if (!result.success && result.error?.includes('not found')) {
216
+ res.status(404).json({ error: result.error });
217
+ return;
218
+ }
219
+ res.json(result);
220
+ }
221
+ catch (err) {
222
+ log.error(`Command ${command} failed for ${req.params.id}:`, err);
223
+ res.status(500).json({ error: err.message });
224
+ }
225
+ });
226
+ // GET /api/buildings/:id/logs?lines=200&service=foo — snapshot logs
227
+ router.get('/:id/logs', async (req, res) => {
228
+ const lines = Number.parseInt(String(req.query.lines ?? '200'), 10);
229
+ const service = typeof req.query.service === 'string' ? req.query.service : undefined;
230
+ try {
231
+ const result = await buildingService.getBuildingLogs(req.params.id, Number.isFinite(lines) && lines > 0 ? Math.min(lines, 5000) : 200, service);
232
+ if (!result.ok) {
233
+ res.status(result.status).json({ error: result.error });
234
+ return;
235
+ }
236
+ res.json({ source: result.source, logs: result.logs, lines: result.logs.split('\n').length });
237
+ }
238
+ catch (err) {
239
+ log.error(`Logs fetch failed for ${req.params.id}:`, err);
240
+ res.status(500).json({ error: err.message });
241
+ }
242
+ });
243
+ // POST /api/buildings/:id/sync-status — force PM2/Docker status refresh
244
+ router.post('/:id/sync-status', async (req, res) => {
245
+ const building = buildingService.getBuilding(req.params.id);
246
+ if (!building) {
247
+ res.status(404).json({ error: `Building not found: ${req.params.id}` });
248
+ return;
249
+ }
250
+ try {
251
+ if (building.pm2?.enabled) {
252
+ await buildingService.syncPM2Status(building.id, emit);
253
+ const refreshed = buildingService.getBuilding(building.id);
254
+ res.json({ source: 'pm2', status: refreshed?.status, pm2Status: refreshed?.pm2Status });
255
+ return;
256
+ }
257
+ if (building.docker?.enabled) {
258
+ await buildingService.syncDockerStatus(building.id, emit);
259
+ const refreshed = buildingService.getBuilding(building.id);
260
+ res.json({ source: 'docker', status: refreshed?.status, dockerStatus: refreshed?.dockerStatus });
261
+ return;
262
+ }
263
+ if (building.terminal?.enabled) {
264
+ const status = terminalService.getTerminalStatus(building);
265
+ res.json({ source: 'terminal', status: status ? 'running' : 'stopped', terminalStatus: status });
266
+ return;
267
+ }
268
+ res.json({ source: 'none', status: building.status });
269
+ }
270
+ catch (err) {
271
+ log.error(`sync-status failed for ${req.params.id}:`, err);
272
+ res.status(500).json({ error: err.message });
273
+ }
274
+ });
275
+ // POST /api/buildings/:id/subordinates — body { subordinateBuildingIds: string[] }
276
+ router.post('/:id/subordinates', async (req, res) => {
277
+ const ids = req.body?.subordinateBuildingIds;
278
+ if (!Array.isArray(ids) || ids.some((s) => typeof s !== 'string')) {
279
+ res.status(400).json({ error: 'subordinateBuildingIds must be an array of strings' });
280
+ return;
281
+ }
282
+ try {
283
+ const result = await buildingService.assignSubordinates(req.params.id, ids, emit);
284
+ if (!result.ok) {
285
+ res.status(result.status).json({
286
+ error: result.status === 404 ? 'Not found' : 'Invalid request',
287
+ errors: result.errors,
288
+ });
289
+ return;
290
+ }
291
+ res.json(redactBuilding(result.building));
292
+ }
293
+ catch (err) {
294
+ log.error(`assignSubordinates failed for ${req.params.id}:`, err);
295
+ res.status(500).json({ error: err.message });
296
+ }
297
+ });
298
+ export default router;
@@ -24,6 +24,7 @@ import eventRouter from './event-routes.js';
24
24
  import workflowRouter from './workflow-routes.js';
25
25
  import sessionsRouter from './sessions.js';
26
26
  import databaseRouter from './database.js';
27
+ import buildingsRouter, { setBroadcast as setBuildingsBroadcast } from './buildings.js';
27
28
  import { getPlugins } from '../integrations/integration-registry.js';
28
29
  const router = Router();
29
30
  // Health check
@@ -50,6 +51,7 @@ router.use('/events', eventRouter);
50
51
  router.use('/workflows', workflowRouter);
51
52
  router.use('/sessions', sessionsRouter);
52
53
  router.use('/database', databaseRouter);
54
+ router.use('/buildings', buildingsRouter);
53
55
  // Integration plugin routes (e.g. /api/slack/*, /api/documents/*, /api/jira/*)
54
56
  // Uses lazy lookup so plugins can be registered after route setup
55
57
  router.use((req, res, next) => {
@@ -68,5 +70,5 @@ router.use('/config', raw({ type: 'application/zip', limit: '100mb' }), configRo
68
70
  // Permission routes are mounted at root level since they're called as /api/permission-request
69
71
  router.use('/', permissionsRouter);
70
72
  // Export the broadcast setters for WebSocket handler to use
71
- export { setNotificationBroadcast, setExecBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast };
73
+ export { setNotificationBroadcast, setExecBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast, setBuildingsBroadcast };
72
74
  export default router;