@dailephd/my-dev-kit-lab 0.4.6 → 0.4.7
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/CHANGELOG.md +11 -2
- package/README.md +7 -1
- package/dist/src/browser/index.js +2 -0
- package/dist/src/browser/playwrightRuntime.js +74 -0
- package/dist/src/browser/types.js +10 -0
- package/dist/src/cli/help.js +76 -0
- package/dist/src/cli/runLabCli.js +29 -1
- package/dist/src/commands/runTutorialRunCommand.js +170 -0
- package/dist/src/commands/runTutorialValidateCommand.js +138 -0
- package/dist/src/core/processTree.js +52 -0
- package/dist/src/core/resolveCommand.js +34 -0
- package/dist/src/core/runMeasuredCommand.js +39 -31
- package/dist/src/runtime/index.js +1 -0
- package/dist/src/runtime/managedProcess.js +380 -0
- package/dist/src/screenshot/captureReportScreenshot.js +30 -27
- package/dist/src/tutorial/index.js +16 -0
- package/dist/src/tutorial/loadTutorialContracts.js +60 -0
- package/dist/src/tutorial/markdownTutorialWriter.js +53 -0
- package/dist/src/tutorial/runTutorial.js +465 -0
- package/dist/src/tutorial/scenarioValidation.js +383 -0
- package/dist/src/tutorial/subtitleWriter.js +88 -0
- package/dist/src/tutorial/targetContractValidation.js +212 -0
- package/dist/src/tutorial/tutorialActions.js +110 -0
- package/dist/src/tutorial/tutorialArtifacts.js +176 -0
- package/dist/src/tutorial/tutorialAssertions.js +249 -0
- package/dist/src/tutorial/tutorialCursor.js +149 -0
- package/dist/src/tutorial/tutorialLocators.js +37 -0
- package/dist/src/tutorial/tutorialManifest.js +74 -0
- package/dist/src/tutorial/tutorialOverlay.js +180 -0
- package/dist/src/tutorial/tutorialPaths.js +86 -0
- package/dist/src/tutorial/tutorialSession.js +398 -0
- package/dist/src/tutorial/types.js +48 -0
- package/dist/src/tutorial/validationPrimitives.js +141 -0
- package/examples/tutorial-browser/index.html +47 -0
- package/examples/tutorial-browser/prepare.mjs +14 -0
- package/examples/tutorial-browser/scenario.json +17 -0
- package/examples/tutorial-browser/server.mjs +14 -0
- package/package.json +8 -4
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { closeBrowserQuietly, errorMessage, isMissingBrowserRuntimeError, launchChromium } from "../browser/index.js";
|
|
4
5
|
const SKIP_WARNING = "PNG screenshot skipped because Playwright or browser runtime is unavailable.";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return (await import("playwright"));
|
|
16
|
-
}
|
|
6
|
+
const REPORT_VIEWPORT = { width: 1440, height: 1080 };
|
|
7
|
+
/**
|
|
8
|
+
* Captures a one-shot PNG of a local HTML report.
|
|
9
|
+
*
|
|
10
|
+
* Playwright loading, Chromium launch, and browser-availability classification
|
|
11
|
+
* are owned by `src/browser`; this module owns only the report-specific policy
|
|
12
|
+
* of mapping a generic runtime outcome onto captured/skipped/failed. A missing
|
|
13
|
+
* Playwright package and a missing Chromium binary are both non-fatal for a
|
|
14
|
+
* report (the HTML report is still produced), so both become "skipped".
|
|
15
|
+
*/
|
|
17
16
|
export async function captureReportScreenshot(htmlPath, pngPath, options) {
|
|
18
|
-
const loadPlaywright = options?.loadPlaywright ?? defaultLoadPlaywright;
|
|
19
17
|
try {
|
|
20
18
|
await access(htmlPath);
|
|
21
19
|
}
|
|
22
|
-
catch
|
|
20
|
+
catch {
|
|
23
21
|
return {
|
|
24
22
|
status: "failed",
|
|
25
23
|
htmlPath,
|
|
@@ -27,11 +25,8 @@ export async function captureReportScreenshot(htmlPath, pngPath, options) {
|
|
|
27
25
|
error: `HTML report not found: ${htmlPath}`
|
|
28
26
|
};
|
|
29
27
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
playwright = await loadPlaywright();
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
28
|
+
const launch = await launchChromium({ headless: true, loadPlaywright: options?.loadPlaywright });
|
|
29
|
+
if (launch.status === "unavailable") {
|
|
35
30
|
return {
|
|
36
31
|
status: "skipped",
|
|
37
32
|
htmlPath,
|
|
@@ -39,10 +34,17 @@ export async function captureReportScreenshot(htmlPath, pngPath, options) {
|
|
|
39
34
|
warning: SKIP_WARNING
|
|
40
35
|
};
|
|
41
36
|
}
|
|
42
|
-
|
|
37
|
+
if (launch.status === "failed") {
|
|
38
|
+
return {
|
|
39
|
+
status: "failed",
|
|
40
|
+
htmlPath,
|
|
41
|
+
pngPath,
|
|
42
|
+
error: launch.error
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const browser = launch.browser;
|
|
43
46
|
try {
|
|
44
|
-
|
|
45
|
-
const page = await browser.newPage({ viewport: { width: 1440, height: 1080 } });
|
|
47
|
+
const page = await browser.newPage({ viewport: { ...REPORT_VIEWPORT } });
|
|
46
48
|
await page.goto(pathToFileURL(path.resolve(htmlPath)).href, { waitUntil: "load" });
|
|
47
49
|
await page.screenshot({ path: pngPath, fullPage: true });
|
|
48
50
|
await browser.close();
|
|
@@ -53,10 +55,11 @@ export async function captureReportScreenshot(htmlPath, pngPath, options) {
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
catch (error) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
await closeBrowserQuietly(browser);
|
|
59
|
+
// A post-launch failure can still be a missing-browser-runtime symptom (for
|
|
60
|
+
// example a browser process that dies on first page use), so the same closed
|
|
61
|
+
// classifier applies here as at launch time.
|
|
62
|
+
if (isMissingBrowserRuntimeError(error)) {
|
|
60
63
|
return {
|
|
61
64
|
status: "skipped",
|
|
62
65
|
htmlPath,
|
|
@@ -68,7 +71,7 @@ export async function captureReportScreenshot(htmlPath, pngPath, options) {
|
|
|
68
71
|
status: "failed",
|
|
69
72
|
htmlPath,
|
|
70
73
|
pngPath,
|
|
71
|
-
error:
|
|
74
|
+
error: errorMessage(error)
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./scenarioValidation.js";
|
|
3
|
+
export * from "./targetContractValidation.js";
|
|
4
|
+
export * from "./loadTutorialContracts.js";
|
|
5
|
+
export * from "./tutorialPaths.js";
|
|
6
|
+
export * from "./tutorialLocators.js";
|
|
7
|
+
export * from "./tutorialActions.js";
|
|
8
|
+
export * from "./tutorialAssertions.js";
|
|
9
|
+
export * from "./tutorialCursor.js";
|
|
10
|
+
export * from "./tutorialOverlay.js";
|
|
11
|
+
export * from "./tutorialArtifacts.js";
|
|
12
|
+
export * from "./subtitleWriter.js";
|
|
13
|
+
export * from "./markdownTutorialWriter.js";
|
|
14
|
+
export * from "./tutorialManifest.js";
|
|
15
|
+
export * from "./tutorialSession.js";
|
|
16
|
+
export * from "./runTutorial.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { validateTutorialScenario } from "./scenarioValidation.js";
|
|
4
|
+
import { validateTutorialTargetContract } from "./targetContractValidation.js";
|
|
5
|
+
/**
|
|
6
|
+
* Contract loading follows the v0.4.6 invocation-path rule: an absolute path is
|
|
7
|
+
* used as supplied, a relative path resolves against invocationCwd -- never
|
|
8
|
+
* against packageRoot, workspaceRoot or the current process directory.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveInvocationPath(invocationCwd, suppliedPath) {
|
|
11
|
+
return path.isAbsolute(suppliedPath) ? path.resolve(suppliedPath) : path.resolve(invocationCwd, suppliedPath);
|
|
12
|
+
}
|
|
13
|
+
export async function loadTutorialScenario(invocationCwd, suppliedPath) {
|
|
14
|
+
const scenarioPath = resolveInvocationPath(invocationCwd, suppliedPath);
|
|
15
|
+
const parsed = await readJsonFile(scenarioPath, "Tutorial scenario");
|
|
16
|
+
if (!parsed.ok) {
|
|
17
|
+
return { ok: false, errors: parsed.errors };
|
|
18
|
+
}
|
|
19
|
+
const validated = validateTutorialScenario(parsed.value, scenarioPath);
|
|
20
|
+
if (!validated.ok) {
|
|
21
|
+
return { ok: false, errors: validated.errors };
|
|
22
|
+
}
|
|
23
|
+
return { ok: true, value: { scenario: validated.value, scenarioPath } };
|
|
24
|
+
}
|
|
25
|
+
export async function loadTutorialTargetContract(invocationCwd, suppliedPath) {
|
|
26
|
+
const targetContractPath = resolveInvocationPath(invocationCwd, suppliedPath);
|
|
27
|
+
const parsed = await readJsonFile(targetContractPath, "Tutorial target contract");
|
|
28
|
+
if (!parsed.ok) {
|
|
29
|
+
return { ok: false, errors: parsed.errors };
|
|
30
|
+
}
|
|
31
|
+
const validated = validateTutorialTargetContract(parsed.value, targetContractPath);
|
|
32
|
+
if (!validated.ok) {
|
|
33
|
+
return { ok: false, errors: validated.errors };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
value: {
|
|
38
|
+
targetContract: validated.value,
|
|
39
|
+
targetContractPath,
|
|
40
|
+
contractRoot: path.dirname(targetContractPath)
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function readJsonFile(filePath, label) {
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = await readFile(filePath, "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
51
|
+
return { ok: false, errors: [`${label} could not be read: ${filePath} (${reason})`] };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
58
|
+
return { ok: false, errors: [`${label} is not valid JSON: ${filePath} (${reason})`] };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function renderTutorialMarkdown(options) {
|
|
2
|
+
const { scenario } = options;
|
|
3
|
+
const stepById = new Map(scenario.steps.map((step) => [step.id, step]));
|
|
4
|
+
const screenshotById = new Map(options.screenshots.filter((record) => record.id !== undefined).map((record) => [record.id, record]));
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push(`# ${scenario.title}`);
|
|
7
|
+
if (scenario.description !== undefined && scenario.description.trim().length > 0) {
|
|
8
|
+
lines.push("");
|
|
9
|
+
lines.push(scenario.description.trim());
|
|
10
|
+
}
|
|
11
|
+
let position = 0;
|
|
12
|
+
for (const stepResult of options.steps) {
|
|
13
|
+
// A not-run step never happened, so it is not presented as part of the
|
|
14
|
+
// tutorial at all.
|
|
15
|
+
if (stepResult.status === "not-run") {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const step = stepById.get(stepResult.id);
|
|
19
|
+
if (!step) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
position += 1;
|
|
23
|
+
lines.push("");
|
|
24
|
+
lines.push(`## Step ${position}: ${step.id}`);
|
|
25
|
+
lines.push("");
|
|
26
|
+
lines.push(step.narration.trim());
|
|
27
|
+
if (stepResult.status === "failed") {
|
|
28
|
+
lines.push("");
|
|
29
|
+
// Stated plainly from the run result; no troubleshooting advice is invented.
|
|
30
|
+
lines.push(`**This step failed during execution.** ${stepResult.error ?? ""}`.trim());
|
|
31
|
+
}
|
|
32
|
+
const screenshotId = step.screenshot?.id;
|
|
33
|
+
if (screenshotId !== undefined) {
|
|
34
|
+
const record = screenshotById.get(screenshotId);
|
|
35
|
+
if (record?.status === "written" && record.path !== undefined) {
|
|
36
|
+
lines.push("");
|
|
37
|
+
lines.push(`})`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
lines.push("");
|
|
42
|
+
return `${lines.join("\n")}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* tutorial.md lives in `artifacts/` and screenshots live in `screenshots/`, both
|
|
46
|
+
* directly beneath runRoot, so a run-root-relative screenshot path becomes a
|
|
47
|
+
* `../`-prefixed sibling link. POSIX separators keep the document portable; an
|
|
48
|
+
* absolute machine path is never written.
|
|
49
|
+
*/
|
|
50
|
+
export function toMarkdownScreenshotLink(runRelativePosixPath) {
|
|
51
|
+
const normalized = runRelativePosixPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
52
|
+
return `../${normalized}`;
|
|
53
|
+
}
|