Skip to content

Commit 07b94d0

Browse files
author
aemiguel
committed
Auto-update toggle, Windows CLI support, agent guide improvements, bump v0.1.44
1 parent 9e1e6ba commit 07b94d0

File tree

7 files changed

+279
-58
lines changed

7 files changed

+279
-58
lines changed

.github/workflows/release.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ jobs:
2121
os: macos-latest
2222
- target: aarch64-apple-darwin
2323
os: macos-latest
24+
- target: x86_64-pc-windows-msvc
25+
os: windows-latest
2426

2527
runs-on: ${{ matrix.os }}
2628

@@ -56,9 +58,12 @@ jobs:
5658
run: cargo build --release --target ${{ matrix.target }}
5759

5860
- name: Package binaries
61+
shell: bash
5962
run: |
63+
ext=""
64+
if [[ "${{ matrix.target }}" == *windows* ]]; then ext=".exe"; fi
6065
for bin in lore lore-server; do
61-
src="target/${{ matrix.target }}/release/${bin}"
66+
src="target/${{ matrix.target }}/release/${bin}${ext}"
6267
tar -czf "${bin}-${{ matrix.target }}.tar.gz" -C "$(dirname "$src")" "$(basename "$src")"
6368
if command -v sha256sum >/dev/null 2>&1; then
6469
sha256sum "${bin}-${{ matrix.target }}.tar.gz" > "${bin}-${{ matrix.target }}.tar.gz.sha256"

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "lore-core"
3-
version = "0.1.43"
3+
version = "0.1.44"
44
edition = "2024"
55
autobins = false
66

@@ -27,7 +27,6 @@ quick-xml = "0.37"
2727
regex = "1"
2828
rand_core = { version = "0.6", features = ["getrandom"] }
2929
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
30-
libc = "0.2"
3130
serde = { version = "1.0", features = ["derive"] }
3231
serde_json = "1.0"
3332
sha2 = "0.10"
@@ -39,6 +38,9 @@ urlencoding = "2.1"
3938
uuid = { version = "1.18", features = ["serde", "v4"] }
4039
v_htmlescape = "0.15"
4140

41+
[target.'cfg(unix)'.dependencies]
42+
libc = "0.2"
43+
4244
[dev-dependencies]
4345
tempfile = "3.20"
4446
tower = { version = "0.5", features = ["util"] }

scripts/install-cli.ps1

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Lore CLI installer for Windows
2+
$ErrorActionPreference = "Stop"
3+
4+
$Repo = if ($env:LORE_GITHUB_REPO) { $env:LORE_GITHUB_REPO } else { "brontoguana/lore" }
5+
$Version = if ($env:LORE_VERSION) { $env:LORE_VERSION } else { "latest" }
6+
$InstallDir = if ($env:LORE_INSTALL_DIR) { $env:LORE_INSTALL_DIR } else { "$env:LOCALAPPDATA\lore\bin" }
7+
$BinaryName = "lore"
8+
9+
function Resolve-LatestVersion {
10+
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -Headers @{ "User-Agent" = "lore-installer" }
11+
return $release.tag_name
12+
}
13+
14+
function Get-CurrentVersion {
15+
$exe = Join-Path $InstallDir "$BinaryName.exe"
16+
if (Test-Path $exe) {
17+
try {
18+
$out = & $exe --version 2>&1
19+
return ($out -replace "^$BinaryName ", "").Trim()
20+
} catch {
21+
return "unknown"
22+
}
23+
}
24+
return "not installed"
25+
}
26+
27+
# Resolve version
28+
if ($Version -eq "latest") {
29+
$RemoteVersion = Resolve-LatestVersion
30+
} else {
31+
$RemoteVersion = $Version
32+
}
33+
34+
$CurrentVersion = Get-CurrentVersion
35+
36+
# Check if update is needed
37+
if ($CurrentVersion -ne "not installed") {
38+
$remoteCmp = $RemoteVersion -replace "^v", ""
39+
$currentCmp = $CurrentVersion -replace "^v", ""
40+
if ($remoteCmp -eq $currentCmp) {
41+
Write-Host "$BinaryName is already at version $CurrentVersion - nothing to do."
42+
exit 0
43+
}
44+
Write-Host "Updating $BinaryName`: $CurrentVersion -> $RemoteVersion"
45+
} else {
46+
Write-Host "Installing $BinaryName $RemoteVersion"
47+
}
48+
49+
$Target = "x86_64-pc-windows-msvc"
50+
$BaseUrl = "https://github.com/$Repo/releases/download/$RemoteVersion"
51+
$ArchiveName = "$BinaryName-$Target.tar.gz"
52+
$TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "lore-install-$([System.Guid]::NewGuid().ToString('N').Substring(0,8))"
53+
54+
try {
55+
New-Item -ItemType Directory -Path $TmpDir -Force | Out-Null
56+
57+
$ArchivePath = Join-Path $TmpDir $ArchiveName
58+
$ChecksumPath = "$ArchivePath.sha256"
59+
60+
Write-Host "Downloading $ArchiveName..."
61+
Invoke-WebRequest -Uri "$BaseUrl/$ArchiveName" -OutFile $ArchivePath -UseBasicParsing
62+
Invoke-WebRequest -Uri "$BaseUrl/$ArchiveName.sha256" -OutFile $ChecksumPath -UseBasicParsing
63+
64+
# Verify checksum
65+
$expected = (Get-Content $ChecksumPath).Split(" ")[0]
66+
$actual = (Get-FileHash -Path $ArchivePath -Algorithm SHA256).Hash.ToLower()
67+
if ($expected -ne $actual) {
68+
Write-Error "Checksum mismatch for $ArchiveName"
69+
exit 1
70+
}
71+
72+
# Extract (tar is available on Windows 10+)
73+
tar -xzf $ArchivePath -C $TmpDir
74+
75+
# Install
76+
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
77+
$src = Join-Path $TmpDir "$BinaryName.exe"
78+
$dst = Join-Path $InstallDir "$BinaryName.exe"
79+
Copy-Item -Path $src -Destination $dst -Force
80+
81+
if ($CurrentVersion -ne "not installed") {
82+
Write-Host "Updated $BinaryName to $RemoteVersion (was $CurrentVersion)"
83+
} else {
84+
Write-Host ""
85+
Write-Host " _ ____ _____ ______ "
86+
Write-Host "| | / __ \| __ \| ____|"
87+
Write-Host "| | | | | | |__) | |__ "
88+
Write-Host "| | | | | | _ /| __| "
89+
Write-Host "| |___| |__| | | \ \| |____ "
90+
Write-Host "|______\____/|_| \_\______|"
91+
Write-Host ""
92+
Write-Host "Installed $BinaryName $RemoteVersion to $dst"
93+
Write-Host ""
94+
95+
# Check if InstallDir is on PATH
96+
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
97+
if ($userPath -notlike "*$InstallDir*") {
98+
Write-Host "Adding $InstallDir to your PATH..."
99+
[Environment]::SetEnvironmentVariable("Path", "$userPath;$InstallDir", "User")
100+
$env:Path = "$env:Path;$InstallDir"
101+
Write-Host "Done. Restart your terminal for PATH changes to take effect."
102+
}
103+
104+
Write-Host ""
105+
Write-Host "Quick start:"
106+
Write-Host " lore setup https://your-server.com"
107+
Write-Host " lore projects # list projects"
108+
Write-Host " lore agent my-agent # start an agent"
109+
Write-Host ""
110+
Write-Host "Run lore --help for all commands."
111+
}
112+
} finally {
113+
Remove-Item -Path $TmpDir -Recurse -Force -ErrorAction SilentlyContinue
114+
}

src/api.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@ fn build_app_with_librarian(
364364
)
365365
.route("/ui/admin/oidc", post(update_oidc_from_ui))
366366
.route("/ui/admin/auto-update", post(update_auto_update_from_ui))
367+
.route(
368+
"/ui/admin/auto-update/toggle-json",
369+
post(toggle_auto_update_json),
370+
)
367371
.route(
368372
"/ui/admin/auto-update/check",
369373
post(check_auto_update_from_ui),
@@ -3397,6 +3401,22 @@ async fn update_oidc_from_ui(
33973401
Ok(Redirect::to("/ui/admin?flash=OIDC%20saved"))
33983402
}
33993403

3404+
async fn toggle_auto_update_json(
3405+
State(state): State<AppState>,
3406+
headers: HeaderMap,
3407+
Form(form): Form<std::collections::HashMap<String, String>>,
3408+
) -> ApiResult<axum::Json<serde_json::Value>> {
3409+
let session = require_ui_admin(&state, &headers)?;
3410+
let csrf = form.get("csrf_token").map(|s| s.as_str()).unwrap_or("");
3411+
verify_csrf(&session, csrf)?;
3412+
let enabled = form.get("enabled").map(|s| s == "true").unwrap_or(false);
3413+
let current = state.auto_update_config.load()?;
3414+
state
3415+
.auto_update_config
3416+
.update(enabled, current.github_repo)?;
3417+
Ok(axum::Json(serde_json::json!({ "ok": true })))
3418+
}
3419+
34003420
async fn update_auto_update_from_ui(
34013421
State(state): State<AppState>,
34023422
headers: HeaderMap,

src/bin/lore.rs

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,7 @@ fn encode_query(value: &str) -> String {
14081408

14091409
// --- Helpers ---
14101410

1411+
#[cfg(unix)]
14111412
fn read_password_hidden(prompt: &str) -> io::Result<String> {
14121413
use std::io::BufRead;
14131414
eprint!("{}", prompt);
@@ -1428,6 +1429,62 @@ fn read_password_hidden(prompt: &str) -> io::Result<String> {
14281429
Ok(password.trim().to_string())
14291430
}
14301431

1432+
#[cfg(windows)]
1433+
fn read_password_hidden(prompt: &str) -> io::Result<String> {
1434+
use std::io::BufRead;
1435+
eprint!("{}", prompt);
1436+
extern "system" {
1437+
fn GetStdHandle(nStdHandle: u32) -> isize;
1438+
fn GetConsoleMode(hConsoleHandle: isize, lpMode: *mut u32) -> i32;
1439+
fn SetConsoleMode(hConsoleHandle: isize, dwMode: u32) -> i32;
1440+
}
1441+
const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; // -10 as u32
1442+
const ENABLE_ECHO_INPUT: u32 = 0x0004;
1443+
unsafe {
1444+
let handle = GetStdHandle(STD_INPUT_HANDLE);
1445+
let mut mode: u32 = 0;
1446+
GetConsoleMode(handle, &mut mode);
1447+
SetConsoleMode(handle, mode & !ENABLE_ECHO_INPUT);
1448+
let mut password = String::new();
1449+
io::stdin().lock().read_line(&mut password)?;
1450+
SetConsoleMode(handle, mode);
1451+
eprintln!();
1452+
Ok(password.trim().to_string())
1453+
}
1454+
}
1455+
1456+
#[cfg(unix)]
1457+
fn is_process_running(pid: u32) -> bool {
1458+
std::process::Command::new("kill")
1459+
.args(["-0", &pid.to_string()])
1460+
.status()
1461+
.map(|s| s.success())
1462+
.unwrap_or(false)
1463+
}
1464+
1465+
#[cfg(unix)]
1466+
fn kill_process(pid: u32) {
1467+
let _ = std::process::Command::new("kill")
1468+
.arg(pid.to_string())
1469+
.status();
1470+
}
1471+
1472+
#[cfg(windows)]
1473+
fn is_process_running(pid: u32) -> bool {
1474+
std::process::Command::new("tasklist")
1475+
.args(["/FI", &format!("PID eq {}", pid), "/NH"])
1476+
.output()
1477+
.map(|o| String::from_utf8_lossy(&o.stdout).contains(&pid.to_string()))
1478+
.unwrap_or(false)
1479+
}
1480+
1481+
#[cfg(windows)]
1482+
fn kill_process(pid: u32) {
1483+
let _ = std::process::Command::new("taskkill")
1484+
.args(["/PID", &pid.to_string(), "/F"])
1485+
.status();
1486+
}
1487+
14311488
fn get_hostname() -> String {
14321489
std::process::Command::new("hostname")
14331490
.output()
@@ -1494,14 +1551,10 @@ async fn agent_command(context: &CliContext, args: AgentArgs) -> CliResult<()> {
14941551
if pid_path.exists() {
14951552
if let Ok(pid_str) = fs::read_to_string(&pid_path) {
14961553
if let Ok(pid) = pid_str.trim().parse::<u32>() {
1497-
let check = std::process::Command::new("kill")
1498-
.args(["-0", &pid.to_string()])
1499-
.status();
1500-
if check.map(|s| s.success()).unwrap_or(false) {
1554+
let is_running = is_process_running(pid);
1555+
if is_running {
15011556
eprintln!("Stopping existing agent '{}' (pid {})", args.name, pid);
1502-
let _ = std::process::Command::new("kill")
1503-
.arg(pid.to_string())
1504-
.status();
1557+
kill_process(pid);
15051558
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
15061559
}
15071560
}

src/main.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::env;
77
use std::fs;
88
use std::io::{self, BufRead, Write};
99
use std::net::SocketAddr;
10+
#[cfg(unix)]
1011
use std::os::unix::io::AsRawFd;
1112
use std::path::PathBuf;
1213

@@ -254,6 +255,7 @@ fn prompt_initial_admin_if_needed(data_root: &str) {
254255
}
255256
}
256257

258+
#[cfg(unix)]
257259
fn read_password_no_echo() -> String {
258260
let stdin_fd = io::stdin().as_raw_fd();
259261
let mut termios = std::mem::MaybeUninit::<libc::termios>::uninit();
@@ -284,6 +286,30 @@ fn read_password_no_echo() -> String {
284286
.to_string()
285287
}
286288

289+
#[cfg(windows)]
290+
fn read_password_no_echo() -> String {
291+
extern "system" {
292+
fn GetStdHandle(nStdHandle: u32) -> isize;
293+
fn GetConsoleMode(hConsoleHandle: isize, lpMode: *mut u32) -> i32;
294+
fn SetConsoleMode(hConsoleHandle: isize, dwMode: u32) -> i32;
295+
}
296+
const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6;
297+
const ENABLE_ECHO_INPUT: u32 = 0x0004;
298+
unsafe {
299+
let handle = GetStdHandle(STD_INPUT_HANDLE);
300+
let mut mode: u32 = 0;
301+
GetConsoleMode(handle, &mut mode);
302+
SetConsoleMode(handle, mode & !ENABLE_ECHO_INPUT);
303+
let mut line = String::new();
304+
let _ = io::stdin().read_line(&mut line);
305+
SetConsoleMode(handle, mode);
306+
eprintln!();
307+
line.trim_end_matches('\n')
308+
.trim_end_matches('\r')
309+
.to_string()
310+
}
311+
}
312+
287313
async fn run_server(data_root: String, bind: String) {
288314
let data_root_path = PathBuf::from(&data_root);
289315

0 commit comments

Comments
 (0)