mstro-app 0.4.38 → 0.4.43

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 (198) hide show
  1. package/bin/commands/login.js +17 -7
  2. package/bin/commands/logout.js +14 -6
  3. package/bin/commands/status.js +9 -3
  4. package/bin/commands/whoami.js +10 -4
  5. package/bin/mstro.js +11 -1
  6. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
  7. package/dist/server/cli/headless/claude-invoker-stream.js +1 -0
  8. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
  9. package/dist/server/cli/headless/index.d.ts +1 -0
  10. package/dist/server/cli/headless/index.d.ts.map +1 -1
  11. package/dist/server/cli/headless/index.js +2 -0
  12. package/dist/server/cli/headless/index.js.map +1 -1
  13. package/dist/server/cli/headless/resilient-runner.d.ts +47 -0
  14. package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -0
  15. package/dist/server/cli/headless/resilient-runner.js +234 -0
  16. package/dist/server/cli/headless/resilient-runner.js.map +1 -0
  17. package/dist/server/cli/headless/retry-strategies.d.ts +44 -0
  18. package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -0
  19. package/dist/server/cli/headless/retry-strategies.js +262 -0
  20. package/dist/server/cli/headless/retry-strategies.js.map +1 -0
  21. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  22. package/dist/server/cli/headless/stall-assessor.js +5 -0
  23. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  24. package/dist/server/cli/headless/tool-watchdog.d.ts +2 -0
  25. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  26. package/dist/server/cli/headless/tool-watchdog.js +31 -4
  27. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  28. package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
  29. package/dist/server/cli/improvisation-retry.js +1 -30
  30. package/dist/server/cli/improvisation-retry.js.map +1 -1
  31. package/dist/server/cli/improvisation-session-manager.d.ts +1 -0
  32. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  33. package/dist/server/cli/improvisation-session-manager.js +16 -3
  34. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  35. package/dist/server/cli/prompt-builders.d.ts.map +1 -1
  36. package/dist/server/cli/prompt-builders.js +31 -13
  37. package/dist/server/cli/prompt-builders.js.map +1 -1
  38. package/dist/server/index.js +1 -9
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/mcp/bouncer-cli.js +5 -4
  41. package/dist/server/mcp/bouncer-cli.js.map +1 -1
  42. package/dist/server/mcp/bouncer-haiku.js +1 -1
  43. package/dist/server/mcp/bouncer-haiku.js.map +1 -1
  44. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  45. package/dist/server/mcp/bouncer-integration.js +14 -8
  46. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  47. package/dist/server/mcp/security-patterns.js +1 -1
  48. package/dist/server/mcp/security-patterns.js.map +1 -1
  49. package/dist/server/services/plan/composer.d.ts.map +1 -1
  50. package/dist/server/services/plan/composer.js +19 -9
  51. package/dist/server/services/plan/composer.js.map +1 -1
  52. package/dist/server/services/plan/executor.d.ts +6 -1
  53. package/dist/server/services/plan/executor.d.ts.map +1 -1
  54. package/dist/server/services/plan/executor.js +158 -76
  55. package/dist/server/services/plan/executor.js.map +1 -1
  56. package/dist/server/services/plan/front-matter.d.ts +1 -0
  57. package/dist/server/services/plan/front-matter.d.ts.map +1 -1
  58. package/dist/server/services/plan/front-matter.js +6 -0
  59. package/dist/server/services/plan/front-matter.js.map +1 -1
  60. package/dist/server/services/plan/issue-classification.d.ts +11 -0
  61. package/dist/server/services/plan/issue-classification.d.ts.map +1 -0
  62. package/dist/server/services/plan/issue-classification.js +20 -0
  63. package/dist/server/services/plan/issue-classification.js.map +1 -0
  64. package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
  65. package/dist/server/services/plan/issue-prompt-builder.js +10 -5
  66. package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
  67. package/dist/server/services/plan/issue-retry.d.ts +0 -5
  68. package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
  69. package/dist/server/services/plan/issue-retry.js +12 -241
  70. package/dist/server/services/plan/issue-retry.js.map +1 -1
  71. package/dist/server/services/plan/parser-core.d.ts.map +1 -1
  72. package/dist/server/services/plan/parser-core.js +1 -0
  73. package/dist/server/services/plan/parser-core.js.map +1 -1
  74. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  75. package/dist/server/services/plan/review-gate.js +9 -6
  76. package/dist/server/services/plan/review-gate.js.map +1 -1
  77. package/dist/server/services/plan/types.d.ts +1 -0
  78. package/dist/server/services/plan/types.d.ts.map +1 -1
  79. package/dist/server/services/platform-credentials.d.ts.map +1 -1
  80. package/dist/server/services/platform-credentials.js +11 -4
  81. package/dist/server/services/platform-credentials.js.map +1 -1
  82. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  83. package/dist/server/services/terminal/pty-manager.js +7 -1
  84. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  85. package/dist/server/services/websocket/handler-context.d.ts +2 -0
  86. package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
  87. package/dist/server/services/websocket/handler.d.ts +2 -0
  88. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  89. package/dist/server/services/websocket/handler.js +18 -7
  90. package/dist/server/services/websocket/handler.js.map +1 -1
  91. package/dist/server/services/websocket/plan-execution-handlers.js +6 -6
  92. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
  93. package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -1
  94. package/dist/server/services/websocket/quality-fix-agent.js +90 -42
  95. package/dist/server/services/websocket/quality-fix-agent.js.map +1 -1
  96. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  97. package/dist/server/services/websocket/quality-handlers.js +48 -7
  98. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  99. package/dist/server/services/websocket/quality-persistence.d.ts +22 -0
  100. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
  101. package/dist/server/services/websocket/quality-persistence.js +48 -1
  102. package/dist/server/services/websocket/quality-persistence.js.map +1 -1
  103. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
  104. package/dist/server/services/websocket/quality-review-agent.js +74 -32
  105. package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
  106. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
  107. package/dist/server/services/websocket/quality-tools.js +18 -18
  108. package/dist/server/services/websocket/quality-tools.js.map +1 -1
  109. package/dist/server/services/websocket/skill-handlers.d.ts +3 -1
  110. package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
  111. package/dist/server/services/websocket/skill-handlers.js +52 -41
  112. package/dist/server/services/websocket/skill-handlers.js.map +1 -1
  113. package/dist/server/services/websocket/skill-watcher.d.ts +17 -0
  114. package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -0
  115. package/dist/server/services/websocket/skill-watcher.js +85 -0
  116. package/dist/server/services/websocket/skill-watcher.js.map +1 -0
  117. package/dist/server/services/websocket/types.d.ts +2 -268
  118. package/dist/server/services/websocket/types.d.ts.map +1 -1
  119. package/dist/server/services/websocket/types.js +0 -4
  120. package/dist/server/services/websocket/types.js.map +1 -1
  121. package/package.json +1 -1
  122. package/server/cli/headless/claude-invoker-stream.ts +1 -0
  123. package/server/cli/headless/index.ts +2 -0
  124. package/server/cli/headless/resilient-runner.ts +354 -0
  125. package/server/cli/headless/retry-strategies.ts +330 -0
  126. package/server/cli/headless/stall-assessor.ts +5 -0
  127. package/server/cli/headless/tool-watchdog.ts +40 -4
  128. package/server/cli/improvisation-retry.ts +1 -32
  129. package/server/cli/improvisation-session-manager.ts +17 -3
  130. package/server/cli/prompt-builders.ts +33 -12
  131. package/server/index.ts +1 -9
  132. package/server/mcp/bouncer-cli.ts +5 -4
  133. package/server/mcp/bouncer-haiku.ts +1 -1
  134. package/server/mcp/bouncer-integration.ts +15 -8
  135. package/server/mcp/security-patterns.ts +1 -1
  136. package/server/services/plan/agents/code-review.md +109 -0
  137. package/server/services/plan/agents/commit-message.md +26 -0
  138. package/server/services/plan/agents/execute-issue.md +10 -1
  139. package/server/services/plan/agents/fix-quality.md +24 -0
  140. package/server/services/plan/agents/pr-description.md +28 -0
  141. package/server/services/plan/composer.ts +20 -9
  142. package/server/services/plan/executor.ts +160 -76
  143. package/server/services/plan/front-matter.ts +7 -0
  144. package/server/services/plan/issue-classification.ts +21 -0
  145. package/server/services/plan/issue-prompt-builder.ts +11 -5
  146. package/server/services/plan/issue-retry.ts +15 -330
  147. package/server/services/plan/parser-core.ts +1 -0
  148. package/server/services/plan/review-gate.ts +9 -6
  149. package/server/services/plan/types.ts +3 -0
  150. package/server/services/platform-credentials.ts +10 -4
  151. package/server/services/terminal/pty-manager.ts +7 -1
  152. package/server/services/websocket/handler-context.ts +2 -0
  153. package/server/services/websocket/handler.ts +18 -8
  154. package/server/services/websocket/plan-execution-handlers.ts +7 -7
  155. package/server/services/websocket/quality-fix-agent.ts +86 -44
  156. package/server/services/websocket/quality-handlers.ts +48 -7
  157. package/server/services/websocket/quality-persistence.ts +75 -1
  158. package/server/services/websocket/quality-review-agent.ts +70 -31
  159. package/server/services/websocket/quality-tools.ts +16 -14
  160. package/server/services/websocket/skill-handlers.ts +50 -40
  161. package/server/services/websocket/skill-watcher.ts +79 -0
  162. package/server/services/websocket/types.ts +0 -311
  163. package/dist/server/services/deploy/ai-broker.d.ts +0 -63
  164. package/dist/server/services/deploy/ai-broker.d.ts.map +0 -1
  165. package/dist/server/services/deploy/ai-broker.js +0 -360
  166. package/dist/server/services/deploy/ai-broker.js.map +0 -1
  167. package/dist/server/services/deploy/board-execution-handler.d.ts +0 -114
  168. package/dist/server/services/deploy/board-execution-handler.d.ts.map +0 -1
  169. package/dist/server/services/deploy/board-execution-handler.js +0 -621
  170. package/dist/server/services/deploy/board-execution-handler.js.map +0 -1
  171. package/dist/server/services/deploy/credentials.d.ts +0 -35
  172. package/dist/server/services/deploy/credentials.d.ts.map +0 -1
  173. package/dist/server/services/deploy/credentials.js +0 -177
  174. package/dist/server/services/deploy/credentials.js.map +0 -1
  175. package/dist/server/services/deploy/deploy-ai-service.d.ts +0 -107
  176. package/dist/server/services/deploy/deploy-ai-service.d.ts.map +0 -1
  177. package/dist/server/services/deploy/deploy-ai-service.js +0 -294
  178. package/dist/server/services/deploy/deploy-ai-service.js.map +0 -1
  179. package/dist/server/services/deploy/headless-session-handler.d.ts +0 -94
  180. package/dist/server/services/deploy/headless-session-handler.d.ts.map +0 -1
  181. package/dist/server/services/deploy/headless-session-handler.js +0 -266
  182. package/dist/server/services/deploy/headless-session-handler.js.map +0 -1
  183. package/dist/server/services/websocket/deploy-handlers.d.ts +0 -14
  184. package/dist/server/services/websocket/deploy-handlers.d.ts.map +0 -1
  185. package/dist/server/services/websocket/deploy-handlers.js +0 -409
  186. package/dist/server/services/websocket/deploy-handlers.js.map +0 -1
  187. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +0 -11
  188. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +0 -1
  189. package/dist/server/services/websocket/handlers/deploy-handlers.js +0 -176
  190. package/dist/server/services/websocket/handlers/deploy-handlers.js.map +0 -1
  191. package/server/cli/headless/RESEARCH.md +0 -627
  192. package/server/services/deploy/ai-broker.ts +0 -512
  193. package/server/services/deploy/board-execution-handler.ts +0 -847
  194. package/server/services/deploy/credentials.ts +0 -200
  195. package/server/services/deploy/deploy-ai-service.ts +0 -401
  196. package/server/services/deploy/headless-session-handler.ts +0 -414
  197. package/server/services/websocket/deploy-handlers.ts +0 -544
  198. package/server/services/websocket/handlers/deploy-handlers.ts +0 -228
