mirror of
https://github.com/humanlayer/humanlayer.git
synced 2025-08-20 19:01:22 +03:00
Merge remote-tracking branch 'allison/allison/eng-1784-bundle-daemon-and-cli-with-wui-for-single-installation' into dexter/eng-1784-bundle-daemon-and-cli-with-wui-for-single-installation
This commit is contained in:
24
.github/workflows/release-macos.yml
vendored
24
.github/workflows/release-macos.yml
vendored
@@ -48,16 +48,32 @@ jobs:
|
||||
working-directory: humanlayer-wui
|
||||
run: bun install
|
||||
|
||||
- name: Build daemon for macOS ARM
|
||||
working-directory: hld
|
||||
run: GOOS=darwin GOARCH=arm64 go build -o hld-darwin-arm64 ./cmd/hld
|
||||
|
||||
- name: Build humanlayer CLI for macOS ARM
|
||||
working-directory: hlyr
|
||||
run: |
|
||||
bun install
|
||||
bun run build
|
||||
bun build ./dist/index.js --compile --target=bun-darwin-arm64 --outfile=humanlayer-darwin-arm64
|
||||
chmod +x humanlayer-darwin-arm64
|
||||
|
||||
- name: Copy binaries to Tauri resources
|
||||
run: |
|
||||
mkdir -p humanlayer-wui/src-tauri/bin
|
||||
cp hld/hld-darwin-arm64 humanlayer-wui/src-tauri/bin/hld
|
||||
cp hlyr/humanlayer-darwin-arm64 humanlayer-wui/src-tauri/bin/humanlayer
|
||||
chmod +x humanlayer-wui/src-tauri/bin/hld
|
||||
chmod +x humanlayer-wui/src-tauri/bin/humanlayer
|
||||
|
||||
- name: Build Tauri app (including DMG)
|
||||
working-directory: humanlayer-wui
|
||||
run: bun run tauri build
|
||||
env:
|
||||
APPLE_SIGNING_IDENTITY: "-" # Ad-hoc signing to prevent "damaged" error
|
||||
|
||||
- name: Build daemon for macOS ARM
|
||||
working-directory: hld
|
||||
run: GOOS=darwin GOARCH=arm64 go build -o hld-darwin-arm64 ./cmd/hld
|
||||
|
||||
- name: Upload DMG artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
38
Makefile
38
Makefile
@@ -500,6 +500,39 @@ wui-nightly-build:
|
||||
cp -r humanlayer-wui/src-tauri/target/release/bundle/macos/CodeLayer.app ~/Applications/
|
||||
@echo "Installed WUI nightly to ~/Applications/CodeLayer.app"
|
||||
|
||||
# Build humanlayer binary for bundling
|
||||
.PHONY: humanlayer-build
|
||||
humanlayer-build:
|
||||
@echo "Building humanlayer CLI binary..."
|
||||
cd hlyr && bun install && bun run build
|
||||
@echo "humanlayer binary built at hlyr/dist/index.js"
|
||||
|
||||
# Build humanlayer standalone binary (requires bun)
|
||||
.PHONY: humanlayer-binary-darwin-arm64
|
||||
humanlayer-binary-darwin-arm64: humanlayer-build
|
||||
@echo "Creating standalone humanlayer binary for macOS ARM64..."
|
||||
cd hlyr && bun build ./dist/index.js --compile --target=bun-darwin-arm64 --outfile=humanlayer-darwin-arm64
|
||||
chmod +x hlyr/humanlayer-darwin-arm64
|
||||
@echo "Standalone binary created at hlyr/humanlayer-darwin-arm64"
|
||||
|
||||
# Build CodeLayer with bundled daemon and humanlayer
|
||||
.PHONY: codelayer-bundle
|
||||
codelayer-bundle:
|
||||
@echo "Building daemon for bundling..."
|
||||
cd hld && GOOS=darwin GOARCH=arm64 go build -o hld-darwin-arm64 ./cmd/hld
|
||||
@echo "Building humanlayer for bundling..."
|
||||
cd hlyr && bun install && bun run build
|
||||
cd hlyr && bun build ./dist/index.js --compile --target=bun-darwin-arm64 --outfile=humanlayer-darwin-arm64
|
||||
@echo "Copying binaries to CodeLayer resources..."
|
||||
mkdir -p humanlayer-wui/src-tauri/bin
|
||||
cp hld/hld-darwin-arm64 humanlayer-wui/src-tauri/bin/hld
|
||||
cp hlyr/humanlayer-darwin-arm64 humanlayer-wui/src-tauri/bin/humanlayer
|
||||
chmod +x humanlayer-wui/src-tauri/bin/hld
|
||||
chmod +x humanlayer-wui/src-tauri/bin/humanlayer
|
||||
@echo "Building CodeLayer with bundled binaries..."
|
||||
cd humanlayer-wui && bun run tauri build
|
||||
@echo "Build complete!"
|
||||
|
||||
# Open nightly WUI
|
||||
.PHONY: wui-nightly
|
||||
wui-nightly: wui-nightly-build
|
||||
@@ -570,6 +603,7 @@ daemon-dev: daemon-dev-build
|
||||
echo "$(TIMESTAMP) starting dev daemon in $$(pwd)" > ~/.humanlayer/logs/daemon-dev-$(TIMESTAMP).log
|
||||
cd hld && HUMANLAYER_DATABASE_PATH=~/.humanlayer/daemon-dev.db \
|
||||
HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock \
|
||||
HUMANLAYER_DAEMON_HTTP_PORT=0 \
|
||||
HUMANLAYER_DAEMON_VERSION_OVERRIDE=$$(git branch --show-current) \
|
||||
./run-with-logging.sh ~/.humanlayer/logs/daemon-dev-$(TIMESTAMP).log ./hld-dev
|
||||
|
||||
@@ -581,6 +615,10 @@ wui-dev:
|
||||
echo "$(TIMESTAMP) starting dev wui in $$(pwd)" > ~/.humanlayer/logs/wui-dev-$(TIMESTAMP).log
|
||||
cd humanlayer-wui && HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock bun run tauri dev 2>&1 | tee -a ~/.humanlayer/logs/wui-dev-$(TIMESTAMP).log
|
||||
|
||||
# Alias for wui-dev that ensures daemon is built first
|
||||
.PHONY: codelayer-dev
|
||||
codelayer-dev: daemon-dev-build wui-dev
|
||||
|
||||
# Show current dev environment setup
|
||||
.PHONY: dev-status
|
||||
dev-status:
|
||||
|
||||
@@ -115,12 +115,9 @@ func New() (*Daemon, error) {
|
||||
approvalManager := approval.NewManager(conversationStore, eventBus)
|
||||
slog.Debug("local approval manager created successfully")
|
||||
|
||||
// Create HTTP server if enabled
|
||||
var httpServer *HTTPServer
|
||||
if cfg.HTTPPort > 0 {
|
||||
// Create HTTP server (always enabled, port 0 means dynamic allocation)
|
||||
slog.Info("creating HTTP server", "port", cfg.HTTPPort)
|
||||
httpServer = NewHTTPServer(cfg, sessionManager, approvalManager, conversationStore, eventBus)
|
||||
}
|
||||
httpServer := NewHTTPServer(cfg, sessionManager, approvalManager, conversationStore, eventBus)
|
||||
|
||||
return &Daemon{
|
||||
config: cfg,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -90,17 +91,35 @@ func (s *HTTPServer) Start(ctx context.Context) error {
|
||||
// Register SSE endpoint directly (not part of strict interface)
|
||||
v1.GET("/stream/events", s.sseHandler.StreamEvents)
|
||||
|
||||
// Create HTTP server
|
||||
// Create listener first to handle port 0
|
||||
addr := fmt.Sprintf("%s:%d", s.config.HTTPHost, s.config.HTTPPort)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on %s: %w", addr, err)
|
||||
}
|
||||
|
||||
// Get actual port after binding
|
||||
actualAddr := listener.Addr().(*net.TCPAddr)
|
||||
actualPort := actualAddr.Port
|
||||
|
||||
// If port 0 was used, output actual port to stdout
|
||||
if s.config.HTTPPort == 0 {
|
||||
fmt.Printf("HTTP_PORT=%d\n", actualPort)
|
||||
}
|
||||
|
||||
slog.Info("Starting HTTP server",
|
||||
"configured_port", s.config.HTTPPort,
|
||||
"actual_address", actualAddr.String())
|
||||
|
||||
// Create HTTP server
|
||||
s.server = &http.Server{
|
||||
Addr: addr,
|
||||
Handler: s.router,
|
||||
}
|
||||
|
||||
// Start server in goroutine
|
||||
go func() {
|
||||
slog.Info("Starting HTTP server", "address", addr)
|
||||
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
// Use the existing listener
|
||||
if err := s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
slog.Error("HTTP server error", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
4
humanlayer-wui/.gitignore
vendored
4
humanlayer-wui/.gitignore
vendored
@@ -25,3 +25,7 @@ dist-ssr
|
||||
|
||||
# Generated Tauri config for local development
|
||||
src-tauri/tauri.conf.local.json
|
||||
|
||||
# Bundled binaries (created during build)
|
||||
src-tauri/bin/
|
||||
!src-tauri/bin/.gitkeep
|
||||
|
||||
@@ -4,17 +4,106 @@ Web/desktop UI for the HumanLayer daemon (`hld`) built with Tauri and React.
|
||||
|
||||
## Development
|
||||
|
||||
### Running in Development Mode
|
||||
|
||||
1. Build the daemon (required for auto-launch):
|
||||
|
||||
```bash
|
||||
make daemon-dev-build
|
||||
```
|
||||
|
||||
2. Start CodeLayer in development mode:
|
||||
```bash
|
||||
make codelayer-dev
|
||||
```
|
||||
|
||||
The daemon starts automatically and invisibly when the app launches. No manual daemon management needed.
|
||||
|
||||
### Disabling Auto-Launch (Advanced Users)
|
||||
|
||||
If you prefer to manage the daemon manually:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Start dev server
|
||||
bun run tauri dev
|
||||
|
||||
# Build for production
|
||||
bun run build
|
||||
export HUMANLAYER_WUI_AUTOLAUNCH_DAEMON=false
|
||||
make codelayer-dev
|
||||
```
|
||||
|
||||
### Using an External Daemon
|
||||
|
||||
To connect to a daemon running on a specific port:
|
||||
|
||||
```bash
|
||||
export HUMANLAYER_DAEMON_HTTP_PORT=7777
|
||||
make codelayer-dev
|
||||
```
|
||||
|
||||
### Building for Production
|
||||
|
||||
To build CodeLayer with bundled daemon:
|
||||
|
||||
```bash
|
||||
make codelayer-bundle
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
1. Build the daemon for macOS ARM64
|
||||
2. Build the humanlayer CLI for macOS ARM64
|
||||
3. Copy both to the Tauri resources
|
||||
4. Build CodeLayer with the bundled binaries
|
||||
|
||||
The resulting DMG will include both binaries and automatically manage their lifecycle.
|
||||
|
||||
### Daemon Management
|
||||
|
||||
The daemon lifecycle is completely automatic:
|
||||
|
||||
**In development mode:**
|
||||
|
||||
- Daemon starts invisibly when CodeLayer launches
|
||||
- Each git branch gets its own daemon instance
|
||||
- Database is copied from `daemon-dev.db` to `daemon-{branch}.db`
|
||||
- Socket and port are isolated per branch
|
||||
- Use debug panel (bottom-left settings icon) for manual control if needed
|
||||
|
||||
**In production mode:**
|
||||
|
||||
- Daemon starts invisibly when CodeLayer launches
|
||||
- Uses default paths (`~/.humanlayer/daemon.db`)
|
||||
- Stops automatically when the app exits
|
||||
- No user interaction or awareness required
|
||||
|
||||
**Error Handling:**
|
||||
|
||||
- If daemon fails to start, app continues normally
|
||||
- Connection can be established later via debug panel (dev) or automatically on retry
|
||||
- All errors are logged but never interrupt the user experience
|
||||
|
||||
### MCP Testing
|
||||
|
||||
To test MCP functionality:
|
||||
|
||||
**In development:**
|
||||
|
||||
- Ensure you have `humanlayer` installed globally: `npm install -g humanlayer`
|
||||
- Start CodeLayer: `make codelayer-dev`
|
||||
- Configure Claude Code to use `humanlayer mcp claude_approvals`
|
||||
- The MCP server will connect to your running daemon
|
||||
|
||||
**In production (after Homebrew installation):**
|
||||
|
||||
- Claude Code can directly execute `humanlayer mcp claude_approvals`
|
||||
- No npm or npx required - Homebrew automatically created symlinks in PATH
|
||||
- The MCP server connects to the daemon started by CodeLayer
|
||||
- Verify PATH setup is working: `which humanlayer` should show `/usr/local/bin/humanlayer`
|
||||
|
||||
**Troubleshooting MCP connection:**
|
||||
|
||||
- If MCP can't find `humanlayer`, restart Claude Code after installation
|
||||
- If launched from Dock, Claude Code may have limited PATH - launch from Terminal instead
|
||||
- Check daemon is running: `ps aux | grep hld`
|
||||
- Check MCP logs in Claude Code for connection errors
|
||||
|
||||
## Quick Start for Frontend Development
|
||||
|
||||
Always use React hooks, never the daemon client directly:
|
||||
|
||||
512
humanlayer-wui/src-tauri/Cargo.lock
generated
512
humanlayer-wui/src-tauri/Cargo.lock
generated
@@ -510,8 +510,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
@@ -559,6 +561,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
@@ -582,9 +594,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -595,7 +607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -747,13 +759,34 @@ dependencies = [
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.4.6",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -764,7 +797,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"redox_users 0.5.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
@@ -871,7 +904,7 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"toml",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
"winreg 0.55.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -880,6 +913,15 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.0"
|
||||
@@ -1007,6 +1049,15 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1014,7 +1065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1028,6 +1079,12 @@ dependencies = [
|
||||
"syn 2.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -1454,6 +1511,25 @@ dependencies = [
|
||||
"syn 2.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"indexmap 2.9.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1502,6 +1578,17 @@ dependencies = [
|
||||
"match_token",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.3.1"
|
||||
@@ -1513,6 +1600,17 @@ dependencies = [
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
@@ -1520,7 +1618,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http 1.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1531,8 +1629,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -1542,10 +1640,20 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humanlayer-wui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
"regex",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -1554,10 +1662,37 @@ dependencies = [
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-store",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.6.0"
|
||||
@@ -1567,8 +1702,8 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
@@ -1577,6 +1712,19 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper 0.14.32",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.14"
|
||||
@@ -1588,9 +1736,9 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.6.0",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
@@ -2147,6 +2295,23 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -2513,6 +2678,50 @@ dependencies = [
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -3042,6 +3251,17 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.0"
|
||||
@@ -3082,6 +3302,46 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 0.1.2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.20"
|
||||
@@ -3092,10 +3352,10 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -3104,7 +3364,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
@@ -3158,6 +3418,15 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
@@ -3179,6 +3448,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
@@ -3212,6 +3490,29 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.24.0"
|
||||
@@ -3407,12 +3708,44 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_child"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"sigchld",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "sigchld"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"os_pipe",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.5"
|
||||
@@ -3474,7 +3807,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
@@ -3589,6 +3922,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
@@ -3609,6 +3948,27 @@ dependencies = [
|
||||
"syn 2.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
@@ -3629,7 +3989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
@@ -3686,14 +4046,14 @@ checksum = "2f7a0f4019c80391d143ee26cd7cd1ed271ac241d3087d333f99f3269ba90812"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
"embed_plist",
|
||||
"getrandom 0.2.16",
|
||||
"glob",
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"http 1.3.1",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -3706,7 +4066,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"reqwest 0.12.20",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -3736,7 +4096,7 @@ checksum = "12f025c389d3adb83114bec704da973142e82fc6ec799c7c750c5e21cefaec83"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"glob",
|
||||
"heck 0.5.0",
|
||||
"json-patch",
|
||||
@@ -3886,6 +4246,43 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b9ffadec5c3523f11e8273465cacb3d86ea7652a28e6e2a2e9b5c182f791d25"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
"open",
|
||||
"os_pipe",
|
||||
"regex",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shared_child",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-store"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5916c609664a56c82aeaefffca9851fd072d4d41f73d63f22ee3ee451508194f"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.7.0"
|
||||
@@ -3895,7 +4292,7 @@ dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.3.1",
|
||||
"jni",
|
||||
"objc2 0.6.1",
|
||||
"objc2-ui-kit",
|
||||
@@ -3915,7 +4312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe52ed0ef40fd7ad51a620ecb3018e32eba3040bb95025216a962a37f6f050c5"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.3.1",
|
||||
"jni",
|
||||
"log",
|
||||
"objc2 0.6.1",
|
||||
@@ -3948,7 +4345,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
"html5ever",
|
||||
"http",
|
||||
"http 1.3.1",
|
||||
"infer",
|
||||
"json-patch",
|
||||
"kuchikiki",
|
||||
@@ -4131,11 +4528,35 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.15"
|
||||
@@ -4221,7 +4642,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -4236,8 +4657,8 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
@@ -4321,7 +4742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"libappindicator",
|
||||
"muda",
|
||||
"objc2 0.6.1",
|
||||
@@ -4485,6 +4906,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
@@ -4964,6 +5391,15 @@ dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -5269,6 +5705,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.55.0"
|
||||
@@ -5328,7 +5774,7 @@ dependencies = [
|
||||
"gdkx11",
|
||||
"gtk",
|
||||
"html5ever",
|
||||
"http",
|
||||
"http 1.3.1",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"kuchikiki",
|
||||
|
||||
@@ -23,7 +23,14 @@ tauri-plugin-opener = "2"
|
||||
tauri-plugin-notification = "2.3.0"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-clipboard-manager = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-store = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
dirs = "5.0"
|
||||
regex = "1.10"
|
||||
reqwest = { version = "0.11", features = ["blocking"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
]
|
||||
},
|
||||
"clipboard-manager:default",
|
||||
"clipboard-manager:allow-write-text"
|
||||
"clipboard-manager:allow-write-text",
|
||||
"shell:allow-spawn",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-kill",
|
||||
"store:default"
|
||||
]
|
||||
}
|
||||
|
||||
488
humanlayer-wui/src-tauri/src/daemon.rs
Normal file
488
humanlayer-wui/src-tauri/src/daemon.rs
Normal file
@@ -0,0 +1,488 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DaemonInfo {
|
||||
pub port: u16,
|
||||
pub pid: u32,
|
||||
pub database_path: String,
|
||||
pub socket_path: String,
|
||||
pub branch_id: String,
|
||||
pub is_running: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonManager {
|
||||
process: Arc<Mutex<Option<Child>>>,
|
||||
info: Arc<Mutex<Option<DaemonInfo>>>,
|
||||
}
|
||||
|
||||
impl DaemonManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
process: Arc::new(Mutex::new(None)),
|
||||
info: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_daemon(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
is_dev: bool,
|
||||
branch_override: Option<String>,
|
||||
) -> Result<DaemonInfo, String> {
|
||||
// Check if already running
|
||||
{
|
||||
let process = self.process.lock().unwrap();
|
||||
if process.is_some() {
|
||||
if let Some(info) = self.info.lock().unwrap().as_ref() {
|
||||
return Ok(info.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if daemon is already running on a specific port
|
||||
if let Ok(port_str) = env::var("HUMANLAYER_DAEMON_HTTP_PORT") {
|
||||
if let Ok(port) = port_str.parse::<u16>() {
|
||||
// Try to connect to existing daemon
|
||||
if check_daemon_health(port).await.is_ok() {
|
||||
let info = DaemonInfo {
|
||||
port,
|
||||
pid: 0, // Unknown PID for external daemon
|
||||
database_path: "external".to_string(),
|
||||
socket_path: "external".to_string(),
|
||||
branch_id: "external".to_string(),
|
||||
is_running: true,
|
||||
};
|
||||
*self.info.lock().unwrap() = Some(info.clone());
|
||||
return Ok(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine branch identifier
|
||||
let branch_id = if is_dev {
|
||||
branch_override
|
||||
.or_else(get_git_branch)
|
||||
.unwrap_or_else(|| "dev".to_string())
|
||||
} else {
|
||||
"production".to_string()
|
||||
};
|
||||
|
||||
// Extract ticket ID if present (e.g., "eng-1234" from "eng-1234-some-feature")
|
||||
let branch_id = extract_ticket_id(&branch_id).unwrap_or(branch_id);
|
||||
|
||||
// Set up paths
|
||||
let home_dir = dirs::home_dir().ok_or("Failed to get home directory")?;
|
||||
let humanlayer_dir = home_dir.join(".humanlayer");
|
||||
fs::create_dir_all(&humanlayer_dir)
|
||||
.map_err(|e| format!("Failed to create .humanlayer directory: {e}"))?;
|
||||
|
||||
// Database path
|
||||
let database_path = if is_dev {
|
||||
// Copy dev database if it doesn't exist
|
||||
let dev_db = humanlayer_dir.join(format!("daemon-{branch_id}.db"));
|
||||
if !dev_db.exists() {
|
||||
let source_db = humanlayer_dir.join("daemon-dev.db");
|
||||
if source_db.exists() {
|
||||
fs::copy(&source_db, &dev_db)
|
||||
.map_err(|e| format!("Failed to copy dev database: {e}"))?;
|
||||
}
|
||||
}
|
||||
dev_db
|
||||
} else {
|
||||
humanlayer_dir.join("daemon.db")
|
||||
};
|
||||
|
||||
// Socket path
|
||||
let socket_path = if is_dev {
|
||||
humanlayer_dir.join(format!("daemon-{branch_id}.sock"))
|
||||
} else {
|
||||
humanlayer_dir.join("daemon.sock")
|
||||
};
|
||||
|
||||
// Get daemon binary path (macOS only)
|
||||
let daemon_path = get_daemon_path(app_handle, is_dev)?;
|
||||
|
||||
// Build environment with port 0 for dynamic allocation
|
||||
let mut env_vars = env::vars().collect::<Vec<_>>();
|
||||
env_vars.push((
|
||||
"HUMANLAYER_DATABASE_PATH".to_string(),
|
||||
database_path.to_str().unwrap().to_string(),
|
||||
));
|
||||
env_vars.push((
|
||||
"HUMANLAYER_DAEMON_SOCKET".to_string(),
|
||||
socket_path.to_str().unwrap().to_string(),
|
||||
));
|
||||
env_vars.push(("HUMANLAYER_DAEMON_HTTP_PORT".to_string(), "0".to_string()));
|
||||
env_vars.push((
|
||||
"HUMANLAYER_DAEMON_HTTP_HOST".to_string(),
|
||||
"localhost".to_string(),
|
||||
));
|
||||
|
||||
if is_dev {
|
||||
env_vars.push((
|
||||
"HUMANLAYER_DAEMON_VERSION_OVERRIDE".to_string(),
|
||||
branch_id.clone(),
|
||||
));
|
||||
// Enable debug logging for daemon in dev mode
|
||||
env_vars.push((
|
||||
"HUMANLAYER_DEBUG".to_string(),
|
||||
"true".to_string(),
|
||||
));
|
||||
env_vars.push((
|
||||
"GIN_MODE".to_string(),
|
||||
"debug".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Start daemon with stdout capture and stderr logging
|
||||
let mut cmd = Command::new(&daemon_path);
|
||||
|
||||
// In dev mode, capture stderr to see what's happening
|
||||
if is_dev {
|
||||
cmd.envs(env_vars)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
} else {
|
||||
cmd.envs(env_vars)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit());
|
||||
}
|
||||
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start daemon: {e}"))?;
|
||||
|
||||
// Get the PID before we do anything else
|
||||
let pid = child.id();
|
||||
tracing::info!("Daemon spawned with PID: {}", pid);
|
||||
|
||||
// If in dev mode, spawn a task to read stderr
|
||||
if is_dev {
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
let branch_id_clone = branch_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stderr);
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
// Parse slog format to extract level
|
||||
let level = extract_log_level(&line);
|
||||
let cleaned_line = remove_timestamp(&line);
|
||||
|
||||
match level {
|
||||
LogLevel::Error => tracing::error!("[Daemon] {}: {}", branch_id_clone, cleaned_line),
|
||||
LogLevel::Warn => tracing::warn!("[Daemon] {}: {}", branch_id_clone, cleaned_line),
|
||||
LogLevel::Info => tracing::info!("[Daemon] {}: {}", branch_id_clone, cleaned_line),
|
||||
LogLevel::Debug => tracing::debug!("[Daemon] {}: {}", branch_id_clone, cleaned_line),
|
||||
LogLevel::Trace => tracing::trace!("[Daemon] {}: {}", branch_id_clone, cleaned_line),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error reading daemon stderr: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Parse stdout to get the actual port
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or("Failed to capture daemon stdout")?;
|
||||
|
||||
let mut reader = BufReader::new(stdout);
|
||||
let mut actual_port = None;
|
||||
let mut first_line = String::new();
|
||||
|
||||
// Read the first line synchronously to get the port
|
||||
reader
|
||||
.read_line(&mut first_line)
|
||||
.map_err(|e| format!("Failed to read daemon stdout: {e}"))?;
|
||||
|
||||
if first_line.starts_with("HTTP_PORT=") {
|
||||
actual_port = first_line.trim().replace("HTTP_PORT=", "").parse::<u16>().ok();
|
||||
}
|
||||
|
||||
let port = actual_port.ok_or("Daemon failed to report port")?;
|
||||
tracing::info!("Got port {} from daemon stdout", port);
|
||||
|
||||
// Now spawn a task to keep reading stdout to prevent SIGPIPE
|
||||
let branch_id_for_stdout = branch_id.clone();
|
||||
tokio::spawn(async move {
|
||||
// Continue reading the rest of stdout
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
tracing::trace!("[Daemon stdout] {}: {}", branch_id_for_stdout, line);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!("Daemon stdout closed for {}: {}", branch_id_for_stdout, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::debug!("Stdout reader task finished for {}", branch_id_for_stdout);
|
||||
});
|
||||
|
||||
// Check if process is still alive after reading port
|
||||
match child.try_wait() {
|
||||
Ok(None) => tracing::info!("Daemon process still running after port read"),
|
||||
Ok(Some(status)) => {
|
||||
return Err(format!("Daemon process exited immediately after starting! Status: {status:?}"));
|
||||
}
|
||||
Err(e) => tracing::error!("Error checking daemon status: {}", e),
|
||||
}
|
||||
|
||||
let daemon_info = DaemonInfo {
|
||||
port,
|
||||
pid,
|
||||
database_path: database_path.to_str().unwrap().to_string(),
|
||||
socket_path: socket_path.to_str().unwrap().to_string(),
|
||||
branch_id: branch_id.clone(),
|
||||
is_running: true,
|
||||
};
|
||||
|
||||
// Store the process and info before awaiting
|
||||
{
|
||||
let mut process = self.process.lock().unwrap();
|
||||
*process = Some(child);
|
||||
}
|
||||
*self.info.lock().unwrap() = Some(daemon_info.clone());
|
||||
|
||||
// Wait for daemon to be ready
|
||||
tracing::info!("Waiting for daemon to be ready on port {}", port);
|
||||
wait_for_daemon(port).await?;
|
||||
tracing::info!("Daemon is ready and responding to health checks");
|
||||
|
||||
// Spawn a task to monitor the daemon process
|
||||
let process_arc = self.process.clone();
|
||||
let branch_id_clone = branch_id.clone();
|
||||
let port_for_monitor = port;
|
||||
tokio::spawn(async move {
|
||||
let mut last_check = std::time::Instant::now();
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
|
||||
let mut process_guard = process_arc.lock().unwrap();
|
||||
if let Some(child) = process_guard.as_mut() {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
// Process has exited
|
||||
tracing::error!(
|
||||
"Daemon process exited unexpectedly! Branch: {}, Port: {}, Exit status: {:?}, Time since last check: {:?}",
|
||||
branch_id_clone,
|
||||
port_for_monitor,
|
||||
status,
|
||||
last_check.elapsed()
|
||||
);
|
||||
// Clear the process
|
||||
*process_guard = None;
|
||||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
// Still running
|
||||
last_check = std::time::Instant::now();
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error checking daemon process status: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No process to monitor
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(daemon_info)
|
||||
}
|
||||
|
||||
pub fn stop_daemon(&self) -> Result<(), String> {
|
||||
let mut process = self.process.lock().unwrap();
|
||||
|
||||
if let Some(mut child) = process.take() {
|
||||
child
|
||||
.kill()
|
||||
.map_err(|e| format!("Failed to stop daemon: {e}"))?;
|
||||
|
||||
// Wait for process to exit
|
||||
let _ = child.wait();
|
||||
|
||||
// Update store to mark daemon as not running
|
||||
if let Some(info) = self.info.lock().unwrap().as_mut() {
|
||||
info.is_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_info(&self) -> Option<DaemonInfo> {
|
||||
self.info.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
let mut process = self.process.lock().unwrap();
|
||||
if let Some(child) = process.as_mut() {
|
||||
// Check if process is still alive
|
||||
match child.try_wait() {
|
||||
Ok(None) => true, // Still running
|
||||
_ => false, // Exited or error
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_daemon_path(app_handle: &AppHandle, is_dev: bool) -> Result<PathBuf, String> {
|
||||
if is_dev {
|
||||
// In dev mode, look for hld-dev in the project
|
||||
let current =
|
||||
env::current_dir().map_err(|e| format!("Failed to get current directory: {e}"))?;
|
||||
|
||||
// Handle both running from src-tauri and from humanlayer-wui
|
||||
let dev_path = if current.ends_with("src-tauri") {
|
||||
current
|
||||
.parent() // humanlayer-wui
|
||||
.and_then(|p| p.parent()) // humanlayer root
|
||||
.ok_or("Failed to get parent directory")?
|
||||
.join("hld")
|
||||
.join("hld-dev")
|
||||
} else {
|
||||
current
|
||||
.parent() // Go up from humanlayer-wui to humanlayer
|
||||
.ok_or("Failed to get parent directory")?
|
||||
.join("hld")
|
||||
.join("hld-dev")
|
||||
};
|
||||
|
||||
if dev_path.exists() {
|
||||
Ok(dev_path)
|
||||
} else {
|
||||
Err(format!(
|
||||
"Development daemon not found at {dev_path:?}. Run 'make daemon-dev-build' first."
|
||||
))
|
||||
}
|
||||
} else {
|
||||
// In production, use bundled binary
|
||||
let resource_dir = app_handle
|
||||
.path()
|
||||
.resource_dir()
|
||||
.map_err(|e| format!("Failed to get resource directory: {e}"))?;
|
||||
|
||||
Ok(resource_dir.join("bin").join("hld"))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_git_branch() -> Option<String> {
|
||||
Command::new("git")
|
||||
.args(["branch", "--show-current"])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| {
|
||||
if output.status.success() {
|
||||
String::from_utf8(output.stdout)
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_ticket_id(branch: &str) -> Option<String> {
|
||||
// Extract patterns like "eng-1234" from branch names
|
||||
let re = regex::Regex::new(r"(eng-\d+)").ok()?;
|
||||
re.captures(branch)
|
||||
.and_then(|cap| cap.get(1))
|
||||
.map(|m| m.as_str().to_string())
|
||||
}
|
||||
|
||||
async fn check_daemon_health(port: u16) -> Result<(), String> {
|
||||
let client = reqwest::Client::new();
|
||||
match client
|
||||
.get(format!("http://localhost:{port}/api/v1/health"))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) if response.status().is_success() => Ok(()),
|
||||
Ok(_) => Err("Daemon health check failed".to_string()),
|
||||
Err(e) => Err(format!("Failed to connect to daemon: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_daemon(port: u16) -> Result<(), String> {
|
||||
let start = std::time::Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(10);
|
||||
|
||||
while start.elapsed() < timeout {
|
||||
if check_daemon_health(port).await.is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
Err("Daemon failed to start within 10 seconds".to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
fn extract_log_level(line: &str) -> LogLevel {
|
||||
// slog format includes level like: "2024-01-30T10:15:30Z INFO message"
|
||||
// or sometimes: "INFO[0001] message" for other loggers
|
||||
|
||||
if line.contains(" ERROR ") || line.contains("ERROR[") {
|
||||
LogLevel::Error
|
||||
} else if line.contains(" WARN ") || line.contains("WARN[") {
|
||||
LogLevel::Warn
|
||||
} else if line.contains(" INFO ") || line.contains("INFO[") {
|
||||
LogLevel::Info
|
||||
} else if line.contains(" DEBUG ") || line.contains("DEBUG[") {
|
||||
LogLevel::Debug
|
||||
} else if line.contains(" TRACE ") || line.contains("TRACE[") {
|
||||
LogLevel::Trace
|
||||
} else {
|
||||
// Default to info for unparseable lines
|
||||
LogLevel::Info
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_timestamp(line: &str) -> &str {
|
||||
// Remove slog timestamp to avoid double timestamps
|
||||
// Format: "2024-01-30T10:15:30Z LEVEL message"
|
||||
if let Some(idx) = line.find(" INFO ") {
|
||||
&line[idx + 6..]
|
||||
} else if let Some(idx) = line.find(" ERROR ") {
|
||||
&line[idx + 7..]
|
||||
} else if let Some(idx) = line.find(" WARN ") {
|
||||
&line[idx + 6..]
|
||||
} else if let Some(idx) = line.find(" DEBUG ") {
|
||||
&line[idx + 7..]
|
||||
} else if let Some(idx) = line.find(" TRACE ") {
|
||||
&line[idx + 7..]
|
||||
} else {
|
||||
// Return original if no timestamp found
|
||||
line
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,221 @@
|
||||
mod daemon;
|
||||
|
||||
use daemon::{DaemonInfo, DaemonManager};
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{Manager, State};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
// Helper to get store path based on dev mode and branch
|
||||
fn get_store_path(is_dev: bool, branch_id: Option<&str>) -> PathBuf {
|
||||
let home = dirs::home_dir().expect("Failed to get home directory");
|
||||
let humanlayer_dir = home.join(".humanlayer");
|
||||
|
||||
if is_dev {
|
||||
// Use branch-specific store in dev mode
|
||||
if let Some(branch) = branch_id {
|
||||
humanlayer_dir.join(format!("codelayer-{branch}.json"))
|
||||
} else {
|
||||
humanlayer_dir.join("codelayer-dev.json")
|
||||
}
|
||||
} else {
|
||||
humanlayer_dir.join("codelayer.json")
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn start_daemon(
|
||||
app_handle: tauri::AppHandle,
|
||||
daemon_manager: State<'_, DaemonManager>,
|
||||
is_dev: bool,
|
||||
branch_override: Option<String>,
|
||||
) -> Result<DaemonInfo, String> {
|
||||
let info = daemon_manager
|
||||
.start_daemon(&app_handle, is_dev, branch_override)
|
||||
.await?;
|
||||
|
||||
// Save to store using branch-specific path in dev mode
|
||||
let store_path = get_store_path(is_dev, Some(&info.branch_id));
|
||||
let store = app_handle
|
||||
.store(&store_path)
|
||||
.map_err(|e| format!("Failed to access store: {e}"))?;
|
||||
|
||||
store.set("current_daemon", serde_json::to_value(&info).unwrap());
|
||||
store
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save store: {e}"))?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn stop_daemon(
|
||||
app_handle: tauri::AppHandle,
|
||||
daemon_manager: State<'_, DaemonManager>,
|
||||
) -> Result<(), String> {
|
||||
// Get daemon info before stopping to know which store to update
|
||||
if let Some(info) = daemon_manager.get_info() {
|
||||
let is_dev = info.branch_id != "production";
|
||||
let store_path = get_store_path(is_dev, Some(&info.branch_id));
|
||||
|
||||
// Stop the daemon
|
||||
daemon_manager.stop_daemon()?;
|
||||
|
||||
// Update store to mark as not running
|
||||
let store = app_handle
|
||||
.store(&store_path)
|
||||
.map_err(|e| format!("Failed to access store: {e}"))?;
|
||||
|
||||
// Get the stored daemon info and update is_running
|
||||
if let Some(mut stored_info) = store
|
||||
.get("current_daemon")
|
||||
.and_then(|v| serde_json::from_value::<DaemonInfo>(v).ok())
|
||||
{
|
||||
stored_info.is_running = false;
|
||||
store.set(
|
||||
"current_daemon",
|
||||
serde_json::to_value(&stored_info).unwrap(),
|
||||
);
|
||||
store
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save store: {e}"))?;
|
||||
}
|
||||
} else {
|
||||
daemon_manager.stop_daemon()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_daemon_info(
|
||||
app_handle: tauri::AppHandle,
|
||||
daemon_manager: State<'_, DaemonManager>,
|
||||
is_dev: bool,
|
||||
) -> Result<Option<DaemonInfo>, String> {
|
||||
// First check if daemon manager has info
|
||||
if let Some(info) = daemon_manager.get_info() {
|
||||
return Ok(Some(info));
|
||||
}
|
||||
|
||||
// Otherwise check store for last known daemon
|
||||
let store_path = get_store_path(is_dev, None);
|
||||
let store = app_handle
|
||||
.store(&store_path)
|
||||
.map_err(|e| format!("Failed to access store: {e}"))?;
|
||||
|
||||
let stored_info = store
|
||||
.get("current_daemon")
|
||||
.and_then(|v| serde_json::from_value::<DaemonInfo>(v).ok());
|
||||
|
||||
Ok(stored_info)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn is_daemon_running(daemon_manager: State<'_, DaemonManager>) -> Result<bool, String> {
|
||||
Ok(daemon_manager.is_running())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Initialize tracing
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.setup(|app| {
|
||||
let daemon_manager = DaemonManager::new();
|
||||
app.manage(daemon_manager.clone());
|
||||
|
||||
// Check if auto-launch is disabled
|
||||
let should_autolaunch = env::var("HUMANLAYER_WUI_AUTOLAUNCH_DAEMON")
|
||||
.map(|v| v.to_lowercase() != "false")
|
||||
.unwrap_or(true);
|
||||
|
||||
if should_autolaunch {
|
||||
// Start daemon automatically
|
||||
let app_handle_clone = app.app_handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let is_dev = cfg!(debug_assertions);
|
||||
|
||||
// Small delay to ensure UI is ready
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Try to start daemon, but don't show errors in UI
|
||||
match daemon_manager
|
||||
.start_daemon(&app_handle_clone, is_dev, None)
|
||||
.await
|
||||
{
|
||||
Ok(info) => {
|
||||
tracing::info!("Daemon started automatically on port {}", info.port);
|
||||
}
|
||||
Err(e) => {
|
||||
// Log error but don't interrupt user experience
|
||||
tracing::error!("Failed to auto-start daemon: {}", e);
|
||||
|
||||
// Common edge cases that shouldn't interrupt the user
|
||||
if e.contains("port") || e.contains("already in use") {
|
||||
tracing::info!("Port conflict detected, user can connect manually");
|
||||
} else if e.contains("binary not found")
|
||||
|| e.contains("daemon not found")
|
||||
{
|
||||
tracing::warn!(
|
||||
"Daemon binary missing, this is expected in some dev scenarios"
|
||||
);
|
||||
} else if e.contains("permission") {
|
||||
tracing::error!(
|
||||
"Permission issue starting daemon, user intervention required"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tracing::info!("Daemon auto-launch disabled by environment variable");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
||||
let daemon_manager = window.state::<DaemonManager>();
|
||||
|
||||
// Always stop daemon when window closes
|
||||
if let Some(info) = daemon_manager.get_info() {
|
||||
let is_dev = info.branch_id != "production";
|
||||
let store_path = get_store_path(is_dev, Some(&info.branch_id));
|
||||
|
||||
if let Ok(store) = window.app_handle().store(&store_path) {
|
||||
// Update store to mark daemon as not running
|
||||
if let Some(mut stored_info) = store
|
||||
.get("current_daemon")
|
||||
.and_then(|v| serde_json::from_value::<DaemonInfo>(v).ok())
|
||||
{
|
||||
stored_info.is_running = false;
|
||||
store.set(
|
||||
"current_daemon",
|
||||
serde_json::to_value(&stored_info).unwrap(),
|
||||
);
|
||||
let _ = store.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the daemon
|
||||
let _ = daemon_manager.stop_daemon();
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
start_daemon,
|
||||
stop_daemon,
|
||||
get_daemon_info,
|
||||
is_daemon_running,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
],
|
||||
"resources": ["bin/hld", "bin/humanlayer"]
|
||||
},
|
||||
"plugins": {
|
||||
"fs": {
|
||||
|
||||
223
humanlayer-wui/src/components/DebugPanel.tsx
Normal file
223
humanlayer-wui/src/components/DebugPanel.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { daemonService, type DaemonInfo } from '@/services/daemon-service'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Loader2, RefreshCw, Link, Server } from 'lucide-react'
|
||||
import { useDaemonConnection } from '@/hooks/useDaemonConnection'
|
||||
|
||||
interface DebugPanelProps {
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function DebugPanel({ open, onOpenChange }: DebugPanelProps) {
|
||||
const { connected, reconnect } = useDaemonConnection()
|
||||
const [isRestarting, setIsRestarting] = useState(false)
|
||||
const [restartError, setRestartError] = useState<string | null>(null)
|
||||
const [customUrl, setCustomUrl] = useState('')
|
||||
const [connectError, setConnectError] = useState<string | null>(null)
|
||||
const [daemonType, setDaemonType] = useState<'managed' | 'external'>('managed')
|
||||
const [externalDaemonUrl, setExternalDaemonUrl] = useState<string | null>(null)
|
||||
const [daemonInfo, setDaemonInfo] = useState<DaemonInfo | null>(null)
|
||||
|
||||
// Only show in dev mode
|
||||
if (!import.meta.env.DEV) {
|
||||
return null
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadDaemonInfo()
|
||||
}, [connected])
|
||||
|
||||
async function loadDaemonInfo() {
|
||||
try {
|
||||
const info = await daemonService.getDaemonInfo()
|
||||
setDaemonInfo(info)
|
||||
const type = daemonService.getDaemonType()
|
||||
setDaemonType(type)
|
||||
if (type === 'external') {
|
||||
setExternalDaemonUrl((window as any).__HUMANLAYER_DAEMON_URL || null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load daemon info:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRestartDaemon() {
|
||||
setIsRestarting(true)
|
||||
setRestartError(null)
|
||||
|
||||
try {
|
||||
// Stop current daemon
|
||||
await daemonService.stopDaemon()
|
||||
|
||||
// Wait a moment
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Start new daemon (with rebuild in dev)
|
||||
await daemonService.startDaemon(true)
|
||||
|
||||
// Wait for it to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
// Reconnect
|
||||
await reconnect()
|
||||
await loadDaemonInfo()
|
||||
|
||||
setIsRestarting(false)
|
||||
} catch (error: any) {
|
||||
setRestartError(error.message || 'Failed to restart daemon')
|
||||
setIsRestarting(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConnectToCustom() {
|
||||
setConnectError(null)
|
||||
|
||||
try {
|
||||
await daemonService.connectToExisting(customUrl)
|
||||
await reconnect()
|
||||
await loadDaemonInfo()
|
||||
setCustomUrl('')
|
||||
} catch (error: any) {
|
||||
setConnectError(error.message || 'Failed to connect')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSwitchToManaged() {
|
||||
try {
|
||||
await daemonService.switchToManagedDaemon()
|
||||
await reconnect()
|
||||
await loadDaemonInfo()
|
||||
} catch (error: any) {
|
||||
setConnectError(error.message || 'Failed to switch to managed daemon')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Debug Panel</DialogTitle>
|
||||
<DialogDescription>Advanced daemon management for developers</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Daemon Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Connection</span>
|
||||
<span
|
||||
className={`text-sm font-medium ${connected ? 'text-[var(--terminal-success)]' : 'text-[var(--terminal-error)]'}`}
|
||||
>
|
||||
{connected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Daemon Type</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-sm font-medium capitalize">{daemonType}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{daemonType === 'external' && externalDaemonUrl && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">External URL</span>
|
||||
<span className="text-sm font-mono">{externalDaemonUrl}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{daemonInfo && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Port</span>
|
||||
<span className="text-sm font-mono">{daemonInfo.port}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Branch</span>
|
||||
<span className="text-sm font-mono">{daemonInfo.branch_id}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{daemonType === 'managed' ? (
|
||||
<Button
|
||||
onClick={handleRestartDaemon}
|
||||
disabled={isRestarting}
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
>
|
||||
{isRestarting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Restarting Daemon...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Restart Daemon
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleSwitchToManaged} className="w-full" variant="outline">
|
||||
Switch to Managed Daemon
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{restartError && <p className="text-sm text-destructive">{restartError}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Connect to Existing Daemon</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Connect to a daemon running on a custom URL
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="url" className="text-sm">
|
||||
Daemon URL
|
||||
</Label>
|
||||
<Input
|
||||
id="url"
|
||||
type="text"
|
||||
placeholder="http://127.0.0.1:7777"
|
||||
value={customUrl}
|
||||
onChange={e => setCustomUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleConnectToCustom}
|
||||
disabled={!customUrl}
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
>
|
||||
<Link className="mr-2 h-4 w-4" />
|
||||
Connect
|
||||
</Button>
|
||||
|
||||
{connectError && <p className="text-sm text-destructive">{connectError}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -15,14 +15,16 @@ import { Toaster } from 'sonner'
|
||||
import { notificationService } from '@/services/NotificationService'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import { MessageCircle, Bug } from 'lucide-react'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { DebugPanel } from '@/components/DebugPanel'
|
||||
import '@/App.css'
|
||||
|
||||
export function Layout() {
|
||||
const [approvals, setApprovals] = useState<any[]>([])
|
||||
const [activeSessionId] = useState<string | null>(null)
|
||||
const { setTheme } = useTheme()
|
||||
const [isDebugPanelOpen, setIsDebugPanelOpen] = useState(false)
|
||||
|
||||
// Use the daemon connection hook for all connection management
|
||||
const { connected, connecting, version, connect } = useDaemonConnection()
|
||||
@@ -240,6 +242,21 @@ export function Layout() {
|
||||
<p>Submit feedback (⌘⇧F)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{import.meta.env.DEV && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => setIsDebugPanelOpen(true)}
|
||||
className="px-1.5 py-0.5 text-xs font-mono border border-border bg-background text-foreground hover:bg-accent/10 transition-colors"
|
||||
>
|
||||
<Bug className="w-3 h-3" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Debug Panel (Dev Only)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ThemeSelector />
|
||||
<div className="flex items-center gap-2 font-mono text-xs">
|
||||
<span className="uppercase tracking-wider">
|
||||
@@ -264,6 +281,9 @@ export function Layout() {
|
||||
|
||||
{/* Notifications */}
|
||||
<Toaster position="bottom-right" richColors />
|
||||
|
||||
{/* Debug Panel */}
|
||||
<DebugPanel open={isDebugPanelOpen} onOpenChange={setIsDebugPanelOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { daemonClient } from '@/lib/daemon'
|
||||
import { formatError } from '@/utils/errors'
|
||||
import { daemonService } from '@/services/daemon-service'
|
||||
|
||||
interface UseDaemonConnectionReturn {
|
||||
connected: boolean
|
||||
@@ -8,6 +9,7 @@ interface UseDaemonConnectionReturn {
|
||||
error: string | null
|
||||
version: string | null
|
||||
connect: () => Promise<void>
|
||||
reconnect: () => Promise<void>
|
||||
checkHealth: () => Promise<void>
|
||||
}
|
||||
|
||||
@@ -16,6 +18,7 @@ export function useDaemonConnection(): UseDaemonConnectionReturn {
|
||||
const [connecting, setConnecting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [version, setVersion] = useState<string | null>(null)
|
||||
const retryCount = useRef(0)
|
||||
|
||||
const checkHealth = useCallback(async () => {
|
||||
try {
|
||||
@@ -31,11 +34,47 @@ export function useDaemonConnection(): UseDaemonConnectionReturn {
|
||||
}, [])
|
||||
|
||||
const connect = useCallback(async () => {
|
||||
try {
|
||||
if (connecting) return
|
||||
|
||||
setConnecting(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await daemonClient.connect()
|
||||
const health = await daemonClient.health()
|
||||
|
||||
setConnected(true)
|
||||
setVersion(health.version)
|
||||
retryCount.current = 0
|
||||
} catch (err: any) {
|
||||
setConnected(false)
|
||||
|
||||
// Check if this is first failure and we have a managed daemon
|
||||
if (retryCount.current === 0) {
|
||||
const isManaged = await daemonService.isDaemonRunning()
|
||||
if (!isManaged) {
|
||||
// Let DaemonManager handle it
|
||||
setError(formatError(err))
|
||||
} else {
|
||||
// Managed daemon might be starting, retry
|
||||
retryCount.current++
|
||||
setTimeout(() => connect(), 2000)
|
||||
}
|
||||
} else {
|
||||
setError(formatError(err))
|
||||
}
|
||||
} finally {
|
||||
setConnecting(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const reconnect = useCallback(async () => {
|
||||
try {
|
||||
setConnecting(true)
|
||||
setError(null)
|
||||
setConnected(false)
|
||||
|
||||
await daemonClient.reconnect()
|
||||
await checkHealth()
|
||||
} catch (err) {
|
||||
setError(formatError(err))
|
||||
@@ -64,6 +103,7 @@ export function useDaemonConnection(): UseDaemonConnectionReturn {
|
||||
error,
|
||||
version,
|
||||
connect,
|
||||
reconnect,
|
||||
checkHealth,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ export class HTTPDaemonClient implements IDaemonClient {
|
||||
}
|
||||
|
||||
private async doConnect(): Promise<void> {
|
||||
const baseUrl = getDaemonUrl()
|
||||
// getDaemonUrl now checks for managed daemon port dynamically
|
||||
const baseUrl = await getDaemonUrl()
|
||||
|
||||
this.client = new HLDClient({
|
||||
baseUrl: `${baseUrl}/api/v1`,
|
||||
@@ -82,6 +83,16 @@ export class HTTPDaemonClient implements IDaemonClient {
|
||||
}
|
||||
}
|
||||
|
||||
async reconnect(): Promise<void> {
|
||||
// Disconnect first if connected
|
||||
if (this.connected || this.connectionPromise) {
|
||||
await this.disconnect()
|
||||
}
|
||||
|
||||
// Now connect to the potentially new URL
|
||||
return this.connect()
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
// Unsubscribe all event streams
|
||||
for (const unsubscribe of this.subscriptions.values()) {
|
||||
@@ -91,6 +102,8 @@ export class HTTPDaemonClient implements IDaemonClient {
|
||||
|
||||
this.connected = false
|
||||
this.client = undefined
|
||||
this.connectionPromise = undefined
|
||||
this.retryCount = 0
|
||||
}
|
||||
|
||||
async health(): Promise<HealthCheckResponse> {
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
// Get daemon URL from environment or default
|
||||
export function getDaemonUrl(): string {
|
||||
// Check for explicit URL first
|
||||
import { daemonService } from '@/services/daemon-service'
|
||||
|
||||
// Get daemon URL from environment or managed daemon
|
||||
export async function getDaemonUrl(): Promise<string> {
|
||||
// Check for custom URL from debug panel first
|
||||
if ((window as any).__HUMANLAYER_DAEMON_URL) {
|
||||
return (window as any).__HUMANLAYER_DAEMON_URL
|
||||
}
|
||||
|
||||
// Check for explicit URL from environment
|
||||
if (import.meta.env.VITE_HUMANLAYER_DAEMON_URL) {
|
||||
return import.meta.env.VITE_HUMANLAYER_DAEMON_URL
|
||||
}
|
||||
|
||||
// Check if we have a managed daemon
|
||||
try {
|
||||
const daemonInfo = await daemonService.getDaemonInfo()
|
||||
if (daemonInfo && daemonInfo.port) {
|
||||
return `http://localhost:${daemonInfo.port}`
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to get managed daemon info:', error)
|
||||
}
|
||||
|
||||
// Check for port override
|
||||
const port = import.meta.env.VITE_HUMANLAYER_DAEMON_HTTP_PORT || '7777'
|
||||
const host = import.meta.env.VITE_HUMANLAYER_DAEMON_HTTP_HOST || 'localhost'
|
||||
@@ -15,7 +32,7 @@ export function getDaemonUrl(): string {
|
||||
// Headers to include with all requests
|
||||
export function getDefaultHeaders(): Record<string, string> {
|
||||
return {
|
||||
'X-Client': 'wui',
|
||||
'X-Client': 'codelayer',
|
||||
'X-Client-Version': import.meta.env.VITE_APP_VERSION || 'unknown',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface SubscriptionHandle {
|
||||
// Client interface using legacy types for backward compatibility
|
||||
export interface DaemonClient {
|
||||
connect(): Promise<void>
|
||||
reconnect(): Promise<void>
|
||||
disconnect(): Promise<void>
|
||||
health(): Promise<HealthCheckResponse>
|
||||
|
||||
|
||||
104
humanlayer-wui/src/services/daemon-service.ts
Normal file
104
humanlayer-wui/src/services/daemon-service.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export interface DaemonInfo {
|
||||
port: number
|
||||
pid: number
|
||||
database_path: string
|
||||
socket_path: string
|
||||
branch_id: string
|
||||
}
|
||||
|
||||
export interface DaemonConfig {
|
||||
branch_id: string
|
||||
port: number
|
||||
database_path: string
|
||||
socket_path: string
|
||||
last_used: string
|
||||
}
|
||||
|
||||
class DaemonService {
|
||||
private static instance: DaemonService
|
||||
|
||||
static getInstance(): DaemonService {
|
||||
if (!DaemonService.instance) {
|
||||
DaemonService.instance = new DaemonService()
|
||||
}
|
||||
return DaemonService.instance
|
||||
}
|
||||
|
||||
async startDaemon(isDev: boolean = false, branchOverride?: string): Promise<DaemonInfo> {
|
||||
try {
|
||||
return await invoke<DaemonInfo>('start_daemon', { isDev, branchOverride })
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to start daemon: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async stopDaemon(): Promise<void> {
|
||||
try {
|
||||
await invoke('stop_daemon')
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to stop daemon: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async getDaemonInfo(isDev: boolean = import.meta.env.DEV): Promise<DaemonInfo | null> {
|
||||
try {
|
||||
return await invoke<DaemonInfo | null>('get_daemon_info', { isDev })
|
||||
} catch (error) {
|
||||
console.error('Failed to get daemon info:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async isDaemonRunning(): Promise<boolean> {
|
||||
try {
|
||||
return await invoke<boolean>('is_daemon_running')
|
||||
} catch (error) {
|
||||
console.error('Failed to check daemon status:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getStoredConfigs(): Promise<DaemonConfig[]> {
|
||||
try {
|
||||
return await invoke<DaemonConfig[]>('get_stored_configs')
|
||||
} catch (error) {
|
||||
console.error('Failed to get stored configs:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async getConfigPath(): Promise<string> {
|
||||
try {
|
||||
return await invoke<string>('get_config_path')
|
||||
} catch {
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
async connectToExisting(url: string): Promise<void> {
|
||||
// Validate URL format
|
||||
try {
|
||||
new URL(url)
|
||||
} catch {
|
||||
throw new Error(`Invalid URL format: ${url}`)
|
||||
}
|
||||
|
||||
// Store the custom URL temporarily (not in Tauri store)
|
||||
;(window as any).__HUMANLAYER_DAEMON_URL = url
|
||||
;(window as any).__HUMANLAYER_DAEMON_TYPE = 'external'
|
||||
}
|
||||
|
||||
async switchToManagedDaemon(): Promise<void> {
|
||||
// Clear the custom URL to switch back to managed daemon
|
||||
delete (window as any).__HUMANLAYER_DAEMON_URL
|
||||
delete (window as any).__HUMANLAYER_DAEMON_TYPE
|
||||
}
|
||||
|
||||
getDaemonType(): 'managed' | 'external' {
|
||||
return (window as any).__HUMANLAYER_DAEMON_TYPE || 'managed'
|
||||
}
|
||||
}
|
||||
|
||||
export const daemonService = DaemonService.getInstance()
|
||||
Reference in New Issue
Block a user