cdk-local 0.81.1 → 0.83.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.
@@ -45,6 +45,71 @@ var StudioEventBus = class {
45
45
  }
46
46
  };
47
47
 
48
+ //#endregion
49
+ //#region src/local/studio-store.ts
50
+ /**
51
+ * Build the studio store and subscribe it to `bus`. The store merges the
52
+ * start/end pair of each invocation (keyed by id) and keeps a ring of log
53
+ * lines; both windows evict oldest-first past their caps.
54
+ */
55
+ function createStudioStore(bus, options = {}) {
56
+ const maxInvocations = options.maxInvocations ?? 200;
57
+ const maxLogs = options.maxLogs ?? 5e3;
58
+ const bindGraceMs = options.bindGraceMs ?? 250;
59
+ const invocations = /* @__PURE__ */ new Map();
60
+ const logs = [];
61
+ const onInvocation = (ev) => {
62
+ invocations.set(ev.id, {
63
+ ...invocations.get(ev.id),
64
+ ...ev
65
+ });
66
+ if (invocations.size > maxInvocations) {
67
+ const oldest = invocations.keys().next().value;
68
+ if (oldest !== void 0) invocations.delete(oldest);
69
+ }
70
+ };
71
+ const onLog = (ev) => {
72
+ logs.push(ev);
73
+ if (logs.length > maxLogs) logs.shift();
74
+ };
75
+ bus.on("invocation", onInvocation);
76
+ bus.on("log", onLog);
77
+ let disposed = false;
78
+ return {
79
+ history: () => ({
80
+ invocations: [...invocations.values()],
81
+ logs: [...logs]
82
+ }),
83
+ searchLogs: (query, opts = {}) => {
84
+ const needle = query.toLowerCase();
85
+ const limit = opts.limit ?? 200;
86
+ const matches = [];
87
+ for (let i = logs.length - 1; i >= 0 && matches.length < limit; i -= 1) {
88
+ const log = logs[i];
89
+ if (!log) continue;
90
+ if (opts.target !== void 0 && log.target !== opts.target) continue;
91
+ if (needle === "" || log.line.toLowerCase().includes(needle)) matches.push(log);
92
+ }
93
+ return matches;
94
+ },
95
+ logsForInvocation: (id) => {
96
+ const inv = invocations.get(id);
97
+ if (!inv) return [];
98
+ if (inv.kind === "lambda") return logs.filter((l) => l.containerId === id);
99
+ const from = inv.ts;
100
+ const to = inv.ts + (inv.durationMs ?? 0) + bindGraceMs;
101
+ return logs.filter((l) => l.target === inv.target && l.ts >= from && l.ts <= to);
102
+ },
103
+ invocation: (id) => invocations.get(id),
104
+ dispose: () => {
105
+ if (disposed) return;
106
+ disposed = true;
107
+ bus.off("invocation", onInvocation);
108
+ bus.off("log", onLog);
109
+ }
110
+ };
111
+ }
112
+
48
113
  //#endregion
49
114
  //#region src/local/studio-ui.ts
