@team-agent/installer 0.3.7 → 0.3.8

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.
@@ -18,6 +18,7 @@
18
18
  use std::collections::BTreeMap;
19
19
  use std::hash::{Hash, Hasher};
20
20
  use std::io::{Read, Write};
21
+ use std::os::unix::fs::FileTypeExt;
21
22
  use std::path::{Path, PathBuf};
22
23
  use std::process::Stdio;
23
24
  use std::time::{Duration, Instant};
@@ -331,6 +332,20 @@ pub(crate) fn socket_name_for_workspace(workspace: &Path) -> String {
331
332
  }
332
333
 
333
334
  pub(crate) fn socket_path_for_workspace(workspace: &Path) -> Option<PathBuf> {
335
+ if let Some(existing) = existing_socket_path_for_workspace(workspace) {
336
+ return Some(existing);
337
+ }
338
+ let uid = unsafe { libc::geteuid() };
339
+ let default_root = PathBuf::from(format!("/tmp/tmux-{uid}"));
340
+ let default_root = default_root.canonicalize().unwrap_or(default_root);
341
+ Some(default_root.join(socket_name_for_workspace(workspace)))
342
+ }
343
+
344
+ pub(crate) fn socket_probe_missing_for_workspace(workspace: &Path) -> bool {
345
+ existing_socket_path_for_workspace(workspace).is_none()
346
+ }
347
+
348
+ fn existing_socket_path_for_workspace(workspace: &Path) -> Option<PathBuf> {
334
349
  let socket_name = socket_name_for_workspace(workspace);
335
350
  let roots = tmux_socket_roots();
336
351
  for root in &roots {
@@ -340,12 +355,19 @@ pub(crate) fn socket_path_for_workspace(workspace: &Path) -> Option<PathBuf> {
340
355
  return Some(candidate.canonicalize().unwrap_or(candidate));
341
356
  }
342
357
  }
343
- let uid = unsafe { libc::geteuid() };
344
- let default_root = PathBuf::from(format!("/tmp/tmux-{uid}"));
345
- let default_root = default_root
346
- .canonicalize()
347
- .unwrap_or(default_root);
348
- Some(default_root.join(socket_name))
358
+ None
359
+ }
360
+
361
+ pub(crate) fn socket_missing_hint_for_workspace(workspace: &Path) -> String {
362
+ let socket_name = socket_name_for_workspace(workspace);
363
+ let roots = tmux_socket_roots()
364
+ .into_iter()
365
+ .map(|root| root.display().to_string())
366
+ .collect::<Vec<_>>()
367
+ .join(", ");
368
+ format!(
369
+ "tmux socket {socket_name} not found under [{roots}]; run `team-agent attach-leader` or restart the team before attaching"
370
+ )
349
371
  }
350
372
 
351
373
  pub(crate) fn attach_command_for_workspace(
@@ -373,7 +395,7 @@ pub(crate) fn attach_commands_for_windows<'a>(
373
395
  .collect()
374
396
  }
375
397
 
