@open-agent-toolkit/cli 0.2.20 → 0.2.22
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/assets/docs/workflows/projects/artifacts.md +1 -1
- package/assets/docs/workflows/skills/explainer-kit.md +188 -23
- package/assets/docs/workflows/skills/index.md +1 -1
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +87 -25
- package/assets/skills/explainer-kit/briefs/deep-dive.md +35 -0
- package/assets/skills/explainer-kit/briefs/engineer-tour.md +45 -0
- package/assets/skills/explainer-kit/briefs/program-recap.md +41 -0
- package/assets/skills/explainer-kit/briefs/project-explainer.md +38 -0
- package/assets/skills/explainer-kit/briefs/project-page.md +38 -0
- package/assets/skills/explainer-kit/briefs/project-recap.md +47 -0
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +28 -0
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +35 -0
- package/assets/skills/explainer-kit/examples/project-recap/content.md +57 -14
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +104 -0
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +26 -4
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +27 -10
- package/assets/skills/explainer-kit/recipes/program-recap.json +35 -11
- package/assets/skills/explainer-kit/recipes/project-explainer.json +27 -10
- package/assets/skills/explainer-kit/recipes/project-recap.json +43 -11
- package/assets/skills/explainer-kit/references/contracts.md +45 -19
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +41 -0
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +52 -0
- package/assets/skills/explainer-kit/scripts/lib/browser-runtime.mjs +442 -0
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +223 -10
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +28 -43
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +237 -0
- package/assets/skills/explainer-kit/scripts/lib/html-safety.mjs +687 -0
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +414 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +313 -14
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +314 -41
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +61 -0
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +166 -12
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +152 -2
- package/assets/skills/explainer-kit/scripts/run.mjs +789 -272
- package/assets/skills/explainer-kit/templates/deck-shell.html +25 -5
- package/assets/skills/explainer-kit/templates/diagram-shell.html +29 -7
- package/assets/skills/explainer-kit/templates/engineer-tour.html +133 -9
- package/assets/skills/explainer-kit/templates/house-style.html +82 -0
- package/assets/skills/oat-brainstorm/SKILL.md +1 -1
- package/assets/skills/oat-brainstorm/scripts/helper.js +18 -11
- package/assets/skills/oat-brainstorm/scripts/server.cjs +109 -55
- package/assets/skills/oat-explainer-kit/SKILL.md +16 -9
- package/assets/skills/oat-explainer-kit/references/author-callback.md +51 -0
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +10 -8
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +14 -0
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +7 -11
- package/assets/skills/oat-project-complete/SKILL.md +18 -2
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +7 -1
- package/assets/skills/oat-wave-execute/SKILL.md +12 -19
- package/assets/skills/oat-wave-program/SKILL.md +12 -13
- package/dist/commands/init/tools/index.d.ts.map +1 -1
- package/dist/commands/init/tools/index.js +4 -4
- package/dist/engine/compute-plan.js +3 -3
- package/package.json +2 -2
- package/assets/skills/explainer-kit/schemas/author-request.schema.json +0 -85
- package/assets/skills/explainer-kit/schemas/author-result.schema.json +0 -65
|
@@ -2,6 +2,8 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { posix } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { canonicalHash, validateContract } from './contracts.mjs';
|
|
5
|
+
import { renderDiagram as renderDiagramBlock } from './diagram.mjs';
|
|
6
|
+
import { MarkdownSafetyError, parseMarkdownDocument } from './markdown.mjs';
|
|
5
7
|
|
|
6
8
|
const RENDER_STRATEGIES = new Set(['default-only', 'user-switchable']);
|
|
7
9
|
const TEMPLATE_BY_TYPE = new Map([
|
|
@@ -50,6 +52,7 @@ export async function renderArtifact({
|
|
|
50
52
|
const sections = content.sections.map((section) => ({
|
|
51
53
|
...section,
|
|
52
54
|
anchor: section.id,
|
|
55
|
+
...prepareSectionMarkdown(section.content, theme),
|
|
53
56
|
}));
|
|
54
57
|
const links = content.artifactLinks ?? [];
|
|
55
58
|
const values = templateValues({
|
|
@@ -82,6 +85,7 @@ export async function renderArtifact({
|
|
|
82
85
|
: undefined,
|
|
83
86
|
mediaType: 'text/html',
|
|
84
87
|
html,
|
|
88
|
+
warnings: sections.flatMap((section) => section.warnings),
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
|
|
@@ -164,7 +168,7 @@ function templateValues({
|
|
|
164
168
|
baseUrl,
|
|
165
169
|
),
|
|
166
170
|
CONTENT: renderSections(sections, { tour: true }),
|
|
167
|
-
DIAGRAM:
|
|
171
|
+
DIAGRAM: renderLegacyDiagram(sections),
|
|
168
172
|
FOOTER: footer,
|
|
169
173
|
};
|
|
170
174
|
case 'deck-shell':
|
|
@@ -181,7 +185,7 @@ function templateValues({
|
|
|
181
185
|
case 'diagram-shell':
|
|
182
186
|
return {
|
|
183
187
|
...common,
|
|
184
|
-
DIAGRAM:
|
|
188
|
+
DIAGRAM: renderLegacyDiagram(sections),
|
|
185
189
|
LEGEND: [
|
|
186
190
|
sections
|
|
187
191
|
.map(
|
|
@@ -213,11 +217,134 @@ function renderSections(sections, { tour = false } = {}) {
|
|
|
213
217
|
const tourAttributes = tour
|
|
214
218
|
? ` data-active-nodes="node-${index + 1}" data-active-edges=""`
|
|
215
219
|
: '';
|
|
216
|
-
return `<section id="${escapeAttribute(section.anchor)}"${tourAttributes}><
|
|
220
|
+
return `<section id="${escapeAttribute(section.anchor)}"${tourAttributes}><h2>${escapeHtml(section.title ?? humanize(section.id))}</h2>${renderMarkdownNodes(section.ast.children)}</section>`;
|
|
217
221
|
})
|
|
218
222
|
.join('');
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
function prepareSectionMarkdown(content, theme) {
|
|
226
|
+
try {
|
|
227
|
+
const parsed = parseMarkdownDocument(content);
|
|
228
|
+
const diagramWarnings = [];
|
|
229
|
+
visitMarkdownNodes(parsed.ast.children, (node) => {
|
|
230
|
+
if (node.type !== 'diagram') return;
|
|
231
|
+
const rendered = renderDiagramBlock(node.source, { theme });
|
|
232
|
+
node.renderedHtml = rendered.html;
|
|
233
|
+
diagramWarnings.push(...rendered.warnings);
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
...parsed,
|
|
237
|
+
warnings: [...parsed.warnings, ...diagramWarnings],
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (
|
|
241
|
+
!(error instanceof MarkdownSafetyError) ||
|
|
242
|
+
!/raw html|node type: html/i.test(error.message)
|
|
243
|
+
) {
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
ast: {
|
|
248
|
+
type: 'document',
|
|
249
|
+
children: [
|
|
250
|
+
{
|
|
251
|
+
type: 'paragraph',
|
|
252
|
+
children: [{ type: 'text', value: content }],
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
warnings: [
|
|
257
|
+
{
|
|
258
|
+
code: 'legacy-raw-html-escaped',
|
|
259
|
+
message: 'Legacy section markup was rendered as escaped text.',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function visitMarkdownNodes(nodes, callback) {
|
|
267
|
+
for (const node of nodes) {
|
|
268
|
+
callback(node);
|
|
269
|
+
if (Array.isArray(node.children)) {
|
|
270
|
+
visitMarkdownNodes(node.children, callback);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function renderMarkdownNodes(nodes) {
|
|
276
|
+
return nodes.map(renderMarkdownNode).join('');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function renderMarkdownNode(node) {
|
|
280
|
+
switch (node.type) {
|
|
281
|
+
case 'text':
|
|
282
|
+
return escapeHtml(node.value);
|
|
283
|
+
case 'paragraph':
|
|
284
|
+
if (node.children.length === 1 && node.children[0].type === 'figure') {
|
|
285
|
+
return renderMarkdownNode(node.children[0]);
|
|
286
|
+
}
|
|
287
|
+
return `<p>${renderMarkdownNodes(node.children)}</p>`;
|
|
288
|
+
case 'heading':
|
|
289
|
+
return `<h${node.depth}>${renderMarkdownNodes(node.children)}</h${node.depth}>`;
|
|
290
|
+
case 'strong':
|
|
291
|
+
return `<strong>${renderMarkdownNodes(node.children)}</strong>`;
|
|
292
|
+
case 'emphasis':
|
|
293
|
+
return `<em>${renderMarkdownNodes(node.children)}</em>`;
|
|
294
|
+
case 'delete':
|
|
295
|
+
return `<del>${renderMarkdownNodes(node.children)}</del>`;
|
|
296
|
+
case 'inlineCode':
|
|
297
|
+
return `<code>${escapeHtml(node.value)}</code>`;
|
|
298
|
+
case 'link':
|
|
299
|
+
return `<a href="${escapeAttribute(node.url)}"${node.title ? ` title="${escapeAttribute(node.title)}"` : ''}>${renderMarkdownNodes(node.children)}</a>`;
|
|
300
|
+
case 'figure':
|
|
301
|
+
return `<figure><a href="${escapeAttribute(node.url)}">${escapeHtml(node.alt || 'View figure')}</a>${node.title || node.alt ? `<figcaption>${escapeHtml(node.title ?? node.alt)}</figcaption>` : ''}</figure>`;
|
|
302
|
+
case 'blockquote':
|
|
303
|
+
return `<blockquote>${renderMarkdownNodes(node.children)}</blockquote>`;
|
|
304
|
+
case 'callout':
|
|
305
|
+
return `<aside class="callout callout--${escapeAttribute(node.kind)}" data-callout="${escapeAttribute(node.kind)}"><div class="callout__label">${escapeHtml(humanize(node.kind))}</div>${renderMarkdownNodes(node.children)}</aside>`;
|
|
306
|
+
case 'timeline':
|
|
307
|
+
return `<ol class="timeline">${node.entries
|
|
308
|
+
.map(
|
|
309
|
+
(entry) =>
|
|
310
|
+
`<li><time>${escapeHtml(entry.date)}</time><span>${escapeHtml(entry.label)}</span></li>`,
|
|
311
|
+
)
|
|
312
|
+
.join('')}</ol>`;
|
|
313
|
+
case 'code':
|
|
314
|
+
return `<pre><code${node.language ? ` class="language-${escapeAttribute(node.language)}"` : ''}>${escapeHtml(node.value)}</code></pre>`;
|
|
315
|
+
case 'diagram':
|
|
316
|
+
return `<div class="diagram-scroll">${node.renderedHtml}</div>`;
|
|
317
|
+
case 'list':
|
|
318
|
+
return renderMarkdownList(node);
|
|
319
|
+
case 'listItem':
|
|
320
|
+
return `<li>${node.checked === null ? '' : `<input type="checkbox" disabled${node.checked ? ' checked' : ''} aria-label="${node.checked ? 'Completed' : 'Incomplete'} task" />`}${renderMarkdownNodes(node.children)}</li>`;
|
|
321
|
+
case 'table':
|
|
322
|
+
return `<div class="table-scroll"><table><thead><tr>${node.header
|
|
323
|
+
.map((cell) => `<th>${renderMarkdownNodes(cell)}</th>`)
|
|
324
|
+
.join('')}</tr></thead><tbody>${node.rows
|
|
325
|
+
.map(
|
|
326
|
+
(row) =>
|
|
327
|
+
`<tr>${row
|
|
328
|
+
.map((cell) => `<td>${renderMarkdownNodes(cell)}</td>`)
|
|
329
|
+
.join('')}</tr>`,
|
|
330
|
+
)
|
|
331
|
+
.join('')}</tbody></table></div>`;
|
|
332
|
+
default:
|
|
333
|
+
throw new MarkdownSafetyError(
|
|
334
|
+
`Unsupported markdown render node: ${String(node.type)}.`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function renderMarkdownList(node) {
|
|
340
|
+
const tag = node.ordered ? 'ol' : 'ul';
|
|
341
|
+
const attributes = [
|
|
342
|
+
node.task ? ' class="task-list"' : '',
|
|
343
|
+
node.ordered && node.start !== 1 ? ` start="${node.start}"` : '',
|
|
344
|
+
].join('');
|
|
345
|
+
return `<${tag}${attributes}>${renderMarkdownNodes(node.children)}</${tag}>`;
|
|
346
|
+
}
|
|
347
|
+
|
|
221
348
|
function renderSlides(sections, links, slug, renderedPath, baseUrl) {
|
|
222
349
|
const slides = sections
|
|
223
350
|
.map(
|
|
@@ -231,11 +358,18 @@ function renderSlides(sections, links, slug, renderedPath, baseUrl) {
|
|
|
231
358
|
: slides;
|
|
232
359
|
}
|
|
233
360
|
|
|
234
|
-
|
|
361
|
+
// Nodes must fit the shell's 360x540 viewBox, so the step shrinks as sections
|
|
362
|
+
// are added rather than overflowing the canvas.
|
|
363
|
+
function renderLegacyDiagram(sections) {
|
|
364
|
+
const margin = 16;
|
|
365
|
+
const span = 540 - margin * 2;
|
|
366
|
+
const step = Math.min(96, span / Math.max(sections.length, 1));
|
|
367
|
+
const height = Math.max(24, step - 16);
|
|
235
368
|
return sections
|
|
236
369
|
.map((section, index) => {
|
|
237
|
-
const y =
|
|
238
|
-
|
|
370
|
+
const y = margin + index * step;
|
|
371
|
+
const label = section.title ?? humanize(section.id);
|
|
372
|
+
return `<g data-node="node-${index + 1}" class="node"><rect x="${margin}" y="${y}" width="${360 - margin * 2}" height="${height}" rx="8"></rect><text x="${margin + 14}" y="${y + height / 2 + 5}">${escapeHtml(label)}</text></g>`;
|
|
239
373
|
})
|
|
240
374
|
.join('');
|
|
241
375
|
}
|
|
@@ -253,12 +387,18 @@ function renderRelatedLinks(links, slug, renderedPath, baseUrl) {
|
|
|
253
387
|
.join(' ');
|
|
254
388
|
}
|
|
255
389
|
|
|
256
|
-
function artifactPath(artifact, slug) {
|
|
390
|
+
export function artifactPath(artifact, slug) {
|
|
257
391
|
const directory = TYPE_DIRECTORIES.get(artifact.type);
|
|
258
392
|
if (!directory)
|
|
259
393
|
throw new Error(`Unsupported artifact type: ${artifact.type}.`);
|
|
260
394
|
const parts = ['site', directory, slug];
|
|
261
|
-
|
|
395
|
+
const origin = artifact.origin ?? 'floor';
|
|
396
|
+
if (
|
|
397
|
+
origin === 'expansion' ||
|
|
398
|
+
(origin === 'floor' && ['diagram', 'deck'].includes(artifact.type))
|
|
399
|
+
) {
|
|
400
|
+
parts.push(artifact.id);
|
|
401
|
+
}
|
|
262
402
|
parts.push('index.html');
|
|
263
403
|
return parts.join('/');
|
|
264
404
|
}
|
|
@@ -337,11 +477,19 @@ function switchableThemeControl(theme) {
|
|
|
337
477
|
}
|
|
338
478
|
|
|
339
479
|
function assertRecipeArtifact(artifact) {
|
|
480
|
+
const hasLegacyKeys =
|
|
481
|
+
isObject(artifact) &&
|
|
482
|
+
hasExactKeys(artifact, ['id', 'type', 'template', 'required']);
|
|
483
|
+
const hasOriginKeys =
|
|
484
|
+
isObject(artifact) &&
|
|
485
|
+
hasExactKeys(artifact, ['id', 'type', 'template', 'required', 'origin']);
|
|
340
486
|
if (
|
|
341
487
|
!isObject(artifact) ||
|
|
342
|
-
!
|
|
488
|
+
(!hasLegacyKeys && !hasOriginKeys) ||
|
|
343
489
|
!SLUG_PATTERN.test(artifact.id ?? '') ||
|
|
344
|
-
typeof artifact.required !== 'boolean'
|
|
490
|
+
typeof artifact.required !== 'boolean' ||
|
|
491
|
+
(artifact.origin !== undefined &&
|
|
492
|
+
!['floor', 'expansion'].includes(artifact.origin))
|
|
345
493
|
) {
|
|
346
494
|
throw new TypeError(
|
|
347
495
|
'Recipe artifact is not a validated artifact descriptor.',
|
|
@@ -391,12 +539,18 @@ function assertContent(content, recipeArtifact) {
|
|
|
391
539
|
throw new TypeError('Artifact links must be an array.');
|
|
392
540
|
}
|
|
393
541
|
for (const link of links) {
|
|
542
|
+
const hasLegacyKeys =
|
|
543
|
+
isObject(link) && hasExactKeys(link, ['id', 'type', 'label']);
|
|
544
|
+
const hasOriginKeys =
|
|
545
|
+
isObject(link) && hasExactKeys(link, ['id', 'type', 'label', 'origin']);
|
|
394
546
|
if (
|
|
395
547
|
!isObject(link) ||
|
|
396
|
-
!
|
|
548
|
+
(!hasLegacyKeys && !hasOriginKeys) ||
|
|
397
549
|
!SLUG_PATTERN.test(link.id ?? '') ||
|
|
398
550
|
!TYPE_DIRECTORIES.has(link.type) ||
|
|
399
|
-
typeof link.label !== 'string'
|
|
551
|
+
typeof link.label !== 'string' ||
|
|
552
|
+
(link.origin !== undefined &&
|
|
553
|
+
!['floor', 'expansion'].includes(link.origin))
|
|
400
554
|
) {
|
|
401
555
|
throw new TypeError(
|
|
402
556
|
'Artifact cross-links must use typed artifact targets.',
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import {
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { basename, posix, resolve, sep } from 'node:path';
|
|
5
6
|
import { pathToFileURL } from 'node:url';
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
RENDER_QA_WARNING_IDS,
|
|
10
|
+
auditArtifactSet,
|
|
11
|
+
renderQaWarningIds,
|
|
12
|
+
runBrowserProbes,
|
|
13
|
+
} from './lib/qa.mjs';
|
|
8
14
|
import { renderArtifact } from './lib/render.mjs';
|
|
9
15
|
import { resolveTheme } from './lib/theme.mjs';
|
|
10
16
|
|
|
@@ -49,6 +55,67 @@ export const RELEASE_ARTIFACTS = Object.freeze([
|
|
|
49
55
|
}),
|
|
50
56
|
]);
|
|
51
57
|
|
|
58
|
+
export async function runRenderQaStage({
|
|
59
|
+
siteDir,
|
|
60
|
+
artifacts,
|
|
61
|
+
browserProbe,
|
|
62
|
+
widths,
|
|
63
|
+
} = {}) {
|
|
64
|
+
assertStageInput(siteDir, artifacts);
|
|
65
|
+
// Render QA is opt-in: the stage drives a probe the caller supplies and never
|
|
66
|
+
// launches a runtime of its own.
|
|
67
|
+
if (typeof browserProbe !== 'function') {
|
|
68
|
+
return {
|
|
69
|
+
valid: true,
|
|
70
|
+
skipped: true,
|
|
71
|
+
warnings: [RENDER_QA_WARNING_IDS.skippedNoProbe],
|
|
72
|
+
issues: [],
|
|
73
|
+
probes: 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return probeSiteArtifacts({
|
|
78
|
+
siteDir,
|
|
79
|
+
artifacts,
|
|
80
|
+
probe: browserProbe,
|
|
81
|
+
widths,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function probeSiteArtifacts({ siteDir, artifacts, probe, widths }) {
|
|
86
|
+
return withSiteServer(siteDir, async (origin) => {
|
|
87
|
+
const probeArtifacts = await Promise.all(
|
|
88
|
+
artifacts.map(async (artifact) => {
|
|
89
|
+
const relativePath = siteRelativePath(artifact.renderedPath);
|
|
90
|
+
return {
|
|
91
|
+
id: artifact.id,
|
|
92
|
+
type: artifact.type,
|
|
93
|
+
html: await readFile(
|
|
94
|
+
resolve(siteDir, ...relativePath.split('/')),
|
|
95
|
+
'utf8',
|
|
96
|
+
),
|
|
97
|
+
url: `${origin}/${relativePath
|
|
98
|
+
.split('/')
|
|
99
|
+
.map(encodeURIComponent)
|
|
100
|
+
.join('/')}`,
|
|
101
|
+
};
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
const browser = await runBrowserProbes({
|
|
105
|
+
artifacts: probeArtifacts,
|
|
106
|
+
probe,
|
|
107
|
+
...(widths && { widths }),
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
valid: true,
|
|
111
|
+
skipped: false,
|
|
112
|
+
warnings: renderQaWarningIds(browser.issues),
|
|
113
|
+
issues: browser.issues,
|
|
114
|
+
probes: browser.probes,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
52
119
|
export function selectReleaseVisualMatrix() {
|
|
53
120
|
const paletteModeCases = RELEASE_PALETTES.flatMap((palette, paletteIndex) =>
|
|
54
121
|
RELEASE_MODES.map((mode, modeIndex) => ({
|
|
@@ -265,6 +332,89 @@ function releaseContent(artifactId) {
|
|
|
265
332
|
};
|
|
266
333
|
}
|
|
267
334
|
|
|
335
|
+
function assertStageInput(siteDir, artifacts) {
|
|
336
|
+
if (typeof siteDir !== 'string' || siteDir.length === 0) {
|
|
337
|
+
throw new TypeError('Render QA stage requires a built site directory.');
|
|
338
|
+
}
|
|
339
|
+
if (
|
|
340
|
+
!Array.isArray(artifacts) ||
|
|
341
|
+
artifacts.length === 0 ||
|
|
342
|
+
artifacts.some(
|
|
343
|
+
(artifact) =>
|
|
344
|
+
typeof artifact?.id !== 'string' ||
|
|
345
|
+
typeof artifact?.type !== 'string' ||
|
|
346
|
+
typeof artifact?.renderedPath !== 'string',
|
|
347
|
+
)
|
|
348
|
+
) {
|
|
349
|
+
throw new TypeError(
|
|
350
|
+
'Render QA stage requires artifacts with id, type, and renderedPath.',
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
for (const artifact of artifacts) siteRelativePath(artifact.renderedPath);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function siteRelativePath(renderedPath) {
|
|
357
|
+
const withoutPrefix = renderedPath.startsWith('site/')
|
|
358
|
+
? renderedPath.slice('site/'.length)
|
|
359
|
+
: renderedPath;
|
|
360
|
+
const normalized = posix.normalize(withoutPrefix);
|
|
361
|
+
if (
|
|
362
|
+
normalized === '.' ||
|
|
363
|
+
normalized.startsWith('../') ||
|
|
364
|
+
posix.isAbsolute(normalized)
|
|
365
|
+
) {
|
|
366
|
+
throw new TypeError(`Unsafe render QA artifact path: ${renderedPath}`);
|
|
367
|
+
}
|
|
368
|
+
return normalized;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function withSiteServer(siteDir, callback) {
|
|
372
|
+
const root = resolve(siteDir);
|
|
373
|
+
const server = createServer(async (request, response) => {
|
|
374
|
+
try {
|
|
375
|
+
const pathname = decodeURIComponent(
|
|
376
|
+
new URL(request.url ?? '/', 'http://127.0.0.1').pathname,
|
|
377
|
+
);
|
|
378
|
+
const candidate = resolve(root, `.${pathname}`);
|
|
379
|
+
if (candidate !== root && !candidate.startsWith(`${root}${sep}`)) {
|
|
380
|
+
response.writeHead(403).end();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const body = await readFile(candidate);
|
|
384
|
+
response.writeHead(200, {
|
|
385
|
+
'content-type': candidate.endsWith('.html')
|
|
386
|
+
? 'text/html; charset=utf-8'
|
|
387
|
+
: 'application/octet-stream',
|
|
388
|
+
'cache-control': 'no-store',
|
|
389
|
+
});
|
|
390
|
+
response.end(body);
|
|
391
|
+
} catch {
|
|
392
|
+
response.writeHead(404).end();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
396
|
+
server.once('error', rejectListen);
|
|
397
|
+
server.listen(0, '127.0.0.1', resolveListen);
|
|
398
|
+
});
|
|
399
|
+
const address = server.address();
|
|
400
|
+
if (!address || typeof address === 'string') {
|
|
401
|
+
await closeServer(server);
|
|
402
|
+
throw new Error('Render QA server did not expose a TCP address.');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
return await callback(`http://127.0.0.1:${address.port}`);
|
|
407
|
+
} finally {
|
|
408
|
+
await closeServer(server);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function closeServer(server) {
|
|
413
|
+
return new Promise((resolveClose, rejectClose) => {
|
|
414
|
+
server.close((error) => (error ? rejectClose(error) : resolveClose()));
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
268
418
|
if (
|
|
269
419
|
process.argv[1] &&
|
|
270
420
|
pathToFileURL(process.argv[1]).href === import.meta.url
|