@pasko70/pibo 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,58 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { detectDesktopEnv, hasDesktopDisplay, printLinuxVirtualDisplayHint } from './desktop-env.js';
4
+ export const PIBO_XVFB_SERVICE_NAME = 'pibo-xvfb.service';
5
+ export const PIBO_XVFB_SERVICE_PATH = `/etc/systemd/system/${PIBO_XVFB_SERVICE_NAME}`;
6
+ export function createPiboXvfbServiceUnit() {
7
+ return [
8
+ '[Unit]',
9
+ 'Description=Virtual X display for Pibo browser automation',
10
+ 'After=network.target',
11
+ '',
12
+ '[Service]',
13
+ 'Type=simple',
14
+ 'ExecStart=/usr/bin/Xvfb :0 -screen 0 1920x1080x24 -ac -nolisten tcp',
15
+ 'Restart=always',
16
+ 'RestartSec=2',
17
+ '',
18
+ '[Install]',
19
+ 'WantedBy=multi-user.target',
20
+ '',
21
+ ].join('\n');
22
+ }
23
+ function isRootUser() {
24
+ return typeof process.getuid === 'function' && process.getuid() === 0;
25
+ }
26
+ async function waitForDisplaySocket(timeoutMs = 5000) {
27
+ const deadline = Date.now() + timeoutMs;
28
+ while (Date.now() < deadline) {
29
+ if (existsSync('/tmp/.X11-unix/X0'))
30
+ return true;
31
+ await new Promise((resolve) => setTimeout(resolve, 100));
32
+ }
33
+ return existsSync('/tmp/.X11-unix/X0');
34
+ }
35
+ export async function ensureLinuxVirtualDisplay(options) {
36
+ if (process.platform !== 'linux')
37
+ return false;
38
+ if (hasDesktopDisplay(detectDesktopEnv()))
39
+ return false;
40
+ if (!isRootUser()) {
41
+ console.log('No desktop display detected and automatic virtual display setup requires root.');
42
+ printLinuxVirtualDisplayHint(' ');
43
+ return false;
44
+ }
45
+ console.log('No desktop display detected. Provisioning virtual X display for browser-use.');
46
+ await options.runInherited('apt-get', ['update']);
47
+ await options.runInherited('apt-get', ['install', '-y', 'xvfb', 'xauth', 'x11-xserver-utils']);
48
+ await writeFile(PIBO_XVFB_SERVICE_PATH, createPiboXvfbServiceUnit(), 'utf8');
49
+ await options.runInherited('systemctl', ['daemon-reload']);
50
+ await options.runInherited('systemctl', ['enable', '--now', PIBO_XVFB_SERVICE_NAME]);
51
+ process.env.DISPLAY = ':0';
52
+ const ready = await waitForDisplaySocket();
53
+ if (!ready) {
54
+ throw new Error('Virtual X display service was installed but DISPLAY :0 did not become ready.');
55
+ }
56
+ console.log('Virtual X display ready on DISPLAY=:0');
57
+ return true;
58
+ }
@@ -49,7 +49,32 @@ function runBuffered(command, args, env = process.env) {
49
49
  });
50
50
  });
51
51
  }
