@renderify/cli 0.1.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/LICENSE +21 -0
- package/README.md +62 -0
- package/bin/renderify.js +2 -0
- package/dist/cli.cjs.js +1214 -0
- package/dist/cli.cjs.js.map +1 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.esm.js +1203 -0
- package/dist/cli.esm.js.map +1 -0
- package/package.json +66 -0
package/dist/cli.esm.js
ADDED
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import http from "http";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import {
|
|
7
|
+
createRenderifyApp,
|
|
8
|
+
DefaultApiIntegration,
|
|
9
|
+
DefaultCodeGenerator,
|
|
10
|
+
DefaultContextManager,
|
|
11
|
+
DefaultCustomizationEngine,
|
|
12
|
+
DefaultPerformanceOptimizer,
|
|
13
|
+
DefaultRenderifyConfig,
|
|
14
|
+
DefaultUIRenderer
|
|
15
|
+
} from "@renderify/core";
|
|
16
|
+
import {
|
|
17
|
+
collectComponentModules,
|
|
18
|
+
collectRuntimeSourceImports,
|
|
19
|
+
isRuntimePlan
|
|
20
|
+
} from "@renderify/ir";
|
|
21
|
+
import { createLLMInterpreter } from "@renderify/llm";
|
|
22
|
+
import { DefaultRuntimeManager, JspmModuleLoader } from "@renderify/runtime";
|
|
23
|
+
import { DefaultSecurityChecker } from "@renderify/security";
|
|
24
|
+
|
|
25
|
+
// src/playground-html.ts
|
|
26
|
+
var PLAYGROUND_HTML = `<!doctype html>
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<head>
|
|
29
|
+
<meta charset="utf-8" />
|
|
30
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
31
|
+
<title>Renderify Runtime Playground</title>
|
|
32
|
+
<style>
|
|
33
|
+
:root {
|
|
34
|
+
--bg-top: #e7f3ff;
|
|
35
|
+
--bg-bottom: #fff9f0;
|
|
36
|
+
--panel: rgba(255, 255, 255, 0.86);
|
|
37
|
+
--line: rgba(17, 24, 39, 0.12);
|
|
38
|
+
--ink: #0f172a;
|
|
39
|
+
--subtle: #475569;
|
|
40
|
+
--brand: #0f766e;
|
|
41
|
+
--brand-2: #0369a1;
|
|
42
|
+
--danger: #b91c1c;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
* {
|
|
46
|
+
box-sizing: border-box;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
body {
|
|
50
|
+
margin: 0;
|
|
51
|
+
color: var(--ink);
|
|
52
|
+
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
|
|
53
|
+
background: radial-gradient(circle at 10% 0%, var(--bg-top), var(--bg-bottom));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.shell {
|
|
57
|
+
min-height: 100vh;
|
|
58
|
+
padding: 20px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.title {
|
|
62
|
+
margin: 0 0 8px;
|
|
63
|
+
font-size: 28px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.sub {
|
|
67
|
+
margin: 0 0 16px;
|
|
68
|
+
color: var(--subtle);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.grid {
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
74
|
+
gap: 14px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.card {
|
|
78
|
+
background: var(--panel);
|
|
79
|
+
border: 1px solid var(--line);
|
|
80
|
+
border-radius: 14px;
|
|
81
|
+
padding: 14px;
|
|
82
|
+
backdrop-filter: blur(8px);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.span-4 {
|
|
86
|
+
grid-column: span 4;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.span-8 {
|
|
90
|
+
grid-column: span 8;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.span-12 {
|
|
94
|
+
grid-column: span 12;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
h2 {
|
|
98
|
+
margin: 0 0 10px;
|
|
99
|
+
font-size: 16px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
textarea {
|
|
103
|
+
width: 100%;
|
|
104
|
+
min-height: 118px;
|
|
105
|
+
border: 1px solid var(--line);
|
|
106
|
+
border-radius: 10px;
|
|
107
|
+
padding: 10px 11px;
|
|
108
|
+
font: inherit;
|
|
109
|
+
resize: vertical;
|
|
110
|
+
background: #fff;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.actions {
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-wrap: wrap;
|
|
116
|
+
gap: 8px;
|
|
117
|
+
margin-top: 10px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
button {
|
|
121
|
+
border: 1px solid transparent;
|
|
122
|
+
border-radius: 10px;
|
|
123
|
+
padding: 8px 12px;
|
|
124
|
+
font: inherit;
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
background: linear-gradient(160deg, var(--brand), var(--brand-2));
|
|
128
|
+
color: #fff;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
button.secondary {
|
|
132
|
+
background: #fff;
|
|
133
|
+
color: var(--ink);
|
|
134
|
+
border-color: var(--line);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
button.danger {
|
|
138
|
+
background: var(--danger);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
button:disabled {
|
|
142
|
+
opacity: 0.45;
|
|
143
|
+
cursor: not-allowed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.status {
|
|
147
|
+
min-height: 20px;
|
|
148
|
+
margin-top: 10px;
|
|
149
|
+
color: var(--subtle);
|
|
150
|
+
font-size: 13px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.render-output {
|
|
154
|
+
min-height: 130px;
|
|
155
|
+
border: 1px dashed rgba(15, 118, 110, 0.35);
|
|
156
|
+
border-radius: 10px;
|
|
157
|
+
padding: 10px;
|
|
158
|
+
background: rgba(255, 255, 255, 0.9);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pre {
|
|
162
|
+
margin: 0;
|
|
163
|
+
max-height: 360px;
|
|
164
|
+
overflow: auto;
|
|
165
|
+
padding: 10px;
|
|
166
|
+
font-size: 12px;
|
|
167
|
+
border-radius: 10px;
|
|
168
|
+
background: #0f172a;
|
|
169
|
+
color: #dbeafe;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@media (max-width: 980px) {
|
|
173
|
+
.span-4,
|
|
174
|
+
.span-8 {
|
|
175
|
+
grid-column: span 12;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
179
|
+
</head>
|
|
180
|
+
<body>
|
|
181
|
+
<div class="shell">
|
|
182
|
+
<h1 class="title">Renderify Playground</h1>
|
|
183
|
+
<p class="sub">Prompt -> RuntimePlan -> Runtime execution -> Browser render</p>
|
|
184
|
+
|
|
185
|
+
<div class="grid">
|
|
186
|
+
<section class="card span-4">
|
|
187
|
+
<h2>Prompt</h2>
|
|
188
|
+
<textarea id="prompt">Build an analytics dashboard with a chart and KPI cards</textarea>
|
|
189
|
+
<div class="actions">
|
|
190
|
+
<button id="run-prompt">Render Prompt</button>
|
|
191
|
+
<button id="stream-prompt" class="secondary">Stream Prompt</button>
|
|
192
|
+
<button id="clear" class="danger">Clear</button>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="status" id="status">Ready.</div>
|
|
195
|
+
</section>
|
|
196
|
+
|
|
197
|
+
<section class="card span-8">
|
|
198
|
+
<h2>Rendered HTML</h2>
|
|
199
|
+
<div class="render-output" id="html-output"></div>
|
|
200
|
+
</section>
|
|
201
|
+
|
|
202
|
+
<section class="card span-6">
|
|
203
|
+
<h2>Plan JSON</h2>
|
|
204
|
+
<textarea id="plan-editor">{}</textarea>
|
|
205
|
+
<div class="actions">
|
|
206
|
+
<button id="run-plan">Render Plan</button>
|
|
207
|
+
<button id="probe-plan" class="secondary">Probe Plan</button>
|
|
208
|
+
<button id="copy-plan-link" class="secondary">Copy Plan Link</button>
|
|
209
|
+
</div>
|
|
210
|
+
</section>
|
|
211
|
+
|
|
212
|
+
<section class="card span-6">
|
|
213
|
+
<h2>Diagnostics</h2>
|
|
214
|
+
<pre id="diagnostics">{}</pre>
|
|
215
|
+
</section>
|
|
216
|
+
|
|
217
|
+
<section class="card span-12">
|
|
218
|
+
<h2>Streaming Feed</h2>
|
|
219
|
+
<pre id="stream-output">[]</pre>
|
|
220
|
+
</section>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<script>
|
|
225
|
+
const byId = (id) => document.getElementById(id);
|
|
226
|
+
const statusEl = byId("status");
|
|
227
|
+
const promptEl = byId("prompt");
|
|
228
|
+
const htmlOutputEl = byId("html-output");
|
|
229
|
+
const planEditorEl = byId("plan-editor");
|
|
230
|
+
const diagnosticsEl = byId("diagnostics");
|
|
231
|
+
const streamOutputEl = byId("stream-output");
|
|
232
|
+
const copyPlanLinkEl = byId("copy-plan-link");
|
|
233
|
+
|
|
234
|
+
const controls = [
|
|
235
|
+
byId("run-prompt"),
|
|
236
|
+
byId("stream-prompt"),
|
|
237
|
+
byId("run-plan"),
|
|
238
|
+
byId("probe-plan"),
|
|
239
|
+
copyPlanLinkEl,
|
|
240
|
+
byId("clear"),
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const setBusy = (busy) => {
|
|
244
|
+
controls.forEach((button) => {
|
|
245
|
+
button.disabled = busy;
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const setStatus = (text) => {
|
|
250
|
+
statusEl.textContent = text;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const safeJson = (value) => {
|
|
254
|
+
try {
|
|
255
|
+
return JSON.stringify(value ?? {}, null, 2);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return String(error);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const HASH_SOURCE_LANGUAGE_KEYS = {
|
|
262
|
+
js64: "js",
|
|
263
|
+
jsx64: "jsx",
|
|
264
|
+
ts64: "ts",
|
|
265
|
+
tsx64: "tsx",
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const toBase64Bytes = (input) => {
|
|
269
|
+
const normalized = String(input ?? "")
|
|
270
|
+
.trim()
|
|
271
|
+
.replace(/[\\r\\n\\t ]+/g, "")
|
|
272
|
+
.replace(/-/g, "+")
|
|
273
|
+
.replace(/_/g, "/");
|
|
274
|
+
if (!normalized) {
|
|
275
|
+
throw new Error("Base64 payload is empty.");
|
|
276
|
+
}
|
|
277
|
+
const remainder = normalized.length % 4;
|
|
278
|
+
const padded =
|
|
279
|
+
remainder === 0 ? normalized : normalized + "=".repeat(4 - remainder);
|
|
280
|
+
return atob(padded);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const decodeBase64Text = (input) => {
|
|
284
|
+
const binary = toBase64Bytes(input);
|
|
285
|
+
const bytes = new Uint8Array(binary.length);
|
|
286
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
287
|
+
bytes[index] = binary.charCodeAt(index);
|
|
288
|
+
}
|
|
289
|
+
return new TextDecoder().decode(bytes);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const encodeBase64Url = (input) => {
|
|
293
|
+
const text = String(input ?? "");
|
|
294
|
+
const bytes = new TextEncoder().encode(text);
|
|
295
|
+
let binary = "";
|
|
296
|
+
for (const byte of bytes) {
|
|
297
|
+
binary += String.fromCharCode(byte);
|
|
298
|
+
}
|
|
299
|
+
return btoa(binary)
|
|
300
|
+
.replace(/\\+/g, "-")
|
|
301
|
+
.replace(/\\//g, "_")
|
|
302
|
+
.replace(/=+$/g, "");
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const getHashSearchParams = () => {
|
|
306
|
+
const rawHash = window.location.hash.startsWith("#")
|
|
307
|
+
? window.location.hash.slice(1)
|
|
308
|
+
: window.location.hash;
|
|
309
|
+
return new URLSearchParams(rawHash);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const parseJsonFromBase64 = (input, label) => {
|
|
313
|
+
try {
|
|
314
|
+
const decoded = decodeBase64Text(input);
|
|
315
|
+
return JSON.parse(decoded);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
318
|
+
throw new Error("Failed to decode " + label + ": " + message);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const resolveSourceHashPayload = (params) => {
|
|
323
|
+
const sourceEntry = Object.entries(HASH_SOURCE_LANGUAGE_KEYS).find(
|
|
324
|
+
([key]) => params.has(key),
|
|
325
|
+
);
|
|
326
|
+
if (!sourceEntry && !params.has("source64")) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const explicitLanguage = String(params.get("language") || "")
|
|
331
|
+
.trim()
|
|
332
|
+
.toLowerCase();
|
|
333
|
+
const language =
|
|
334
|
+
(sourceEntry ? sourceEntry[1] : undefined) ||
|
|
335
|
+
(explicitLanguage === "js" ||
|
|
336
|
+
explicitLanguage === "jsx" ||
|
|
337
|
+
explicitLanguage === "ts" ||
|
|
338
|
+
explicitLanguage === "tsx"
|
|
339
|
+
? explicitLanguage
|
|
340
|
+
: "jsx");
|
|
341
|
+
const sourceRaw = sourceEntry ? params.get(sourceEntry[0]) : params.get("source64");
|
|
342
|
+
if (!sourceRaw) {
|
|
343
|
+
throw new Error("Source hash payload is empty.");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const code = decodeBase64Text(sourceRaw);
|
|
347
|
+
const runtimeRaw = String(params.get("runtime") || "").trim().toLowerCase();
|
|
348
|
+
const runtime =
|
|
349
|
+
runtimeRaw === "renderify" || runtimeRaw === "preact"
|
|
350
|
+
? runtimeRaw
|
|
351
|
+
: language === "jsx" || language === "tsx"
|
|
352
|
+
? "preact"
|
|
353
|
+
: "renderify";
|
|
354
|
+
const exportName = String(params.get("exportName") || "default").trim() || "default";
|
|
355
|
+
const manifestPayload = params.get("manifest64");
|
|
356
|
+
const moduleManifest = manifestPayload
|
|
357
|
+
? parseJsonFromBase64(manifestPayload, "manifest64")
|
|
358
|
+
: undefined;
|
|
359
|
+
const planId =
|
|
360
|
+
String(params.get("id") || "").trim() ||
|
|
361
|
+
"hash_source_" + Date.now().toString(36);
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
specVersion: "runtime-plan/v1",
|
|
365
|
+
id: planId,
|
|
366
|
+
version: 1,
|
|
367
|
+
root: {
|
|
368
|
+
type: "element",
|
|
369
|
+
tag: "div",
|
|
370
|
+
children: [{ type: "text", value: "Renderify source root" }],
|
|
371
|
+
},
|
|
372
|
+
capabilities: {},
|
|
373
|
+
...(moduleManifest &&
|
|
374
|
+
typeof moduleManifest === "object" &&
|
|
375
|
+
!Array.isArray(moduleManifest)
|
|
376
|
+
? { moduleManifest }
|
|
377
|
+
: {}),
|
|
378
|
+
source: {
|
|
379
|
+
code,
|
|
380
|
+
language,
|
|
381
|
+
exportName,
|
|
382
|
+
runtime,
|
|
383
|
+
},
|
|
384
|
+
metadata: {
|
|
385
|
+
tags: ["hash-deeplink", "source"],
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
async function request(path, method, body) {
|
|
391
|
+
const response = await fetch(path, {
|
|
392
|
+
method,
|
|
393
|
+
headers: { "content-type": "application/json" },
|
|
394
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const payload = await response.json();
|
|
398
|
+
if (!response.ok) {
|
|
399
|
+
throw new Error(payload && payload.error ? String(payload.error) : "request failed");
|
|
400
|
+
}
|
|
401
|
+
return payload;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const applyRenderPayload = (payload) => {
|
|
405
|
+
htmlOutputEl.innerHTML = String(payload.html ?? "");
|
|
406
|
+
planEditorEl.value = safeJson(payload.planDetail ?? {});
|
|
407
|
+
diagnosticsEl.textContent = safeJson({
|
|
408
|
+
traceId: payload.traceId,
|
|
409
|
+
state: payload.state ?? {},
|
|
410
|
+
diagnostics: payload.diagnostics ?? [],
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
async function renderPlanObject(plan, statusText) {
|
|
415
|
+
setBusy(true);
|
|
416
|
+
setStatus(statusText || "Rendering plan...");
|
|
417
|
+
try {
|
|
418
|
+
const payload = await request("/api/plan", "POST", { plan });
|
|
419
|
+
applyRenderPayload(payload);
|
|
420
|
+
setStatus("Plan rendered.");
|
|
421
|
+
return payload;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
setStatus("Plan render failed.");
|
|
424
|
+
diagnosticsEl.textContent = String(error);
|
|
425
|
+
throw error;
|
|
426
|
+
} finally {
|
|
427
|
+
setBusy(false);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function runPrompt() {
|
|
432
|
+
const prompt = promptEl.value.trim();
|
|
433
|
+
if (!prompt) {
|
|
434
|
+
setStatus("Prompt is required.");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
setBusy(true);
|
|
439
|
+
setStatus("Rendering prompt...");
|
|
440
|
+
try {
|
|
441
|
+
const payload = await request("/api/prompt", "POST", { prompt });
|
|
442
|
+
applyRenderPayload(payload);
|
|
443
|
+
streamOutputEl.textContent = "[]";
|
|
444
|
+
setStatus("Prompt rendered.");
|
|
445
|
+
} catch (error) {
|
|
446
|
+
setStatus("Prompt render failed.");
|
|
447
|
+
diagnosticsEl.textContent = String(error);
|
|
448
|
+
} finally {
|
|
449
|
+
setBusy(false);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function streamPrompt() {
|
|
454
|
+
const prompt = promptEl.value.trim();
|
|
455
|
+
if (!prompt) {
|
|
456
|
+
setStatus("Prompt is required.");
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
setBusy(true);
|
|
461
|
+
setStatus("Streaming prompt...");
|
|
462
|
+
const streamEvents = [];
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const response = await fetch("/api/prompt-stream", {
|
|
466
|
+
method: "POST",
|
|
467
|
+
headers: { "content-type": "application/json" },
|
|
468
|
+
body: JSON.stringify({ prompt }),
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
if (!response.ok || !response.body) {
|
|
472
|
+
throw new Error("stream request failed");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const reader = response.body.getReader();
|
|
476
|
+
const decoder = new TextDecoder();
|
|
477
|
+
let buffer = "";
|
|
478
|
+
|
|
479
|
+
while (true) {
|
|
480
|
+
const { done, value } = await reader.read();
|
|
481
|
+
if (done) {
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
buffer += decoder.decode(value, { stream: true });
|
|
486
|
+
const lines = buffer.split("\\n");
|
|
487
|
+
buffer = lines.pop() ?? "";
|
|
488
|
+
|
|
489
|
+
for (const line of lines) {
|
|
490
|
+
if (!line.trim()) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const event = JSON.parse(line);
|
|
494
|
+
streamEvents.push({
|
|
495
|
+
type: event.type,
|
|
496
|
+
planId: event.planId ?? null,
|
|
497
|
+
llmTextLength: String(event.llmText ?? "").length,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
if (event.html) {
|
|
501
|
+
htmlOutputEl.innerHTML = String(event.html);
|
|
502
|
+
}
|
|
503
|
+
if (event.type === "final" && event.final) {
|
|
504
|
+
applyRenderPayload(event.final);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
streamOutputEl.textContent = safeJson(streamEvents);
|
|
510
|
+
setStatus("Stream completed.");
|
|
511
|
+
} catch (error) {
|
|
512
|
+
setStatus("Stream failed.");
|
|
513
|
+
diagnosticsEl.textContent = String(error);
|
|
514
|
+
} finally {
|
|
515
|
+
setBusy(false);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function runPlan() {
|
|
520
|
+
const raw = planEditorEl.value.trim();
|
|
521
|
+
if (!raw) {
|
|
522
|
+
setStatus("Plan JSON is required.");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const plan = JSON.parse(raw);
|
|
528
|
+
await renderPlanObject(plan, "Rendering plan...");
|
|
529
|
+
} catch (error) {
|
|
530
|
+
setStatus("Plan render failed.");
|
|
531
|
+
diagnosticsEl.textContent = String(error);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function probePlan() {
|
|
536
|
+
const raw = planEditorEl.value.trim();
|
|
537
|
+
if (!raw) {
|
|
538
|
+
setStatus("Plan JSON is required.");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
setBusy(true);
|
|
543
|
+
setStatus("Probing plan...");
|
|
544
|
+
try {
|
|
545
|
+
const plan = JSON.parse(raw);
|
|
546
|
+
const payload = await request("/api/probe-plan", "POST", { plan });
|
|
547
|
+
diagnosticsEl.textContent = safeJson(payload);
|
|
548
|
+
setStatus("Plan probe completed.");
|
|
549
|
+
} catch (error) {
|
|
550
|
+
setStatus("Plan probe failed.");
|
|
551
|
+
diagnosticsEl.textContent = String(error);
|
|
552
|
+
} finally {
|
|
553
|
+
setBusy(false);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function clearAll() {
|
|
558
|
+
htmlOutputEl.innerHTML = "";
|
|
559
|
+
diagnosticsEl.textContent = "{}";
|
|
560
|
+
streamOutputEl.textContent = "[]";
|
|
561
|
+
setStatus("Cleared.");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function copyPlanLink() {
|
|
565
|
+
const raw = planEditorEl.value.trim();
|
|
566
|
+
if (!raw) {
|
|
567
|
+
setStatus("Plan JSON is required.");
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
const parsed = JSON.parse(raw);
|
|
573
|
+
const encoded = encodeBase64Url(JSON.stringify(parsed));
|
|
574
|
+
const shareUrl =
|
|
575
|
+
window.location.origin +
|
|
576
|
+
window.location.pathname +
|
|
577
|
+
"#plan64=" +
|
|
578
|
+
encoded;
|
|
579
|
+
|
|
580
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
581
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
582
|
+
setStatus("Plan share link copied.");
|
|
583
|
+
diagnosticsEl.textContent = safeJson({
|
|
584
|
+
shareUrl,
|
|
585
|
+
});
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
diagnosticsEl.textContent = shareUrl;
|
|
590
|
+
setStatus("Clipboard unavailable; share URL written to diagnostics.");
|
|
591
|
+
} catch (error) {
|
|
592
|
+
setStatus("Failed to create share link.");
|
|
593
|
+
diagnosticsEl.textContent = String(error);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function renderFromHashPayload() {
|
|
598
|
+
const params = getHashSearchParams();
|
|
599
|
+
if (Array.from(params.keys()).length === 0) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const plan64 = params.get("plan64");
|
|
605
|
+
const plan = plan64
|
|
606
|
+
? parseJsonFromBase64(plan64, "plan64")
|
|
607
|
+
: resolveSourceHashPayload(params);
|
|
608
|
+
|
|
609
|
+
if (!plan) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
planEditorEl.value = safeJson(plan);
|
|
614
|
+
await renderPlanObject(plan, "Rendering hash payload...");
|
|
615
|
+
setStatus("Hash payload rendered.");
|
|
616
|
+
} catch (error) {
|
|
617
|
+
setStatus("Hash payload render failed.");
|
|
618
|
+
diagnosticsEl.textContent = String(error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
byId("run-prompt").addEventListener("click", runPrompt);
|
|
623
|
+
byId("stream-prompt").addEventListener("click", streamPrompt);
|
|
624
|
+
byId("run-plan").addEventListener("click", runPlan);
|
|
625
|
+
byId("probe-plan").addEventListener("click", probePlan);
|
|
626
|
+
copyPlanLinkEl.addEventListener("click", () => {
|
|
627
|
+
void copyPlanLink();
|
|
628
|
+
});
|
|
629
|
+
byId("clear").addEventListener("click", clearAll);
|
|
630
|
+
|
|
631
|
+
void renderFromHashPayload();
|
|
632
|
+
window.addEventListener("hashchange", () => {
|
|
633
|
+
void renderFromHashPayload();
|
|
634
|
+
});
|
|
635
|
+
</script>
|
|
636
|
+
</body>
|
|
637
|
+
</html>`;
|
|
638
|
+
|
|
639
|
+
// src/index.ts
|
|
640
|
+
var DEFAULT_PROMPT = "Hello Renderify runtime";
|
|
641
|
+
var DEFAULT_PORT = 4317;
|
|
642
|
+
var JSON_BODY_LIMIT_BYTES = 1e6;
|
|
643
|
+
var AUTO_MANIFEST_INTEGRITY_TIMEOUT_MS = 8e3;
|
|
644
|
+
var REMOTE_MODULE_INTEGRITY_CACHE = /* @__PURE__ */ new Map();
|
|
645
|
+
var { readFile } = fs.promises;
|
|
646
|
+
function createLLM(config) {
|
|
647
|
+
const provider = config.get("llmProvider") ?? "openai";
|
|
648
|
+
const providerOptions = {
|
|
649
|
+
apiKey: config.get("llmApiKey"),
|
|
650
|
+
timeoutMs: config.get("llmRequestTimeoutMs")
|
|
651
|
+
};
|
|
652
|
+
if (provider === "openai" || provider === "google" || typeof process.env.RENDERIFY_LLM_MODEL === "string") {
|
|
653
|
+
providerOptions.model = config.get("llmModel");
|
|
654
|
+
}
|
|
655
|
+
if (provider === "openai" || provider === "google" || typeof process.env.RENDERIFY_LLM_BASE_URL === "string") {
|
|
656
|
+
providerOptions.baseUrl = config.get("llmBaseUrl");
|
|
657
|
+
}
|
|
658
|
+
return createLLMInterpreter({
|
|
659
|
+
provider,
|
|
660
|
+
providerOptions
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
async function main() {
|
|
664
|
+
const args = parseArgs(process.argv.slice(2));
|
|
665
|
+
if (args.command === "help") {
|
|
666
|
+
printHelp();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const config = new DefaultRenderifyConfig();
|
|
670
|
+
await config.load();
|
|
671
|
+
const llm = createLLM(config);
|
|
672
|
+
const runtimeModuleLoader = new JspmModuleLoader({
|
|
673
|
+
cdnBaseUrl: config.get("jspmCdnUrl")
|
|
674
|
+
});
|
|
675
|
+
const runtime = new DefaultRuntimeManager({
|
|
676
|
+
moduleLoader: runtimeModuleLoader,
|
|
677
|
+
enforceModuleManifest: config.get("runtimeEnforceModuleManifest") !== false,
|
|
678
|
+
allowIsolationFallback: config.get("runtimeAllowIsolationFallback") === true,
|
|
679
|
+
supportedPlanSpecVersions: config.get(
|
|
680
|
+
"runtimeSupportedSpecVersions"
|
|
681
|
+
),
|
|
682
|
+
enableDependencyPreflight: config.get("runtimeEnableDependencyPreflight") !== false,
|
|
683
|
+
failOnDependencyPreflightError: config.get("runtimeFailOnDependencyPreflightError") === true,
|
|
684
|
+
remoteFetchTimeoutMs: config.get("runtimeRemoteFetchTimeoutMs") ?? 12e3,
|
|
685
|
+
remoteFetchRetries: config.get("runtimeRemoteFetchRetries") ?? 2,
|
|
686
|
+
remoteFetchBackoffMs: config.get("runtimeRemoteFetchBackoffMs") ?? 150,
|
|
687
|
+
remoteFallbackCdnBases: config.get(
|
|
688
|
+
"runtimeRemoteFallbackCdnBases"
|
|
689
|
+
) ?? ["https://esm.sh"],
|
|
690
|
+
browserSourceSandboxMode: config.get(
|
|
691
|
+
"runtimeBrowserSourceSandboxMode"
|
|
692
|
+
),
|
|
693
|
+
browserSourceSandboxTimeoutMs: config.get("runtimeBrowserSourceSandboxTimeoutMs") ?? 4e3,
|
|
694
|
+
browserSourceSandboxFailClosed: config.get("runtimeBrowserSourceSandboxFailClosed") !== false
|
|
695
|
+
});
|
|
696
|
+
const renderifyApp = createRenderifyApp({
|
|
697
|
+
config,
|
|
698
|
+
context: new DefaultContextManager(),
|
|
699
|
+
llm,
|
|
700
|
+
codegen: new DefaultCodeGenerator(),
|
|
701
|
+
runtime,
|
|
702
|
+
security: new DefaultSecurityChecker(),
|
|
703
|
+
performance: new DefaultPerformanceOptimizer(),
|
|
704
|
+
ui: new DefaultUIRenderer(),
|
|
705
|
+
apiIntegration: new DefaultApiIntegration(),
|
|
706
|
+
customization: new DefaultCustomizationEngine()
|
|
707
|
+
});
|
|
708
|
+
await renderifyApp.start();
|
|
709
|
+
try {
|
|
710
|
+
switch (args.command) {
|
|
711
|
+
case "run": {
|
|
712
|
+
const result = await renderifyApp.renderPrompt(
|
|
713
|
+
args.prompt ?? DEFAULT_PROMPT
|
|
714
|
+
);
|
|
715
|
+
console.log(result.html);
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
case "plan": {
|
|
719
|
+
const result = await renderifyApp.renderPrompt(
|
|
720
|
+
args.prompt ?? DEFAULT_PROMPT
|
|
721
|
+
);
|
|
722
|
+
console.log(JSON.stringify(result.plan, null, 2));
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case "probe-plan": {
|
|
726
|
+
if (!args.planFile) {
|
|
727
|
+
throw new Error("probe-plan requires a JSON file path");
|
|
728
|
+
}
|
|
729
|
+
const plan = await loadPlanFile(args.planFile);
|
|
730
|
+
const security = await renderifyApp.getSecurityChecker().checkPlan(plan);
|
|
731
|
+
const runtimeProbe = await runtime.probePlan(plan);
|
|
732
|
+
const runtimeErrorDiagnostics = runtimeProbe.diagnostics.filter(
|
|
733
|
+
(item) => item.level === "error"
|
|
734
|
+
);
|
|
735
|
+
const preflightDiagnostics = runtimeProbe.diagnostics.filter(
|
|
736
|
+
(item) => item.code.startsWith("RUNTIME_PREFLIGHT_")
|
|
737
|
+
);
|
|
738
|
+
const report = {
|
|
739
|
+
planId: plan.id,
|
|
740
|
+
safe: security.safe,
|
|
741
|
+
securityIssueCount: security.issues.length,
|
|
742
|
+
runtimeErrorCount: runtimeErrorDiagnostics.length,
|
|
743
|
+
preflightIssueCount: preflightDiagnostics.length,
|
|
744
|
+
ok: security.safe && security.issues.length === 0 && runtimeErrorDiagnostics.length === 0,
|
|
745
|
+
securityIssues: security.issues,
|
|
746
|
+
securityDiagnostics: security.diagnostics,
|
|
747
|
+
dependencyStatuses: runtimeProbe.dependencies,
|
|
748
|
+
runtimeDiagnostics: runtimeProbe.diagnostics
|
|
749
|
+
};
|
|
750
|
+
console.log(JSON.stringify(report, null, 2));
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
case "render-plan": {
|
|
754
|
+
if (!args.planFile) {
|
|
755
|
+
throw new Error("render-plan requires a JSON file path");
|
|
756
|
+
}
|
|
757
|
+
const plan = await loadPlanFile(args.planFile);
|
|
758
|
+
const result = await renderifyApp.renderPlan(plan, {
|
|
759
|
+
prompt: `render-plan:${path.basename(args.planFile)}`
|
|
760
|
+
});
|
|
761
|
+
console.log(result.html);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case "playground": {
|
|
765
|
+
const port = args.port ?? resolvePlaygroundPort();
|
|
766
|
+
await runPlaygroundServer({
|
|
767
|
+
app: renderifyApp,
|
|
768
|
+
port,
|
|
769
|
+
moduleLoader: runtimeModuleLoader,
|
|
770
|
+
autoManifestIntegrityTimeoutMs: AUTO_MANIFEST_INTEGRITY_TIMEOUT_MS
|
|
771
|
+
});
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
} finally {
|
|
776
|
+
await renderifyApp.stop();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function parseArgs(argv) {
|
|
780
|
+
const [rawCommand, ...rest] = argv;
|
|
781
|
+
switch (rawCommand) {
|
|
782
|
+
case void 0:
|
|
783
|
+
return { command: "run", prompt: DEFAULT_PROMPT };
|
|
784
|
+
case "run":
|
|
785
|
+
return {
|
|
786
|
+
command: "run",
|
|
787
|
+
prompt: rest.join(" ").trim() || DEFAULT_PROMPT
|
|
788
|
+
};
|
|
789
|
+
case "plan":
|
|
790
|
+
return {
|
|
791
|
+
command: "plan",
|
|
792
|
+
prompt: rest.join(" ").trim() || DEFAULT_PROMPT
|
|
793
|
+
};
|
|
794
|
+
case "probe-plan":
|
|
795
|
+
return { command: "probe-plan", planFile: rest[0] };
|
|
796
|
+
case "render-plan":
|
|
797
|
+
return { command: "render-plan", planFile: rest[0] };
|
|
798
|
+
case "playground":
|
|
799
|
+
return {
|
|
800
|
+
command: "playground",
|
|
801
|
+
port: parsePort(rest[0])
|
|
802
|
+
};
|
|
803
|
+
case "help":
|
|
804
|
+
return { command: "help" };
|
|
805
|
+
default:
|
|
806
|
+
return {
|
|
807
|
+
command: "run",
|
|
808
|
+
prompt: [rawCommand, ...rest].join(" ").trim() || DEFAULT_PROMPT
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function parsePort(rawValue) {
|
|
813
|
+
if (!rawValue) {
|
|
814
|
+
return void 0;
|
|
815
|
+
}
|
|
816
|
+
const parsed = Number(rawValue);
|
|
817
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
818
|
+
throw new Error(`Invalid port: ${rawValue}`);
|
|
819
|
+
}
|
|
820
|
+
return parsed;
|
|
821
|
+
}
|
|
822
|
+
function resolvePlaygroundPort() {
|
|
823
|
+
const candidates = [process.env.RENDERIFY_PLAYGROUND_PORT, process.env.PORT];
|
|
824
|
+
for (const candidate of candidates) {
|
|
825
|
+
const port = parsePortFromEnv(candidate);
|
|
826
|
+
if (port !== void 0) {
|
|
827
|
+
return port;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return DEFAULT_PORT;
|
|
831
|
+
}
|
|
832
|
+
function parsePortFromEnv(rawValue) {
|
|
833
|
+
if (!rawValue) {
|
|
834
|
+
return void 0;
|
|
835
|
+
}
|
|
836
|
+
const parsed = Number(rawValue);
|
|
837
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
return parsed;
|
|
841
|
+
}
|
|
842
|
+
function printHelp() {
|
|
843
|
+
console.log(`Usage:
|
|
844
|
+
renderify run <prompt> Render prompt and print HTML
|
|
845
|
+
renderify plan <prompt> Print runtime plan JSON
|
|
846
|
+
renderify probe-plan <file> Probe RuntimePlan dependencies and policy compatibility
|
|
847
|
+
renderify render-plan <file> Execute RuntimePlan JSON file
|
|
848
|
+
renderify playground [port] Start browser runtime playground`);
|
|
849
|
+
}
|
|
850
|
+
async function loadPlanFile(filePath) {
|
|
851
|
+
const absolute = path.resolve(filePath);
|
|
852
|
+
const content = await readFile(absolute, "utf8");
|
|
853
|
+
const parsed = JSON.parse(content);
|
|
854
|
+
if (!isRuntimePlan(parsed)) {
|
|
855
|
+
throw new Error(`Invalid RuntimePlan JSON in file: ${absolute}`);
|
|
856
|
+
}
|
|
857
|
+
return parsed;
|
|
858
|
+
}
|
|
859
|
+
async function runPlaygroundServer(options) {
|
|
860
|
+
const { app, port, moduleLoader, autoManifestIntegrityTimeoutMs } = options;
|
|
861
|
+
const server = http.createServer((req, res) => {
|
|
862
|
+
void handlePlaygroundRequest(
|
|
863
|
+
req,
|
|
864
|
+
res,
|
|
865
|
+
app,
|
|
866
|
+
moduleLoader,
|
|
867
|
+
autoManifestIntegrityTimeoutMs
|
|
868
|
+
);
|
|
869
|
+
});
|
|
870
|
+
await new Promise((resolve, reject) => {
|
|
871
|
+
server.once("error", reject);
|
|
872
|
+
server.listen(port, () => {
|
|
873
|
+
server.off("error", reject);
|
|
874
|
+
resolve();
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
const info = server.address();
|
|
878
|
+
const resolvedPort = typeof info === "object" && info !== null ? info.port : port;
|
|
879
|
+
const baseUrl = `http://127.0.0.1:${resolvedPort}`;
|
|
880
|
+
console.log(`Renderify playground is running at ${baseUrl}`);
|
|
881
|
+
console.log("Press Ctrl+C to stop.");
|
|
882
|
+
await new Promise((resolve, reject) => {
|
|
883
|
+
let closed = false;
|
|
884
|
+
const finalize = (error) => {
|
|
885
|
+
if (closed) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
closed = true;
|
|
889
|
+
process.off("SIGINT", onSignal);
|
|
890
|
+
process.off("SIGTERM", onSignal);
|
|
891
|
+
if (error) {
|
|
892
|
+
reject(error);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
resolve();
|
|
896
|
+
};
|
|
897
|
+
const onSignal = () => {
|
|
898
|
+
server.close((error) => {
|
|
899
|
+
if (error) {
|
|
900
|
+
finalize(error);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
finalize();
|
|
904
|
+
});
|
|
905
|
+
};
|
|
906
|
+
process.on("SIGINT", onSignal);
|
|
907
|
+
process.on("SIGTERM", onSignal);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
async function handlePlaygroundRequest(req, res, app, moduleLoader, autoManifestIntegrityTimeoutMs) {
|
|
911
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
912
|
+
const parsedUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
913
|
+
const pathname = parsedUrl.pathname;
|
|
914
|
+
try {
|
|
915
|
+
if (method === "GET" && pathname === "/") {
|
|
916
|
+
sendHtml(res, PLAYGROUND_HTML);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (method === "GET" && pathname === "/api/health") {
|
|
920
|
+
sendJson(res, 200, { ok: true, status: "ready" });
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (method === "POST" && pathname === "/api/prompt") {
|
|
924
|
+
const body = await readJsonBody(req);
|
|
925
|
+
const prompt = typeof body.prompt === "string" && body.prompt.trim().length > 0 ? body.prompt.trim() : DEFAULT_PROMPT;
|
|
926
|
+
const result = await app.renderPrompt(prompt);
|
|
927
|
+
sendJson(res, 200, serializeRenderResult(result));
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (method === "POST" && pathname === "/api/prompt-stream") {
|
|
931
|
+
const body = await readJsonBody(req);
|
|
932
|
+
const prompt = typeof body.prompt === "string" && body.prompt.trim().length > 0 ? body.prompt.trim() : DEFAULT_PROMPT;
|
|
933
|
+
await sendPromptStream(res, app, prompt);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (method === "POST" && pathname === "/api/plan") {
|
|
937
|
+
const body = await readJsonBody(req);
|
|
938
|
+
const plan = body.plan;
|
|
939
|
+
if (!isRuntimePlan(plan)) {
|
|
940
|
+
sendJson(res, 400, { error: "body.plan must be a RuntimePlan object" });
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const hydratedPlan = await hydratePlaygroundPlanManifest(plan, {
|
|
944
|
+
moduleLoader,
|
|
945
|
+
requireIntegrity: app.getSecurityChecker().getPolicy().requireModuleIntegrity,
|
|
946
|
+
integrityTimeoutMs: autoManifestIntegrityTimeoutMs
|
|
947
|
+
});
|
|
948
|
+
const result = await app.renderPlan(hydratedPlan, {
|
|
949
|
+
prompt: "playground:plan"
|
|
950
|
+
});
|
|
951
|
+
sendJson(res, 200, serializeRenderResult(result));
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (method === "POST" && pathname === "/api/probe-plan") {
|
|
955
|
+
const body = await readJsonBody(req);
|
|
956
|
+
const plan = body.plan;
|
|
957
|
+
if (!isRuntimePlan(plan)) {
|
|
958
|
+
sendJson(res, 400, { error: "body.plan must be a RuntimePlan object" });
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const hydratedPlan = await hydratePlaygroundPlanManifest(plan, {
|
|
962
|
+
moduleLoader,
|
|
963
|
+
requireIntegrity: app.getSecurityChecker().getPolicy().requireModuleIntegrity,
|
|
964
|
+
integrityTimeoutMs: autoManifestIntegrityTimeoutMs
|
|
965
|
+
});
|
|
966
|
+
const security = await app.getSecurityChecker().checkPlan(hydratedPlan);
|
|
967
|
+
const runtimeProbe = await app.getRuntimeManager().probePlan(hydratedPlan);
|
|
968
|
+
sendJson(res, 200, {
|
|
969
|
+
safe: security.safe,
|
|
970
|
+
securityIssues: security.issues,
|
|
971
|
+
securityDiagnostics: security.diagnostics,
|
|
972
|
+
dependencies: runtimeProbe.dependencies,
|
|
973
|
+
runtimeDiagnostics: runtimeProbe.diagnostics
|
|
974
|
+
});
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
sendJson(res, 404, { error: "Not found" });
|
|
978
|
+
} catch (error) {
|
|
979
|
+
sendJson(res, 500, {
|
|
980
|
+
error: error instanceof Error ? error.message : String(error)
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function serializeRenderResult(result) {
|
|
985
|
+
return {
|
|
986
|
+
traceId: result.traceId,
|
|
987
|
+
html: result.html,
|
|
988
|
+
plan: {
|
|
989
|
+
id: result.plan.id,
|
|
990
|
+
version: result.plan.version
|
|
991
|
+
},
|
|
992
|
+
planDetail: result.plan,
|
|
993
|
+
diagnostics: result.execution.diagnostics,
|
|
994
|
+
state: result.execution.state ?? {}
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
async function readJsonBody(req) {
|
|
998
|
+
const chunks = [];
|
|
999
|
+
let totalBytes = 0;
|
|
1000
|
+
for await (const chunk of req) {
|
|
1001
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1002
|
+
totalBytes += buffer.length;
|
|
1003
|
+
if (totalBytes > JSON_BODY_LIMIT_BYTES) {
|
|
1004
|
+
throw new Error(`JSON body exceeds ${JSON_BODY_LIMIT_BYTES} bytes`);
|
|
1005
|
+
}
|
|
1006
|
+
chunks.push(buffer);
|
|
1007
|
+
}
|
|
1008
|
+
if (chunks.length === 0) {
|
|
1009
|
+
return {};
|
|
1010
|
+
}
|
|
1011
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
1012
|
+
if (raw.length === 0) {
|
|
1013
|
+
return {};
|
|
1014
|
+
}
|
|
1015
|
+
const parsed = JSON.parse(raw);
|
|
1016
|
+
if (!isRecord(parsed)) {
|
|
1017
|
+
throw new Error("JSON body must be an object");
|
|
1018
|
+
}
|
|
1019
|
+
return parsed;
|
|
1020
|
+
}
|
|
1021
|
+
function sendJson(res, statusCode, payload) {
|
|
1022
|
+
const body = JSON.stringify(payload);
|
|
1023
|
+
res.writeHead(statusCode, {
|
|
1024
|
+
"content-type": "application/json; charset=utf-8",
|
|
1025
|
+
"content-length": Buffer.byteLength(body),
|
|
1026
|
+
"cache-control": "no-store"
|
|
1027
|
+
});
|
|
1028
|
+
res.end(body);
|
|
1029
|
+
}
|
|
1030
|
+
function sendHtml(res, html) {
|
|
1031
|
+
res.writeHead(200, {
|
|
1032
|
+
"content-type": "text/html; charset=utf-8",
|
|
1033
|
+
"content-length": Buffer.byteLength(html),
|
|
1034
|
+
"cache-control": "no-store"
|
|
1035
|
+
});
|
|
1036
|
+
res.end(html);
|
|
1037
|
+
}
|
|
1038
|
+
async function sendPromptStream(res, app, prompt) {
|
|
1039
|
+
res.writeHead(200, {
|
|
1040
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
1041
|
+
"cache-control": "no-store",
|
|
1042
|
+
connection: "keep-alive",
|
|
1043
|
+
"x-accel-buffering": "no"
|
|
1044
|
+
});
|
|
1045
|
+
try {
|
|
1046
|
+
for await (const chunk of app.renderPromptStream(prompt)) {
|
|
1047
|
+
const serialized = serializePromptStreamChunk(chunk);
|
|
1048
|
+
res.write(`${JSON.stringify(serialized)}
|
|
1049
|
+
`);
|
|
1050
|
+
}
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
res.write(
|
|
1053
|
+
`${JSON.stringify({
|
|
1054
|
+
type: "error",
|
|
1055
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1056
|
+
})}
|
|
1057
|
+
`
|
|
1058
|
+
);
|
|
1059
|
+
} finally {
|
|
1060
|
+
res.end();
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function serializePromptStreamChunk(chunk) {
|
|
1064
|
+
if (chunk.type === "final") {
|
|
1065
|
+
return {
|
|
1066
|
+
type: chunk.type,
|
|
1067
|
+
traceId: chunk.traceId,
|
|
1068
|
+
prompt: chunk.prompt,
|
|
1069
|
+
llmText: chunk.llmText,
|
|
1070
|
+
final: chunk.final ? serializeRenderResult(chunk.final) : void 0,
|
|
1071
|
+
html: chunk.html,
|
|
1072
|
+
diagnostics: chunk.diagnostics ?? [],
|
|
1073
|
+
planId: chunk.planId
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
type: chunk.type,
|
|
1078
|
+
traceId: chunk.traceId,
|
|
1079
|
+
prompt: chunk.prompt,
|
|
1080
|
+
llmText: chunk.llmText,
|
|
1081
|
+
delta: chunk.delta,
|
|
1082
|
+
html: chunk.html,
|
|
1083
|
+
diagnostics: chunk.diagnostics ?? [],
|
|
1084
|
+
planId: chunk.planId
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
async function hydratePlaygroundPlanManifest(plan, options) {
|
|
1088
|
+
const bareSpecifiers = await collectRuntimePlanBareSpecifiers(plan);
|
|
1089
|
+
if (bareSpecifiers.length === 0) {
|
|
1090
|
+
return plan;
|
|
1091
|
+
}
|
|
1092
|
+
const nextManifest = { ...plan.moduleManifest ?? {} };
|
|
1093
|
+
let changed = false;
|
|
1094
|
+
for (const specifier of bareSpecifiers) {
|
|
1095
|
+
if (nextManifest[specifier]) {
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
let resolvedUrl;
|
|
1099
|
+
try {
|
|
1100
|
+
resolvedUrl = options.moduleLoader.resolveSpecifier(specifier);
|
|
1101
|
+
} catch {
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const descriptor = {
|
|
1105
|
+
resolvedUrl
|
|
1106
|
+
};
|
|
1107
|
+
if (options.requireIntegrity && isHttpUrl(resolvedUrl)) {
|
|
1108
|
+
const integrity = await fetchRemoteModuleIntegrity(
|
|
1109
|
+
resolvedUrl,
|
|
1110
|
+
options.integrityTimeoutMs
|
|
1111
|
+
);
|
|
1112
|
+
if (integrity) {
|
|
1113
|
+
descriptor.integrity = integrity;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
nextManifest[specifier] = descriptor;
|
|
1117
|
+
changed = true;
|
|
1118
|
+
}
|
|
1119
|
+
if (!changed) {
|
|
1120
|
+
return plan;
|
|
1121
|
+
}
|
|
1122
|
+
return {
|
|
1123
|
+
...plan,
|
|
1124
|
+
moduleManifest: nextManifest
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
async function collectRuntimePlanBareSpecifiers(plan) {
|
|
1128
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
1129
|
+
for (const specifier of plan.imports ?? []) {
|
|
1130
|
+
if (isBareModuleSpecifier(specifier)) {
|
|
1131
|
+
specifiers.add(specifier);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
for (const specifier of plan.capabilities.allowedModules ?? []) {
|
|
1135
|
+
if (isBareModuleSpecifier(specifier)) {
|
|
1136
|
+
specifiers.add(specifier);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
for (const moduleSpecifier of collectComponentModules(plan.root)) {
|
|
1140
|
+
if (isBareModuleSpecifier(moduleSpecifier)) {
|
|
1141
|
+
specifiers.add(moduleSpecifier);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (plan.source?.code) {
|
|
1145
|
+
for (const sourceImport of await collectRuntimeSourceImports(
|
|
1146
|
+
plan.source.code
|
|
1147
|
+
)) {
|
|
1148
|
+
if (isBareModuleSpecifier(sourceImport)) {
|
|
1149
|
+
specifiers.add(sourceImport);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return [...specifiers];
|
|
1154
|
+
}
|
|
1155
|
+
function isBareModuleSpecifier(specifier) {
|
|
1156
|
+
const trimmed = specifier.trim();
|
|
1157
|
+
if (trimmed.length === 0) {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
return !trimmed.startsWith("./") && !trimmed.startsWith("../") && !trimmed.startsWith("/") && !trimmed.startsWith("http://") && !trimmed.startsWith("https://") && !trimmed.startsWith("data:") && !trimmed.startsWith("blob:");
|
|
1161
|
+
}
|
|
1162
|
+
function isHttpUrl(value) {
|
|
1163
|
+
try {
|
|
1164
|
+
const parsed = new URL(value);
|
|
1165
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
1166
|
+
} catch {
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
async function fetchRemoteModuleIntegrity(url, timeoutMs) {
|
|
1171
|
+
const cached = REMOTE_MODULE_INTEGRITY_CACHE.get(url);
|
|
1172
|
+
if (cached) {
|
|
1173
|
+
return cached;
|
|
1174
|
+
}
|
|
1175
|
+
const controller = new AbortController();
|
|
1176
|
+
const timeout = setTimeout(() => controller.abort(), Math.max(1, timeoutMs));
|
|
1177
|
+
try {
|
|
1178
|
+
const response = await fetch(url, {
|
|
1179
|
+
signal: controller.signal
|
|
1180
|
+
});
|
|
1181
|
+
if (!response.ok) {
|
|
1182
|
+
return void 0;
|
|
1183
|
+
}
|
|
1184
|
+
const body = await response.arrayBuffer();
|
|
1185
|
+
const digest = createHash("sha384").update(Buffer.from(body)).digest("base64");
|
|
1186
|
+
const integrity = `sha384-${digest}`;
|
|
1187
|
+
REMOTE_MODULE_INTEGRITY_CACHE.set(url, integrity);
|
|
1188
|
+
return integrity;
|
|
1189
|
+
} catch {
|
|
1190
|
+
return void 0;
|
|
1191
|
+
} finally {
|
|
1192
|
+
clearTimeout(timeout);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function isRecord(value) {
|
|
1196
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1197
|
+
}
|
|
1198
|
+
void main().catch((error) => {
|
|
1199
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1200
|
+
console.error(message);
|
|
1201
|
+
process.exitCode = 1;
|
|
1202
|
+
});
|
|
1203
|
+
//# sourceMappingURL=cli.esm.js.map
|