50
115
  /**
@@ -150,12 +215,27 @@ const STUDIO_CSS = `
150
215
  .section pre { margin: 0; white-space: pre-wrap; word-break: break-word; color: #cfcfcf; }
151
216
  .endpoint { display: block; color: #6aa9ff; text-decoration: none; padding: 2px 0; }
152
217
  .endpoint:hover { text-decoration: underline; }
218
+ .searchbar { padding: 6px 10px; border-bottom: 1px solid #2a2a2a; background: #151515;
219
+ position: sticky; top: 28px; z-index: 1; }
220
+ .searchbar input {
221
+ width: 100%; background: #111; color: #ddd; border: 1px solid #333; border-radius: 3px;
222
+ padding: 5px 8px; font: 12px ui-monospace, Menlo, monospace;
223
+ }
224
+ .searchbar input:focus { outline: none; border-color: #4ec97a; }
225
+ #log-results { display: none; }
226
+ #log-results.active { display: block; }
227
+ .log-hit { padding: 4px 12px; border-bottom: 1px solid #222; white-space: pre-wrap;
228
+ word-break: break-word; }
229
+ .log-hit .lt { color: #777; }
230
+ .log-hit .lg { color: #6aa9ff; }
231
+ .log-hits-meta { padding: 6px 12px; color: #888; font-size: 11px; }
153
232
  #conn { font-size: 11px; }
154
233
  #conn.up { color: #7bd88f; }
155
234
  #conn.down { color: #e0707a; }
156
235
  `;
157
236
  const STUDIO_SCRIPT = `
158
237
  const KIND_LABEL = { lambda: 'Lambda', api: 'API', alb: 'ALB', ecs: 'ECS', agentcore: 'AgentCore' };
238
+ const SERVE_KINDS = ['api', 'alb', 'ecs']; // long-running serve targets
159
239
  const rowsById = new Map(); // invocationId -> timeline row element
160
240
  const invById = new Map(); // invocationId -> latest invocation event
161
241
  const logsById = new Map(); // invocationId / serve targetId -> [log lines]
@@ -186,10 +266,11 @@ const STUDIO_SCRIPT = `
186
266
  pane.appendChild(el('div', 'group-title', group.title));
187
267
  for (const entry of group.entries) {
188
268
  total += 1;
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';
269
+ // Lambda targets are single-shot invokes; api / alb / ecs are
270
+ // long-running serves. Other kinds list but are not yet runnable.
271
+ // Within ecs, only services are servable (task defs are run-task).
272
+ const isServe = SERVE_KINDS.includes(group.kind) && (group.kind !== 'ecs' || entry.servable === true);
273
+ const runnable = group.kind === 'lambda' || isServe;
193
274
  const t = el('div', runnable ? 'target runnable' : 'target');
194
275
  const name = el('span', 'name', entry.id);
195
276
  name.title = entry.id; // full path on hover even when truncated
@@ -201,16 +282,16 @@ const STUDIO_SCRIPT = `
201
282
  t.appendChild(btn);
202
283
  t.onclick = () => selectTarget(entry.id, 'lambda');
203
284
  targetEls.set(entry.id, t);
204
- } else if (group.kind === 'api') {
285
+ } else if (isServe) {
205
286
  // A serve target: a running-state dot + a Start/Stop button
206
287
  // slot, both refreshed by updateServeRow on serve events.
207
288
  const dot = el('span', 'run-dot');
208
289
  const btnSlot = el('span', 'btn-slot');
209
290
  t.appendChild(dot);
210
291
  t.appendChild(btnSlot);
211
- t.onclick = () => selectTarget(entry.id, 'api');
292
+ t.onclick = () => selectTarget(entry.id, group.kind);
212
293
  targetEls.set(entry.id, t);
213
- serveMeta.set(entry.id, { dot, btnSlot });
294
+ serveMeta.set(entry.id, { dot, btnSlot, kind: group.kind });
214
295
  updateServeRow(entry.id);
215
296
  }
216
297
  pane.appendChild(t);
@@ -251,7 +332,10 @@ const STUDIO_SCRIPT = `
251
332
  const status = st ? st.status : 'stopped';
252
333
  const running = status === 'running';
253
334
  const starting = status === 'starting';
254
- meta.dot.textContent = running ? '● ' + firstPort(st.endpoints) : starting ? '○ starting' : '';
335
+ // A serve with a host endpoint shows the dot + :port; a pure-compute
336
+ // ecs service has no endpoint, so just the dot + running.
337
+ const port = running ? firstPort(st.endpoints) : '';
338
+ meta.dot.textContent = running ? (port ? '● ' + port : '● running') : starting ? '○ starting' : '';
255
339
  meta.dot.className = 'run-dot' + (starting ? ' starting' : '');
256
340
  meta.btnSlot.innerHTML = '';
257
341
  const btn = running || starting
@@ -275,7 +359,7 @@ const STUDIO_SCRIPT = `
275
359
  function selectTarget(id, kind) {
276
360
  highlightTarget(id);
277
361
  shownDetailId = null;
278
- if (kind === 'api') {
362
+ if (SERVE_KINDS.includes(kind)) {
279
363
  shownServeId = id;
280
364
  shownInvId = null;
281
365
  active = null;
@@ -287,13 +371,17 @@ const STUDIO_SCRIPT = `
287
371
  }
288
372
 
289
373
  async function startServe(id) {
374
+ // The serve kind (api / alb / ecs) drives which headless command the
375
+ // server spawns; it is recorded on the row when the target list loads.
376
+ const meta = serveMeta.get(id);
377
+ const kind = meta ? meta.kind : 'api';
290
378
  serveState.set(id, { status: 'starting', endpoints: [] });
291
379
  updateServeRow(id);
292
380
  try {
293
381
  const res = await fetch('/api/run', {
294
382
  method: 'POST',
295
383
  headers: { 'content-type': 'application/json' },
296
- body: JSON.stringify({ targetId: id, kind: 'api' }),
384
+ body: JSON.stringify({ targetId: id, kind }),
297
385
  });
298
386
  const data = await res.json();
299
387
  if (!res.ok) {
@@ -343,6 +431,8 @@ const STUDIO_SCRIPT = `
343
431
  }
344
432
  ws.appendChild(head);
345
433
 
434
+ const meta = serveMeta.get(id);
435
+ const isEcs = meta && meta.kind === 'ecs';
346
436
  const epSec = el('div', 'section');
347
437
  epSec.appendChild(el('h3', null, 'Endpoints'));
348
438
  if (running && st.endpoints.length) {
@@ -350,6 +440,10 @@ const STUDIO_SCRIPT = `
350
440
  const link = href(url);
351
441
  epSec.appendChild(link);
352
442
  }
443
+ } else if (running && isEcs) {
444
+ // A pure-compute ECS service has no host endpoint — it just runs the
445
+ // replicas (reach them container-to-container via Cloud Map).
446
+ epSec.appendChild(el('pre', null, '(running — pure compute service, no host endpoint)'));
353
447
  } else {
354
448
  epSec.appendChild(el('pre', null, starting ? '(starting…)' : '(not running)'));
355
449
  }
@@ -475,7 +569,7 @@ const STUDIO_SCRIPT = `
475
569
 
476
570
  function addInvocation(ev) {
477
571
  invById.set(ev.id, Object.assign(invById.get(ev.id) || {}, ev));
478
- const timeline = document.getElementById('timeline');
572
+ const timeline = document.getElementById('timeline-rows');
479
573
  const placeholder = timeline.querySelector('.empty');
480
574
  if (placeholder) placeholder.remove();
481
575
 
@@ -555,6 +649,84 @@ const STUDIO_SCRIPT = `
555
649
  respSec.appendChild(h);
556
650
  respSec.appendChild(el('pre', null, ev.response != null ? fmt(ev.response) : '(pending…)'));
557
651
  ws.appendChild(respSec);
652
+
653
+ // Logs bound to THIS request at CloudWatch granularity (D5), fetched
654
+ // from the server store.
655
+ const logSec = el('div', 'section');
656
+ logSec.appendChild(el('h3', null, 'Logs'));
657
+ const logPre = el('pre', null, '(loading…)');
658
+ logSec.appendChild(logPre);
659
+ ws.appendChild(logSec);
660
+ fetchInvocationLogs(id, logPre);
661
+ }
662
+
663
+ async function fetchInvocationLogs(id, pre) {
664
+ try {
665
+ const res = await fetch('/api/invocations/' + encodeURIComponent(id) + '/logs');
666
+ const data = await res.json();
667
+ const lines = (data.logs || []).map((l) => l.line);
668
+ if (shownDetailId === id) pre.textContent = lines.length ? lines.join('\\n') : '(none)';
669
+ } catch (err) {
670
+ if (shownDetailId === id) pre.textContent = '(failed to load logs)';
671
+ }
672
+ }
673
+
674
+ // Full-text log search over the server store. A non-empty query shows
675
+ // matching log lines INSTEAD of the timeline rows; clearing restores them.
676
+ let searchTimer = null;
677
+ function wireLogSearch() {
678
+ const input = document.getElementById('log-search');
679
+ const rows = document.getElementById('timeline-rows');
680
+ const results = document.getElementById('log-results');
681
+ input.addEventListener('input', () => {
682
+ if (searchTimer) clearTimeout(searchTimer);
683
+ searchTimer = setTimeout(() => runLogSearch(input.value.trim(), rows, results), 180);
684
+ });
685
+ }
686
+
687
+ async function runLogSearch(query, rows, results) {
688
+ if (query === '') {
689
+ results.classList.remove('active');
690
+ rows.style.display = '';
691
+ results.innerHTML = '';
692
+ return;
693
+ }
694
+ rows.style.display = 'none';
695
+ results.classList.add('active');
696
+ try {
697
+ const res = await fetch('/api/logs?q=' + encodeURIComponent(query));
698
+ const data = await res.json();
699
+ const hits = data.logs || [];
700
+ results.innerHTML = '';
701
+ results.appendChild(el('div', 'log-hits-meta', hits.length + ' match' + (hits.length === 1 ? '' : 'es')));
702
+ for (const h of hits) {
703
+ const row = el('div', 'log-hit');
704
+ row.appendChild(el('span', 'lt', new Date(h.ts).toLocaleTimeString() + ' '));
705
+ row.appendChild(el('span', 'lg', (h.target || '') + ' '));
706
+ row.appendChild(document.createTextNode(h.line));
707
+ results.appendChild(row);
708
+ }
709
+ } catch (err) {
710
+ results.innerHTML = '';
711
+ results.appendChild(el('div', 'log-hits-meta', 'Search failed: ' + err));
712
+ }
713
+ }
714
+
715
+ // Pull retained history on (re)connect so the timeline + logs reflect the
716
+ // whole session, not just events since this page loaded.
717
+ async function loadHistory() {
718
+ try {
719
+ const res = await fetch('/api/history');
720
+ const data = await res.json();
721
+ for (const log of (data.logs || [])) {
722
+ const arr = logsById.get(log.containerId) || [];
723
+ arr.push(log.line);
724
+ logsById.set(log.containerId, arr);
725
+ }
726
+ for (const inv of (data.invocations || [])) addInvocation(inv);
727
+ } catch (err) {
728
+ /* live SSE still drives the timeline; history is best-effort */
729
+ }
558
730
  }
