mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
Compare commits
3 Commits
feat/click
...
fix/dev-wa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a423852471 | ||
|
|
5712515bec | ||
|
|
092cfc90e0 |
@@ -32,6 +32,8 @@ export interface LimaShellProcess {
|
||||
exited: Promise<number>
|
||||
}
|
||||
|
||||
const LIMA_VERBOSE_LOGGING = false
|
||||
|
||||
export class LimaCli {
|
||||
constructor(private readonly cfg: LimaCliConfig) {}
|
||||
|
||||
@@ -159,15 +161,18 @@ export class LimaCli {
|
||||
limaHome: this.cfg.limaHome,
|
||||
})
|
||||
|
||||
const stderrLogger = LIMA_VERBOSE_LOGGING
|
||||
? (line: string) => {
|
||||
logger.debug(VM_TELEMETRY_EVENTS.limaStderrChunk, {
|
||||
pid: proc.pid,
|
||||
firstArg: args[0],
|
||||
line,
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
drainToString(proc.stdout),
|
||||
drainToString(proc.stderr, (line) => {
|
||||
logger.debug(VM_TELEMETRY_EVENTS.limaStderrChunk, {
|
||||
pid: proc.pid,
|
||||
firstArg: args[0],
|
||||
line,
|
||||
})
|
||||
}),
|
||||
drainToString(proc.stderr, stderrLogger),
|
||||
proc.exited,
|
||||
])
|
||||
const durationMs = Date.now() - started
|
||||
|
||||
@@ -4,14 +4,23 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs'
|
||||
import { accessSync, constants, existsSync } from 'node:fs'
|
||||
import { homedir, arch as osArch } from 'node:os'
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
||||
import {
|
||||
delimiter,
|
||||
dirname,
|
||||
isAbsolute,
|
||||
join,
|
||||
relative,
|
||||
resolve,
|
||||
sep,
|
||||
} from 'node:path'
|
||||
import { PATHS } from '@browseros/shared/constants/paths'
|
||||
|
||||
export const VM_NAME = 'browseros-vm'
|
||||
export const GUEST_VM_STATE = '/mnt/browseros/vm'
|
||||
export const GUEST_IMAGE_CACHE = '/mnt/browseros/cache/images'
|
||||
const HOST_LIMACTL_BINARY = 'limactl'
|
||||
|
||||
export type Arch = 'arm64' | 'x64'
|
||||
|
||||
@@ -88,7 +97,7 @@ export function decompressedDiskPath(
|
||||
}
|
||||
|
||||
export function resolveBundledLimactl(resourcesDir: string): string {
|
||||
if (usesHostVmTools()) return 'limactl'
|
||||
if (usesHostVmTools()) return resolveHostLimactl()
|
||||
|
||||
const candidate = join(resourcesDir, 'bin', 'third_party', 'lima', 'limactl')
|
||||
if (!existsSync(candidate)) {
|
||||
@@ -99,6 +108,14 @@ export function resolveBundledLimactl(resourcesDir: string): string {
|
||||
return candidate
|
||||
}
|
||||
|
||||
function resolveHostLimactl(): string {
|
||||
const resolved = findExecutableOnPath(HOST_LIMACTL_BINARY)
|
||||
if (resolved) return resolved
|
||||
throw new Error(
|
||||
'Lima is not installed or limactl is not on PATH. Install with brew install lima.',
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveBundledLimaTemplate(resourcesDir: string): string {
|
||||
if (usesHostVmTools()) {
|
||||
const sourceTemplate = findSourceLimaTemplate(resourcesDir)
|
||||
@@ -120,6 +137,20 @@ function usesHostVmTools(): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
function findExecutableOnPath(binary: string): string | null {
|
||||
const pathEnv = process.env.PATH
|
||||
if (!pathEnv) return null
|
||||
for (const dir of pathEnv.split(delimiter)) {
|
||||
if (!dir) continue
|
||||
const candidate = join(dir, binary)
|
||||
try {
|
||||
accessSync(candidate, constants.X_OK)
|
||||
return candidate
|
||||
} catch {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function findSourceLimaTemplate(resourcesDir: string): string | null {
|
||||
let current = resolve(resourcesDir)
|
||||
while (true) {
|
||||
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { logger } from '../../../src/lib/logger'
|
||||
import { LimaCommandError, VmNotReadyError } from '../../../src/lib/vm/errors'
|
||||
import { LimaCli } from '../../../src/lib/vm/lima-cli'
|
||||
import { VM_TELEMETRY_EVENTS } from '../../../src/lib/vm/telemetry'
|
||||
import { fakeLimactl } from '../../__helpers__/fake-limactl'
|
||||
import { fakeSsh } from '../../__helpers__/fake-ssh'
|
||||
|
||||
@@ -104,6 +106,23 @@ describe('LimaCli', () => {
|
||||
expect(error.stderr).toBe('cannot start')
|
||||
})
|
||||
|
||||
it('does not log limactl stderr chunks by default', async () => {
|
||||
const debug = spyOn(logger, 'debug').mockImplementation(() => {})
|
||||
const limactlPath = await fakeLimactl(
|
||||
{ start: { stderr: 'boot noise\n' } },
|
||||
logPath,
|
||||
)
|
||||
const cli = new LimaCli({ limactlPath, limaHome })
|
||||
|
||||
await cli.start('browseros-vm')
|
||||
|
||||
expect(
|
||||
debug.mock.calls.some(
|
||||
([message]) => message === VM_TELEMETRY_EVENTS.limaStderrChunk,
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('stops and deletes VMs', async () => {
|
||||
const limactlPath = await fakeLimactl({ stop: {}, delete: {} }, logPath)
|
||||
const cli = new LimaCli({ limactlPath, limaHome })
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it } from 'bun:test'
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { PATHS } from '@browseros/shared/constants/paths'
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
|
||||
describe('VM paths', () => {
|
||||
const originalNodeEnv = process.env.NODE_ENV
|
||||
const originalPath = process.env.PATH
|
||||
|
||||
afterEach(() => {
|
||||
if (originalNodeEnv === undefined) {
|
||||
@@ -35,6 +36,11 @@ describe('VM paths', () => {
|
||||
} else {
|
||||
process.env.NODE_ENV = originalNodeEnv
|
||||
}
|
||||
if (originalPath === undefined) {
|
||||
delete process.env.PATH
|
||||
} else {
|
||||
process.env.PATH = originalPath
|
||||
}
|
||||
})
|
||||
|
||||
it('uses production VM directories below .browseros', () => {
|
||||
@@ -129,16 +135,44 @@ describe('VM paths', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('uses PATH limactl in development mode', () => {
|
||||
it('uses PATH limactl in development mode', async () => {
|
||||
process.env.NODE_ENV = 'development'
|
||||
const binDir = await createFakeLimactlPath()
|
||||
|
||||
expect(resolveBundledLimactl('/tmp/missing-dev-resources')).toBe('limactl')
|
||||
try {
|
||||
expect(resolveBundledLimactl('/tmp/missing-dev-resources')).toBe(
|
||||
join(binDir, 'limactl'),
|
||||
)
|
||||
} finally {
|
||||
await rm(binDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('uses PATH limactl in test mode', () => {
|
||||
it('uses PATH limactl in test mode', async () => {
|
||||
process.env.NODE_ENV = 'test'
|
||||
const binDir = await createFakeLimactlPath()
|
||||
|
||||
expect(resolveBundledLimactl('/tmp/missing-test-resources')).toBe('limactl')
|
||||
try {
|
||||
expect(resolveBundledLimactl('/tmp/missing-test-resources')).toBe(
|
||||
join(binDir, 'limactl'),
|
||||
)
|
||||
} finally {
|
||||
await rm(binDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('throws with a brew install hint when host limactl is missing', async () => {
|
||||
process.env.NODE_ENV = 'development'
|
||||
const binDir = await mkdtemp(join(tmpdir(), 'missing-host-limactl-'))
|
||||
process.env.PATH = binDir
|
||||
|
||||
try {
|
||||
expect(() => resolveBundledLimactl('/tmp/missing-dev-resources')).toThrow(
|
||||
'brew install lima',
|
||||
)
|
||||
} finally {
|
||||
await rm(binDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('throws with a build-tools hint when bundled limactl is missing', () => {
|
||||
@@ -185,3 +219,12 @@ describe('VM paths', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function createFakeLimactlPath(): Promise<string> {
|
||||
const binDir = await mkdtemp(join(tmpdir(), 'host-limactl-'))
|
||||
const limactlPath = join(binDir, 'limactl')
|
||||
await writeFile(limactlPath, '#!/bin/sh\n')
|
||||
await chmod(limactlPath, 0o755)
|
||||
process.env.PATH = binDir
|
||||
return binDir
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -42,6 +43,9 @@ func runWatch(cmd *cobra.Command, args []string) error {
|
||||
if err := ensureDevCachePresent(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureLimactlPresent(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultPorts := proc.DefaultLocalPorts()
|
||||
p := defaultPorts
|
||||
@@ -191,7 +195,20 @@ func ensureDevCachePresent() error {
|
||||
}
|
||||
manifestPath := filepath.Join(home, ".browseros-dev", "cache", "vm", "manifest.json")
|
||||
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("VM cache missing at %s; run bun run dev:setup once", manifestPath)
|
||||
return fmt.Errorf("%s %s",
|
||||
proc.ErrorColor.Sprint("VM cache is missing."),
|
||||
proc.DimColor.Sprintf("Run %s once.", proc.BoldColor.Sprint("bun run dev:setup")),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureLimactlPresent() error {
|
||||
if _, err := exec.LookPath("limactl"); err != nil {
|
||||
return fmt.Errorf("%s %s",
|
||||
proc.ErrorColor.Sprint("Lima is not installed."),
|
||||
proc.DimColor.Sprintf("Install with %s.", proc.BoldColor.Sprint("brew install lima")),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
59
packages/browseros-agent/tools/dev/cmd/watch_test.go
Normal file
59
packages/browseros-agent/tools/dev/cmd/watch_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnsureDevCachePresentMissingMessage(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
|
||||
err := ensureDevCachePresent()
|
||||
if err == nil {
|
||||
t.Fatal("expected missing cache error")
|
||||
}
|
||||
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "VM cache is missing.") {
|
||||
t.Fatalf("expected missing cache message, got %q", msg)
|
||||
}
|
||||
if strings.Count(msg, "dev:setup") != 1 {
|
||||
t.Fatalf("expected dev:setup once, got %q", msg)
|
||||
}
|
||||
if strings.Contains(msg, home) {
|
||||
t.Fatalf("expected cache path to be hidden, got %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureLimactlPresentMissingMessage(t *testing.T) {
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
|
||||
err := ensureLimactlPresent()
|
||||
if err == nil {
|
||||
t.Fatal("expected missing Lima error")
|
||||
}
|
||||
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "Lima is not installed.") {
|
||||
t.Fatalf("expected missing Lima message, got %q", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "brew install lima") {
|
||||
t.Fatalf("expected brew install hint, got %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureLimactlPresentFindsPathBinary(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
limactlPath := filepath.Join(binDir, "limactl")
|
||||
if err := os.WriteFile(limactlPath, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
if err := ensureLimactlPresent(); err != nil {
|
||||
t.Fatalf("expected limactl to resolve, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ echo "[setup] Generating agent code..."
|
||||
bun run codegen:agent
|
||||
|
||||
echo "[setup] Syncing VM cache..."
|
||||
printf '\033[31m[setup] First VM cache sync can take about 5 minutes.\033[0m\n'
|
||||
NODE_ENV=development bun run --filter @browseros/build-tools cache:sync
|
||||
|
||||
echo "[setup] Ready"
|
||||
|
||||
Reference in New Issue
Block a user