@caddis/cli 0.0.0 → 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 +150 -1
- package/bundles/antigravity-plugin/agents/code-reviewer.md +47 -0
- package/bundles/antigravity-plugin/agents/preflight.md +53 -0
- package/bundles/antigravity-plugin/guard_agy.py +338 -0
- package/bundles/antigravity-plugin/hooks.json +36 -0
- package/bundles/antigravity-plugin/mcp_config.json +8 -0
- package/bundles/antigravity-plugin/mcp_ping_server.py +55 -0
- package/bundles/antigravity-plugin/plugin.json +5 -0
- package/bundles/antigravity-plugin/session_end_agy.py +57 -0
- package/bundles/antigravity-plugin/skills/_registry.md +115 -0
- package/bundles/antigravity-plugin/skills/add-rules/SKILL.md +45 -0
- package/bundles/antigravity-plugin/skills/api-design/SKILL.md +249 -0
- package/bundles/antigravity-plugin/skills/backend-development/SKILL.md +305 -0
- package/bundles/antigravity-plugin/skills/best-practices/SKILL.md +500 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +263 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +326 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +245 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/anti-patterns.md +571 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/before-after-examples.md +1114 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/best-practices-guide.md +513 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/common-workflows.md +692 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/prompt-patterns.md +547 -0
- package/bundles/antigravity-plugin/skills/brainstorming/SKILL.md +57 -0
- package/bundles/antigravity-plugin/skills/ci-cd-pipeline/SKILL.md +315 -0
- package/bundles/antigravity-plugin/skills/code-documentation/SKILL.md +271 -0
- package/bundles/antigravity-plugin/skills/code-review/SKILL.md +122 -0
- package/bundles/antigravity-plugin/skills/codebase-audit/SKILL.md +204 -0
- package/bundles/antigravity-plugin/skills/context-curator/SKILL.md +157 -0
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +40 -0
- package/bundles/antigravity-plugin/skills/css-architecture/SKILL.md +305 -0
- package/bundles/antigravity-plugin/skills/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
- package/bundles/antigravity-plugin/skills/database-design/SKILL.md +177 -0
- package/bundles/antigravity-plugin/skills/db-diagram/SKILL.md +148 -0
- package/bundles/antigravity-plugin/skills/db-diagram/scripts/sql_to_graph.py +1212 -0
- package/bundles/antigravity-plugin/skills/digress/SKILL.md +61 -0
- package/bundles/antigravity-plugin/skills/draw-io/SKILL.md +162 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/aws-icons.md +677 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/layout-guidelines.md +142 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/troubleshooting.md +118 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/workflows.md +103 -0
- package/bundles/antigravity-plugin/skills/draw-io/scripts/convert-drawio-to-png.sh +25 -0
- package/bundles/antigravity-plugin/skills/draw-io/scripts/find_aws_icon.py +79 -0
- package/bundles/antigravity-plugin/skills/error-handling/SKILL.md +260 -0
- package/bundles/antigravity-plugin/skills/excalidraw-db/SKILL.md +38 -0
- package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +300 -0
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +198 -0
- package/bundles/antigravity-plugin/skills/frontend-design/SKILL.md +193 -0
- package/bundles/antigravity-plugin/skills/frontend-design/references/my-tech-stack.md +127 -0
- package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +195 -0
- package/bundles/antigravity-plugin/skills/git-commit/SKILL.md +285 -0
- package/bundles/antigravity-plugin/skills/golden-plan/SKILL.md +577 -0
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +100 -0
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +114 -0
- package/bundles/antigravity-plugin/skills/javascript-typescript/SKILL.md +142 -0
- package/bundles/antigravity-plugin/skills/kb/SKILL.md +60 -0
- package/bundles/antigravity-plugin/skills/mermaid-db/SKILL.md +33 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/SKILL.md +236 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/advanced-features.md +556 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/architecture-diagrams.md +192 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/c4-diagrams.md +410 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/class-diagrams.md +361 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/erd-diagrams.md +510 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/flowcharts.md +450 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/sequence-diagrams.md +394 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/troubleshooting.md +335 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/workflows.md +418 -0
- package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +68 -0
- package/bundles/antigravity-plugin/skills/mockup/SKILL.md +242 -0
- package/bundles/antigravity-plugin/skills/particle-art/SKILL.md +243 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/canvas-utils.ts +171 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/dot-field.template.tsx +203 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/flow-field.template.tsx +263 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/node-shape.template.tsx +261 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/shape-sampler.ts +281 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/stipple-morph.template.tsx +167 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/stipple.template.tsx +175 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/trail-ghost.template.tsx +266 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/usage-examples.md +320 -0
- package/bundles/antigravity-plugin/skills/playwright/API_REFERENCE.md +653 -0
- package/bundles/antigravity-plugin/skills/playwright/SKILL.md +454 -0
- package/bundles/antigravity-plugin/skills/playwright/lib/helpers.js +441 -0
- package/bundles/antigravity-plugin/skills/playwright/package.json +26 -0
- package/bundles/antigravity-plugin/skills/playwright/run.js +228 -0
- package/bundles/antigravity-plugin/skills/prd/SKILL.md +107 -0
- package/bundles/antigravity-plugin/skills/preflight/SKILL.md +435 -0
- package/bundles/antigravity-plugin/skills/python/SKILL.md +388 -0
- package/bundles/antigravity-plugin/skills/react-best-practices/SKILL.md +269 -0
- package/bundles/antigravity-plugin/skills/react-dev/README.md +404 -0
- package/bundles/antigravity-plugin/skills/react-dev/SKILL.md +459 -0
- package/bundles/antigravity-plugin/skills/react-dev/examples/generic-components.md +579 -0
- package/bundles/antigravity-plugin/skills/react-dev/examples/server-components.md +579 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/event-handlers.md +574 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/hooks.md +456 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/react-19-patterns.md +638 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/react-router.md +1002 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/tanstack-router.md +587 -0
- package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +486 -0
- package/bundles/antigravity-plugin/skills/resume/SKILL.md +36 -0
- package/bundles/antigravity-plugin/skills/security-review/SKILL.md +196 -0
- package/bundles/antigravity-plugin/skills/setup-project-ai/SKILL.md +61 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +107 -0
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +103 -0
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +102 -0
- package/bundles/antigravity-plugin/skills/skill-creator/LICENSE.txt +202 -0
- package/bundles/antigravity-plugin/skills/skill-creator/SKILL.md +491 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/analyzer.md +274 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/comparator.md +202 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/grader.md +223 -0
- package/bundles/antigravity-plugin/skills/skill-creator/assets/eval_review.html +146 -0
- package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/bundles/antigravity-plugin/skills/skill-creator/references/schemas.md +430 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/__init__.py +0 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/generate_report.py +326 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/improve_description.py +247 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/package_skill.py +136 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_eval.py +310 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_loop.py +328 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/utils.py +47 -0
- package/bundles/antigravity-plugin/skills/sql/SKILL.md +321 -0
- package/bundles/antigravity-plugin/skills/tdd/SKILL.md +37 -0
- package/bundles/antigravity-plugin/skills/tdd-workflow/SKILL.md +188 -0
- package/bundles/antigravity-plugin/skills/technical-writing/SKILL.md +286 -0
- package/bundles/antigravity-plugin/skills/test-strategy/SKILL.md +155 -0
- package/bundles/antigravity-plugin/skills/ui-brief/SKILL.md +84 -0
- package/bundles/antigravity-plugin/skills/ui-review/SKILL.md +176 -0
- package/bundles/antigravity-plugin/skills/ui-review/references/framework-fixes.md +471 -0
- package/bundles/antigravity-plugin/skills/ui-review/references/visual-checklist.md +236 -0
- package/bundles/antigravity-plugin/skills/usage-review/SKILL.md +77 -0
- package/bundles/antigravity-plugin/skills/use-model/SKILL.md +64 -0
- package/bundles/antigravity-plugin/skills/using-git-worktrees/SKILL.md +217 -0
- package/bundles/antigravity-plugin/skills/version/SKILL.md +18 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/SKILL.md +732 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/LICENSE.txt +202 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/SKILL.md +97 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/console_logging.py +35 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/scripts/with_server.py +106 -0
- package/bundles/antigravity-plugin/skills/windows-deployment/SKILL.md +880 -0
- package/bundles/antigravity-plugin/skills/writing-plans/SKILL.md +384 -0
- package/bundles/antigravity-plugin/statusline-command-agy.sh +91 -0
- package/bundles/antigravity-plugin/warm_start_agy.py +149 -0
- package/bundles/manifest.json +6 -0
- package/dist/cli.js +5363 -0
- package/package.json +61 -4
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// playwright-helpers.js
|
|
2
|
+
// Reusable utility functions for Playwright automation
|
|
3
|
+
|
|
4
|
+
const { chromium, firefox, webkit } = require('playwright');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse extra HTTP headers from environment variables.
|
|
8
|
+
* Supports two formats:
|
|
9
|
+
* - PW_HEADER_NAME + PW_HEADER_VALUE: Single header (simple, common case)
|
|
10
|
+
* - PW_EXTRA_HEADERS: JSON object for multiple headers (advanced)
|
|
11
|
+
* Single header format takes precedence if both are set.
|
|
12
|
+
* @returns {Object|null} Headers object or null if none configured
|
|
13
|
+
*/
|
|
14
|
+
function getExtraHeadersFromEnv() {
|
|
15
|
+
const headerName = process.env.PW_HEADER_NAME;
|
|
16
|
+
const headerValue = process.env.PW_HEADER_VALUE;
|
|
17
|
+
|
|
18
|
+
if (headerName && headerValue) {
|
|
19
|
+
return { [headerName]: headerValue };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const headersJson = process.env.PW_EXTRA_HEADERS;
|
|
23
|
+
if (headersJson) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(headersJson);
|
|
26
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
console.warn('PW_EXTRA_HEADERS must be a JSON object, ignoring...');
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.warn('Failed to parse PW_EXTRA_HEADERS as JSON:', e.message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Launch browser with standard configuration
|
|
40
|
+
* @param {string} browserType - 'chromium', 'firefox', or 'webkit'
|
|
41
|
+
* @param {Object} options - Additional launch options
|
|
42
|
+
*/
|
|
43
|
+
async function launchBrowser(browserType = 'chromium', options = {}) {
|
|
44
|
+
const defaultOptions = {
|
|
45
|
+
headless: process.env.HEADLESS !== 'false',
|
|
46
|
+
slowMo: process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0,
|
|
47
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const browsers = { chromium, firefox, webkit };
|
|
51
|
+
const browser = browsers[browserType];
|
|
52
|
+
|
|
53
|
+
if (!browser) {
|
|
54
|
+
throw new Error(`Invalid browser type: ${browserType}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return await browser.launch({ ...defaultOptions, ...options });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a new page with viewport and user agent
|
|
62
|
+
* @param {Object} context - Browser context
|
|
63
|
+
* @param {Object} options - Page options
|
|
64
|
+
*/
|
|
65
|
+
async function createPage(context, options = {}) {
|
|
66
|
+
const page = await context.newPage();
|
|
67
|
+
|
|
68
|
+
if (options.viewport) {
|
|
69
|
+
await page.setViewportSize(options.viewport);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.userAgent) {
|
|
73
|
+
await page.setExtraHTTPHeaders({
|
|
74
|
+
'User-Agent': options.userAgent
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Set default timeout
|
|
79
|
+
page.setDefaultTimeout(options.timeout || 30000);
|
|
80
|
+
|
|
81
|
+
return page;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Smart wait for page to be ready
|
|
86
|
+
* @param {Object} page - Playwright page
|
|
87
|
+
* @param {Object} options - Wait options
|
|
88
|
+
*/
|
|
89
|
+
async function waitForPageReady(page, options = {}) {
|
|
90
|
+
const waitOptions = {
|
|
91
|
+
waitUntil: options.waitUntil || 'networkidle',
|
|
92
|
+
timeout: options.timeout || 30000
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await page.waitForLoadState(waitOptions.waitUntil, {
|
|
97
|
+
timeout: waitOptions.timeout
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.warn('Page load timeout, continuing...');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Additional wait for dynamic content if selector provided
|
|
104
|
+
if (options.waitForSelector) {
|
|
105
|
+
await page.waitForSelector(options.waitForSelector, {
|
|
106
|
+
timeout: options.timeout
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Safe click with retry logic
|
|
113
|
+
* @param {Object} page - Playwright page
|
|
114
|
+
* @param {string} selector - Element selector
|
|
115
|
+
* @param {Object} options - Click options
|
|
116
|
+
*/
|
|
117
|
+
async function safeClick(page, selector, options = {}) {
|
|
118
|
+
const maxRetries = options.retries || 3;
|
|
119
|
+
const retryDelay = options.retryDelay || 1000;
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
122
|
+
try {
|
|
123
|
+
await page.waitForSelector(selector, {
|
|
124
|
+
state: 'visible',
|
|
125
|
+
timeout: options.timeout || 5000
|
|
126
|
+
});
|
|
127
|
+
await page.click(selector, {
|
|
128
|
+
force: options.force || false,
|
|
129
|
+
timeout: options.timeout || 5000
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (i === maxRetries - 1) {
|
|
134
|
+
console.error(`Failed to click ${selector} after ${maxRetries} attempts`);
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
console.log(`Retry ${i + 1}/${maxRetries} for clicking ${selector}`);
|
|
138
|
+
await page.waitForTimeout(retryDelay);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Safe text input with clear before type
|
|
145
|
+
* @param {Object} page - Playwright page
|
|
146
|
+
* @param {string} selector - Input selector
|
|
147
|
+
* @param {string} text - Text to type
|
|
148
|
+
* @param {Object} options - Type options
|
|
149
|
+
*/
|
|
150
|
+
async function safeType(page, selector, text, options = {}) {
|
|
151
|
+
await page.waitForSelector(selector, {
|
|
152
|
+
state: 'visible',
|
|
153
|
+
timeout: options.timeout || 10000
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (options.clear !== false) {
|
|
157
|
+
await page.fill(selector, '');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (options.slow) {
|
|
161
|
+
await page.type(selector, text, { delay: options.delay || 100 });
|
|
162
|
+
} else {
|
|
163
|
+
await page.fill(selector, text);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Extract text from multiple elements
|
|
169
|
+
* @param {Object} page - Playwright page
|
|
170
|
+
* @param {string} selector - Elements selector
|
|
171
|
+
*/
|
|
172
|
+
async function extractTexts(page, selector) {
|
|
173
|
+
await page.waitForSelector(selector, { timeout: 10000 });
|
|
174
|
+
return await page.$$eval(selector, elements =>
|
|
175
|
+
elements.map(el => el.textContent?.trim()).filter(Boolean)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Take screenshot with timestamp
|
|
181
|
+
* @param {Object} page - Playwright page
|
|
182
|
+
* @param {string} name - Screenshot name
|
|
183
|
+
* @param {Object} options - Screenshot options
|
|
184
|
+
*/
|
|
185
|
+
async function takeScreenshot(page, name, options = {}) {
|
|
186
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
187
|
+
const filename = `${name}-${timestamp}.png`;
|
|
188
|
+
|
|
189
|
+
await page.screenshot({
|
|
190
|
+
path: filename,
|
|
191
|
+
fullPage: options.fullPage !== false,
|
|
192
|
+
...options
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
console.log(`Screenshot saved: ${filename}`);
|
|
196
|
+
return filename;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Handle authentication
|
|
201
|
+
* @param {Object} page - Playwright page
|
|
202
|
+
* @param {Object} credentials - Username and password
|
|
203
|
+
* @param {Object} selectors - Login form selectors
|
|
204
|
+
*/
|
|
205
|
+
async function authenticate(page, credentials, selectors = {}) {
|
|
206
|
+
const defaultSelectors = {
|
|
207
|
+
username: 'input[name="username"], input[name="email"], #username, #email',
|
|
208
|
+
password: 'input[name="password"], #password',
|
|
209
|
+
submit: 'button[type="submit"], input[type="submit"], button:has-text("Login"), button:has-text("Sign in")'
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const finalSelectors = { ...defaultSelectors, ...selectors };
|
|
213
|
+
|
|
214
|
+
await safeType(page, finalSelectors.username, credentials.username);
|
|
215
|
+
await safeType(page, finalSelectors.password, credentials.password);
|
|
216
|
+
await safeClick(page, finalSelectors.submit);
|
|
217
|
+
|
|
218
|
+
// Wait for navigation or success indicator
|
|
219
|
+
await Promise.race([
|
|
220
|
+
page.waitForNavigation({ waitUntil: 'networkidle' }),
|
|
221
|
+
page.waitForSelector(selectors.successIndicator || '.dashboard, .user-menu, .logout', { timeout: 10000 })
|
|
222
|
+
]).catch(() => {
|
|
223
|
+
console.log('Login might have completed without navigation');
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Scroll page
|
|
229
|
+
* @param {Object} page - Playwright page
|
|
230
|
+
* @param {string} direction - 'down', 'up', 'top', 'bottom'
|
|
231
|
+
* @param {number} distance - Pixels to scroll (for up/down)
|
|
232
|
+
*/
|
|
233
|
+
async function scrollPage(page, direction = 'down', distance = 500) {
|
|
234
|
+
switch (direction) {
|
|
235
|
+
case 'down':
|
|
236
|
+
await page.evaluate(d => window.scrollBy(0, d), distance);
|
|
237
|
+
break;
|
|
238
|
+
case 'up':
|
|
239
|
+
await page.evaluate(d => window.scrollBy(0, -d), distance);
|
|
240
|
+
break;
|
|
241
|
+
case 'top':
|
|
242
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
243
|
+
break;
|
|
244
|
+
case 'bottom':
|
|
245
|
+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
await page.waitForTimeout(500); // Wait for scroll animation
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Extract table data
|
|
253
|
+
* @param {Object} page - Playwright page
|
|
254
|
+
* @param {string} tableSelector - Table selector
|
|
255
|
+
*/
|
|
256
|
+
async function extractTableData(page, tableSelector) {
|
|
257
|
+
await page.waitForSelector(tableSelector);
|
|
258
|
+
|
|
259
|
+
return await page.evaluate((selector) => {
|
|
260
|
+
const table = document.querySelector(selector);
|
|
261
|
+
if (!table) return null;
|
|
262
|
+
|
|
263
|
+
const headers = Array.from(table.querySelectorAll('thead th')).map(th =>
|
|
264
|
+
th.textContent?.trim()
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const rows = Array.from(table.querySelectorAll('tbody tr')).map(tr => {
|
|
268
|
+
const cells = Array.from(tr.querySelectorAll('td'));
|
|
269
|
+
if (headers.length > 0) {
|
|
270
|
+
return cells.reduce((obj, cell, index) => {
|
|
271
|
+
obj[headers[index] || `column_${index}`] = cell.textContent?.trim();
|
|
272
|
+
return obj;
|
|
273
|
+
}, {});
|
|
274
|
+
} else {
|
|
275
|
+
return cells.map(cell => cell.textContent?.trim());
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return { headers, rows };
|
|
280
|
+
}, tableSelector);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Wait for and dismiss cookie banners
|
|
285
|
+
* @param {Object} page - Playwright page
|
|
286
|
+
* @param {number} timeout - Max time to wait
|
|
287
|
+
*/
|
|
288
|
+
async function handleCookieBanner(page, timeout = 3000) {
|
|
289
|
+
const commonSelectors = [
|
|
290
|
+
'button:has-text("Accept")',
|
|
291
|
+
'button:has-text("Accept all")',
|
|
292
|
+
'button:has-text("OK")',
|
|
293
|
+
'button:has-text("Got it")',
|
|
294
|
+
'button:has-text("I agree")',
|
|
295
|
+
'.cookie-accept',
|
|
296
|
+
'#cookie-accept',
|
|
297
|
+
'[data-testid="cookie-accept"]'
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
for (const selector of commonSelectors) {
|
|
301
|
+
try {
|
|
302
|
+
const element = await page.waitForSelector(selector, {
|
|
303
|
+
timeout: timeout / commonSelectors.length,
|
|
304
|
+
state: 'visible'
|
|
305
|
+
});
|
|
306
|
+
if (element) {
|
|
307
|
+
await element.click();
|
|
308
|
+
console.log('Cookie banner dismissed');
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
} catch (e) {
|
|
312
|
+
// Continue to next selector
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Retry a function with exponential backoff
|
|
321
|
+
* @param {Function} fn - Function to retry
|
|
322
|
+
* @param {number} maxRetries - Maximum retry attempts
|
|
323
|
+
* @param {number} initialDelay - Initial delay in ms
|
|
324
|
+
*/
|
|
325
|
+
async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
|
|
326
|
+
let lastError;
|
|
327
|
+
|
|
328
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
329
|
+
try {
|
|
330
|
+
return await fn();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
lastError = error;
|
|
333
|
+
const delay = initialDelay * Math.pow(2, i);
|
|
334
|
+
console.log(`Attempt ${i + 1} failed, retrying in ${delay}ms...`);
|
|
335
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
throw lastError;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Create browser context with common settings
|
|
344
|
+
* @param {Object} browser - Browser instance
|
|
345
|
+
* @param {Object} options - Context options
|
|
346
|
+
*/
|
|
347
|
+
async function createContext(browser, options = {}) {
|
|
348
|
+
const envHeaders = getExtraHeadersFromEnv();
|
|
349
|
+
|
|
350
|
+
// Merge environment headers with any passed in options
|
|
351
|
+
const mergedHeaders = {
|
|
352
|
+
...envHeaders,
|
|
353
|
+
...options.extraHTTPHeaders
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const defaultOptions = {
|
|
357
|
+
viewport: { width: 1280, height: 720 },
|
|
358
|
+
userAgent: options.mobile
|
|
359
|
+
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1'
|
|
360
|
+
: undefined,
|
|
361
|
+
permissions: options.permissions || [],
|
|
362
|
+
geolocation: options.geolocation,
|
|
363
|
+
locale: options.locale || 'en-US',
|
|
364
|
+
timezoneId: options.timezoneId || 'America/New_York',
|
|
365
|
+
// Only include extraHTTPHeaders if we have any
|
|
366
|
+
...(Object.keys(mergedHeaders).length > 0 && { extraHTTPHeaders: mergedHeaders })
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
return await browser.newContext({ ...defaultOptions, ...options });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Detect running dev servers on common ports
|
|
374
|
+
* @param {Array<number>} customPorts - Additional ports to check
|
|
375
|
+
* @returns {Promise<Array>} Array of detected server URLs
|
|
376
|
+
*/
|
|
377
|
+
async function detectDevServers(customPorts = []) {
|
|
378
|
+
const http = require('http');
|
|
379
|
+
|
|
380
|
+
// Common dev server ports
|
|
381
|
+
const commonPorts = [3000, 3001, 3002, 5173, 8080, 8000, 4200, 5000, 9000, 1234];
|
|
382
|
+
const allPorts = [...new Set([...commonPorts, ...customPorts])];
|
|
383
|
+
|
|
384
|
+
const detectedServers = [];
|
|
385
|
+
|
|
386
|
+
console.log('🔍 Checking for running dev servers...');
|
|
387
|
+
|
|
388
|
+
for (const port of allPorts) {
|
|
389
|
+
try {
|
|
390
|
+
await new Promise((resolve, reject) => {
|
|
391
|
+
const req = http.request({
|
|
392
|
+
hostname: 'localhost',
|
|
393
|
+
port: port,
|
|
394
|
+
path: '/',
|
|
395
|
+
method: 'HEAD',
|
|
396
|
+
timeout: 500
|
|
397
|
+
}, (res) => {
|
|
398
|
+
if (res.statusCode < 500) {
|
|
399
|
+
detectedServers.push(`http://localhost:${port}`);
|
|
400
|
+
console.log(` ✅ Found server on port ${port}`);
|
|
401
|
+
}
|
|
402
|
+
resolve();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
req.on('error', () => resolve());
|
|
406
|
+
req.on('timeout', () => {
|
|
407
|
+
req.destroy();
|
|
408
|
+
resolve();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
req.end();
|
|
412
|
+
});
|
|
413
|
+
} catch (e) {
|
|
414
|
+
// Port not available, continue
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (detectedServers.length === 0) {
|
|
419
|
+
console.log(' ❌ No dev servers detected');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return detectedServers;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = {
|
|
426
|
+
launchBrowser,
|
|
427
|
+
createPage,
|
|
428
|
+
waitForPageReady,
|
|
429
|
+
safeClick,
|
|
430
|
+
safeType,
|
|
431
|
+
extractTexts,
|
|
432
|
+
takeScreenshot,
|
|
433
|
+
authenticate,
|
|
434
|
+
scrollPage,
|
|
435
|
+
extractTableData,
|
|
436
|
+
handleCookieBanner,
|
|
437
|
+
retryWithBackoff,
|
|
438
|
+
createContext,
|
|
439
|
+
detectDevServers,
|
|
440
|
+
getExtraHeadersFromEnv
|
|
441
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "playwright-skill",
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "General-purpose browser automation with Playwright for Claude Code with auto-detection and smart test management",
|
|
5
|
+
"author": "lackeyjb",
|
|
6
|
+
"main": "run.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"setup": "npm install && npx playwright install chromium",
|
|
9
|
+
"install-all-browsers": "npx playwright install chromium firefox webkit"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"playwright",
|
|
13
|
+
"automation",
|
|
14
|
+
"browser-testing",
|
|
15
|
+
"web-automation",
|
|
16
|
+
"claude-skill",
|
|
17
|
+
"general-purpose"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"playwright": "^1.57.0"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=14.0.0"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Universal Playwright Executor for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Executes Playwright automation code from:
|
|
6
|
+
* - File path: node run.js script.js
|
|
7
|
+
* - Inline code: node run.js 'await page.goto("...")'
|
|
8
|
+
* - Stdin: cat script.js | node run.js
|
|
9
|
+
*
|
|
10
|
+
* Ensures proper module resolution by running from skill directory.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
// Change to skill directory for proper module resolution
|
|
18
|
+
process.chdir(__dirname);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if Playwright is installed
|
|
22
|
+
*/
|
|
23
|
+
function checkPlaywrightInstalled() {
|
|
24
|
+
try {
|
|
25
|
+
require.resolve('playwright');
|
|
26
|
+
return true;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Install Playwright if missing
|
|
34
|
+
*/
|
|
35
|
+
function installPlaywright() {
|
|
36
|
+
console.log('📦 Playwright not found. Installing...');
|
|
37
|
+
try {
|
|
38
|
+
execSync('npm install', { stdio: 'inherit', cwd: __dirname });
|
|
39
|
+
execSync('npx playwright install chromium', { stdio: 'inherit', cwd: __dirname });
|
|
40
|
+
console.log('✅ Playwright installed successfully');
|
|
41
|
+
return true;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error('❌ Failed to install Playwright:', e.message);
|
|
44
|
+
console.error('Please run manually: cd', __dirname, '&& npm run setup');
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get code to execute from various sources
|
|
51
|
+
*/
|
|
52
|
+
function getCodeToExecute() {
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
|
|
55
|
+
// Case 1: File path provided
|
|
56
|
+
if (args.length > 0 && fs.existsSync(args[0])) {
|
|
57
|
+
const filePath = path.resolve(args[0]);
|
|
58
|
+
console.log(`📄 Executing file: ${filePath}`);
|
|
59
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Case 2: Inline code provided as argument
|
|
63
|
+
if (args.length > 0) {
|
|
64
|
+
console.log('⚡ Executing inline code');
|
|
65
|
+
return args.join(' ');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Case 3: Code from stdin
|
|
69
|
+
if (!process.stdin.isTTY) {
|
|
70
|
+
console.log('📥 Reading from stdin');
|
|
71
|
+
return fs.readFileSync(0, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// No input
|
|
75
|
+
console.error('❌ No code to execute');
|
|
76
|
+
console.error('Usage:');
|
|
77
|
+
console.error(' node run.js script.js # Execute file');
|
|
78
|
+
console.error(' node run.js "code here" # Execute inline');
|
|
79
|
+
console.error(' cat script.js | node run.js # Execute from stdin');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clean up old temporary execution files from previous runs
|
|
85
|
+
*/
|
|
86
|
+
function cleanupOldTempFiles() {
|
|
87
|
+
try {
|
|
88
|
+
const files = fs.readdirSync(__dirname);
|
|
89
|
+
const tempFiles = files.filter(f => f.startsWith('.temp-execution-') && f.endsWith('.js'));
|
|
90
|
+
|
|
91
|
+
if (tempFiles.length > 0) {
|
|
92
|
+
tempFiles.forEach(file => {
|
|
93
|
+
const filePath = path.join(__dirname, file);
|
|
94
|
+
try {
|
|
95
|
+
fs.unlinkSync(filePath);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// Ignore errors - file might be in use or already deleted
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// Ignore directory read errors
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Wrap code in async IIFE if not already wrapped
|
|
108
|
+
*/
|
|
109
|
+
function wrapCodeIfNeeded(code) {
|
|
110
|
+
// Check if code already has require() and async structure
|
|
111
|
+
const hasRequire = code.includes('require(');
|
|
112
|
+
const hasAsyncIIFE = code.includes('(async () => {') || code.includes('(async()=>{');
|
|
113
|
+
|
|
114
|
+
// If it's already a complete script, return as-is
|
|
115
|
+
if (hasRequire && hasAsyncIIFE) {
|
|
116
|
+
return code;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// If it's just Playwright commands, wrap in full template
|
|
120
|
+
if (!hasRequire) {
|
|
121
|
+
return `
|
|
122
|
+
const { chromium, firefox, webkit, devices } = require('playwright');
|
|
123
|
+
const helpers = require('./lib/helpers');
|
|
124
|
+
|
|
125
|
+
// Extra headers from environment variables (if configured)
|
|
126
|
+
const __extraHeaders = helpers.getExtraHeadersFromEnv();
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Utility to merge environment headers into context options.
|
|
130
|
+
* Use when creating contexts with raw Playwright API instead of helpers.createContext().
|
|
131
|
+
* @param {Object} options - Context options
|
|
132
|
+
* @returns {Object} Options with extraHTTPHeaders merged in
|
|
133
|
+
*/
|
|
134
|
+
function getContextOptionsWithHeaders(options = {}) {
|
|
135
|
+
if (!__extraHeaders) return options;
|
|
136
|
+
return {
|
|
137
|
+
...options,
|
|
138
|
+
extraHTTPHeaders: {
|
|
139
|
+
...__extraHeaders,
|
|
140
|
+
...(options.extraHTTPHeaders || {})
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
(async () => {
|
|
146
|
+
try {
|
|
147
|
+
${code}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('❌ Automation error:', error.message);
|
|
150
|
+
if (error.stack) {
|
|
151
|
+
console.error(error.stack);
|
|
152
|
+
}
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// If has require but no async wrapper
|
|
160
|
+
if (!hasAsyncIIFE) {
|
|
161
|
+
return `
|
|
162
|
+
(async () => {
|
|
163
|
+
try {
|
|
164
|
+
${code}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('❌ Automation error:', error.message);
|
|
167
|
+
if (error.stack) {
|
|
168
|
+
console.error(error.stack);
|
|
169
|
+
}
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
173
|
+
`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return code;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Main execution
|
|
181
|
+
*/
|
|
182
|
+
async function main() {
|
|
183
|
+
console.log('🎭 Playwright Skill - Universal Executor\n');
|
|
184
|
+
|
|
185
|
+
// Clean up old temp files from previous runs
|
|
186
|
+
cleanupOldTempFiles();
|
|
187
|
+
|
|
188
|
+
// Check Playwright installation
|
|
189
|
+
if (!checkPlaywrightInstalled()) {
|
|
190
|
+
const installed = installPlaywright();
|
|
191
|
+
if (!installed) {
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get code to execute
|
|
197
|
+
const rawCode = getCodeToExecute();
|
|
198
|
+
const code = wrapCodeIfNeeded(rawCode);
|
|
199
|
+
|
|
200
|
+
// Create temporary file for execution
|
|
201
|
+
const tempFile = path.join(__dirname, `.temp-execution-${Date.now()}.js`);
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// Write code to temp file
|
|
205
|
+
fs.writeFileSync(tempFile, code, 'utf8');
|
|
206
|
+
|
|
207
|
+
// Execute the code
|
|
208
|
+
console.log('🚀 Starting automation...\n');
|
|
209
|
+
require(tempFile);
|
|
210
|
+
|
|
211
|
+
// Note: Temp file will be cleaned up on next run
|
|
212
|
+
// This allows long-running async operations to complete safely
|
|
213
|
+
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('❌ Execution failed:', error.message);
|
|
216
|
+
if (error.stack) {
|
|
217
|
+
console.error('\n📋 Stack trace:');
|
|
218
|
+
console.error(error.stack);
|
|
219
|
+
}
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Run main function
|
|
225
|
+
main().catch(error => {
|
|
226
|
+
console.error('❌ Fatal error:', error.message);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
});
|