agentgui 1.0.1114 → 1.0.1116

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.
@@ -1,206 +0,0 @@
1
- #!/usr/bin/env node
2
- // Boot agentgui with a fixture DB on an ephemeral port and capture a set of PNGs.
3
- // Uses puppeteer-core with a system-provided chromium (set CHROME or it autodetects).
4
- //
5
- // Usage: node scripts/capture-screenshots.mjs [--fixtures=./fixtures] [--out=./docs/screenshots]
6
-
7
- import fs from 'fs';
8
- import path from 'path';
9
- import net from 'net';
10
- import { spawn } from 'child_process';
11
- import { fileURLToPath } from 'url';
12
-
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
- const ROOT = path.resolve(__dirname, '..');
15
-
16
- const argMap = Object.fromEntries(process.argv.slice(2).map(a => {
17
- const [k, v] = a.replace(/^--/, '').split('=');
18
- return [k, v ?? true];
19
- }));
20
- const FIXTURES = path.resolve(argMap.fixtures || path.join(ROOT, 'fixtures'));
21
- const OUT = path.resolve(argMap.out || path.join(ROOT, 'docs/screenshots'));
22
-
23
- function pickChrome() {
24
- if (process.env.CHROME && fs.existsSync(process.env.CHROME)) return process.env.CHROME;
25
- const home = process.env.USERPROFILE || process.env.HOME || '';
26
- const candidates = [
27
- // Linux
28
- '/usr/bin/chromium',
29
- '/usr/bin/chromium-browser',
30
- '/usr/bin/google-chrome',
31
- '/usr/bin/google-chrome-stable',
32
- '/snap/bin/chromium',
33
- // macOS
34
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
35
- '/Applications/Chromium.app/Contents/MacOS/Chromium',
36
- // Windows - program files variants
37
- 'C:/Program Files/Google/Chrome/Application/chrome.exe',
38
- 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
39
- path.join(home, 'AppData/Local/Google/Chrome/Application/chrome.exe'),
40
- path.join(home, 'AppData/Local/Chromium/Application/chrome.exe'),
41
- // puppeteer-core bundled chromium (if installed via npm)
42
- ...(() => { try { return [require('puppeteer-core').executablePath()]; } catch { return []; } })(),
43
- ];
44
- for (const c of candidates) if (c && fs.existsSync(c)) return c;
45
- throw new Error('No chromium binary found. Set CHROME env var, install Google Chrome, or apt install chromium.');
46
- }
47
-
48
- async function findFreePort() {
49
- return await new Promise((resolve, reject) => {
50
- const s = net.createServer();
51
- s.unref();
52
- s.on('error', reject);
53
- s.listen(0, () => {
54
- const p = s.address().port;
55
- s.close(() => resolve(p));
56
- });
57
- });
58
- }
59
-
60
- async function waitForHealthy(url, timeoutMs = 20000) {
61
- const start = Date.now();
62
- while (Date.now() - start < timeoutMs) {
63
- try {
64
- const r = await fetch(url);
65
- if (r.ok) return true;
66
- } catch {}
67
- await new Promise(r => setTimeout(r, 250));
68
- }
69
- throw new Error(`Server did not become healthy at ${url} within ${timeoutMs}ms`);
70
- }
71
-
72
- async function main() {
73
- fs.mkdirSync(OUT, { recursive: true });
74
- if (!fs.existsSync(FIXTURES)) {
75
- console.warn(`[capture] fixture dir does not exist: ${FIXTURES} - using empty data dir`);
76
- fs.mkdirSync(FIXTURES, { recursive: true });
77
- }
78
-
79
- const port = await findFreePort();
80
- const BASE = `http://localhost:${port}/gm`;
81
-
82
- console.log(`[capture] booting server on :${port} with data=${FIXTURES}`);
83
- const serverEnv = {
84
- ...process.env,
85
- PASSWORD: '',
86
- PORT: String(port),
87
- HOT_RELOAD: 'false',
88
- STARTUP_CWD: FIXTURES,
89
- PORTABLE_DATA_DIR: FIXTURES,
90
- BASE_URL: '/gm',
91
- AGENTGUI_SKIP_AUTO_IMPORT: '1', // do not merge user's ~/.claude/projects into the fixture DB
92
- };
93
- const server = spawn(process.execPath, [path.join(ROOT, 'server.js')], {
94
- env: serverEnv,
95
- stdio: ['ignore', 'pipe', 'pipe'],
96
- cwd: ROOT,
97
- });
98
- const srvLog = [];
99
- server.stdout.on('data', d => { srvLog.push(d); process.stdout.write(d); });
100
- server.stderr.on('data', d => { srvLog.push(d); process.stderr.write(d); });
101
-
102
- const cleanup = () => {
103
- try { server.kill('SIGTERM'); } catch {}
104
- setTimeout(() => { try { server.kill('SIGKILL'); } catch {} }, 1500);
105
- };
106
- process.on('exit', cleanup);
107
- process.on('SIGINT', () => { cleanup(); process.exit(130); });
108
-
109
- let serverExitCode = null;
110
- server.on('exit', (code) => { serverExitCode = code ?? 0; });
111
-
112
- try {
113
- await waitForHealthy(`${BASE}/api/health`);
114
- if (serverExitCode !== null) throw new Error(`Server exited with code ${serverExitCode} before becoming healthy`);
115
- console.log('[capture] server healthy - launching browser');
116
-
117
- const { default: puppeteer } = await import('puppeteer-core');
118
- const browser = await puppeteer.launch({
119
- executablePath: pickChrome(),
120
- headless: 'new',
121
- args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--hide-scrollbars'],
122
- defaultViewport: { width: 1440, height: 900, deviceScaleFactor: 1 },
123
- });
124
-
125
- const shots = [
126
- { name: 'home-light', theme: 'light', url: `${BASE}/` },
127
- { name: 'home-dark', theme: 'dark', url: `${BASE}/` },
128
- { name: 'conversation-light', theme: 'light', url: `${BASE}/`, firstConv: true },
129
- { name: 'conversation-dark', theme: 'dark', url: `${BASE}/`, firstConv: true },
130
- { name: 'tools-manager', theme: 'light', url: `${BASE}/`, openTools: true },
131
- ];
132
-
133
- for (const s of shots) {
134
- const page = await browser.newPage();
135
- page.setDefaultNavigationTimeout(30000);
136
- await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]);
137
- await page.evaluateOnNewDocument((theme) => {
138
- try { localStorage.setItem('theme', theme); } catch {}
139
- if (theme === 'dark') {
140
- document.documentElement.classList.add('dark');
141
- document.documentElement.setAttribute('data-theme', 'dark');
142
- }
143
- // Determinism: disable transitions/animations so repeated shots match byte-for-byte
144
- const style = document.createElement('style');
145
- style.textContent = `*,*::before,*::after{transition:none!important;animation:none!important;caret-color:transparent!important}`;
146
- if (document.head) document.head.appendChild(style);
147
- else document.addEventListener('DOMContentLoaded', () => document.head.appendChild(style));
148
- }, s.theme);
149
-
150
- console.log(`[capture] -> ${s.name} (${s.theme})`);
151
- await page.goto(s.url, { waitUntil: 'domcontentloaded' });
152
- // Determinism: wait until the sidebar has transitioned out of "Loading..."
153
- // and rendered at least one conversation row.
154
- await page.waitForFunction(() => {
155
- const ul = document.querySelector('.sidebar-list');
156
- if (!ul) return false;
157
- const rows = ul.querySelectorAll('li:not(.sidebar-empty)');
158
- return rows.length > 0;
159
- }, { timeout: 8000 }).catch(() => {});
160
- await new Promise(r => setTimeout(r, 600));
161
-
162
- if (s.firstConv) {
163
- const clicked = await page.evaluate(() => {
164
- const row = document.querySelector('.sidebar-list li:not(.sidebar-empty), .conversation-item:not(.sidebar-empty)');
165
- if (row) { row.click(); return true; }
166
- return false;
167
- });
168
- if (clicked) {
169
- await page.waitForFunction(() => {
170
- const out = document.querySelector('#output');
171
- return out && out.children.length > 0;
172
- }, { timeout: 5000 }).catch(() => {});
173
- await new Promise(r => setTimeout(r, 600));
174
- }
175
- }
176
- if (s.openTools) {
177
- const clicked = await page.evaluate(() => {
178
- const btn = document.getElementById('toolsManagerBtn');
179
- if (btn) { btn.click(); return true; }
180
- return false;
181
- });
182
- if (clicked) await new Promise(r => setTimeout(r, 400));
183
- }
184
-
185
- const outPath = path.join(OUT, `${s.name}.png`);
186
- await page.screenshot({ path: outPath, type: 'png', fullPage: false });
187
- const sz = fs.statSync(outPath).size;
188
- console.log(` wrote ${outPath} (${(sz/1024).toFixed(1)}KB)`);
189
- await page.close();
190
- }
191
-
192
- await browser.close();
193
- } finally {
194
- cleanup();
195
- }
196
-
197
- // Final summary
198
- const produced = fs.readdirSync(OUT).filter(f => f.endsWith('.png')).sort();
199
- console.log(`\n[capture] done. ${produced.length} png(s) in ${OUT}`);
200
- for (const f of produced) {
201
- const p = path.join(OUT, f);
202
- console.log(` ${f} ${(fs.statSync(p).size/1024).toFixed(1)}KB`);
203
- }
204
- }
205
-
206
- main().catch(e => { console.error(e); process.exit(1); });