Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/playwright-dev/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ npx playwright cli video-stop
# afterwards, use ffmpeg to turn the video into mp4 for sharing.
```

Full CLI reference: `packages/playwright-core/src/tools/cli-client/skill/SKILL.md`. In this repo, invoke as `npx playwright cli` instead of `playwright-cli`.
Full CLI reference: `packages/playwright-core/src/tools/skills/playwright-cli/SKILL.md`. In this repo, invoke as `npx playwright cli` instead of `playwright-cli`.
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const ignores = [
"packages/playwright-core/types/*",
"packages/playwright-ct-core/src/generated/*",
"packages/playwright/bundles/expect/third_party/",
"packages/skills/",
"packages/playwright-core/src/tools/skills/",
"packages/html-reporter/bundle.ts",
"packages/html-reporter/playwright.config.ts",
"packages/html-reporter/playwright/*",
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/tools/cli-client/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export async function program(options?: { embedderVersion?: string}) {
} else {
const lines = ['playwright-cli - run playwright mcp commands from terminal'];
if (process.env.CLAUDECODE || process.env.COPILOT_CLI)
lines.push(`Agent skill: ${path.relative(process.cwd(), libPath('tools', 'cli-client', 'skill', 'SKILL.md'))}`);
lines.push(`Agent skill: ${path.relative(process.cwd(), libPath('tools', 'skills', 'playwright-cli', 'SKILL.md'))}`);
lines.push(help.global);
output.help(lines.join('\n\n'));
}
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/tools/cli-daemon/DEPS.list
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[*]
../../package.ts
../utils/socketConnection.ts
../utils/installSkills.ts
../cli-client/registry.ts
../backend/
../mcp/
Expand Down
12 changes: 5 additions & 7 deletions packages/playwright-core/src/tools/cli-daemon/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import path from 'path';

import { getAsBooleanFromENV, guessClientName } from '@utils/env';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { libPath } from '../../package';
import { startCliDaemonServer } from './daemon';
import { setupExitWatchdog } from '../mcp/watchdog';
import { createBrowserWithInfo } from '../mcp/browserFactory';
import * as configUtils from '../mcp/config';
import { createClientInfo } from '../cli-client/registry';
import { installSkills } from '../utils/installSkills';
import { registry as browserRegistry } from '../../server/registry/index';
import type { Command } from 'commander';

Expand Down Expand Up @@ -92,16 +92,14 @@ export async function initWorkspace(initSkills: string | undefined) {
console.log(`✅ Workspace initialized at \`${cwd}\`.`);

if (initSkills) {
const skillSourceDir = libPath('tools', 'cli-client', 'skill');
const target = initSkills === 'agents' ? 'agents' : 'claude';
const skillDestDir = path.join(cwd, `.${target}`, 'skills', 'playwright-cli');
if (!fs.existsSync(skillSourceDir)) {
console.error('❌ Skills source directory not found:', skillSourceDir);
try {
await installSkills(['playwright-cli'], target);
} catch (error) {
console.error('❌', error instanceof Error ? error.message : error);
// eslint-disable-next-line no-restricted-properties
process.exit(1);
}
await fs.promises.cp(skillSourceDir, skillDestDir, { recursive: true });
console.log(`✅ Skills installed to \`${path.relative(cwd, skillDestDir)}\`.`);
}

await ensureConfiguredBrowserInstalled();
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export { decorateMCPCommand } from './mcp/program';
export { program as cliProgram } from './cli-client/program';
export { generateHelp, generateHelpJSON } from './cli-daemon/helpGenerator';
export { decorateProgram as decorateCliDaemonProgram, initWorkspace } from './cli-daemon/program';
export { allSkills, installSkills } from './utils/installSkills';
export { openDashboardApp, openDashboardForContext } from './dashboard/dashboardApp';

export type { ContextConfig } from './backend/context';
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/tools/trace/DEPS.list
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[*]
../../package.ts
../utils/installSkills.ts
@isomorphic/**
@utils/**

Expand Down
32 changes: 0 additions & 32 deletions packages/playwright-core/src/tools/trace/installSkill.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/playwright-core/src/tools/trace/traceCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { traceSnapshot } from './traceSnapshot';
import { traceScreenshot } from './traceScreenshot';
import { traceAttachments } from './traceAttachments';
import { traceAttachment } from './traceAttachments';
import { installSkill } from './installSkill';
import { installSkills } from '../utils/installSkills';

import type { Command } from 'commander';

Expand Down Expand Up @@ -145,6 +145,6 @@ export function addTraceCommands(program: Command, logErrorAndExit: (e: Error) =
.command('install-skill')
.description('install SKILL.md for LLM integration')
.action(async () => {
installSkill().catch(logErrorAndExit);
installSkills(['playwright-trace']).catch(logErrorAndExit);
});
}
4 changes: 4 additions & 0 deletions packages/playwright-core/src/tools/utils/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@

[extension.ts]
"strict"

[installSkills.ts]
"strict"
../../package.ts
39 changes: 39 additions & 0 deletions packages/playwright-core/src/tools/utils/installSkills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/* eslint-disable no-console */

import fs from 'fs';
import path from 'path';

import { libPath } from '../../package';

export const allSkills = ['playwright-cli', 'playwright-component-testing', 'playwright-trace'] as const;

export type SkillName = typeof allSkills[number];
export type SkillTarget = 'claude' | 'agents';

export async function installSkills(skills: readonly SkillName[], target: SkillTarget = 'claude') {
const cwd = process.cwd();
for (const skill of skills) {
const sourceDir = libPath('tools', 'skills', skill);
if (!fs.existsSync(sourceDir))
throw new Error(`Skill source directory not found: ${sourceDir}`);
const destDir = path.join(cwd, `.${target}`, 'skills', skill);
await fs.promises.cp(sourceDir, destDir, { recursive: true });
console.log(`✅ Skill installed to \`${path.relative(cwd, destDir)}\`.`);
}
}
18 changes: 18 additions & 0 deletions packages/playwright/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,23 @@ function addInitAgentsCommand(program: Command) {
});
}

