From f667cf8042356f5e933809ee39559f2a8a49d817 Mon Sep 17 00:00:00 2001 From: ADD-SP Date: Sun, 12 Apr 2026 09:58:03 -0700 Subject: [PATCH] fix: add config migration for `[stats]` section and bump to `0.2.1` v0.2.0 shipped the required [stats] config field without migration logic, so any pre-existing clawshell.toml fails to parse on upgrade. - New to_v0_2_1 migration step injects [stats] with persist_path = "/etc/clawshell/stats.json" when the section is absent. - Version gate (from < 0.2.1) catches both 0.1.x and 0.2.0 configs. - Bump version to 0.2.1 across Cargo.toml, example config, README, and all npm packages. --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- clawshell.example.toml | 2 +- npm/clawshell-darwin-arm64/package.json | 2 +- npm/clawshell-linux-arm64/package.json | 2 +- npm/clawshell-linux-x64/package.json | 2 +- npm/clawshell/package.json | 8 +- .../targets/clawshell_toml/versions/mod.rs | 62 +++++++++++++ .../clawshell_toml/versions/to_v0_2_1.rs | 93 +++++++++++++++++++ 10 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 src/migration/targets/clawshell_toml/versions/to_v0_2_1.rs diff --git a/Cargo.lock b/Cargo.lock index dc4f442..44e7d67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,7 +339,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clawshell" -version = "0.2.0" +version = "0.2.1" dependencies = [ "assert_cmd", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 09ef5b6..d6cdf7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clawshell" -version = "0.2.0" +version = "0.2.1" edition = "2024" license-file = "LICENSE" homepage = "https://clawshell.org/" diff --git a/README.md b/README.md index cc27edb..8579edf 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ sudo clawshell config --edit # open in $EDITOR A minimal config looks like this: ```toml -version = "0.2.0" +version = "0.2.1" log_level = "info" [server] diff --git a/clawshell.example.toml b/clawshell.example.toml index 7b60c30..73d3773 100644 --- a/clawshell.example.toml +++ b/clawshell.example.toml @@ -1,5 +1,5 @@ # ClawShell Configuration -version = "0.2.0" +version = "0.2.1" # Log level: trace, debug, info, warn, error log_level = "info" diff --git a/npm/clawshell-darwin-arm64/package.json b/npm/clawshell-darwin-arm64/package.json index 1229d85..6d77fbc 100644 --- a/npm/clawshell-darwin-arm64/package.json +++ b/npm/clawshell-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@clawshell/clawshell-darwin-arm64", - "version": "0.2.0", + "version": "0.2.1", "description": "ClawShell binary for macOS ARM64 (Apple Silicon)", "license": "Apache-2.0", "repository": { diff --git a/npm/clawshell-linux-arm64/package.json b/npm/clawshell-linux-arm64/package.json index f679f78..ff38848 100644 --- a/npm/clawshell-linux-arm64/package.json +++ b/npm/clawshell-linux-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@clawshell/clawshell-linux-arm64", - "version": "0.2.0", + "version": "0.2.1", "description": "ClawShell binary for Linux ARM64", "license": "Apache-2.0", "repository": { diff --git a/npm/clawshell-linux-x64/package.json b/npm/clawshell-linux-x64/package.json index 33f27d3..ee3d844 100644 --- a/npm/clawshell-linux-x64/package.json +++ b/npm/clawshell-linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "@clawshell/clawshell-linux-x64", - "version": "0.2.0", + "version": "0.2.1", "description": "ClawShell binary for Linux x64", "license": "Apache-2.0", "repository": { diff --git a/npm/clawshell/package.json b/npm/clawshell/package.json index d14866c..a5e3153 100644 --- a/npm/clawshell/package.json +++ b/npm/clawshell/package.json @@ -1,6 +1,6 @@ { "name": "@clawshell/clawshell", - "version": "0.2.0", + "version": "0.2.1", "description": "The essential safety harness for OpenClaw's PII & API data", "license": "Apache-2.0", "repository": { @@ -12,9 +12,9 @@ "clawshell": "bin/clawshell.js" }, "optionalDependencies": { - "@clawshell/clawshell-darwin-arm64": "0.2.0", - "@clawshell/clawshell-linux-arm64": "0.2.0", - "@clawshell/clawshell-linux-x64": "0.2.0" + "@clawshell/clawshell-darwin-arm64": "0.2.1", + "@clawshell/clawshell-linux-arm64": "0.2.1", + "@clawshell/clawshell-linux-x64": "0.2.1" }, "engines": { "node": ">=16" diff --git a/src/migration/targets/clawshell_toml/versions/mod.rs b/src/migration/targets/clawshell_toml/versions/mod.rs index 686581d..4a07491 100644 --- a/src/migration/targets/clawshell_toml/versions/mod.rs +++ b/src/migration/targets/clawshell_toml/versions/mod.rs @@ -1,4 +1,5 @@ mod to_v0_1_0_alpha_0; +mod to_v0_2_1; use crate::migration::core::{AmbiguityResolver, ConfigVersion}; use crate::migration::target::TargetError; @@ -33,6 +34,13 @@ pub fn apply_versioned_steps( output.merge(step_output); } + let v0_2_1: ConfigVersion = "0.2.1".parse().expect("literal config version must parse"); + + if from < &v0_2_1 && to >= &v0_2_1 { + let step_output = to_v0_2_1::apply(target_name, table, resolver)?; + output.merge(step_output); + } + Ok(output) } @@ -105,4 +113,58 @@ openai_base_url = "https://api.openai.com" assert!(output.applied_steps.is_empty()); assert!(output.warnings.is_empty()); } + + #[test] + fn test_apply_versioned_steps_adds_stats_from_0_1_1() { + let mut table: toml::value::Table = toml::from_str( + r#" +version = "0.1.1" + +[server] +host = "127.0.0.1" +[upstream] +openai_base_url = "https://api.openai.com" +"#, + ) + .unwrap(); + + let from: ConfigVersion = "0.1.1".parse().unwrap(); + let to: ConfigVersion = "0.2.1".parse().unwrap(); + + let mut resolver = NoopResolver; + let output = apply_versioned_steps("clawshell", &mut table, &from, &to, &mut resolver) + .expect("migration should succeed"); + + assert!(output.applied_steps.iter().any(|s| s.contains("[stats]"))); + let stats = table.get("stats").unwrap().as_table().unwrap(); + assert_eq!( + stats.get("persist_path").unwrap().as_str().unwrap(), + "/etc/clawshell/stats.json" + ); + } + + #[test] + fn test_apply_versioned_steps_adds_stats_from_0_2_0() { + let mut table: toml::value::Table = toml::from_str( + r#" +version = "0.2.0" + +[server] +host = "127.0.0.1" +[upstream] +openai_base_url = "https://api.openai.com" +"#, + ) + .unwrap(); + + let from: ConfigVersion = "0.2.0".parse().unwrap(); + let to: ConfigVersion = "0.2.1".parse().unwrap(); + + let mut resolver = NoopResolver; + let output = apply_versioned_steps("clawshell", &mut table, &from, &to, &mut resolver) + .expect("migration should succeed"); + + assert!(output.applied_steps.iter().any(|s| s.contains("[stats]"))); + assert!(table.contains_key("stats")); + } } diff --git a/src/migration/targets/clawshell_toml/versions/to_v0_2_1.rs b/src/migration/targets/clawshell_toml/versions/to_v0_2_1.rs new file mode 100644 index 0000000..f2d2952 --- /dev/null +++ b/src/migration/targets/clawshell_toml/versions/to_v0_2_1.rs @@ -0,0 +1,93 @@ +use crate::migration::core::AmbiguityResolver; +use crate::migration::target::TargetError; + +use super::VersionStepOutput; + +const STEP_ID: &str = "add `[stats]` section with default persist_path"; + +pub fn apply( + _target_name: &str, + table: &mut toml::value::Table, + _resolver: &mut dyn AmbiguityResolver, +) -> Result { + let mut output = VersionStepOutput::default(); + + if !table.contains_key("stats") { + let mut stats = toml::value::Table::new(); + stats.insert( + "persist_path".to_string(), + toml::Value::String("/etc/clawshell/stats.json".to_string()), + ); + table.insert("stats".to_string(), toml::Value::Table(stats)); + output.applied_steps.push(STEP_ID.to_string()); + } + + Ok(output) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::migration::core::{AmbiguityResolutionError, AmbiguousChoice, MigrationIssue}; + + #[derive(Debug)] + struct NoopResolver; + + impl AmbiguityResolver for NoopResolver { + fn resolve( + &mut self, + _issue: &MigrationIssue, + ) -> Result { + Ok(AmbiguousChoice::ApplyRecommended) + } + } + + #[test] + fn test_injects_stats_when_missing() { + let mut table: toml::value::Table = toml::from_str( + r#" +[server] +host = "127.0.0.1" +[upstream] +openai_base_url = "https://api.openai.com" +"#, + ) + .unwrap(); + + let mut resolver = NoopResolver; + let output = apply("clawshell", &mut table, &mut resolver).unwrap(); + assert_eq!(output.applied_steps.len(), 1); + assert!(output.applied_steps[0].contains("[stats]")); + + let stats = table.get("stats").unwrap().as_table().unwrap(); + assert_eq!( + stats.get("persist_path").unwrap().as_str().unwrap(), + "/etc/clawshell/stats.json" + ); + } + + #[test] + fn test_skips_when_stats_already_present() { + let mut table: toml::value::Table = toml::from_str( + r#" +[server] +host = "127.0.0.1" +[upstream] +openai_base_url = "https://api.openai.com" +[stats] +persist_path = "/custom/path/stats.json" +"#, + ) + .unwrap(); + + let mut resolver = NoopResolver; + let output = apply("clawshell", &mut table, &mut resolver).unwrap(); + assert!(output.applied_steps.is_empty()); + + let stats = table.get("stats").unwrap().as_table().unwrap(); + assert_eq!( + stats.get("persist_path").unwrap().as_str().unwrap(), + "/custom/path/stats.json" + ); + } +}