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.
- package/dist/cli.js +2 -2
- package/dist/internal.d.ts +26 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -2
- package/dist/{local-studio-DvNmnaF0.js → local-studio-8r3jr9nj.js} +282 -41
- package/dist/local-studio-8r3jr9nj.js.map +1 -0
- package/package.json +1 -1
- package/dist/local-studio-DvNmnaF0.js.map +0 -1
|
@@ -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;
|
|
190
|
-
// long-running serves
|
|
191
|
-
//
|
|
192
|
-
const
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
-
/**
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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.
|
|
1326
|
-
*
|
|
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 ??
|
|
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
|
-
|
|
1607
|
+
spec.command,
|
|
1371
1608
|
targetId,
|
|
1372
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
1453
|
-
if (m
|
|
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
|
-
|
|
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,
|
|
1752
|
-
//# sourceMappingURL=local-studio-
|
|
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
|