376
- fn tmux_socket_roots() -> Vec<PathBuf> {
398
+ pub(crate) fn tmux_socket_roots() -> Vec<PathBuf> {
377
399
  let uid = unsafe { libc::geteuid() };
378
400
  let mut roots = vec![PathBuf::from(format!("/tmp/tmux-{uid}"))];
379
401
  if let Some(tmpdir) = std::env::var_os("TMPDIR") {
@@ -384,6 +406,29 @@ fn tmux_socket_roots() -> Vec<PathBuf> {
384
406
  roots
385
407
  }
386
408
 
409
+ pub(crate) fn tmux_socket_endpoints() -> Vec<String> {
410
+ let mut endpoints = Vec::new();
411
+ for root in tmux_socket_roots() {
412
+ let Ok(entries) = std::fs::read_dir(root) else {
413
+ continue;
414
+ };
415
+ for entry in entries.flatten() {
416
+ let Ok(file_type) = entry.file_type() else {
417
+ continue;
418
+ };
419
+ if !file_type.is_socket() {
420
+ continue;
421
+ }
422
+ let path = entry.path();
423
+ let path = path.canonicalize().unwrap_or(path);
424
+ endpoints.push(path.to_string_lossy().to_string());
425
+ }
426
+ }
427
+ endpoints.sort();
428
+ endpoints.dedup();
429
+ endpoints
430
+ }
431
+
387
432
  pub(crate) fn socket_name_from_tmux_env() -> Option<String> {
388
433
  let tmux = std::env::var("TMUX")
389
434
  .ok()
@@ -673,6 +718,10 @@ impl Transport for TmuxBackend {
673
718
  BackendKind::Tmux
674
719
  }
675
720
 
721
+ fn probes_real_tmux_socket_roots(&self) -> bool {
722
+ true
723
+ }
724
+
676
725
  fn spawn_first(
677
726
  &self,
678
727
  session: &SessionName,
@@ -859,6 +908,40 @@ impl Transport for TmuxBackend {
859
908
  }
860
909
  }
861
910
 
911
+ fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
912
+ let argv = self.tmux_argv(&[
913
+ "tmux".to_string(),
914
+ "display-message".to_string(),
915
+ "-p".to_string(),
916
+ "-t".to_string(),
917
+ pane.as_str().to_string(),
918
+ "#{pane_id}".to_string(),
919
+ ]);
920
+ let output = self.runner.run(&argv)?;
921
+ if output.success {
922
+ let pane_id = output.stdout.trim();
923
+ if pane_id.is_empty() {
924
+ return Ok(Some(false));
925
+ }
926
+ if pane_id == pane.as_str()
927
+ && pane_id.starts_with('%')
928
+ && pane_id[1..].chars().all(|ch| ch.is_ascii_digit())
929
+ {
930
+ return Ok(Some(true));
931
+ }
932
+ return Ok(None);
933
+ }
934
+ let stderr = output.stderr.to_ascii_lowercase();
935
+ if stderr.contains("can't find pane")
936
+ || stderr.contains("no such pane")
937
+ || (stderr.contains("can't find") && stderr.contains("pane"))
938
+ {
939
+ Ok(Some(false))
940
+ } else {
941
+ Ok(None)
942
+ }
943
+ }
944
+
862
945
  fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
863
946
  // P5 (C-P5-3): `#{pane_pid}` rides the single list-panes call (field index 11),
864
947
  // killing the per-pane display-message N+1 fallback.
@@ -18,18 +18,39 @@ pub struct SpawnRecord {
18
18
  pub argv: Vec<String>,
19
19
  }
20
20
 
21
- #[derive(Debug, Clone, Default)]
21
+ #[derive(Debug, Clone)]
22
22
  struct OfflineState {
23
23
  session_present: bool,
24
24
  session_absent_after_spawn_first: bool,
25
25
  targets: Vec<PaneInfo>,
26
26
  windows: Vec<WindowName>,
27
+ pane_presence: BTreeMap<String, bool>,
28
+ liveness: BTreeMap<String, PaneLiveness>,
29
+ default_liveness: PaneLiveness,
27
30
  calls: Vec<&'static str>,
28
31
  spawns: Vec<SpawnRecord>,
29
32
  inject_targets: Vec<Target>,
30
33
  inject_payloads: Vec<String>,
31
34
  }
32
35
 
36
+ impl Default for OfflineState {
37
+ fn default() -> Self {
38
+ Self {
39
+ session_present: false,
40
+ session_absent_after_spawn_first: false,
41
+ targets: Vec::new(),
42
+ windows: Vec::new(),
43
+ pane_presence: BTreeMap::new(),
44
+ liveness: BTreeMap::new(),
45
+ default_liveness: PaneLiveness::Unknown,
46
+ calls: Vec::new(),
47
+ spawns: Vec::new(),
48
+ inject_targets: Vec::new(),
49
+ inject_payloads: Vec::new(),
50
+ }
51
+ }
52
+ }
53
+
33
54
  #[derive(Debug, Clone, Default)]
34
55
  pub struct OfflineTransport {
35
56
  inner: Arc<Mutex<OfflineState>>,
@@ -60,6 +81,25 @@ impl OfflineTransport {
60
81
  self
61
82
  }
62
83
 
84
+ pub fn with_default_liveness(self, liveness: PaneLiveness) -> Self {
85
+ self.with_state(|state| state.default_liveness = liveness);
86
+ self
87
+ }
88
+
89
+ pub fn with_liveness(self, pane: impl Into<String>, liveness: PaneLiveness) -> Self {
90
+ self.with_state(|state| {
91
+ state.liveness.insert(pane.into(), liveness);
92
+ });
93
+ self
94
+ }
95
+
96
+ pub fn with_pane_presence(self, pane: impl Into<String>, present: bool) -> Self {
97
+ self.with_state(|state| {
98
+ state.pane_presence.insert(pane.into(), present);
99
+ });
100
+ self
101
+ }
102
+
63
103
  pub fn calls(&self) -> Vec<&'static str> {
64
104
  self.with_state(|state| state.calls.clone())
65
105
  }
@@ -198,9 +238,22 @@ impl Transport for OfflineTransport {
198
238
  Ok(None)
199
239
  }
200
240
 
201
- fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
202
- self.record("liveness");
203
- Ok(PaneLiveness::Unknown)
241
+ fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
242
+ Ok(self.with_state(|state| {
243
+ state.calls.push("liveness");
244
+ state
245
+ .liveness
246
+ .get(pane.as_str())
247
+ .copied()
248
+ .unwrap_or(state.default_liveness)
249
+ }))
250
+ }
251
+
252
+ fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
253
+ Ok(self.with_state(|state| {
254
+ state.calls.push("has_pane");
255
+ state.pane_presence.get(pane.as_str()).copied()
256
+ }))
204
257
  }
205
258
 
206
259
  fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
@@ -399,6 +399,12 @@ pub trait Transport: Send + Sync {
399
399
  /// 后端种类(诊断/事件用)。
400
400
  fn kind(&self) -> BackendKind;
401
401
 
402
+ /// Only the concrete tmux backend should scan real tmux socket roots.
403
+ /// Test doubles stay hermetic and use their injected probe results.
404
+ fn probes_real_tmux_socket_roots(&self) -> bool {
405
+ false
406
+ }
407
+
402
408
  // —— SPAWN(ST):所有后端天然满足;cwd/env 是 spawn 参数,无独立动词(§gap-setenv)——
403
409
 
404
410
  /// tmux=`new-session -d` / wezterm=`spawn --new-window` / conpty=`openpty`+spawn。
@@ -481,6 +487,13 @@ pub trait Transport: Send + Sync {
481
487
  /// pane 存活三态(`PaneLiveness`,bug-085 穷尽 match;unknown ≠ dead ≠ live)。
482
488
  fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError>;
483
489
 
490
+ /// Cheap direct pane existence check when a backend can prove it. `Ok(None)`
491
+ /// preserves the existing Unknown boundary.
492
+ fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
493
+ let _ = pane;
494
+ Ok(None)
495
+ }
496
+
484
497
  // —— ENUMERATE / IDENTITY(SL + 进程探测):身份/rebind 地基 ——
485
498
 
486
499
  /// 全局枚举所有 pane + 每 pane 的 leader_env。tmux=`list-panes -a` + 读进程 env;
package/npm/install.mjs CHANGED
@@ -80,13 +80,13 @@ function install(argv) {
80
80
  writeExecWrapper(path.join(binDir, "team-agent"), runtimeBinary, []);
81
81
  writeExecWrapper(path.join(binDir, "team_orchestrator"), runtimeBinary, ["mcp-server"]);
82
82
  writeExecWrapper(path.join(binDir, "team-agent-coordinator"), runtimeBinary, ["coordinator"]);
83
- installSkills();
83
+ installSkills(runtimeBinary);
84
84
 
85
85
  const teamAgent = path.join(binDir, "team-agent");
86
86
  console.log(`installed: ${teamAgent}`);
87
87
  console.log(`runtime: ${dest}`);
88
88
  console.log(`binary: ${platformBinary.packageName}`);
89
- console.log("skill: installed for Codex and Claude");
89
+ console.log("skill: installed for Codex, Claude and Copilot");
90
90
  console.log(`PATH: ensure ${binDir} is on PATH`);
91
91
 
92
92
  // 0.3.6 hotfix · C-5 cr verdict — post-install binary smoke 门(走 `--help`
@@ -149,14 +149,24 @@ function runDoctor(argv) {
149
149
  function uninstall(argv) {
150
150
  const opts = parseOptions(argv);
151
151
  const prefix = path.resolve(expandHome(opts.prefix || path.join(os.homedir(), ".local")));
152
+ // 卸载 skill 走二进制单源(同一 SkillTarget 表 codex/claude/copilot),在删 wrapper 前调
153
+ // (删 wrapper 后 PATH 上的 team-agent 没了,但 runtime 二进制仍在;用 runtime 二进制直调)。
154
+ const teamAgentBin = path.join(prefix, "bin", "team-agent");
155
+ if (fs.existsSync(teamAgentBin)) {
156
+ const res = spawnSync(teamAgentBin, ["install-skill", "--target", "all", "--uninstall", "--json"], {
157
+ text: true,
158
+ encoding: "utf8",
159
+ timeout: VERSION_SMOKE_TIMEOUT_MS,
160
+ });
161
+ if (res.status !== 0) {
162
+ console.error(`WARN: skill uninstall via binary failed (status=${res.status ?? "signal"}); skill dirs may remain under ~/.codex|.claude|.copilot/skills/team-agent`);
163
+ }
164
+ }
152
165
  for (const name of ["team-agent", "team_orchestrator", "team-agent-coordinator"]) {
153
166
  fs.rmSync(path.join(prefix, "bin", name), { force: true });
154
167
  }
155
- for (const skillDir of skillDestinations()) {
156
- fs.rmSync(skillDir, { recursive: true, force: true });
157
- }
158
168
  console.log(`removed wrappers from ${path.join(prefix, "bin")}`);
159
- console.log("removed skills from ~/.codex/skills/team-agent and ~/.claude/skills/team-agent");
169
+ console.log("removed skills from ~/.codex, ~/.claude and ~/.copilot skills/team-agent");
160
170
  if (opts.purgeRuntime) {
161
171
  const runtimeRoot = path.resolve(expandHome(opts.runtimeDir || path.join(os.homedir(), ".team-agent", "runtime")));
162
172
  fs.rmSync(runtimeRoot, { recursive: true, force: true });
@@ -238,46 +248,32 @@ exec ${shellQuote(binary)} ${argPrefix}"$@"
238
248
  fs.chmodSync(file, 0o755);
239
249
  }
240
250
 
241
- function installSkills() {
251
+ // RED-1 根治(单源):skill 安装唯一实现在二进制 `install-skill`(SkillTarget 表:
252
+ // codex/claude/copilot)。install.mjs 不再有自己的 JS 拷贝逻辑/目标硬编码——改调二进制,
253
+ // 失败显式报错(非零退出),绝不静默回退 JS。
254
+ function installSkills(runtimeBinary) {
242
255
  const source = path.join(packageRoot, "skills", "team-agent");
243
256
  if (!fs.existsSync(source)) {
244
257
  throw new Error(`skill source not found: ${source}`);
245
258
  }
246
- for (const dest of skillDestinations()) {
247
- fs.rmSync(dest, { recursive: true, force: true });
248
- copyTree(source, dest);
259
+ const res = spawnSync(runtimeBinary, ["install-skill", "--target", "all", "--source", source, "--json"], {
260
+ text: true,
261
+ encoding: "utf8",
262
+ timeout: VERSION_SMOKE_TIMEOUT_MS,
263
+ });
264
+ if (res.status !== 0) {
265
+ const log = (res.stderr || res.stdout || "").trim() || "no stderr/stdout";
266
+ console.error(`ERROR: skill install failed (status=${res.status ?? "signal"})`);
267
+ console.error(`ACTION: reinstall, or run \`team-agent install-skill --target all --source ${source}\` manually`);
268
+ console.error(`LOG: ${runtimeBinary} install-skill --target all => ${log}`);
269
+ process.exit(1);
249
270
  }
250
271
  }
251
272
 
252
- function skillDestinations() {
253
- return [
254
- path.join(os.homedir(), ".codex", "skills", "team-agent"),
255
- path.join(os.homedir(), ".claude", "skills", "team-agent"),
256
- ];
257
- }
258
-
259
273
  function makeDoctorWorkspace() {
260
274
  return fs.mkdtempSync(path.join(os.tmpdir(), "team-agent-doctor-"));
261
275
  }
262
276
 
263
- function copyTree(src, dest) {
264
- const stat = fs.lstatSync(src);
265
- if (stat.isDirectory()) {
266
- fs.mkdirSync(dest, { recursive: true, mode: stat.mode });
267
- for (const entry of fs.readdirSync(src)) {
268
- if (entry === ".DS_Store") {
269
- continue;
270
- }
271
- copyTree(path.join(src, entry), path.join(dest, entry));
272
- }
273
- return;
274
- }
275
- if (stat.isFile()) {
276
- fs.copyFileSync(src, dest);
277
- fs.chmodSync(dest, stat.mode);
278
- }
279
- }
280
-
281
277
  function expandHome(value) {
282
278
  if (value === "~") {
283
279
  return os.homedir();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
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.7",
24
- "@team-agent/cli-darwin-x64": "0.3.7",
25
- "@team-agent/cli-linux-x64": "0.3.7"
23
+ "@team-agent/cli-darwin-arm64": "0.3.8",
24
+ "@team-agent/cli-darwin-x64": "0.3.8",
25
+ "@team-agent/cli-linux-x64": "0.3.8"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",