@ranger-testing/ranger-cli 1.0.1 → 1.0.3

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.
@@ -0,0 +1,259 @@
1
+ ---
2
+ name: bug-basher
3
+ description: "Explores new features via browser to find bugs. Analyzes git diff to understand changes, generates exploration ideas, then systematically tests them to document any issues found."
4
+ tools: Glob, Grep, Read, Bash, mcp__ranger-browser__browser_navigate, mcp__ranger-browser__browser_snapshot, mcp__ranger-browser__browser_take_screenshot, mcp__ranger-browser__browser_click, mcp__ranger-browser__browser_type, mcp__ranger-browser__browser_hover, mcp__ranger-browser__browser_select_option, mcp__ranger-browser__browser_press_key, mcp__ranger-browser__browser_fill_form, mcp__ranger-browser__browser_wait_for, mcp__ranger-browser__browser_evaluate, mcp__ranger-browser__browser_console_messages, mcp__ranger-browser__browser_network_requests, mcp__ranger-browser__browser_tabs, mcp__ranger-browser__browser_navigate_back, mcp__ranger__get_product_docs
5
+ model: sonnet
6
+ color: red
7
+ ---
8
+
9
+ You are a Bug Basher agent. Your job is to explore newly developed features like a curious, slightly mischievous user would - clicking around, trying unexpected inputs, and hunting for bugs. Unlike quality advocates who verify specific flows work, you're an explorer looking for what might break.
10
+
11
+ You analyze the git diff to understand what changed, then systematically explore those areas in the browser to find issues before real users do.
12
+
13
+ # Your Workflow
14
+
15
+ ## Step 1: Analyze What Changed
16
+
17
+ First, understand the scope of changes to know where to focus your exploration:
18
+
19
+ 1. **Determine the default branch:**
20
+ ```bash
21
+ DEFAULT_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
22
+ ```
23
+
24
+ 2. **Get the diff against the default branch:**
25
+ ```bash
26
+ git diff $DEFAULT_BRANCH...HEAD --name-only # List changed files
27
+ git diff $DEFAULT_BRANCH...HEAD # Full diff for context
28
+ ```
29
+
30
+ 3. **Understand the changes:**
31
+ - Use `Read` to examine modified files in detail
32
+ - Focus on UI components, routes, API interactions, state management
33
+ - Identify:
34
+ - New features or pages added
35
+ - Existing features modified
36
+ - Components that interact with the changed code
37
+ - Edge cases implied by the code (error handlers, validation, conditionals)
38
+
39
+ 4. **Get product context:**
40
+ - Call `mcp__ranger__get_product_docs` to understand the broader product
41
+ - Map changed files to pages/features in the sitemap
42
+ - Understand how changes fit into the larger application
43
+
44
+ ## Step 2: Generate Exploration Ideas
45
+
46
+ Based on your analysis, create a list of things to explore. Think like a curious user and a skeptical tester:
47
+
48
+ ### Categories of Exploration
49
+
50
+ 1. **Happy Path Variations:**
51
+ - Different valid inputs (short, long, special characters, unicode)
52
+ - Different sequences of actions to achieve the same goal
53
+ - Different starting states
54
+
55
+ 2. **Edge Cases:**
56
+ - Empty states (no data, first-time user)
57
+ - Boundary conditions (max length, min values, limits)
58
+ - Rapid actions (double-click, fast typing, spam submit)
59
+
60
+ 3. **Error Conditions:**
61
+ - Invalid inputs (wrong format, missing required fields)
62
+ - Network issues (what happens if requests fail?)
63
+ - Permission/authentication edge cases
64
+
65
+ 4. **Integration Points:**
66
+ - How does this feature interact with others?
67
+ - Navigation to/from the feature
68
+ - State persistence across page refreshes
69
+ - Browser back/forward behavior
70
+
71
+ 5. **Visual & UX:**
72
+ - Responsive behavior (if applicable)
73
+ - Loading states and transitions
74
+ - Error message clarity
75
+ - Accessibility concerns
76
+
77
+ ### Present Your Exploration Plan
78
+
79
+ Before diving in, share your exploration plan with the user:
80
+ - What areas you'll focus on
81
+ - What kinds of issues you'll look for
82
+ - Estimated scope of exploration
83
+
84
+ Ask if there are specific areas they want you to prioritize or skip.
85
+
86
+ ## Step 3: Explore in the Browser
87
+
88
+ Now systematically work through your exploration ideas:
89
+
90
+ 1. **Navigate to the feature:**
91
+ - Use `browser_navigate` to go to the relevant page
92
+ - Use `browser_snapshot` to understand the current state
93
+
94
+ 2. **Explore methodically:**
95
+ - Work through your exploration ideas one by one
96
+ - Try unexpected things a real user might do
97
+ - Pay attention to:
98
+ - Console errors (`browser_console_messages`)
99
+ - Failed network requests (`browser_network_requests`)
100
+ - Unexpected UI states
101
+ - Missing feedback or confusing behavior
102
+
103
+ 3. **Document as you go:**
104
+ - Take screenshots of interesting states (`browser_take_screenshot`)
105
+ - Note any unexpected behavior, even if not clearly a bug
106
+ - Track what you've explored vs. what's remaining
107
+
108
+ 4. **Follow your curiosity:**
109
+ - If something seems off, dig deeper
110
+ - Try variations of actions that seem to cause issues
111
+ - Look for patterns in bugs (similar issues across features)
112
+
113
+ ## Step 4: Document Issues Found
114
+
115
+ For each issue discovered, document it clearly:
116
+
117
+ ### Issue Report Format
118
+
119
+ ```
120
+ ## Issue: [Brief Title]
121
+
122
+ **Severity:** Blocker | Major | Minor | Cosmetic
123
+
124
+ **Location:** [URL/Page/Component]
125
+
126
+ **Steps to Reproduce:**
127
+ 1. [Step 1]
128
+ 2. [Step 2]
129
+ 3. ...
130
+
131
+ **Expected Behavior:**
132
+ [What should happen]
133
+
134
+ **Actual Behavior:**
135
+ [What actually happens]
136
+
137
+ **Evidence:**
138
+ - Screenshot: [filename if taken]
139
+ - Console errors: [any relevant errors]
140
+ - Network failures: [any failed requests]
141
+
142
+ **Notes:**
143
+ [Any additional context, theories about cause, related issues]
144
+ ```
145
+
146
+ ### Severity Guidelines
147
+
148
+ - **Blocker:** Feature is completely broken, data loss, security issue
149
+ - **Major:** Feature partially works but core functionality impaired
150
+ - **Minor:** Feature works but has noticeable issues affecting UX
151
+ - **Cosmetic:** Visual glitches, typos, minor polish issues
152
+
153
+ ## Step 5: Summarize Findings
154
+
155
+ After exploration, provide a summary:
156
+
157
+ 1. **Overview:**
158
+ - How much of the feature was explored
159
+ - Overall quality assessment
160
+ - Number and severity of issues found
161
+
162
+ 2. **Issues by Severity:**
163
+ - List blockers first, then major, minor, cosmetic
164
+ - Group related issues together
165
+
166
+ 3. **Areas Not Explored:**
167
+ - What you didn't have time to test
168
+ - What needs different setup/data to test
169
+ - Suggested follow-up exploration
170
+
171
+ 4. **Positive Observations:**
172
+ - What works well
173
+ - Good UX patterns noticed
174
+ - Robust error handling found
175
+
176
+ # Guidelines
177
+
178
+ ## Think Like Different Users
179
+
180
+ - **The Hurried User:** Clicks fast, doesn't read instructions, takes shortcuts
181
+ - **The Confused User:** Doesn't understand the UI, tries wrong things first
182
+ - **The Thorough User:** Fills every field, reads every message, explores every option
183
+ - **The Malicious User:** Tries to break things, enters weird data, looks for exploits
184
+
185
+ ## Be Systematic but Creative
186
+
187
+ - Have a plan, but don't be afraid to deviate when something interesting appears
188
+ - Track what you've tested to avoid repetition
189
+ - Balance breadth (covering more areas) with depth (thoroughly testing one area)
190
+
191
+ ## Communicate Clearly
192
+
193
+ - Explain what you're about to explore before doing it
194
+ - Report issues as you find them, don't wait until the end
195
+ - Be specific about reproduction steps - someone else needs to reproduce this
196
+ - Screenshots are invaluable - take them liberally
197
+
198
+ ## Know When to Stop
199
+
200
+ - If you find a blocker, report it immediately - no point testing more
201
+ - If you've found several major issues, summarize and stop - feature needs work
202
+ - Time-box exploration if the feature is large
203
+
204
+ # Example Interaction
205
+
206
+ **User:** Explore the new settings page I just built
207
+
208
+ **Bug Basher:**
209
+
210
+ 1. Analyzes git diff to see what changed
211
+ 2. Reads changed files to understand the feature
212
+ 3. Gets product docs for context
213
+ 4. Presents exploration plan:
214
+
215
+ > "I've analyzed your changes. You've added a new Settings page with sections for Profile, Notifications, and Privacy. Here's my exploration plan:
216
+ >
217
+ > **Areas to explore:**
218
+ > 1. Profile editing - name, email, avatar upload
219
+ > 2. Notification toggles - all combinations, persistence
220
+ > 3. Privacy settings - data export, account deletion flow
221
+ > 4. Navigation - getting to/from settings, section switching
222
+ > 5. Edge cases - empty states, invalid inputs, rapid toggling
223
+ >
224
+ > I'll focus especially on the avatar upload (file handling is often buggy) and the account deletion flow (critical user action). Should I proceed, or would you like me to prioritize differently?"
225
+
226
+ 5. Explores the feature:
227
+
228
+ > "Starting exploration. Navigating to /settings..."
229
+ >
230
+ > [Takes snapshot, sees profile section]
231
+ >
232
+ > "Testing profile editing first. Trying a very long name (200 characters)..."
233
+ >
234
+ > [Types long name, clicks save]
235
+ >
236
+ > "**Issue Found:** The long name is accepted but truncates in the header display, cutting off mid-word. Taking screenshot..."
237
+ >
238
+ > "Moving on to avatar upload. Trying a large file (10MB)..."
239
+ >
240
+ > [Uploads file]
241
+ >
242
+ > "Upload seems to hang with no progress indicator. Checking network requests... The request is still pending after 30 seconds. Checking console... No errors, but no feedback to user either."
243
+ >
244
+ > "**Issue Found:** Large avatar uploads have no progress indicator and may timeout silently."
245
+
246
+ 6. Summarizes findings:
247
+
248
+ > "**Exploration Summary**
249
+ >
250
+ > Explored: Profile section (100%), Notifications (80%), Privacy (50%)
251
+ >
252
+ > **Issues Found: 4**
253
+ > - 1 Major: Avatar upload hangs on large files with no feedback
254
+ > - 2 Minor: Long names truncate poorly, notification toggle has no confirmation
255
+ > - 1 Cosmetic: Privacy section header misaligned on narrow viewport
256
+ >
257
+ > **Not Explored:** Account deletion (needs confirmation about test account), Data export (long-running operation)
258
+ >
259
+ > **Overall:** The feature is functional but needs polish. The avatar upload issue should be addressed before release. Would you like details on any specific issue?"
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: ui-verifier
3
+ description: "Lightweight agent that opens a browser and clicks through a UI flow to verify it works. Returns immediately when bugs are found. Provide a URL to visit and a detailed summary of the functionality to be verified."
4
+ tools: mcp__ranger-browser__browser_navigate, mcp__ranger-browser__browser_snapshot, mcp__ranger-browser__browser_take_screenshot, mcp__ranger-browser__browser_click, mcp__ranger-browser__browser_type, mcp__ranger-browser__browser_hover, mcp__ranger-browser__browser_select_option, mcp__ranger-browser__browser_press_key, mcp__ranger-browser__browser_fill_form, mcp__ranger-browser__browser_wait_for, mcp__ranger-browser__browser_console_messages, mcp__ranger-browser__browser_network_requests, mcp__ranger-browser__browser_tabs, mcp__ranger-browser__browser_navigate_back
5
+ model: opus
6
+ color: blue
7
+ ---
8
+
9
+ You are a UI Verifier agent. Your ONLY job is to click through a UI flow and report bugs immediately when you find them.
10
+
11
+ You do NOT:
12
+ - Analyze code or git diffs
13
+ - Suggest tests
14
+ - Draft test cases
15
+ - Do anything other than verify UI functionality
16
+
17
+ # Input
18
+
19
+ You will receive:
20
+ 1. A URL or starting point
21
+ 2. A description of the feature/flow to verify
22
+ 3. Expected behavior
23
+
24
+ # Your Workflow
25
+
26
+ ## 1. Navigate and Take Initial Snapshot
27
+
28
+ ```
29
+ browser_navigate → URL
30
+ browser_snapshot → see the page
31
+ ```
32
+
33
+ ## 2. Click Through the Flow
34
+
35
+ Follow the described user flow step by step:
36
+ - Use `browser_click`, `browser_type`, `browser_fill_form`, etc.
37
+ - Take `browser_snapshot` after each significant action
38
+ - Watch for anything that doesn't match expected behavior
39
+
40
+ ## 3. Return Immediately on Bugs
41
+
42
+ **CRITICAL:** As soon as you encounter a bug that blocks functionality or clearly doesn't match expectations:
43
+
44
+ 1. Take a screenshot with `browser_take_screenshot`
45
+ 2. Check `browser_console_messages` for errors
46
+ 3. **STOP and return to the main agent** with:
47
+ - What you were trying to do
48
+ - What you expected
49
+ - What actually happened
50
+ - The screenshot/evidence
51
+ - Severity: `BLOCKER` (can't proceed), `MAJOR` (wrong behavior), or `MINOR` (polish issue)
52
+
53
+ **Do NOT continue testing other parts of the flow.** The main agent needs to fix the issue first.
54
+
55
+ ## 4. If Everything Works
56
+
57
+ If you complete the entire flow without issues:
58
+ - Return a brief summary: "Verified [flow name]. All steps completed successfully."
59
+ - List the key actions you took
60
+
61
+ # Example Returns
62
+
63
+ ## Bug Found (return immediately)
64
+
65
+ ```
66
+ BUG FOUND - BLOCKER
67
+
68
+ Trying to: Submit the login form
69
+ Expected: Navigate to dashboard
70
+ Actual: Form submission does nothing, no error shown
71
+
72
+ Console errors:
73
+ - TypeError: Cannot read property 'submit' of undefined
74
+
75
+ Screenshot: [attached]
76
+
77
+ Recommend: Check the form submission handler
78
+ ```
79
+
80
+ ## Success
81
+
82
+ ```
83
+ VERIFIED - Login Flow
84
+
85
+ All steps completed:
86
+ 1. Navigated to /login
87
+ 2. Entered email and password
88
+ 3. Clicked submit
89
+ 4. Successfully redirected to /dashboard
90
+ 5. User name displayed correctly in header
91
+
92
+ No issues found.
93
+ ```
94
+
95
+ # Guidelines
96
+
97
+ - Be fast - don't over-test, just verify the described flow
98
+ - Be specific - exact error messages, exact steps
99
+ - Be visual - always include screenshots for bugs
100
+ - Return early - don't waste time if something is broken
package/build/cli.js CHANGED
@@ -1,130 +1,46 @@
1
1
  #!/usr/bin/env node
2
- import { readdir, mkdir, copyFile, writeFile } from 'fs/promises';
3
- import { join, resolve, dirname } from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { existsSync } from 'fs';
6
- import { execSync } from 'child_process';
7
2
  import yargs from 'yargs/yargs';
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = dirname(__filename);
10
- async function initAgents(targetDir, token, serverUrl) {
11
- const resolvedDir = resolve(targetDir);
12
- console.log(`Initializing Claude Code agents in: ${resolvedDir}`);
13
- // Validate API token by checking /me endpoint
14
- const mcpServerUrl = serverUrl ||
15
- process.env.MCP_SERVER_URL ||
16
- 'https://mcp-server-301751771437.us-central1.run.app';
17
- console.log('Validating API token...');
18
- try {
19
- const response = await fetch(`${mcpServerUrl}/me`, {
20
- method: 'GET',
21
- headers: {
22
- Authorization: `Bearer ${token}`,
23
- },
24
- });
25
- if (!response.ok) {
26
- console.error(`\n❌ Authentication failed: ${response.status} ${response.statusText}`);
27
- console.error('Please check your API token and try again.');
28
- process.exit(1);
29
- }
30
- console.log('✓ API token validated successfully');
31
- }
32
- catch (error) {
33
- console.error('\n❌ Failed to connect to MCP server:', error instanceof Error ? error.message : error);
34
- console.error('Please check your network connection and server URL.');
35
- process.exit(1);
36
- }
37
- // Create .claude/agents directory
38
- const claudeAgentsDir = join(resolvedDir, '.claude', 'agents');
39
- if (!existsSync(claudeAgentsDir)) {
40
- await mkdir(claudeAgentsDir, { recursive: true });
41
- console.log(`✓ Created directory: ${claudeAgentsDir}`);
42
- }
43
- // Copy all agent files from agents directory
44
- // When running tsx cli.ts: __dirname is packages/cli, so agents is './agents'
45
- // When built: __dirname is packages/cli/build, so agents is '../agents'
46
- const sourceAgentsDir = existsSync(join(__dirname, 'agents'))
47
- ? join(__dirname, 'agents')
48
- : join(__dirname, '..', 'agents');
49
- try {
50
- const agentFiles = await readdir(sourceAgentsDir);
51
- const mdFiles = agentFiles.filter((file) => file.endsWith('.md'));
52
- if (mdFiles.length === 0) {
53
- console.warn('Warning: No agent files found in source directory');
54
- }
55
- for (const file of mdFiles) {
56
- const sourcePath = join(sourceAgentsDir, file);
57
- const targetPath = join(claudeAgentsDir, file);
58
- await copyFile(sourcePath, targetPath);
59
- console.log(`✓ Created agent: ${file}`);
60
- }
61
- }
62
- catch (error) {
63
- console.error('Error copying agent files:', error);
64
- process.exit(1);
65
- }
66
- // Create .mcp.json config
67
- const mcpConfig = {
68
- mcpServers: {
69
- ranger: {
70
- type: 'http',
71
- url: `${mcpServerUrl}/mcp`,
72
- headers: {
73
- Authorization: `Bearer ${token}`,
74
- },
75
- },
76
- 'ranger-browser': {
77
- command: 'npx',
78
- args: ['@ranger-testing/playwright', 'run-mcp-server'],
79
- },
80
- },
81
- };
82
- const mcpConfigPath = join(resolvedDir, '.mcp.json');
83
- await writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
84
- console.log(`✓ Created .mcp.json configuration`);
85
- // Check that @ranger-testing/playwright is installed globally
86
- try {
87
- execSync('npm list -g @ranger-testing/playwright', {
88
- stdio: 'pipe',
89
- });
90
- console.log('✓ @ranger-testing/playwright is installed globally');
91
- }
92
- catch (error) {
93
- console.error('\n┌─────────────────────────────────────────────────────┐');
94
- console.error('│ ❌ Missing required dependency │');
95
- console.error('├─────────────────────────────────────────────────────┤');
96
- console.error('│ @ranger-testing/playwright is not installed. │');
97
- console.error('│ │');
98
- console.error('│ Please install it globally first: │');
99
- console.error('│ npm install -g @ranger-testing/playwright │');
100
- console.error('└─────────────────────────────────────────────────────┘\n');
101
- process.exit(1);
102
- }
103
- console.log('\n✅ Claude Code agents initialized successfully!');
104
- }
3
+ import { addEnv, start, useEnv, updateEnv } from './commands/index.js';
4
+ import dotenv from 'dotenv';
5
+ dotenv.config();
105
6
  // Setup yargs CLI
106
7
  yargs(process.argv.slice(2))
107
8
  .version('1.0.0')
108
- .command('init-agents [dir]', 'Initialize Claude Code agents in the specified directory', (yargs) => {
109
- return yargs
110
- .positional('dir', {
9
+ .command('add env <env-name>', 'Add environment configuration', (yargs) => {
10
+ return yargs.positional('env-name', {
111
11
  type: 'string',
112
- description: 'Target directory (defaults to current directory)',
113
- default: process.cwd(),
114
- })
115
- .option('token', {
116
- alias: 't',
12
+ description: 'Name of the environment (e.g., local, staging, prod)',
13
+ demandOption: true,
14
+ });
15
+ }, async (argv) => {
16
+ await addEnv(argv['env-name']);
17
+ })
18
+ .command('use <env-name>', 'Switch to using a specific environment', (yargs) => {
19
+ return yargs.positional('env-name', {
117
20
  type: 'string',
118
- description: 'Authentication token for the MCP server',
21
+ description: 'Name of the environment',
119
22
  demandOption: true,
120
- })
121
- .option('url', {
122
- alias: 'u',
23
+ });
24
+ }, async (argv) => {
25
+ await useEnv(argv['env-name']);
26
+ })
27
+ .command('update env <env-name>', 'Update authentication for an existing environment', (yargs) => {
28
+ return yargs.positional('env-name', {
123
29
  type: 'string',
124
- description: 'MCP server URL (defaults to MCP_SERVER_URL env or production server)',
30
+ description: 'Name of the environment to update',
31
+ demandOption: true,
32
+ });
33
+ }, async (argv) => {
34
+ await updateEnv(argv['env-name']);
35
+ })
36
+ .command('start <token>', 'Initialize Ranger in your project', (yargs) => {
37
+ return yargs.positional('token', {
38
+ type: 'string',
39
+ description: 'Ranger API token with MCP access',
40
+ demandOption: true,
125
41
  });
126
42
  }, async (argv) => {
127
- await initAgents(argv.dir, argv.token, argv.url);
43
+ await start(argv.token);
128
44
  })
129
45
  .demandCommand(1, 'You must specify a command')
130
46
  .help()
@@ -0,0 +1,21 @@
1
+ import { mkdir } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ export async function addApp(appName) {
5
+ const rangerDir = join(process.cwd(), '.ranger');
6
+ const appDir = join(rangerDir, appName);
7
+ // Create .ranger/ if needed
8
+ if (!existsSync(rangerDir)) {
9
+ await mkdir(rangerDir, { recursive: true });
10
+ console.log(`Created .ranger/ directory`);
11
+ }
12
+ // Create .ranger/<app-name>/
13
+ if (existsSync(appDir)) {
14
+ console.log(`App "${appName}" already exists at ${appDir}`);
15
+ return;
16
+ }
17
+ await mkdir(appDir, { recursive: true });
18
+ console.log(`\n✅ Created app: ${appName}`);
19
+ console.log(` Location: ${appDir}`);
20
+ console.log(`\nNext step: Run 'ranger-dev add env ${appName} local' to configure an environment.`);
21
+ }
@@ -0,0 +1,103 @@
1
+ import { mkdir, writeFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import inquirer from 'inquirer';
5
+ import { chromium } from 'playwright';
6
+ import { loadMcpConfig, saveMcpConfig, hasRangerServer, setRangerBrowser, } from './utils/mcpConfig.js';
7
+ import { installAgent } from './utils/agents.js';
8
+ export async function addEnv(envName) {
9
+ const envDir = join(process.cwd(), '.ranger', envName);
10
+ // Check if env already exists
11
+ if (existsSync(envDir)) {
12
+ const { overwrite } = await inquirer.prompt([
13
+ {
14
+ type: 'confirm',
15
+ name: 'overwrite',
16
+ message: `Environment "${envName}" already exists. Overwrite?`,
17
+ default: false,
18
+ },
19
+ ]);
20
+ if (!overwrite) {
21
+ console.log('Aborted.');
22
+ return;
23
+ }
24
+ }
25
+ // Prompt for auth requirement
26
+ const { requiresAuth } = await inquirer.prompt([
27
+ {
28
+ type: 'confirm',
29
+ name: 'requiresAuth',
30
+ message: 'Does this application require authentication?',
31
+ default: false,
32
+ },
33
+ ]);
34
+ // Create env directory
35
+ await mkdir(envDir, { recursive: true });
36
+ const authPath = join(envDir, 'auth.json');
37
+ let storageStatePath = undefined;
38
+ if (requiresAuth) {
39
+ console.log('\n📋 Authentication Setup');
40
+ console.log(' A browser will open. Please log in to your application.');
41
+ console.log(' When you are done logging in, close the browser.\n');
42
+ // Launch Playwright browser
43
+ const browser = await chromium.launch({ headless: false });
44
+ const context = await browser.newContext();
45
+ const page = await context.newPage();
46
+ await page.goto('about:blank');
47
+ // Wait for browser to be closed by user
48
+ storageStatePath = await new Promise((resolve) => {
49
+ // Save storage state when page closes (before context is destroyed)
50
+ page.on('close', async () => {
51
+ try {
52
+ console.log('Saving authentication state...');
53
+ await context.storageState({ path: authPath });
54
+ resolve(authPath);
55
+ }
56
+ catch {
57
+ // Context may already be closed
58
+ }
59
+ });
60
+ });
61
+ await browser.close();
62
+ console.log(`\n✓ Auth state saved to: ${authPath}`);
63
+ }
64
+ // Write config.json
65
+ const config = {
66
+ name: envName,
67
+ browser: {
68
+ isolated: true,
69
+ browserName: 'chromium',
70
+ launchOptions: {
71
+ headless: false,
72
+ },
73
+ contextOptions: {
74
+ permissions: ['clipboard-read', 'clipboard-write'],
75
+ extraHTTPHeaders: {},
76
+ storageState: storageStatePath || undefined,
77
+ },
78
+ },
79
+ capabilities: ['core', 'tabs', 'install', 'pdf', 'vision'],
80
+ imageResponses: 'allow',
81
+ saveTrace: true,
82
+ };
83
+ const configPath = join(envDir, 'config.json');
84
+ await writeFile(configPath, JSON.stringify(config, null, 2));
85
+ console.log(`✓ Created config: ${configPath}`);
86
+ // Load existing .mcp.json or create new one
87
+ const mcpConfig = await loadMcpConfig();
88
+ // Check if ranger server is configured
89
+ if (!hasRangerServer(mcpConfig)) {
90
+ console.error('\nRanger Tests MCP is not initialized.');
91
+ console.error(' To initialize, run: ranger start <token>');
92
+ process.exit(1);
93
+ }
94
+ // Update ranger-browser with the config path
95
+ setRangerBrowser(mcpConfig, configPath);
96
+ await saveMcpConfig(mcpConfig);
97
+ console.log(`✓ Updated .mcp.json configuration`);
98
+ // Copy browser-based agents to .claude/agents
99
+ await installAgent('quality-advocate');
100
+ await installAgent('bug-basher');
101
+ await installAgent('ui-verifier');
102
+ console.log(`\n✅ Environment "${envName}" configured`);
103
+ }
@@ -0,0 +1,4 @@
1
+ export { addEnv } from './addEnv.js';
2
+ export { start } from './start.js';
3
+ export { useEnv } from './useEnv.js';
4
+ export { updateEnv } from './updateEnv.js';
@@ -0,0 +1,84 @@
1
+ import { readdir, mkdir, copyFile } from 'fs/promises';
2
+ import { join, resolve, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { existsSync, readdirSync } from 'fs';
5
+ import { execSync } from 'child_process';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ export async function initAgents(targetDir) {
9
+ const resolvedDir = resolve(targetDir);
10
+ // Check that .ranger/<app>/local exists
11
+ const rangerDir = join(resolvedDir, '.ranger');
12
+ if (!existsSync(rangerDir)) {
13
+ console.error('\n❌ No .ranger directory found.');
14
+ console.error(' Run first: ranger-dev add app <app-name>');
15
+ console.error(' Then: ranger-dev add env <app-name> local');
16
+ process.exit(1);
17
+ }
18
+ // Check for at least one app with a local env
19
+ const apps = readdirSync(rangerDir, { withFileTypes: true })
20
+ .filter((d) => d.isDirectory())
21
+ .map((d) => d.name);
22
+ const hasLocalEnv = apps.some((app) => existsSync(join(rangerDir, app, 'local')));
23
+ if (!hasLocalEnv) {
24
+ console.error('\n❌ No local environment configured.');
25
+ console.error(' Run first: ranger-dev add env <app-name> local');
26
+ process.exit(1);
27
+ }
28
+ // Check that .mcp.json exists
29
+ const mcpConfigPath = join(resolvedDir, '.mcp.json');
30
+ if (!existsSync(mcpConfigPath)) {
31
+ console.error('\n❌ No .mcp.json found.');
32
+ console.error(' This should have been created when you ran: ranger-dev add env <app-name> local');
33
+ process.exit(1);
34
+ }
35
+ console.log(`Initializing Claude Code agents in: ${resolvedDir}`);
36
+ // Create .claude/agents directory
37
+ const claudeAgentsDir = join(resolvedDir, '.claude', 'agents');
38
+ if (!existsSync(claudeAgentsDir)) {
39
+ await mkdir(claudeAgentsDir, { recursive: true });
40
+ console.log(`✓ Created directory: ${claudeAgentsDir}`);
41
+ }
42
+ // Copy all agent files from agents directory
43
+ // When running tsx cli.ts: __dirname is packages/cli/commands, so agents is '../agents'
44
+ // When built: __dirname is packages/cli/build/commands, so agents is '../../agents'
45
+ const sourceAgentsDir = existsSync(join(__dirname, '..', 'agents'))
46
+ ? join(__dirname, '..', 'agents')
47
+ : join(__dirname, '..', '..', 'agents');
48
+ try {
49
+ const agentFiles = await readdir(sourceAgentsDir);
50
+ const mdFiles = agentFiles.filter((file) => file.endsWith('.md'));
51
+ if (mdFiles.length === 0) {
52
+ console.warn('Warning: No agent files found in source directory');
53
+ }
54
+ for (const file of mdFiles) {
55
+ const sourcePath = join(sourceAgentsDir, file);
56
+ const targetPath = join(claudeAgentsDir, file);
57
+ await copyFile(sourcePath, targetPath);
58
+ console.log(`✓ Created agent: ${file}`);
59
+ }
60
+ }
61
+ catch (error) {
62
+ console.error('Error copying agent files:', error);
63
+ process.exit(1);
64
+ }
65
+ // Check that @ranger-testing/playwright is installed globally
66
+ try {
67
+ execSync('npm list -g @ranger-testing/playwright', {
68
+ stdio: 'pipe',
69
+ });
70
+ console.log('✓ @ranger-testing/playwright is installed globally');
71
+ }
72
+ catch (error) {
73
+ console.error('\n┌─────────────────────────────────────────────────────┐');
74
+ console.error('│ ❌ Missing required dependency │');
75
+ console.error('├─────────────────────────────────────────────────────┤');
76
+ console.error('│ @ranger-testing/playwright is not installed. │');
77
+ console.error('│ │');
78
+ console.error('│ Please install it globally first: │');
79
+ console.error('│ npm install -g @ranger-testing/playwright │');
80
+ console.error('└─────────────────────────────────────────────────────┘\n');
81
+ process.exit(1);
82
+ }
83
+ console.log('\n✅ Claude Code agents initialized successfully!');
84
+ }
@@ -0,0 +1,72 @@
1
+ import { mkdir, readFile, appendFile, writeFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { loadMcpConfig, saveMcpConfig, setRangerServer, } from './utils/mcpConfig.js';
5
+ import { installAgent } from './utils/agents.js';
6
+ export async function start(token) {
7
+ console.log('\n🚀 Ranger Setup\n');
8
+ // Validate API token
9
+ const mcpServerUrl = process.env.MCP_SERVER_URL ||
10
+ 'https://mcp-server-301751771437.us-central1.run.app';
11
+ console.log('Validating API token...');
12
+ try {
13
+ const response = await fetch(`${mcpServerUrl}/me`, {
14
+ method: 'GET',
15
+ headers: {
16
+ Authorization: `Bearer ${token}`,
17
+ },
18
+ });
19
+ if (!response.ok) {
20
+ console.error(`\n❌ Authentication failed: ${response.status} ${response.statusText}`);
21
+ console.error('Please check your API token and try again.');
22
+ process.exit(1);
23
+ }
24
+ console.log('✓ API token validated successfully');
25
+ }
26
+ catch (error) {
27
+ console.error('\n❌ Failed to connect to MCP server:', error instanceof Error ? error.message : error);
28
+ console.error('Please check your network connection and server URL.');
29
+ process.exit(1);
30
+ }
31
+ // Load or create .mcp.json and set ranger server
32
+ const mcpConfig = await loadMcpConfig();
33
+ setRangerServer(mcpConfig, token, mcpServerUrl);
34
+ await saveMcpConfig(mcpConfig);
35
+ console.log('✓ Created/updated .mcp.json with Ranger MCP server');
36
+ // Create .ranger/ directory
37
+ const rangerDir = join(process.cwd(), '.ranger');
38
+ if (!existsSync(rangerDir)) {
39
+ await mkdir(rangerDir, { recursive: true });
40
+ console.log('✓ Created .ranger/ directory');
41
+ }
42
+ else {
43
+ console.log('✓ .ranger/ directory already exists');
44
+ }
45
+ // Copy e2e-test-recommender agent to .claude/agents
46
+ await installAgent('e2e-test-recommender');
47
+ // Update .gitignore
48
+ const gitignorePath = join(process.cwd(), '.gitignore');
49
+ const gitignoreEntries = ['.ranger-mcp', '.ranger/**/local*'];
50
+ if (existsSync(gitignorePath)) {
51
+ const existingContent = await readFile(gitignorePath, 'utf-8');
52
+ const missingEntries = gitignoreEntries.filter((entry) => !existingContent.includes(entry));
53
+ if (missingEntries.length > 0) {
54
+ const newContent = '\n# Ranger\n' + missingEntries.join('\n') + '\n';
55
+ await appendFile(gitignorePath, newContent);
56
+ console.log('✓ Updated .gitignore with Ranger entries');
57
+ }
58
+ else {
59
+ console.log('✓ .gitignore already contains Ranger entries');
60
+ }
61
+ }
62
+ else {
63
+ const content = '# Ranger\n' + gitignoreEntries.join('\n') + '\n';
64
+ await writeFile(gitignorePath, content);
65
+ console.log('✓ Created .gitignore with Ranger entries');
66
+ }
67
+ // Log next steps
68
+ console.log('\n✅ Ranger setup complete!\n');
69
+ console.log('Next steps:');
70
+ console.log(' Add an environment:');
71
+ console.log(' ranger add env local\n');
72
+ }
@@ -0,0 +1,59 @@
1
+ import { writeFile, readFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { chromium } from 'playwright';
5
+ export async function updateEnv(envName) {
6
+ const envDir = join(process.cwd(), '.ranger', envName);
7
+ const configPath = join(envDir, 'config.json');
8
+ const authPath = join(envDir, 'auth.json');
9
+ // Check if env exists
10
+ if (!existsSync(envDir)) {
11
+ console.error(`\nEnvironment "${envName}" not found.`);
12
+ console.error(` Run first: ranger add env ${envName}`);
13
+ process.exit(1);
14
+ }
15
+ // Check if config exists
16
+ if (!existsSync(configPath)) {
17
+ console.error(`\nConfig not found at ${configPath}`);
18
+ console.error(` Run first: ranger add env ${envName}`);
19
+ process.exit(1);
20
+ }
21
+ // Load existing config
22
+ const configContent = await readFile(configPath, 'utf-8');
23
+ const config = JSON.parse(configContent);
24
+ // Check if existing auth state exists
25
+ const hasExistingAuth = existsSync(authPath);
26
+ console.log('\n📋 Authentication Update');
27
+ console.log(' A browser will open with your existing session (if any).');
28
+ console.log(' Update your authentication as needed.');
29
+ console.log(' When you are done, close the browser.\n');
30
+ // Launch Playwright browser with existing storage state if available
31
+ const browser = await chromium.launch({ headless: false });
32
+ const contextOptions = hasExistingAuth ? { storageState: authPath } : {};
33
+ const context = await browser.newContext(contextOptions);
34
+ const page = await context.newPage();
35
+ await page.goto('about:blank');
36
+ // Wait for browser to be closed by user
37
+ await new Promise((resolve) => {
38
+ page.on('close', async () => {
39
+ try {
40
+ console.log('Saving authentication state...');
41
+ await context.storageState({ path: authPath });
42
+ resolve();
43
+ }
44
+ catch {
45
+ // Context may already be closed
46
+ resolve();
47
+ }
48
+ });
49
+ });
50
+ await browser.close();
51
+ console.log(`\n✓ Auth state saved to: ${authPath}`);
52
+ // Update config to reference the auth file
53
+ config.browser = config.browser || {};
54
+ config.browser.contextOptions = config.browser.contextOptions || {};
55
+ config.browser.contextOptions.storageState = authPath;
56
+ await writeFile(configPath, JSON.stringify(config, null, 2));
57
+ console.log(`✓ Updated config: ${configPath}`);
58
+ console.log(`\n✅ Environment "${envName}" authentication updated`);
59
+ }
@@ -0,0 +1,28 @@
1
+ import { join } from 'path';
2
+ import { existsSync } from 'fs';
3
+ import { loadMcpConfig, saveMcpConfig, setRangerBrowser, } from './utils/mcpConfig.js';
4
+ import { installAgent } from './utils/agents.js';
5
+ export async function useEnv(envName) {
6
+ const envDir = join(process.cwd(), '.ranger', envName);
7
+ const configPath = join(envDir, 'config.json');
8
+ // Check env exists
9
+ if (!existsSync(envDir)) {
10
+ console.error(`\n❌ Environment "${envName}" not found.`);
11
+ console.error(` Run first: ranger add env ${envName}`);
12
+ process.exit(1);
13
+ }
14
+ // Check config exists
15
+ if (!existsSync(configPath)) {
16
+ console.error(`\n❌ Config not found at ${configPath}`);
17
+ console.error(` Run first: ranger add env ${envName}`);
18
+ process.exit(1);
19
+ }
20
+ // Load and update MCP config
21
+ const mcpConfig = await loadMcpConfig();
22
+ setRangerBrowser(mcpConfig, configPath);
23
+ await saveMcpConfig(mcpConfig);
24
+ // Install browser-based agents if they don't exist
25
+ await installAgent('quality-advocate');
26
+ await installAgent('bug-basher');
27
+ console.log(`\n✅ Now using environment "${envName}"`);
28
+ }
@@ -0,0 +1,45 @@
1
+ import { mkdir, readdir, copyFile } from 'fs/promises';
2
+ import { join, dirname } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ function getSourceAgentsDir() {
8
+ // Check multiple possible locations for the agents directory
9
+ const possiblePaths = [
10
+ join(__dirname, '..', '..', 'agents'),
11
+ join(__dirname, '..', '..', '..', 'agents'),
12
+ ];
13
+ for (const p of possiblePaths) {
14
+ if (existsSync(p)) {
15
+ return p;
16
+ }
17
+ }
18
+ return possiblePaths[0];
19
+ }
20
+ export async function installAgent(agentName) {
21
+ const claudeAgentsDir = join(process.cwd(), '.claude', 'agents');
22
+ if (!existsSync(claudeAgentsDir)) {
23
+ await mkdir(claudeAgentsDir, { recursive: true });
24
+ }
25
+ const sourceAgentsDir = getSourceAgentsDir();
26
+ try {
27
+ const agentFiles = await readdir(sourceAgentsDir);
28
+ const agentFile = agentFiles.find((f) => f.includes(agentName));
29
+ if (agentFile) {
30
+ const sourcePath = join(sourceAgentsDir, agentFile);
31
+ const targetPath = join(claudeAgentsDir, agentFile);
32
+ await copyFile(sourcePath, targetPath);
33
+ console.log(`✓ Added agent: ${agentFile}`);
34
+ return true;
35
+ }
36
+ else {
37
+ console.warn(`Warning: ${agentName} agent not found in source directory`);
38
+ return false;
39
+ }
40
+ }
41
+ catch (error) {
42
+ console.error('Error copying agent file:', error);
43
+ return false;
44
+ }
45
+ }
@@ -0,0 +1,47 @@
1
+ import { readFile, writeFile } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ export function getMcpConfigPath() {
5
+ return join(process.cwd(), '.mcp.json');
6
+ }
7
+ export async function loadMcpConfig() {
8
+ const mcpConfigPath = getMcpConfigPath();
9
+ if (existsSync(mcpConfigPath)) {
10
+ try {
11
+ const existingConfig = await readFile(mcpConfigPath, 'utf-8');
12
+ return JSON.parse(existingConfig);
13
+ }
14
+ catch {
15
+ // If parse fails, start fresh
16
+ return { mcpServers: {} };
17
+ }
18
+ }
19
+ return { mcpServers: {} };
20
+ }
21
+ export async function saveMcpConfig(config) {
22
+ const mcpConfigPath = getMcpConfigPath();
23
+ await writeFile(mcpConfigPath, JSON.stringify(config, null, 2));
24
+ }
25
+ export function hasRangerServer(config) {
26
+ return !!config.mcpServers.ranger;
27
+ }
28
+ export function setRangerServer(config, token, serverUrl) {
29
+ config.mcpServers.ranger = {
30
+ type: 'http',
31
+ url: `${serverUrl}/mcp`,
32
+ headers: {
33
+ Authorization: `Bearer ${token}`,
34
+ },
35
+ };
36
+ }
37
+ export function setRangerBrowser(config, configPath) {
38
+ config.mcpServers['ranger-browser'] = {
39
+ command: 'npx',
40
+ args: [
41
+ '@ranger-testing/playwright',
42
+ 'run-mcp-server',
43
+ '--config',
44
+ configPath,
45
+ ],
46
+ };
47
+ }
@@ -0,0 +1,13 @@
1
+ import { chromium } from 'playwright';
2
+ async function main() {
3
+ const browser = await chromium.launch({ headless: false });
4
+ const context = await browser.newContext({
5
+ storageState: '/Users/adwithmukherjee/dev/lavender-core/lavender-core/.ranger/debug-tool/local/auth.json',
6
+ });
7
+ const page = await context.newPage();
8
+ await page.goto('http://localhost:3000');
9
+ // Keep browser open
10
+ console.log('Browser opened. Press Ctrl+C to close.');
11
+ await new Promise(() => { });
12
+ }
13
+ main().catch(console.error);
package/package.json CHANGED
@@ -1,20 +1,25 @@
1
1
  {
2
2
  "name": "@ranger-testing/ranger-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "bin": {
6
- "ranger": "./build/cli.js"
6
+ "ranger": "./build/cli.js",
7
+ "ranger-dev": "./build/cli.js"
7
8
  },
8
9
  "scripts": {
9
- "build": "tsc && chmod 755 build/cli.js",
10
- "dev": "tsx cli.ts"
10
+ "build": "tsc && cp -r src/agents build/ && chmod 755 build/cli.js",
11
+ "dev": "tsx src/cli.ts"
11
12
  },
12
- "files": ["build", "agents"],
13
+ "files": ["build"],
13
14
  "dependencies": {
14
- "zod": "^3.23.8",
15
- "yargs": "^17.7.2"
15
+ "dotenv": "^16.4.5",
16
+ "inquirer": "^9.2.12",
17
+ "playwright": "^1.40.0",
18
+ "yargs": "^17.7.2",
19
+ "zod": "^3.23.8"
16
20
  },
17
21
  "devDependencies": {
22
+ "@types/inquirer": "^9.0.7",
18
23
  "@types/node": "^22.0.0",
19
24
  "@types/yargs": "^17.0.32",
20
25
  "typescript": "^5.0.0"
File without changes