@eyeglass/cli 0.1.4 → 0.1.6
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/README.md +28 -7
- package/dist/index.js +368 -28
- package/dist/index.test.js +111 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,20 +8,38 @@ CLI for initializing [Eyeglass](https://github.com/donutboyband/eyeglass) in you
|
|
|
8
8
|
npx @eyeglass/cli init
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
This
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
3. **Configure** your bundler (Vite, Next.js, CRA, or Remix)
|
|
11
|
+
This will interactively prompt you to select which AI coding agents to configure:
|
|
12
|
+
- **Claude Code** - stdio MCP (`.claude/settings.json`)
|
|
13
|
+
- **GitHub Copilot CLI** - local MCP (`.copilot/mcp-config.json`)
|
|
14
|
+
- **OpenAI Codex CLI** - HTTP API (`.codex/eyeglass.md`)
|
|
16
15
|
|
|
17
16
|
## Options
|
|
18
17
|
|
|
19
18
|
```bash
|
|
19
|
+
# Interactive setup (prompts for agent selection)
|
|
20
|
+
npx @eyeglass/cli init
|
|
21
|
+
|
|
22
|
+
# Configure specific agents
|
|
23
|
+
npx @eyeglass/cli init --claude # Claude Code only
|
|
24
|
+
npx @eyeglass/cli init --copilot # GitHub Copilot CLI only
|
|
25
|
+
npx @eyeglass/cli init --codex # OpenAI Codex only
|
|
26
|
+
npx @eyeglass/cli init --claude --copilot # Multiple agents
|
|
27
|
+
|
|
28
|
+
# Other options
|
|
20
29
|
npx @eyeglass/cli init --dry-run # Preview changes without making them
|
|
21
30
|
npx @eyeglass/cli init --skip-install # Skip installing @eyeglass/inspector
|
|
22
31
|
npx @eyeglass/cli help # Show help
|
|
23
32
|
```
|
|
24
33
|
|
|
34
|
+
## What It Does
|
|
35
|
+
|
|
36
|
+
1. **Installs** `@eyeglass/inspector` as a dev dependency
|
|
37
|
+
2. **Creates** agent-specific config files:
|
|
38
|
+
- Claude: `.claude/settings.json` + `.claude/skills/eyeglass.md`
|
|
39
|
+
- Copilot CLI: `.copilot/mcp-config.json`
|
|
40
|
+
- Codex: `.codex/eyeglass.md` (HTTP API instructions)
|
|
41
|
+
3. **Configures** your bundler (Vite, Next.js, CRA, or Remix)
|
|
42
|
+
|
|
25
43
|
## Supported Frameworks
|
|
26
44
|
|
|
27
45
|
| Framework | Auto-Detection | Component Names | File Paths |
|
|
@@ -34,8 +52,11 @@ npx @eyeglass/cli help # Show help
|
|
|
34
52
|
## After Setup
|
|
35
53
|
|
|
36
54
|
1. Start your dev server (`npm run dev`)
|
|
37
|
-
2.
|
|
38
|
-
3.
|
|
55
|
+
2. Start the Eyeglass bridge: `npx eyeglass-bridge`
|
|
56
|
+
3. Start your AI coding agent:
|
|
57
|
+
- **Claude Code**: Run `claude` and say "watch eyeglass" or "/eyeglass"
|
|
58
|
+
- **Copilot CLI**: Run `gh copilot` in your project directory
|
|
59
|
+
- **Codex**: See `.codex/eyeglass.md` for HTTP API usage
|
|
39
60
|
4. Select elements in your browser and submit requests!
|
|
40
61
|
|
|
41
62
|
See the [main repo](https://github.com/donutboyband/eyeglass) for full documentation.
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import * as readline from 'readline';
|
|
4
5
|
import { execFileSync } from 'child_process';
|
|
5
6
|
// ============================================================================
|
|
6
7
|
// Templates
|
|
7
8
|
// ============================================================================
|
|
9
|
+
// ASCII art logo based on eyeglass.svg - glasses with cursor pointer in right lens
|
|
10
|
+
// SVG has: two round lenses, short vertical bridge, temples angling up-outward, pointer in right lens
|
|
11
|
+
const LOGO = `
|
|
12
|
+
\x1b[36m ╱ ╲
|
|
13
|
+
╲ ╭───────╮ ╭───────╮ ╱
|
|
14
|
+
│ │ │ \x1b[34m▲\x1b[36m │
|
|
15
|
+
│ │ │ │ \x1b[34m█▸\x1b[36m │
|
|
16
|
+
╰───────╯ ╰───────╯\x1b[0m
|
|
17
|
+
\x1b[1meyeglass\x1b[0m
|
|
18
|
+
`;
|
|
8
19
|
const NEXT_CONFIG_ADDITION = `
|
|
9
20
|
// Eyeglass configuration - Auto-generated by eyeglass init
|
|
10
21
|
const withEyeglass = (nextConfig) => {
|
|
@@ -26,14 +37,28 @@ const withEyeglass = (nextConfig) => {
|
|
|
26
37
|
};
|
|
27
38
|
};
|
|
28
39
|
`;
|
|
40
|
+
const MCP_STDIO_CONFIG = {
|
|
41
|
+
command: 'npx',
|
|
42
|
+
args: ['eyeglass-bridge'],
|
|
43
|
+
};
|
|
29
44
|
const CLAUDE_CONFIG = {
|
|
45
|
+
mcpServers: {
|
|
46
|
+
eyeglass: MCP_STDIO_CONFIG,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
// GitHub Copilot CLI agent config format (.copilot/mcp-config.json)
|
|
50
|
+
const COPILOT_MCP_CONFIG = {
|
|
30
51
|
mcpServers: {
|
|
31
52
|
eyeglass: {
|
|
53
|
+
type: 'local',
|
|
32
54
|
command: 'npx',
|
|
33
55
|
args: ['eyeglass-bridge'],
|
|
56
|
+
tools: ['*'],
|
|
34
57
|
},
|
|
35
58
|
},
|
|
36
59
|
};
|
|
60
|
+
// Codex CLI uses HTTP endpoints, so we just provide instructions
|
|
61
|
+
const CODEX_BASE_URL = 'http://localhost:3300/api';
|
|
37
62
|
const EYEGLASS_SKILL = `---
|
|
38
63
|
name: eyeglass
|
|
39
64
|
description: Listen for Eyeglass requests from the browser and make UI changes
|
|
@@ -120,6 +145,48 @@ function readFile(filePath) {
|
|
|
120
145
|
function writeFile(filePath, content) {
|
|
121
146
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
122
147
|
}
|
|
148
|
+
async function promptAgentSelection() {
|
|
149
|
+
const agents = [
|
|
150
|
+
{ key: '1', name: 'Claude Code', type: 'claude', selected: false },
|
|
151
|
+
{ key: '2', name: 'GitHub Copilot CLI', type: 'copilot', selected: false },
|
|
152
|
+
{ key: '3', name: 'OpenAI Codex CLI', type: 'codex', selected: false },
|
|
153
|
+
];
|
|
154
|
+
const rl = readline.createInterface({
|
|
155
|
+
input: process.stdin,
|
|
156
|
+
output: process.stdout,
|
|
157
|
+
});
|
|
158
|
+
const question = (prompt) => {
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
rl.question(prompt, resolve);
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
console.log('\n\x1b[1mSelect coding agents to configure:\x1b[0m');
|
|
164
|
+
console.log('\x1b[2m(Enter numbers separated by spaces, or "a" for all)\x1b[0m\n');
|
|
165
|
+
for (const agent of agents) {
|
|
166
|
+
console.log(` ${agent.key}) ${agent.name}`);
|
|
167
|
+
}
|
|
168
|
+
console.log('');
|
|
169
|
+
const answer = await question('\x1b[36m>\x1b[0m ');
|
|
170
|
+
rl.close();
|
|
171
|
+
const input = answer.trim().toLowerCase();
|
|
172
|
+
if (input === 'a' || input === 'all') {
|
|
173
|
+
return agents.map((a) => a.type);
|
|
174
|
+
}
|
|
175
|
+
const selected = [];
|
|
176
|
+
const parts = input.split(/[\s,]+/);
|
|
177
|
+
for (const part of parts) {
|
|
178
|
+
const agent = agents.find((a) => a.key === part);
|
|
179
|
+
if (agent && !selected.includes(agent.type)) {
|
|
180
|
+
selected.push(agent.type);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Default to Claude if nothing selected
|
|
184
|
+
if (selected.length === 0) {
|
|
185
|
+
log('No selection made, defaulting to Claude Code', 'warn');
|
|
186
|
+
return ['claude'];
|
|
187
|
+
}
|
|
188
|
+
return selected;
|
|
189
|
+
}
|
|
123
190
|
function detectPackageManager() {
|
|
124
191
|
const cwd = process.cwd();
|
|
125
192
|
if (fileExists(path.join(cwd, 'bun.lockb')))
|
|
@@ -200,6 +267,46 @@ function detectProject() {
|
|
|
200
267
|
return { type: 'unknown', typescript: hasTs };
|
|
201
268
|
}
|
|
202
269
|
// ============================================================================
|
|
270
|
+
// HTML Entry Point Detection
|
|
271
|
+
// ============================================================================
|
|
272
|
+
/**
|
|
273
|
+
* Parses index.html to find the entry script path.
|
|
274
|
+
* Looks for <script type="module" src="..."> tags and returns the first match.
|
|
275
|
+
* Works for vanilla Vite, React, Vue, and other projects that use index.html.
|
|
276
|
+
*/
|
|
277
|
+
function findEntryFromHtml(cwd) {
|
|
278
|
+
// Common locations for index.html
|
|
279
|
+
const htmlFiles = [
|
|
280
|
+
'index.html',
|
|
281
|
+
'public/index.html',
|
|
282
|
+
'src/index.html',
|
|
283
|
+
];
|
|
284
|
+
for (const htmlFile of htmlFiles) {
|
|
285
|
+
const htmlPath = path.join(cwd, htmlFile);
|
|
286
|
+
if (!fileExists(htmlPath))
|
|
287
|
+
continue;
|
|
288
|
+
const content = readFile(htmlPath);
|
|
289
|
+
// Match <script type="module" src="..."> - handles various quote styles and spacing
|
|
290
|
+
// This regex captures the src attribute value from module scripts
|
|
291
|
+
const moduleScriptRegex = /<script[^>]*type\s*=\s*["']module["'][^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
|
|
292
|
+
const altModuleScriptRegex = /<script[^>]*src\s*=\s*["']([^"']+)["'][^>]*type\s*=\s*["']module["'][^>]*>/gi;
|
|
293
|
+
let match = moduleScriptRegex.exec(content) || altModuleScriptRegex.exec(content);
|
|
294
|
+
if (match && match[1]) {
|
|
295
|
+
let scriptSrc = match[1];
|
|
296
|
+
// Remove leading slash if present (Vite uses /src/main.js format)
|
|
297
|
+
if (scriptSrc.startsWith('/')) {
|
|
298
|
+
scriptSrc = scriptSrc.slice(1);
|
|
299
|
+
}
|
|
300
|
+
// Resolve the path relative to the project root
|
|
301
|
+
const scriptPath = path.join(cwd, scriptSrc);
|
|
302
|
+
if (fileExists(scriptPath)) {
|
|
303
|
+
return scriptPath;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
// ============================================================================
|
|
203
310
|
// Config Modifiers
|
|
204
311
|
// ============================================================================
|
|
205
312
|
function setupEyeglassSkill(dryRun) {
|
|
@@ -268,6 +375,132 @@ function setupClaudeConfig(dryRun) {
|
|
|
268
375
|
}
|
|
269
376
|
return true;
|
|
270
377
|
}
|
|
378
|
+
function setupCopilotConfig(dryRun) {
|
|
379
|
+
const cwd = process.cwd();
|
|
380
|
+
const copilotDir = path.join(cwd, '.copilot');
|
|
381
|
+
const configPath = path.join(copilotDir, 'mcp-config.json');
|
|
382
|
+
if (fileExists(configPath)) {
|
|
383
|
+
try {
|
|
384
|
+
const existing = JSON.parse(readFile(configPath));
|
|
385
|
+
if (existing.mcpServers?.eyeglass) {
|
|
386
|
+
log('Copilot CLI MCP config already exists', 'success');
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
// Merge with existing config
|
|
390
|
+
const merged = {
|
|
391
|
+
...existing,
|
|
392
|
+
mcpServers: {
|
|
393
|
+
...existing.mcpServers,
|
|
394
|
+
...COPILOT_MCP_CONFIG.mcpServers,
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
if (dryRun) {
|
|
398
|
+
log('Would update .copilot/mcp-config.json with eyeglass MCP config');
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
writeFile(configPath, JSON.stringify(merged, null, 2));
|
|
402
|
+
log('Added eyeglass to existing Copilot CLI MCP config', 'success');
|
|
403
|
+
}
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
log('Could not parse existing .copilot/mcp-config.json', 'warn');
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (dryRun) {
|
|
412
|
+
log(`Would create ${configPath}`);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
if (!fileExists(copilotDir)) {
|
|
416
|
+
fs.mkdirSync(copilotDir, { recursive: true });
|
|
417
|
+
}
|
|
418
|
+
writeFile(configPath, JSON.stringify(COPILOT_MCP_CONFIG, null, 2));
|
|
419
|
+
log('Created .copilot/mcp-config.json for GitHub Copilot CLI', 'success');
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
function setupCodexConfig(dryRun) {
|
|
424
|
+
const cwd = process.cwd();
|
|
425
|
+
const codexDir = path.join(cwd, '.codex');
|
|
426
|
+
const configPath = path.join(codexDir, 'eyeglass.md');
|
|
427
|
+
const codexInstructions = `# Eyeglass Integration for Codex CLI
|
|
428
|
+
|
|
429
|
+
Eyeglass provides HTTP endpoints for interacting with UI elements selected in the browser.
|
|
430
|
+
|
|
431
|
+
## Base URL
|
|
432
|
+
\`${CODEX_BASE_URL}\`
|
|
433
|
+
|
|
434
|
+
## Endpoints
|
|
435
|
+
|
|
436
|
+
### Wait for Request (blocking)
|
|
437
|
+
\`\`\`
|
|
438
|
+
GET /api/wait?timeout=300000
|
|
439
|
+
\`\`\`
|
|
440
|
+
Blocks until user selects an element. Returns markdown with element context.
|
|
441
|
+
|
|
442
|
+
### Get Current Focus
|
|
443
|
+
\`\`\`
|
|
444
|
+
GET /api/focus
|
|
445
|
+
\`\`\`
|
|
446
|
+
Returns the currently focused element as markdown.
|
|
447
|
+
|
|
448
|
+
### Update Status
|
|
449
|
+
\`\`\`
|
|
450
|
+
POST /api/status
|
|
451
|
+
Content-Type: application/json
|
|
452
|
+
|
|
453
|
+
{"status": "fixing", "message": "Working on it..."}
|
|
454
|
+
\`\`\`
|
|
455
|
+
Status values: \`idle\`, \`pending\`, \`fixing\`, \`success\`, \`failed\`
|
|
456
|
+
|
|
457
|
+
### Report Action
|
|
458
|
+
\`\`\`
|
|
459
|
+
POST /api/action
|
|
460
|
+
Content-Type: application/json
|
|
461
|
+
|
|
462
|
+
{"action": "reading", "target": "src/components/Button.tsx"}
|
|
463
|
+
\`\`\`
|
|
464
|
+
Actions: \`reading\`, \`writing\`, \`searching\`, \`thinking\`
|
|
465
|
+
|
|
466
|
+
### Send Thought
|
|
467
|
+
\`\`\`
|
|
468
|
+
POST /api/thought
|
|
469
|
+
Content-Type: application/json
|
|
470
|
+
|
|
471
|
+
{"content": "I think we should use a CSS variable for this color..."}
|
|
472
|
+
\`\`\`
|
|
473
|
+
|
|
474
|
+
### Get History
|
|
475
|
+
\`\`\`
|
|
476
|
+
GET /api/history
|
|
477
|
+
\`\`\`
|
|
478
|
+
Returns up to 5 previous focus requests.
|
|
479
|
+
|
|
480
|
+
## Workflow
|
|
481
|
+
|
|
482
|
+
1. Start the bridge: \`npx eyeglass-bridge\`
|
|
483
|
+
2. Call \`GET /api/wait\` to block until user selects an element
|
|
484
|
+
3. Make your changes based on the returned context
|
|
485
|
+
4. Call \`POST /api/status\` with \`{"status": "success", "message": "Done!"}\`
|
|
486
|
+
5. Loop back to step 2
|
|
487
|
+
`;
|
|
488
|
+
if (fileExists(configPath)) {
|
|
489
|
+
log('Codex eyeglass instructions already exist', 'success');
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
if (dryRun) {
|
|
493
|
+
log(`Would create ${configPath}`);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
if (!fileExists(codexDir)) {
|
|
497
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
498
|
+
}
|
|
499
|
+
writeFile(configPath, codexInstructions);
|
|
500
|
+
log('Created .codex/eyeglass.md with HTTP API instructions', 'success');
|
|
501
|
+
}
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
271
504
|
function setupVite(configFile, dryRun) {
|
|
272
505
|
const cwd = process.cwd();
|
|
273
506
|
// Find entry file (main.tsx, main.ts, main.jsx, main.js, src/main.*, etc.)
|
|
@@ -289,8 +522,16 @@ function setupVite(configFile, dryRun) {
|
|
|
289
522
|
break;
|
|
290
523
|
}
|
|
291
524
|
}
|
|
525
|
+
// Fallback: parse index.html to find the actual entry script
|
|
526
|
+
if (!entryFile) {
|
|
527
|
+
entryFile = findEntryFromHtml(cwd);
|
|
528
|
+
if (entryFile) {
|
|
529
|
+
log(`Found entry file from index.html: ${path.relative(cwd, entryFile)}`, 'info');
|
|
530
|
+
}
|
|
531
|
+
}
|
|
292
532
|
if (!entryFile) {
|
|
293
533
|
log('Could not find entry file - please import @eyeglass/inspector manually in your main file', 'warn');
|
|
534
|
+
log('Tip: Check your index.html for the entry script path', 'info');
|
|
294
535
|
return false;
|
|
295
536
|
}
|
|
296
537
|
const content = readFile(entryFile);
|
|
@@ -380,8 +621,16 @@ function setupCRA(entryFile, dryRun) {
|
|
|
380
621
|
}
|
|
381
622
|
}
|
|
382
623
|
}
|
|
624
|
+
// Fallback: parse index.html to find the actual entry script
|
|
625
|
+
if (!entryFile) {
|
|
626
|
+
entryFile = findEntryFromHtml(cwd) ?? undefined;
|
|
627
|
+
if (entryFile) {
|
|
628
|
+
log(`Found entry file from index.html: ${path.relative(cwd, entryFile)}`, 'info');
|
|
629
|
+
}
|
|
630
|
+
}
|
|
383
631
|
if (!entryFile || !fileExists(entryFile)) {
|
|
384
632
|
log('Could not find entry file - please import @eyeglass/inspector manually', 'warn');
|
|
633
|
+
log('Tip: Check your index.html for the entry script path', 'info');
|
|
385
634
|
return false;
|
|
386
635
|
}
|
|
387
636
|
const content = readFile(entryFile);
|
|
@@ -402,12 +651,25 @@ function setupCRA(entryFile, dryRun) {
|
|
|
402
651
|
}
|
|
403
652
|
return true;
|
|
404
653
|
}
|
|
405
|
-
function init(options) {
|
|
654
|
+
async function init(options) {
|
|
406
655
|
const { dryRun, skipInstall } = options;
|
|
407
|
-
|
|
656
|
+
let { agents } = options;
|
|
657
|
+
console.log(LOGO);
|
|
408
658
|
if (dryRun) {
|
|
409
659
|
log('Running in dry-run mode - no changes will be made\n', 'warn');
|
|
410
660
|
}
|
|
661
|
+
// Prompt for agent selection if not provided
|
|
662
|
+
if (!agents || agents.length === 0) {
|
|
663
|
+
agents = await promptAgentSelection();
|
|
664
|
+
}
|
|
665
|
+
const agentNames = agents.map((a) => {
|
|
666
|
+
switch (a) {
|
|
667
|
+
case 'claude': return 'Claude Code';
|
|
668
|
+
case 'copilot': return 'GitHub Copilot CLI';
|
|
669
|
+
case 'codex': return 'OpenAI Codex CLI';
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
log(`Configuring for: ${agentNames.join(', ')}\n`);
|
|
411
673
|
// Detect project
|
|
412
674
|
const project = detectProject();
|
|
413
675
|
log(`Detected: ${project.type}${project.typescript ? ' (TypeScript)' : ''}`);
|
|
@@ -426,10 +688,22 @@ function init(options) {
|
|
|
426
688
|
}
|
|
427
689
|
}
|
|
428
690
|
}
|
|
429
|
-
// Step 2: Setup
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
691
|
+
// Step 2: Setup agent-specific configs
|
|
692
|
+
console.log('');
|
|
693
|
+
for (const agent of agents) {
|
|
694
|
+
switch (agent) {
|
|
695
|
+
case 'claude':
|
|
696
|
+
setupClaudeConfig(dryRun);
|
|
697
|
+
setupEyeglassSkill(dryRun);
|
|
698
|
+
break;
|
|
699
|
+
case 'copilot':
|
|
700
|
+
setupCopilotConfig(dryRun);
|
|
701
|
+
break;
|
|
702
|
+
case 'codex':
|
|
703
|
+
setupCodexConfig(dryRun);
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
433
707
|
// Step 4: Setup project-specific integration
|
|
434
708
|
console.log('');
|
|
435
709
|
switch (project.type) {
|
|
@@ -447,51 +721,105 @@ function init(options) {
|
|
|
447
721
|
case 'remix':
|
|
448
722
|
setupCRA(project.entryFile, dryRun);
|
|
449
723
|
break;
|
|
450
|
-
default:
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
724
|
+
default: {
|
|
725
|
+
// Try to find entry file from index.html for unknown project types
|
|
726
|
+
const entryFromHtml = findEntryFromHtml(process.cwd());
|
|
727
|
+
if (entryFromHtml) {
|
|
728
|
+
log(`Found entry file from index.html: ${path.relative(process.cwd(), entryFromHtml)}`, 'info');
|
|
729
|
+
const content = readFile(entryFromHtml);
|
|
730
|
+
if (content.includes('@eyeglass/inspector')) {
|
|
731
|
+
log('Inspector already imported', 'success');
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
const importStatement = `import '@eyeglass/inspector';\n`;
|
|
735
|
+
const newContent = importStatement + content;
|
|
736
|
+
if (dryRun) {
|
|
737
|
+
log(`Would add inspector import to ${path.relative(process.cwd(), entryFromHtml)}`);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
writeFile(entryFromHtml, newContent);
|
|
741
|
+
log(`Added inspector import to ${path.relative(process.cwd(), entryFromHtml)}`, 'success');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
log('Unknown project type - please configure manually:', 'warn');
|
|
747
|
+
console.log(' 1. Import @eyeglass/inspector in your entry file');
|
|
748
|
+
console.log(' 2. Or add <script type="module">import "@eyeglass/inspector";</script> to your HTML\n');
|
|
749
|
+
}
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
454
752
|
}
|
|
455
753
|
// Done!
|
|
456
|
-
console.log('\n\x1b[
|
|
754
|
+
console.log('\n\x1b[1mSetup complete!\x1b[0m\n');
|
|
755
|
+
printUsageInstructions(agents);
|
|
756
|
+
}
|
|
757
|
+
function printUsageInstructions(agents) {
|
|
457
758
|
console.log('\x1b[1mHow to use Eyeglass:\x1b[0m\n');
|
|
458
759
|
console.log(' 1. Start your dev server');
|
|
459
|
-
console.log(' 2.
|
|
460
|
-
console.log('
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
760
|
+
console.log(' 2. Start the Eyeglass bridge: \x1b[36mnpx eyeglass-bridge\x1b[0m');
|
|
761
|
+
console.log('');
|
|
762
|
+
if (agents.includes('claude')) {
|
|
763
|
+
console.log(' \x1b[1mClaude Code:\x1b[0m');
|
|
764
|
+
console.log(' - Run \x1b[36mclaude\x1b[0m in this directory');
|
|
765
|
+
console.log(' - Say \x1b[36m"watch eyeglass"\x1b[0m or \x1b[36m"/eyeglass"\x1b[0m');
|
|
766
|
+
console.log('');
|
|
767
|
+
}
|
|
768
|
+
if (agents.includes('copilot')) {
|
|
769
|
+
console.log(' \x1b[1mGitHub Copilot CLI:\x1b[0m');
|
|
770
|
+
console.log(' - Run \x1b[36mgh copilot\x1b[0m in this directory');
|
|
771
|
+
console.log(' - Copilot will auto-connect to the eyeglass MCP server');
|
|
772
|
+
console.log('');
|
|
773
|
+
}
|
|
774
|
+
if (agents.includes('codex')) {
|
|
775
|
+
console.log(' \x1b[1mOpenAI Codex CLI:\x1b[0m');
|
|
776
|
+
console.log(' - See \x1b[36m.codex/eyeglass.md\x1b[0m for HTTP API docs');
|
|
777
|
+
console.log(' - Use \x1b[36mGET http://localhost:3300/api/wait\x1b[0m to listen');
|
|
778
|
+
console.log('');
|
|
779
|
+
}
|
|
780
|
+
console.log(' 3. In your browser, hover over any element and click to select');
|
|
781
|
+
console.log(' 4. Type your request (e.g., "make this blue") and submit');
|
|
782
|
+
console.log(' → Your agent automatically receives it and starts working!\n');
|
|
783
|
+
console.log('\x1b[2mTip: You never need to leave your browser - your agent watches for requests.\x1b[0m\n');
|
|
466
784
|
}
|
|
467
785
|
function help() {
|
|
468
|
-
console.log(
|
|
469
|
-
|
|
786
|
+
console.log(LOGO);
|
|
787
|
+
console.log(`\x1b[2mVisual debugging for AI coding agents\x1b[0m
|
|
470
788
|
|
|
471
|
-
Point at UI elements in your browser and tell
|
|
472
|
-
|
|
789
|
+
Point at UI elements in your browser and tell your AI agent what to change.
|
|
790
|
+
Works with Claude Code, GitHub Copilot CLI, and OpenAI Codex CLI.
|
|
473
791
|
|
|
474
792
|
\x1b[1mUSAGE\x1b[0m
|
|
475
793
|
npx @eyeglass/cli <command> [options]
|
|
476
794
|
|
|
477
795
|
\x1b[1mCOMMANDS\x1b[0m
|
|
478
|
-
init Initialize Eyeglass in your project
|
|
796
|
+
init Initialize Eyeglass in your project (interactive)
|
|
479
797
|
help Show this help message
|
|
480
798
|
|
|
481
799
|
\x1b[1mOPTIONS\x1b[0m
|
|
482
800
|
--dry-run Preview changes without making them
|
|
483
801
|
--skip-install Skip installing @eyeglass/inspector
|
|
802
|
+
--claude Configure for Claude Code (stdio MCP)
|
|
803
|
+
--copilot Configure for GitHub Copilot CLI (local MCP)
|
|
804
|
+
--codex Configure for OpenAI Codex CLI (HTTP API)
|
|
484
805
|
|
|
485
806
|
\x1b[1mEXAMPLES\x1b[0m
|
|
486
|
-
npx @eyeglass/cli init #
|
|
807
|
+
npx @eyeglass/cli init # Interactive agent selection
|
|
808
|
+
npx @eyeglass/cli init --claude # Setup for Claude Code only
|
|
809
|
+
npx @eyeglass/cli init --copilot --codex # Setup for multiple agents
|
|
487
810
|
npx @eyeglass/cli init --dry-run # Preview what would change
|
|
488
811
|
|
|
812
|
+
\x1b[1mSUPPORTED AGENTS\x1b[0m
|
|
813
|
+
Claude Code - Uses stdio MCP (.claude/settings.json)
|
|
814
|
+
GitHub Copilot CLI - Uses local MCP (.copilot/mcp-config.json)
|
|
815
|
+
OpenAI Codex - Uses HTTP API (.codex/eyeglass.md)
|
|
816
|
+
|
|
489
817
|
\x1b[1mWORKFLOW\x1b[0m
|
|
490
818
|
1. Run \x1b[36mnpx @eyeglass/cli init\x1b[0m in your project
|
|
491
819
|
2. Start your dev server
|
|
492
|
-
3.
|
|
493
|
-
4. In your browser: hover over element →
|
|
494
|
-
5.
|
|
820
|
+
3. Start your AI coding agent
|
|
821
|
+
4. In your browser: hover over element → click → type request
|
|
822
|
+
5. Your agent automatically picks up requests and makes changes!
|
|
495
823
|
|
|
496
824
|
\x1b[1mMORE INFO\x1b[0m
|
|
497
825
|
https://github.com/donutboyband/eyeglass
|
|
@@ -503,13 +831,25 @@ Claude watches for requests, so you never need to leave your browser.
|
|
|
503
831
|
const args = process.argv.slice(2);
|
|
504
832
|
const command = args.find((arg) => !arg.startsWith('-'));
|
|
505
833
|
const flags = args.filter((arg) => arg.startsWith('-'));
|
|
834
|
+
// Parse agent flags
|
|
835
|
+
const agentFlags = [];
|
|
836
|
+
if (flags.includes('--claude'))
|
|
837
|
+
agentFlags.push('claude');
|
|
838
|
+
if (flags.includes('--copilot'))
|
|
839
|
+
agentFlags.push('copilot');
|
|
840
|
+
if (flags.includes('--codex'))
|
|
841
|
+
agentFlags.push('codex');
|
|
506
842
|
const options = {
|
|
507
843
|
dryRun: flags.includes('--dry-run'),
|
|
508
844
|
skipInstall: flags.includes('--skip-install'),
|
|
845
|
+
agents: agentFlags.length > 0 ? agentFlags : undefined,
|
|
509
846
|
};
|
|
510
847
|
switch (command) {
|
|
511
848
|
case 'init':
|
|
512
|
-
init(options)
|
|
849
|
+
init(options).catch((err) => {
|
|
850
|
+
console.error('Error:', err.message);
|
|
851
|
+
process.exit(1);
|
|
852
|
+
});
|
|
513
853
|
break;
|
|
514
854
|
case 'help':
|
|
515
855
|
case undefined:
|
package/dist/index.test.js
CHANGED
|
@@ -58,6 +58,34 @@ function detectPackageManager(cwd, mockFs) {
|
|
|
58
58
|
return 'yarn';
|
|
59
59
|
return 'npm';
|
|
60
60
|
}
|
|
61
|
+
// Helper to recreate findEntryFromHtml logic
|
|
62
|
+
function findEntryFromHtml(cwd, mockFs) {
|
|
63
|
+
const htmlFiles = [
|
|
64
|
+
'index.html',
|
|
65
|
+
'public/index.html',
|
|
66
|
+
'src/index.html',
|
|
67
|
+
];
|
|
68
|
+
for (const htmlFile of htmlFiles) {
|
|
69
|
+
const htmlPath = path.join(cwd, htmlFile);
|
|
70
|
+
if (!mockFs.existsSync(htmlPath))
|
|
71
|
+
continue;
|
|
72
|
+
const content = mockFs.readFileSync(htmlPath);
|
|
73
|
+
const moduleScriptRegex = /<script[^>]*type\s*=\s*["']module["'][^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
|
|
74
|
+
const altModuleScriptRegex = /<script[^>]*src\s*=\s*["']([^"']+)["'][^>]*type\s*=\s*["']module["'][^>]*>/gi;
|
|
75
|
+
let match = moduleScriptRegex.exec(content) || altModuleScriptRegex.exec(content);
|
|
76
|
+
if (match && match[1]) {
|
|
77
|
+
let scriptSrc = match[1];
|
|
78
|
+
if (scriptSrc.startsWith('/')) {
|
|
79
|
+
scriptSrc = scriptSrc.slice(1);
|
|
80
|
+
}
|
|
81
|
+
const scriptPath = path.join(cwd, scriptSrc);
|
|
82
|
+
if (mockFs.existsSync(scriptPath)) {
|
|
83
|
+
return scriptPath;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
61
89
|
describe('eyeglass CLI', () => {
|
|
62
90
|
beforeEach(() => {
|
|
63
91
|
vi.clearAllMocks();
|
|
@@ -234,6 +262,89 @@ export default defineConfig({
|
|
|
234
262
|
expect(layoutFiles.length).toBe(8);
|
|
235
263
|
});
|
|
236
264
|
});
|
|
265
|
+
describe('findEntryFromHtml', () => {
|
|
266
|
+
it('should find entry script from index.html with leading slash', () => {
|
|
267
|
+
const mockFs = {
|
|
268
|
+
existsSync: (p) => p.endsWith('index.html') || p.endsWith('src/main.js'),
|
|
269
|
+
readFileSync: () => `<!DOCTYPE html>
|
|
270
|
+
<html>
|
|
271
|
+
<body>
|
|
272
|
+
<script type="module" src="/src/main.js"></script>
|
|
273
|
+
</body>
|
|
274
|
+
</html>`,
|
|
275
|
+
};
|
|
276
|
+
const result = findEntryFromHtml('/my/project', mockFs);
|
|
277
|
+
expect(result).toContain('src/main.js');
|
|
278
|
+
});
|
|
279
|
+
it('should find entry script from index.html without leading slash', () => {
|
|
280
|
+
const mockFs = {
|
|
281
|
+
existsSync: (p) => p.endsWith('index.html') || p.endsWith('src/app.js'),
|
|
282
|
+
readFileSync: () => `<!DOCTYPE html>
|
|
283
|
+
<html>
|
|
284
|
+
<body>
|
|
285
|
+
<script type="module" src="src/app.js"></script>
|
|
286
|
+
</body>
|
|
287
|
+
</html>`,
|
|
288
|
+
};
|
|
289
|
+
const result = findEntryFromHtml('/my/project', mockFs);
|
|
290
|
+
expect(result).toContain('src/app.js');
|
|
291
|
+
});
|
|
292
|
+
it('should find entry script with src before type attribute', () => {
|
|
293
|
+
const mockFs = {
|
|
294
|
+
existsSync: (p) => p.endsWith('index.html') || p.endsWith('src/counter.ts'),
|
|
295
|
+
readFileSync: () => `<!DOCTYPE html>
|
|
296
|
+
<html>
|
|
297
|
+
<body>
|
|
298
|
+
<script src="/src/counter.ts" type="module"></script>
|
|
299
|
+
</body>
|
|
300
|
+
</html>`,
|
|
301
|
+
};
|
|
302
|
+
const result = findEntryFromHtml('/my/project', mockFs);
|
|
303
|
+
expect(result).toContain('src/counter.ts');
|
|
304
|
+
});
|
|
305
|
+
it('should find entry script from public/index.html', () => {
|
|
306
|
+
const mockFs = {
|
|
307
|
+
existsSync: (p) => p.endsWith('public/index.html') || p.endsWith('src/index.js'),
|
|
308
|
+
readFileSync: () => `<!DOCTYPE html>
|
|
309
|
+
<html>
|
|
310
|
+
<body>
|
|
311
|
+
<script type="module" src="/src/index.js"></script>
|
|
312
|
+
</body>
|
|
313
|
+
</html>`,
|
|
314
|
+
};
|
|
315
|
+
const result = findEntryFromHtml('/my/project', mockFs);
|
|
316
|
+
expect(result).toContain('src/index.js');
|
|
317
|
+
});
|
|
318
|
+
it('should return null when index.html does not exist', () => {
|
|
319
|
+
const mockFs = {
|
|
320
|
+
existsSync: () => false,
|
|
321
|
+
readFileSync: () => '',
|
|
322
|
+
};
|
|
323
|
+
const result = findEntryFromHtml('/my/project', mockFs);
|
|
324
|
+
expect(result).toBeNull();
|
|
325
|
+
});
|
|
326
|
+
it('should return null when script file does not exist', () => {
|
|
327
|
+
const mockFs = {
|
|
328
|
+
existsSync: (p) => p.endsWith('index.html'),
|
|
329
|
+
readFileSync: () => `<script type="module" src="/src/main.js"></script>`,
|
|
330
|
+
};
|
|
331
|
+
const result = findEntryFromHtml('/my/project', mockFs);
|
|
332
|
+
expect(result).toBeNull();
|
|
333
|
+
});
|
|
334
|
+
it('should return null when no module script is found', () => {
|
|
335
|
+
const mockFs = {
|
|
336
|
+
existsSync: (p) => p.endsWith('index.html'),
|
|
337
|
+
readFileSync: () => `<!DOCTYPE html>
|
|
338
|
+
<html>
|
|
339
|
+
<body>
|
|
340
|
+
<script src="/src/main.js"></script>
|
|
341
|
+
</body>
|
|
342
|
+
</html>`,
|
|
343
|
+
};
|
|
344
|
+
const result = findEntryFromHtml('/my/project', mockFs);
|
|
345
|
+
expect(result).toBeNull();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
237
348
|
describe('use client directive handling', () => {
|
|
238
349
|
it('should preserve use client directive at top', () => {
|
|
239
350
|
const fileContent = `'use client';
|