52
- function runInherited(command, args, env = process.env) {
52
+ function isRootUser() {
53
+ return typeof process.getuid === 'function' && process.getuid() === 0;
54
+ }
55
+ async function resolveUvCommand() {
56
+ const uv = await runBuffered('uv', ['--version']);
57
+ if (uv.ok)
58
+ return 'uv';
59
+ const localUv = join(homedir(), '.local', 'bin', 'uv');
60
+ if (existsSync(localUv)) {
61
+ const local = await runBuffered(localUv, ['--version']);
62
+ if (local.ok)
63
+ return localUv;
64
+ }
65
+ if (process.platform === 'linux' && isRootUser()) {
66
+ console.log('uv was not found on PATH. Installing uv automatically.');
67
+ await runInheritedCommand('sh', ['-c', 'curl -LsSf https://astral.sh/uv/install.sh | sh']);
68
+ const installed = await runBuffered(localUv, ['--version'], {
69
+ ...process.env,
70
+ PATH: `${join(homedir(), '.local', 'bin')}:${process.env.PATH ?? ''}`,
71
+ });
72
+ if (installed.ok)
73
+ return localUv;
74
+ }
75
+ return undefined;
76
+ }
77
+ export function runInheritedCommand(command, args, env = process.env) {
53
78
  return new Promise((resolve, reject) => {
54
79
  console.log(`Running: ${command} ${args.join(' ')}`);
55
80
  const child = spawn(command, args, {
@@ -82,9 +107,10 @@ function runInherited(command, args, env = process.env) {
82
107
  }
83
108
  export async function printToolPythonRuntimeDoctor(name, spec) {
84
109
  const paths = getToolPythonRuntimePaths(name, spec);
85
- const uv = await runBuffered('uv', ['--version']);
110
+ const uvCommand = await resolveUvCommand();
111
+ const uv = uvCommand ? await runBuffered(uvCommand, ['--version']) : { ok: false, output: 'missing' };
86
112
  const python = uv.ok
87
- ? await runBuffered('uv', ['python', 'find', spec.pythonVersion])
113
+ ? await runBuffered(uvCommand, ['python', 'find', spec.pythonVersion])
88
114
  : { ok: false, output: 'skipped because uv is missing' };
89
115
  console.log(`${name}`);
90
116
  console.log(` uv: ${uv.ok ? uv.output || 'ok' : `missing (${uv.output})`}`);
@@ -122,13 +148,13 @@ export async function printToolPythonRuntimeDoctor(name, spec) {
122
148
  }
123
149
  }
124
150
  export async function installToolPythonRuntime(name, spec) {
125
- const uv = await runBuffered('uv', ['--version']);
126
- if (!uv.ok) {
151
+ const uvCommand = await resolveUvCommand();
152
+ if (!uvCommand) {
127
153
  throw new Error(formatCliError({
128
154
  code: ErrorCode.CLIENT_ERROR,
129
155
  type: 'CLI_TOOL_UV_MISSING',
130
156
  message: 'uv was not found on PATH',
131
- details: uv.output,
157
+ details: 'spawn uv ENOENT',
132
158
  suggestion: 'Install uv first. macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh. Windows PowerShell: irm https://astral.sh/uv/install.ps1 | iex.',
133
159
  }));
134
160
  }
@@ -136,9 +162,9 @@ export async function installToolPythonRuntime(name, spec) {
136
162
  await mkdir(paths.rootDir, { recursive: true });
137
163
  await mkdir(paths.homeDir, { recursive: true });
138
164
  if (!existsSync(paths.venvDir)) {
139
- await runInherited('uv', ['venv', paths.venvDir, '--python', spec.pythonVersion]);
165
+ await runInheritedCommand(uvCommand, ['venv', paths.venvDir, '--python', spec.pythonVersion]);
140
166
  }
141
- await runInherited('uv', [
167
+ await runInheritedCommand(uvCommand, [
142
168
  'pip',
143
169
  'install',
144
170
  '--python',
@@ -146,7 +172,7 @@ export async function installToolPythonRuntime(name, spec) {
146
172
  spec.packageName,
147
173
  ]);
148
174
  if (spec.postInstallArgs?.length) {
149
- await runInherited(paths.executablePath, spec.postInstallArgs, getToolPythonRuntimeEnv(paths, spec));
175
+ await runInheritedCommand(paths.executablePath, spec.postInstallArgs, getToolPythonRuntimeEnv(paths, spec));
150
176
  }
151
177
  return paths;
152
178
  }
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { detectDesktopEnv, hasDesktopDisplay, printDesktopEnvStatus, printLinuxVirtualDisplayHint } from './desktop-env.js';
3
3
  import { BROWSER_USE_GUIDE, REMOTE_BROWSER_GUIDE } from './guides.js';
4
- import { getToolPythonRuntimePaths, installToolPythonRuntime, printToolPythonRuntimeDoctor, removeToolPythonRuntime, } from './python-runtime.js';
4
+ import { ensureLinuxVirtualDisplay } from './linux-virtual-display.js';
5
+ import { getToolPythonRuntimePaths, installToolPythonRuntime, runInheritedCommand, printToolPythonRuntimeDoctor, removeToolPythonRuntime, } from './python-runtime.js';
5
6
  const MAX_AGENT_CONTEXT_SNIPPET_LENGTH = 480;
6
7
  const INSTALLED_TOOLS_CONTEXT_PATH = '.pibo/context/installed-pibo-tools.md';
7
8
  const REGISTRY = [
@@ -90,6 +91,9 @@ export function getInstalledCliToolContextFile() {
90
91
  }
91
92
  export async function installCliTool(entry, runSetup) {
92
93
  if (runSetup) {
94
+ if (entry.name === 'browser-use') {
95
+ await ensureLinuxVirtualDisplay({ runInherited: runInheritedCommand });
96
+ }
93
97
  await installToolPythonRuntime(entry.name, entry.runtime);
94
98
  }
95
99
  const status = getCliToolStatus(entry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "description": "Minimal TypeScript wrapper around Pi Coding Agent.",
6
6
  "files": [