compass-st 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/VERSION +1 -0
- package/bin/install +174 -0
- package/bootstrap.sh +95 -0
- package/cli/Cargo.lock +270 -0
- package/cli/Cargo.toml +24 -0
- package/cli/src/cmd/context.rs +59 -0
- package/cli/src/cmd/dag.rs +133 -0
- package/cli/src/cmd/git.rs +148 -0
- package/cli/src/cmd/hook.rs +51 -0
- package/cli/src/cmd/index.rs +363 -0
- package/cli/src/cmd/manifest.rs +34 -0
- package/cli/src/cmd/memory.rs +680 -0
- package/cli/src/cmd/migrate.rs +790 -0
- package/cli/src/cmd/mod.rs +14 -0
- package/cli/src/cmd/progress.rs +107 -0
- package/cli/src/cmd/project.rs +1700 -0
- package/cli/src/cmd/session.rs +64 -0
- package/cli/src/cmd/state.rs +317 -0
- package/cli/src/cmd/validate/mod.rs +506 -0
- package/cli/src/cmd/validate/prd.rs +472 -0
- package/cli/src/cmd/version.rs +89 -0
- package/cli/src/helpers.rs +40 -0
- package/cli/src/main.rs +75 -0
- package/cli/tests/fixtures/plan_empty_pointers.json +60 -0
- package/cli/tests/fixtures/plan_missing_pointers.json +59 -0
- package/cli/tests/fixtures/plan_too_many_pointers.json +92 -0
- package/cli/tests/fixtures/plan_v1_valid.json +64 -0
- package/cli/tests/fixtures/prd_bad_flow_bullet.md +37 -0
- package/cli/tests/fixtures/prd_bad_flow_prose.md +33 -0
- package/cli/tests/fixtures/prd_good_flow.md +41 -0
- package/cli/tests/fixtures/prd_xref_dangling.md +38 -0
- package/cli/tests/fixtures/prd_xref_valid.md +53 -0
- package/cli/tests/fixtures/projects/proj_a/.compass/.state/config.json +12 -0
- package/cli/tests/fixtures/projects/proj_b/.compass/.state/config.json +12 -0
- package/cli/tests/fixtures/projects/proj_c/.compass/.state/config.json +12 -0
- package/cli/tests/fixtures/registry/all_dead.json +18 -0
- package/cli/tests/fixtures/registry/corrupt.json +1 -0
- package/cli/tests/fixtures/registry/empty.json +1 -0
- package/cli/tests/fixtures/registry/last_active_dead.json +24 -0
- package/cli/tests/fixtures/registry/multi_alive.json +24 -0
- package/cli/tests/fixtures/registry/one_alive.json +12 -0
- package/cli/tests/fixtures/v0_project/.compass/.state/config.json +5 -0
- package/cli/tests/fixtures/v0_project/.compass/.state/sessions/onboarding-redesign/plan.json +29 -0
- package/cli/tests/fixtures/v0_project/.compass/.state/sessions/sample-feature/context.json +11 -0
- package/cli/tests/fixtures/v0_project/.compass/.state/sessions/sample-feature/plan.json +49 -0
- package/core/colleagues/base-rules.md +112 -0
- package/core/colleagues/manifest.json +85 -0
- package/core/colleagues/market-analyst.md +50 -0
- package/core/colleagues/prioritizer.md +53 -0
- package/core/colleagues/researcher.md +54 -0
- package/core/colleagues/reviewer.md +55 -0
- package/core/colleagues/stakeholder-comm.md +59 -0
- package/core/colleagues/story-breaker.md +57 -0
- package/core/colleagues/ux-reviewer.md +54 -0
- package/core/colleagues/writer.md +55 -0
- package/core/commands/compass/brief.md +28 -0
- package/core/commands/compass/check.md +27 -0
- package/core/commands/compass/epic.md +32 -0
- package/core/commands/compass/feedback.md +32 -0
- package/core/commands/compass/help.md +24 -0
- package/core/commands/compass/ideate.md +32 -0
- package/core/commands/compass/init.md +30 -0
- package/core/commands/compass/plan.md +27 -0
- package/core/commands/compass/prd.md +39 -0
- package/core/commands/compass/prioritize.md +36 -0
- package/core/commands/compass/prototype.md +28 -0
- package/core/commands/compass/release.md +32 -0
- package/core/commands/compass/research.md +31 -0
- package/core/commands/compass/roadmap.md +32 -0
- package/core/commands/compass/run.md +28 -0
- package/core/commands/compass/setup.md +32 -0
- package/core/commands/compass/sprint.md +32 -0
- package/core/commands/compass/status.md +32 -0
- package/core/commands/compass/story.md +37 -0
- package/core/commands/compass/undo.md +33 -0
- package/core/commands/compass/update.md +29 -0
- package/core/hooks/context-monitor.sh +5 -0
- package/core/hooks/manifest-tracker.sh +62 -0
- package/core/hooks/statusline.sh +12 -0
- package/core/hooks/update-checker.sh +24 -0
- package/core/integrations/confluence.md +267 -0
- package/core/integrations/figma.md +277 -0
- package/core/integrations/jira.md +436 -0
- package/core/integrations/vercel.md +170 -0
- package/core/manifest.json +172 -0
- package/core/shared/SCHEMAS-v1.md +404 -0
- package/core/shared/progress.md +145 -0
- package/core/shared/project-scan.md +293 -0
- package/core/shared/resolve-project.md +136 -0
- package/core/shared/ux-rules.md +52 -0
- package/core/shared/version-backup.md +38 -0
- package/core/templates/prd-template.md +145 -0
- package/core/templates/story-template.md +99 -0
- package/core/workflows/brief.md +184 -0
- package/core/workflows/check.md +436 -0
- package/core/workflows/epic.md +177 -0
- package/core/workflows/feedback.md +164 -0
- package/core/workflows/help.md +79 -0
- package/core/workflows/ideate.md +320 -0
- package/core/workflows/init.md +524 -0
- package/core/workflows/migrate.md +136 -0
- package/core/workflows/plan.md +320 -0
- package/core/workflows/prd.md +632 -0
- package/core/workflows/prioritize.md +301 -0
- package/core/workflows/project.md +177 -0
- package/core/workflows/prototype.md +174 -0
- package/core/workflows/release.md +179 -0
- package/core/workflows/research.md +613 -0
- package/core/workflows/roadmap.md +152 -0
- package/core/workflows/run.md +367 -0
- package/core/workflows/setup.md +294 -0
- package/core/workflows/sprint.md +187 -0
- package/core/workflows/status.md +185 -0
- package/core/workflows/story.md +477 -0
- package/core/workflows/undo.md +42 -0
- package/core/workflows/update.md +127 -0
- package/package.json +37 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
use serde_json::json;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
use std::fs;
|
|
4
|
+
use std::cmp::Reverse;
|
|
5
|
+
|
|
6
|
+
pub fn run(args: &[String]) -> Result<String, String> {
|
|
7
|
+
if args.is_empty() { return Err("Usage: compass-cli session <latest|list> [dir]".into()); }
|
|
8
|
+
let sessions_dir = if args.len() > 1 {
|
|
9
|
+
args[1].clone()
|
|
10
|
+
} else {
|
|
11
|
+
".compass/.state/sessions".to_string()
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
match args[0].as_str() {
|
|
15
|
+
"latest" => session_latest(Path::new(&sessions_dir)),
|
|
16
|
+
"list" => session_list(Path::new(&sessions_dir)),
|
|
17
|
+
_ => Err(format!("Unknown session command: {}", args[0])),
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn session_latest(dir: &Path) -> Result<String, String> {
|
|
22
|
+
if !dir.exists() {
|
|
23
|
+
return Ok(serde_json::to_string_pretty(&json!({"found": false})).unwrap());
|
|
24
|
+
}
|
|
25
|
+
let mut entries: Vec<_> = fs::read_dir(dir)
|
|
26
|
+
.map_err(|e| e.to_string())?
|
|
27
|
+
.filter_map(|e| e.ok())
|
|
28
|
+
.filter(|e| e.path().is_dir())
|
|
29
|
+
.collect();
|
|
30
|
+
entries.sort_by_key(|e| Reverse(e.metadata().ok().and_then(|m| m.modified().ok())));
|
|
31
|
+
|
|
32
|
+
match entries.first() {
|
|
33
|
+
Some(entry) => {
|
|
34
|
+
let p = entry.path();
|
|
35
|
+
let name = p.file_name().unwrap_or_default().to_string_lossy().to_string();
|
|
36
|
+
let files: Vec<String> = fs::read_dir(&p)
|
|
37
|
+
.map(|rd| rd.filter_map(|e| e.ok())
|
|
38
|
+
.map(|e| e.file_name().to_string_lossy().to_string())
|
|
39
|
+
.collect())
|
|
40
|
+
.unwrap_or_default();
|
|
41
|
+
Ok(serde_json::to_string_pretty(&json!({
|
|
42
|
+
"found": true,
|
|
43
|
+
"name": name,
|
|
44
|
+
"dir": p.to_string_lossy(),
|
|
45
|
+
"files": files,
|
|
46
|
+
})).unwrap())
|
|
47
|
+
}
|
|
48
|
+
None => Ok(serde_json::to_string_pretty(&json!({"found": false})).unwrap()),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn session_list(dir: &Path) -> Result<String, String> {
|
|
53
|
+
if !dir.exists() { return Ok(json!({"sessions": []}).to_string()); }
|
|
54
|
+
let sessions: Vec<serde_json::Value> = fs::read_dir(dir)
|
|
55
|
+
.map_err(|e| e.to_string())?
|
|
56
|
+
.filter_map(|e| e.ok())
|
|
57
|
+
.filter(|e| e.path().is_dir())
|
|
58
|
+
.map(|e| json!({
|
|
59
|
+
"name": e.file_name().to_string_lossy(),
|
|
60
|
+
"path": e.path().to_string_lossy(),
|
|
61
|
+
}))
|
|
62
|
+
.collect();
|
|
63
|
+
Ok(serde_json::to_string_pretty(&json!({"sessions": sessions})).unwrap())
|
|
64
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
use crate::helpers;
|
|
2
|
+
use serde_json::json;
|
|
3
|
+
use sha2::{Digest, Sha256};
|
|
4
|
+
use std::fs;
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
|
+
use std::time::{Duration, SystemTime};
|
|
7
|
+
|
|
8
|
+
const CONFIG_CACHE_TTL: u64 = 300;
|
|
9
|
+
|
|
10
|
+
pub fn run(args: &[String]) -> Result<String, String> {
|
|
11
|
+
if args.is_empty() { return Err("Usage: compass-cli state <get|update|get-config> <dir> [json]".into()); }
|
|
12
|
+
|
|
13
|
+
match args[0].as_str() {
|
|
14
|
+
"get" => {
|
|
15
|
+
if args.len() < 2 { return Err("Usage: compass-cli state get <dir>".into()); }
|
|
16
|
+
let state_path = resolve_state_path(&args[1]);
|
|
17
|
+
let data = helpers::read_json(&state_path)?;
|
|
18
|
+
Ok(serde_json::to_string_pretty(&data).unwrap())
|
|
19
|
+
}
|
|
20
|
+
"update" => {
|
|
21
|
+
if args.len() < 3 { return Err("Usage: compass-cli state update <dir> <json>".into()); }
|
|
22
|
+
let state_path = resolve_state_path(&args[1]);
|
|
23
|
+
let mut data = if state_path.exists() {
|
|
24
|
+
helpers::read_json(&state_path)?
|
|
25
|
+
} else {
|
|
26
|
+
json!({})
|
|
27
|
+
};
|
|
28
|
+
let patch: serde_json::Value = serde_json::from_str(&args[2])
|
|
29
|
+
.map_err(|e| format!("Invalid JSON patch: {}", e))?;
|
|
30
|
+
if let (Some(obj), Some(patch_obj)) = (data.as_object_mut(), patch.as_object()) {
|
|
31
|
+
for (k, v) in patch_obj { obj.insert(k.clone(), v.clone()); }
|
|
32
|
+
obj.insert("_updated_at".to_string(), json!(chrono_now()));
|
|
33
|
+
}
|
|
34
|
+
helpers::write_json(&state_path, &data)?;
|
|
35
|
+
Ok(serde_json::to_string_pretty(&json!({"success": true, "state": data})).unwrap())
|
|
36
|
+
}
|
|
37
|
+
"get-config" => {
|
|
38
|
+
// Legacy form: `state get-config <dir> [--no-cache]` — kept for back-compat.
|
|
39
|
+
// New form (no positional dir): forward to `project resolve`.
|
|
40
|
+
let positional_dir = args
|
|
41
|
+
.iter()
|
|
42
|
+
.skip(1)
|
|
43
|
+
.find(|a| !a.starts_with("--"))
|
|
44
|
+
.map(|s| s.as_str());
|
|
45
|
+
let no_cache = args.iter().any(|a| a == "--no-cache");
|
|
46
|
+
match positional_dir {
|
|
47
|
+
Some(dir) => {
|
|
48
|
+
eprintln!(
|
|
49
|
+
"DEPRECATED: 'compass-cli state get-config <dir>' is deprecated. \
|
|
50
|
+
Use 'compass-cli project resolve' instead."
|
|
51
|
+
);
|
|
52
|
+
get_config(dir, no_cache)
|
|
53
|
+
}
|
|
54
|
+
None => get_config_via_project_resolve(),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
_ => Err(format!("Unknown state command: {}", args[0])),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn resolve_state_path(dir: &str) -> PathBuf {
|
|
62
|
+
let primary = Path::new(dir).join(".state").join("state.json");
|
|
63
|
+
if primary.exists() { primary } else { Path::new(dir).join("state.json") }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// New-style `state get-config` (no positional dir): delegate to
|
|
67
|
+
/// `compass-cli project resolve` and, on `status=ok`, return only the `config`
|
|
68
|
+
/// object as pretty JSON — preserving the v1.1 API shape that callers expect.
|
|
69
|
+
/// On `status=ambiguous` or `status=none`, return an Err pointing the user to
|
|
70
|
+
/// `project resolve` for full status info (exit 1 semantics via main dispatcher).
|
|
71
|
+
fn get_config_via_project_resolve() -> Result<String, String> {
|
|
72
|
+
let raw = crate::cmd::project::run(&["resolve".to_string()])?;
|
|
73
|
+
let val: serde_json::Value = serde_json::from_str(&raw)
|
|
74
|
+
.map_err(|e| format!("Failed to parse project resolve output: {}", e))?;
|
|
75
|
+
|
|
76
|
+
let status = val.get("status").and_then(|v| v.as_str()).unwrap_or("");
|
|
77
|
+
match status {
|
|
78
|
+
"ok" => {
|
|
79
|
+
let config = val.get("config").cloned().unwrap_or(json!({}));
|
|
80
|
+
Ok(serde_json::to_string_pretty(&config).unwrap())
|
|
81
|
+
}
|
|
82
|
+
"ambiguous" => Err(
|
|
83
|
+
"Multiple Compass projects registered and none is active. \
|
|
84
|
+
Run 'compass-cli project resolve' for full status, \
|
|
85
|
+
then 'compass-cli project use <path>' to pick one."
|
|
86
|
+
.to_string(),
|
|
87
|
+
),
|
|
88
|
+
"none" => Err(
|
|
89
|
+
"No Compass project found. \
|
|
90
|
+
Run 'compass-cli project resolve' for details, \
|
|
91
|
+
or '/compass:init' to create one."
|
|
92
|
+
.to_string(),
|
|
93
|
+
),
|
|
94
|
+
other => Err(format!(
|
|
95
|
+
"Unexpected status '{}' from project resolve. \
|
|
96
|
+
Run 'compass-cli project resolve' directly to inspect.",
|
|
97
|
+
other
|
|
98
|
+
)),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn get_config(dir: &str, no_cache: bool) -> Result<String, String> {
|
|
103
|
+
let config_path = Path::new(dir).join(".compass").join(".state").join("config.json");
|
|
104
|
+
|
|
105
|
+
if !config_path.exists() {
|
|
106
|
+
return Err(format!("Config not found at {}. Run /compass:init first.", config_path.display()));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let cache_path = cache_path_for(&config_path);
|
|
110
|
+
|
|
111
|
+
if !no_cache {
|
|
112
|
+
if let Some(cached) = read_cache_if_fresh(&cache_path, &config_path) {
|
|
113
|
+
return Ok(cached);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let data = helpers::read_json(&config_path)?;
|
|
118
|
+
let serialized = serde_json::to_string_pretty(&data).unwrap();
|
|
119
|
+
|
|
120
|
+
let _ = fs::write(&cache_path, &serialized);
|
|
121
|
+
|
|
122
|
+
Ok(serialized)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn cache_path_for(config_path: &Path) -> PathBuf {
|
|
126
|
+
let abs = fs::canonicalize(config_path).unwrap_or_else(|_| config_path.to_path_buf());
|
|
127
|
+
let mut hasher = Sha256::new();
|
|
128
|
+
hasher.update(abs.to_string_lossy().as_bytes());
|
|
129
|
+
let hash = hex::encode(hasher.finalize());
|
|
130
|
+
let short = &hash[..16];
|
|
131
|
+
std::env::temp_dir().join(format!("compass-config-{}.json", short))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn read_cache_if_fresh(cache_path: &Path, source_path: &Path) -> Option<String> {
|
|
135
|
+
let cache_meta = fs::metadata(cache_path).ok()?;
|
|
136
|
+
let cache_mtime = cache_meta.modified().ok()?;
|
|
137
|
+
let age = SystemTime::now().duration_since(cache_mtime).ok()?;
|
|
138
|
+
|
|
139
|
+
if age >= Duration::from_secs(CONFIG_CACHE_TTL) {
|
|
140
|
+
return None;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if let Ok(src_meta) = fs::metadata(source_path) {
|
|
144
|
+
if let Ok(src_mtime) = src_meta.modified() {
|
|
145
|
+
if src_mtime > cache_mtime {
|
|
146
|
+
return None;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fs::read_to_string(cache_path).ok()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn chrono_now() -> String {
|
|
155
|
+
let d = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
|
|
156
|
+
format!("{}Z", d.as_secs())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[cfg(test)]
|
|
160
|
+
mod tests {
|
|
161
|
+
use super::*;
|
|
162
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
163
|
+
use std::time::UNIX_EPOCH;
|
|
164
|
+
|
|
165
|
+
// Shared guard with project.rs — $HOME is process-global, every module
|
|
166
|
+
// that mutates it MUST hold the same Mutex or they race each other.
|
|
167
|
+
use crate::cmd::project::test_support::HOME_GUARD;
|
|
168
|
+
static TMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
169
|
+
|
|
170
|
+
fn unique_tmp_dir(tag: &str) -> PathBuf {
|
|
171
|
+
let nanos = SystemTime::now()
|
|
172
|
+
.duration_since(UNIX_EPOCH)
|
|
173
|
+
.map(|d| d.as_nanos())
|
|
174
|
+
.unwrap_or(0);
|
|
175
|
+
let n = TMP_COUNTER.fetch_add(1, Ordering::SeqCst);
|
|
176
|
+
let pid = std::process::id();
|
|
177
|
+
let dir = std::env::temp_dir().join(format!(
|
|
178
|
+
"compass-cli-statetest-{}-{}-{}-{}",
|
|
179
|
+
tag, pid, nanos, n
|
|
180
|
+
));
|
|
181
|
+
fs::create_dir_all(&dir).expect("create unique tmp dir");
|
|
182
|
+
dir
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
struct HomeGuard {
|
|
186
|
+
home: PathBuf,
|
|
187
|
+
prev_home: Option<std::ffi::OsString>,
|
|
188
|
+
prev_cwd: Option<PathBuf>,
|
|
189
|
+
_lock: std::sync::MutexGuard<'static, ()>,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
impl HomeGuard {
|
|
193
|
+
fn new(tag: &str) -> Self {
|
|
194
|
+
let lock = HOME_GUARD.lock().unwrap_or_else(|e| e.into_inner());
|
|
195
|
+
let home = unique_tmp_dir(tag);
|
|
196
|
+
let prev_home = std::env::var_os("HOME");
|
|
197
|
+
let prev_cwd = std::env::current_dir().ok();
|
|
198
|
+
std::env::set_var("HOME", &home);
|
|
199
|
+
fs::create_dir_all(home.join(".compass")).expect("mkdir ~/.compass");
|
|
200
|
+
HomeGuard { home, prev_home, prev_cwd, _lock: lock }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn registry_file(&self) -> PathBuf {
|
|
204
|
+
self.home.join(".compass").join("projects.json")
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
impl Drop for HomeGuard {
|
|
209
|
+
fn drop(&mut self) {
|
|
210
|
+
if let Some(p) = &self.prev_cwd {
|
|
211
|
+
let _ = std::env::set_current_dir(p);
|
|
212
|
+
}
|
|
213
|
+
match &self.prev_home {
|
|
214
|
+
Some(p) => std::env::set_var("HOME", p),
|
|
215
|
+
None => std::env::remove_var("HOME"),
|
|
216
|
+
}
|
|
217
|
+
let _ = fs::remove_dir_all(&self.home);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn make_project(parent: &Path, name: &str) -> PathBuf {
|
|
222
|
+
let root = parent.join(name);
|
|
223
|
+
let state = root.join(".compass").join(".state");
|
|
224
|
+
fs::create_dir_all(&state).expect("create state dir");
|
|
225
|
+
let config = json!({
|
|
226
|
+
"version": "1.1.1",
|
|
227
|
+
"project": {"name": name, "po": "@test"},
|
|
228
|
+
"lang": "en",
|
|
229
|
+
});
|
|
230
|
+
fs::write(state.join("config.json"), config.to_string())
|
|
231
|
+
.expect("write config.json");
|
|
232
|
+
fs::canonicalize(&root).unwrap_or(root)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// REQ-02: `state get-config` with NO dir arg forwards to
|
|
236
|
+
/// `project resolve` and returns the resolved project's config.
|
|
237
|
+
#[test]
|
|
238
|
+
fn get_config_forwards_to_resolve() {
|
|
239
|
+
let g = HomeGuard::new("get_config_fwd");
|
|
240
|
+
let parent = unique_tmp_dir("get_config_fwd_parent");
|
|
241
|
+
let root = make_project(&parent, "proj_fwd");
|
|
242
|
+
let root_str = root.to_string_lossy().to_string();
|
|
243
|
+
|
|
244
|
+
let reg = json!({
|
|
245
|
+
"version": "1.0",
|
|
246
|
+
"last_active": root_str,
|
|
247
|
+
"projects": [{
|
|
248
|
+
"path": root_str,
|
|
249
|
+
"name": "proj_fwd",
|
|
250
|
+
"created_at": "2026-04-01T00:00:00Z",
|
|
251
|
+
"last_used": "2026-04-01T00:00:00Z",
|
|
252
|
+
}],
|
|
253
|
+
});
|
|
254
|
+
fs::write(g.registry_file(), reg.to_string())
|
|
255
|
+
.expect("write registry");
|
|
256
|
+
|
|
257
|
+
let out = run(&["get-config".to_string()]).expect("get-config (no dir) ok");
|
|
258
|
+
// Must return the project's config JSON — contains `project` and `lang`.
|
|
259
|
+
assert!(
|
|
260
|
+
out.contains("\"project\"") && out.contains("\"lang\""),
|
|
261
|
+
"expected forwarded config to contain project+lang fields; got:\n{}",
|
|
262
|
+
out
|
|
263
|
+
);
|
|
264
|
+
let v: serde_json::Value =
|
|
265
|
+
serde_json::from_str(&out).expect("returned body is valid JSON");
|
|
266
|
+
assert_eq!(v["project"]["name"], json!("proj_fwd"));
|
|
267
|
+
assert_eq!(v["lang"], json!("en"));
|
|
268
|
+
|
|
269
|
+
let _ = fs::remove_dir_all(&parent);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// REQ-13: `state get-config <dir>` is the legacy alias. It must
|
|
273
|
+
/// still work (back-compat) and emit a DEPRECATED warning. We
|
|
274
|
+
/// verify the legacy branch executes by asserting it returns the
|
|
275
|
+
/// legacy-dir config (a path not reachable via the forwarded
|
|
276
|
+
/// `project resolve` flow when no registry is present).
|
|
277
|
+
#[test]
|
|
278
|
+
fn get_config_arg_warns() {
|
|
279
|
+
let _g = HomeGuard::new("get_config_arg_warns");
|
|
280
|
+
// Build a legacy-shape project dir: <dir>/.compass/.state/config.json.
|
|
281
|
+
let tmp = unique_tmp_dir("get_config_arg_warns_proj");
|
|
282
|
+
let state_dir = tmp.join(".compass").join(".state");
|
|
283
|
+
fs::create_dir_all(&state_dir).expect("create legacy state dir");
|
|
284
|
+
let legacy_cfg = json!({
|
|
285
|
+
"version": "1.1.1",
|
|
286
|
+
"project": {"name": "legacy_proj", "po": "@legacy"},
|
|
287
|
+
"legacy_marker": true,
|
|
288
|
+
});
|
|
289
|
+
fs::write(state_dir.join("config.json"), legacy_cfg.to_string())
|
|
290
|
+
.expect("write legacy config.json");
|
|
291
|
+
|
|
292
|
+
let dir_str = tmp.to_string_lossy().to_string();
|
|
293
|
+
// --no-cache ensures the legacy branch actually reads the file
|
|
294
|
+
// (not a stale cache from a prior run in the shared tmp).
|
|
295
|
+
let out = run(&[
|
|
296
|
+
"get-config".to_string(),
|
|
297
|
+
dir_str,
|
|
298
|
+
"--no-cache".to_string(),
|
|
299
|
+
])
|
|
300
|
+
.expect("legacy get-config <dir> ok");
|
|
301
|
+
|
|
302
|
+
// Deprecation branch executed iff it returned the legacy config
|
|
303
|
+
// (note: `project resolve` path would not have `legacy_marker`).
|
|
304
|
+
let v: serde_json::Value =
|
|
305
|
+
serde_json::from_str(&out).expect("legacy output is JSON");
|
|
306
|
+
assert_eq!(v["legacy_marker"], json!(true));
|
|
307
|
+
assert_eq!(v["project"]["name"], json!("legacy_proj"));
|
|
308
|
+
|
|
309
|
+
// Stderr capture is not feasible in an in-process unit test without
|
|
310
|
+
// platform-specific fd dup; integration tests cover the DEPRECATED
|
|
311
|
+
// string itself. The legacy_marker assertion above proves the
|
|
312
|
+
// deprecated branch ran (the `eprintln!("DEPRECATED: ...")` fires
|
|
313
|
+
// immediately before `get_config(dir, no_cache)` is called).
|
|
314
|
+
|
|
315
|
+
let _ = fs::remove_dir_all(&tmp);
|
|
316
|
+
}
|
|
317
|
+
}
|