cdk-local 0.78.1 → 0.80.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.
@@ -1,484 +0,0 @@
1
- import { c as getEmbedConfig, s as getLogger, u as setEmbedConfig } from "./docker-cmd-Dqx2YENO.js";
2
- import { Hn as listTargets, Ln as resolveApp, cr as withErrorHandling, fr as appOptions, gr as regionOption, hr as parseContextOptions, lr as applyRoleArnIfSet, mr as contextOptions, pr as commonOptions, zn as Synthesizer } from "./local-list-DBlBRfA-.js";
3
- import { Command, Option } from "commander";
4
- import { spawn } from "node:child_process";
5
- import { createServer } from "node:http";
6
- import { EventEmitter } from "node:events";
7
-
8
- //#region src/local/studio-events.ts
9
- /**
10
- * In-process event bus that every studio observation flows through. The
11
- * studio HTTP server subscribes and forwards events to the browser over
12
- * SSE; the dispatch / log-streaming layers emit onto it.
13
- *
14
- * A thin typed wrapper over {@link EventEmitter} so producers and the
15
- * server agree on the event shapes without `any`. Re-exported from
16
- * `cdk-local/internal` so a host CLI embedding studio can subscribe.
17
- */
18
- var StudioEventBus = class {
19
- emitter = new EventEmitter();
20
- constructor() {
21
- this.emitter.setMaxListeners(0);
22
- }
23
- emit(event, ...args) {
24
- this.emitter.emit(event, ...args);
25
- }
26
- on(event, listener) {
27
- this.emitter.on(event, listener);
28
- return this;
29
- }
30
- off(event, listener) {
31
- this.emitter.off(event, listener);
32
- return this;
33
- }
34
- /**
35
- * Number of listeners currently subscribed to `event`. Exposed so the
36
- * SSE server's subscribe / unsubscribe symmetry can be asserted (a
37
- * dropped client must not leak a listener).
38
- */
39
- listenerCount(event) {
40
- return this.emitter.listenerCount(event);
41
- }
42
- };
43
-
44
- //#endregion
45
- //#region src/local/studio-ui.ts
46
- /**
47
- * The studio web UI, embedded as a string so it ships inside the
48
- * `cdk-local` npm package (decision D9) with no asset-copy build step —
49
- * `tsdown` bundles this module like any other source file. Served by
50
- * the studio HTTP server (`startStudioServer`) at `GET /`.
51
- *
52
- * Slice A renders the 3-pane shell (decision D6): left = target list
53
- * (from `GET /api/targets`), center = live timeline (from the
54
- * `GET /api/events` SSE stream — empty until the invoke / serve slices
55
- * emit invocations), right = detail panel (filled on row click). It is
56
- * intentionally framework-free vanilla JS (decision D7).
57
- */
58
- const STUDIO_CSS = `
59
- * { box-sizing: border-box; }
60
- body {
61
- margin: 0; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
62
- color: #e6e6e6; background: #1a1a1a; height: 100vh; overflow: hidden;
63
- }
64
- header {
65
- padding: 8px 14px; background: #111; border-bottom: 1px solid #333;
66
- display: flex; align-items: center; gap: 10px;
67
- }
68
- header .brand { font-weight: 700; color: #fff; }
69
- header .meta { color: #888; font-size: 12px; }
70
- main { display: grid; grid-template-columns: 280px 1fr 360px; height: calc(100vh - 38px); }
71
- .pane { overflow: auto; border-right: 1px solid #333; }
72
- .pane:last-child { border-right: 0; }
73
- .pane h2 {
74
- margin: 0; padding: 8px 12px; font-size: 11px; text-transform: uppercase;
75
- letter-spacing: 0.5px; color: #888; background: #151515;
76
- position: sticky; top: 0; border-bottom: 1px solid #2a2a2a;
77
- }
78
- .group-title { padding: 8px 12px 2px; color: #6aa9ff; font-size: 11px; }
79
- .target { padding: 5px 12px; cursor: default; border-bottom: 1px solid #222; }
80
- .target .name { color: #ddd; }
81
- .target .kind { color: #777; font-size: 11px; }
82
- .empty { padding: 16px 12px; color: #666; }
83
- .row {
84
- padding: 5px 12px; border-bottom: 1px solid #222; cursor: pointer;
85
- display: flex; gap: 8px; white-space: nowrap;
86
- }
87
- .row:hover { background: #222; }
88
- .row.sel { background: #2a3550; }
89
- .row .ts { color: #777; }
90
- .row .label { color: #ddd; flex: 1; overflow: hidden; text-overflow: ellipsis; }
91
- .row .status { color: #7bd88f; }
92
- #detail .section { padding: 8px 12px; border-bottom: 1px solid #222; }
93
- #detail .section h3 { margin: 0 0 6px; font-size: 11px; color: #888; text-transform: uppercase; }
94
- #detail pre { margin: 0; white-space: pre-wrap; word-break: break-word; color: #cfcfcf; }
95
- #conn { font-size: 11px; }
96
- #conn.up { color: #7bd88f; }
97
- #conn.down { color: #e0707a; }
98
- `;
99
- const STUDIO_SCRIPT = `
100
- const KIND_LABEL = { lambda: 'Lambda', api: 'API', alb: 'ALB', ecs: 'ECS', agentcore: 'AgentCore' };
101
- const rowsById = new Map();
102
-
103
- function el(tag, cls, text) {
104
- const e = document.createElement(tag);
105
- if (cls) e.className = cls;
106
- if (text != null) e.textContent = text;
107
- return e;
108
- }
109
-
110
- async function loadTargets() {
111
- const pane = document.getElementById('targets');
112
- try {
113
- const res = await fetch('/api/targets');
114
- const data = await res.json();
115
- pane.querySelectorAll('.group-title,.target,.empty').forEach((n) => n.remove());
116
- let total = 0;
117
- for (const group of data.groups) {
118
- if (!group.entries.length) continue;
119
- pane.appendChild(el('div', 'group-title', group.title));
120
- for (const entry of group.entries) {
121
- total += 1;
122
- const t = el('div', 'target');
123
- t.appendChild(el('span', 'name', entry.id));
124
- t.appendChild(document.createTextNode(' '));
125
- t.appendChild(el('span', 'kind', '(' + (KIND_LABEL[group.kind] || group.kind) + ')'));
126
- pane.appendChild(t);
127
- }
128
- }
129
- if (!total) pane.appendChild(el('div', 'empty', 'No runnable targets found.'));
130
- } catch (err) {
131
- pane.appendChild(el('div', 'empty', 'Failed to load targets: ' + err));
132
- }
133
- }
134
-
135
- function addInvocation(ev) {
136
- const timeline = document.getElementById('timeline');
137
- const placeholder = timeline.querySelector('.empty');
138
- if (placeholder) placeholder.remove();
139
- let row = rowsById.get(ev.id);
140
- if (!row) {
141
- row = el('div', 'row');
142
- row.appendChild(el('span', 'ts'));
143
- row.appendChild(el('span', 'label'));
144
- row.appendChild(el('span', 'status'));
145
- row.onclick = () => selectRow(ev.id);
146
- rowsById.set(ev.id, row);
147
- timeline.appendChild(row);
148
- }
149
- row._ev = Object.assign(row._ev || {}, ev);
150
- const d = new Date(ev.ts);
151
- row.querySelector('.ts').textContent = d.toLocaleTimeString();
152
- row.querySelector('.label').textContent = (ev.kind ? ev.target + ' ' : '') + (ev.label || '');
153
- row.querySelector('.status').textContent =
154
- ev.status != null ? ev.status + (ev.durationMs != null ? ' ' + ev.durationMs + 'ms' : '') : '...';
155
- }
156
-
157
- function selectRow(id) {
158
- document.querySelectorAll('.row.sel').forEach((n) => n.classList.remove('sel'));
159
- const row = rowsById.get(id);
160
- if (row) row.classList.add('sel');
161
- const ev = row && row._ev;
162
- const detail = document.getElementById('detail');
163
- detail.innerHTML = '';
164
- if (!ev) return;
165
- const sec = (title, body) => {
166
- const s = el('div', 'section');
167
- s.appendChild(el('h3', null, title));
168
- const pre = el('pre');
169
- pre.textContent = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
170
- s.appendChild(pre);
171
- detail.appendChild(s);
172
- };
173
- sec('Request', ev.request != null ? ev.request : '(none)');
174
- sec('Response', ev.response != null ? ev.response : '(pending)');
175
- }
176
-
177
- function connect() {
178
- const conn = document.getElementById('conn');
179
- const es = new EventSource('/api/events');
180
- es.addEventListener('open', () => { conn.textContent = '● live'; conn.className = 'up'; });
181
- es.addEventListener('error', () => { conn.textContent = '● disconnected'; conn.className = 'down'; });
182
- es.addEventListener('invocation', (e) => addInvocation(JSON.parse(e.data)));
183
- }
184
-
185
- loadTargets();
186
- connect();
187
- `;
188
- /**
189
- * Render the full studio HTML document. `appLabel` is shown in the
190
- * header (the CDK app / stack context); `cliName` brands the title for
191
- * host CLIs that rebrand `cdkl`.
192
- */
193
- function renderStudioHtml(appLabel, cliName) {
194
- const safeApp = escapeHtml(appLabel);
195
- const safeCli = escapeHtml(cliName);
196
- return `<!doctype html>
197
- <html lang="en">
198
- <head>
199
- <meta charset="utf-8" />
200
- <meta name="viewport" content="width=device-width, initial-scale=1" />
201
- <title>${safeCli} studio</title>
202
- <style>${STUDIO_CSS}</style>
203
- </head>
204
- <body>
205
- <header>
206
- <span class="brand">${safeCli} studio</span>
207
- <span class="meta">${safeApp}</span>
208
- <span id="conn" class="down">● connecting</span>
209
- </header>
210
- <main>
211
- <section class="pane" id="targets"><h2>Targets</h2></section>
212
- <section class="pane" id="timeline"><h2>Timeline</h2><div class="empty">No requests yet. Invoke or serve a target to see activity.</div></section>
213
- <section class="pane" id="detail"><h2>Detail</h2><div class="empty">Select a request.</div></section>
214
- </main>
215
- <script>${STUDIO_SCRIPT}<\/script>
216
- </body>
217
- </html>`;
218
- }
219
- /** Minimal HTML-escape for the few interpolated text values. */
220
- function escapeHtml(s) {
221
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
222
- }
223
-
224
- //#endregion
225
- //#region src/local/studio-server.ts
226
- /**
227
- * Project a {@link TargetListing} (the same enumeration `cdkl list`
228
- * prints) into the grouped shape the studio UI renders. ECS services and
229
- * task definitions are folded into one `ecs` group; everything else maps
230
- * one category to one group. Exported so a unit test can assert the
231
- * projection without booting the server.
232
- */
233
- function toStudioTargetGroups(listing) {
234
- const map = (entries) => entries.map((e) => {
235
- const t = {
236
- id: e.displayPath ?? e.qualifiedId,
237
- qualifiedId: e.qualifiedId
238
- };
239
- if (e.kind) t.surface = e.kind;
240
- return t;
241
- });
242
- return [
243
- {
244
- kind: "lambda",
245
- title: "Lambda Functions",
246
- entries: map(listing.lambdas)
247
- },
248
- {
249
- kind: "api",
250
- title: "APIs",
251
- entries: map(listing.apis)
252
- },
253
- {
254
- kind: "ecs",
255
- title: "ECS Services / Tasks",
256
- entries: [...map(listing.ecsServices), ...map(listing.ecsTaskDefinitions)]
257
- },
258
- {
259
- kind: "agentcore",
260
- title: "AgentCore Runtimes",
261
- entries: map(listing.agentCoreRuntimes)
262
- },
263
- {
264
- kind: "alb",
265
- title: "Load Balancers",
266
- entries: map(listing.loadBalancers)
267
- }
268
- ];
269
- }
270
- const SSE_HEARTBEAT_MS = 15e3;
271
- /**
272
- * Boot the studio HTTP server: serves the embedded UI at `/`, the target
273
- * list at `/api/targets`, and a Server-Sent-Events stream of the bus's
274
- * `invocation` / `log` events at `/api/events`. Localhost-only by
275
- * default. Resolves once the socket is listening.
276
- */
277
- async function startStudioServer(options) {
278
- const host = options.host ?? "127.0.0.1";
279
- const maxBump = options.maxPortBump ?? 20;
280
- const html = renderStudioHtml(options.appLabel, options.cliName);
281
- const targetsJson = JSON.stringify({ groups: options.targetGroups });
282
- const server = createServer((req, res) => handleRequest(req, res, options.bus, html, targetsJson));
283
- const boundPort = await listenWithBump(server, host, options.port, maxBump);
284
- return {
285
- url: `http://${host}:${boundPort}`,
286
- port: boundPort,
287
- close: () => new Promise((resolveClose, reject) => {
288
- server.close((err) => err ? reject(err) : resolveClose());
289
- server.closeAllConnections?.();
290
- })
291
- };
292
- }
293
- function handleRequest(req, res, bus, html, targetsJson) {
294
- const path = (req.url ?? "/").split("?")[0];
295
- if (req.method === "GET" && (path === "/" || path === "/index.html")) {
296
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
297
- res.end(html);
298
- return;
299
- }
300
- if (req.method === "GET" && path === "/api/targets") {
301
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
302
- res.end(targetsJson);
303
- return;
304
- }
305
- if (req.method === "GET" && path === "/api/events") {
306
- serveSse(req, res, bus);
307
- return;
308
- }
309
- res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
310
- res.end("Not found");
311
- }
312
- function serveSse(req, res, bus) {
313
- res.writeHead(200, {
314
- "content-type": "text/event-stream; charset=utf-8",
315
- "cache-control": "no-cache, no-transform",
316
- connection: "keep-alive"
317
- });
318
- let closed = false;
319
- const heartbeat = setInterval(() => safeWrite(":hb\n\n"), SSE_HEARTBEAT_MS);
320
- heartbeat.unref?.();
321
- const onInvocation = (ev) => {
322
- safeWrite(`event: invocation\ndata: ${JSON.stringify(ev)}\n\n`);
323
- };
324
- const onLog = (ev) => {
325
- safeWrite(`event: log\ndata: ${JSON.stringify(ev)}\n\n`);
326
- };
327
- function cleanup() {
328
- if (closed) return;
329
- closed = true;
330
- clearInterval(heartbeat);
331
- bus.off("invocation", onInvocation);
332
- bus.off("log", onLog);
333
- }
334
- function safeWrite(chunk) {
335
- if (closed || res.writableEnded || res.destroyed) {
336
- cleanup();
337
- return;
338
- }
339
- res.write(chunk);
340
- }
341
- bus.on("invocation", onInvocation);
342
- bus.on("log", onLog);
343
- req.on("close", cleanup);
344
- res.on("close", cleanup);
345
- res.on("error", cleanup);
346
- safeWrite(":ok\n\n");
347
- }
348
- /**
349
- * Listen on `port`, retrying `port+1`, `port+2`, ... on `EADDRINUSE` up
350
- * to `maxBump` extra attempts. Resolves with the bound port.
351
- */
352
- function listenWithBump(server, host, port, maxBump) {
353
- return new Promise((resolveListen, reject) => {
354
- let attempt = 0;
355
- const tryListen = (p) => {
356
- const onError = (err) => {
357
- if (err.code === "EADDRINUSE" && attempt < maxBump) {
358
- attempt += 1;
359
- server.removeListener("error", onError);
360
- tryListen(p + 1);
361
- return;
362
- }
363
- reject(err);
364
- };
365
- server.once("error", onError);
366
- server.listen(p, host, () => {
367
- server.removeListener("error", onError);
368
- resolveListen(server.address().port);
369
- });
370
- };
371
- tryListen(port);
372
- });
373
- }
374
-
375
- //#endregion
376
- //#region src/cli/commands/local-studio.ts
377
- const DEFAULT_STUDIO_PORT = 9999;
378
- /**
379
- * Parse + validate the `--studio-port` value. Accepts `0` (OS-assigned)
380
- * through `65535`. Exported so a unit test can assert the bounds without
381
- * driving the full command. Throws on anything out of range / non-numeric.
382
- */
383
- function parseStudioPort(raw) {
384
- const port = raw.trim() === "" ? NaN : Number(raw);
385
- if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`--studio-port must be 0..65535 (got ${raw}).`);
386
- return port;
387
- }
388
- async function localStudioCommand(options) {
389
- const logger = getLogger();
390
- if (options.verbose) logger.setLevel("debug");
391
- const port = parseStudioPort(options.studioPort);
392
- await applyRoleArnIfSet({
393
- roleArn: options.roleArn,
394
- region: void 0,
395
- profile: options.profile
396
- });
397
- const appCmd = resolveApp(options.app);
398
- if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
399
- logger.info("Synthesizing CDK app...");
400
- const synthesizer = new Synthesizer();
401
- const context = parseContextOptions(options.context);
402
- const synthOpts = {
403
- app: appCmd,
404
- output: options.output,
405
- ...options.profile && { profile: options.profile },
406
- ...Object.keys(context).length > 0 && { context }
407
- };
408
- const { stacks } = await synthesizer.synthesize(synthOpts);
409
- const targetGroups = toStudioTargetGroups(listTargets(stacks));
410
- const appLabel = stacks.map((s) => s.stackName).join(", ") || appCmd;
411
- const server = await startStudioServer({
412
- port,
413
- bus: new StudioEventBus(),
414
- targetGroups,
415
- appLabel,
416
- cliName: getEmbedConfig().cliName
417
- });
418
- const cliName = getEmbedConfig().cliName;
419
- logger.info(`${cliName} studio is running at ${server.url}`);
420
- logger.info("Press Ctrl-C to stop.");
421
- if (options.open && process.stdout.isTTY) openBrowser(server.url);
422
- await blockUntilShutdown(server, cliName);
423
- }
424
- /** Best-effort cross-platform browser open. Failures are non-fatal. */
425
- function openBrowser(url) {
426
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
427
- try {
428
- const child = spawn(cmd, [url], {
429
- stdio: "ignore",
430
- detached: true,
431
- shell: process.platform === "win32"
432
- });
433
- child.on("error", () => void 0);
434
- child.unref();
435
- } catch {}
436
- }
437
- /**
438
- * Block until SIGINT / SIGTERM, then close the studio server and resolve.
439
- * Mirrors the long-running serve commands' graceful-shutdown contract.
440
- */
441
- function blockUntilShutdown(server, cliName) {
442
- return new Promise((resolveShutdown) => {
443
- let shuttingDown = false;
444
- const shutdown = (signal) => {
445
- if (shuttingDown) return;
446
- shuttingDown = true;
447
- getLogger().info(`Received ${signal}; stopping ${cliName} studio...`);
448
- server.close().catch((err) => getLogger().warn(`Error stopping studio server: ${String(err)}`)).finally(() => resolveShutdown());
449
- };
450
- process.on("SIGINT", () => shutdown("SIGINT"));
451
- process.on("SIGTERM", () => shutdown("SIGTERM"));
452
- });
453
- }
454
- function createLocalStudioCommand(opts = {}) {
455
- setEmbedConfig(opts.embedConfig);
456
- const cmd = new Command("studio").description("Open the local studio: a web console that lists the synthesized CDK app's runnable targets and lets you invoke / serve them from the browser while watching all activity in one timeline. The interactive counterpart to the headless invoke / start-* commands.").action(withErrorHandling(async (options) => {
457
- await localStudioCommand(options);
458
- }));
459
- addStudioSpecificOptions(cmd);
460
- [
461
- ...commonOptions(),
462
- ...appOptions(),
463
- ...contextOptions
464
- ].forEach((opt) => cmd.addOption(opt));
465
- cmd.addOption(regionOption);
466
- return cmd;
467
- }
468
- /**
469
- * Register the option block `cdkl studio` adds on top of the shared
470
- * common / app / context option helpers. Kept in a named helper (not
471
- * inline in {@link createLocalStudioCommand}) so a host CLI embedding
472
- * this factory inherits new studio flags without a duplicate
473
- * `.addOption(...)` block, matching every other `add<Cmd>SpecificOptions`
474
- * extraction. Chainable: returns `cmd`.
475
- */
476
- function addStudioSpecificOptions(cmd) {
477
- cmd.addOption(new Option("--studio-port <port>", "Preferred port for the studio web server (bumps to the next free port on collision)").default(String(DEFAULT_STUDIO_PORT)));
478
- cmd.addOption(new Option("--no-open", "Do not auto-open the browser when studio starts (TTY only)"));
479
- return cmd;
480
- }
481
-
482
- //#endregion
483
- export { renderStudioHtml as a, toStudioTargetGroups as i, createLocalStudioCommand as n, StudioEventBus as o, startStudioServer as r, addStudioSpecificOptions as t };
484
- //# sourceMappingURL=local-studio-BuU6kMYP.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"local-studio-BuU6kMYP.js","names":[],"sources":["../src/local/studio-events.ts","../src/local/studio-ui.ts","../src/local/studio-server.ts","../src/cli/commands/local-studio.ts"],"sourcesContent":["import { EventEmitter } from 'node:events';\n\n/**\n * The kind of runnable target an invocation ran against. Mirrors the\n * categories `cdkl list` / the studio target list expose.\n */\nexport type StudioTargetKind = 'lambda' | 'api' | 'alb' | 'ecs' | 'agentcore';\n\n/**\n * One request observed by `cdkl studio` — a single-shot invoke or one\n * request to a served target. Emitted twice per request: once at start\n * (status/durationMs/response absent) and once at end (filled in). The\n * UI threads the two by {@link StudioInvocationEvent.id}.\n *\n * Slice A defines the shape; the dispatch layer that emits populated\n * events lands with the invoke / serve slices.\n */\nexport interface StudioInvocationEvent {\n /** Correlation id, unique per request. Stable across the start/end pair. */\n id: string;\n /** Wall-clock epoch ms when the request started. */\n ts: number;\n /** Target id the request ran against (stack-qualified or display path). */\n target: string;\n /** The target's kind, for the UI's per-kind affordances. */\n kind: StudioTargetKind;\n /** Short request label for the timeline row (e.g. `GET /orders`). */\n label: string;\n /** The request payload (Lambda event JSON or the HTTP request shape). */\n request?: unknown;\n /** Response payload, set on the end event. */\n response?: unknown;\n /** HTTP status / invoke outcome code, set on the end event. */\n status?: number;\n /** Wall-clock duration in ms, set on the end event. */\n durationMs?: number;\n /**\n * When this invocation is a re-run of an earlier one (Phase 3), the id\n * of the source invocation. The UI links the new row to its origin.\n */\n reinvokeOf?: string;\n}\n\n/**\n * One container stdout/stderr line, tagged with its emitting container\n * so the detail panel can scope logs at CloudWatch granularity (Lambda\n * per-invocation, ECS per-container).\n */\nexport interface StudioLogEvent {\n /** Wall-clock epoch ms when the line was observed. */\n ts: number;\n /** Container id that emitted the line. */\n containerId: string;\n /** Target id the container backs, for the UI grouping. */\n target: string;\n /** The raw log line (without trailing newline). */\n line: string;\n /** Parsed stream, when known. */\n stream?: 'stdout' | 'stderr';\n}\n\n/**\n * Map of event name -> listener argument, for the typed wrapper below.\n */\ninterface StudioEventMap {\n invocation: [StudioInvocationEvent];\n log: [StudioLogEvent];\n}\n\n/**\n * In-process event bus that every studio observation flows through. The\n * studio HTTP server subscribes and forwards events to the browser over\n * SSE; the dispatch / log-streaming layers emit onto it.\n *\n * A thin typed wrapper over {@link EventEmitter} so producers and the\n * server agree on the event shapes without `any`. Re-exported from\n * `cdk-local/internal` so a host CLI embedding studio can subscribe.\n */\nexport class StudioEventBus {\n private readonly emitter = new EventEmitter();\n\n constructor() {\n // The bus can accumulate many SSE subscribers across a long session;\n // raise the cap so Node does not warn about a suspected leak.\n this.emitter.setMaxListeners(0);\n }\n\n emit<E extends keyof StudioEventMap>(event: E, ...args: StudioEventMap[E]): void {\n this.emitter.emit(event, ...args);\n }\n\n on<E extends keyof StudioEventMap>(\n event: E,\n listener: (...args: StudioEventMap[E]) => void\n ): this {\n this.emitter.on(event, listener as (...args: unknown[]) => void);\n return this;\n }\n\n off<E extends keyof StudioEventMap>(\n event: E,\n listener: (...args: StudioEventMap[E]) => void\n ): this {\n this.emitter.off(event, listener as (...args: unknown[]) => void);\n return this;\n }\n\n /**\n * Number of listeners currently subscribed to `event`. Exposed so the\n * SSE server's subscribe / unsubscribe symmetry can be asserted (a\n * dropped client must not leak a listener).\n */\n listenerCount<E extends keyof StudioEventMap>(event: E): number {\n return this.emitter.listenerCount(event);\n }\n}\n","/**\n * The studio web UI, embedded as a string so it ships inside the\n * `cdk-local` npm package (decision D9) with no asset-copy build step —\n * `tsdown` bundles this module like any other source file. Served by\n * the studio HTTP server (`startStudioServer`) at `GET /`.\n *\n * Slice A renders the 3-pane shell (decision D6): left = target list\n * (from `GET /api/targets`), center = live timeline (from the\n * `GET /api/events` SSE stream — empty until the invoke / serve slices\n * emit invocations), right = detail panel (filled on row click). It is\n * intentionally framework-free vanilla JS (decision D7).\n */\n\nconst STUDIO_CSS = `\n * { box-sizing: border-box; }\n body {\n margin: 0; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;\n color: #e6e6e6; background: #1a1a1a; height: 100vh; overflow: hidden;\n }\n header {\n padding: 8px 14px; background: #111; border-bottom: 1px solid #333;\n display: flex; align-items: center; gap: 10px;\n }\n header .brand { font-weight: 700; color: #fff; }\n header .meta { color: #888; font-size: 12px; }\n main { display: grid; grid-template-columns: 280px 1fr 360px; height: calc(100vh - 38px); }\n .pane { overflow: auto; border-right: 1px solid #333; }\n .pane:last-child { border-right: 0; }\n .pane h2 {\n margin: 0; padding: 8px 12px; font-size: 11px; text-transform: uppercase;\n letter-spacing: 0.5px; color: #888; background: #151515;\n position: sticky; top: 0; border-bottom: 1px solid #2a2a2a;\n }\n .group-title { padding: 8px 12px 2px; color: #6aa9ff; font-size: 11px; }\n .target { padding: 5px 12px; cursor: default; border-bottom: 1px solid #222; }\n .target .name { color: #ddd; }\n .target .kind { color: #777; font-size: 11px; }\n .empty { padding: 16px 12px; color: #666; }\n .row {\n padding: 5px 12px; border-bottom: 1px solid #222; cursor: pointer;\n display: flex; gap: 8px; white-space: nowrap;\n }\n .row:hover { background: #222; }\n .row.sel { background: #2a3550; }\n .row .ts { color: #777; }\n .row .label { color: #ddd; flex: 1; overflow: hidden; text-overflow: ellipsis; }\n .row .status { color: #7bd88f; }\n #detail .section { padding: 8px 12px; border-bottom: 1px solid #222; }\n #detail .section h3 { margin: 0 0 6px; font-size: 11px; color: #888; text-transform: uppercase; }\n #detail pre { margin: 0; white-space: pre-wrap; word-break: break-word; color: #cfcfcf; }\n #conn { font-size: 11px; }\n #conn.up { color: #7bd88f; }\n #conn.down { color: #e0707a; }\n`;\n\nconst STUDIO_SCRIPT = `\n const KIND_LABEL = { lambda: 'Lambda', api: 'API', alb: 'ALB', ecs: 'ECS', agentcore: 'AgentCore' };\n const rowsById = new Map();\n\n function el(tag, cls, text) {\n const e = document.createElement(tag);\n if (cls) e.className = cls;\n if (text != null) e.textContent = text;\n return e;\n }\n\n async function loadTargets() {\n const pane = document.getElementById('targets');\n try {\n const res = await fetch('/api/targets');\n const data = await res.json();\n pane.querySelectorAll('.group-title,.target,.empty').forEach((n) => n.remove());\n let total = 0;\n for (const group of data.groups) {\n if (!group.entries.length) continue;\n pane.appendChild(el('div', 'group-title', group.title));\n for (const entry of group.entries) {\n total += 1;\n const t = el('div', 'target');\n t.appendChild(el('span', 'name', entry.id));\n t.appendChild(document.createTextNode(' '));\n t.appendChild(el('span', 'kind', '(' + (KIND_LABEL[group.kind] || group.kind) + ')'));\n pane.appendChild(t);\n }\n }\n if (!total) pane.appendChild(el('div', 'empty', 'No runnable targets found.'));\n } catch (err) {\n pane.appendChild(el('div', 'empty', 'Failed to load targets: ' + err));\n }\n }\n\n function addInvocation(ev) {\n const timeline = document.getElementById('timeline');\n const placeholder = timeline.querySelector('.empty');\n if (placeholder) placeholder.remove();\n let row = rowsById.get(ev.id);\n if (!row) {\n row = el('div', 'row');\n row.appendChild(el('span', 'ts'));\n row.appendChild(el('span', 'label'));\n row.appendChild(el('span', 'status'));\n row.onclick = () => selectRow(ev.id);\n rowsById.set(ev.id, row);\n timeline.appendChild(row);\n }\n row._ev = Object.assign(row._ev || {}, ev);\n const d = new Date(ev.ts);\n row.querySelector('.ts').textContent = d.toLocaleTimeString();\n row.querySelector('.label').textContent = (ev.kind ? ev.target + ' ' : '') + (ev.label || '');\n row.querySelector('.status').textContent =\n ev.status != null ? ev.status + (ev.durationMs != null ? ' ' + ev.durationMs + 'ms' : '') : '...';\n }\n\n function selectRow(id) {\n document.querySelectorAll('.row.sel').forEach((n) => n.classList.remove('sel'));\n const row = rowsById.get(id);\n if (row) row.classList.add('sel');\n const ev = row && row._ev;\n const detail = document.getElementById('detail');\n detail.innerHTML = '';\n if (!ev) return;\n const sec = (title, body) => {\n const s = el('div', 'section');\n s.appendChild(el('h3', null, title));\n const pre = el('pre');\n pre.textContent = typeof body === 'string' ? body : JSON.stringify(body, null, 2);\n s.appendChild(pre);\n detail.appendChild(s);\n };\n sec('Request', ev.request != null ? ev.request : '(none)');\n sec('Response', ev.response != null ? ev.response : '(pending)');\n }\n\n function connect() {\n const conn = document.getElementById('conn');\n const es = new EventSource('/api/events');\n es.addEventListener('open', () => { conn.textContent = '● live'; conn.className = 'up'; });\n es.addEventListener('error', () => { conn.textContent = '● disconnected'; conn.className = 'down'; });\n es.addEventListener('invocation', (e) => addInvocation(JSON.parse(e.data)));\n }\n\n loadTargets();\n connect();\n`;\n\n/**\n * Render the full studio HTML document. `appLabel` is shown in the\n * header (the CDK app / stack context); `cliName` brands the title for\n * host CLIs that rebrand `cdkl`.\n */\nexport function renderStudioHtml(appLabel: string, cliName: string): string {\n const safeApp = escapeHtml(appLabel);\n const safeCli = escapeHtml(cliName);\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>${safeCli} studio</title>\n<style>${STUDIO_CSS}</style>\n</head>\n<body>\n<header>\n <span class=\"brand\">${safeCli} studio</span>\n <span class=\"meta\">${safeApp}</span>\n <span id=\"conn\" class=\"down\">● connecting</span>\n</header>\n<main>\n <section class=\"pane\" id=\"targets\"><h2>Targets</h2></section>\n <section class=\"pane\" id=\"timeline\"><h2>Timeline</h2><div class=\"empty\">No requests yet. Invoke or serve a target to see activity.</div></section>\n <section class=\"pane\" id=\"detail\"><h2>Detail</h2><div class=\"empty\">Select a request.</div></section>\n</main>\n<script>${STUDIO_SCRIPT}</script>\n</body>\n</html>`;\n}\n\n/** Minimal HTML-escape for the few interpolated text values. */\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n","import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport {\n StudioEventBus,\n type StudioInvocationEvent,\n type StudioLogEvent,\n} from './studio-events.js';\nimport { renderStudioHtml } from './studio-ui.js';\nimport type { TargetListing } from './target-lister.js';\n\n/** One target as the studio UI consumes it (`GET /api/targets`). */\nexport interface StudioTarget {\n /** Stable target id — the display path when available, else qualified id. */\n id: string;\n /** Stack-qualified `<Stack>:<LogicalId>` for disambiguation. */\n qualifiedId: string;\n /** API surface kind (REST v1 / HTTP v2 / ...), only for `api` entries. */\n surface?: string;\n}\n\n/** A category of targets, grouped by the studio kind that runs them. */\nexport interface StudioTargetGroup {\n /** Studio kind discriminator shared with {@link StudioInvocationEvent}. */\n kind: 'lambda' | 'api' | 'alb' | 'ecs' | 'agentcore';\n /** Human-readable group heading. */\n title: string;\n entries: StudioTarget[];\n}\n\n/**\n * Project a {@link TargetListing} (the same enumeration `cdkl list`\n * prints) into the grouped shape the studio UI renders. ECS services and\n * task definitions are folded into one `ecs` group; everything else maps\n * one category to one group. Exported so a unit test can assert the\n * projection without booting the server.\n */\nexport function toStudioTargetGroups(listing: TargetListing): StudioTargetGroup[] {\n const map = (entries: TargetListing['lambdas']): StudioTarget[] =>\n entries.map((e) => {\n const t: StudioTarget = { id: e.displayPath ?? e.qualifiedId, qualifiedId: e.qualifiedId };\n if (e.kind) t.surface = e.kind;\n return t;\n });\n return [\n { kind: 'lambda', title: 'Lambda Functions', entries: map(listing.lambdas) },\n { kind: 'api', title: 'APIs', entries: map(listing.apis) },\n {\n kind: 'ecs',\n title: 'ECS Services / Tasks',\n entries: [...map(listing.ecsServices), ...map(listing.ecsTaskDefinitions)],\n },\n { kind: 'agentcore', title: 'AgentCore Runtimes', entries: map(listing.agentCoreRuntimes) },\n { kind: 'alb', title: 'Load Balancers', entries: map(listing.loadBalancers) },\n ];\n}\n\n/** Inputs to {@link startStudioServer}. */\nexport interface StudioServerOptions {\n /** Preferred listen port; bumps on collision (decision: collision-safe). */\n port: number;\n /** Listen host. Defaults to `127.0.0.1` (localhost-only). */\n host?: string;\n /** The shared event bus the SSE stream forwards. */\n bus: StudioEventBus;\n /** Target groups to serve at `GET /api/targets`. */\n targetGroups: StudioTargetGroup[];\n /** Header label for the running app / stack context. */\n appLabel: string;\n /** CLI brand name (`cdkl`, or a host rebrand). */\n cliName: string;\n /**\n * Max consecutive ports to try on `EADDRINUSE` before giving up.\n * Defaults to 20.\n */\n maxPortBump?: number;\n}\n\n/** A running studio server. */\nexport interface RunningStudioServer {\n /** The URL the UI is served at, e.g. `http://127.0.0.1:9999`. */\n url: string;\n /** The actually-bound port (may differ from the requested one). */\n port: number;\n /** Stop the server and release the port. */\n close: () => Promise<void>;\n}\n\nconst SSE_HEARTBEAT_MS = 15_000;\n\n/**\n * Boot the studio HTTP server: serves the embedded UI at `/`, the target\n * list at `/api/targets`, and a Server-Sent-Events stream of the bus's\n * `invocation` / `log` events at `/api/events`. Localhost-only by\n * default. Resolves once the socket is listening.\n */\nexport async function startStudioServer(\n options: StudioServerOptions\n): Promise<RunningStudioServer> {\n const host = options.host ?? '127.0.0.1';\n const maxBump = options.maxPortBump ?? 20;\n const html = renderStudioHtml(options.appLabel, options.cliName);\n const targetsJson = JSON.stringify({ groups: options.targetGroups });\n\n const server = createServer((req, res) =>\n handleRequest(req, res, options.bus, html, targetsJson)\n );\n\n const boundPort = await listenWithBump(server, host, options.port, maxBump);\n\n return {\n url: `http://${host}:${boundPort}`,\n port: boundPort,\n close: () =>\n new Promise<void>((resolveClose, reject) => {\n server.close((err) => (err ? reject(err) : resolveClose()));\n // closeAllConnections exists on Node 18.2+; SSE keeps sockets open\n // so without this `close()` would hang on live EventSource clients.\n server.closeAllConnections?.();\n }),\n };\n}\n\nfunction handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n bus: StudioEventBus,\n html: string,\n targetsJson: string\n): void {\n const url = req.url ?? '/';\n const path = url.split('?')[0];\n\n if (req.method === 'GET' && (path === '/' || path === '/index.html')) {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n res.end(html);\n return;\n }\n if (req.method === 'GET' && path === '/api/targets') {\n res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });\n res.end(targetsJson);\n return;\n }\n if (req.method === 'GET' && path === '/api/events') {\n serveSse(req, res, bus);\n return;\n }\n res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });\n res.end('Not found');\n}\n\nfunction serveSse(req: IncomingMessage, res: ServerResponse, bus: StudioEventBus): void {\n res.writeHead(200, {\n 'content-type': 'text/event-stream; charset=utf-8',\n 'cache-control': 'no-cache, no-transform',\n connection: 'keep-alive',\n });\n\n let closed = false;\n const heartbeat = setInterval(() => safeWrite(':hb\\n\\n'), SSE_HEARTBEAT_MS);\n heartbeat.unref?.();\n\n const onInvocation = (ev: StudioInvocationEvent): void => {\n safeWrite(`event: invocation\\ndata: ${JSON.stringify(ev)}\\n\\n`);\n };\n const onLog = (ev: StudioLogEvent): void => {\n safeWrite(`event: log\\ndata: ${JSON.stringify(ev)}\\n\\n`);\n };\n\n // Idempotent teardown: unsubscribe from the bus + stop the heartbeat so\n // a dropped EventSource client never leaks a listener.\n function cleanup(): void {\n if (closed) return;\n closed = true;\n clearInterval(heartbeat);\n bus.off('invocation', onInvocation);\n bus.off('log', onLog);\n }\n\n // Guard every write: on shutdown `close()` destroys live SSE sockets\n // (closeAllConnections), and a bus emit / heartbeat tick can race that\n // destroy. Writing to a destroyed socket emits an unhandled `error` on\n // the response — which, with no top-level uncaughtException handler on\n // the studio path, would crash the process on Ctrl-C. Drop the write\n // (and tear down) instead.\n function safeWrite(chunk: string): void {\n if (closed || res.writableEnded || res.destroyed) {\n cleanup();\n return;\n }\n res.write(chunk);\n }\n\n bus.on('invocation', onInvocation);\n bus.on('log', onLog);\n\n // `close` fires on client disconnect AND on server-side socket destroy;\n // `error` fires if a write loses the race with the destroy. All three\n // route through the idempotent cleanup so the bus listeners + heartbeat\n // never leak and a write error never propagates as uncaught.\n req.on('close', cleanup);\n res.on('close', cleanup);\n res.on('error', cleanup);\n\n // Open the stream so EventSource fires `open` immediately.\n safeWrite(':ok\\n\\n');\n}\n\n/**\n * Listen on `port`, retrying `port+1`, `port+2`, ... on `EADDRINUSE` up\n * to `maxBump` extra attempts. Resolves with the bound port.\n */\nfunction listenWithBump(\n server: Server,\n host: string,\n port: number,\n maxBump: number\n): Promise<number> {\n return new Promise<number>((resolveListen, reject) => {\n let attempt = 0;\n const tryListen = (p: number): void => {\n const onError = (err: NodeJS.ErrnoException): void => {\n if (err.code === 'EADDRINUSE' && attempt < maxBump) {\n attempt += 1;\n server.removeListener('error', onError);\n tryListen(p + 1);\n return;\n }\n reject(err);\n };\n server.once('error', onError);\n server.listen(p, host, () => {\n server.removeListener('error', onError);\n // Resolve with the ACTUAL bound port from the socket — when the\n // requested port is 0 the OS assigns a free one, which `p` does\n // not reflect.\n resolveListen((server.address() as AddressInfo).port);\n });\n };\n tryListen(port);\n });\n}\n","import { spawn } from 'node:child_process';\nimport { Command, Option } from 'commander';\nimport {\n appOptions,\n commonOptions,\n contextOptions,\n regionOption,\n parseContextOptions,\n} from '../options.js';\nimport { getLogger } from '../../utils/logger.js';\nimport { applyRoleArnIfSet } from '../../utils/role-arn.js';\nimport { withErrorHandling } from '../../utils/error-handler.js';\nimport { Synthesizer, type SynthesisOptions } from '../../synthesis/synthesizer.js';\nimport { resolveApp } from '../config-loader.js';\nimport {\n getEmbedConfig,\n setEmbedConfig,\n type CdkLocalEmbedConfig,\n} from '../../local/embed-config.js';\nimport { listTargets } from '../../local/target-lister.js';\nimport { StudioEventBus } from '../../local/studio-events.js';\nimport {\n startStudioServer,\n toStudioTargetGroups,\n type RunningStudioServer,\n} from '../../local/studio-server.js';\n\nconst DEFAULT_STUDIO_PORT = 9999;\n\n/**\n * Parse + validate the `--studio-port` value. Accepts `0` (OS-assigned)\n * through `65535`. Exported so a unit test can assert the bounds without\n * driving the full command. Throws on anything out of range / non-numeric.\n */\nexport function parseStudioPort(raw: string): number {\n // `Number('')` / `Number(' ')` coerce to 0, which would pass the range\n // check; reject blank input explicitly.\n const port = raw.trim() === '' ? NaN : Number(raw);\n if (!Number.isInteger(port) || port < 0 || port > 65535) {\n throw new Error(`--studio-port must be 0..65535 (got ${raw}).`);\n }\n return port;\n}\n\ninterface LocalStudioOptions {\n app?: string;\n output: string;\n verbose: boolean;\n profile?: string;\n roleArn?: string;\n context?: string[];\n region?: string;\n /** `--studio-port`: preferred listen port (bumps on collision). */\n studioPort: string;\n /** `--no-open`: suppress auto-opening the browser (Commander sets `open`). */\n open: boolean;\n}\n\n/**\n * Factory options for {@link createLocalStudioCommand}.\n */\nexport interface CreateLocalStudioCommandOptions {\n /** Embed-time branding overrides for a host wrapping this factory. */\n embedConfig?: CdkLocalEmbedConfig;\n}\n\nasync function localStudioCommand(options: LocalStudioOptions): Promise<void> {\n const logger = getLogger();\n if (options.verbose) logger.setLevel('debug');\n\n const port = parseStudioPort(options.studioPort);\n\n await applyRoleArnIfSet({\n roleArn: options.roleArn,\n region: undefined,\n profile: options.profile,\n });\n\n const appCmd = resolveApp(options.app);\n if (!appCmd) {\n throw new Error(\n `No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add \"app\" to cdk.json.`\n );\n }\n\n logger.info('Synthesizing CDK app...');\n const synthesizer = new Synthesizer();\n const context = parseContextOptions(options.context);\n const synthOpts: SynthesisOptions = {\n app: appCmd,\n output: options.output,\n ...(options.profile && { profile: options.profile }),\n ...(Object.keys(context).length > 0 && { context }),\n };\n const { stacks } = await synthesizer.synthesize(synthOpts);\n\n const listing = listTargets(stacks);\n const targetGroups = toStudioTargetGroups(listing);\n const appLabel = stacks.map((s) => s.stackName).join(', ') || appCmd;\n\n const bus = new StudioEventBus();\n const server = await startStudioServer({\n port,\n bus,\n targetGroups,\n appLabel,\n cliName: getEmbedConfig().cliName,\n });\n\n const cliName = getEmbedConfig().cliName;\n logger.info(`${cliName} studio is running at ${server.url}`);\n logger.info('Press Ctrl-C to stop.');\n\n // Auto-open the browser only in an interactive terminal (never in CI /\n // piped / integ runs) and unless --no-open was passed.\n if (options.open && process.stdout.isTTY) {\n openBrowser(server.url);\n }\n\n await blockUntilShutdown(server, cliName);\n}\n\n/** Best-effort cross-platform browser open. Failures are non-fatal. */\nfunction openBrowser(url: string): void {\n const cmd =\n process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';\n try {\n const child = spawn(cmd, [url], {\n stdio: 'ignore',\n detached: true,\n shell: process.platform === 'win32',\n });\n child.on('error', () => undefined);\n child.unref();\n } catch {\n // Opening the browser is a convenience; ignore any failure.\n }\n}\n\n/**\n * Block until SIGINT / SIGTERM, then close the studio server and resolve.\n * Mirrors the long-running serve commands' graceful-shutdown contract.\n */\nfunction blockUntilShutdown(server: RunningStudioServer, cliName: string): Promise<void> {\n return new Promise<void>((resolveShutdown) => {\n let shuttingDown = false;\n const shutdown = (signal: string): void => {\n if (shuttingDown) return;\n shuttingDown = true;\n getLogger().info(`Received ${signal}; stopping ${cliName} studio...`);\n void server\n .close()\n .catch((err: unknown) => getLogger().warn(`Error stopping studio server: ${String(err)}`))\n .finally(() => resolveShutdown());\n };\n process.on('SIGINT', () => shutdown('SIGINT'));\n process.on('SIGTERM', () => shutdown('SIGTERM'));\n });\n}\n\nexport function createLocalStudioCommand(opts: CreateLocalStudioCommandOptions = {}): Command {\n setEmbedConfig(opts.embedConfig);\n const cmd = new Command('studio')\n .description(\n \"Open the local studio: a web console that lists the synthesized CDK app's runnable \" +\n 'targets and lets you invoke / serve them from the browser while watching all activity ' +\n 'in one timeline. The interactive counterpart to the headless invoke / start-* commands.'\n )\n .action(\n withErrorHandling(async (options: LocalStudioOptions) => {\n await localStudioCommand(options);\n })\n );\n\n addStudioSpecificOptions(cmd);\n [...commonOptions(), ...appOptions(), ...contextOptions].forEach((opt) => cmd.addOption(opt));\n cmd.addOption(regionOption);\n return cmd;\n}\n\n/**\n * Register the option block `cdkl studio` adds on top of the shared\n * common / app / context option helpers. Kept in a named helper (not\n * inline in {@link createLocalStudioCommand}) so a host CLI embedding\n * this factory inherits new studio flags without a duplicate\n * `.addOption(...)` block, matching every other `add<Cmd>SpecificOptions`\n * extraction. Chainable: returns `cmd`.\n */\nexport function addStudioSpecificOptions(cmd: Command): Command {\n cmd.addOption(\n new Option(\n '--studio-port <port>',\n 'Preferred port for the studio web server (bumps to the next free port on collision)'\n ).default(String(DEFAULT_STUDIO_PORT))\n );\n cmd.addOption(\n new Option('--no-open', 'Do not auto-open the browser when studio starts (TTY only)')\n );\n return cmd;\n}\n"],"mappings":"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"}