@tsconfigdotjson/vibe-os 0.2.0 → 0.4.0
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 +19 -847
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/dist/web/assets/index-82MhMOB4.css +1 -0
- package/dist/web/assets/index-82MhMOB4.css.br +0 -0
- package/dist/web/assets/index-82MhMOB4.css.gz +0 -0
- package/dist/web/assets/index-DCB2nO0u.js +36 -0
- package/dist/web/assets/index-DCB2nO0u.js.br +0 -0
- package/dist/web/assets/index-DCB2nO0u.js.gz +0 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/scripts/notices.ts +3 -1
- package/server/api.ts +505 -20
- package/server/assets.generated.ts +13 -13
- package/server/attach.ts +65 -16
- package/server/browser.ts +1 -65
- package/server/cli.ts +48 -150
- package/server/client.ts +440 -0
- package/server/config.ts +64 -2
- package/server/db.ts +19 -0
- package/server/doctor.ts +176 -123
- package/server/harness.ts +378 -590
- package/server/harnesses/claude.ts +248 -0
- package/server/harnesses/codex.ts +241 -0
- package/server/harnesses/cursor.ts +202 -0
- package/server/harnesses/hermes.ts +259 -0
- package/server/harnesses/util.ts +181 -0
- package/server/headless.ts +417 -0
- package/server/index.ts +47 -14
- package/server/memory.ts +418 -0
- package/server/profiles.ts +27 -15
- package/server/projects.ts +52 -3
- package/server/reaper.ts +5 -3
- package/server/screens.ts +392 -0
- package/server/session.ts +84 -95
- package/server/setup.ts +73 -7
- package/server/windows.ts +51 -1
- package/shared/blanks.ts +113 -0
- package/shared/harness.ts +375 -0
- package/shared/wire.ts +107 -94
- package/dist/web/assets/index-B77Qk8B5.js +0 -35
- package/dist/web/assets/index-B77Qk8B5.js.br +0 -0
- package/dist/web/assets/index-B77Qk8B5.js.gz +0 -0
- package/dist/web/assets/index-CH5Em-Mo.css +0 -1
- package/dist/web/assets/index-CH5Em-Mo.css.br +0 -0
- package/dist/web/assets/index-CH5Em-Mo.css.gz +0 -0
package/server/attach.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// process that owns the pty is `dtach -n <socket> …` and every attachment is
|
|
21
21
|
// `dtach -a <socket>`. They differ in argv, so the two are never confused.
|
|
22
22
|
|
|
23
|
-
import { execFile } from "node:child_process";
|
|
23
|
+
import { execFile, spawn } from "node:child_process";
|
|
24
24
|
import { readdir, rm } from "node:fs/promises";
|
|
25
25
|
import { promisify } from "node:util";
|
|
26
26
|
import { and, desc, eq } from "drizzle-orm";
|
|
@@ -84,6 +84,8 @@ export interface AttachTarget {
|
|
|
84
84
|
project: string;
|
|
85
85
|
/** The role the window was opened as, when it has one. */
|
|
86
86
|
role: string | null;
|
|
87
|
+
/** The caller's own tag for the window. */
|
|
88
|
+
label: string | null;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
// ── resolution ───────────────────────────────────────────────────────────────
|
|
@@ -114,6 +116,7 @@ const targetRows = (db: Db) =>
|
|
|
114
116
|
lastOpenedAt: workspaces.lastOpenedAt,
|
|
115
117
|
project: projects.name,
|
|
116
118
|
role: profiles.name,
|
|
119
|
+
label: windows.label,
|
|
117
120
|
})
|
|
118
121
|
.from(windows)
|
|
119
122
|
.innerJoin(workspaces, eq(windows.workspaceId, workspaces.id))
|
|
@@ -133,6 +136,7 @@ const toTarget = (r: TargetRow): AttachTarget => ({
|
|
|
133
136
|
workspace: r.workspace,
|
|
134
137
|
project: r.project,
|
|
135
138
|
role: r.role,
|
|
139
|
+
label: r.label,
|
|
136
140
|
});
|
|
137
141
|
|
|
138
142
|
export function resolveTarget(
|
|
@@ -189,41 +193,86 @@ export function commandFor(
|
|
|
189
193
|
db: Db,
|
|
190
194
|
config: Config,
|
|
191
195
|
target: AttachTarget,
|
|
196
|
+
opts?: { resume?: boolean },
|
|
192
197
|
): string | undefined {
|
|
193
198
|
const profile = target.profileId
|
|
194
199
|
? getProfile(db, target.profileId)
|
|
195
200
|
: undefined;
|
|
196
|
-
return windowCommand(target.session, target.cwd, config, profile);
|
|
201
|
+
return windowCommand(target.session, target.cwd, config, profile, opts);
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
// ── talking to dtach ─────────────────────────────────────────────────────────
|
|
200
205
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Whether something still holds the other end of a session socket.
|
|
208
|
+
*
|
|
209
|
+
* Writing nothing to it succeeds only then, which is what separates a live
|
|
210
|
+
* session from a socket a killed one left behind. "Nothing" has to be an empty
|
|
211
|
+
* stdin: `dtach -p` sends stdin until EOF, and execFile hands it a pipe that is
|
|
212
|
+
* never closed, so every probe ran to its timeout and read as dead.
|
|
213
|
+
*/
|
|
214
|
+
function answers(sock: string): Promise<boolean> {
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
const child = spawn("dtach", ["-p", sock], {
|
|
217
|
+
stdio: "ignore",
|
|
218
|
+
timeout: 10_000,
|
|
219
|
+
});
|
|
220
|
+
child.on("error", () => resolve(false));
|
|
221
|
+
child.on("exit", (code) => resolve(code === 0));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Every socket in the sessions directory, split by whether anything answers.
|
|
227
|
+
*
|
|
228
|
+
* A dead one is a session that died without its harness exiting: dtach removes
|
|
229
|
+
* its socket when the program underneath exits, and only a kill (a reboot, the
|
|
230
|
+
* OOM killer) leaves the file behind with nothing holding it.
|
|
231
|
+
*/
|
|
232
|
+
async function probeSessions(
|
|
233
|
+
config: Config,
|
|
234
|
+
): Promise<{ live: Set<string>; dead: Set<string> }> {
|
|
235
|
+
const live = new Set<string>();
|
|
236
|
+
const dead = new Set<string>();
|
|
203
237
|
try {
|
|
204
238
|
const dir = `${config.stateDir}/sessions`;
|
|
205
239
|
const names = await readdir(dir);
|
|
206
|
-
const live = new Set<string>();
|
|
207
240
|
await Promise.all(
|
|
208
241
|
names
|
|
209
242
|
.filter((n) => n.endsWith(".sock"))
|
|
210
243
|
.map(async (n) => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// a socket a crashed one left behind.
|
|
214
|
-
const ok = await run("dtach", ["-p", `${dir}/${n}`], {
|
|
215
|
-
timeout: 10_000,
|
|
216
|
-
})
|
|
217
|
-
.then(() => true)
|
|
218
|
-
.catch(() => false);
|
|
219
|
-
if (ok) live.add(n.replace(/\.sock$/, ""));
|
|
244
|
+
const ok = await answers(`${dir}/${n}`);
|
|
245
|
+
(ok ? live : dead).add(n.replace(/\.sock$/, ""));
|
|
220
246
|
}),
|
|
221
247
|
);
|
|
222
|
-
return live;
|
|
223
248
|
} catch {
|
|
224
249
|
// No sessions directory yet: nothing has ever been opened.
|
|
225
|
-
return new Set();
|
|
226
250
|
}
|
|
251
|
+
return { live, dead };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Sessions with something still holding the other end of their socket. */
|
|
255
|
+
export async function liveSessions(config: Config): Promise<Set<string>> {
|
|
256
|
+
return (await probeSessions(config)).live;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Windows whose session died under them rather than ending.
|
|
261
|
+
*
|
|
262
|
+
* Meant for server start, before any browser logs in: the first login to a
|
|
263
|
+
* window removes its dead socket on the way to creating a new session, and
|
|
264
|
+
* with it the only evidence there was something to bring back.
|
|
265
|
+
*/
|
|
266
|
+
export async function interruptedWindows(
|
|
267
|
+
db: Db,
|
|
268
|
+
config: Config,
|
|
269
|
+
): Promise<string[]> {
|
|
270
|
+
if (!config.sessions) return [];
|
|
271
|
+
const { dead } = await probeSessions(config);
|
|
272
|
+
if (dead.size === 0) return [];
|
|
273
|
+
return listTargets(db)
|
|
274
|
+
.filter((t) => dead.has(t.session))
|
|
275
|
+
.map((t) => t.windowId);
|
|
227
276
|
}
|
|
228
277
|
|
|
229
278
|
/**
|
package/server/browser.ts
CHANGED
|
@@ -167,10 +167,7 @@ export interface Unit {
|
|
|
167
167
|
/**
|
|
168
168
|
* Where the units are written, named once.
|
|
169
169
|
*
|
|
170
|
-
* `install-browser` writes them and
|
|
171
|
-
* discovery in `harness.ts`, and `connect-hermes`. They had a path literal each
|
|
172
|
-
* until the third one wanted the same file, which is one copy past the point
|
|
173
|
-
* where they can be trusted to stay in step.
|
|
170
|
+
* `install-browser` writes them and doctor goes looking for them.
|
|
174
171
|
*/
|
|
175
172
|
export const UNIT_DIR = "/etc/systemd/system";
|
|
176
173
|
export const CHROME_UNIT_PATH = `${UNIT_DIR}/vibe-os-chrome.service`;
|
|
@@ -419,67 +416,6 @@ export function vncPasswordFileFromUnit(
|
|
|
419
416
|
return match ? match[1] : null;
|
|
420
417
|
}
|
|
421
418
|
|
|
422
|
-
/**
|
|
423
|
-
* What Hermes has to be told so its browser tools drive this Chrome.
|
|
424
|
-
*
|
|
425
|
-
* Hermes reads its CDP target from `browser.cdp_url` in `~/.hermes/config.yaml`
|
|
426
|
-
* and offers no flag for it, so this is set once for the box rather than per
|
|
427
|
-
* profile. `browser-use` is named explicitly even though it is already the
|
|
428
|
-
* default, because the default is conditional on the CLI being runnable and a
|
|
429
|
-
* config that says what it means survives someone else reading it.
|
|
430
|
-
*
|
|
431
|
-
* `cloud_provider: local` is what stops a Browserbase or Firecrawl key, set
|
|
432
|
-
* later for something else, from quietly taking the browser back.
|
|
433
|
-
*
|
|
434
|
-
* Returned as pairs rather than written here: they are handed to
|
|
435
|
-
* `hermes config set`, which owns that file and knows where a secret goes.
|
|
436
|
-
*/
|
|
437
|
-
export function hermesBrowserSettings(
|
|
438
|
-
cdpPort: number,
|
|
439
|
-
): { key: string; value: string }[] {
|
|
440
|
-
return [
|
|
441
|
-
{ key: "browser.cdp_url", value: `http://127.0.0.1:${cdpPort}` },
|
|
442
|
-
{ key: "browser.backend", value: "browser-use" },
|
|
443
|
-
{ key: "browser.cloud_provider", value: "local" },
|
|
444
|
-
];
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Whether a configured `cdp_url` names this box's Chrome.
|
|
449
|
-
*
|
|
450
|
-
* Written by hand as often as by `connect-hermes`, so the comparison is on what
|
|
451
|
-
* the URL means rather than on its spelling: `localhost` and `127.0.0.1` are the
|
|
452
|
-
* same host, and `ws://` and `http://` are the same endpoint to a CDP client.
|
|
453
|
-
* A bare `127.0.0.1:9222` is accepted too, because it is what people type.
|
|
454
|
-
*
|
|
455
|
-
* The port has to be explicit. A URL with no port is not a CDP endpoint that
|
|
456
|
-
* would ever have worked, and inferring 80 from `http://` would report a
|
|
457
|
-
* misconfigured box as connected.
|
|
458
|
-
*/
|
|
459
|
-
export function cdpUrlMatches(
|
|
460
|
-
configured: string | null | undefined,
|
|
461
|
-
cdpPort: number,
|
|
462
|
-
): boolean {
|
|
463
|
-
const raw = configured?.trim();
|
|
464
|
-
if (!raw) return false;
|
|
465
|
-
// A scheme is optional in what people write but not in what URL will parse.
|
|
466
|
-
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw)
|
|
467
|
-
? raw
|
|
468
|
-
: `http://${raw}`;
|
|
469
|
-
let url: URL;
|
|
470
|
-
try {
|
|
471
|
-
url = new URL(withScheme);
|
|
472
|
-
} catch {
|
|
473
|
-
return false;
|
|
474
|
-
}
|
|
475
|
-
if (!/^(https?|wss?):$/.test(url.protocol)) return false;
|
|
476
|
-
if (url.port !== String(cdpPort)) return false;
|
|
477
|
-
// Chrome binds the debug port to loopback, so anything else is a different
|
|
478
|
-
// browser on a different machine and saying "connected" would be a guess.
|
|
479
|
-
const host = url.hostname.replace(/^\[/, "").replace(/\]$/, "");
|
|
480
|
-
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
481
|
-
}
|
|
482
|
-
|
|
483
419
|
export interface MemInfo {
|
|
484
420
|
totalKb: number;
|
|
485
421
|
swapKb: number;
|
package/server/cli.ts
CHANGED
|
@@ -18,17 +18,14 @@ import {
|
|
|
18
18
|
BROWSER_COMMANDS,
|
|
19
19
|
browserServices,
|
|
20
20
|
browserUnits,
|
|
21
|
-
CHROME_UNIT_PATH,
|
|
22
|
-
DEFAULT_CDP_PORT,
|
|
23
21
|
defaultBrowserOptions,
|
|
24
|
-
hermesBrowserSettings,
|
|
25
|
-
portsFromUnits,
|
|
26
22
|
type Unit,
|
|
27
23
|
VNC_PASSWORD_LENGTH,
|
|
28
24
|
vncPasswordFileFromUnit,
|
|
29
25
|
vncPasswordPath,
|
|
30
26
|
XVNC_UNIT_PATH,
|
|
31
27
|
} from "./browser.ts";
|
|
28
|
+
import { runClient } from "./client.ts";
|
|
32
29
|
import {
|
|
33
30
|
type Config,
|
|
34
31
|
parseCliArgs,
|
|
@@ -38,12 +35,13 @@ import {
|
|
|
38
35
|
} from "./config.ts";
|
|
39
36
|
import { openDb } from "./db.ts";
|
|
40
37
|
import { type Check, homeFor, onPath, runDoctor } from "./doctor.ts";
|
|
41
|
-
import {
|
|
38
|
+
import { loadHarnesses } from "./harness.ts";
|
|
42
39
|
import { startServer } from "./index.ts";
|
|
43
40
|
import { color, describeError, log } from "./log.ts";
|
|
44
41
|
import { reapLeaks } from "./reaper.ts";
|
|
45
42
|
import { ENTRY, FETCH_WASM, IS_COMPILED } from "./runtime.ts";
|
|
46
43
|
import { confirmFirewall, runSetup } from "./setup.ts";
|
|
44
|
+
import { takeResume } from "./windows.ts";
|
|
47
45
|
|
|
48
46
|
const run = promisify(execFile);
|
|
49
47
|
const BUN = process.execPath;
|
|
@@ -60,9 +58,20 @@ const HELP = `
|
|
|
60
58
|
vibe-os setup fix what doctor finds, asking first
|
|
61
59
|
vibe-os install-service write and enable a systemd unit (needs root)
|
|
62
60
|
vibe-os install-browser run one Chrome on a virtual display (needs root)
|
|
63
|
-
vibe-os connect-hermes point Hermes' browser tools at that Chrome
|
|
64
61
|
vibe-os fetch-wasm (re)download the SSH WASM runtime
|
|
65
62
|
|
|
63
|
+
vibe-os ls list windows, and what each is doing
|
|
64
|
+
vibe-os workspace new <project> [--from <branch>] [--name <name>]
|
|
65
|
+
cut a workspace
|
|
66
|
+
vibe-os workspace ls [project]
|
|
67
|
+
list workspaces
|
|
68
|
+
vibe-os open <workspace> [--profile <name>] [--prompt <text>]
|
|
69
|
+
[--blank label=value] [--submit] [--force] [--label <text>]
|
|
70
|
+
open a window and start it
|
|
71
|
+
vibe-os output <window> [--lines <n>]
|
|
72
|
+
print what a window shows
|
|
73
|
+
vibe-os close <window> close a window and end its session
|
|
74
|
+
|
|
66
75
|
${color.bold("Options")}
|
|
67
76
|
--port <n> HTTP port (default 80)
|
|
68
77
|
--host <addr> bind address (default 0.0.0.0)
|
|
@@ -86,6 +95,13 @@ const HELP = `
|
|
|
86
95
|
one the browser reached the desktop on
|
|
87
96
|
--user <name> unix user to log in as (default: current user)
|
|
88
97
|
--no-sessions plain login shells instead of persistent dtach sessions
|
|
98
|
+
--memory-high <size>
|
|
99
|
+
throttle a window past this (default 40%, of RAM)
|
|
100
|
+
--memory-max <size> kill a window past this (default 50%)
|
|
101
|
+
--memory-swap-max <size>
|
|
102
|
+
swap a window may use (default 10%, of RAM). Sizes
|
|
103
|
+
are systemd's: 1500M, 2G, 40%, infinity
|
|
104
|
+
--no-memory-limit start windows without a memory scope
|
|
89
105
|
--cert-ttl <secs> certificate lifetime (default 43200)
|
|
90
106
|
|
|
91
107
|
--workspace <dir> root for projects and worktrees (default ~/workspace)
|
|
@@ -95,6 +111,21 @@ const HELP = `
|
|
|
95
111
|
-h, --help show this
|
|
96
112
|
-v, --version print the version
|
|
97
113
|
|
|
114
|
+
${color.bold("ls, workspace, open, output, close")}
|
|
115
|
+
--url <url> the server to talk to (default VIBE_OS_URL, or on this
|
|
116
|
+
box the address start is listening on)
|
|
117
|
+
--token <value> its token (default VIBE_OS_TOKEN, or on this box the
|
|
118
|
+
one start remembered)
|
|
119
|
+
--json print JSON instead of text
|
|
120
|
+
--prompt <text> paste this into the window; - reads it from stdin
|
|
121
|
+
--blank <label=value>
|
|
122
|
+
fill a blank in the profile's prompt and paste that.
|
|
123
|
+
Repeatable
|
|
124
|
+
--submit press Enter after pasting
|
|
125
|
+
--force open even when the box is short of memory
|
|
126
|
+
--label <text> tag the window, to find it again in ls --json
|
|
127
|
+
--lines <n> how many lines output prints (default 50)
|
|
128
|
+
|
|
98
129
|
${color.bold("install-browser")}
|
|
99
130
|
--geometry <WxH> virtual screen size (default 1600x900)
|
|
100
131
|
--display <n> X display number (default 99)
|
|
@@ -109,10 +140,6 @@ const HELP = `
|
|
|
109
140
|
this. An existing one is kept unless --no-vnc-password
|
|
110
141
|
--no-vnc-password serve the display with no authentication
|
|
111
142
|
|
|
112
|
-
${color.bold("connect-hermes")}
|
|
113
|
-
--cdp-port <n> the debug port to point Hermes at, when there is no
|
|
114
|
-
installed browser unit to read it from
|
|
115
|
-
|
|
116
143
|
${color.bold("doctor")}
|
|
117
144
|
--reap kill processes that outlived their workspace or session
|
|
118
145
|
|
|
@@ -308,7 +335,7 @@ async function installBrowser(
|
|
|
308
335
|
` ${color.cyan("sudo apt install -y tigervnc-standalone-server openbox x11-utils")}`,
|
|
309
336
|
);
|
|
310
337
|
console.log(
|
|
311
|
-
` ${color.dim("Chrome is not in the distro repos;
|
|
338
|
+
` ${color.dim("Chrome is not in the distro repos; for the Google apt repo see https://github.com/tsconfigdotjson/vibe-os/blob/main/docs/deploying.md#an-always-on-browser")}`,
|
|
312
339
|
);
|
|
313
340
|
console.log("");
|
|
314
341
|
return 1;
|
|
@@ -427,142 +454,6 @@ async function installBrowser(
|
|
|
427
454
|
` ${color.dim("Sign in to the extension once, through the viewer. The profile keeps it.")}`,
|
|
428
455
|
);
|
|
429
456
|
|
|
430
|
-
// Last, and quietly. Hermes usually arrives after the browser, so not finding
|
|
431
|
-
// it is the common case rather than a failure worth interrupting the summary.
|
|
432
|
-
if (await connectHermes(user, home, opts.cdpPort, true)) {
|
|
433
|
-
console.log("");
|
|
434
|
-
console.log(
|
|
435
|
-
` ${color.dim(`Hermes browser tools point at 127.0.0.1:${opts.cdpPort}.`)}`,
|
|
436
|
-
);
|
|
437
|
-
} else {
|
|
438
|
-
console.log("");
|
|
439
|
-
console.log(
|
|
440
|
-
` ${color.dim("For Hermes browser tools, install it and run: vibe-os connect-hermes")}`,
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
console.log("");
|
|
444
|
-
return 0;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Points Hermes' browser tools at this box's Chrome.
|
|
449
|
-
*
|
|
450
|
-
* Every setting goes through `hermes config set` rather than into the YAML
|
|
451
|
-
* directly. Hermes owns that file, knows which of its keys are secrets and
|
|
452
|
-
* belong in `.env` instead, and migrates its own schema between versions —
|
|
453
|
-
* three things a hand-written merge here would have to keep guessing at.
|
|
454
|
-
*
|
|
455
|
-
* ── Why this is a box-wide setting and not a profile one ─────────────────────
|
|
456
|
-
* The CDP target lives at `browser.cdp_url` in `~/.hermes/config.yaml` and has
|
|
457
|
-
* no command-line equivalent, so there is nothing a profile could carry. Doing
|
|
458
|
-
* it per launch would mean two Python startups before every window and a global
|
|
459
|
-
* file rewritten by whichever window opened last.
|
|
460
|
-
*
|
|
461
|
-
* Returns false when Hermes is not installed, which is not an error: plenty of
|
|
462
|
-
* boxes run the browser for the Claude extension and nothing else.
|
|
463
|
-
*/
|
|
464
|
-
async function connectHermes(
|
|
465
|
-
user: string,
|
|
466
|
-
home: string,
|
|
467
|
-
cdpPort: number,
|
|
468
|
-
/** True when this process is root and has to drop to the login user. */
|
|
469
|
-
asUser: boolean,
|
|
470
|
-
): Promise<boolean> {
|
|
471
|
-
/*
|
|
472
|
-
* A login shell, deliberately.
|
|
473
|
-
*
|
|
474
|
-
* Hermes installs to `/usr/local/bin` when it can write there and
|
|
475
|
-
* `~/.local/bin` when it cannot, and root's PATH reaches neither reliably
|
|
476
|
-
* under systemd. `runuser -l` runs `.profile`, which is what put the second
|
|
477
|
-
* one on PATH in the first place. runuser is util-linux, the same package
|
|
478
|
-
* `flock` comes from, so it is present wherever the session lock already is.
|
|
479
|
-
*/
|
|
480
|
-
const asLoginUser = (script: string): [string, string[]] =>
|
|
481
|
-
asUser
|
|
482
|
-
? ["runuser", ["-u", user, "--", "/bin/sh", "-lc", script]]
|
|
483
|
-
: ["/bin/sh", ["-lc", script]];
|
|
484
|
-
|
|
485
|
-
const [probe, probeArgs] = asLoginUser("command -v hermes");
|
|
486
|
-
const installed = await run(probe, probeArgs, {
|
|
487
|
-
timeout: 10_000,
|
|
488
|
-
env: { ...process.env, HOME: home },
|
|
489
|
-
})
|
|
490
|
-
.then(({ stdout }) => Boolean(stdout.trim()))
|
|
491
|
-
.catch(() => false);
|
|
492
|
-
if (!installed) return false;
|
|
493
|
-
|
|
494
|
-
for (const { key, value } of hermesBrowserSettings(cdpPort)) {
|
|
495
|
-
// Quoted as a single argument: a value never contains a space today, and
|
|
496
|
-
// the day one does is not the day to find out this was a bare expansion.
|
|
497
|
-
const [cmd, args] = asLoginUser(
|
|
498
|
-
`hermes config set ${JSON.stringify(key)} ${JSON.stringify(value)}`,
|
|
499
|
-
);
|
|
500
|
-
try {
|
|
501
|
-
await run(cmd, args, {
|
|
502
|
-
timeout: 60_000,
|
|
503
|
-
env: { ...process.env, HOME: home },
|
|
504
|
-
});
|
|
505
|
-
log.ok(`hermes ${key} = ${value}`);
|
|
506
|
-
} catch (err) {
|
|
507
|
-
log.error(`could not set hermes ${key}: ${describeError(err)}`);
|
|
508
|
-
return false;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
return true;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* `vibe-os connect-hermes` — the same thing, for a Hermes installed later.
|
|
516
|
-
*
|
|
517
|
-
* `install-browser` does this as its last step, but it is the browser's
|
|
518
|
-
* installer and Hermes usually arrives after it. No root: it writes one file in
|
|
519
|
-
* your own home directory, through a command you own.
|
|
520
|
-
*/
|
|
521
|
-
async function connectHermesCommand(
|
|
522
|
-
config: Config,
|
|
523
|
-
values: RawOptions,
|
|
524
|
-
): Promise<number> {
|
|
525
|
-
const user = process.env.SUDO_USER ?? config.user;
|
|
526
|
-
const home = (await homeFor(user)) ?? os.homedir();
|
|
527
|
-
|
|
528
|
-
// What the unit actually opened, not what the default is. install-browser may
|
|
529
|
-
// have been given --cdp-port long ago, and writing the default into Hermes
|
|
530
|
-
// would point it at a port nothing is listening on.
|
|
531
|
-
const unit = await readFile(CHROME_UNIT_PATH, "utf8").catch(() => null);
|
|
532
|
-
const cdpPort = values["cdp-port"]
|
|
533
|
-
? numeric(values["cdp-port"], "cdp-port")
|
|
534
|
-
: unit
|
|
535
|
-
? portsFromUnits({ chrome: unit }).cdpPort
|
|
536
|
-
: DEFAULT_CDP_PORT;
|
|
537
|
-
|
|
538
|
-
if (!unit && !values["cdp-port"]) {
|
|
539
|
-
log.warn(
|
|
540
|
-
`no browser installed here, so this assumes the default port ${cdpPort}`,
|
|
541
|
-
);
|
|
542
|
-
log.warn("run sudo vibe-os install-browser first, or pass --cdp-port");
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const connected = await connectHermes(
|
|
546
|
-
user,
|
|
547
|
-
home,
|
|
548
|
-
cdpPort,
|
|
549
|
-
process.getuid?.() === 0,
|
|
550
|
-
);
|
|
551
|
-
if (!connected) {
|
|
552
|
-
log.error(`hermes is not installed for ${user}`);
|
|
553
|
-
console.log("");
|
|
554
|
-
console.log(` ${color.cyan(HERMES_INSTALL)}`);
|
|
555
|
-
console.log("");
|
|
556
|
-
return 1;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
console.log("");
|
|
560
|
-
console.log(
|
|
561
|
-
` Hermes browser tools will drive the box's Chrome on 127.0.0.1:${cdpPort}.`,
|
|
562
|
-
);
|
|
563
|
-
console.log(
|
|
564
|
-
` ${color.dim("Watch it work: ssh -L 5900:127.0.0.1:5900, then any VNC viewer.")}`,
|
|
565
|
-
);
|
|
566
457
|
console.log("");
|
|
567
458
|
return 0;
|
|
568
459
|
}
|
|
@@ -714,7 +605,9 @@ async function attach(
|
|
|
714
605
|
}
|
|
715
606
|
if (!target) return 0;
|
|
716
607
|
|
|
717
|
-
const command = commandFor(db, config, target
|
|
608
|
+
const command = commandFor(db, config, target, {
|
|
609
|
+
resume: takeResume(db, target.windowId),
|
|
610
|
+
});
|
|
718
611
|
if (!command) {
|
|
719
612
|
log.error(`could not build a command for ${target.ref}`);
|
|
720
613
|
return 1;
|
|
@@ -795,6 +688,7 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
795
688
|
let config: Awaited<ReturnType<typeof resolveConfig>>;
|
|
796
689
|
try {
|
|
797
690
|
config = await resolveConfig(values);
|
|
691
|
+
loadHarnesses(config.stateDir);
|
|
798
692
|
} catch (err) {
|
|
799
693
|
log.error(describeError(err));
|
|
800
694
|
console.log(HELP);
|
|
@@ -828,8 +722,6 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
828
722
|
return installService(config, argv);
|
|
829
723
|
case "install-browser":
|
|
830
724
|
return installBrowser(config, values);
|
|
831
|
-
case "connect-hermes":
|
|
832
|
-
return connectHermesCommand(config, values);
|
|
833
725
|
case "fetch-wasm": {
|
|
834
726
|
// A standalone binary carries the wasm inside it: there is no script on
|
|
835
727
|
// disk to run (FETCH_WASM points into the virtual /$bunfs root) and
|
|
@@ -846,6 +738,12 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
846
738
|
process.stdout.write(stdout);
|
|
847
739
|
return 0;
|
|
848
740
|
}
|
|
741
|
+
case "ls":
|
|
742
|
+
case "workspace":
|
|
743
|
+
case "open":
|
|
744
|
+
case "output":
|
|
745
|
+
case "close":
|
|
746
|
+
return runClient(command, positionals.slice(1), values, config);
|
|
849
747
|
case "help":
|
|
850
748
|
console.log(HELP);
|
|
851
749
|
return 0;
|