@dailephd/my-dev-kit-lab 0.4.6 → 0.4.8

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +25 -2
  2. package/README.md +8 -1
  3. package/dist/src/browser/index.js +2 -0
  4. package/dist/src/browser/playwrightRuntime.js +74 -0
  5. package/dist/src/browser/types.js +10 -0
  6. package/dist/src/cli/help.js +76 -0
  7. package/dist/src/cli/runLabCli.js +29 -1
  8. package/dist/src/commands/runTutorialRunCommand.js +170 -0
  9. package/dist/src/commands/runTutorialValidateCommand.js +138 -0
  10. package/dist/src/core/processTree.js +52 -0
  11. package/dist/src/core/resolveCommand.js +34 -0
  12. package/dist/src/core/runMeasuredCommand.js +39 -31
  13. package/dist/src/runtime/index.js +1 -0
  14. package/dist/src/runtime/managedProcess.js +380 -0
  15. package/dist/src/screenshot/captureReportScreenshot.js +30 -27
  16. package/dist/src/tutorial/index.js +17 -0
  17. package/dist/src/tutorial/loadTutorialContracts.js +60 -0
  18. package/dist/src/tutorial/markdownTutorialWriter.js +53 -0
  19. package/dist/src/tutorial/runTutorial.js +465 -0
  20. package/dist/src/tutorial/scenarioValidation.js +427 -0
  21. package/dist/src/tutorial/subtitleWriter.js +88 -0
  22. package/dist/src/tutorial/targetContractValidation.js +212 -0
  23. package/dist/src/tutorial/tutorialActions.js +175 -0
  24. package/dist/src/tutorial/tutorialArtifacts.js +176 -0
  25. package/dist/src/tutorial/tutorialAssertions.js +249 -0
  26. package/dist/src/tutorial/tutorialCursor.js +149 -0
  27. package/dist/src/tutorial/tutorialLocators.js +37 -0
  28. package/dist/src/tutorial/tutorialManifest.js +74 -0
  29. package/dist/src/tutorial/tutorialOverlay.js +180 -0
  30. package/dist/src/tutorial/tutorialPaths.js +86 -0
  31. package/dist/src/tutorial/tutorialPointerGeometry.js +16 -0
  32. package/dist/src/tutorial/tutorialSession.js +424 -0
  33. package/dist/src/tutorial/types.js +51 -0
  34. package/dist/src/tutorial/validationPrimitives.js +141 -0
  35. package/examples/tutorial-browser/index.html +109 -0
  36. package/examples/tutorial-browser/prepare.mjs +14 -0
  37. package/examples/tutorial-browser/scenario.json +19 -0
  38. package/examples/tutorial-browser/server.mjs +14 -0
  39. 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
- function isMissingBrowserRuntime(error) {
6
- const message = error instanceof Error ? error.message : String(error);
7
- return (message.includes("Executable doesn't exist") ||
8
- message.includes("browserType.launch") ||
9
- message.includes("playwright install") ||
10
- message.includes("Failed to launch") ||
11
- message.includes("Could not find Chrome") ||
12
- message.includes("ENOENT"));
13
- }
14
- async function defaultLoadPlaywright() {
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 (error) {
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
- let playwright;
31
- try {
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
- let browser;
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
- browser = await playwright.chromium.launch({ headless: true });
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
- if (browser) {
57
- await browser.close().catch(() => undefined);
58
- }
59
- if (isMissingBrowserRuntime(error)) {
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: error instanceof Error ? error.message : String(error)
74
+ error: errorMessage(error)
72
75
  };
73
76
  }
74
77
  }
@@ -0,0 +1,17 @@
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 "./tutorialPointerGeometry.js";
9
+ export * from "./tutorialAssertions.js";
10
+ export * from "./tutorialCursor.js";
11
+ export * from "./tutorialOverlay.js";
12
+ export * from "./tutorialArtifacts.js";
13
+ export * from "./subtitleWriter.js";
14
+ export * from "./markdownTutorialWriter.js";
15
+ export * from "./tutorialManifest.js";
16
+ export * from "./tutorialSession.js";
17
+ 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(`![${step.id}](${toMarkdownScreenshotLink(record.path)})`);
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
+ }