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,59 @@
|
|
|
1
|
+
use crate::helpers;
|
|
2
|
+
use serde_json::json;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
pub fn run(args: &[String]) -> Result<String, String> {
|
|
6
|
+
if args.len() < 3 { return Err("Usage: compass-cli context pack <session_dir> <task_id>".into()); }
|
|
7
|
+
if args[0] != "pack" && args[0] != "get" {
|
|
8
|
+
return Err(format!("Unknown context command: {}", args[0]));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let session_dir = Path::new(&args[1]);
|
|
12
|
+
let task_id = &args[2];
|
|
13
|
+
let plan_path = session_dir.join("plan.json");
|
|
14
|
+
|
|
15
|
+
if !plan_path.exists() { return Err("plan.json not found in session dir".into()); }
|
|
16
|
+
let plan = helpers::read_json(&plan_path)?;
|
|
17
|
+
|
|
18
|
+
let tasks_key = if plan.get("colleagues").is_some() { "colleagues" } else { "tasks" };
|
|
19
|
+
let tasks = plan.get(tasks_key).and_then(|t| t.as_array())
|
|
20
|
+
.ok_or("No tasks array in plan")?;
|
|
21
|
+
|
|
22
|
+
let task = tasks.iter()
|
|
23
|
+
.find(|t| t["id"].as_str() == Some(task_id))
|
|
24
|
+
.ok_or(format!("Task {} not found", task_id))?;
|
|
25
|
+
|
|
26
|
+
let mut files: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
|
|
27
|
+
|
|
28
|
+
if let Some(pointers) = task.get("context_pointers").and_then(|p| p.as_array()) {
|
|
29
|
+
for ptr in pointers {
|
|
30
|
+
if let Some(ptr_str) = ptr.as_str() {
|
|
31
|
+
// Parse "path:start-end" or just "path"
|
|
32
|
+
let (file_path, _range) = if let Some(colon_pos) = ptr_str.rfind(':') {
|
|
33
|
+
let maybe_range = &ptr_str[colon_pos + 1..];
|
|
34
|
+
if maybe_range.contains('-') && maybe_range.chars().all(|c| c.is_ascii_digit() || c == '-') {
|
|
35
|
+
(&ptr_str[..colon_pos], Some(maybe_range))
|
|
36
|
+
} else {
|
|
37
|
+
(ptr_str, None)
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
(ptr_str, None)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if let Ok(content) = std::fs::read_to_string(file_path) {
|
|
44
|
+
files.insert(file_path.to_string(), json!(content));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let output = json!({"task_id": task_id, "files": files});
|
|
51
|
+
|
|
52
|
+
// Write context pack file
|
|
53
|
+
if args[0] == "pack" {
|
|
54
|
+
let out_path = session_dir.join(format!("{}.context.json", task_id));
|
|
55
|
+
helpers::write_json(&out_path, &output)?;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Ok(serde_json::to_string_pretty(&output).unwrap())
|
|
59
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
use crate::helpers;
|
|
2
|
+
use serde_json::json;
|
|
3
|
+
use std::collections::{HashMap, HashSet, VecDeque};
|
|
4
|
+
use std::path::Path;
|
|
5
|
+
|
|
6
|
+
pub fn run(args: &[String]) -> Result<String, String> {
|
|
7
|
+
if args.len() < 2 { return Err("Usage: compass-cli dag <check|waves> <path>".into()); }
|
|
8
|
+
let data = helpers::read_json(Path::new(&args[1]))?;
|
|
9
|
+
let tasks_key = if data.get("colleagues").is_some() { "colleagues" } else { "tasks" };
|
|
10
|
+
let tasks = data.get(tasks_key).and_then(|t| t.as_array())
|
|
11
|
+
.ok_or("Missing tasks/colleagues array")?;
|
|
12
|
+
|
|
13
|
+
match args[0].as_str() {
|
|
14
|
+
"check" => dag_check(tasks),
|
|
15
|
+
"waves" => dag_waves(tasks),
|
|
16
|
+
_ => Err(format!("Unknown dag command: {}", args[0])),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn dag_check(tasks: &[serde_json::Value]) -> Result<String, String> {
|
|
21
|
+
let ids: HashSet<&str> = tasks.iter().filter_map(|t| t["id"].as_str()).collect();
|
|
22
|
+
let mut dangling = vec![];
|
|
23
|
+
let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
|
|
24
|
+
|
|
25
|
+
for task in tasks {
|
|
26
|
+
let id = task["id"].as_str().unwrap_or("");
|
|
27
|
+
adj.entry(id).or_default();
|
|
28
|
+
if let Some(deps) = task.get("depends_on").and_then(|d| d.as_array()) {
|
|
29
|
+
for dep in deps {
|
|
30
|
+
if let Some(dep_id) = dep.as_str() {
|
|
31
|
+
if !ids.contains(dep_id) { dangling.push(format!("{} -> {}", id, dep_id)); }
|
|
32
|
+
adj.entry(dep_id).or_default().push(id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Cycle detection via DFS
|
|
39
|
+
let mut visited: HashSet<&str> = HashSet::new();
|
|
40
|
+
let mut stack: HashSet<&str> = HashSet::new();
|
|
41
|
+
let mut cycles = vec![];
|
|
42
|
+
|
|
43
|
+
fn dfs<'a>(
|
|
44
|
+
node: &'a str,
|
|
45
|
+
adj: &HashMap<&'a str, Vec<&'a str>>,
|
|
46
|
+
visited: &mut HashSet<&'a str>,
|
|
47
|
+
stack: &mut HashSet<&'a str>,
|
|
48
|
+
cycles: &mut Vec<String>,
|
|
49
|
+
) {
|
|
50
|
+
visited.insert(node);
|
|
51
|
+
stack.insert(node);
|
|
52
|
+
if let Some(neighbors) = adj.get(node) {
|
|
53
|
+
for &next in neighbors {
|
|
54
|
+
if !visited.contains(next) {
|
|
55
|
+
dfs(next, adj, visited, stack, cycles);
|
|
56
|
+
} else if stack.contains(next) {
|
|
57
|
+
cycles.push(format!("{} -> {}", node, next));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
stack.remove(node);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for &id in &ids {
|
|
65
|
+
if !visited.contains(id) {
|
|
66
|
+
dfs(id, &adj, &mut visited, &mut stack, &mut cycles);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Ok(serde_json::to_string_pretty(&json!({
|
|
71
|
+
"valid": cycles.is_empty() && dangling.is_empty(),
|
|
72
|
+
"cycles": cycles,
|
|
73
|
+
"dangling": dangling,
|
|
74
|
+
})).unwrap())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn dag_waves(tasks: &[serde_json::Value]) -> Result<String, String> {
|
|
78
|
+
let mut in_degree: HashMap<String, usize> = HashMap::new();
|
|
79
|
+
let mut adj: HashMap<String, Vec<String>> = HashMap::new();
|
|
80
|
+
let mut task_map: HashMap<String, &serde_json::Value> = HashMap::new();
|
|
81
|
+
|
|
82
|
+
for task in tasks {
|
|
83
|
+
let id = task["id"].as_str().unwrap_or("").to_string();
|
|
84
|
+
in_degree.entry(id.clone()).or_insert(0);
|
|
85
|
+
adj.entry(id.clone()).or_default();
|
|
86
|
+
task_map.insert(id.clone(), task);
|
|
87
|
+
|
|
88
|
+
if let Some(deps) = task.get("depends_on").and_then(|d| d.as_array()) {
|
|
89
|
+
for dep in deps {
|
|
90
|
+
if let Some(dep_id) = dep.as_str() {
|
|
91
|
+
adj.entry(dep_id.to_string()).or_default().push(id.clone());
|
|
92
|
+
*in_degree.entry(id.clone()).or_insert(0) += 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let mut waves: Vec<Vec<serde_json::Value>> = vec![];
|
|
99
|
+
let mut queue: VecDeque<String> = in_degree.iter()
|
|
100
|
+
.filter(|(_, °)| deg == 0)
|
|
101
|
+
.map(|(id, _)| id.clone())
|
|
102
|
+
.collect();
|
|
103
|
+
|
|
104
|
+
while !queue.is_empty() {
|
|
105
|
+
let mut wave = vec![];
|
|
106
|
+
let mut next_queue = VecDeque::new();
|
|
107
|
+
while let Some(id) = queue.pop_front() {
|
|
108
|
+
if let Some(task) = task_map.get(&id) {
|
|
109
|
+
wave.push(json!({
|
|
110
|
+
"id": id,
|
|
111
|
+
"name": task["name"].as_str().unwrap_or(""),
|
|
112
|
+
"complexity": task["complexity"].as_str().unwrap_or(""),
|
|
113
|
+
"budget_tokens": task["budget_tokens"].as_u64().unwrap_or(0),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
if let Some(neighbors) = adj.get(&id) {
|
|
117
|
+
for next in neighbors {
|
|
118
|
+
if let Some(deg) = in_degree.get_mut(next) {
|
|
119
|
+
*deg -= 1;
|
|
120
|
+
if *deg == 0 { next_queue.push_back(next.clone()); }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if !wave.is_empty() { waves.push(wave); }
|
|
126
|
+
queue = next_queue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
Ok(serde_json::to_string_pretty(&json!({
|
|
130
|
+
"wave_count": waves.len(),
|
|
131
|
+
"waves": waves,
|
|
132
|
+
})).unwrap())
|
|
133
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
use serde_json::json;
|
|
2
|
+
use std::process::Command;
|
|
3
|
+
|
|
4
|
+
const DOC_DIRS: &[&str] = &["prd", "epics", "research", "technical", "wiki", "compass"];
|
|
5
|
+
|
|
6
|
+
fn git_available() -> bool {
|
|
7
|
+
Command::new("git")
|
|
8
|
+
.arg("--version")
|
|
9
|
+
.output()
|
|
10
|
+
.map(|o| o.status.success())
|
|
11
|
+
.unwrap_or(false)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fn run_git(args: &[&str]) -> Result<std::process::Output, String> {
|
|
15
|
+
Command::new("git")
|
|
16
|
+
.args(args)
|
|
17
|
+
.output()
|
|
18
|
+
.map_err(|e| e.to_string())
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn run(args: &[String]) -> Result<String, String> {
|
|
22
|
+
if !git_available() {
|
|
23
|
+
return Ok(json!({"available": false}).to_string());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if args.is_empty() {
|
|
27
|
+
return Err("Usage: compass-cli git <branch|commit|status> [args...]".into());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
match args[0].as_str() {
|
|
31
|
+
"branch" => cmd_branch(args),
|
|
32
|
+
"commit" => cmd_commit(args),
|
|
33
|
+
"status" => cmd_status(),
|
|
34
|
+
_ => Err(format!("Unknown git command: {}", args[0])),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn cmd_branch(args: &[String]) -> Result<String, String> {
|
|
39
|
+
if args.len() < 2 {
|
|
40
|
+
return Err("Usage: compass-cli git branch <name>".into());
|
|
41
|
+
}
|
|
42
|
+
let name = &args[1];
|
|
43
|
+
let branch = format!("docs/{}", name);
|
|
44
|
+
|
|
45
|
+
let out = run_git(&["checkout", "-b", &branch])
|
|
46
|
+
.map_err(|e| e.to_string())?;
|
|
47
|
+
|
|
48
|
+
if out.status.success() {
|
|
49
|
+
Ok(json!({
|
|
50
|
+
"success": true,
|
|
51
|
+
"branch": branch,
|
|
52
|
+
"action": "created_and_checked_out"
|
|
53
|
+
})
|
|
54
|
+
.to_string())
|
|
55
|
+
} else {
|
|
56
|
+
// Branch may already exist — try just checking out
|
|
57
|
+
let out2 = run_git(&["checkout", &branch]).map_err(|e| e.to_string())?;
|
|
58
|
+
if out2.status.success() {
|
|
59
|
+
Ok(json!({
|
|
60
|
+
"success": true,
|
|
61
|
+
"branch": branch,
|
|
62
|
+
"action": "checked_out_existing"
|
|
63
|
+
})
|
|
64
|
+
.to_string())
|
|
65
|
+
} else {
|
|
66
|
+
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
|
67
|
+
Err(format!("git checkout failed: {}", stderr))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn cmd_commit(args: &[String]) -> Result<String, String> {
|
|
73
|
+
if args.len() < 2 {
|
|
74
|
+
return Err("Usage: compass-cli git commit <message>".into());
|
|
75
|
+
}
|
|
76
|
+
let message = &args[1];
|
|
77
|
+
|
|
78
|
+
// Stage only files inside designated doc dirs
|
|
79
|
+
let mut staged_count = 0usize;
|
|
80
|
+
for dir in DOC_DIRS {
|
|
81
|
+
let out = run_git(&["add", "--", dir]).map_err(|e| e.to_string())?;
|
|
82
|
+
if out.status.success() {
|
|
83
|
+
staged_count += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if there is anything staged
|
|
88
|
+
let status_out = run_git(&["diff", "--cached", "--name-only"]).map_err(|e| e.to_string())?;
|
|
89
|
+
let staged_files: Vec<String> = String::from_utf8_lossy(&status_out.stdout)
|
|
90
|
+
.lines()
|
|
91
|
+
.map(|l| l.to_string())
|
|
92
|
+
.filter(|l| !l.is_empty())
|
|
93
|
+
.collect();
|
|
94
|
+
|
|
95
|
+
if staged_files.is_empty() {
|
|
96
|
+
return Ok(json!({
|
|
97
|
+
"success": false,
|
|
98
|
+
"reason": "nothing_to_commit",
|
|
99
|
+
"staged": staged_count
|
|
100
|
+
})
|
|
101
|
+
.to_string());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let commit_out = run_git(&["commit", "-m", message]).map_err(|e| e.to_string())?;
|
|
105
|
+
|
|
106
|
+
if commit_out.status.success() {
|
|
107
|
+
Ok(json!({
|
|
108
|
+
"success": true,
|
|
109
|
+
"message": message,
|
|
110
|
+
"files": staged_files
|
|
111
|
+
})
|
|
112
|
+
.to_string())
|
|
113
|
+
} else {
|
|
114
|
+
let stderr = String::from_utf8_lossy(&commit_out.stderr).to_string();
|
|
115
|
+
Err(format!("git commit failed: {}", stderr))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn cmd_status() -> Result<String, String> {
|
|
120
|
+
// Current branch
|
|
121
|
+
let branch_out = run_git(&["rev-parse", "--abbrev-ref", "HEAD"]).map_err(|e| e.to_string())?;
|
|
122
|
+
let branch = String::from_utf8_lossy(&branch_out.stdout)
|
|
123
|
+
.trim()
|
|
124
|
+
.to_string();
|
|
125
|
+
|
|
126
|
+
// Changed files in doc dirs only
|
|
127
|
+
let status_out = run_git(&["status", "--porcelain"]).map_err(|e| e.to_string())?;
|
|
128
|
+
let changed: Vec<String> = String::from_utf8_lossy(&status_out.stdout)
|
|
129
|
+
.lines()
|
|
130
|
+
.filter_map(|line| {
|
|
131
|
+
let path = line.get(3..).unwrap_or("").trim().to_string();
|
|
132
|
+
let in_doc_dir = DOC_DIRS.iter().any(|d| path.starts_with(d));
|
|
133
|
+
if in_doc_dir {
|
|
134
|
+
Some(path)
|
|
135
|
+
} else {
|
|
136
|
+
None
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.collect();
|
|
140
|
+
|
|
141
|
+
Ok(json!({
|
|
142
|
+
"available": true,
|
|
143
|
+
"branch": branch,
|
|
144
|
+
"changed_files": changed,
|
|
145
|
+
"changed_count": changed.len()
|
|
146
|
+
})
|
|
147
|
+
.to_string())
|
|
148
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
pub fn run(args: &[String]) -> Result<String, String> {
|
|
4
|
+
if args.is_empty() { return Err("Usage: compass-cli hook <statusline|update-checker>".into()); }
|
|
5
|
+
match args[0].as_str() {
|
|
6
|
+
"statusline" => statusline(),
|
|
7
|
+
"update-checker" | "check-update" => update_checker(),
|
|
8
|
+
"context-monitor" => Ok(String::new()), // placeholder
|
|
9
|
+
_ => Err(format!("Unknown hook: {}", args[0])),
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn statusline() -> Result<String, String> {
|
|
14
|
+
let config_path = Path::new(".compass/.state/config.json");
|
|
15
|
+
if !config_path.exists() { return Ok("Compass: not initialized".to_string()); }
|
|
16
|
+
|
|
17
|
+
let content = std::fs::read_to_string(config_path).map_err(|e| e.to_string())?;
|
|
18
|
+
let data: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
|
|
19
|
+
|
|
20
|
+
let project = data.pointer("/project/name").and_then(|v| v.as_str()).unwrap_or("?");
|
|
21
|
+
let prefix = data.get("prefix").and_then(|v| v.as_str()).unwrap_or("?");
|
|
22
|
+
let mode = data.get("mode").and_then(|v| v.as_str()).unwrap_or("?");
|
|
23
|
+
|
|
24
|
+
Ok(format!("Compass: {} ({}) | {}", project, prefix, mode))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn update_checker() -> Result<String, String> {
|
|
28
|
+
// Same logic as shell hook but in Rust — check once per day
|
|
29
|
+
let home = std::env::var("HOME").unwrap_or_default();
|
|
30
|
+
let cache_file = Path::new(&home).join(".compass").join(".update-check-cache");
|
|
31
|
+
|
|
32
|
+
if cache_file.exists() {
|
|
33
|
+
if let Ok(content) = std::fs::read_to_string(&cache_file) {
|
|
34
|
+
if let Ok(ts) = content.trim().parse::<u64>() {
|
|
35
|
+
let now = std::time::SystemTime::now()
|
|
36
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
37
|
+
.unwrap().as_secs();
|
|
38
|
+
if now - ts < 86400 { return Ok(String::new()); }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Write cache timestamp
|
|
44
|
+
let now = std::time::SystemTime::now()
|
|
45
|
+
.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
|
46
|
+
let _ = std::fs::write(&cache_file, now.to_string());
|
|
47
|
+
|
|
48
|
+
// We can't easily do HTTP in pure Rust without dependencies, so just output empty
|
|
49
|
+
// The shell hook handles the actual GitHub API call
|
|
50
|
+
Ok(String::new())
|
|
51
|
+
}
|