Iteration 1: file-backed projects with auto-named temps, lock file, and L1 CLI
Replaces the in-memory database with an on-disk project. Startup either opens a project at the positional CLI path (L1) or creates an auto-named temp project (<YYYYMMDD>-<word>-<word>-<word>) under the OS-standard data directory or a --data-dir override. The new project::Project type owns the directory skeleton and a PID+hostname lock file with stale-lock takeover via sysinfo. The status bar now shows "Project: <Display Name>", derived by a small kebab/snake/camel prettifier. Per-command persistence to YAML/CSV/history.log is NOT yet wired -- that's Iteration 2; for now playground.db carries the state across quits. Tests: 257 passing (231 lib + 9 new integration + 17 existing), 0 failing, 0 skipped. Clippy clean with nursery lints.
This commit is contained in:
+83
-2
@@ -13,6 +13,13 @@ use crate::theme::Theme;
|
||||
pub struct Args {
|
||||
pub theme: Theme,
|
||||
pub log_path: Option<PathBuf>,
|
||||
/// `--data-dir <PATH>`: replace the OS-standard data root
|
||||
/// for the duration of this run (ADR-0015 §1).
|
||||
pub data_dir: Option<PathBuf>,
|
||||
/// Positional path argument: open an existing project at
|
||||
/// this path (L1, ADR-0015 §1). Mutually exclusive with
|
||||
/// `--resume` once that lands.
|
||||
pub project_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -27,6 +34,8 @@ pub enum ArgsError {
|
||||
},
|
||||
#[error("unknown argument: {0}")]
|
||||
Unknown(String),
|
||||
#[error("only one project path may be supplied; got both `{first}` and `{second}`")]
|
||||
MultiplePaths { first: String, second: String },
|
||||
}
|
||||
|
||||
impl Args {
|
||||
@@ -43,6 +52,8 @@ impl Args {
|
||||
{
|
||||
let mut theme = default_theme();
|
||||
let mut log_path = env::var_os("RDBMS_PLAYGROUND_LOG_FILE").map(PathBuf::from);
|
||||
let mut data_dir: Option<PathBuf> = None;
|
||||
let mut project_path: Option<PathBuf> = None;
|
||||
let mut iter = iter.into_iter().map(Into::into);
|
||||
while let Some(arg) = iter.next() {
|
||||
match arg.as_str() {
|
||||
@@ -64,10 +75,30 @@ impl Args {
|
||||
let value = iter.next().ok_or(ArgsError::MissingValue("log-file"))?;
|
||||
log_path = Some(PathBuf::from(value));
|
||||
}
|
||||
other => return Err(ArgsError::Unknown(other.to_string())),
|
||||
"--data-dir" => {
|
||||
let value = iter.next().ok_or(ArgsError::MissingValue("data-dir"))?;
|
||||
data_dir = Some(PathBuf::from(value));
|
||||
}
|
||||
other if other.starts_with("--") => {
|
||||
return Err(ArgsError::Unknown(other.to_string()));
|
||||
}
|
||||
other => {
|
||||
if let Some(existing) = &project_path {
|
||||
return Err(ArgsError::MultiplePaths {
|
||||
first: existing.display().to_string(),
|
||||
second: other.to_string(),
|
||||
});
|
||||
}
|
||||
project_path = Some(PathBuf::from(other));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self { theme, log_path })
|
||||
Ok(Self {
|
||||
theme,
|
||||
log_path,
|
||||
data_dir,
|
||||
project_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,4 +160,54 @@ mod tests {
|
||||
let err = Args::parse(["--bogus"]).unwrap_err();
|
||||
assert!(matches!(err, ArgsError::Unknown(s) if s == "--bogus"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_dir_flag_parses() {
|
||||
let args = Args::parse(["--data-dir", "/tmp/playground-data"]).unwrap();
|
||||
assert_eq!(args.data_dir.as_deref(), Some(std::path::Path::new("/tmp/playground-data")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_dir_flag_missing_value() {
|
||||
let err = Args::parse(["--data-dir"]).unwrap_err();
|
||||
assert!(matches!(err, ArgsError::MissingValue("data-dir")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positional_path_parses() {
|
||||
let args = Args::parse(["/home/me/projects/MyProject"]).unwrap();
|
||||
assert_eq!(
|
||||
args.project_path.as_deref(),
|
||||
Some(std::path::Path::new("/home/me/projects/MyProject"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_dir_and_positional_can_coexist() {
|
||||
let args = Args::parse([
|
||||
"--data-dir",
|
||||
"/tmp/data",
|
||||
"/home/me/MyProject",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(args.data_dir.as_deref(), Some(std::path::Path::new("/tmp/data")));
|
||||
assert_eq!(
|
||||
args.project_path.as_deref(),
|
||||
Some(std::path::Path::new("/home/me/MyProject"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_positional_paths_error() {
|
||||
let err = Args::parse(["/a", "/b"]).unwrap_err();
|
||||
assert!(matches!(err, ArgsError::MultiplePaths { .. }), "got: {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_double_dash_flag_errors_even_with_positional() {
|
||||
// Make sure the path-vs-flag distinction is robust:
|
||||
// unknown flags don't get silently swallowed as paths.
|
||||
let err = Args::parse(["--bogus", "/some/path"]).unwrap_err();
|
||||
assert!(matches!(&err, ArgsError::Unknown(s) if s == "--bogus"), "got: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user