@team-agent/installer 0.3.16 → 0.3.17
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/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/mod.rs +1 -1
- package/crates/team-agent/src/cli/tests/base.rs +10 -0
- package/crates/team-agent/src/leader/lease.rs +93 -11
- package/crates/team-agent/src/leader/start.rs +38 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +2 -2
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -3019,7 +3019,7 @@ pub mod leader_port {
|
|
|
3019
3019
|
provider: crate::provider::Provider,
|
|
3020
3020
|
_confirm: bool,
|
|
3021
3021
|
) -> Result<Value, CliError> {
|
|
3022
|
-
let result = crate::leader::attach_leader(workspace, pane, provider)
|
|
3022
|
+
let result = crate::leader::attach_leader(workspace, team, pane, provider)
|
|
3023
3023
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
3024
3024
|
let requeued =
|
|
3025
3025
|
attach_requeued_exhausted_watchers(workspace, result.bound_pane_id.as_ref())?;
|
|
@@ -90,6 +90,16 @@ use super::*;
|
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
#[test]
|
|
94
|
+
fn leader_launcher_args_external_leader_after_dashdash_is_provider_arg() {
|
|
95
|
+
let got = leader_launcher_args(&["--".into(), "--external-leader".into()]).unwrap();
|
|
96
|
+
assert!(!got.external_leader);
|
|
97
|
+
assert_eq!(
|
|
98
|
+
got.provider_args,
|
|
99
|
+
vec!["--".to_string(), "--external-leader".to_string()]
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
#[test]
|
|
94
104
|
fn leader_launcher_args_attach_session_spaced() {
|
|
95
105
|
// golden: ["--attach-session","mysess"] -> attach_session="mysess"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//! leader::lease — attach / claim / autobind 统一 CAS 路径 + claim_lease_no_incident
|
|
2
2
|
//! + 双写 / 分叉检测。
|
|
3
3
|
|
|
4
|
+
use std::collections::BTreeSet;
|
|
4
5
|
use std::path::Path;
|
|
5
6
|
|
|
6
7
|
use serde_json::{json, Value};
|
|
@@ -25,27 +26,34 @@ use super::{
|
|
|
25
26
|
/// 整段临界区做 state 变更 + 事件 + 双写 + requeue exhausted watchers。
|
|
26
27
|
pub fn attach_leader(
|
|
27
28
|
workspace: &Path,
|
|
29
|
+
team: Option<&str>,
|
|
28
30
|
pane: Option<&PaneId>,
|
|
29
31
|
provider: Provider,
|
|
30
32
|
) -> Result<LeaseResult, LeaderError> {
|
|
31
33
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
32
|
-
let
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
let scoped_team = team.filter(|value| !value.is_empty());
|
|
35
|
+
let mut state = if scoped_team.is_some() {
|
|
36
|
+
crate::state::projection::select_runtime_state(workspace, scoped_team)?
|
|
37
|
+
} else {
|
|
38
|
+
crate::state::persist::load_runtime_state(workspace)?
|
|
39
|
+
};
|
|
40
|
+
let targets = attach_leader_targets(workspace, &state);
|
|
36
41
|
let pane_id = pane
|
|
37
42
|
.cloned()
|
|
38
43
|
.or_else(|| std::env::var("TMUX_PANE").ok().filter(|p| !p.is_empty()).map(PaneId::new))
|
|
39
44
|
.ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
|
|
40
45
|
let non_empty_pane_id = NonEmptyPaneId::try_from_pane(&pane_id)?;
|
|
41
|
-
let Some(target) = targets.iter().find(|target| target.pane_id == pane_id) else {
|
|
46
|
+
let Some(target) = targets.iter().find(|target| target.info.pane_id == pane_id) else {
|
|
42
47
|
return Err(LeaderError::Validation(format!("tmux pane not found: {pane_id}")));
|
|
43
48
|
};
|
|
44
|
-
let mut receiver = receiver_for_attach_target(workspace, &state, target, provider, Discovery::ExplicitPane)?;
|
|
45
|
-
let
|
|
49
|
+
let mut receiver = receiver_for_attach_target(workspace, &state, &target.info, provider, Discovery::ExplicitPane)?;
|
|
50
|
+
if let Some(endpoint) = target.endpoint.as_ref() {
|
|
51
|
+
receiver.tmux_socket = Some(endpoint.clone());
|
|
52
|
+
}
|
|
53
|
+
let validation = validate_attach_target(workspace, &state, &target.info);
|
|
46
54
|
if validation.is_err() {
|
|
47
|
-
let pane_info = pane_info_value(target);
|
|
48
|
-
let targets_value = Value::Array(targets.iter().map(pane_info_value).collect());
|
|
55
|
+
let pane_info = pane_info_value(&target.info);
|
|
56
|
+
let targets_value = Value::Array(targets.iter().map(|target| pane_info_value(&target.info)).collect());
|
|
49
57
|
let owner_record = state_owner(&state);
|
|
50
58
|
if let Some((readopted, validation)) = crate::leader::try_readopt_leader_pane(
|
|
51
59
|
workspace,
|
|
@@ -85,7 +93,7 @@ pub fn attach_leader(
|
|
|
85
93
|
let epoch = current_owner_epoch(&state);
|
|
86
94
|
if state.get("team_owner").is_some() {
|
|
87
95
|
write_receiver_to_state(&mut state, &receiver)?;
|
|
88
|
-
|
|
96
|
+
write_claim_state(workspace, &state, scoped_team, None)?;
|
|
89
97
|
event_log.write(
|
|
90
98
|
super::LeaderEvent::ReceiverAttached.name(),
|
|
91
99
|
json!({"pane_id": pane_id.as_str(), "owner_epoch": epoch.0}),
|
|
@@ -108,7 +116,7 @@ pub fn attach_leader(
|
|
|
108
116
|
receiver.leader_session_uuid = Some(identity.leader_session_uuid.clone());
|
|
109
117
|
let owner = make_owner(provider, &non_empty_pane_id, &identity, next_epoch);
|
|
110
118
|
write_binding_to_state(&mut state, &receiver, &owner)?;
|
|
111
|
-
|
|
119
|
+
write_claim_state(workspace, &state, scoped_team, None)?;
|
|
112
120
|
event_log.write(
|
|
113
121
|
super::LeaderEvent::ReceiverAttached.name(),
|
|
114
122
|
json!({"pane_id": pane_id.as_str(), "owner_epoch": next_epoch.0}),
|
|
@@ -126,6 +134,80 @@ pub fn attach_leader(
|
|
|
126
134
|
})
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
#[derive(Clone)]
|
|
138
|
+
struct AttachLeaderTarget {
|
|
139
|
+
info: PaneInfo,
|
|
140
|
+
endpoint: Option<String>,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTarget> {
|
|
144
|
+
let mut targets = Vec::new();
|
|
145
|
+
for endpoint in state_recorded_tmux_endpoints(state) {
|
|
146
|
+
let backend = tmux_backend_for_endpoint(&endpoint);
|
|
147
|
+
targets.extend(
|
|
148
|
+
backend
|
|
149
|
+
.list_targets()
|
|
150
|
+
.unwrap_or_default()
|
|
151
|
+
.into_iter()
|
|
152
|
+
.map(|info| AttachLeaderTarget {
|
|
153
|
+
info,
|
|
154
|
+
endpoint: Some(endpoint.clone()),
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
targets.extend(
|
|
159
|
+
crate::tmux_backend::TmuxBackend::for_workspace(workspace)
|
|
160
|
+
.list_targets()
|
|
161
|
+
.unwrap_or_default()
|
|
162
|
+
.into_iter()
|
|
163
|
+
.map(|info| AttachLeaderTarget { info, endpoint: None }),
|
|
164
|
+
);
|
|
165
|
+
targets
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn tmux_backend_for_endpoint(endpoint: &str) -> crate::tmux_backend::TmuxBackend {
|
|
169
|
+
if endpoint.is_empty() || endpoint == "default" {
|
|
170
|
+
crate::tmux_backend::TmuxBackend::new()
|
|
171
|
+
} else if Path::new(endpoint).is_absolute() {
|
|
172
|
+
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint)
|
|
173
|
+
} else {
|
|
174
|
+
crate::tmux_backend::TmuxBackend::for_socket_name(endpoint)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn state_recorded_tmux_endpoints(state: &Value) -> BTreeSet<String> {
|
|
179
|
+
let mut out = BTreeSet::new();
|
|
180
|
+
push_state_tmux_endpoints(state, &mut out);
|
|
181
|
+
if let Some(teams) = state.get("teams").and_then(Value::as_object) {
|
|
182
|
+
for team_state in teams.values() {
|
|
183
|
+
push_state_tmux_endpoints(team_state, &mut out);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
out
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn push_state_tmux_endpoints(state: &Value, out: &mut BTreeSet<String>) {
|
|
190
|
+
for key in ["tmux_socket", "tmux_endpoint"] {
|
|
191
|
+
if let Some(endpoint) = state
|
|
192
|
+
.get(key)
|
|
193
|
+
.and_then(Value::as_str)
|
|
194
|
+
.filter(|value| !value.is_empty())
|
|
195
|
+
{
|
|
196
|
+
out.insert(endpoint.to_string());
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for key in ["team_owner", "leader_receiver"] {
|
|
200
|
+
if let Some(endpoint) = state
|
|
201
|
+
.get(key)
|
|
202
|
+
.and_then(|value| value.get("tmux_socket"))
|
|
203
|
+
.and_then(Value::as_str)
|
|
204
|
+
.filter(|value| !value.is_empty())
|
|
205
|
+
{
|
|
206
|
+
out.insert(endpoint.to_string());
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
129
211
|
fn requeue_exhausted_watchers_after_attach(
|
|
130
212
|
workspace: &Path,
|
|
131
213
|
state: &Value,
|
|
@@ -205,6 +205,9 @@ pub fn execute_leader_plan(
|
|
|
205
205
|
.unwrap_or_else(|| "signal".to_string())
|
|
206
206
|
)));
|
|
207
207
|
}
|
|
208
|
+
if plan.is_external_leader {
|
|
209
|
+
persist_external_leader_topology_marker(plan, workspace)?;
|
|
210
|
+
}
|
|
208
211
|
if detached {
|
|
209
212
|
Ok(LeaderLaunchOutcome {
|
|
210
213
|
status: LeaderLaunchStatus::Detached,
|
|
@@ -529,6 +532,41 @@ fn persist_managed_leader_binding(
|
|
|
529
532
|
Ok(())
|
|
530
533
|
}
|
|
531
534
|
|
|
535
|
+
fn persist_external_leader_topology_marker(
|
|
536
|
+
plan: &LeaderStartPlan,
|
|
537
|
+
workspace: &Path,
|
|
538
|
+
) -> Result<(), LeaderError> {
|
|
539
|
+
let identity = plan
|
|
540
|
+
.identity
|
|
541
|
+
.as_ref()
|
|
542
|
+
.ok_or_else(|| LeaderError::Start("external leader identity missing".to_string()))?;
|
|
543
|
+
let mut state = crate::state::persist::load_runtime_state(workspace)
|
|
544
|
+
.unwrap_or_else(|_| serde_json::json!({}));
|
|
545
|
+
if let Some(obj) = state.as_object_mut() {
|
|
546
|
+
obj.entry("workspace".to_string()).or_insert_with(|| {
|
|
547
|
+
serde_json::json!(resolve_workspace_for_hash(workspace).to_string_lossy().to_string())
|
|
548
|
+
});
|
|
549
|
+
obj.entry("active_team_key".to_string())
|
|
550
|
+
.or_insert_with(|| serde_json::json!(identity.team_id.as_str()));
|
|
551
|
+
if let Some(session) = plan.session_name.as_ref() {
|
|
552
|
+
obj.entry("session_name".to_string())
|
|
553
|
+
.or_insert_with(|| serde_json::json!(session.as_str()));
|
|
554
|
+
}
|
|
555
|
+
obj.insert("is_external_leader".to_string(), serde_json::json!(true));
|
|
556
|
+
}
|
|
557
|
+
let entry = crate::state::projection::compact_team_state(&state);
|
|
558
|
+
if let Some(obj) = state.as_object_mut() {
|
|
559
|
+
let teams = obj
|
|
560
|
+
.entry("teams".to_string())
|
|
561
|
+
.or_insert_with(|| serde_json::json!({}));
|
|
562
|
+
if let Some(teams) = teams.as_object_mut() {
|
|
563
|
+
teams.insert(identity.team_id.as_str().to_string(), entry);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
crate::state::persist::save_runtime_state(workspace, &state)?;
|
|
567
|
+
Ok(())
|
|
568
|
+
}
|
|
569
|
+
|
|
532
570
|
fn insert_detach_flag(argv: &mut Vec<String>) -> bool {
|
|
533
571
|
if argv.iter().any(|arg| arg == "-d") {
|
|
534
572
|
return false;
|
|
@@ -14,7 +14,7 @@ use super::*;
|
|
|
14
14
|
fn attach_leader_errors_when_pane_not_resolvable() {
|
|
15
15
|
let ws = std::env::temp_dir().join(format!("ta_rs_attach_{}", std::process::id()));
|
|
16
16
|
std::fs::create_dir_all(&ws).unwrap();
|
|
17
|
-
let r = attach_leader(&ws, Some(&PaneId::new("%1")), Provider::Codex);
|
|
17
|
+
let r = attach_leader(&ws, None, Some(&PaneId::new("%1")), Provider::Codex);
|
|
18
18
|
// 不可解析 pane → Err(Validation),错误串提及 pane not found。
|
|
19
19
|
match r {
|
|
20
20
|
Err(LeaderError::Validation(msg)) => {
|
|
@@ -39,7 +39,7 @@ use super::*;
|
|
|
39
39
|
let ws = std::env::temp_dir().join(format!("ta_rs_attach_ok_{}", std::process::id()));
|
|
40
40
|
std::fs::create_dir_all(&ws).unwrap();
|
|
41
41
|
let pane = PaneId::new("%1");
|
|
42
|
-
let r = attach_leader(&ws, Some(&pane), Provider::Codex).unwrap();
|
|
42
|
+
let r = attach_leader(&ws, None, Some(&pane), Provider::Codex).unwrap();
|
|
43
43
|
assert!(r.ok);
|
|
44
44
|
assert_eq!(r.status, LeaseStatus::Claimed);
|
|
45
45
|
let owner = r.owner.as_ref().expect("attach 成功必带 owner");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.3.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.3.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.3.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.3.17",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.17",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.17"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|