559
731
 
560
732
  function connect() {
@@ -616,8 +788,10 @@ const STUDIO_SCRIPT = `
616
788
  }
617
789
 
618
790
  loadTargets().then(loadRunning);
791
+ loadHistory();
619
792
  connect();
620
793
  initSplitters();
794
+ wireLogSearch();
621
795
  `;
622
796
  /**
623
797
  * Render the full studio HTML document. `appLabel` is shown in the
@@ -646,7 +820,12 @@ function renderStudioHtml(appLabel, cliName) {
646
820
  <div class="splitter" id="split-left"></div>
647
821
  <section class="pane" id="workspace"><div class="empty">Pick a Lambda to invoke, or an API to serve, on the left.</div></section>
648
822
  <div class="splitter" id="split-right"></div>
649
- <section class="pane" id="timeline"><h2>Timeline</h2><div class="empty">No requests yet.</div></section>
823
+ <section class="pane" id="timeline">
824
+ <h2>Timeline</h2>
825
+ <div class="searchbar"><input id="log-search" type="search" placeholder="Search logs…" autocomplete="off" spellcheck="false" /></div>
826
+ <div id="timeline-rows"><div class="empty">No requests yet.</div></div>
827
+ <div id="log-results"></div>
828
+ </section>
650
829
  </main>
651
830
  <script>${STUDIO_SCRIPT}<\/script>
652
831
  </body>
@@ -667,12 +846,13 @@ function escapeHtml(s) {
667
846
  * projection without booting the server.
668
847
  */
669
848
  function toStudioTargetGroups(listing) {
670
- const map = (entries) => entries.map((e) => {
849
+ const map = (entries, opts = {}) => entries.map((e) => {
671
850
  const t = {
672
851
  id: e.displayPath ?? e.qualifiedId,
673
852
  qualifiedId: e.qualifiedId
674
853
  };
675
854
  if (e.kind) t.surface = e.kind;
855
+ if (opts.servable !== void 0) t.servable = opts.servable;
676
856
  return t;
677
857
  });
678
858
  return [
@@ -689,7 +869,7 @@ function toStudioTargetGroups(listing) {
689
869
  {
690
870
  kind: "ecs",
691
871
  title: "ECS Services / Tasks",
692
- entries: [...map(listing.ecsServices), ...map(listing.ecsTaskDefinitions)]
872
+ entries: [...map(listing.ecsServices, { servable: true }), ...map(listing.ecsTaskDefinitions, { servable: false })]
693
873
  },
694
874
  {
695
875
  kind: "agentcore",
@@ -727,7 +907,8 @@ async function startStudioServer(options) {
727
907
  };
728
908
  }
729
909
  function handleRequest(req, res, bus, html, targetsJson, options) {
730
- const path = (req.url ?? "/").split("?")[0];
910
+ const url = req.url ?? "/";
911
+ const path = url.split("?")[0];
731
912
  if (req.method === "GET" && (path === "/" || path === "/index.html")) {
732
913
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
733
914
  res.end(html);
@@ -744,6 +925,32 @@ function handleRequest(req, res, bus, html, targetsJson, options) {
744
925
  res.end(JSON.stringify(running));
745
926
  return;
746
927
  }
928
+ if (req.method === "GET" && path === "/api/history") {
929
+ const history = options.store ? options.store.history() : {
930
+ invocations: [],
931
+ logs: []
932
+ };
933
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
934
+ res.end(JSON.stringify(history));
935
+ return;
936
+ }
937
+ if (req.method === "GET" && path === "/api/logs") {
938
+ const params = new URLSearchParams(url.split("?")[1] ?? "");
939
+ const query = params.get("q") ?? "";
940
+ const target = params.get("target") || void 0;
941
+ const logs = options.store ? options.store.searchLogs(query, target !== void 0 ? { target } : {}) : [];
942
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
943
+ res.end(JSON.stringify({ logs }));
944
+ return;
945
+ }
946
+ const invLogsMatch = /^\/api\/invocations\/([^/]+)\/logs$/.exec(path ?? "");
947
+ if (req.method === "GET" && invLogsMatch) {
948
+ const id = decodeURIComponent(invLogsMatch[1] ?? "");
949
+ const logs = options.store ? options.store.logsForInvocation(id) : [];
950
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
951
+ res.end(JSON.stringify({ logs }));
952
+ return;
953
+ }
747
954
  if (req.method === "GET" && path === "/api/events") {
748
955
  serveSse(req, res, bus);
749
956
  return;
@@ -1300,10 +1507,37 @@ function bridgeUpgrade(req, clientSocket, head, upstreamHost, upstreamPort) {
1300
1507
 
1301
1508
  //#endregion
1302
1509
  //#region src/local/studio-serve-manager.ts
1303
- /** Kinds the serve manager can start in this build. */
1304
- const SERVE_SUPPORTED = ["api"];
1305
- /** `Server listening on <url>` is the stable ready marker `start-api` prints. */
1306
- const LISTENING_RE = /Server listening on (\S+)/;
1510
+ /**
1511
+ * The serve lifecycle per kind. `api` + `alb` expose host HTTP endpoints
1512
+ * the studio capture proxy fronts; `ecs` (start-service) is pure compute
1513
+ * with no host port, so it has no endpoint and no capture — studio just
1514
+ * runs the replicas + streams their logs.
1515
+ */
1516
+ const SERVE_SPECS = {
1517
+ api: {
1518
+ command: "start-api",
1519
+ portArgs: [
1520
+ "--port",
1521
+ "0",
1522
+ "--host",
1523
+ "127.0.0.1"
1524
+ ],
1525
+ readyRe: /Server listening on (\S+)/,
1526
+ capturesHttp: true
1527
+ },
1528
+ alb: {
1529
+ command: "start-alb",
1530
+ portArgs: [],
1531
+ readyRe: /ALB front-door: (https?:\/\/\S+)/,
1532
+ capturesHttp: true
1533
+ },
1534
+ ecs: {
1535
+ command: "start-service",
1536
+ portArgs: [],
1537
+ readyRe: /Service\(s\) running:/,
1538
+ capturesHttp: false
1539
+ }
1540
+ };
1307
1541
  /**
1308
1542
  * Build the studio serve manager. Slice C1 drives a long-running
1309
1543
  * `cdkl start-api <target>` child — studio is a control plane over the
@@ -1322,15 +1556,18 @@ const LISTENING_RE = /Server listening on (\S+)/;
1322
1556
  * Slice C2 fronts each HTTP serve endpoint with a capture proxy
1323
1557
  * ({@link startStudioProxy}) so every request to the served port lands
1324
1558
  * on the studio timeline (decision D4a); the `endpoints` the UI is
1325
- * handed are the proxy URLs. Full-text log search and alb / ecs serve
1326
- * kinds are still to come.
1559
+ * handed are the proxy URLs. The serve-kinds slice generalized this to a
1560
+ * per-kind {@link ServeKindSpec}: `api` (`start-api`) + `alb`
1561
+ * (`start-alb`) expose host HTTP endpoints the proxy captures, while
1562
+ * `ecs` (`start-service`) is pure compute — no host port, no capture,
1563
+ * just the running replicas + their streamed logs.
1327
1564
  */
1328
1565
  function createStudioServeManager(config) {
1329
1566
  const spawnFn = config.spawnFn ?? spawn;
1330
1567
  const nodeBin = config.nodeBin ?? process.execPath;
1331
1568
  const clock = config.clock ?? Date.now;
1332
1569
  const readyTimeoutMs = config.readyTimeoutMs ?? 12e4;
1333
- const stopGraceMs = config.stopGraceMs ?? 1e4;
1570
+ const stopGraceMs = config.stopGraceMs ?? 45e3;
1334
1571
  const setTimeoutFn = config.setTimeoutFn ?? setTimeout;
1335
1572
  const clearTimeoutFn = config.clearTimeoutFn ?? clearTimeout;
1336
1573
  const cwd = config.cwd ?? process.cwd();
@@ -1365,14 +1602,11 @@ function createStudioServeManager(config) {
1365
1602
  if (message !== void 0) ev.message = message;
1366
1603
  config.bus.emit("serve", ev);
1367
1604
  }
1368
- function buildArgs(targetId) {
1605
+ function buildArgs(targetId, spec) {
1369
1606
  const args = [
1370
- "start-api",
1607
+ spec.command,
1371
1608
  targetId,
1372
- "--port",
1373
- "0",
1374
- "--host",
1375
- "127.0.0.1"
1609
+ ...spec.portArgs
1376
1610
  ];
1377
1611
  if (config.app) args.push("--app", config.app);
1378
1612
  if (config.profile) args.push("--profile", config.profile);
@@ -1381,13 +1615,14 @@ function createStudioServeManager(config) {
1381
1615
  return args;
1382
1616
  }
1383
1617
  async function start(req) {
1384
- if (!SERVE_SUPPORTED.includes(req.kind)) throw new Error(`Serving '${req.kind}' targets from studio is not supported yet (API only in this build).`);
1618
+ const spec = SERVE_SPECS[req.kind];
1619
+ if (!spec) throw new Error(`Serving '${req.kind}' targets from studio is not supported (serve kinds: ${Object.keys(SERVE_SPECS).join(", ")}).`);
1385
1620
  const existing = entries.get(req.targetId);
1386
1621
  if (existing && existing.status !== "stopped" && existing.status !== "error") throw new Error(`'${req.targetId}' is already running.`);
1387
1622
  const startedAt = clock();
1388
1623
  let child;
1389
1624
  try {
1390
- child = spawnFn(nodeBin, [config.cliEntry, ...buildArgs(req.targetId)], { cwd });
1625
+ child = spawnFn(nodeBin, [config.cliEntry, ...buildArgs(req.targetId, spec)], { cwd });
1391
1626
  } catch (err) {
1392
1627
  throw err instanceof Error ? err : new Error(String(err));
1393
1628
  }
@@ -1411,7 +1646,7 @@ function createStudioServeManager(config) {
1411
1646
  if (settled) return;
1412
1647
  settled = true;
1413
1648
  entry.status = "error";
1414
- emitServe(entry, `Timed out after ${readyTimeoutMs}ms waiting for the server to listen.`);
1649
+ emitServe(entry, `Timed out after ${readyTimeoutMs}ms waiting for the serve to be ready.`);
1415
1650
  stopChild(child, stopGraceMs, setTimeoutFn, clearTimeoutFn);
1416
1651
  closeProxies(entry);
1417
1652
  entries.delete(req.targetId);
@@ -1426,9 +1661,9 @@ function createStudioServeManager(config) {
1426
1661
  emitServe(entry);
1427
1662
  resolve(publicState(entry));
1428
1663
  };
1429
- const onListening = async (childUrl) => {
1664
+ const onReady = async (childUrl) => {
1430
1665
  let endpoint = childUrl;
1431
- if (captureRequests && /^https?:/i.test(childUrl)) try {
1666
+ if (childUrl && spec.capturesHttp && captureRequests && /^https?:/i.test(childUrl)) try {
1432
1667
  const proxy = await proxyFactory({
1433
1668
  bus: config.bus,
1434
1669
  target: req.targetId,
@@ -1444,13 +1679,13 @@ function createStudioServeManager(config) {
1444
1679
  await closeProxies(entry);
1445
1680
  return;
1446
1681
  }
1447
- if (!entry.endpoints.includes(endpoint)) entry.endpoints.push(endpoint);
1682
+ if (endpoint && !entry.endpoints.includes(endpoint)) entry.endpoints.push(endpoint);
1448
1683
  if (settled) emitServe(entry);
1449
1684
  else becomeRunning();
1450
1685
  };
1451
1686
  streamLines(child.stdout, (line) => {
1452
- const m = LISTENING_RE.exec(line);
1453
- if (m?.[1]) onListening(m[1]);
1687
+ const m = spec.readyRe.exec(line);
1688
+ if (m) onReady(m[1]);
1454
1689
  emitLog(config.bus, clock, req.targetId, line, "stdout");
1455
1690
  });
1456
1691
  streamLines(child.stderr, (line) => {
@@ -1651,6 +1886,7 @@ async function localStudioCommand(options) {
1651
1886
  const { stacks } = await synthesizer.synthesize(synthOpts);
1652
1887
  const targetGroups = toStudioTargetGroups(listTargets(stacks));
1653
1888
  const appLabel = stacks.map((s) => s.stackName).join(", ") || appCmd;
1889
+ const servableEcs = new Set(targetGroups.filter((g) => g.kind === "ecs").flatMap((g) => g.entries.filter((e) => e.servable).map((e) => e.id)));
1654
1890
  const bus = new StudioEventBus();
1655
1891
  const childConfig = {
1656
1892
  cliEntry: process.argv[1] ?? "",
@@ -1663,15 +1899,19 @@ async function localStudioCommand(options) {
1663
1899
  };
1664
1900
  const dispatcher = createStudioDispatcher(childConfig);
1665
1901
  const serveManager = createStudioServeManager(childConfig);
1902
+ const store = createStudioStore(bus);
1666
1903
  const server = await startStudioServer({
1667
1904
  port,
1668
1905
  bus,
1669
1906
  targetGroups,
1670
1907
  appLabel,
1671
1908
  cliName: getEmbedConfig().cliName,
1909
+ store,
1672
1910
  onRun: (body) => {
1673
1911
  const req = coerceRunRequest(body);
1674
- return req.kind === "lambda" ? dispatcher.run(req) : serveManager.start(req);
1912
+ if (req.kind === "lambda") return dispatcher.run(req);
1913
+ if (req.kind === "ecs" && !servableEcs.has(req.targetId)) return Promise.reject(/* @__PURE__ */ new Error(`'${req.targetId}' is not a servable ECS service (an ECS task definition runs via run-task, not start-service).`));
1914
+ return serveManager.start(req);
1675
1915
  },
1676
1916
  onStop: async (body) => {
1677
1917
  const req = coerceStopRequest(body);
@@ -1684,7 +1924,7 @@ async function localStudioCommand(options) {
1684
1924
  logger.info(`${cliName} studio is running at ${server.url}`);
1685
1925
  logger.info("Press Ctrl-C to stop.");
1686
1926
  if (options.open && process.stdout.isTTY) openBrowser(server.url);
1687
- await blockUntilShutdown(server, serveManager, cliName);
1927
+ await blockUntilShutdown(server, serveManager, store, cliName);
1688
1928
  }
1689
1929
  /** Best-effort cross-platform browser open. Failures are non-fatal. */
1690
1930
  function openBrowser(url) {
@@ -1706,13 +1946,14 @@ function openBrowser(url) {
1706
1946
  * BEFORE the server closes so their RIE containers are torn down rather
1707
1947
  * than orphaned.
1708
1948
  */
1709
- function blockUntilShutdown(server, serveManager, cliName) {
1949
+ function blockUntilShutdown(server, serveManager, store, cliName) {
1710
1950
  return new Promise((resolveShutdown) => {
1711
1951
  let shuttingDown = false;
1712
1952
  const shutdown = (signal) => {
1713
1953
  if (shuttingDown) return;
1714
1954
  shuttingDown = true;
1715
1955
  getLogger().info(`Received ${signal}; stopping ${cliName} studio...`);
1956
+ store.dispose();
1716
1957
  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());
1717
1958
  };
1718
1959
  process.on("SIGINT", () => shutdown("SIGINT"));
@@ -1748,5 +1989,5 @@ function addStudioSpecificOptions(cmd) {
1748
1989
  }
1749
1990
 
1750
1991
  //#endregion
1751
- 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 };
1752
- //# sourceMappingURL=local-studio-DvNmnaF0.js.map
1992
+ export { createStudioServeManager as a, startStudioServer as c, createStudioStore as d, StudioEventBus as f, createLocalStudioCommand as i, toStudioTargetGroups as l, coerceRunRequest as n, startStudioProxy as o, coerceStopRequest as r, createStudioDispatcher as s, addStudioSpecificOptions as t, renderStudioHtml as u };
1993
+ //# sourceMappingURL=local-studio-8r3jr9nj.js.map