@@ -1,544 +0,0 @@
1
- // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
- // Licensed under the MIT License. See LICENSE file for details.
3
-
4
- /**
5
- * Deploy Handlers — WebSocket message router for deployment management.
6
- *
7
- * Handles deployCreate, deployStop, deployResume, deployDelete,
8
- * deployList, deployGetStatus, deployUpdateConfig, deploySetApiKey,
9
- * and delegates deployHttpRequest to the HTTP relay handler.
10
- */
11
-
12
- import { connect } from 'node:net';
13
- import {
14
- checkValidationRateLimit,
15
- getApiKeyStatus,
16
- storeApiKey,
17
- validateAnthropicKey,
18
- } from '../deploy/credentials.js';
19
- import { getCredentials } from '../platform-credentials.js';
20
- import type { HandlerContext } from './handler-context.js';
21
- import { handleDeployHttpRequest } from './handlers/deploy-handlers.js';
22
- import type {
23
- DeployCreateData,
24
- DeployDeleteData,
25
- DeployGetStatusData,
26
- DeploymentInfo,
27
- DeployResumeData,
28
- DeploySetApiKeyData,
29
- DeployStopData,
30
- DeployUpdateConfigData,
31
- WebSocketMessage,
32
- WSContext,
33
- } from './types.js';
34
-
35
- // ── Platform API ─────────��───────────────────────────────────
36
-
37
- const PLATFORM_URL = process.env.PLATFORM_URL || 'https://api.mstro.app';
38
-
39
- async function platformFetch(
40
- path: string,
41
- options: RequestInit = {},
42
- ): Promise<Response> {
43
- const creds = getCredentials();
44
- if (!creds) {
45
- throw new Error('Not authenticated. Run `mstro login` first.');
46
- }
47
- const url = `${PLATFORM_URL}${path}`;
48
- return fetch(url, {
49
- ...options,
50
- headers: {
51
- 'Content-Type': 'application/json',
52
- Authorization: `Bearer ${creds.token}`,
53
- ...options.headers,
54
- },
55
- signal: AbortSignal.timeout(15_000),
56
- });
57
- }
58
-
59
- // ── Local deployment state ────��──────────────────────────────
60
-
61
- interface LocalDeployment {
62
- info: DeploymentInfo;
63
- proxyActive: boolean;
64
- }
65
-
66
- const localDeployments = new Map<string, LocalDeployment>();
67
-
68
- function toDeploymentInfo(serverRow: Record<string, unknown>): DeploymentInfo {
69
- return {
70
- deploymentId: serverRow.id as string,
71
- config: {
72
- subdomain: serverRow.subdomain as string,
73
- port: (serverRow.port as number) || 0,
74
- aiEnabled: false,
75
- },
76
- status: (serverRow.status as string) === 'active' ? 'running' : (serverRow.status as string) === 'stopped' ? 'stopped' : 'starting',
77
- url: `https://${serverRow.subdomain as string}.mstro.app`,
78
- createdAt: (serverRow.createdAt as string) || new Date().toISOString(),
79
- updatedAt: (serverRow.updatedAt as string) || new Date().toISOString(),
80
- };
81
- }
82
-
83
- // ── Port validation ──────────────────────────────────────────
84
-
85
- function validatePort(port: number): Promise<boolean> {
86
- return new Promise((resolve) => {
87
- const socket = connect({ host: '127.0.0.1', port }, () => {
88
- socket.destroy();
89
- resolve(true);
90
- });
91
- socket.on('error', () => {
92
- socket.destroy();
93
- resolve(false);
94
- });
95
- socket.setTimeout(2_000, () => {
96
- socket.destroy();
97
- resolve(false);
98
- });
99
- });
100
- }
101
-
102
- // ── Error helper ───────���─────────────────────────────────────
103
-
104
- function sendDeployError(
105
- ctx: HandlerContext,
106
- ws: WSContext,
107
- error: string,
108
- deploymentId?: string,
109
- ): void {
110
- ctx.send(ws, {
111
- type: 'deployError',
112
- data: { error, deploymentId },
113
- });
114
- }
115
-
116
- // ── Message router ───────────────────────────────────────────
117
-
118
- export function handleDeployMessage(
119
- ctx: HandlerContext,
120
- ws: WSContext,
121
- msg: WebSocketMessage,
122
- _tabId: string,
123
- _workingDir: string,
124
- _permission?: 'view',
125
- ): void {
126
- const handlers: Record<string, () => void | Promise<void>> = {
127
- deployCreate: () => handleDeployCreate(ctx, ws, msg),
128
- deployStop: () => handleDeployStop(ctx, ws, msg),
129
- deployResume: () => handleDeployResume(ctx, ws, msg),
130
- deployDelete: () => handleDeployDelete(ctx, ws, msg),
131
- deployList: () => handleDeployList(ctx, ws),
132
- deployGetStatus: () => handleDeployGetStatus(ctx, ws, msg),
133
- deployUpdateConfig: () => handleDeployUpdateConfig(ctx, ws, msg),
134
- deploySetApiKey: () => handleDeploySetApiKey(ctx, ws, msg),
135
- deployValidateApiKey: () => handleDeployValidateApiKey(ctx, ws),
136
- deployHttpRequest: () => handleDeployHttpRequest(ctx, ws, msg),
137
- };
138
-
139
- const handler = handlers[msg.type];
140
- if (!handler) return;
141
-
142
- try {
143
- const result = handler();
144
- // Handle async handlers — catch rejections
145
- if (result instanceof Promise) {
146
- result.catch((error: unknown) => {
147
- const errMsg = error instanceof Error ? error.message : String(error);
148
- sendDeployError(ctx, ws, errMsg);
149
- });
150
- }
151
- } catch (error) {
152
- const errMsg = error instanceof Error ? error.message : String(error);
153
- sendDeployError(ctx, ws, errMsg);
154
- }
155
- }
156
-
157
- // ── Handler implementations ──────────────────────────────────
158
-
159
- /**
160
- * deployCreate — validate port, call platform API, store local state.
161
- */
162
- async function handleDeployCreate(
163
- ctx: HandlerContext,
164
- ws: WSContext,
165
- msg: WebSocketMessage,
166
- ): Promise<void> {
167
- const data = msg.data as DeployCreateData | undefined;
168
- if (!data?.subdomain || !data?.port) {
169
- sendDeployError(ctx, ws, 'subdomain and port are required');
170
- return;
171
- }
172
-
173
- // Validate local port is in use
174
- const portInUse = await validatePort(data.port);
175
- if (!portInUse) {
176
- sendDeployError(ctx, ws, `No service running on port ${data.port}. Start your app first.`);
177
- return;
178
- }
179
-
180
- // Call platform server API to create deployment record
181
- const creds = getCredentials();
182
- if (!creds) {
183
- sendDeployError(ctx, ws, 'Not authenticated. Run `mstro login` first.');
184
- return;
185
- }
186
-
187
- const response = await platformFetch('/api/deployments', {
188
- method: 'POST',
189
- body: JSON.stringify({
190
- orchestraPairingId: creds.clientId,
191
- subdomain: data.subdomain,
192
- port: data.port,
193
- }),
194
- });
195
-
196
- if (!response.ok) {
197
- const body = await response.json().catch(() => ({ error: 'Failed to create deployment' })) as { error?: string };
198
- sendDeployError(ctx, ws, body.error || `Server returned ${response.status}`);
199
- return;
200
- }
201
-
202
- const { deployment: serverDeployment } = await response.json() as { deployment: Record<string, unknown> };
203
- const info = toDeploymentInfo(serverDeployment);
204
- info.config.port = data.port;
205
- info.config.aiEnabled = data.aiEnabled ?? false;
206
- info.status = 'running';
207
-
208
- // Store local state
209
- localDeployments.set(info.deploymentId, { info, proxyActive: true });
210
-
211
- ctx.send(ws, {
212
- type: 'deployCreated',
213
- data: { deployment: info },
214
- });
215
-
216
- // Notify the platform server that this deployment is now active
217
- ctx.broadcastToAll({
218
- type: 'deployStatus',
219
- data: { deploymentId: info.deploymentId, status: 'running' },
220
- });
221
- }
222
-
223
- /**
224
- * deployStop — toggle local HTTP proxy off.
225
- */
226
- async function handleDeployStop(
227
- ctx: HandlerContext,
228
- ws: WSContext,
229
- msg: WebSocketMessage,
230
- ): Promise<void> {
231
- const data = msg.data as DeployStopData | undefined;
232
- if (!data?.deploymentId) {
233
- sendDeployError(ctx, ws, 'deploymentId is required');
234
- return;
235
- }
236
-
237
- const local = localDeployments.get(data.deploymentId);
238
- if (!local) {
239
- sendDeployError(ctx, ws, 'Deployment not found locally', data.deploymentId);
240
- return;
241
- }
242
-
243
- local.proxyActive = false;
244
- local.info.status = 'stopped';
245
- local.info.updatedAt = new Date().toISOString();
246
-
247
- // Update server status
248
- await platformFetch(`/api/deployments/${data.deploymentId}`, {
249
- method: 'PATCH',
250
- body: JSON.stringify({ status: 'stopped' }),
251
- }).catch(() => {
252
- // Non-critical: local state is authoritative for stop/resume
253
- });
254
-
255
- ctx.send(ws, {
256
- type: 'deployStopped',
257
- data: { deploymentId: data.deploymentId },
258
- });
259
-
260
- ctx.broadcastToAll({
261
- type: 'deployStatus',
262
- data: { deploymentId: data.deploymentId, status: 'stopped' },
263
- });
264
- }
265
-
266
- /**
267
- * deployResume — toggle local HTTP proxy on.
268
- */
269
- async function handleDeployResume(
270
- ctx: HandlerContext,
271
- ws: WSContext,
272
- msg: WebSocketMessage,
273
- ): Promise<void> {
274
- const data = msg.data as DeployResumeData | undefined;
275
- if (!data?.deploymentId) {
276
- sendDeployError(ctx, ws, 'deploymentId is required');
277
- return;
278
- }
279
-
280
- const local = localDeployments.get(data.deploymentId);
281
- if (!local) {
282
- sendDeployError(ctx, ws, 'Deployment not found locally', data.deploymentId);
283
- return;
284
- }
285
-
286
- // Validate the port is still in use before resuming
287
- const portInUse = await validatePort(local.info.config.port);
288
- if (!portInUse) {
289
- sendDeployError(
290
- ctx,
291
- ws,
292
- `No service running on port ${local.info.config.port}. Start your app first.`,
293
- data.deploymentId,
294
- );
295
- return;
296
- }
297
-
298
- local.proxyActive = true;
299
- local.info.status = 'running';
300
- local.info.updatedAt = new Date().toISOString();
301
-
302
- // Update server status
303
- await platformFetch(`/api/deployments/${data.deploymentId}`, {
304
- method: 'PATCH',
305
- body: JSON.stringify({ status: 'active' }),
306
- }).catch(() => {
307
- // Non-critical
308
- });
309
-
310
- ctx.send(ws, {
311
- type: 'deployResumed',
312
- data: { deploymentId: data.deploymentId },
313
- });
314
-
315
- ctx.broadcastToAll({
316
- type: 'deployStatus',
317
- data: { deploymentId: data.deploymentId, status: 'running' },
318
- });
319
- }
320
-
321
- /**
322
- * deployList — return all local deployment states.
323
- */
324
- function handleDeployList(ctx: HandlerContext, ws: WSContext): void {
325
- const deployments: DeploymentInfo[] = [];
326
- for (const local of localDeployments.values()) {
327
- deployments.push(local.info);
328
- }
329
- ctx.send(ws, {
330
- type: 'deployListResult',
331
- data: { deployments },
332
- });
333
- }
334
-
335
- /**
336
- * deployGetStatus — return detailed status for one deployment.
337
- */
338
- function handleDeployGetStatus(
339
- ctx: HandlerContext,
340
- ws: WSContext,
341
- msg: WebSocketMessage,
342
- ): void {
343
- const data = msg.data as DeployGetStatusData | undefined;
344
- if (!data?.deploymentId) {
345
- sendDeployError(ctx, ws, 'deploymentId is required');
346
- return;
347
- }
348
-
349
- const local = localDeployments.get(data.deploymentId);
350
- if (!local) {
351
- sendDeployError(ctx, ws, 'Deployment not found', data.deploymentId);
352
- return;
353
- }
354
-
355
- ctx.send(ws, {
356
- type: 'deployStatusResult',
357
- data: { deployment: local.info },
358
- });
359
- }
360
-
361
- /**
362
- * deployUpdateConfig — update config locally and on server.
363
- */
364
- async function handleDeployUpdateConfig(
365
- ctx: HandlerContext,
366
- ws: WSContext,
367
- msg: WebSocketMessage,
368
- ): Promise<void> {
369
- const data = msg.data as DeployUpdateConfigData | undefined;
370
- if (!data?.deploymentId) {
371
- sendDeployError(ctx, ws, 'deploymentId is required');
372
- return;
373
- }
374
-
375
- const local = localDeployments.get(data.deploymentId);
376
- if (!local) {
377
- sendDeployError(ctx, ws, 'Deployment not found', data.deploymentId);
378
- return;
379
- }
380
-
381
- // Apply config updates locally
382
- if (data.config.subdomain !== undefined) {
383
- local.info.config.subdomain = data.config.subdomain;
384
- local.info.url = `https://${data.config.subdomain}.mstro.app`;
385
- }
386
- if (data.config.port !== undefined) {
387
- local.info.config.port = data.config.port;
388
- }
389
- if (data.config.aiEnabled !== undefined) {
390
- local.info.config.aiEnabled = data.config.aiEnabled;
391
- }
392
- if (data.config.customDomain !== undefined) {
393
- local.info.config.customDomain = data.config.customDomain;
394
- }
395
- local.info.updatedAt = new Date().toISOString();
396
-
397
- // Sync to platform server
398
- const serverUpdates: Record<string, unknown> = {};
399
- if (data.config.subdomain !== undefined) {
400
- serverUpdates.subdomain = data.config.subdomain;
401
- }
402
- if (data.config.port !== undefined) {
403
- serverUpdates.port = data.config.port;
404
- }
405
-
406
- if (Object.keys(serverUpdates).length > 0) {
407
- const response = await platformFetch(`/api/deployments/${data.deploymentId}`, {
408
- method: 'PATCH',
409
- body: JSON.stringify(serverUpdates),
410
- });
411
-
412
- if (!response.ok) {
413
- const body = await response.json().catch(() => ({ error: 'Failed to update deployment' })) as { error?: string };
414
- sendDeployError(ctx, ws, body.error || `Server returned ${response.status}`, data.deploymentId);
415
- return;
416
- }
417
- }
418
-
419
- ctx.send(ws, {
420
- type: 'deployConfigUpdated',
421
- data: { deployment: local.info },
422
- });
423
- }
424
-
425
- /**
426
- * deploySetApiKey — validate with Anthropic API, encrypt, and store locally.
427
- * The API key is NEVER logged, even partially.
428
- * Rate limited to max 10 validation attempts per minute.
429
- */
430
- async function handleDeploySetApiKey(
431
- ctx: HandlerContext,
432
- ws: WSContext,
433
- msg: WebSocketMessage,
434
- ): Promise<void> {
435
- const data = msg.data as DeploySetApiKeyData | undefined;
436
- if (!data?.apiKey) {
437
- sendDeployError(ctx, ws, 'apiKey is required');
438
- return;
439
- }
440
-
441
- // Enforce rate limit before calling the Anthropic API
442
- if (!checkValidationRateLimit()) {
443
- ctx.send(ws, {
444
- type: 'deployApiKeyStatus',
445
- data: { status: 'rate_limited' as const },
446
- });
447
- return;
448
- }
449
-
450
- // Validate the key with Anthropic API
451
- const isValid = await validateAnthropicKey(data.apiKey);
452
- if (!isValid) {
453
- ctx.send(ws, {
454
- type: 'deployApiKeyStatus',
455
- data: { status: 'invalid' as const },
456
- });
457
- return;
458
- }
459
-
460
- // Encrypt and store locally
461
- const { lastFour } = storeApiKey(data.apiKey);
462
-
463
- ctx.send(ws, {
464
- type: 'deployApiKeyStatus',
465
- data: { status: 'valid' as const, lastFour, source: 'stored' as const },
466
- });
467
- }
468
-
469
- /**
470
- * deployValidateApiKey — check current key status (env var or stored).
471
- * When env var is detected, source is 'env' so the UI can show
472
- * "detected from environment".
473
- */
474
- function handleDeployValidateApiKey(ctx: HandlerContext, ws: WSContext): void {
475
- const keyStatus = getApiKeyStatus();
476
-
477
- ctx.send(ws, {
478
- type: 'deployApiKeyStatus',
479
- data: keyStatus,
480
- });
481
- }
482
-
483
- /**
484
- * deployDelete — stop proxy and call server API to delete.
485
- */
486
- async function handleDeployDelete(
487
- ctx: HandlerContext,
488
- ws: WSContext,
489
- msg: WebSocketMessage,
490
- ): Promise<void> {
491
- const data = msg.data as DeployDeleteData | undefined;
492
- if (!data?.deploymentId) {
493
- sendDeployError(ctx, ws, 'deploymentId is required');
494
- return;
495
- }
496
-
497
- // Stop local proxy
498
- const local = localDeployments.get(data.deploymentId);
499
- if (local) {
500
- local.proxyActive = false;
501
- }
502
-
503
- // Call server API to delete
504
- const response = await platformFetch(`/api/deployments/${data.deploymentId}`, {
505
- method: 'DELETE',
506
- });
507
-
508
- if (!response.ok) {
509
- const body = await response.json().catch(() => ({ error: 'Failed to delete deployment' })) as { error?: string };
510
- sendDeployError(ctx, ws, body.error || `Server returned ${response.status}`, data.deploymentId);
511
- return;
512
- }
513
-
514
- // Remove from local state
515
- localDeployments.delete(data.deploymentId);
516
-
517
- ctx.send(ws, {
518
- type: 'deployDeleted',
519
- data: { deploymentId: data.deploymentId },
520
- });
521
-
522
- ctx.broadcastToAll({
523
- type: 'deployStatus',
524
- data: { deploymentId: data.deploymentId, status: 'stopped' },
525
- });
526
- }
527
-
528
- /**
529
- * Check if a deployment's proxy is active. Used by the HTTP relay
530
- * to decide whether to forward incoming requests.
531
- */
532
- export function isDeploymentProxyActive(deploymentId: string): boolean {
533
- const local = localDeployments.get(deploymentId);
534
- return local?.proxyActive ?? false;
535
- }
536
-
537
- /**
538
- * Get the local port for a deployment. Used by the HTTP relay
539
- * to forward requests to the correct local service.
540
- */
541
- export function getDeploymentPort(deploymentId: string): number | null {
542
- const local = localDeployments.get(deploymentId);
543
- return local?.info.config.port ?? null;
544
- }