demo-this-pr 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 +289 -0
- package/assets/demo-this-pr-logo.svg +41 -0
- package/dist/auto.d.ts +17 -0
- package/dist/auto.js +457 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +201 -0
- package/dist/demoPlan.d.ts +10 -0
- package/dist/demoPlan.js +159 -0
- package/dist/git.d.ts +2 -0
- package/dist/git.js +87 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/playwright.d.ts +11 -0
- package/dist/playwright.js +846 -0
- package/dist/prKit.d.ts +2 -0
- package/dist/prKit.js +99 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +239 -0
- package/dist/runs.d.ts +1 -0
- package/dist/runs.js +30 -0
- package/dist/shell.d.ts +3 -0
- package/dist/shell.js +61 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.js +1 -0
- package/dist/viewer.d.ts +1 -0
- package/dist/viewer.js +32 -0
- package/package.json +61 -0
package/dist/demoPlan.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export async function buildDemoPlan(input) {
|
|
3
|
+
if (input.demoPlanPath) {
|
|
4
|
+
return readDemoPlan(input.demoPlanPath);
|
|
5
|
+
}
|
|
6
|
+
const parsedSteps = input.demoSteps.map(parseDemoStep);
|
|
7
|
+
const steps = parsedSteps.length > 0 ? parsedSteps : defaultSteps(input.demoUrl);
|
|
8
|
+
return {
|
|
9
|
+
title: input.feature,
|
|
10
|
+
baseUrl: input.demoUrl,
|
|
11
|
+
why: input.why,
|
|
12
|
+
steps
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export async function readDemoPlan(path) {
|
|
16
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
17
|
+
if (!isRecord(parsed)) {
|
|
18
|
+
throw new Error("Demo plan must be a JSON object.");
|
|
19
|
+
}
|
|
20
|
+
const title = readString(parsed.title, "title") ?? "PR demo";
|
|
21
|
+
const baseUrl = readString(parsed.baseUrl, "baseUrl");
|
|
22
|
+
const why = readString(parsed.why, "why");
|
|
23
|
+
const rawSteps = Array.isArray(parsed.steps) ? parsed.steps : [];
|
|
24
|
+
const steps = rawSteps.map((step, index) => normalizeDemoStep(step, index));
|
|
25
|
+
if (steps.length === 0) {
|
|
26
|
+
throw new Error("Demo plan must include at least one step.");
|
|
27
|
+
}
|
|
28
|
+
return { title, baseUrl, why, steps };
|
|
29
|
+
}
|
|
30
|
+
export function parseDemoStep(step) {
|
|
31
|
+
const [kind, ...rest] = step.split(":");
|
|
32
|
+
const payload = rest.join(":").trim();
|
|
33
|
+
if (!kind || !payload) {
|
|
34
|
+
throw new Error(`Invalid demo step "${step}". Expected kind:payload.`);
|
|
35
|
+
}
|
|
36
|
+
if (kind === "goto") {
|
|
37
|
+
return { caption: `Open ${payload}`, goto: payload, screenshot: "open" };
|
|
38
|
+
}
|
|
39
|
+
if (kind === "click") {
|
|
40
|
+
return { caption: `Click ${payload}`, click: payload };
|
|
41
|
+
}
|
|
42
|
+
if (kind === "fill") {
|
|
43
|
+
const [selector, ...valueParts] = payload.split(":");
|
|
44
|
+
const value = valueParts.join(":");
|
|
45
|
+
if (!selector || !value) {
|
|
46
|
+
throw new Error(`Invalid fill step "${step}". Expected fill:selector:value.`);
|
|
47
|
+
}
|
|
48
|
+
return { caption: `Fill ${selector}`, fill: { selector, value } };
|
|
49
|
+
}
|
|
50
|
+
if (kind === "press") {
|
|
51
|
+
const [selector, ...keyParts] = payload.split(":");
|
|
52
|
+
const key = keyParts.join(":");
|
|
53
|
+
if (!selector || !key) {
|
|
54
|
+
throw new Error(`Invalid press step "${step}". Expected press:selector:key.`);
|
|
55
|
+
}
|
|
56
|
+
return { caption: `Press ${key}`, press: { selector, key } };
|
|
57
|
+
}
|
|
58
|
+
if (kind === "expect") {
|
|
59
|
+
return { caption: `Verify ${payload}`, expectText: payload, screenshot: "verify" };
|
|
60
|
+
}
|
|
61
|
+
if (kind === "screenshot") {
|
|
62
|
+
return { caption: `Capture ${payload}`, screenshot: payload };
|
|
63
|
+
}
|
|
64
|
+
if (kind === "wait") {
|
|
65
|
+
const waitMs = Number(payload);
|
|
66
|
+
if (!Number.isFinite(waitMs) || waitMs < 0) {
|
|
67
|
+
throw new Error(`Invalid wait step "${step}". Wait must be a non-negative number.`);
|
|
68
|
+
}
|
|
69
|
+
return { caption: `Wait ${waitMs}ms`, waitMs };
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Unknown demo step kind "${kind}".`);
|
|
72
|
+
}
|
|
73
|
+
function defaultSteps(demoUrl) {
|
|
74
|
+
if (!demoUrl) {
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
caption: "Demo URL not configured",
|
|
78
|
+
screenshot: "demo-not-run"
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
caption: "Open the feature entry point",
|
|
85
|
+
goto: "/",
|
|
86
|
+
screenshot: "entry-point"
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
function normalizeDemoStep(value, index) {
|
|
91
|
+
if (!isRecord(value)) {
|
|
92
|
+
throw new Error(`Demo step ${index + 1} must be an object.`);
|
|
93
|
+
}
|
|
94
|
+
const step = {};
|
|
95
|
+
const caption = readString(value.caption, `steps[${index}].caption`);
|
|
96
|
+
if (caption) {
|
|
97
|
+
step.caption = caption;
|
|
98
|
+
}
|
|
99
|
+
const goto = readString(value.goto, `steps[${index}].goto`);
|
|
100
|
+
if (goto) {
|
|
101
|
+
step.goto = goto;
|
|
102
|
+
}
|
|
103
|
+
const click = readString(value.click, `steps[${index}].click`);
|
|
104
|
+
if (click) {
|
|
105
|
+
step.click = click;
|
|
106
|
+
}
|
|
107
|
+
if (value.fill !== undefined) {
|
|
108
|
+
if (!isRecord(value.fill)) {
|
|
109
|
+
throw new Error(`steps[${index}].fill must be an object.`);
|
|
110
|
+
}
|
|
111
|
+
const selector = readString(value.fill.selector, `steps[${index}].fill.selector`);
|
|
112
|
+
const fillValue = readString(value.fill.value, `steps[${index}].fill.value`);
|
|
113
|
+
if (!selector || fillValue === undefined) {
|
|
114
|
+
throw new Error(`steps[${index}].fill requires selector and value.`);
|
|
115
|
+
}
|
|
116
|
+
step.fill = { selector, value: fillValue };
|
|
117
|
+
}
|
|
118
|
+
if (value.press !== undefined) {
|
|
119
|
+
if (!isRecord(value.press)) {
|
|
120
|
+
throw new Error(`steps[${index}].press must be an object.`);
|
|
121
|
+
}
|
|
122
|
+
const selector = readString(value.press.selector, `steps[${index}].press.selector`);
|
|
123
|
+
const key = readString(value.press.key, `steps[${index}].press.key`);
|
|
124
|
+
if (!selector || !key) {
|
|
125
|
+
throw new Error(`steps[${index}].press requires selector and key.`);
|
|
126
|
+
}
|
|
127
|
+
step.press = { selector, key };
|
|
128
|
+
}
|
|
129
|
+
const expectText = readString(value.expectText, `steps[${index}].expectText`);
|
|
130
|
+
if (expectText) {
|
|
131
|
+
step.expectText = expectText;
|
|
132
|
+
}
|
|
133
|
+
const screenshot = readString(value.screenshot, `steps[${index}].screenshot`);
|
|
134
|
+
if (screenshot) {
|
|
135
|
+
step.screenshot = screenshot;
|
|
136
|
+
}
|
|
137
|
+
if (value.waitMs !== undefined) {
|
|
138
|
+
if (typeof value.waitMs !== "number" || !Number.isFinite(value.waitMs) || value.waitMs < 0) {
|
|
139
|
+
throw new Error(`steps[${index}].waitMs must be a non-negative number.`);
|
|
140
|
+
}
|
|
141
|
+
step.waitMs = value.waitMs;
|
|
142
|
+
}
|
|
143
|
+
if (!step.goto && !step.click && !step.fill && !step.press && !step.expectText && !step.screenshot && step.waitMs === undefined) {
|
|
144
|
+
throw new Error(`Demo step ${index + 1} has no action.`);
|
|
145
|
+
}
|
|
146
|
+
return step;
|
|
147
|
+
}
|
|
148
|
+
function readString(value, key) {
|
|
149
|
+
if (value === undefined) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
if (typeof value !== "string") {
|
|
153
|
+
throw new Error(`${key} must be a string.`);
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
function isRecord(value) {
|
|
158
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
159
|
+
}
|
package/dist/git.d.ts
ADDED
package/dist/git.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { runShellCommand } from "./shell.js";
|
|
3
|
+
export async function readGitContext(cwd, requestedBase) {
|
|
4
|
+
const rootResult = await git(cwd, "rev-parse --show-toplevel");
|
|
5
|
+
if (rootResult.exitCode !== 0) {
|
|
6
|
+
return emptyGitContext(cwd, requestedBase ?? "main");
|
|
7
|
+
}
|
|
8
|
+
const root = rootResult.stdout.trim();
|
|
9
|
+
const branch = (await git(root, "rev-parse --abbrev-ref HEAD")).stdout.trim() || "unknown";
|
|
10
|
+
const headSha = (await git(root, "rev-parse --short HEAD")).stdout.trim() || "unknown";
|
|
11
|
+
const base = requestedBase ?? (await detectBase(root));
|
|
12
|
+
const range = `${base}...HEAD`;
|
|
13
|
+
const filesResult = await git(root, `diff --name-status ${range}`);
|
|
14
|
+
const worktreeResult = await git(root, "status --porcelain");
|
|
15
|
+
const commitsResult = await git(root, `log --oneline ${base}..HEAD`);
|
|
16
|
+
return {
|
|
17
|
+
available: true,
|
|
18
|
+
root,
|
|
19
|
+
branch,
|
|
20
|
+
base,
|
|
21
|
+
headSha,
|
|
22
|
+
commits: parseLines(commitsResult.stdout),
|
|
23
|
+
changedFiles: mergeChangedFiles(parseChangedFiles(filesResult.stdout), parsePorcelainStatus(worktreeResult.stdout))
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function detectBase(root) {
|
|
27
|
+
const originHead = await git(root, "symbolic-ref --quiet --short refs/remotes/origin/HEAD");
|
|
28
|
+
const detected = originHead.stdout.trim().replace(/^origin\//u, "");
|
|
29
|
+
if (originHead.exitCode === 0 && detected) {
|
|
30
|
+
return detected;
|
|
31
|
+
}
|
|
32
|
+
const mainExists = await git(root, "rev-parse --verify main");
|
|
33
|
+
if (mainExists.exitCode === 0) {
|
|
34
|
+
return "main";
|
|
35
|
+
}
|
|
36
|
+
const masterExists = await git(root, "rev-parse --verify master");
|
|
37
|
+
if (masterExists.exitCode === 0) {
|
|
38
|
+
return "master";
|
|
39
|
+
}
|
|
40
|
+
return "main";
|
|
41
|
+
}
|
|
42
|
+
function parseChangedFiles(output) {
|
|
43
|
+
return parseLines(output).map((line) => {
|
|
44
|
+
const [status = "?", ...pathParts] = line.split(/\s+/u);
|
|
45
|
+
return {
|
|
46
|
+
status,
|
|
47
|
+
path: pathParts.join(" ")
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function parsePorcelainStatus(output) {
|
|
52
|
+
return parseLines(output).map((line) => {
|
|
53
|
+
const status = line.slice(0, 2).trim() || "?";
|
|
54
|
+
const rawPath = line.slice(3).trim();
|
|
55
|
+
const path = rawPath.includes(" -> ") ? rawPath.split(" -> ").at(-1) ?? rawPath : rawPath;
|
|
56
|
+
return { status, path };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function mergeChangedFiles(...groups) {
|
|
60
|
+
const merged = new Map();
|
|
61
|
+
for (const group of groups) {
|
|
62
|
+
for (const file of group) {
|
|
63
|
+
if (!file.path) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
merged.set(file.path, file);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...merged.values()].sort((first, second) => first.path.localeCompare(second.path));
|
|
70
|
+
}
|
|
71
|
+
function parseLines(output) {
|
|
72
|
+
return output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
async function git(cwd, args) {
|
|
75
|
+
return runShellCommand(`git ${args}`, cwd);
|
|
76
|
+
}
|
|
77
|
+
function emptyGitContext(cwd, base) {
|
|
78
|
+
return {
|
|
79
|
+
available: false,
|
|
80
|
+
root: resolve(cwd),
|
|
81
|
+
branch: "unknown",
|
|
82
|
+
base,
|
|
83
|
+
headSha: "unknown",
|
|
84
|
+
commits: [],
|
|
85
|
+
changedFiles: []
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createPrKit } from "./prKit.js";
|
|
2
|
+
export { buildDemoPlan, parseDemoStep, readDemoPlan } from "./demoPlan.js";
|
|
3
|
+
export { readGitContext } from "./git.js";
|
|
4
|
+
export { renderHtmlReport, renderMarkdownReport } from "./report.js";
|
|
5
|
+
export { defaultRunOutputDir } from "./runs.js";
|
|
6
|
+
export { writePlaywrightArtifacts, runPlaywrightDemo } from "./playwright.js";
|
|
7
|
+
export type { CommandResult, DemoPlan, DemoRunResult, DemoStep, GitContext, GitFileChange, PrKitOptions, PrKitResult } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createPrKit } from "./prKit.js";
|
|
2
|
+
export { buildDemoPlan, parseDemoStep, readDemoPlan } from "./demoPlan.js";
|
|
3
|
+
export { readGitContext } from "./git.js";
|
|
4
|
+
export { renderHtmlReport, renderMarkdownReport } from "./report.js";
|
|
5
|
+
export { defaultRunOutputDir } from "./runs.js";
|
|
6
|
+
export { writePlaywrightArtifacts, runPlaywrightDemo } from "./playwright.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DemoPlan, DemoRunResult } from "./types.js";
|
|
2
|
+
export declare function writePlaywrightArtifacts(outputDir: string, plan: DemoPlan): Promise<{
|
|
3
|
+
specPath: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
planPath: string;
|
|
6
|
+
mcpGuidePath: string;
|
|
7
|
+
mcpScriptPath: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function runPlaywrightDemo(outputDir: string, plan: DemoPlan, options: {
|
|
10
|
+
headed: boolean;
|
|
11
|
+
}): Promise<DemoRunResult>;
|