cdk-local 0.79.0 → 0.81.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.
@@ -5,7 +5,8 @@ import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { Command, Option } from "commander";
7
7
  import { spawn } from "node:child_process";
8
- import { createServer } from "node:http";
8
+ import { connect } from "node:net";
9
+ import { createServer as createServer$1, request } from "node:http";
9
10
  import { EventEmitter } from "node:events";
10
11
 
11
12
  //#region src/local/studio-events.ts
@@ -53,14 +54,17 @@ var StudioEventBus = class {
53
54
  * the studio HTTP server (`startStudioServer`) at `GET /`.
54
55
  *
55
56
  * 3-pane shell (decision D6), framework-free vanilla JS (decision D7):
56
- * - left = target list (from `GET /api/targets`); each runnable
57
- * Lambda has an [Invoke] button and a selected-highlight.
58
- * - center = the WORKSPACE for the selected target: an event composer
59
- * (textarea + Invoke button) with the latest run's Request /
60
- * Response / Logs shown BELOW it, so you can edit and re-invoke
61
- * repeatedly without losing the composer.
62
- * - right = the timeline (history) of every invocation; clicking a
63
- * row loads it back into the workspace.
57
+ * - left = target list (from `GET /api/targets`); each Lambda has an
58
+ * [Invoke] button, each API a [Start] / [Stop] serve control with a
59
+ * `running :port` indicator (slice C1), plus a selected-highlight.
60
+ * - center = the WORKSPACE for the selected target: for a Lambda, an
61
+ * event composer (textarea + Invoke button) with the latest run's
62
+ * Request / Response / Logs shown below; for an API, a Start/Stop
63
+ * control with the served endpoints + streaming logs.
64
+ * - right = the timeline (history) of every invocation AND every
65
+ * captured serve request (slice C2); clicking a Lambda row reloads
66
+ * it into the composer, clicking a captured request row opens a
67
+ * read-only Request / Response detail.
64
68
  *
65
69
  * The center workspace is deliberately adjacent to the left target list
66
70
  * (short eye-travel: pick a target -> compose right next to it), and is
@@ -106,6 +110,13 @@ const STUDIO_CSS = `
106
110
  }
107
111
  .target .invoke-btn:hover { background: #6fe097; }
108
112
  .target.sel .invoke-btn { background: #6fe097; }
113
+ .target .stop-btn {
114
+ padding: 2px 10px; font: 11px ui-monospace, Menlo, monospace; font-weight: 700;
115
+ color: #2a0d0d; background: #e0707a; border: 0; border-radius: 3px; cursor: pointer;
116
+ }
117
+ .target .stop-btn:hover { background: #ec8a92; }
118
+ .target .run-dot { color: #7bd88f; font-size: 11px; white-space: nowrap; }
119
+ .target .run-dot.starting { color: #e0b54e; }
109
120
  .empty { padding: 16px 12px; color: #666; }
110
121
  .row {
111
122
  padding: 5px 12px; border-bottom: 1px solid #222; cursor: pointer;
@@ -137,6 +148,8 @@ const STUDIO_CSS = `
137
148
  .section h3 .ok { color: #7bd88f; }
138
149
  .section h3 .bad { color: #e0707a; }
139
150
  .section pre { margin: 0; white-space: pre-wrap; word-break: break-word; color: #cfcfcf; }
151
+ .endpoint { display: block; color: #6aa9ff; text-decoration: none; padding: 2px 0; }
152
+ .endpoint:hover { text-decoration: underline; }
140
153
  #conn { font-size: 11px; }
141
154
  #conn.up { color: #7bd88f; }
142
155
  #conn.down { color: #e0707a; }
@@ -145,10 +158,14 @@ const STUDIO_SCRIPT = `
145
158
  const KIND_LABEL = { lambda: 'Lambda', api: 'API', alb: 'ALB', ecs: 'ECS', agentcore: 'AgentCore' };
146
159
  const rowsById = new Map(); // invocationId -> timeline row element
147
160
  const invById = new Map(); // invocationId -> latest invocation event
148
- const logsById = new Map(); // invocationId -> [log lines]
161
+ const logsById = new Map(); // invocationId / serve targetId -> [log lines]
149
162
  const targetEls = new Map(); // targetId -> left-pane element
163
+ const serveMeta = new Map(); // serve targetId -> { dot, btnSlot } row controls
164
+ const serveState = new Map(); // serve targetId -> { status, endpoints }
150
165
  let active = null; // { id, kind, ta, btn, msg, result }
151
- let shownInvId = null; // invocation whose result is in the workspace
166
+ let shownInvId = null; // lambda invocation whose result is in the workspace
167
+ let shownServeId = null; // serve target whose workspace is shown
168
+ let shownDetailId = null; // captured request whose read-only detail is shown
152
169
 
153
170
  function el(tag, cls, text) {
154
171
  const e = document.createElement(tag);
@@ -169,20 +186,32 @@ const STUDIO_SCRIPT = `
169
186
  pane.appendChild(el('div', 'group-title', group.title));
170
187
  for (const entry of group.entries) {
171
188
  total += 1;
172
- // Slice B: only Lambda targets are runnable from the UI (single-shot
173
- // invoke). Other kinds list but are not yet selectable.
174
- const runnable = group.kind === 'lambda';
189
+ // Lambda targets are single-shot invokes; API targets are
190
+ // long-running serves (slice C1). Other kinds list but are not
191
+ // yet runnable.
192
+ const runnable = group.kind === 'lambda' || group.kind === 'api';
175
193
  const t = el('div', runnable ? 'target runnable' : 'target');
176
194
  const name = el('span', 'name', entry.id);
177
195
  name.title = entry.id; // full path on hover even when truncated
178
196
  t.appendChild(name);
179
197
  t.appendChild(el('span', 'kind', '(' + (KIND_LABEL[group.kind] || group.kind) + ')'));
180
- if (runnable) {
198
+ if (group.kind === 'lambda') {
181
199
  const btn = el('button', 'invoke-btn', 'Invoke');
182
- btn.onclick = (e) => { e.stopPropagation(); selectTarget(entry.id, group.kind); };
200
+ btn.onclick = (e) => { e.stopPropagation(); selectTarget(entry.id, 'lambda'); };
183
201
  t.appendChild(btn);
184
- t.onclick = () => selectTarget(entry.id, group.kind);
202
+ t.onclick = () => selectTarget(entry.id, 'lambda');
185
203
  targetEls.set(entry.id, t);
204
+ } else if (group.kind === 'api') {
205
+ // A serve target: a running-state dot + a Start/Stop button
206
+ // slot, both refreshed by updateServeRow on serve events.
207
+ const dot = el('span', 'run-dot');
208
+ const btnSlot = el('span', 'btn-slot');
209
+ t.appendChild(dot);
210
+ t.appendChild(btnSlot);
211
+ t.onclick = () => selectTarget(entry.id, 'api');
212
+ targetEls.set(entry.id, t);
213
+ serveMeta.set(entry.id, { dot, btnSlot });
214
+ updateServeRow(entry.id);
186
215
  }
187
216
  pane.appendChild(t);
188
217
  }
@@ -193,6 +222,50 @@ const STUDIO_SCRIPT = `
193
222
  }
194
223
  }
195
224
 
225
+ // Pull any already-running serves (e.g. after a UI reload) so the rows
226
+ // and workspace reflect them without waiting for a fresh serve event.
227
+ async function loadRunning() {
228
+ try {
229
+ const res = await fetch('/api/running');
230
+ const data = await res.json();
231
+ for (const s of (data.running || [])) {
232
+ serveState.set(s.targetId, { status: s.status, endpoints: s.endpoints || [] });
233
+ updateServeRow(s.targetId);
234
+ }
235
+ } catch (err) {
236
+ /* best-effort; the serve SSE stream still drives live updates */
237
+ }
238
+ }
239
+
240
+ function firstPort(endpoints) {
241
+ const u = (endpoints || [])[0];
242
+ if (!u) return '';
243
+ const m = /:(\\d+)/.exec(u);
244
+ return m ? ':' + m[1] : '';
245
+ }
246
+
247
+ function updateServeRow(id) {
248
+ const meta = serveMeta.get(id);
249
+ if (!meta) return;
250
+ const st = serveState.get(id);
251
+ const status = st ? st.status : 'stopped';
252
+ const running = status === 'running';
253
+ const starting = status === 'starting';
254
+ meta.dot.textContent = running ? '● ' + firstPort(st.endpoints) : starting ? '○ starting' : '';
255
+ meta.dot.className = 'run-dot' + (starting ? ' starting' : '');
256
+ meta.btnSlot.innerHTML = '';
257
+ const btn = running || starting
258
+ ? el('button', 'stop-btn', 'Stop')
259
+ : el('button', 'invoke-btn', 'Start');
260
+ btn.onclick = (e) => {
261
+ e.stopPropagation();
262
+ if (running || starting) stopServe(id); else startServe(id);
263
+ };
264
+ meta.btnSlot.appendChild(btn);
265
+ // Refresh the workspace if it is showing this serve.
266
+ if (shownServeId === id) renderServeWorkspace(id);
267
+ }
268
+
196
269
  function highlightTarget(id) {
197
270
  document.querySelectorAll('.target.sel').forEach((n) => n.classList.remove('sel'));
198
271
  const t = targetEls.get(id);
@@ -201,7 +274,105 @@ const STUDIO_SCRIPT = `
201
274
 
202
275
  function selectTarget(id, kind) {
203
276
  highlightTarget(id);
204
- renderComposer(id, kind, '{}');
277
+ shownDetailId = null;
278
+ if (kind === 'api') {
279
+ shownServeId = id;
280
+ shownInvId = null;
281
+ active = null;
282
+ renderServeWorkspace(id);
283
+ } else {
284
+ shownServeId = null;
285
+ renderComposer(id, kind, '{}');
286
+ }
287
+ }
288
+
289
+ async function startServe(id) {
290
+ serveState.set(id, { status: 'starting', endpoints: [] });
291
+ updateServeRow(id);
292
+ try {
293
+ const res = await fetch('/api/run', {
294
+ method: 'POST',
295
+ headers: { 'content-type': 'application/json' },
296
+ body: JSON.stringify({ targetId: id, kind: 'api' }),
297
+ });
298
+ const data = await res.json();
299
+ if (!res.ok) {
300
+ // Roll back the optimistic 'starting' on a rejected start.
301
+ serveState.set(id, { status: 'error', endpoints: [] });
302
+ updateServeRow(id);
303
+ if (shownServeId === id) renderServeWorkspace(id, data.error || ('HTTP ' + res.status));
304
+ }
305
+ // On success the serve SSE 'running' event fills in the endpoints.
306
+ } catch (err) {
307
+ serveState.set(id, { status: 'error', endpoints: [] });
308
+ updateServeRow(id);
309
+ if (shownServeId === id) renderServeWorkspace(id, String(err));
310
+ }
311
+ }
312
+
313
+ async function stopServe(id) {
314
+ try {
315
+ await fetch('/api/stop', {
316
+ method: 'POST',
317
+ headers: { 'content-type': 'application/json' },
318
+ body: JSON.stringify({ targetId: id }),
319
+ });
320
+ // The serve SSE 'stopped' event clears the running state.
321
+ } catch (err) {
322
+ /* the stop SSE event (or a later refresh) reconciles state */
323
+ }
324
+ }
325
+
326
+ function renderServeWorkspace(id, errMsg) {
327
+ const ws = document.getElementById('workspace');
328
+ ws.innerHTML = '';
329
+ const st = serveState.get(id) || { status: 'stopped', endpoints: [] };
330
+ const running = st.status === 'running';
331
+ const starting = st.status === 'starting';
332
+
333
+ const head = el('div', 'composer');
334
+ head.appendChild(el('div', 'target-name', 'Serve ' + id));
335
+ const btn = running || starting
336
+ ? el('button', null, 'Stop')
337
+ : el('button', null, starting ? 'Starting…' : 'Start');
338
+ btn.onclick = () => { if (running || starting) stopServe(id); else startServe(id); };
339
+ head.appendChild(btn);
340
+ if (errMsg) {
341
+ const m = el('div', 'err', errMsg);
342
+ head.appendChild(m);
343
+ }
344
+ ws.appendChild(head);
345
+
346
+ const epSec = el('div', 'section');
347
+ epSec.appendChild(el('h3', null, 'Endpoints'));
348
+ if (running && st.endpoints.length) {
349
+ for (const url of st.endpoints) {
350
+ const link = href(url);
351
+ epSec.appendChild(link);
352
+ }
353
+ } else {
354
+ epSec.appendChild(el('pre', null, starting ? '(starting…)' : '(not running)'));
355
+ }
356
+ ws.appendChild(epSec);
357
+
358
+ const logs = logsById.get(id) || [];
359
+ const logSec = el('div', 'section');
360
+ logSec.appendChild(el('h3', null, 'Logs'));
361
+ logSec.appendChild(el('pre', null, logs.length ? logs.join('\\n') : '(none)'));
362
+ ws.appendChild(logSec);
363
+ }
364
+
365
+ // Build an <a> that opens an http(s) endpoint in a new tab; ws:// URLs
366
+ // are shown as plain text (not navigable in a browser tab).
367
+ function href(url) {
368
+ if (/^https?:/.test(url)) {
369
+ const a = el('a', 'endpoint', url);
370
+ a.href = url;
371
+ a.target = '_blank';
372
+ a.rel = 'noopener';
373
+ return a;
374
+ }
375
+ return el('div', 'endpoint', url);
205
376
  }
206
377
 
207
378
  function renderComposer(id, kind, eventText) {
@@ -228,6 +399,7 @@ const STUDIO_SCRIPT = `
228
399
  active = { id, kind, ta, btn, msg, result };
229
400
  btn.onclick = () => runInvoke();
230
401
  shownInvId = null;
402
+ shownDetailId = null;
231
403
  ta.focus();
232
404
  }
233
405
 
@@ -328,8 +500,9 @@ const STUDIO_SCRIPT = `
328
500
  : '…';
329
501
  statusEl.className = 'status' + (merged.status != null && (merged.status < 200 || merged.status >= 300) ? ' err' : '');
330
502
 
331
- // Live-refresh the workspace result if it is showing this invocation.
503
+ // Live-refresh the workspace if it is showing this invocation.
332
504
  if (shownInvId === ev.id) renderResult(ev.id);
505
+ if (shownDetailId === ev.id) renderCapturedDetail(ev.id);
333
506
  }
334
507
 
335
508
  function loadInvocation(id) {
@@ -339,9 +512,49 @@ const STUDIO_SCRIPT = `
339
512
  const row = rowsById.get(id);
340
513
  if (row) row.classList.add('sel');
341
514
  highlightTarget(ev.target);
342
- renderComposer(ev.target, ev.kind, ev.request != null ? fmt(ev.request) : '{}');
343
- shownInvId = id;
344
- renderResult(id);
515
+ if (ev.kind === 'lambda') {
516
+ // A Lambda invocation row reloads into the re-invokable composer.
517
+ shownDetailId = null;
518
+ shownServeId = null;
519
+ renderComposer(ev.target, ev.kind, ev.request != null ? fmt(ev.request) : '{}');
520
+ shownInvId = id;
521
+ renderResult(id);
522
+ } else {
523
+ // A captured serve request (slice C2) opens a READ-ONLY detail —
524
+ // re-invoking a captured request is Phase 3.
525
+ shownInvId = null;
526
+ shownServeId = null;
527
+ active = null;
528
+ renderCapturedDetail(id);
529
+ }
530
+ }
531
+
532
+ // Read-only Request / Response detail for a captured serve request.
533
+ function renderCapturedDetail(id) {
534
+ shownDetailId = id;
535
+ const ev = invById.get(id);
536
+ const ws = document.getElementById('workspace');
537
+ ws.innerHTML = '';
538
+ if (!ev) return;
539
+
540
+ const head = el('div', 'composer');
541
+ head.appendChild(el('div', 'target-name', (ev.label || 'request') + ' — ' + (ev.target || '')));
542
+ ws.appendChild(head);
543
+
544
+ const reqSec = el('div', 'section');
545
+ reqSec.appendChild(el('h3', null, 'Request'));
546
+ reqSec.appendChild(el('pre', null, ev.request != null ? fmt(ev.request) : '(none)'));
547
+ ws.appendChild(reqSec);
548
+
549
+ const respSec = el('div', 'section');
550
+ const h = el('h3', null, 'Response');
551
+ if (ev.status != null) {
552
+ const cls = ev.status >= 200 && ev.status < 300 ? 'ok' : 'bad';
553
+ h.appendChild(el('span', cls, ' ' + ev.status + (ev.durationMs != null ? ' · ' + ev.durationMs + 'ms' : '')));
554
+ }
555
+ respSec.appendChild(h);
556
+ respSec.appendChild(el('pre', null, ev.response != null ? fmt(ev.response) : '(pending…)'));
557
+ ws.appendChild(respSec);
345
558
  }
346
559
 
347
560
  function connect() {
@@ -350,15 +563,28 @@ const STUDIO_SCRIPT = `
350
563
  es.addEventListener('open', () => { conn.textContent = '● live'; conn.className = 'up'; });
351
564
  es.addEventListener('error', () => { conn.textContent = '● disconnected'; conn.className = 'down'; });
352
565
  es.addEventListener('invocation', (e) => addInvocation(JSON.parse(e.data)));
566
+ es.addEventListener('serve', (e) => onServeEvent(JSON.parse(e.data)));
353
567
  es.addEventListener('log', (e) => {
354
568
  const ev = JSON.parse(e.data);
355
569
  const arr = logsById.get(ev.containerId) || [];
356
570
  arr.push(ev.line);
357
571
  logsById.set(ev.containerId, arr);
358
572
  if (shownInvId === ev.containerId) renderResult(ev.containerId);
573
+ if (shownServeId === ev.containerId) renderServeWorkspace(ev.containerId);
359
574
  });
360
575
  }
361
576
 
577
+ function onServeEvent(ev) {
578
+ // A 'stopped' / 'error' transition clears the running state; otherwise
579
+ // record the latest status + endpoints for the row + workspace.
580
+ if (ev.status === 'stopped' || ev.status === 'error') {
581
+ serveState.set(ev.target, { status: ev.status, endpoints: [] });
582
+ } else {
583
+ serveState.set(ev.target, { status: ev.status, endpoints: ev.endpoints || [] });
584
+ }
585
+ updateServeRow(ev.target);
586
+ }
587
+
362
588
  function initSplitters() {
363
589
  const main = document.querySelector('main');
364
590
  let left = 280, right = 320;
@@ -389,7 +615,7 @@ const STUDIO_SCRIPT = `
389
615
  wire('split-right', (dx, l0, r0) => { right = clamp(r0 - dx); });
390
616
  }
391
617
 
392
- loadTargets();
618
+ loadTargets().then(loadRunning);
393
619
  connect();
394
620
  initSplitters();
395
621
  `;
@@ -418,7 +644,7 @@ function renderStudioHtml(appLabel, cliName) {
418
644
  <main>
419
645
  <section class="pane" id="targets"><h2>Targets</h2></section>
420
646
  <div class="splitter" id="split-left"></div>
421
- <section class="pane" id="workspace"><div class="empty">Pick a Lambda on the left to invoke it.</div></section>
647
+ <section class="pane" id="workspace"><div class="empty">Pick a Lambda to invoke, or an API to serve, on the left.</div></section>
422
648
  <div class="splitter" id="split-right"></div>
423
649
  <section class="pane" id="timeline"><h2>Timeline</h2><div class="empty">No requests yet.</div></section>
424
650
  </main>
@@ -489,7 +715,7 @@ async function startStudioServer(options) {
489
715
  const maxBump = options.maxPortBump ?? 20;
490
716
  const html = renderStudioHtml(options.appLabel, options.cliName);
491
717
  const targetsJson = JSON.stringify({ groups: options.targetGroups });
492
- const server = createServer((req, res) => handleRequest(req, res, options.bus, html, targetsJson, options.onRun));
718
+ const server = createServer$1((req, res) => handleRequest(req, res, options.bus, html, targetsJson, options));
493
719
  const boundPort = await listenWithBump(server, host, options.port, maxBump);
494
720
  return {
495
721
  url: `http://${host}:${boundPort}`,
@@ -500,7 +726,7 @@ async function startStudioServer(options) {
500
726
  })
501
727
  };
502
728
  }
503
- function handleRequest(req, res, bus, html, targetsJson, onRun) {
729
+ function handleRequest(req, res, bus, html, targetsJson, options) {
504
730
  const path = (req.url ?? "/").split("?")[0];
505
731
  if (req.method === "GET" && (path === "/" || path === "/index.html")) {
506
732
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
@@ -512,25 +738,40 @@ function handleRequest(req, res, bus, html, targetsJson, onRun) {
512
738
  res.end(targetsJson);
513
739
  return;
514
740
  }
741
+ if (req.method === "GET" && path === "/api/running") {
742
+ const running = options.getRunning ? options.getRunning() : { running: [] };
743
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
744
+ res.end(JSON.stringify(running));
745
+ return;
746
+ }
515
747
  if (req.method === "GET" && path === "/api/events") {
516
748
  serveSse(req, res, bus);
517
749
  return;
518
750
  }
519
751
  if (req.method === "POST" && path === "/api/run") {
520
- handleRun(req, res, onRun);
752
+ handleDispatch(req, res, options.onRun);
753
+ return;
754
+ }
755
+ if (req.method === "POST" && path === "/api/stop") {
756
+ handleDispatch(req, res, options.onStop);
521
757
  return;
522
758
  }
523
759
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
524
760
  res.end("Not found");
525
761
  }
526
762
  const MAX_RUN_BODY_BYTES = 5 * 1024 * 1024;
527
- /** Reply to `POST /api/run`: parse the JSON body, dispatch via `onRun`. */
528
- async function handleRun(req, res, onRun) {
763
+ /**
764
+ * Reply to a JSON POST endpoint (`/api/run` / `/api/stop`): parse the
765
+ * bounded JSON body and dispatch via `handler`. 501 when no handler is
766
+ * wired (the observe-only shell), 400 on a malformed body, 500 when the
767
+ * handler throws.
768
+ */
769
+ async function handleDispatch(req, res, handler) {
529
770
  const sendJson = (statusCode, payload) => {
530
771
  res.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
531
772
  res.end(JSON.stringify(payload));
532
773
  };
533
- if (!onRun) {
774
+ if (!handler) {
534
775
  sendJson(501, { error: "Running targets is not supported by this studio server." });
535
776
  return;
536
777
  }
@@ -542,7 +783,7 @@ async function handleRun(req, res, onRun) {
542
783
  return;
543
784
  }
544
785
  try {
545
- sendJson(200, await onRun(body));
786
+ sendJson(200, await handler(body));
546
787
  } catch (err) {
547
788
  sendJson(500, { error: err instanceof Error ? err.message : String(err) });
548
789
  }
@@ -600,12 +841,16 @@ function serveSse(req, res, bus) {
600
841
  const onLog = (ev) => {
601
842
  safeWrite(`event: log\ndata: ${JSON.stringify(ev)}\n\n`);
602
843
  };
844
+ const onServe = (ev) => {
845
+ safeWrite(`event: serve\ndata: ${JSON.stringify(ev)}\n\n`);
846
+ };
603
847
  function cleanup() {
604
848
  if (closed) return;
605
849
  closed = true;
606
850
  clearInterval(heartbeat);
607
851
  bus.off("invocation", onInvocation);
608
852
  bus.off("log", onLog);
853
+ bus.off("serve", onServe);
609
854
  }
610
855
  function safeWrite(chunk) {
611
856
  if (closed || res.writableEnded || res.destroyed) {
@@ -616,6 +861,7 @@ function serveSse(req, res, bus) {
616
861
  }
617
862
  bus.on("invocation", onInvocation);
618
863
  bus.on("log", onLog);
864
+ bus.on("serve", onServe);
619
865
  req.on("close", cleanup);
620
866
  res.on("close", cleanup);
621
867
  res.on("error", cleanup);
@@ -857,6 +1103,480 @@ function tryParseJson(raw) {
857
1103
  }
858
1104
  }
859
1105
 
1106
+ //#endregion
1107
+ //#region src/local/studio-proxy.ts
1108
+ let proxyIdCounter = 0;
1109
+ /**
1110
+ * Start a capturing reverse proxy in front of a studio serve target
1111
+ * (decision D4a: because every request to the served port flows through
1112
+ * `cdkl studio`, the timeline observes them regardless of source —
1113
+ * browser, curl, or the in-UI pad alike).
1114
+ *
1115
+ * Each HTTP request is forwarded to `upstream` and, in parallel,
1116
+ * captured (method / path / headers / bounded body) and emitted as an
1117
+ * `invocation` start event; when the upstream response completes, an end
1118
+ * event carries the status / headers / bounded body / duration. The full
1119
+ * bodies stream through untouched — only the captured copies are bounded.
1120
+ * `Upgrade` (WebSocket) requests are bridged raw to the upstream without
1121
+ * capture so they keep working.
1122
+ *
1123
+ * Studio is a control plane over the CLI, so this proxy sits in front of
1124
+ * the long-running `cdkl start-api` child the serve manager spawned; it
1125
+ * does NOT re-implement any routing — it forwards verbatim.
1126
+ */
1127
+ function startStudioProxy(config) {
1128
+ const host = config.host ?? "127.0.0.1";
1129
+ const clock = config.clock ?? Date.now;
1130
+ const maxCapture = config.maxCaptureBytes ?? 64 * 1024;
1131
+ const idFactory = config.idFactory ?? (() => {
1132
+ proxyIdCounter += 1;
1133
+ return `req-${clock()}-${proxyIdCounter}`;
1134
+ });
1135
+ const upstreamUrl = new URL(config.upstream);
1136
+ const upstreamHost = upstreamUrl.hostname;
1137
+ const upstreamPort = Number(upstreamUrl.port) || 80;
1138
+ const server = createServer$1((clientReq, clientRes) => {
1139
+ const id = idFactory();
1140
+ const startedAt = clock();
1141
+ const path = clientReq.url ?? "/";
1142
+ const method = clientReq.method ?? "GET";
1143
+ const label = `${method} ${path.split("?")[0]}`;
1144
+ const reqBody = boundedCollector(maxCapture);
1145
+ config.bus.emit("invocation", {
1146
+ id,
1147
+ ts: startedAt,
1148
+ target: config.target,
1149
+ kind: config.kind,
1150
+ label,
1151
+ request: {
1152
+ method,
1153
+ path,
1154
+ headers: { ...clientReq.headers }
1155
+ }
1156
+ });
1157
+ let ended = false;
1158
+ const emitEnd = (status, response) => {
1159
+ if (ended) return;
1160
+ ended = true;
1161
+ config.bus.emit("invocation", {
1162
+ id,
1163
+ ts: startedAt,
1164
+ target: config.target,
1165
+ kind: config.kind,
1166
+ label,
1167
+ request: {
1168
+ method,
1169
+ path,
1170
+ headers: { ...clientReq.headers },
1171
+ body: reqBody.text()
1172
+ },
1173
+ response,
1174
+ status,
1175
+ durationMs: clock() - startedAt
1176
+ });
1177
+ };
1178
+ const upstreamReq = request({
1179
+ host: upstreamHost,
1180
+ port: upstreamPort,
1181
+ method,
1182
+ path,
1183
+ headers: clientReq.headers
1184
+ }, (upstreamRes) => {
1185
+ const respBody = boundedCollector(maxCapture);
1186
+ clientRes.writeHead(upstreamRes.statusCode ?? 502, stripHopByHop(upstreamRes.headers));
1187
+ upstreamRes.on("data", (chunk) => respBody.push(chunk));
1188
+ upstreamRes.pipe(clientRes);
1189
+ upstreamRes.on("end", () => emitEnd(upstreamRes.statusCode ?? 502, {
1190
+ status: upstreamRes.statusCode,
1191
+ headers: { ...upstreamRes.headers },
1192
+ body: respBody.text()
1193
+ }));
1194
+ upstreamRes.on("error", () => emitEnd(502, "upstream response stream error"));
1195
+ });
1196
+ upstreamReq.on("error", (err) => {
1197
+ if (!clientRes.headersSent) clientRes.writeHead(502, { "content-type": "text/plain" });
1198
+ clientRes.end(`studio proxy: upstream error: ${err.message}`);
1199
+ emitEnd(502, `upstream error: ${err.message}`);
1200
+ });
1201
+ clientReq.on("data", (chunk) => reqBody.push(chunk));
1202
+ clientReq.on("error", () => {
1203
+ upstreamReq.destroy();
1204
+ emitEnd(499, "client aborted the request");
1205
+ });
1206
+ clientReq.pipe(upstreamReq);
1207
+ });
1208
+ const upgradeSockets = /* @__PURE__ */ new Set();
1209
+ server.on("upgrade", (req, clientSocket, head) => {
1210
+ const sock = clientSocket;
1211
+ upgradeSockets.add(sock);
1212
+ sock.on("close", () => upgradeSockets.delete(sock));
1213
+ bridgeUpgrade(req, sock, head, upstreamHost, upstreamPort);
1214
+ });
1215
+ return new Promise((resolve, reject) => {
1216
+ server.once("error", reject);
1217
+ server.listen(0, host, () => {
1218
+ server.removeListener("error", reject);
1219
+ const port = server.address().port;
1220
+ resolve({
1221
+ url: `http://${host}:${port}`,
1222
+ port,
1223
+ close: () => new Promise((resolveClose, rejectClose) => {
1224
+ for (const sock of upgradeSockets) sock.destroy();
1225
+ upgradeSockets.clear();
1226
+ server.close((err) => err ? rejectClose(err) : resolveClose());
1227
+ server.closeAllConnections?.();
1228
+ })
1229
+ });
1230
+ });
1231
+ });
1232
+ }
1233
+ /** RFC 7230 hop-by-hop headers — never forwarded across a proxy. */
1234
+ const HOP_BY_HOP = new Set([
1235
+ "connection",
1236
+ "keep-alive",
1237
+ "proxy-authenticate",
1238
+ "proxy-authorization",
1239
+ "te",
1240
+ "trailer",
1241
+ "transfer-encoding",
1242
+ "upgrade"
1243
+ ]);
1244
+ /** Copy `headers` without the hop-by-hop ones (which describe one connection). */
1245
+ function stripHopByHop(headers) {
1246
+ const out = {};
1247
+ for (const [k, v] of Object.entries(headers)) if (!HOP_BY_HOP.has(k.toLowerCase())) out[k] = v;
1248
+ return out;
1249
+ }
1250
+ /** A bounded byte collector that decodes to a (possibly truncated) utf8 string. */
1251
+ function boundedCollector(maxBytes) {
1252
+ const chunks = [];
1253
+ let size = 0;
1254
+ let truncated = false;
1255
+ return {
1256
+ push: (c) => {
1257
+ if (size >= maxBytes) {
1258
+ truncated = true;
1259
+ return;
1260
+ }
1261
+ const room = maxBytes - size;
1262
+ if (c.length > room) {
1263
+ chunks.push(c.subarray(0, room));
1264
+ size = maxBytes;
1265
+ truncated = true;
1266
+ } else {
1267
+ chunks.push(c);
1268
+ size += c.length;
1269
+ }
1270
+ },
1271
+ text: () => {
1272
+ const s = Buffer.concat(chunks).toString("utf8");
1273
+ return truncated ? `${s}… (truncated)` : s;
1274
+ }
1275
+ };
1276
+ }
1277
+ /** Raw-bridge an `Upgrade` request (e.g. WebSocket) to the upstream. */
1278
+ function bridgeUpgrade(req, clientSocket, head, upstreamHost, upstreamPort) {
1279
+ const upstream = connect(upstreamPort, upstreamHost, () => {
1280
+ let raw = `${req.method} ${req.url} HTTP/1.1\r\n`;
1281
+ for (let i = 0; i < req.rawHeaders.length; i += 2) raw += `${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`;
1282
+ raw += "\r\n";
1283
+ upstream.write(raw);
1284
+ if (head && head.length > 0) upstream.write(head);
1285
+ clientSocket.pipe(upstream);
1286
+ upstream.pipe(clientSocket);
1287
+ });
1288
+ const teardown = () => {
1289
+ clientSocket.destroy();
1290
+ upstream.destroy();
1291
+ };
1292
+ upstream.on("error", teardown);
1293
+ upstream.on("close", teardown);
1294
+ clientSocket.on("error", teardown);
1295
+ clientSocket.on("close", teardown);
1296
+ }
1297
+
1298
+ //#endregion
1299
+ //#region src/local/studio-serve-manager.ts
1300
+ /** Kinds the serve manager can start in this build. */
1301
+ const SERVE_SUPPORTED = ["api"];
1302
+ /** `Server listening on <url>` is the stable ready marker `start-api` prints. */
1303
+ const LISTENING_RE = /Server listening on (\S+)/;
1304
+ /**
1305
+ * Build the studio serve manager. Slice C1 drives a long-running
1306
+ * `cdkl start-api <target>` child — studio is a control plane over the
1307
+ * CLI (the same pattern as the single-shot invoke dispatcher), so it
1308
+ * spawns the SAME headless serve command rather than re-wiring its
1309
+ * internals. This preserves byte-for-byte parity and isolates the
1310
+ * server's process-global behavior in a child.
1311
+ *
1312
+ * `start()` spawns the child with `--port 0` (OS-assigned, collision
1313
+ * free), streams its stdout/stderr onto the bus as `log` events keyed by
1314
+ * the target id, and resolves once the child prints its first
1315
+ * `Server listening on <url>` line — emitting a `serve` `running` event
1316
+ * with the discovered endpoints. `stop()` SIGTERMs the child (SIGKILL
1317
+ * after a grace window) and emits `stopped`.
1318
+ *
1319
+ * Slice C2 fronts each HTTP serve endpoint with a capture proxy
1320
+ * ({@link startStudioProxy}) so every request to the served port lands
1321
+ * on the studio timeline (decision D4a); the `endpoints` the UI is
1322
+ * handed are the proxy URLs. Full-text log search and alb / ecs serve
1323
+ * kinds are still to come.
1324
+ */
1325
+ function createStudioServeManager(config) {
1326
+ const spawnFn = config.spawnFn ?? spawn;
1327
+ const nodeBin = config.nodeBin ?? process.execPath;
1328
+ const clock = config.clock ?? Date.now;
1329
+ const readyTimeoutMs = config.readyTimeoutMs ?? 12e4;
1330
+ const stopGraceMs = config.stopGraceMs ?? 1e4;
1331
+ const setTimeoutFn = config.setTimeoutFn ?? setTimeout;
1332
+ const clearTimeoutFn = config.clearTimeoutFn ?? clearTimeout;
1333
+ const cwd = config.cwd ?? process.cwd();
1334
+ const proxyFactory = config.proxyFactory ?? startStudioProxy;
1335
+ const captureRequests = config.captureRequests ?? true;
1336
+ const entries = /* @__PURE__ */ new Map();
1337
+ /** Close every capture proxy fronting `e` (best-effort; idempotent). */
1338
+ async function closeProxies(e) {
1339
+ const proxies = e.proxies.splice(0);
1340
+ await Promise.all(proxies.map((p) => p.close().catch(() => void 0)));
1341
+ }
1342
+ function publicState(e) {
1343
+ const s = {
1344
+ targetId: e.targetId,
1345
+ kind: e.kind,
1346
+ status: e.status,
1347
+ endpoints: [...e.endpoints],
1348
+ startedAt: e.startedAt
1349
+ };
1350
+ if (e.pid !== void 0) s.pid = e.pid;
1351
+ return s;
1352
+ }
1353
+ function emitServe(e, message) {
1354
+ const ev = {
1355
+ ts: clock(),
1356
+ target: e.targetId,
1357
+ kind: e.kind,
1358
+ status: e.status,
1359
+ endpoints: [...e.endpoints]
1360
+ };
1361
+ if (e.pid !== void 0) ev.pid = e.pid;
1362
+ if (message !== void 0) ev.message = message;
1363
+ config.bus.emit("serve", ev);
1364
+ }
1365
+ function buildArgs(targetId) {
1366
+ const args = [
1367
+ "start-api",
1368
+ targetId,
1369
+ "--port",
1370
+ "0",
1371
+ "--host",
1372
+ "127.0.0.1"
1373
+ ];
1374
+ if (config.app) args.push("--app", config.app);
1375
+ if (config.profile) args.push("--profile", config.profile);
1376
+ if (config.region) args.push("--region", config.region);
1377
+ for (const [k, v] of Object.entries(config.context ?? {})) args.push("-c", `${k}=${v}`);
1378
+ return args;
1379
+ }
1380
+ async function start(req) {
1381
+ if (!SERVE_SUPPORTED.includes(req.kind)) throw new Error(`Serving '${req.kind}' targets from studio is not supported yet (API only in this build).`);
1382
+ const existing = entries.get(req.targetId);
1383
+ if (existing && existing.status !== "stopped" && existing.status !== "error") throw new Error(`'${req.targetId}' is already running.`);
1384
+ const startedAt = clock();
1385
+ let child;
1386
+ try {
1387
+ child = spawnFn(nodeBin, [config.cliEntry, ...buildArgs(req.targetId)], { cwd });
1388
+ } catch (err) {
1389
+ throw err instanceof Error ? err : new Error(String(err));
1390
+ }
1391
+ const entry = {
1392
+ targetId: req.targetId,
1393
+ kind: req.kind,
1394
+ status: "starting",
1395
+ endpoints: [],
1396
+ startedAt,
1397
+ child,
1398
+ proxies: []
1399
+ };
1400
+ if (child.pid !== void 0) entry.pid = child.pid;
1401
+ entries.set(req.targetId, entry);
1402
+ emitServe(entry);
1403
+ return new Promise((resolve, reject) => {
1404
+ let settled = false;
1405
+ child.stdout.setEncoding("utf8");
1406
+ child.stderr.setEncoding("utf8");
1407
+ const timer = setTimeoutFn(() => {
1408
+ if (settled) return;
1409
+ settled = true;
1410
+ entry.status = "error";
1411
+ emitServe(entry, `Timed out after ${readyTimeoutMs}ms waiting for the server to listen.`);
1412
+ stopChild(child, stopGraceMs, setTimeoutFn, clearTimeoutFn);
1413
+ closeProxies(entry);
1414
+ entries.delete(req.targetId);
1415
+ reject(/* @__PURE__ */ new Error(`'${req.targetId}' did not start within ${readyTimeoutMs}ms.`));
1416
+ }, readyTimeoutMs);
1417
+ timer.unref?.();
1418
+ const becomeRunning = () => {
1419
+ if (settled) return;
1420
+ settled = true;
1421
+ clearTimeoutFn(timer);
1422
+ entry.status = "running";
1423
+ emitServe(entry);
1424
+ resolve(publicState(entry));
1425
+ };
1426
+ const onListening = async (childUrl) => {
1427
+ let endpoint = childUrl;
1428
+ if (captureRequests && /^https?:/i.test(childUrl)) try {
1429
+ const proxy = await proxyFactory({
1430
+ bus: config.bus,
1431
+ target: req.targetId,
1432
+ kind: req.kind,
1433
+ upstream: childUrl
1434
+ });
1435
+ entry.proxies.push(proxy);
1436
+ endpoint = proxy.url;
1437
+ } catch {
1438
+ endpoint = childUrl;
1439
+ }
1440
+ if (entry.stopping || settled && !entries.has(req.targetId)) {
1441
+ await closeProxies(entry);
1442
+ return;
1443
+ }
1444
+ if (!entry.endpoints.includes(endpoint)) entry.endpoints.push(endpoint);
1445
+ if (settled) emitServe(entry);
1446
+ else becomeRunning();
1447
+ };
1448
+ streamLines(child.stdout, (line) => {
1449
+ const m = LISTENING_RE.exec(line);
1450
+ if (m?.[1]) onListening(m[1]);
1451
+ emitLog(config.bus, clock, req.targetId, line, "stdout");
1452
+ });
1453
+ streamLines(child.stderr, (line) => {
1454
+ emitLog(config.bus, clock, req.targetId, line, "stderr");
1455
+ });
1456
+ child.on("error", (err) => {
1457
+ if (settled) {
1458
+ entry.status = "error";
1459
+ emitServe(entry, err.message);
1460
+ closeProxies(entry);
1461
+ entries.delete(req.targetId);
1462
+ return;
1463
+ }
1464
+ settled = true;
1465
+ clearTimeoutFn(timer);
1466
+ entry.status = "error";
1467
+ emitServe(entry, err.message);
1468
+ closeProxies(entry);
1469
+ entries.delete(req.targetId);
1470
+ reject(err);
1471
+ });
1472
+ child.on("close", (code) => {
1473
+ if (!settled) {
1474
+ settled = true;
1475
+ clearTimeoutFn(timer);
1476
+ if (entry.stopping) {
1477
+ reject(/* @__PURE__ */ new Error(`'${req.targetId}' was stopped before it finished starting.`));
1478
+ return;
1479
+ }
1480
+ entry.status = "error";
1481
+ const msg = `Server exited before listening (code ${code ?? "null"}).`;
1482
+ emitServe(entry, msg);
1483
+ closeProxies(entry);
1484
+ entries.delete(req.targetId);
1485
+ reject(new Error(msg));
1486
+ return;
1487
+ }
1488
+ if (entries.get(req.targetId) === entry && entry.status === "running") {
1489
+ entry.status = "stopped";
1490
+ emitServe(entry, `Server process exited (code ${code ?? "null"}).`);
1491
+ closeProxies(entry);
1492
+ entries.delete(req.targetId);
1493
+ }
1494
+ });
1495
+ });
1496
+ }
1497
+ async function stop(req) {
1498
+ const entry = entries.get(req.targetId);
1499
+ if (!entry) throw new Error(`'${req.targetId}' is not running.`);
1500
+ entry.stopping = true;
1501
+ entries.delete(req.targetId);
1502
+ await Promise.all([closeProxies(entry), stopChild(entry.child, stopGraceMs, setTimeoutFn, clearTimeoutFn)]);
1503
+ entry.status = "stopped";
1504
+ emitServe(entry);
1505
+ }
1506
+ function list() {
1507
+ return [...entries.values()].map(publicState);
1508
+ }
1509
+ async function stopAll() {
1510
+ const targets = [...entries.keys()];
1511
+ await Promise.all(targets.map((targetId) => stop({ targetId }).catch(() => void 0)));
1512
+ }
1513
+ return {
1514
+ start,
1515
+ stop,
1516
+ list,
1517
+ stopAll
1518
+ };
1519
+ }
1520
+ /** Emit one container log line onto the bus, keyed by the serve target id. */
1521
+ function emitLog(bus, clock, target, line, stream) {
1522
+ bus.emit("log", {
1523
+ ts: clock(),
1524
+ containerId: target,
1525
+ target,
1526
+ line,
1527
+ stream
1528
+ });
1529
+ }
1530
+ /**
1531
+ * Line-buffer a child stream and invoke `onLine` per complete line
1532
+ * (trailing newline stripped, blank lines dropped). Flushes any partial
1533
+ * final line on stream end.
1534
+ */
1535
+ function streamLines(stream, onLine) {
1536
+ let buf = "";
1537
+ stream.on("data", (chunk) => {
1538
+ buf += chunk;
1539
+ let nl = buf.indexOf("\n");
1540
+ while (nl !== -1) {
1541
+ const line = buf.slice(0, nl).replace(/\r$/, "");
1542
+ buf = buf.slice(nl + 1);
1543
+ if (line.length > 0) onLine(line);
1544
+ nl = buf.indexOf("\n");
1545
+ }
1546
+ });
1547
+ stream.on("end", () => {
1548
+ const line = buf.replace(/\r$/, "");
1549
+ if (line.length > 0) onLine(line);
1550
+ buf = "";
1551
+ });
1552
+ }
1553
+ /**
1554
+ * SIGTERM a child and resolve once it exits, escalating to SIGKILL after
1555
+ * `graceMs`. Resolves immediately if the child has already exited.
1556
+ */
1557
+ function stopChild(child, graceMs, setTimeoutFn, clearTimeoutFn) {
1558
+ return new Promise((resolve) => {
1559
+ let done = false;
1560
+ let kill;
1561
+ const finish = () => {
1562
+ if (done) return;
1563
+ done = true;
1564
+ if (kill) clearTimeoutFn(kill);
1565
+ resolve();
1566
+ };
1567
+ child.once("close", finish);
1568
+ if (child.exitCode !== null || child.signalCode !== null) {
1569
+ finish();
1570
+ return;
1571
+ }
1572
+ kill = setTimeoutFn(() => {
1573
+ if (!done) child.kill("SIGKILL");
1574
+ }, graceMs);
1575
+ kill.unref?.();
1576
+ child.kill("SIGTERM");
1577
+ });
1578
+ }
1579
+
860
1580
  //#endregion
861
1581
  //#region src/cli/commands/local-studio.ts
862
1582
  const STUDIO_TARGET_KINDS = [
@@ -883,6 +1603,17 @@ function coerceRunRequest(body) {
883
1603
  event
884
1604
  };
885
1605
  }
1606
+ /**
1607
+ * Validate + narrow the untyped `POST /api/stop` body into a
1608
+ * {@link StudioStopRequest}. Throws (→ 400 from the server) on a missing
1609
+ * / empty target id.
1610
+ */
1611
+ function coerceStopRequest(body) {
1612
+ if (typeof body !== "object" || body === null) throw new Error("Request body must be a JSON object.");
1613
+ const { targetId } = body;
1614
+ if (typeof targetId !== "string" || targetId.trim() === "") throw new Error("Request body must include a non-empty \"targetId\" string.");
1615
+ return { targetId };
1616
+ }
886
1617
  const DEFAULT_STUDIO_PORT = 9999;
887
1618
  /**
888
1619
  * Parse + validate the `--studio-port` value. Accepts `0` (OS-assigned)
@@ -918,7 +1649,7 @@ async function localStudioCommand(options) {
918
1649
  const targetGroups = toStudioTargetGroups(listTargets(stacks));
919
1650
  const appLabel = stacks.map((s) => s.stackName).join(", ") || appCmd;
920
1651
  const bus = new StudioEventBus();
921
- const dispatcher = createStudioDispatcher({
1652
+ const childConfig = {
922
1653
  cliEntry: process.argv[1] ?? "",
923
1654
  bus,
924
1655
  cwd: process.cwd(),
@@ -926,20 +1657,31 @@ async function localStudioCommand(options) {
926
1657
  ...options.profile ? { profile: options.profile } : {},
927
1658
  ...options.region ? { region: options.region } : {},
928
1659
  ...Object.keys(context).length > 0 ? { context } : {}
929
- });
1660
+ };
1661
+ const dispatcher = createStudioDispatcher(childConfig);
1662
+ const serveManager = createStudioServeManager(childConfig);
930
1663
  const server = await startStudioServer({
931
1664
  port,
932
1665
  bus,
933
1666
  targetGroups,
934
1667
  appLabel,
935
1668
  cliName: getEmbedConfig().cliName,
936
- onRun: (body) => dispatcher.run(coerceRunRequest(body))
1669
+ onRun: (body) => {
1670
+ const req = coerceRunRequest(body);
1671
+ return req.kind === "lambda" ? dispatcher.run(req) : serveManager.start(req);
1672
+ },
1673
+ onStop: async (body) => {
1674
+ const req = coerceStopRequest(body);
1675
+ await serveManager.stop(req);
1676
+ return { stopped: req.targetId };
1677
+ },
1678
+ getRunning: () => ({ running: serveManager.list() })
937
1679
  });
938
1680
  const cliName = getEmbedConfig().cliName;
939
1681
  logger.info(`${cliName} studio is running at ${server.url}`);
940
1682
  logger.info("Press Ctrl-C to stop.");
941
1683
  if (options.open && process.stdout.isTTY) openBrowser(server.url);
942
- await blockUntilShutdown(server, cliName);
1684
+ await blockUntilShutdown(server, serveManager, cliName);
943
1685
  }
944
1686
  /** Best-effort cross-platform browser open. Failures are non-fatal. */
945
1687
  function openBrowser(url) {
@@ -955,17 +1697,20 @@ function openBrowser(url) {
955
1697
  } catch {}
956
1698
  }
957
1699
  /**
958
- * Block until SIGINT / SIGTERM, then close the studio server and resolve.
959
- * Mirrors the long-running serve commands' graceful-shutdown contract.
1700
+ * Block until SIGINT / SIGTERM, then stop every running serve child,
1701
+ * close the studio server, and resolve. Mirrors the long-running serve
1702
+ * commands' graceful-shutdown contract — the serve children are killed
1703
+ * BEFORE the server closes so their RIE containers are torn down rather
1704
+ * than orphaned.
960
1705
  */
961
- function blockUntilShutdown(server, cliName) {
1706
+ function blockUntilShutdown(server, serveManager, cliName) {
962
1707
  return new Promise((resolveShutdown) => {
963
1708
  let shuttingDown = false;
964
1709
  const shutdown = (signal) => {
965
1710
  if (shuttingDown) return;
966
1711
  shuttingDown = true;
967
1712
  getLogger().info(`Received ${signal}; stopping ${cliName} studio...`);
968
- server.close().catch((err) => getLogger().warn(`Error stopping studio server: ${String(err)}`)).finally(() => resolveShutdown());
1713
+ serveManager.stopAll().catch((err) => getLogger().warn(`Error stopping serve targets: ${String(err)}`)).then(() => server.close()).catch((err) => getLogger().warn(`Error stopping studio server: ${String(err)}`)).finally(() => resolveShutdown());
969
1714
  };
970
1715
  process.on("SIGINT", () => shutdown("SIGINT"));
971
1716
  process.on("SIGTERM", () => shutdown("SIGTERM"));
@@ -1000,5 +1745,5 @@ function addStudioSpecificOptions(cmd) {
1000
1745
  }
1001
1746
 
1002
1747
  //#endregion
1003
- export { startStudioServer as a, StudioEventBus as c, createStudioDispatcher as i, coerceRunRequest as n, toStudioTargetGroups as o, createLocalStudioCommand as r, renderStudioHtml as s, addStudioSpecificOptions as t };
1004
- //# sourceMappingURL=local-studio-CSW1raG0.js.map
1748
+ export { createStudioServeManager as a, startStudioServer as c, StudioEventBus as d, createLocalStudioCommand as i, toStudioTargetGroups as l, coerceRunRequest as n, startStudioProxy as o, coerceStopRequest as r, createStudioDispatcher as s, addStudioSpecificOptions as t, renderStudioHtml as u };
1749
+ //# sourceMappingURL=local-studio-KUaxIkpx.js.map