screencast-axi 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 +555 -0
- package/dist/bin/screencast-axi.d.ts +2 -0
- package/dist/bin/screencast-axi.js +16 -0
- package/dist/bin/screencast-axi.js.map +1 -0
- package/dist/src/auth/strategies.d.ts +54 -0
- package/dist/src/auth/strategies.js +137 -0
- package/dist/src/auth/strategies.js.map +1 -0
- package/dist/src/auth/types.d.ts +65 -0
- package/dist/src/auth/types.js +2 -0
- package/dist/src/auth/types.js.map +1 -0
- package/dist/src/browser.d.ts +100 -0
- package/dist/src/browser.js +168 -0
- package/dist/src/browser.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +160 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commands/auth.d.ts +4 -0
- package/dist/src/commands/auth.js +207 -0
- package/dist/src/commands/auth.js.map +1 -0
- package/dist/src/commands/check.d.ts +12 -0
- package/dist/src/commands/check.js +103 -0
- package/dist/src/commands/check.js.map +1 -0
- package/dist/src/commands/doctor.d.ts +15 -0
- package/dist/src/commands/doctor.js +131 -0
- package/dist/src/commands/doctor.js.map +1 -0
- package/dist/src/commands/guide.d.ts +6 -0
- package/dist/src/commands/guide.js +257 -0
- package/dist/src/commands/guide.js.map +1 -0
- package/dist/src/commands/home.d.ts +12 -0
- package/dist/src/commands/home.js +78 -0
- package/dist/src/commands/home.js.map +1 -0
- package/dist/src/commands/list.d.ts +6 -0
- package/dist/src/commands/list.js +128 -0
- package/dist/src/commands/list.js.map +1 -0
- package/dist/src/commands/record.d.ts +7 -0
- package/dist/src/commands/record.js +266 -0
- package/dist/src/commands/record.js.map +1 -0
- package/dist/src/commands/scaffold.d.ts +4 -0
- package/dist/src/commands/scaffold.js +122 -0
- package/dist/src/commands/scaffold.js.map +1 -0
- package/dist/src/commands/setup.d.ts +14 -0
- package/dist/src/commands/setup.js +137 -0
- package/dist/src/commands/setup.js.map +1 -0
- package/dist/src/config.d.ts +126 -0
- package/dist/src/config.js +275 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/director.d.ts +217 -0
- package/dist/src/director.js +415 -0
- package/dist/src/director.js.map +1 -0
- package/dist/src/duration.d.ts +33 -0
- package/dist/src/duration.js +68 -0
- package/dist/src/duration.js.map +1 -0
- package/dist/src/encode.d.ts +67 -0
- package/dist/src/encode.js +225 -0
- package/dist/src/encode.js.map +1 -0
- package/dist/src/errors.d.ts +19 -0
- package/dist/src/errors.js +27 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/flags.d.ts +39 -0
- package/dist/src/flags.js +130 -0
- package/dist/src/flags.js.map +1 -0
- package/dist/src/forensics.d.ts +51 -0
- package/dist/src/forensics.js +107 -0
- package/dist/src/forensics.js.map +1 -0
- package/dist/src/hooks.d.ts +9 -0
- package/dist/src/hooks.js +20 -0
- package/dist/src/hooks.js.map +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/inventory.d.ts +45 -0
- package/dist/src/inventory.js +89 -0
- package/dist/src/inventory.js.map +1 -0
- package/dist/src/manifest.d.ts +89 -0
- package/dist/src/manifest.js +137 -0
- package/dist/src/manifest.js.map +1 -0
- package/dist/src/output.d.ts +8 -0
- package/dist/src/output.js +2 -0
- package/dist/src/output.js.map +1 -0
- package/dist/src/overlay.d.ts +105 -0
- package/dist/src/overlay.js +302 -0
- package/dist/src/overlay.js.map +1 -0
- package/dist/src/reference.d.ts +1 -0
- package/dist/src/reference.js +108 -0
- package/dist/src/reference.js.map +1 -0
- package/dist/src/run.d.ts +82 -0
- package/dist/src/run.js +321 -0
- package/dist/src/run.js.map +1 -0
- package/dist/src/skill.d.ts +12 -0
- package/dist/src/skill.js +69 -0
- package/dist/src/skill.js.map +1 -0
- package/dist/src/toolchain.d.ts +62 -0
- package/dist/src/toolchain.js +127 -0
- package/dist/src/toolchain.js.map +1 -0
- package/dist/src/types.d.ts +88 -0
- package/dist/src/types.js +18 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/version.d.ts +10 -0
- package/dist/src/version.js +11 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +93 -0
- package/skills/screencast-axi/SKILL.md +49 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { ScreencastError } from "../errors.js";
|
|
4
|
+
/** Signed-out. The default, and correct for most public surfaces. */
|
|
5
|
+
export function noAuth() {
|
|
6
|
+
return { name: "none" };
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Sign in once by hand; every take afterwards reuses the session.
|
|
10
|
+
*
|
|
11
|
+
* This is the strategy that makes the tool work on a site nobody wrote a
|
|
12
|
+
* strategy for. The browser opens, a person signs in however that site wants -
|
|
13
|
+
* OAuth, SSO, a magic link, two-factor - and closes the window. The session
|
|
14
|
+
* lives in the persistent Chrome profile, so no code here has to know anything
|
|
15
|
+
* about how that site authenticates, and no credential is ever handled by this
|
|
16
|
+
* package.
|
|
17
|
+
*
|
|
18
|
+
* It needs `browser.profileDir` set, since a persistent profile is the thing
|
|
19
|
+
* doing the remembering.
|
|
20
|
+
*/
|
|
21
|
+
export function profileAuth(options = {}) {
|
|
22
|
+
return {
|
|
23
|
+
name: "profile",
|
|
24
|
+
async assertSignedIn(page, ctx) {
|
|
25
|
+
const landing = options.landingPath ?? "/";
|
|
26
|
+
await page.goto(new URL(landing, ctx.baseUrl).toString(), {
|
|
27
|
+
waitUntil: "domcontentloaded",
|
|
28
|
+
});
|
|
29
|
+
if (options.signedInSelector) {
|
|
30
|
+
try {
|
|
31
|
+
await page.locator(options.signedInSelector).first().waitFor({ state: "visible" });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw notSignedIn(`nothing matched \`${options.signedInSelector}\` on ${page.url()}`, ctx);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (options.signedInCookie) {
|
|
39
|
+
const [name, expected] = splitCookie(options.signedInCookie);
|
|
40
|
+
const deadline = Date.now() + 15_000;
|
|
41
|
+
for (;;) {
|
|
42
|
+
const cookies = await page.context().cookies();
|
|
43
|
+
const found = cookies.find((c) => c.name === name);
|
|
44
|
+
if (found && (expected === undefined || found.value === expected))
|
|
45
|
+
return;
|
|
46
|
+
if (Date.now() > deadline) {
|
|
47
|
+
throw notSignedIn(found
|
|
48
|
+
? `\`${name}\` is \`${found.value}\`, expected \`${expected}\``
|
|
49
|
+
: `the \`${name}\` cookie never appeared`, ctx);
|
|
50
|
+
}
|
|
51
|
+
await page.waitForTimeout(250);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Nothing to check against. Say so rather than implying it passed:
|
|
55
|
+
// "the URL is not /login" is a false pass, since a build-error page
|
|
56
|
+
// satisfies it too.
|
|
57
|
+
ctx.log("signed-in state was not verified - set signedInSelector or signedInCookie to check it");
|
|
58
|
+
},
|
|
59
|
+
async interactiveLogin(_context, page, ctx) {
|
|
60
|
+
await page.goto(ctx.baseUrl, { waitUntil: "domcontentloaded" });
|
|
61
|
+
ctx.log("Sign in in the browser window, then close it when you are done.");
|
|
62
|
+
// Resolves when the human closes the window, which is the signal that
|
|
63
|
+
// they consider themselves signed in.
|
|
64
|
+
await new Promise((done) => page.on("close", () => done()));
|
|
65
|
+
},
|
|
66
|
+
describe: () => ({
|
|
67
|
+
strategy: "profile",
|
|
68
|
+
...(options.landingPath ? { landing: options.landingPath } : {}),
|
|
69
|
+
...(options.signedInSelector ? { verifies: options.signedInSelector } : {}),
|
|
70
|
+
...(options.signedInCookie ? { verifies_cookie: options.signedInCookie } : {}),
|
|
71
|
+
...(options.label ? { as: options.label } : {}),
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A saved Playwright session file.
|
|
77
|
+
*
|
|
78
|
+
* Portable and CI-friendly in a way a Chrome profile is not: it is one JSON
|
|
79
|
+
* file that can be produced once and committed to a secret store. Needs an
|
|
80
|
+
* isolated context, so it cannot be combined with `browser.profileDir`.
|
|
81
|
+
*/
|
|
82
|
+
export function storageStateAuth(options) {
|
|
83
|
+
const at = (root) => isAbsolute(options.path) ? options.path : resolve(root, options.path);
|
|
84
|
+
return {
|
|
85
|
+
name: "storageState",
|
|
86
|
+
async preflight(ctx) {
|
|
87
|
+
const file = at(ctx.rootDir);
|
|
88
|
+
if (!existsSync(file)) {
|
|
89
|
+
throw new ScreencastError(`No saved session at ${file}`, "AUTH_NOT_READY", [
|
|
90
|
+
"Create one: `screencast-axi auth login --interactive --save-state " +
|
|
91
|
+
`${options.path}\``,
|
|
92
|
+
"A storage state is produced once by a human and reused by every take",
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
prepareContext(ctx) {
|
|
97
|
+
return { storageState: at(ctx.rootDir) };
|
|
98
|
+
},
|
|
99
|
+
async assertSignedIn(page, ctx) {
|
|
100
|
+
if (!options.signedInSelector)
|
|
101
|
+
return;
|
|
102
|
+
await page.goto(new URL(options.landingPath ?? "/", ctx.baseUrl).toString(), {
|
|
103
|
+
waitUntil: "domcontentloaded",
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
await page.locator(options.signedInSelector).first().waitFor({ state: "visible" });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw notSignedIn(`nothing matched \`${options.signedInSelector}\` - the saved session may have expired`, ctx);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
describe: () => ({ strategy: "storageState", file: options.path }),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** HTTP basic auth, which is how most staging environments are gated. */
|
|
116
|
+
export function basicAuth(options) {
|
|
117
|
+
return {
|
|
118
|
+
name: "basicAuth",
|
|
119
|
+
prepareContext: () => ({
|
|
120
|
+
httpCredentials: { username: options.username, password: options.password },
|
|
121
|
+
}),
|
|
122
|
+
// The password is deliberately absent: `describe` output is printed.
|
|
123
|
+
describe: () => ({ strategy: "basicAuth", username: options.username }),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Splits `name=value` into its parts; a bare name means "any value". */
|
|
127
|
+
export function splitCookie(spec) {
|
|
128
|
+
const at = spec.indexOf("=");
|
|
129
|
+
return at === -1 ? [spec, undefined] : [spec.slice(0, at), spec.slice(at + 1)];
|
|
130
|
+
}
|
|
131
|
+
function notSignedIn(reason, ctx) {
|
|
132
|
+
return new ScreencastError(`Not signed in: ${reason}`, "NOT_SIGNED_IN", [
|
|
133
|
+
`Sign in by hand once: \`screencast-axi auth login --interactive --base-url ${ctx.baseUrl}\``,
|
|
134
|
+
"Then check it took: `screencast-axi auth check`",
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=strategies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategies.js","sourceRoot":"","sources":["../../../src/auth/strategies.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright";
|
|
2
|
+
/**
|
|
3
|
+
* How a take gets past a login.
|
|
4
|
+
*
|
|
5
|
+
* The four jobs the interface separates were, in the recorder this grew out
|
|
6
|
+
* of, one 300-line function - and the seams matter:
|
|
7
|
+
*
|
|
8
|
+
* - `preflight` runs before the browser opens, so "the auth server is down" is
|
|
9
|
+
* discovered in a second rather than mid-take, after the capture has already
|
|
10
|
+
* started rolling.
|
|
11
|
+
* - `assertSignedIn` is separate from `signIn` because they answer different
|
|
12
|
+
* questions. The provider saying yes is not the app agreeing: a build-error
|
|
13
|
+
* page, or an app that never booted, sails past any check based on the URL.
|
|
14
|
+
* Naming it in the interface means a new strategy cannot skip it by
|
|
15
|
+
* accident.
|
|
16
|
+
* - `interactiveLogin` is the human-in-the-loop path, kept apart because it is
|
|
17
|
+
* the one thing that cannot run unattended.
|
|
18
|
+
*/
|
|
19
|
+
export interface AuthIdentity {
|
|
20
|
+
/** Stable id, if the strategy knows one. */
|
|
21
|
+
readonly id: string;
|
|
22
|
+
/** What to print: an email, a username, a profile name. Never a secret. */
|
|
23
|
+
readonly label: string;
|
|
24
|
+
}
|
|
25
|
+
export interface AuthPreflightContext {
|
|
26
|
+
readonly baseUrl: string;
|
|
27
|
+
/** Directory the config was loaded from; relative paths anchor here. */
|
|
28
|
+
readonly rootDir: string;
|
|
29
|
+
log(message: string): void;
|
|
30
|
+
}
|
|
31
|
+
export interface AuthContext extends AuthPreflightContext {
|
|
32
|
+
readonly scenario: {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly title: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Context options a strategy can set before any page exists. */
|
|
38
|
+
export interface ContextPatch {
|
|
39
|
+
readonly storageState?: string;
|
|
40
|
+
readonly httpCredentials?: {
|
|
41
|
+
readonly username: string;
|
|
42
|
+
readonly password: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface AuthStrategy {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
/** No browser: env vars present, a file on disk, a host reachable. */
|
|
48
|
+
preflight?(ctx: AuthPreflightContext): Promise<void>;
|
|
49
|
+
/** Cookies, storage state, HTTP credentials - applied at context creation. */
|
|
50
|
+
prepareContext?(ctx: AuthPreflightContext): ContextPatch;
|
|
51
|
+
/** Page-level sign-in. Returns who the take is now running as. */
|
|
52
|
+
signIn?(page: Page, ctx: AuthContext): Promise<AuthIdentity>;
|
|
53
|
+
/** Proves the app agrees someone is signed in, not just the provider. */
|
|
54
|
+
assertSignedIn?(page: Page, ctx: AuthContext): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Opens a browser for a human to sign in by hand.
|
|
57
|
+
*
|
|
58
|
+
* Resolves when they are done - normally when they close the window. A
|
|
59
|
+
* strategy without this reports that `auth login` does not apply to it.
|
|
60
|
+
*/
|
|
61
|
+
interactiveLogin?(context: BrowserContext, page: Page, ctx: AuthContext): Promise<void>;
|
|
62
|
+
/** Non-secret fields for `auth check` and `doctor` to print. */
|
|
63
|
+
describe?(): Record<string, string | number | boolean>;
|
|
64
|
+
}
|
|
65
|
+
export type AuthConfig = AuthStrategy | Record<string, AuthStrategy>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/auth/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Browser, BrowserContext, LaunchOptions, Page } from "playwright";
|
|
2
|
+
import type { Viewport } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Finding Playwright, and opening the context a take records in.
|
|
5
|
+
*
|
|
6
|
+
* Playwright is an optional peer rather than a dependency: a consuming project
|
|
7
|
+
* almost always has its own, and a second copy would mean a second browser
|
|
8
|
+
* download and - the one that actually bites - a `Page` type that does not
|
|
9
|
+
* match the `Page` the recorder hands a scenario.
|
|
10
|
+
*/
|
|
11
|
+
export interface PlaywrightModule {
|
|
12
|
+
readonly chromium: {
|
|
13
|
+
launch(options?: LaunchOptions): Promise<Browser>;
|
|
14
|
+
launchPersistentContext(userDataDir: string, options?: object): Promise<BrowserContext>;
|
|
15
|
+
};
|
|
16
|
+
readonly devices: Record<string, DevicePreset>;
|
|
17
|
+
}
|
|
18
|
+
export interface DevicePreset {
|
|
19
|
+
readonly viewport: Viewport;
|
|
20
|
+
readonly userAgent: string;
|
|
21
|
+
readonly deviceScaleFactor: number;
|
|
22
|
+
readonly isMobile: boolean;
|
|
23
|
+
readonly hasTouch: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function resolvePlaywright(): Promise<{
|
|
26
|
+
module: PlaywrightModule;
|
|
27
|
+
specifier: string;
|
|
28
|
+
}>;
|
|
29
|
+
export interface ResolvedViewport {
|
|
30
|
+
readonly viewport: Viewport;
|
|
31
|
+
readonly deviceScaleFactor: number;
|
|
32
|
+
readonly isMobile: boolean;
|
|
33
|
+
readonly hasTouch: boolean;
|
|
34
|
+
readonly userAgent?: string;
|
|
35
|
+
/** The preset this came from, for the manifest and the CLI to report. */
|
|
36
|
+
readonly device?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ViewportRequest {
|
|
39
|
+
readonly device?: string;
|
|
40
|
+
readonly viewport?: Viewport;
|
|
41
|
+
readonly orientation?: "portrait" | "landscape";
|
|
42
|
+
readonly deviceScaleFactor?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Turns a device name, an explicit viewport, or both into what a context needs.
|
|
46
|
+
*
|
|
47
|
+
* Device presets are worth going through rather than just setting a width:
|
|
48
|
+
* they carry `deviceScaleFactor` (2-3 on phones), which is the difference
|
|
49
|
+
* between a crisp phone clip and a blurry one, plus the `isMobile` and
|
|
50
|
+
* `hasTouch` flags that decide whether the page's own `@media (hover: none)`
|
|
51
|
+
* rules apply at all.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveViewport(request: ViewportRequest, fallback: Viewport): Promise<ResolvedViewport>;
|
|
54
|
+
export interface ContextOptions {
|
|
55
|
+
readonly resolved: ResolvedViewport;
|
|
56
|
+
readonly headless: boolean;
|
|
57
|
+
readonly args: readonly string[];
|
|
58
|
+
readonly profileDir?: string;
|
|
59
|
+
/** Directory for the raw capture. Absent means no video is recorded. */
|
|
60
|
+
readonly recordVideoDir?: string;
|
|
61
|
+
readonly colorScheme?: "light" | "dark";
|
|
62
|
+
readonly locale?: string;
|
|
63
|
+
readonly timezoneId?: string;
|
|
64
|
+
/** Applied by an auth strategy before any page exists. */
|
|
65
|
+
readonly storageState?: string;
|
|
66
|
+
readonly httpCredentials?: {
|
|
67
|
+
readonly username: string;
|
|
68
|
+
readonly password: string;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The size the raw video is captured at.
|
|
73
|
+
*
|
|
74
|
+
* Never larger than the viewport. Playwright renders the page at the CSS
|
|
75
|
+
* viewport and composites it into a canvas of this size *without scaling up* -
|
|
76
|
+
* measured in Chromium, asking for three times the viewport puts the page in
|
|
77
|
+
* the top-left corner and leaves the rest empty grey. Scaling down does work,
|
|
78
|
+
* which is what the cap relies on.
|
|
79
|
+
*
|
|
80
|
+
* So a device preset's `deviceScaleFactor` cannot buy a sharper capture: it
|
|
81
|
+
* still matters for how the page renders and lays itself out, but the video is
|
|
82
|
+
* CSS-pixel resolution either way.
|
|
83
|
+
*/
|
|
84
|
+
export declare function captureSize(resolved: ResolvedViewport): Viewport;
|
|
85
|
+
export interface OpenedContext {
|
|
86
|
+
readonly context: BrowserContext;
|
|
87
|
+
readonly page: Page;
|
|
88
|
+
/** When the context was created - the clock the head-trim is measured from. */
|
|
89
|
+
readonly createdAt: number;
|
|
90
|
+
close(): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Opens the context a take runs in.
|
|
94
|
+
*
|
|
95
|
+
* A `profileDir` means a persistent context, which is what lets a session
|
|
96
|
+
* someone signed into by hand survive between takes. Without one the context
|
|
97
|
+
* is isolated, which is what `storageState` needs - the two are mutually
|
|
98
|
+
* exclusive, and saying so is better than silently preferring one.
|
|
99
|
+
*/
|
|
100
|
+
export declare function openContext(options: ContextOptions): Promise<OpenedContext>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ScreencastError } from "./errors.js";
|
|
2
|
+
/** Where a consumer's Playwright might live, best first. */
|
|
3
|
+
const CANDIDATES = ["playwright", "playwright-core", "@playwright/test"];
|
|
4
|
+
let cached = null;
|
|
5
|
+
export async function resolvePlaywright() {
|
|
6
|
+
if (cached)
|
|
7
|
+
return cached;
|
|
8
|
+
const tried = [];
|
|
9
|
+
for (const specifier of CANDIDATES) {
|
|
10
|
+
try {
|
|
11
|
+
const loaded = (await import(specifier));
|
|
12
|
+
// `@playwright/test` re-exports chromium, which is why it qualifies.
|
|
13
|
+
if (loaded.chromium) {
|
|
14
|
+
cached = { module: loaded, specifier };
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
|
+
tried.push(`${specifier} (no chromium export)`);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
tried.push(specifier);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new ScreencastError("Playwright is not installed", "PLAYWRIGHT_MISSING", [
|
|
24
|
+
"Install it: `pnpm add -D playwright`",
|
|
25
|
+
"Then download the browser: `pnpm exec playwright install chromium`",
|
|
26
|
+
`Looked for: ${tried.join(", ")}`,
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Turns a device name, an explicit viewport, or both into what a context needs.
|
|
31
|
+
*
|
|
32
|
+
* Device presets are worth going through rather than just setting a width:
|
|
33
|
+
* they carry `deviceScaleFactor` (2-3 on phones), which is the difference
|
|
34
|
+
* between a crisp phone clip and a blurry one, plus the `isMobile` and
|
|
35
|
+
* `hasTouch` flags that decide whether the page's own `@media (hover: none)`
|
|
36
|
+
* rules apply at all.
|
|
37
|
+
*/
|
|
38
|
+
export async function resolveViewport(request, fallback) {
|
|
39
|
+
let base = {
|
|
40
|
+
viewport: request.viewport ?? fallback,
|
|
41
|
+
deviceScaleFactor: request.deviceScaleFactor ?? 1,
|
|
42
|
+
isMobile: false,
|
|
43
|
+
hasTouch: false,
|
|
44
|
+
};
|
|
45
|
+
if (request.device) {
|
|
46
|
+
const { module } = await resolvePlaywright();
|
|
47
|
+
const preset = module.devices[request.device];
|
|
48
|
+
if (!preset) {
|
|
49
|
+
const names = Object.keys(module.devices);
|
|
50
|
+
const near = names.filter((n) => n.toLowerCase().includes(request.device.toLowerCase()));
|
|
51
|
+
throw new ScreencastError(`Unknown device: ${request.device}`, "UNKNOWN_DEVICE", [
|
|
52
|
+
near.length > 0
|
|
53
|
+
? `Did you mean: ${near.slice(0, 5).join(", ")}?`
|
|
54
|
+
: `Playwright ships ${names.length} presets, e.g. ${names.slice(0, 4).join(", ")}`,
|
|
55
|
+
"Device names are Playwright's own and are case-sensitive",
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
base = {
|
|
59
|
+
viewport: request.viewport ?? preset.viewport,
|
|
60
|
+
deviceScaleFactor: request.deviceScaleFactor ?? preset.deviceScaleFactor,
|
|
61
|
+
isMobile: preset.isMobile,
|
|
62
|
+
hasTouch: preset.hasTouch,
|
|
63
|
+
userAgent: preset.userAgent,
|
|
64
|
+
device: request.device,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (request.orientation) {
|
|
68
|
+
const { width, height } = base.viewport;
|
|
69
|
+
const wantsPortrait = request.orientation === "portrait";
|
|
70
|
+
const isPortrait = height >= width;
|
|
71
|
+
if (wantsPortrait !== isPortrait) {
|
|
72
|
+
base = { ...base, viewport: { width: height, height: width } };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return base;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Upper bound on either dimension of the raw capture.
|
|
79
|
+
*
|
|
80
|
+
* A very large desktop viewport would otherwise produce a needlessly heavy
|
|
81
|
+
* capture that the deliverable scales down anyway.
|
|
82
|
+
*/
|
|
83
|
+
const MAX_CAPTURE_EDGE = 1600;
|
|
84
|
+
/**
|
|
85
|
+
* The size the raw video is captured at.
|
|
86
|
+
*
|
|
87
|
+
* Never larger than the viewport. Playwright renders the page at the CSS
|
|
88
|
+
* viewport and composites it into a canvas of this size *without scaling up* -
|
|
89
|
+
* measured in Chromium, asking for three times the viewport puts the page in
|
|
90
|
+
* the top-left corner and leaves the rest empty grey. Scaling down does work,
|
|
91
|
+
* which is what the cap relies on.
|
|
92
|
+
*
|
|
93
|
+
* So a device preset's `deviceScaleFactor` cannot buy a sharper capture: it
|
|
94
|
+
* still matters for how the page renders and lays itself out, but the video is
|
|
95
|
+
* CSS-pixel resolution either way.
|
|
96
|
+
*/
|
|
97
|
+
export function captureSize(resolved) {
|
|
98
|
+
const { width, height } = resolved.viewport;
|
|
99
|
+
const scale = Math.min(1, MAX_CAPTURE_EDGE / Math.max(width, height));
|
|
100
|
+
// Even dimensions: h264's yuv420p requires them, and rounding here avoids a
|
|
101
|
+
// one-pixel scale in the encoder.
|
|
102
|
+
const even = (n) => Math.max(2, Math.round((n * scale) / 2) * 2);
|
|
103
|
+
return { width: even(width), height: even(height) };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Opens the context a take runs in.
|
|
107
|
+
*
|
|
108
|
+
* A `profileDir` means a persistent context, which is what lets a session
|
|
109
|
+
* someone signed into by hand survive between takes. Without one the context
|
|
110
|
+
* is isolated, which is what `storageState` needs - the two are mutually
|
|
111
|
+
* exclusive, and saying so is better than silently preferring one.
|
|
112
|
+
*/
|
|
113
|
+
export async function openContext(options) {
|
|
114
|
+
if (options.profileDir && options.storageState) {
|
|
115
|
+
throw new ScreencastError("A persistent profile and a saved storage state cannot both be used", "CONFIG_CONFLICT", [
|
|
116
|
+
"A storage state needs an isolated context; a profile needs a persistent one",
|
|
117
|
+
"Drop `browser.profileDir`, or use an auth strategy that does not set storageState",
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
const { module } = await resolvePlaywright();
|
|
121
|
+
const { resolved } = options;
|
|
122
|
+
const shared = {
|
|
123
|
+
viewport: resolved.viewport,
|
|
124
|
+
deviceScaleFactor: resolved.deviceScaleFactor,
|
|
125
|
+
isMobile: resolved.isMobile,
|
|
126
|
+
hasTouch: resolved.hasTouch,
|
|
127
|
+
...(resolved.userAgent ? { userAgent: resolved.userAgent } : {}),
|
|
128
|
+
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
|
|
129
|
+
...(options.locale ? { locale: options.locale } : {}),
|
|
130
|
+
...(options.timezoneId ? { timezoneId: options.timezoneId } : {}),
|
|
131
|
+
...(options.httpCredentials ? { httpCredentials: options.httpCredentials } : {}),
|
|
132
|
+
...(options.recordVideoDir
|
|
133
|
+
? { recordVideo: { dir: options.recordVideoDir, size: captureSize(resolved) } }
|
|
134
|
+
: {}),
|
|
135
|
+
};
|
|
136
|
+
if (options.profileDir) {
|
|
137
|
+
const context = await module.chromium.launchPersistentContext(options.profileDir, {
|
|
138
|
+
headless: options.headless,
|
|
139
|
+
args: [...options.args],
|
|
140
|
+
...shared,
|
|
141
|
+
});
|
|
142
|
+
const createdAt = Date.now();
|
|
143
|
+
const page = context.pages()[0] ?? (await context.newPage());
|
|
144
|
+
return { context, page, createdAt, close: () => context.close() };
|
|
145
|
+
}
|
|
146
|
+
const browser = await module.chromium.launch({
|
|
147
|
+
headless: options.headless,
|
|
148
|
+
args: [...options.args],
|
|
149
|
+
});
|
|
150
|
+
const context = await browser.newContext({
|
|
151
|
+
...shared,
|
|
152
|
+
...(options.storageState ? { storageState: options.storageState } : {}),
|
|
153
|
+
});
|
|
154
|
+
const createdAt = Date.now();
|
|
155
|
+
const page = await context.newPage();
|
|
156
|
+
return {
|
|
157
|
+
context,
|
|
158
|
+
page,
|
|
159
|
+
createdAt,
|
|
160
|
+
// The capture is only finalised when the context closes, so the context
|
|
161
|
+
// has to go first and the browser after it.
|
|
162
|
+
close: async () => {
|
|
163
|
+
await context.close();
|
|
164
|
+
await browser.close();
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/browser.ts"],"names":[],"mappings":"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"}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { runAxiCli } from "axi-sdk-js";
|
|
2
|
+
import { encode as toon } from "@toon-format/toon";
|
|
3
|
+
import { isScreencastError } from "./errors.js";
|
|
4
|
+
import { ScenarioFailure } from "./run.js";
|
|
5
|
+
import { VERSION } from "./version.js";
|
|
6
|
+
import { homeView } from "./commands/home.js";
|
|
7
|
+
import { guideCommand, guideHelp } from "./commands/guide.js";
|
|
8
|
+
import { recordCommand, RECORD_FLAGS, REHEARSE_FLAGS } from "./commands/record.js";
|
|
9
|
+
import { scaffoldCommand, SCAFFOLD_FLAGS } from "./commands/scaffold.js";
|
|
10
|
+
import { authCommand, AUTH_FLAGS } from "./commands/auth.js";
|
|
11
|
+
import { listCommand, showCommand, LIST_FLAGS, SHOW_FLAGS } from "./commands/list.js";
|
|
12
|
+
import { checkCommand, CHECK_FLAGS } from "./commands/check.js";
|
|
13
|
+
import { doctorCommand, DOCTOR_FLAGS } from "./commands/doctor.js";
|
|
14
|
+
import { setupCommand, initCommand, SETUP_FLAGS, INIT_FLAGS } from "./commands/setup.js";
|
|
15
|
+
import { renderFlagHelp } from "./flags.js";
|
|
16
|
+
export const DESCRIPTION = "Record cinematic product screencasts from scripted Playwright scenarios";
|
|
17
|
+
/**
|
|
18
|
+
* Top-level help.
|
|
19
|
+
*
|
|
20
|
+
* Terse by design: the agent-facing source of truth is this block plus
|
|
21
|
+
* `<command> --help`, and every line an agent reads costs tokens. Deeper
|
|
22
|
+
* guidance lives behind `guide <topic>` so it is pulled a topic at a time.
|
|
23
|
+
*/
|
|
24
|
+
const TOP_LEVEL_HELP = `${[
|
|
25
|
+
`usage: screencast-axi <command> [args] [flags]`,
|
|
26
|
+
``,
|
|
27
|
+
`commands:`,
|
|
28
|
+
` init Write a config and a scenarios directory`,
|
|
29
|
+
` scaffold <id> Write a scenario skeleton`,
|
|
30
|
+
` rehearse <id|path> Dry run: prints what it does. --headed to watch it`,
|
|
31
|
+
` record <id|path> Run it for real and encode the clip`,
|
|
32
|
+
` auth login|check Sign in by hand once, or check the saved session`,
|
|
33
|
+
` list Every scenario, and what needs re-shooting`,
|
|
34
|
+
` show <id> One clip in full`,
|
|
35
|
+
` check Manifest, scenarios and files, cross-referenced`,
|
|
36
|
+
` doctor Everything a recording needs, checked at once`,
|
|
37
|
+
` setup Install the browser; name what you must install`,
|
|
38
|
+
` guide [topic] Topic-sized guidance. Run bare to list topics`,
|
|
39
|
+
``,
|
|
40
|
+
`flags must come after the command. \`--version\`, \`--help\` excepted.`,
|
|
41
|
+
].join("\n")}\n`;
|
|
42
|
+
function commandHelp(name, description, usage, specs) {
|
|
43
|
+
const flags = renderFlagHelp(specs);
|
|
44
|
+
return `${[
|
|
45
|
+
`command: ${name}`,
|
|
46
|
+
`description: ${description}`,
|
|
47
|
+
``,
|
|
48
|
+
`usage:`,
|
|
49
|
+
...usage.map((u) => ` ${u}`),
|
|
50
|
+
...(flags.length > 0 ? [``, `flags:`, ...flags] : []),
|
|
51
|
+
].join("\n")}\n`;
|
|
52
|
+
}
|
|
53
|
+
const COMMAND_HELP = {
|
|
54
|
+
guide: guideHelp(),
|
|
55
|
+
record: commandHelp("record", "Run a scenario and encode the clip", [
|
|
56
|
+
"screencast-axi record <id> A scenario the config lists",
|
|
57
|
+
"screencast-axi record ./tour.ts A file, no config needed",
|
|
58
|
+
"screencast-axi record --all Every configured scenario",
|
|
59
|
+
], RECORD_FLAGS),
|
|
60
|
+
rehearse: commandHelp("rehearse", "Run a scenario without recording or encoding, to prove its selectors", ["screencast-axi rehearse <id>", "screencast-axi rehearse ./tour.ts --headed"], REHEARSE_FLAGS),
|
|
61
|
+
auth: commandHelp("auth", "Sign in by hand once; every take afterwards reuses the session", [
|
|
62
|
+
"screencast-axi auth login --interactive Opens a browser for a person to sign in",
|
|
63
|
+
"screencast-axi auth check Reports whether the session still works",
|
|
64
|
+
"screencast-axi auth check <name> A named strategy from the config",
|
|
65
|
+
], AUTH_FLAGS),
|
|
66
|
+
list: commandHelp("list", "Every scenario, with what needs re-shooting", ["screencast-axi list", "screencast-axi list --stale", "screencast-axi list --full"], LIST_FLAGS),
|
|
67
|
+
show: commandHelp("show", "One clip in full: its files, sizes, narration and when it was shot", ["screencast-axi show product-tour"], SHOW_FLAGS),
|
|
68
|
+
check: commandHelp("check", "Cross-references the manifest, the scenarios and the files on disk", ["screencast-axi check", "screencast-axi check --fix-orphans"], CHECK_FLAGS),
|
|
69
|
+
doctor: commandHelp("doctor", "Checks everything a recording needs, in one pass", ["screencast-axi doctor"], DOCTOR_FLAGS),
|
|
70
|
+
setup: commandHelp("setup", "Installs the browser and names anything you have to install yourself", ["screencast-axi setup", "screencast-axi setup --browsers-only"], SETUP_FLAGS),
|
|
71
|
+
init: commandHelp("init", "Writes a config, a scenarios directory, and a gitignore entry", ["screencast-axi init", "screencast-axi init --url https://example.com"], INIT_FLAGS),
|
|
72
|
+
scaffold: commandHelp("scaffold", "Write a scenario skeleton so the boilerplate is never what goes wrong", [
|
|
73
|
+
"screencast-axi scaffold product-tour --url https://example.com",
|
|
74
|
+
"screencast-axi scaffold product-tour --url https://example.com --tour 5",
|
|
75
|
+
], SCAFFOLD_FLAGS),
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Renders an error with everything needed to act on it.
|
|
79
|
+
*
|
|
80
|
+
* The SDK's own error shape is message + code + suggestions, which is right
|
|
81
|
+
* for a usage error but throws away the point of the forensics. A take that
|
|
82
|
+
* failed on a stale selector should hand back the URL it reached, a
|
|
83
|
+
* screenshot, and what each part of the selector actually matched - so the
|
|
84
|
+
* next attempt is informed rather than another blind minute.
|
|
85
|
+
*
|
|
86
|
+
* Rendering here also means the library half never has to construct an SDK
|
|
87
|
+
* error, which is what keeps it free of an ESM-only dependency.
|
|
88
|
+
*/
|
|
89
|
+
function formatError(error) {
|
|
90
|
+
if (error instanceof ScenarioFailure) {
|
|
91
|
+
const f = error.forensics;
|
|
92
|
+
return {
|
|
93
|
+
output: `${toon({
|
|
94
|
+
error: error.message,
|
|
95
|
+
code: error.code,
|
|
96
|
+
scenario: error.scenarioId,
|
|
97
|
+
phase: error.phase,
|
|
98
|
+
...(error.lastStep !== null ? { last_step: error.lastStep } : {}),
|
|
99
|
+
...(f.url ? { url: f.url } : {}),
|
|
100
|
+
...(f.screenshot ? { screenshot: f.screenshot } : {}),
|
|
101
|
+
...(f.nearMatches
|
|
102
|
+
? {
|
|
103
|
+
near_matches: f.nearMatches.map((m) => ({
|
|
104
|
+
selector: m.selector,
|
|
105
|
+
count: m.count,
|
|
106
|
+
visible: m.visible,
|
|
107
|
+
})),
|
|
108
|
+
}
|
|
109
|
+
: {}),
|
|
110
|
+
...(f.notes ? { notes: f.notes } : {}),
|
|
111
|
+
...(error.suggestions.length > 0 ? { help: error.suggestions } : {}),
|
|
112
|
+
})}\n`,
|
|
113
|
+
exitCode: 1,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (isScreencastError(error)) {
|
|
117
|
+
return {
|
|
118
|
+
output: `${toon({
|
|
119
|
+
error: error.message,
|
|
120
|
+
code: error.code,
|
|
121
|
+
...(error.suggestions.length > 0 ? { help: error.suggestions } : {}),
|
|
122
|
+
})}\n`,
|
|
123
|
+
// A usage error is the caller's to fix, and exits 2 so a script can tell
|
|
124
|
+
// it apart from work that ran and failed.
|
|
125
|
+
exitCode: error.code === "VALIDATION_ERROR" ? 2 : 1,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
output: `${toon({
|
|
130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
131
|
+
code: "UNKNOWN",
|
|
132
|
+
})}\n`,
|
|
133
|
+
exitCode: 1,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export async function main(argv) {
|
|
137
|
+
await runAxiCli({
|
|
138
|
+
formatError,
|
|
139
|
+
description: DESCRIPTION,
|
|
140
|
+
version: VERSION,
|
|
141
|
+
topLevelHelp: TOP_LEVEL_HELP,
|
|
142
|
+
...(argv ? { argv } : {}),
|
|
143
|
+
home: () => homeView(),
|
|
144
|
+
commands: {
|
|
145
|
+
guide: (args) => guideCommand(args),
|
|
146
|
+
scaffold: (args) => scaffoldCommand(args),
|
|
147
|
+
auth: (args) => authCommand(args),
|
|
148
|
+
list: (args) => listCommand(args),
|
|
149
|
+
show: (args) => showCommand(args),
|
|
150
|
+
check: (args) => checkCommand(args),
|
|
151
|
+
doctor: (args) => doctorCommand(args),
|
|
152
|
+
setup: (args) => setupCommand(args),
|
|
153
|
+
init: (args) => initCommand(args),
|
|
154
|
+
record: (args) => recordCommand(args, "record"),
|
|
155
|
+
rehearse: (args) => recordCommand(args, "rehearse"),
|
|
156
|
+
},
|
|
157
|
+
getCommandHelp: (command) => COMMAND_HELP[command],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=cli.js.map
|