Skip to content

Commit 5944ce1

Browse files
committed
feat(one): add settings window for environment variable configuration
1 parent 7c63934 commit 5944ce1

4 files changed

Lines changed: 589 additions & 22 deletions

File tree

apps/objectos-one/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,32 @@ The sidecar receives `OBJECTOS_HOME`, `OS_DATABASE_URL`,
4343
`OS_STORAGE_ROOT`, and `OS_CACHE_DIR` pointed inside that folder, so a
4444
clean uninstall is just deleting that directory.
4545

46+
## Configuration
47+
48+
Open **tray → Settings…** to edit environment variables passed to the
49+
ObjectOS server. Variables are stored in `<data-dir>/one.config.json` as
50+
a flat map and forwarded verbatim when the runtime starts.
51+
52+
Commonly used keys:
53+
54+
| Key | Default | Notes |
55+
|-----------------|------------------|------------------------------------------------------------------|
56+
| `PORT` | auto (3000+) | Fixed port. If in use, falls back to auto with a log line. |
57+
| `HOST` | `127.0.0.1` | `0.0.0.0` exposes the server to the LAN. **Set up auth first.** |
58+
| `OBJECTOS_HOME` | `~/.objectstack` | Data directory. Absolute path. |
59+
| `LOG_LEVEL` || Forwarded to the Node server. |
60+
| _anything else_ || Forwarded as-is to the sidecar process. |
61+
62+
The Settings window also reflects values inherited from the parent
63+
shell — those win over the saved file, so a one-off override still
64+
works:
65+
66+
```
67+
PORT=4001 HOST=0.0.0.0 open -a ObjectOS
68+
```
69+
70+
Save in the Settings window triggers an automatic runtime restart.
71+
4672
## Prerequisites
4773

4874
- 20 + pnpm 10Node
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//! User configuration for ObjectOS One.
2+
//!
3+
//! Model: a flat map of environment variables that get merged into the
4+
//! sidecar process. This intentionally avoids hard-coding "known" config
5+
//! fields — anything the Node server reads from env is configurable.
6+
//!
7+
//! Resolution order (later overrides earlier) for the values handed to the
8+
//! sidecar:
9+
//! 1. Inherited process env (parent shell)
10+
//! 2. `one.config.json` `env` map (user-edited via Settings window)
11+
//! 3. Explicit `OBJECTOS_*` env vars in the parent shell (always win,
12+
//! so a one-off `OBJECTOS_PORT=4001 open -a ObjectOS` overrides the
13+
//! saved config)
14+
15+
use std::{collections::BTreeMap, fs, path::PathBuf};
16+
17+
use serde::{Deserialize, Serialize};
18+
19+
pub const CONFIG_FILE: &str = "one.config.json";
20+
21+
/// Persisted user configuration. `env` is the only field on purpose —
22+
/// keeping the schema flat means new server-side env vars don't require any
23+
/// shell change.
24+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25+
pub struct StoredConfig {
26+
/// Environment variables to pass to the sidecar. BTreeMap so the file
27+
/// stays diff-friendly (sorted keys).
28+
#[serde(default)]
29+
pub env: BTreeMap<String, String>,
30+
}
31+
32+
/// Defaults for the data dir when nothing is set.
33+
pub fn default_data_dir() -> PathBuf {
34+
let mut p = dirs::home_dir().expect("home dir");
35+
p.push(".objectstack");
36+
p
37+
}
38+
39+
/// Path to the JSON config file. Lives next to the user data dir so a clean
40+
/// uninstall (rm of data dir) also clears the config.
41+
pub fn config_file_path() -> PathBuf {
42+
let base = std::env::var("OBJECTOS_HOME")
43+
.map(PathBuf::from)
44+
.unwrap_or_else(default_data_dir);
45+
base.join(CONFIG_FILE)
46+
}
47+
48+
/// Read the config file. Missing → empty config. Malformed → empty config
49+
/// (with a warning) so a typo never bricks the app.
50+
pub fn load() -> StoredConfig {
51+
let path = config_file_path();
52+
let Ok(raw) = fs::read_to_string(&path) else {
53+
return StoredConfig::default();
54+
};
55+
match serde_json::from_str::<StoredConfig>(&raw) {
56+
Ok(cfg) => cfg,
57+
Err(e) => {
58+
eprintln!(
59+
"[one] config: ignoring malformed {} ({e}); using defaults",
60+
path.display()
61+
);
62+
StoredConfig::default()
63+
}
64+
}
65+
}
66+
67+
/// Persist the StoredConfig to disk (pretty-printed for hand-edits).
68+
pub fn save(cfg: &StoredConfig) -> std::io::Result<PathBuf> {
69+
let path = config_file_path();
70+
if let Some(parent) = path.parent() {
71+
fs::create_dir_all(parent)?;
72+
}
73+
let json = serde_json::to_string_pretty(cfg)
74+
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
75+
fs::write(&path, json + "\n")?;
76+
Ok(path)
77+
}
78+
79+
/// Read an effective env var with the same resolution the sidecar will see.
80+
/// Useful for Rust-side decisions (port binding, data dir creation) that
81+
/// must agree with what the Node process gets.
82+
pub fn effective(key: &str) -> Option<String> {
83+
// Parent-shell value wins so debug overrides always work.
84+
if let Ok(v) = std::env::var(key) {
85+
if !v.is_empty() {
86+
return Some(v);
87+
}
88+
}
89+
load().env.get(key).cloned()
90+
}
91+
92+
/// Convenience: parse an env var as u16.
93+
pub fn effective_u16(key: &str) -> Option<u16> {
94+
effective(key).and_then(|s| s.parse().ok())
95+
}
96+
97+
/// Effective host the sidecar will bind to. Default: 127.0.0.1.
98+
pub fn effective_host() -> String {
99+
effective("HOST").unwrap_or_else(|| "127.0.0.1".to_string())
100+
}
101+
102+
/// Effective data dir. Default: ~/.objectstack.
103+
pub fn effective_data_dir() -> PathBuf {
104+
effective("OBJECTOS_HOME")
105+
.map(PathBuf::from)
106+
.unwrap_or_else(default_data_dir)
107+
}
108+
109+
/// Snapshot returned to the Settings window. Includes the stored map plus
110+
/// indicators for keys that are currently overridden by the parent shell
111+
/// (so the UI can show "(set in shell — saved value ignored)").
112+
#[derive(Debug, Clone, Serialize)]
113+
pub struct ConfigSnapshot {
114+
pub env: BTreeMap<String, String>,
115+
pub shell_overrides: BTreeMap<String, String>,
116+
pub config_path: PathBuf,
117+
}
118+
119+
pub fn snapshot() -> ConfigSnapshot {
120+
let stored = load();
121+
let mut shell_overrides = BTreeMap::new();
122+
for key in stored.env.keys() {
123+
if let Ok(v) = std::env::var(key) {
124+
if !v.is_empty() {
125+
shell_overrides.insert(key.clone(), v);
126+
}
127+
}
128+
}
129+
ConfigSnapshot {
130+
env: stored.env,
131+
shell_overrides,
132+
config_path: config_file_path(),
133+
}
134+
}
135+
136+

0 commit comments

Comments
 (0)