@galda/cli 0.10.38 → 0.10.39

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,96 @@
1
+ #!/usr/bin/env node
2
+ // Manager for AI — the worker's camera.
3
+ //
4
+ // One job: take a URL or a local HTML file and write a PNG, in headless Chrome,
5
+ // without ever coming to the front. It is handed to the worker (see SHOT_CLI in
6
+ // lib.mjs) so that a request like "3案見せて" can come back as pictures of the
7
+ // thing that was actually built.
8
+ //
9
+ // It decides nothing. It does not know what a UI change is, does not pick what
10
+ // to shoot, and is never invoked by the Manager on the worker's behalf — the
11
+ // agent that did the work aims the camera. Staging the photo ourselves is the
12
+ // bug this replaces, not a feature to re-add here.
13
+ //
14
+ // node engine/shot.mjs <url|file> <out.png> [--width N] [--height N]
15
+ // [--full] [--wait MS] [--wait-for SELECTOR]
16
+ //
17
+ // Exit 0 prints the absolute path written; any failure exits 1 with one line
18
+ // the worker can act on (a half-written PNG is worse than a clear error).
19
+
20
+ import { existsSync, mkdirSync, statSync } from 'node:fs';
21
+ import { dirname, resolve } from 'node:path';
22
+ import { createRequire } from 'node:module';
23
+ import { parseShotArgs, shotTargetUrl, pickChromePath } from './lib.mjs';
24
+
25
+ const require = createRequire(import.meta.url);
26
+
27
+ function die(msg) {
28
+ console.error(`shot: ${msg}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ const opt = parseShotArgs(process.argv.slice(2));
33
+ if (opt.error) die(opt.error);
34
+
35
+ // A local target that does not exist is the common typo, and Chrome would
36
+ // happily screenshot its own "file not found" page — a picture of nothing.
37
+ if (!/^(https?|file):\/\//i.test(opt.target) && !existsSync(resolve(process.cwd(), opt.target))) {
38
+ die(`no such file: ${opt.target}`);
39
+ }
40
+
41
+ const url = shotTargetUrl(opt.target, (p) => resolve(process.cwd(), p));
42
+ const out = resolve(process.cwd(), opt.out);
43
+ const chrome = pickChromePath(process.env, existsSync);
44
+ if (!chrome) die('Chrome not found — install Google Chrome or set CHROME_PATH to the binary');
45
+
46
+ let puppeteer;
47
+ try {
48
+ puppeteer = require('puppeteer-core');
49
+ } catch {
50
+ die('puppeteer-core is not installed next to the Manager — run npm install in the Manager directory');
51
+ }
52
+
53
+ mkdirSync(dirname(out), { recursive: true });
54
+
55
+ // Failures inside the block below are recorded, not exited on: process.exit()
56
+ // skips `finally`, and that would leave a headless Chrome running on the user's
57
+ // machine for every mistyped selector.
58
+ let browser, failure = null;
59
+ try {
60
+ browser = await puppeteer.launch({
61
+ executablePath: chrome,
62
+ // headless: no window exists, so it cannot steal focus from the human whose
63
+ // machine this is running on (a standing rule for every browser we launch).
64
+ headless: 'new',
65
+ args: ['--no-sandbox', '--hide-scrollbars', '--force-color-profile=srgb'],
66
+ defaultViewport: { width: opt.width, height: opt.height, deviceScaleFactor: 2 },
67
+ });
68
+ const page = await browser.newPage();
69
+ // domcontentloaded, not networkidle: a live page that polls never settles, and
70
+ // a screenshot that times out is a picture nobody gets (see the same trap in
71
+ // the verification core).
72
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
73
+ if (opt.waitFor) {
74
+ try {
75
+ await page.waitForSelector(opt.waitFor, { timeout: 15000 });
76
+ } catch {
77
+ throw new Error(`--wait-for never matched: ${opt.waitFor}`);
78
+ }
79
+ }
80
+ if (opt.wait) await new Promise((r) => setTimeout(r, opt.wait));
81
+ await page.screenshot({ path: out, fullPage: opt.full });
82
+ } catch (e) {
83
+ failure = String(e?.message ?? e).split('\n')[0];
84
+ } finally {
85
+ // disconnect + kill rather than close(): close() has hung for minutes on this
86
+ // transport, and a camera that never returns blocks the whole task.
87
+ try {
88
+ const proc = browser?.process?.();
89
+ await browser?.disconnect?.();
90
+ proc?.kill('SIGKILL');
91
+ } catch { /* the browser is already gone — nothing to clean up */ }
92
+ }
93
+
94
+ if (failure) die(failure);
95
+ if (!existsSync(out) || statSync(out).size === 0) die('no image was written');
96
+ console.log(out);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.38",
3
+ "version": "0.10.39",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {