Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ rmcp = { version = "0.16", features = ["client", "reqwest", "transport-child-pro
clap = { version = "4.5", features = ["derive"] }
dialoguer = { version = "0.11", features = ["password"] }

# Daemonization
daemonize = "0.5"
# Daemonization / low-level OS calls
libc = "0.2"
ignore = "0.4"

Expand Down Expand Up @@ -146,6 +145,12 @@ urlencoding = "2.1.3"
[features]
metrics = ["dep:prometheus"]

[target.'cfg(unix)'.dependencies]
daemonize = "0.5"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Threading"] }

[lints.clippy]
dbg_macro = "deny"
todo = "deny"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ spacebot status # show pid and uptime
spacebot auth login # authenticate via Anthropic OAuth
```

On Unix, daemon control uses a Unix domain socket in the instance directory. On Windows, it uses a local named pipe with the same `start`/`stop`/`status` CLI flow.

The binary creates all databases and directories automatically on first run. See the [quickstart guide](docs/content/docs/(getting-started)/quickstart.mdx) for more detail.

### Authentication
Expand Down
81 changes: 64 additions & 17 deletions src/api/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ use std::io::Write as _;
use std::path::Component;
use std::path::Path;
use std::sync::Arc;
#[cfg(windows)]
use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt as _;
use zip::CompressionMethod;
use zip::write::SimpleFileOptions;

Expand Down Expand Up @@ -159,28 +163,71 @@ pub(super) async fn storage_status(
}

fn read_filesystem_usage(path: &Path) -> anyhow::Result<StorageStatus> {
let mut stats = std::mem::MaybeUninit::<libc::statvfs>::uninit();
let path_cstring = std::ffi::CString::new(path.as_os_str().as_encoded_bytes())?;
#[cfg(unix)]
{
let mut stats = std::mem::MaybeUninit::<libc::statvfs>::uninit();
let path_cstring = std::ffi::CString::new(path.as_os_str().as_encoded_bytes())?;

let result = unsafe { libc::statvfs(path_cstring.as_ptr(), stats.as_mut_ptr()) };
if result != 0 {
return Err(anyhow::anyhow!("statvfs call failed"));
}

let stats = unsafe { stats.assume_init() };
let block_size = stats.f_frsize as u128;
let total_blocks = stats.f_blocks as u128;
let avail_blocks = stats.f_bavail as u128;

let total_bytes = (block_size * total_blocks) as u64;
let used_bytes = directory_size_bytes(path)?;
let available_bytes = (block_size * avail_blocks) as u64;

let result = unsafe { libc::statvfs(path_cstring.as_ptr(), stats.as_mut_ptr()) };
if result != 0 {
return Err(anyhow::anyhow!("statvfs call failed"));
return Ok(StorageStatus {
used_bytes,
total_bytes,
available_bytes,
});
}

let stats = unsafe { stats.assume_init() };
let block_size = stats.f_frsize as u128;
let total_blocks = stats.f_blocks as u128;
let avail_blocks = stats.f_bavail as u128;
#[cfg(not(unix))]
{
#[cfg(windows)]
{
let mut available_bytes = 0u64;
let mut total_bytes = 0u64;
let mut free_bytes = 0u64;
let mut path_wide: Vec<u16> = path.as_os_str().encode_wide().collect();
path_wide.push(0);

let result = unsafe {
GetDiskFreeSpaceExW(
path_wide.as_ptr(),
&mut available_bytes,
&mut total_bytes,
&mut free_bytes,
)
};
if result == 0 {
return Err(anyhow::anyhow!("GetDiskFreeSpaceExW call failed"));
}

let total_bytes = (block_size * total_blocks) as u64;
let used_bytes = directory_size_bytes(path)?;
let available_bytes = (block_size * avail_blocks) as u64;
let used_bytes = directory_size_bytes(path)?;
return Ok(StorageStatus {
used_bytes,
total_bytes,
available_bytes,
});
}

Ok(StorageStatus {
used_bytes,
total_bytes,
available_bytes,
})
#[cfg(not(windows))]
let used_bytes = directory_size_bytes(path)?;
#[cfg(not(windows))]
Ok(StorageStatus {
used_bytes,
total_bytes: 0,
available_bytes: 0,
})
}
Comment on lines +192 to +230
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested conditional compilation structure (#[cfg(not(unix))] containing #[cfg(windows)] and #[cfg(not(windows))]) is complex and could be simplified. Consider using mutually exclusive top-level conditions like #[cfg(unix)], #[cfg(windows)], and #[cfg(not(any(unix, windows)))] for better readability and maintainability.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readability improvement only, behavior is correct and covered by cargo check --release after the platform changes.
Can be done in a follow up.

}

fn directory_size_bytes(root: &Path) -> anyhow::Result<u64> {
Expand Down
Loading