greenrun-cli 0.2.9 → 0.2.10

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.
@@ -50,7 +50,7 @@ export declare class ApiClient {
50
50
  name?: string;
51
51
  }): Promise<unknown>;
52
52
  deletePage(id: string): Promise<unknown>;
53
- listTests(projectId: string): Promise<unknown>;
53
+ listTests(projectId: string, compact?: boolean): Promise<unknown>;
54
54
  createTest(projectId: string, data: {
55
55
  name: string;
56
56
  instructions: string;
@@ -98,12 +98,10 @@ export declare class ApiClient {
98
98
  test_id: any;
99
99
  test_name: any;
100
100
  run_id: any;
101
- instructions: any;
102
101
  credential_name: any;
103
102
  pages: any;
104
103
  tags: any;
105
- script: any;
106
- script_generated_at: any;
104
+ has_script: any;
107
105
  }[];
108
106
  }>;
109
107
  }
@@ -61,8 +61,9 @@ export class ApiClient {
61
61
  return this.request('DELETE', `/pages/${id}`);
62
62
  }
63
63
  // Tests
64
- async listTests(projectId) {
65
- return this.request('GET', `/projects/${projectId}/tests`);
64
+ async listTests(projectId, compact) {
65
+ const query = compact ? '?compact=1' : '';
66
+ return this.request('GET', `/projects/${projectId}/tests${query}`);
66
67
  }
67
68
  async createTest(projectId, data) {
68
69
  return this.request('POST', `/projects/${projectId}/tests`, data);
@@ -106,7 +107,7 @@ export class ApiClient {
106
107
  async prepareTestBatch(projectId, filter, testIds) {
107
108
  const [projectResult, testsResult] = await Promise.all([
108
109
  this.getProject(projectId),
109
- this.listTests(projectId),
110
+ this.listTests(projectId, true),
110
111
  ]);
111
112
  const project = projectResult.project;
112
113
  let tests = (testsResult.tests || []).filter((t) => t.status === 'active');
@@ -127,44 +128,30 @@ export class ApiClient {
127
128
  tests = tests.filter((t) => (t.name || '').toLowerCase().includes(term));
128
129
  }
129
130
  }
131
+ const projectSummary = {
132
+ id: project.id, name: project.name, base_url: project.base_url,
133
+ auth_mode: project.auth_mode ?? 'none',
134
+ login_url: project.login_url ?? null,
135
+ register_url: project.register_url ?? null,
136
+ login_instructions: project.login_instructions ?? null,
137
+ register_instructions: project.register_instructions ?? null,
138
+ credentials: project.credentials ?? null,
139
+ };
130
140
  if (tests.length === 0) {
131
- return {
132
- project: {
133
- id: project.id, name: project.name, base_url: project.base_url,
134
- auth_mode: project.auth_mode ?? 'none',
135
- login_url: project.login_url ?? null,
136
- register_url: project.register_url ?? null,
137
- login_instructions: project.login_instructions ?? null,
138
- register_instructions: project.register_instructions ?? null,
139
- credentials: project.credentials ?? null,
140
- },
141
- tests: [],
142
- };
141
+ return { project: projectSummary, tests: [] };
143
142
  }
144
- // Fetch full test details in parallel
145
- const fullTests = await Promise.all(tests.map((t) => this.getTest(t.id)));
146
- // Start runs in parallel
143
+ // Start runs in parallel (listTests already has full details, no need for getTest)
147
144
  const runs = await Promise.all(tests.map((t) => this.startRun(t.id)));
148
145
  return {
149
- project: {
150
- id: project.id, name: project.name, base_url: project.base_url,
151
- auth_mode: project.auth_mode ?? 'none',
152
- login_url: project.login_url ?? null,
153
- register_url: project.register_url ?? null,
154
- login_instructions: project.login_instructions ?? null,
155
- register_instructions: project.register_instructions ?? null,
156
- credentials: project.credentials ?? null,
157
- },
158
- tests: fullTests.map((ft, i) => ({
159
- test_id: ft.test.id,
160
- test_name: ft.test.name,
146
+ project: projectSummary,
147
+ tests: tests.map((t, i) => ({
148
+ test_id: t.id,
149
+ test_name: t.name,
161
150
  run_id: runs[i].run.id,
162
- instructions: ft.test.instructions,
163
- credential_name: ft.test.credential_name ?? null,
164
- pages: ft.test.pages || [],
165
- tags: ft.test.tags || [],
166
- script: ft.test.script ?? null,
167
- script_generated_at: ft.test.script_generated_at ?? null,
151
+ credential_name: t.credential_name ?? null,
152
+ pages: (t.pages || []).map((p) => ({ id: p.id, url: p.url })),
153
+ tags: (t.tags || []).map((tg) => tg.name || tg),
154
+ has_script: t.has_script ?? !!t.script,
168
155
  })),
169
156
  };
170
157
  }
@@ -36,6 +36,43 @@ function prompt(rl, question) {
36
36
  });
37
37
  });
38
38
  }
39
+ function detectSystemChrome() {
40
+ const platform = process.platform;
41
+ if (platform === 'darwin') {
42
+ return existsSync('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
43
+ }
44
+ if (platform === 'win32') {
45
+ const dirs = [process.env['PROGRAMFILES'], process.env['PROGRAMFILES(X86)'], process.env['LOCALAPPDATA']];
46
+ return dirs.some(dir => dir && existsSync(join(dir, 'Google', 'Chrome', 'Application', 'chrome.exe')));
47
+ }
48
+ // Linux
49
+ try {
50
+ execSync('which google-chrome-stable || which google-chrome || which chromium-browser || which chromium', { stdio: 'pipe' });
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ function installPlaywrightChromium() {
58
+ try {
59
+ console.log(' Installing @playwright/test (this may take a minute)...');
60
+ execSync('npm install -g @playwright/test@latest', { stdio: 'inherit' });
61
+ console.log(' Installing Chromium browser...');
62
+ execSync('npx playwright install --with-deps chromium', { stdio: 'inherit' });
63
+ return true;
64
+ }
65
+ catch {
66
+ console.error(' Failed to install Playwright. You can install manually:');
67
+ console.error(' npm install -g @playwright/test@latest');
68
+ console.error(' npx playwright install --with-deps chromium\n');
69
+ return false;
70
+ }
71
+ }
72
+ function checkNodeVersion() {
73
+ const match = process.version.match(/^v(\d+)\./);
74
+ return match ? parseInt(match[1], 10) >= 18 : false;
75
+ }
39
76
  function checkPrerequisites() {
40
77
  let claude = false;
41
78
  try {
@@ -55,14 +92,15 @@ async function validateToken(token) {
55
92
  'Accept': 'application/json',
56
93
  },
57
94
  });
58
- if (!response.ok)
59
- return { valid: false };
95
+ if (!response.ok) {
96
+ return { valid: false, error: `API returned HTTP ${response.status}` };
97
+ }
60
98
  const data = await response.json();
61
99
  const projects = Array.isArray(data) ? data : (data.data ?? []);
62
100
  return { valid: true, projectCount: projects.length };
63
101
  }
64
- catch {
65
- return { valid: false };
102
+ catch (err) {
103
+ return { valid: false, error: err?.message || String(err) };
66
104
  }
67
105
  }
68
106
  function getClaudeConfigPath() {
@@ -107,24 +145,24 @@ function configureMcpLocal(token) {
107
145
  console.error(` claude mcp add greenrun --transport stdio -e GREENRUN_API_TOKEN=${token} -- npx -y greenrun-cli@latest\n`);
108
146
  }
109
147
  }
110
- function configurePlaywrightMcp() {
148
+ function configurePlaywrightMcp(browser = 'chrome') {
111
149
  try {
112
150
  setLocalMcpServer('playwright', {
113
151
  type: 'stdio',
114
152
  command: 'npx',
115
153
  args: [
116
154
  '@playwright/mcp@latest',
117
- '--browser', 'chrome',
155
+ '--browser', browser,
118
156
  '--user-data-dir', join(homedir(), '.greenrun', 'browser-profile'),
119
157
  ],
120
158
  env: {},
121
159
  });
122
- console.log(' Configured playwright MCP server');
160
+ console.log(` Configured playwright MCP server (${browser})`);
123
161
  }
124
162
  catch {
125
163
  console.error('\nFailed to write Playwright MCP config to ~/.claude.json');
126
164
  console.error('You can add it manually:\n');
127
- console.error(' claude mcp add playwright -- npx @playwright/mcp@latest --browser chrome --user-data-dir ~/.greenrun/browser-profile\n');
165
+ console.error(` claude mcp add playwright -- npx @playwright/mcp@latest --browser ${browser} --user-data-dir ~/.greenrun/browser-profile\n`);
128
166
  }
129
167
  }
130
168
  function configureMcpProject(token) {
@@ -287,6 +325,12 @@ export async function runInit(args) {
287
325
  const opts = parseFlags(args);
288
326
  const interactive = !opts.token;
289
327
  console.log('\nGreenrun - Browser Test Management for Claude Code\n');
328
+ // Node version gate
329
+ if (!checkNodeVersion()) {
330
+ console.error(`Error: Node.js 18 or later is required (detected ${process.version}).`);
331
+ console.error('Install a newer version: https://nodejs.org/\n');
332
+ process.exit(1);
333
+ }
290
334
  // Prerequisites
291
335
  console.log('Prerequisites:');
292
336
  const prereqs = checkPrerequisites();
@@ -318,7 +362,7 @@ export async function runInit(args) {
318
362
  process.stdout.write(' Validating... ');
319
363
  const validation = await validateToken(token);
320
364
  if (!validation.valid) {
321
- console.log('Failed! Invalid token or cannot reach the API.');
365
+ console.log(`Failed! ${validation.error || 'Invalid token or cannot reach the API.'}`);
322
366
  rl.close();
323
367
  process.exit(1);
324
368
  }
@@ -348,12 +392,37 @@ export async function runInit(args) {
348
392
  process.stdout.write('Validating token... ');
349
393
  const validation = await validateToken(token);
350
394
  if (!validation.valid) {
351
- console.log('Failed!');
395
+ console.log(`Failed! ${validation.error || 'Invalid token or cannot reach the API.'}`);
352
396
  process.exit(1);
353
397
  }
354
398
  console.log(`Connected! (${validation.projectCount} project${validation.projectCount === 1 ? '' : 's'} found)`);
355
399
  scope = scope || 'local';
356
400
  }
401
+ // Detect browser
402
+ let browser = 'chrome';
403
+ if (!detectSystemChrome()) {
404
+ if (interactive) {
405
+ const rl2 = createInterface({ input: process.stdin, output: process.stdout });
406
+ console.log('Chrome not detected on this system.');
407
+ const installChoice = await prompt(rl2, ' Install Playwright Chromium? [Y/n]: ');
408
+ rl2.close();
409
+ if (installChoice.toLowerCase() !== 'n') {
410
+ if (installPlaywrightChromium()) {
411
+ browser = 'chromium';
412
+ }
413
+ else {
414
+ console.log(' Continuing with chrome config. You can install Chrome manually later.\n');
415
+ }
416
+ }
417
+ }
418
+ else {
419
+ console.log('Chrome not detected. Installing Playwright Chromium...');
420
+ if (installPlaywrightChromium()) {
421
+ browser = 'chromium';
422
+ }
423
+ }
424
+ console.log();
425
+ }
357
426
  // Configure MCP
358
427
  console.log('Configuring MCP servers...');
359
428
  if (scope === 'project') {
@@ -362,7 +431,7 @@ export async function runInit(args) {
362
431
  else {
363
432
  configureMcpLocal(token);
364
433
  }
365
- configurePlaywrightMcp();
434
+ configurePlaywrightMcp(browser);
366
435
  console.log(' MCP servers configured.\n');
367
436
  // Install extras
368
437
  if (opts.claudeMd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greenrun-cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "CLI and MCP server for Greenrun - browser test management for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",