@team-agent/installer 0.3.37 → 0.3.39
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/emit.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch.rs +21 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +2 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +7 -1
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +139 -0
- package/crates/team-agent/src/transport/test_support.rs +13 -6
- package/npm/install.mjs +172 -3
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -41,6 +41,10 @@ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
|
|
|
41
41
|
println!("{}", command_help(None));
|
|
42
42
|
return ExitCode::Ok;
|
|
43
43
|
}
|
|
44
|
+
if matches!(command, "-V" | "--version") {
|
|
45
|
+
println!("team-agent {}", env!("CARGO_PKG_VERSION"));
|
|
46
|
+
return ExitCode::Ok;
|
|
47
|
+
}
|
|
44
48
|
// CR-063/G4: every registered subcommand's `--help` must short-circuit before dispatch,
|
|
45
49
|
// before argument validation, leader-pane checks, or runtime-state writes.
|
|
46
50
|
//
|
|
@@ -2805,6 +2805,7 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
2805
2805
|
let workspace = workspace.to_path_buf();
|
|
2806
2806
|
let mut spec = crate::compiler::compile_team(agents_dir)
|
|
2807
2807
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
2808
|
+
override_spec_workspace(&mut spec, &workspace);
|
|
2808
2809
|
if !open_display {
|
|
2809
2810
|
override_spec_display_backend(&mut spec, "none");
|
|
2810
2811
|
}
|
|
@@ -3548,6 +3549,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
3548
3549
|
// 就地读外部 role 文档编译,注入 base team spec 的 agents/routing。role 文件留在原处。
|
|
3549
3550
|
let mut spec = crate::compiler::compile_team(team_dir)
|
|
3550
3551
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
3552
|
+
override_spec_workspace(&mut spec, run_workspace);
|
|
3551
3553
|
let workspace_s = spec
|
|
3552
3554
|
.get("team")
|
|
3553
3555
|
.and_then(|team| team.get("workspace"))
|
|
@@ -4724,6 +4726,25 @@ pub(crate) fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
|
4724
4726
|
override_spec_runtime_str(spec, "session_name", session_name);
|
|
4725
4727
|
}
|
|
4726
4728
|
|
|
4729
|
+
pub(crate) fn override_spec_workspace(spec: &mut Value, workspace: &Path) {
|
|
4730
|
+
let workspace_s = workspace.to_string_lossy().to_string();
|
|
4731
|
+
let Value::Map(root) = spec else { return };
|
|
4732
|
+
if let Some((_, Value::Map(team))) = root.iter_mut().find(|(k, _)| k == "team") {
|
|
4733
|
+
if let Some((_, value)) = team.iter_mut().find(|(k, _)| k == "workspace") {
|
|
4734
|
+
*value = Value::Str(workspace_s.clone());
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
if let Some((_, Value::List(agents))) = root.iter_mut().find(|(k, _)| k == "agents") {
|
|
4738
|
+
for agent in agents {
|
|
4739
|
+
if let Value::Map(fields) = agent {
|
|
4740
|
+
if let Some((_, value)) = fields.iter_mut().find(|(k, _)| k == "working_directory") {
|
|
4741
|
+
*value = Value::Str(workspace_s.clone());
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4727
4748
|
fn override_spec_display_backend(spec: &mut Value, display_backend: &str) {
|
|
4728
4749
|
override_spec_runtime_str(spec, "display_backend", display_backend);
|
|
4729
4750
|
}
|
|
@@ -621,6 +621,10 @@ fn mark_agent_started(
|
|
|
621
621
|
"spawned_at".to_string(),
|
|
622
622
|
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
623
623
|
);
|
|
624
|
+
agent.insert(
|
|
625
|
+
"spawn_cwd".to_string(),
|
|
626
|
+
serde_json::json!(spawn.spawn_cwd.to_string_lossy().to_string()),
|
|
627
|
+
);
|
|
624
628
|
crate::lifecycle::launch::persist_command_plan_state(
|
|
625
629
|
agent,
|
|
626
630
|
&spawn.plan,
|
|
@@ -5,6 +5,7 @@ pub(super) struct SpawnedAgentWindow {
|
|
|
5
5
|
pub plan: crate::provider::CommandPlan,
|
|
6
6
|
pub profile_launch: crate::provider::ProviderProfileLaunch,
|
|
7
7
|
pub layout_placement: Option<crate::lifecycle::launch::LayoutPlacement>,
|
|
8
|
+
pub spawn_cwd: std::path::PathBuf,
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
pub(super) fn spawn_agent_window(
|
|
@@ -238,6 +239,7 @@ pub(super) fn spawn_agent_window(
|
|
|
238
239
|
plan,
|
|
239
240
|
profile_launch,
|
|
240
241
|
layout_placement: layout_placement.cloned(),
|
|
242
|
+
spawn_cwd: spawn_cwd.to_path_buf(),
|
|
241
243
|
})
|
|
242
244
|
}
|
|
243
245
|
|
|
@@ -309,7 +309,7 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
309
309
|
transport,
|
|
310
310
|
Some(&safety),
|
|
311
311
|
layout_placement.as_ref(),
|
|
312
|
-
|
|
312
|
+
None,
|
|
313
313
|
) {
|
|
314
314
|
Ok(spawn) => spawn,
|
|
315
315
|
Err(error) => {
|
|
@@ -935,6 +935,10 @@ fn mark_agent_respawned(
|
|
|
935
935
|
"spawned_at".to_string(),
|
|
936
936
|
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
937
937
|
);
|
|
938
|
+
agent.insert(
|
|
939
|
+
"spawn_cwd".to_string(),
|
|
940
|
+
serde_json::json!(spawn.spawn_cwd.to_string_lossy().to_string()),
|
|
941
|
+
);
|
|
938
942
|
if matches!(
|
|
939
943
|
restart_mode,
|
|
940
944
|
StartMode::Fresh | StartMode::FreshAfterMissingRollout
|
|
@@ -1382,6 +1386,7 @@ fn rebuild_runtime_spec_from_roles(
|
|
|
1382
1386
|
// 重建:compile_team(角色定义) + 保留运行期 session_name override。
|
|
1383
1387
|
let mut spec = crate::compiler::compile_team(&team_dir)
|
|
1384
1388
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1389
|
+
crate::lifecycle::launch::override_spec_workspace(&mut spec, run_workspace);
|
|
1385
1390
|
if let Some(session_name) = state
|
|
1386
1391
|
.get("session_name")
|
|
1387
1392
|
.and_then(serde_json::Value::as_str)
|
|
@@ -1589,6 +1594,7 @@ mod tests {
|
|
|
1589
1594
|
plan: crate::provider::CommandPlan::argv_only(vec!["codex".to_string()]),
|
|
1590
1595
|
profile_launch: crate::provider::ProviderProfileLaunch::default(),
|
|
1591
1596
|
layout_placement: None,
|
|
1597
|
+
spawn_cwd: std::path::PathBuf::from("/tmp/team-epoch"),
|
|
1592
1598
|
};
|
|
1593
1599
|
let before = chrono::Utc::now();
|
|
1594
1600
|
|
|
@@ -225,6 +225,60 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
225
225
|
);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
#[test]
|
|
229
|
+
fn quick_start_default_workspace_compiled_spec_uses_project_root() {
|
|
230
|
+
let workspace = temp_ws();
|
|
231
|
+
std::fs::create_dir_all(workspace.join("agents")).unwrap();
|
|
232
|
+
std::fs::write(workspace.join("TEAM.md"), QS_TEAM_MD).unwrap();
|
|
233
|
+
std::fs::write(workspace.join("agents").join("implementer.md"), QS_VALID_ROLE).unwrap();
|
|
234
|
+
seed_healthy_coordinator(&workspace);
|
|
235
|
+
let transport = OfflineTransport::new();
|
|
236
|
+
|
|
237
|
+
let report = quick_start_with_transport_in_workspace(
|
|
238
|
+
&workspace,
|
|
239
|
+
&workspace,
|
|
240
|
+
None,
|
|
241
|
+
true,
|
|
242
|
+
true,
|
|
243
|
+
None,
|
|
244
|
+
&transport,
|
|
245
|
+
)
|
|
246
|
+
.expect("quick-start workspace-root team should reach a report");
|
|
247
|
+
assert!(
|
|
248
|
+
matches!(report, QuickStartReport::Ready { .. }),
|
|
249
|
+
"quick-start failed: {report:?}"
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
253
|
+
let spec_path = state
|
|
254
|
+
.get("spec_path")
|
|
255
|
+
.and_then(serde_json::Value::as_str)
|
|
256
|
+
.map(PathBuf::from)
|
|
257
|
+
.expect("quick-start state should carry spec_path");
|
|
258
|
+
let spec = crate::model::yaml::loads(&std::fs::read_to_string(spec_path).unwrap()).unwrap();
|
|
259
|
+
assert_eq!(
|
|
260
|
+
spec.get("team")
|
|
261
|
+
.and_then(|team| team.get("workspace"))
|
|
262
|
+
.and_then(crate::model::yaml::Value::as_str),
|
|
263
|
+
Some(workspace.to_string_lossy().as_ref()),
|
|
264
|
+
"compiled team.workspace must be the project root, not its parent"
|
|
265
|
+
);
|
|
266
|
+
for agent in spec.get("agents").and_then(crate::model::yaml::Value::as_list).unwrap() {
|
|
267
|
+
assert_eq!(
|
|
268
|
+
agent
|
|
269
|
+
.get("working_directory")
|
|
270
|
+
.and_then(crate::model::yaml::Value::as_str),
|
|
271
|
+
Some(workspace.to_string_lossy().as_ref()),
|
|
272
|
+
"compiled agent working_directory must be the project root"
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
assert_eq!(
|
|
276
|
+
transport.spawn_cwd_records(),
|
|
277
|
+
vec![workspace],
|
|
278
|
+
"quick-start must spawn workers from the project root"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
228
282
|
// P0 — quick_start over an INVALID role doc (missing `provider`) must surface the REAL compile
|
|
229
283
|
// error, distinct from the stub's hardcoded "no role docs found". Golden: compile_team raises
|
|
230
284
|
// "missing front matter field provider" (compiler.py:_validate_role_doc), before preflight.
|
|
@@ -1533,6 +1587,91 @@ fn restart_with_transport_spawns_resumable_workers_not_stub() {
|
|
|
1533
1587
|
);
|
|
1534
1588
|
}
|
|
1535
1589
|
|
|
1590
|
+
#[test]
|
|
1591
|
+
fn restart_runtime_spec_spawns_from_run_workspace_not_runtime_dir() {
|
|
1592
|
+
let workspace = temp_ws();
|
|
1593
|
+
std::fs::create_dir_all(workspace.join("agents")).unwrap();
|
|
1594
|
+
let rollout = workspace.join("alpha-rollout.jsonl");
|
|
1595
|
+
std::fs::write(&rollout, "{}\n").unwrap();
|
|
1596
|
+
std::fs::write(
|
|
1597
|
+
workspace.join("TEAM.md"),
|
|
1598
|
+
"---\nname: current\nobjective: Restart cwd probe.\nprovider: codex\n---\n\nteam.\n",
|
|
1599
|
+
)
|
|
1600
|
+
.unwrap();
|
|
1601
|
+
std::fs::write(workspace.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
|
|
1602
|
+
let spec = crate::compiler::compile_team(&workspace).expect("compile project-root team");
|
|
1603
|
+
let spec_path = crate::model::paths::runtime_spec_path(&workspace, "current");
|
|
1604
|
+
std::fs::create_dir_all(spec_path.parent().unwrap()).unwrap();
|
|
1605
|
+
std::fs::write(&spec_path, crate::model::yaml::dumps(&spec)).unwrap();
|
|
1606
|
+
crate::state::persist::save_runtime_state(
|
|
1607
|
+
&workspace,
|
|
1608
|
+
&json!({
|
|
1609
|
+
"active_team_key": "current",
|
|
1610
|
+
"session_name": "team-current",
|
|
1611
|
+
"spec_path": spec_path.to_string_lossy(),
|
|
1612
|
+
"team_dir": workspace.to_string_lossy(),
|
|
1613
|
+
"agents": {
|
|
1614
|
+
"alpha": {
|
|
1615
|
+
"status": "running",
|
|
1616
|
+
"provider": "codex",
|
|
1617
|
+
"session_id": "sess-a",
|
|
1618
|
+
"rollout_path": rollout.to_string_lossy(),
|
|
1619
|
+
"first_send_at": "2026-05-27T10:00:00+00:00",
|
|
1620
|
+
"spawn_cwd": null
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}),
|
|
1624
|
+
)
|
|
1625
|
+
.unwrap();
|
|
1626
|
+
seed_healthy_coordinator(&workspace);
|
|
1627
|
+
let transport = OfflineTransport::new();
|
|
1628
|
+
|
|
1629
|
+
let result = restart_with_transport(&workspace, false, Some("current"), &transport);
|
|
1630
|
+
|
|
1631
|
+
assert!(
|
|
1632
|
+
matches!(result, Ok(RestartReport::Restarted { .. })),
|
|
1633
|
+
"restart should succeed for a resumable project-root team: {result:?}"
|
|
1634
|
+
);
|
|
1635
|
+
assert_eq!(
|
|
1636
|
+
transport.spawn_cwd_records(),
|
|
1637
|
+
vec![workspace.clone()],
|
|
1638
|
+
"restart must spawn from run_workspace, not .team/runtime/<team>"
|
|
1639
|
+
);
|
|
1640
|
+
let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
1641
|
+
assert_eq!(
|
|
1642
|
+
state
|
|
1643
|
+
.get("agents")
|
|
1644
|
+
.and_then(|agents| agents.get("alpha"))
|
|
1645
|
+
.and_then(|agent| agent.get("spawn_cwd"))
|
|
1646
|
+
.and_then(serde_json::Value::as_str),
|
|
1647
|
+
Some(workspace.to_string_lossy().as_ref()),
|
|
1648
|
+
"restart must persist the actual spawn cwd for later diagnostics"
|
|
1649
|
+
);
|
|
1650
|
+
let rebuilt_spec =
|
|
1651
|
+
crate::model::yaml::loads(&std::fs::read_to_string(&spec_path).unwrap()).unwrap();
|
|
1652
|
+
assert_eq!(
|
|
1653
|
+
rebuilt_spec
|
|
1654
|
+
.get("team")
|
|
1655
|
+
.and_then(|team| team.get("workspace"))
|
|
1656
|
+
.and_then(crate::model::yaml::Value::as_str),
|
|
1657
|
+
Some(workspace.to_string_lossy().as_ref()),
|
|
1658
|
+
"restart spec rebuild must keep team.workspace at the project root"
|
|
1659
|
+
);
|
|
1660
|
+
for agent in rebuilt_spec
|
|
1661
|
+
.get("agents")
|
|
1662
|
+
.and_then(crate::model::yaml::Value::as_list)
|
|
1663
|
+
.unwrap()
|
|
1664
|
+
{
|
|
1665
|
+
assert_eq!(
|
|
1666
|
+
agent
|
|
1667
|
+
.get("working_directory")
|
|
1668
|
+
.and_then(crate::model::yaml::Value::as_str),
|
|
1669
|
+
Some(workspace.to_string_lossy().as_ref()),
|
|
1670
|
+
"restart spec rebuild must keep agent working_directory at the project root"
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1536
1675
|
#[test]
|
|
1537
1676
|
fn restart_allow_fresh_does_not_force_fresh_other_agents_when_one_session_capture_times_out() {
|
|
1538
1677
|
let ws = temp_ws().join("restartatomic");
|
|
@@ -18,6 +18,7 @@ pub struct SpawnRecord {
|
|
|
18
18
|
pub session: SessionName,
|
|
19
19
|
pub window: WindowName,
|
|
20
20
|
pub argv: Vec<String>,
|
|
21
|
+
pub cwd: std::path::PathBuf,
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
#[derive(Debug, Clone)]
|
|
@@ -200,6 +201,10 @@ impl OfflineTransport {
|
|
|
200
201
|
})
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
pub fn spawn_cwd_records(&self) -> Vec<std::path::PathBuf> {
|
|
205
|
+
self.with_state(|state| state.spawns.iter().map(|record| record.cwd.clone()).collect())
|
|
206
|
+
}
|
|
207
|
+
|
|
203
208
|
pub fn pane_title_records(&self) -> Vec<(String, String, String, String)> {
|
|
204
209
|
self.with_state(|state| state.pane_titles.clone())
|
|
205
210
|
}
|
|
@@ -238,6 +243,7 @@ impl OfflineTransport {
|
|
|
238
243
|
session: &SessionName,
|
|
239
244
|
window: &WindowName,
|
|
240
245
|
argv: &[String],
|
|
246
|
+
cwd: &Path,
|
|
241
247
|
) -> Result<SpawnResult, TransportError> {
|
|
242
248
|
let pane_index = self.with_state(|state| {
|
|
243
249
|
state.calls.push(kind);
|
|
@@ -246,6 +252,7 @@ impl OfflineTransport {
|
|
|
246
252
|
session: session.clone(),
|
|
247
253
|
window: window.clone(),
|
|
248
254
|
argv: argv.to_vec(),
|
|
255
|
+
cwd: cwd.to_path_buf(),
|
|
249
256
|
});
|
|
250
257
|
if let Some(error) = state.spawn_failures.get(window.as_str()) {
|
|
251
258
|
return Err(TransportError::Spawn {
|
|
@@ -296,10 +303,10 @@ impl Transport for OfflineTransport {
|
|
|
296
303
|
session: &SessionName,
|
|
297
304
|
window: &WindowName,
|
|
298
305
|
argv: &[String],
|
|
299
|
-
|
|
306
|
+
cwd: &Path,
|
|
300
307
|
_env: &BTreeMap<String, String>,
|
|
301
308
|
) -> Result<SpawnResult, TransportError> {
|
|
302
|
-
self.spawn_result("spawn_first", session, window, argv)
|
|
309
|
+
self.spawn_result("spawn_first", session, window, argv, cwd)
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
fn spawn_into(
|
|
@@ -307,10 +314,10 @@ impl Transport for OfflineTransport {
|
|
|
307
314
|
session: &SessionName,
|
|
308
315
|
window: &WindowName,
|
|
309
316
|
argv: &[String],
|
|
310
|
-
|
|
317
|
+
cwd: &Path,
|
|
311
318
|
_env: &BTreeMap<String, String>,
|
|
312
319
|
) -> Result<SpawnResult, TransportError> {
|
|
313
|
-
self.spawn_result("spawn_into", session, window, argv)
|
|
320
|
+
self.spawn_result("spawn_into", session, window, argv, cwd)
|
|
314
321
|
}
|
|
315
322
|
|
|
316
323
|
fn spawn_split_with_env_unset(
|
|
@@ -318,11 +325,11 @@ impl Transport for OfflineTransport {
|
|
|
318
325
|
session: &SessionName,
|
|
319
326
|
window: &WindowName,
|
|
320
327
|
argv: &[String],
|
|
321
|
-
|
|
328
|
+
cwd: &Path,
|
|
322
329
|
_env: &BTreeMap<String, String>,
|
|
323
330
|
_env_unset: &[String],
|
|
324
331
|
) -> Result<SpawnResult, TransportError> {
|
|
325
|
-
self.spawn_result("spawn_split", session, window, argv)
|
|
332
|
+
self.spawn_result("spawn_split", session, window, argv, cwd)
|
|
326
333
|
}
|
|
327
334
|
|
|
328
335
|
fn inject(
|
package/npm/install.mjs
CHANGED
|
@@ -149,9 +149,16 @@ function install(argv) {
|
|
|
149
149
|
|
|
150
150
|
const runtimeBinary = path.join(dest, "bin", "team-agent");
|
|
151
151
|
fs.mkdirSync(binDir, { recursive: true });
|
|
152
|
-
writeExecWrapper(path.join(binDir, "team-agent"), runtimeBinary, []);
|
|
152
|
+
writeExecWrapper(path.join(binDir, "team-agent"), runtimeBinary, [], { allowForeign: true });
|
|
153
153
|
writeExecWrapper(path.join(binDir, "team_orchestrator"), runtimeBinary, ["mcp-server"]);
|
|
154
154
|
writeExecWrapper(path.join(binDir, "team-agent-coordinator"), runtimeBinary, ["coordinator"]);
|
|
155
|
+
const shadowRepairs = repairPathShadowingTeamAgentCommands({
|
|
156
|
+
env: process.env,
|
|
157
|
+
home: os.homedir(),
|
|
158
|
+
binDir,
|
|
159
|
+
runtimeBinary,
|
|
160
|
+
log: (line) => console.log(line),
|
|
161
|
+
});
|
|
155
162
|
installSkills(runtimeBinary);
|
|
156
163
|
writeInstallManifest(runtimeRoot, {
|
|
157
164
|
version,
|
|
@@ -160,6 +167,7 @@ function install(argv) {
|
|
|
160
167
|
runtimeBinary,
|
|
161
168
|
installedAt: new Date().toISOString(),
|
|
162
169
|
installTargetKind: installTarget.kind,
|
|
170
|
+
pathShadowRepairs: shadowRepairs.map((repair) => repair.file),
|
|
163
171
|
});
|
|
164
172
|
|
|
165
173
|
const teamAgent = path.join(binDir, "team-agent");
|
|
@@ -209,6 +217,11 @@ function install(argv) {
|
|
|
209
217
|
} finally {
|
|
210
218
|
fs.rmSync(doctorWorkspace, { recursive: true, force: true });
|
|
211
219
|
}
|
|
220
|
+
const pathCheck = verifyInstalledTeamAgentOnPath({
|
|
221
|
+
env: process.env,
|
|
222
|
+
expectedVersion: version,
|
|
223
|
+
});
|
|
224
|
+
console.log(`post-install: ${pathCheck.entry} --version = ${pathCheck.version}`);
|
|
212
225
|
}
|
|
213
226
|
|
|
214
227
|
function runDoctor(argv) {
|
|
@@ -309,6 +322,162 @@ export function resolveInstallBinDir(options = {}) {
|
|
|
309
322
|
return { binDir, kind: "shell_rc", readyNow: false, rc };
|
|
310
323
|
}
|
|
311
324
|
|
|
325
|
+
export function repairPathShadowingTeamAgentCommands(options = {}) {
|
|
326
|
+
const env = options.env || process.env;
|
|
327
|
+
const home = options.home || os.homedir();
|
|
328
|
+
const binDir = path.resolve(expandHomeFor(options.binDir || "", home));
|
|
329
|
+
const runtimeBinary = options.runtimeBinary;
|
|
330
|
+
const log = typeof options.log === "function" ? options.log : null;
|
|
331
|
+
if (!runtimeBinary) {
|
|
332
|
+
throw new Error("runtimeBinary is required");
|
|
333
|
+
}
|
|
334
|
+
const installedWrapper = path.join(binDir, "team-agent");
|
|
335
|
+
const repairs = [];
|
|
336
|
+
const candidates = pathShadowRepairCandidates(env.PATH || "", home, binDir);
|
|
337
|
+
log?.(`path-shadow: scanning ${candidates.length} candidate bin dirs before ${binDir}`);
|
|
338
|
+
for (const candidateDir of candidates) {
|
|
339
|
+
const entry = candidateDir.dir;
|
|
340
|
+
const candidate = path.join(entry, "team-agent");
|
|
341
|
+
if (isVersionManagedPath(entry)) {
|
|
342
|
+
log?.(`path-shadow: skip ${candidate} source=${candidateDir.source} reason=version-managed-path`);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!fs.existsSync(candidate)) {
|
|
346
|
+
if (candidateDir.source === "home-local-bin") {
|
|
347
|
+
log?.(`path-shadow: checked ${candidate} source=${candidateDir.source} reason=not-found`);
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
log?.(`path-shadow: found ${candidate} source=${candidateDir.source}`);
|
|
352
|
+
if (!isExecutableFile(candidate)) {
|
|
353
|
+
log?.(`path-shadow: skip ${candidate} source=${candidateDir.source} reason=not-executable-file`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (sameFile(candidate, installedWrapper)) {
|
|
357
|
+
log?.(`path-shadow: skip ${candidate} source=${candidateDir.source} reason=installed-wrapper`);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (sameFile(candidate, runtimeBinary)) {
|
|
361
|
+
log?.(`path-shadow: skip ${candidate} source=${candidateDir.source} reason=runtime-binary`);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
writeExecWrapper(candidate, runtimeBinary, [], { allowForeign: true });
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
368
|
+
throw new Error(`failed to update PATH-shadowing team-agent at ${candidate}: ${detail}`);
|
|
369
|
+
}
|
|
370
|
+
log?.(`path-shadow: updated ${candidate} source=${candidateDir.source} to runtime shim`);
|
|
371
|
+
repairs.push({ file: candidate, binDir: entry, source: candidateDir.source });
|
|
372
|
+
}
|
|
373
|
+
if (repairs.length === 0) {
|
|
374
|
+
log?.("path-shadow: no stale team-agent command repaired");
|
|
375
|
+
}
|
|
376
|
+
return repairs;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function verifyInstalledTeamAgentOnPath(options = {}) {
|
|
380
|
+
const env = options.env || process.env;
|
|
381
|
+
const expectedVersion = options.expectedVersion;
|
|
382
|
+
if (!expectedVersion) {
|
|
383
|
+
throw new Error("expectedVersion is required");
|
|
384
|
+
}
|
|
385
|
+
const which = spawnSync("which", ["team-agent"], {
|
|
386
|
+
text: true,
|
|
387
|
+
encoding: "utf8",
|
|
388
|
+
env,
|
|
389
|
+
timeout: VERSION_SMOKE_TIMEOUT_MS,
|
|
390
|
+
});
|
|
391
|
+
const entry = (which.stdout || "").split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
392
|
+
if (which.status !== 0 || !entry) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`post-install version check failed: \`which team-agent\` did not find the installed command. ` +
|
|
395
|
+
`Expected version ${expectedVersion}. Add the install bin directory to PATH or restart your shell.`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
const versionProbe = spawnSync(entry, ["--version"], {
|
|
399
|
+
text: true,
|
|
400
|
+
encoding: "utf8",
|
|
401
|
+
env,
|
|
402
|
+
timeout: VERSION_SMOKE_TIMEOUT_MS,
|
|
403
|
+
});
|
|
404
|
+
const versionOutput = `${versionProbe.stdout || ""}\n${versionProbe.stderr || ""}`;
|
|
405
|
+
if (versionProbe.status !== 0) {
|
|
406
|
+
const log = versionOutput.trim() || "no stderr/stdout";
|
|
407
|
+
throw new Error(
|
|
408
|
+
`post-install version check failed: PATH resolves team-agent to ${entry}, but \`${entry} --version\` failed ` +
|
|
409
|
+
`(status=${versionProbe.status ?? "signal"}). Expected version ${expectedVersion}. ` +
|
|
410
|
+
`A stale binary may be shadowing the installed shim. LOG: ${log}`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const actualVersion = parseTeamAgentVersion(versionOutput);
|
|
414
|
+
if (actualVersion !== expectedVersion) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`post-install version check failed: PATH resolves team-agent to ${entry}, version ${actualVersion || "unknown"} ` +
|
|
417
|
+
`but installer installed ${expectedVersion}. Remove or update the earlier PATH entry that shadows Team Agent.`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
return { entry, version: actualVersion };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function parseTeamAgentVersion(output) {
|
|
424
|
+
const text = String(output || "").trim();
|
|
425
|
+
const prefixed = text.match(/^team-agent\s+([^\s]+)$/m);
|
|
426
|
+
if (prefixed) {
|
|
427
|
+
return prefixed[1];
|
|
428
|
+
}
|
|
429
|
+
const bare = text.match(/^([0-9]+\.[0-9]+\.[0-9]+(?:[-+][^\s]+)?)$/m);
|
|
430
|
+
return bare ? bare[1] : null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function shadowingPathEntries(searchPath, home, binDir) {
|
|
434
|
+
const entries = uniquePathEntries(searchPath, home);
|
|
435
|
+
const installedIndex = entries.findIndex((entry) => path.resolve(entry) === path.resolve(binDir));
|
|
436
|
+
if (installedIndex === -1) {
|
|
437
|
+
return entries;
|
|
438
|
+
}
|
|
439
|
+
return entries.slice(0, installedIndex);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function pathShadowRepairCandidates(searchPath, home, binDir) {
|
|
443
|
+
const candidates = [];
|
|
444
|
+
const seen = new Set();
|
|
445
|
+
const add = (dir, source) => {
|
|
446
|
+
const resolved = path.resolve(expandHomeFor(dir, home));
|
|
447
|
+
if (seen.has(resolved)) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
seen.add(resolved);
|
|
451
|
+
candidates.push({ dir: resolved, source });
|
|
452
|
+
};
|
|
453
|
+
for (const entry of shadowingPathEntries(searchPath, home, binDir)) {
|
|
454
|
+
add(entry, "path-before-install");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const homeLocalBin = path.join(home, ".local", "bin");
|
|
458
|
+
if (!sameFile(homeLocalBin, binDir)) {
|
|
459
|
+
add(homeLocalBin, "home-local-bin");
|
|
460
|
+
}
|
|
461
|
+
return candidates;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function isExecutableFile(file) {
|
|
465
|
+
try {
|
|
466
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
467
|
+
return fs.statSync(file).isFile();
|
|
468
|
+
} catch {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function sameFile(left, right) {
|
|
474
|
+
try {
|
|
475
|
+
return fs.realpathSync(left) === fs.realpathSync(right);
|
|
476
|
+
} catch {
|
|
477
|
+
return path.resolve(left) === path.resolve(right);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
312
481
|
function uniquePathEntries(searchPath, home) {
|
|
313
482
|
const seen = new Set();
|
|
314
483
|
const entries = [];
|
|
@@ -499,8 +668,8 @@ function copyExecutable(src, dest) {
|
|
|
499
668
|
fs.chmodSync(dest, 0o755);
|
|
500
669
|
}
|
|
501
670
|
|
|
502
|
-
function writeExecWrapper(file, binary, fixedArgs) {
|
|
503
|
-
if (fs.existsSync(file) && !isInstallerManagedWrapper(file)) {
|
|
671
|
+
function writeExecWrapper(file, binary, fixedArgs, options = {}) {
|
|
672
|
+
if (fs.existsSync(file) && !options.allowForeign && !isInstallerManagedWrapper(file)) {
|
|
504
673
|
throw new Error(`refusing to overwrite non-Team Agent installer wrapper: ${file}`);
|
|
505
674
|
}
|
|
506
675
|
const args = fixedArgs.map(shellQuote).join(" ");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.39",
|
|
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.39",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.39",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.39"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|