@team-agent/installer 0.3.37 → 0.3.38

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 CHANGED
@@ -566,7 +566,7 @@ dependencies = [
566
566
 
567
567
  [[package]]
568
568
  name = "team-agent"
569
- version = "0.3.37"
569
+ version = "0.3.38"
570
570
  dependencies = [
571
571
  "anyhow",
572
572
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent"]
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.3.37"
12
+ version = "0.3.38"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -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
- Some(spec_workspace),
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
- _cwd: &Path,
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
- _cwd: &Path,
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
- _cwd: &Path,
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,15 @@ 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
+ });
155
161
  installSkills(runtimeBinary);
156
162
  writeInstallManifest(runtimeRoot, {
157
163
  version,
@@ -160,6 +166,7 @@ function install(argv) {
160
166
  runtimeBinary,
161
167
  installedAt: new Date().toISOString(),
162
168
  installTargetKind: installTarget.kind,
169
+ pathShadowRepairs: shadowRepairs.map((repair) => repair.file),
163
170
  });
164
171
 
165
172
  const teamAgent = path.join(binDir, "team-agent");
@@ -176,6 +183,9 @@ function install(argv) {
176
183
  console.log(`runtime: ${dest}`);
177
184
  console.log(`binary: ${platformBinary.packageName}`);
178
185
  console.log("skill: installed for Codex, Claude and Copilot");
186
+ for (const repair of shadowRepairs) {
187
+ console.log(`path-shadow: updated ${repair.file} to runtime shim`);
188
+ }
179
189
 
180
190
  // 0.3.6 hotfix · C-5 cr verdict — post-install binary smoke 门(走 `--help`
181
191
  // 子命令,因为 0.3.x CLI 现阶段没有 --version)。真跑一次 binary 才能抓住
@@ -209,6 +219,11 @@ function install(argv) {
209
219
  } finally {
210
220
  fs.rmSync(doctorWorkspace, { recursive: true, force: true });
211
221
  }
222
+ const pathCheck = verifyInstalledTeamAgentOnPath({
223
+ env: process.env,
224
+ expectedVersion: version,
225
+ });
226
+ console.log(`post-install: ${pathCheck.entry} --version = ${pathCheck.version}`);
212
227
  }
213
228
 
214
229
  function runDoctor(argv) {
@@ -309,6 +324,115 @@ export function resolveInstallBinDir(options = {}) {
309
324
  return { binDir, kind: "shell_rc", readyNow: false, rc };
310
325
  }
311
326
 
327
+ export function repairPathShadowingTeamAgentCommands(options = {}) {
328
+ const env = options.env || process.env;
329
+ const home = options.home || os.homedir();
330
+ const binDir = path.resolve(expandHomeFor(options.binDir || "", home));
331
+ const runtimeBinary = options.runtimeBinary;
332
+ if (!runtimeBinary) {
333
+ throw new Error("runtimeBinary is required");
334
+ }
335
+ const installedWrapper = path.join(binDir, "team-agent");
336
+ const repairs = [];
337
+ for (const entry of shadowingPathEntries(env.PATH || "", home, binDir)) {
338
+ if (isVersionManagedPath(entry)) {
339
+ continue;
340
+ }
341
+ const candidate = path.join(entry, "team-agent");
342
+ if (!isExecutableFile(candidate) || sameFile(candidate, installedWrapper) || sameFile(candidate, runtimeBinary)) {
343
+ continue;
344
+ }
345
+ try {
346
+ writeExecWrapper(candidate, runtimeBinary, [], { allowForeign: true });
347
+ } catch (error) {
348
+ const detail = error instanceof Error ? error.message : String(error);
349
+ throw new Error(`failed to update PATH-shadowing team-agent at ${candidate}: ${detail}`);
350
+ }
351
+ repairs.push({ file: candidate, binDir: entry });
352
+ }
353
+ return repairs;
354
+ }
355
+
356
+ export function verifyInstalledTeamAgentOnPath(options = {}) {
357
+ const env = options.env || process.env;
358
+ const expectedVersion = options.expectedVersion;
359
+ if (!expectedVersion) {
360
+ throw new Error("expectedVersion is required");
361
+ }
362
+ const which = spawnSync("which", ["team-agent"], {
363
+ text: true,
364
+ encoding: "utf8",
365
+ env,
366
+ timeout: VERSION_SMOKE_TIMEOUT_MS,
367
+ });
368
+ const entry = (which.stdout || "").split(/\r?\n/).map((line) => line.trim()).find(Boolean);
369
+ if (which.status !== 0 || !entry) {
370
+ throw new Error(
371
+ `post-install version check failed: \`which team-agent\` did not find the installed command. ` +
372
+ `Expected version ${expectedVersion}. Add the install bin directory to PATH or restart your shell.`,
373
+ );
374
+ }
375
+ const versionProbe = spawnSync(entry, ["--version"], {
376
+ text: true,
377
+ encoding: "utf8",
378
+ env,
379
+ timeout: VERSION_SMOKE_TIMEOUT_MS,
380
+ });
381
+ const versionOutput = `${versionProbe.stdout || ""}\n${versionProbe.stderr || ""}`;
382
+ if (versionProbe.status !== 0) {
383
+ const log = versionOutput.trim() || "no stderr/stdout";
384
+ throw new Error(
385
+ `post-install version check failed: PATH resolves team-agent to ${entry}, but \`${entry} --version\` failed ` +
386
+ `(status=${versionProbe.status ?? "signal"}). Expected version ${expectedVersion}. ` +
387
+ `A stale binary may be shadowing the installed shim. LOG: ${log}`,
388
+ );
389
+ }
390
+ const actualVersion = parseTeamAgentVersion(versionOutput);
391
+ if (actualVersion !== expectedVersion) {
392
+ throw new Error(
393
+ `post-install version check failed: PATH resolves team-agent to ${entry}, version ${actualVersion || "unknown"} ` +
394
+ `but installer installed ${expectedVersion}. Remove or update the earlier PATH entry that shadows Team Agent.`,
395
+ );
396
+ }
397
+ return { entry, version: actualVersion };
398
+ }
399
+
400
+ export function parseTeamAgentVersion(output) {
401
+ const text = String(output || "").trim();
402
+ const prefixed = text.match(/^team-agent\s+([^\s]+)$/m);
403
+ if (prefixed) {
404
+ return prefixed[1];
405
+ }
406
+ const bare = text.match(/^([0-9]+\.[0-9]+\.[0-9]+(?:[-+][^\s]+)?)$/m);
407
+ return bare ? bare[1] : null;
408
+ }
409
+
410
+ function shadowingPathEntries(searchPath, home, binDir) {
411
+ const entries = uniquePathEntries(searchPath, home);
412
+ const installedIndex = entries.findIndex((entry) => path.resolve(entry) === path.resolve(binDir));
413
+ if (installedIndex === -1) {
414
+ return entries;
415
+ }
416
+ return entries.slice(0, installedIndex);
417
+ }
418
+
419
+ function isExecutableFile(file) {
420
+ try {
421
+ fs.accessSync(file, fs.constants.X_OK);
422
+ return fs.statSync(file).isFile();
423
+ } catch {
424
+ return false;
425
+ }
426
+ }
427
+
428
+ function sameFile(left, right) {
429
+ try {
430
+ return fs.realpathSync(left) === fs.realpathSync(right);
431
+ } catch {
432
+ return path.resolve(left) === path.resolve(right);
433
+ }
434
+ }
435
+
312
436
  function uniquePathEntries(searchPath, home) {
313
437
  const seen = new Set();
314
438
  const entries = [];
@@ -499,8 +623,8 @@ function copyExecutable(src, dest) {
499
623
  fs.chmodSync(dest, 0o755);
500
624
  }
501
625
 
502
- function writeExecWrapper(file, binary, fixedArgs) {
503
- if (fs.existsSync(file) && !isInstallerManagedWrapper(file)) {
626
+ function writeExecWrapper(file, binary, fixedArgs, options = {}) {
627
+ if (fs.existsSync(file) && !options.allowForeign && !isInstallerManagedWrapper(file)) {
504
628
  throw new Error(`refusing to overwrite non-Team Agent installer wrapper: ${file}`);
505
629
  }
506
630
  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.37",
3
+ "version": "0.3.38",
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.37",
24
- "@team-agent/cli-darwin-x64": "0.3.37",
25
- "@team-agent/cli-linux-x64": "0.3.37"
23
+ "@team-agent/cli-darwin-arm64": "0.3.38",
24
+ "@team-agent/cli-darwin-x64": "0.3.38",
25
+ "@team-agent/cli-linux-x64": "0.3.38"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",