packset_daemon/
workspace.rs1use std::path::Path;
8use std::process::Command;
9
10use packset_core::identity::{self, Strategy};
11use serde_json::{json, Value};
12
13#[must_use]
15pub fn git_remote(cwd: &Path) -> Option<String> {
16 let out = Command::new("git")
17 .arg("-C")
18 .arg(cwd)
19 .args(["remote", "get-url", "origin"])
20 .output()
21 .ok()?;
22 if !out.status.success() {
23 return None;
24 }
25 let url = String::from_utf8_lossy(&out.stdout).trim().to_string();
26 (!url.is_empty()).then_some(url)
27}
28
29#[must_use]
35pub fn resolve(cwd: &Path, strategy: Strategy, session: Option<&str>) -> String {
36 let root = std::fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
37 match strategy {
38 Strategy::Global => "global".into(),
39 Strategy::PerSession => session.map_or_else(
40 || format!("dir:{}", root.display()),
41 |id| format!("session:{id}"),
42 ),
43 Strategy::PerDirectory => format!("dir:{}", root.display()),
44 Strategy::PerRepo => git_remote(&root)
45 .and_then(|url| identity::normalize_remote(&url).ok())
46 .unwrap_or_else(|| format!("dir:{}", root.display())),
47 }
48}
49
50#[must_use]
52pub fn user_peer(explicit: Option<&str>) -> String {
53 if let Some(name) = explicit.map(str::trim).filter(|n| !n.is_empty()) {
54 return name.to_string();
55 }
56 for var in ["GROK_INSIDE_USER_PEER", "USER", "LOGNAME"] {
57 if let Ok(value) = std::env::var(var) {
58 let value = value.trim();
59 if !value.is_empty() {
60 return value.to_string();
61 }
62 }
63 }
64 "unknown".into()
65}
66
67pub fn identity(
73 cwd: &Path,
74 strategy: Strategy,
75 harness: &str,
76 profile: Option<&str>,
77 session: Option<&str>,
78 turn: i64,
79 user: Option<&str>,
80) -> Result<Value, String> {
81 let root = std::fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
82 Ok(json!({
83 "schema": identity::SCHEMA,
84 "workspace": resolve(&root, strategy, session),
85 "workspace_strategy": strategy.as_str(),
86 "user_peer": user_peer(user),
87 "agent_peer": identity::agent_peer(harness, profile)?,
88 "harness": harness,
89 "profile": profile.unwrap_or(""),
90 "session_id": session,
91 "cwd": root.display().to_string(),
92 "turn": turn,
93 }))
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn global_is_global_wherever_it_is_asked() {
102 assert_eq!(resolve(Path::new("/tmp"), Strategy::Global, None), "global");
103 }
104
105 #[test]
106 fn a_directory_strategy_names_the_directory() {
107 let name = resolve(Path::new("/tmp"), Strategy::PerDirectory, None);
108 assert!(name.starts_with("dir:"), "{name}");
109 }
110
111 #[test]
112 fn a_session_without_an_id_falls_back_to_the_directory() {
113 let name = resolve(Path::new("/tmp"), Strategy::PerSession, None);
116 assert!(name.starts_with("dir:"), "{name}");
117 assert_eq!(
118 resolve(Path::new("/tmp"), Strategy::PerSession, Some("abc")),
119 "session:abc"
120 );
121 }
122
123 #[test]
124 fn a_checkout_with_no_remote_names_its_own_path() {
125 let dir = tempfile::tempdir().unwrap();
126 let name = resolve(dir.path(), Strategy::PerRepo, None);
127 assert!(name.starts_with("dir:"), "{name}");
128 }
129
130 #[test]
131 fn an_identity_carries_the_schema_and_the_peers() {
132 let dir = tempfile::tempdir().unwrap();
133 let ident = identity(
134 dir.path(),
135 Strategy::PerRepo,
136 "hermes",
137 None,
138 None,
139 0,
140 Some("rg"),
141 )
142 .unwrap();
143 assert_eq!(ident["schema"], json!(packset_core::identity::SCHEMA));
144 assert_eq!(ident["agent_peer"], json!("hermes"));
145 assert_eq!(ident["user_peer"], json!("rg"));
146 assert_eq!(ident["workspace_strategy"], json!("per-repo"));
147 assert_eq!(ident["turn"], json!(0));
148 assert!(identity(dir.path(), Strategy::PerRepo, "", None, None, 0, None).is_err());
149 }
150}