function addInitSkillsCommand(program: Command) {
const command = program.command('init-skills');
command.description('Install Playwright agent skills');
const option = command.createOption('--loop <loop>', 'Agentic loop provider');
option.choices(['claude', 'agents']);
option.default('claude');
command.addOption(option);
command.action(async opts => {
try {
await tools.installSkills(tools.allSkills, opts.loop);
} catch (e) {
console.error(e);
gracefullyProcessExitDoNotHang(1);
}
});
}

const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure', 'retain-on-first-failure', 'retain-on-failure-and-retries'];

// Note: update docs/src/test-cli-js.md when you update this, program is the source of truth.
Expand Down Expand Up @@ -237,3 +254,4 @@ addClearCacheCommand(program);
addTestMCPServerCommand(program);
addTestServerCommand(program);
addInitAgentsCommand(program);
addInitSkillsCommand(program);
4 changes: 2 additions & 2 deletions tests/mcp/cli-misc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test('install workspace', async ({ cli }, testInfo) => {

test('install workspace w/skills', async ({ cli }, testInfo) => {
const { output } = await cli('install', '--skills');
expect(output).toContain(`Skills installed to \`.claude${path.sep}skills${path.sep}playwright-cli\`.`);
expect(output).toContain(`Skill installed to \`.claude${path.sep}skills${path.sep}playwright-cli\`.`);

const skillFile = testInfo.outputPath('.claude', 'skills', 'playwright-cli', 'SKILL.md');
expect(fs.existsSync(skillFile)).toBe(true);
Expand All @@ -54,7 +54,7 @@ test('install workspace w/skills', async ({ cli }, testInfo) => {

test('install workspace w/--skills=agents', async ({ cli }, testInfo) => {
const { output } = await cli('install', '--skills=agents');
expect(output).toContain(`Skills installed to \`.agents${path.sep}skills${path.sep}playwright-cli\`.`);
expect(output).toContain(`Skill installed to \`.agents${path.sep}skills${path.sep}playwright-cli\`.`);

const skillFile = testInfo.outputPath('.agents', 'skills', 'playwright-cli', 'SKILL.md');
expect(fs.existsSync(skillFile)).toBe(true);
Expand Down
19 changes: 19 additions & 0 deletions tests/mcp/init-agents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,22 @@ test('codex generates agent toml files', async ({ }) => {
expect(healerToml).toContain(`sandbox_mode = "workspace-write"`);
expect(healerToml).toMatch(/enabled_tools = \[[^\]]*"test_debug"[^\]]*\]/);
});

test('init-skills installs all skills', async ({ }) => {
const baseDir = await writeFiles({});

await spawnAsync('npx', ['playwright', 'init-skills'], { cwd: baseDir, shell: true });

for (const skill of ['playwright-cli', 'playwright-component-testing', 'playwright-trace'])
expect(fs.existsSync(path.join(baseDir, '.claude', 'skills', skill, 'SKILL.md'))).toBe(true);
expect(fs.existsSync(path.join(baseDir, '.claude', 'skills', 'playwright-cli', 'references', 'tracing.md'))).toBe(true);
});

test('init-skills installs into .agents with --loop agents', async ({ }) => {
const baseDir = await writeFiles({});

await spawnAsync('npx', ['playwright', 'init-skills', '--loop', 'agents'], { cwd: baseDir, shell: true });

for (const skill of ['playwright-cli', 'playwright-component-testing', 'playwright-trace'])
expect(fs.existsSync(path.join(baseDir, '.agents', 'skills', skill, 'SKILL.md'))).toBe(true);
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@
"exclude": [
"packages/*/lib",
"packages/extension",
"packages/skills",
"packages/playwright-core/src/tools/skills",
]
}
15 changes: 2 additions & 13 deletions utils/build/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -1055,20 +1055,9 @@ copyFiles.push({
to: 'packages/playwright/lib',
});

// Agent skills ship as-is: SKILL.md, referenced docs and templates.
copyFiles.push({
files: 'packages/skills/**/*',
from: 'packages/skills',
to: 'packages/playwright/lib/skills',
});

copyFiles.push({
files: 'packages/playwright-core/src/tools/cli-client/skill/**/*.md',
from: 'packages/playwright-core/src',
to: 'packages/playwright-core/lib',
});

copyFiles.push({
files: 'packages/playwright-core/src/tools/trace/SKILL.md',
files: 'packages/playwright-core/src/tools/skills/**/*',
from: 'packages/playwright-core/src',
to: 'packages/playwright-core/lib',
});
Expand Down
3 changes: 2 additions & 1 deletion utils/check_deps.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ async function innerCheckDeps(root) {
});
const sourceFiles = program.getSourceFiles();
const errors = [];
sourceFiles.filter(x => !x.fileName.includes(path.sep + 'node_modules' + path.sep) && !x.fileName.includes(path.sep + 'bundles' + path.sep)).map(x => visit(x, x.fileName, x.getFullText()));
// Agent skills are shipped as-is (SKILL.md plus user-facing templates), they are not part of the program.
sourceFiles.filter(x => !x.fileName.includes(path.sep + 'node_modules' + path.sep) && !x.fileName.includes(path.sep + 'bundles' + path.sep) && !x.fileName.includes('/tools/skills/')).map(x => visit(x, x.fileName, x.getFullText()));

if (errors.length) {
for (const error of errors)
Expand Down
Loading