@team-agent/installer 0.3.6 → 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.
Files changed (45) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +52 -7
  4. package/crates/team-agent/src/cli/diagnose.rs +9 -0
  5. package/crates/team-agent/src/cli/emit.rs +175 -0
  6. package/crates/team-agent/src/cli/mod.rs +455 -63
  7. package/crates/team-agent/src/cli/status_port.rs +62 -0
  8. package/crates/team-agent/src/cli/tests/base.rs +9 -4
  9. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +83 -1
  10. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  11. package/crates/team-agent/src/cli/tests/run_delegation.rs +10 -2
  12. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +86 -21
  13. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +76 -0
  14. package/crates/team-agent/src/cli/types.rs +3 -2
  15. package/crates/team-agent/src/compiler.rs +73 -50
  16. package/crates/team-agent/src/coordinator/tick.rs +108 -20
  17. package/crates/team-agent/src/db/migration.rs +17 -1
  18. package/crates/team-agent/src/leader/owner_bind.rs +59 -20
  19. package/crates/team-agent/src/lifecycle/launch.rs +378 -56
  20. package/crates/team-agent/src/lifecycle/restart/common.rs +4 -9
  21. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +91 -12
  22. package/crates/team-agent/src/lifecycle/restart/selection.rs +6 -4
  23. package/crates/team-agent/src/lifecycle/tests/core.rs +238 -3
  24. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +257 -7
  25. package/crates/team-agent/src/lifecycle/types.rs +2 -0
  26. package/crates/team-agent/src/mcp_server/normalize.rs +29 -7
  27. package/crates/team-agent/src/mcp_server/tests/golden.rs +7 -5
  28. package/crates/team-agent/src/mcp_server/tests/normalize.rs +5 -2
  29. package/crates/team-agent/src/mcp_server/tools.rs +25 -1
  30. package/crates/team-agent/src/mcp_server/wire.rs +11 -1
  31. package/crates/team-agent/src/model/paths.rs +7 -0
  32. package/crates/team-agent/src/model/spec.rs +23 -1
  33. package/crates/team-agent/src/packaging/install.rs +42 -4
  34. package/crates/team-agent/src/packaging/tests.rs +91 -14
  35. package/crates/team-agent/src/packaging/types.rs +13 -1
  36. package/crates/team-agent/src/provider/adapter.rs +381 -15
  37. package/crates/team-agent/src/state/identity.rs +29 -0
  38. package/crates/team-agent/src/state/selector.rs +48 -14
  39. package/crates/team-agent/src/tmux_backend/tests.rs +44 -0
  40. package/crates/team-agent/src/tmux_backend.rs +104 -9
  41. package/crates/team-agent/src/transport/test_support.rs +57 -4
  42. package/crates/team-agent/src/transport.rs +13 -0
  43. package/npm/install.mjs +31 -35
  44. package/package.json +4 -4
  45. package/skills/team-agent/SKILL.md +82 -5
package/Cargo.lock CHANGED
@@ -566,7 +566,7 @@ dependencies = [
566
566
 
567
567
  [[package]]
568
568
  name = "team-agent"
569
- version = "0.3.6"
569
+ version = "0.3.8"
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.6"
12
+ version = "0.3.8"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -133,13 +133,33 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
133
133
  }
134
134
  Ok(result)
135
135
  } else {
136
- Ok(CmdResult::human(
137
- value
138
- .get("summary")
139
- .and_then(Value::as_str)
140
- .unwrap_or("quick-start complete"),
141
- ))
136
+ // E13:happy 人类路径必须带 attach_commands(json 路径 cli/mod.rs:1775 已有)。
137
+ Ok(CmdResult::human(&quickstart_human(&value)))
138
+ }
139
+ }
140
+
141
+ /// E13:quick-start "team 起了" 人类输出 = summary + attach 块。所有成功出口共用(别每分支手拷)
142
+ /// attach_commands 缺/空 → 只 summary(向后兼容)。
143
+ fn quickstart_human(value: &Value) -> String {
144
+ let summary = value
145
+ .get("summary")
146
+ .and_then(Value::as_str)
147
+ .unwrap_or("quick-start complete");
148
+ let attach: Vec<&str> = value
149
+ .get("attach_commands")
150
+ .and_then(Value::as_array)
151
+ .map(|items| items.iter().filter_map(Value::as_str).collect())
152
+ .unwrap_or_default();
153
+ if attach.is_empty() {
154
+ return summary.to_string();
155
+ }
156
+ let mut out = String::from(summary);
157
+ out.push_str("\n\nattach:");
158
+ for cmd in attach {
159
+ out.push_str("\n ");
160
+ out.push_str(cmd);
142
161
  }
162
+ out
143
163
  }
144
164
 
145
165
  /// `cmd_compile`(`commands.py:42`)。
@@ -1522,9 +1542,34 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1522
1542
  mod tests {
1523
1543
  #![allow(clippy::unwrap_used)]
1524
1544
 
1525
- use super::agent_pane_id;
1545
+ use super::{agent_pane_id, quickstart_human};
1526
1546
  use serde_json::json;
1527
1547
 
1548
+ // E13:happy 人类输出必须带 attach 块(此前 else 分支只打 summary 丢 attach_commands)。
1549
+ #[test]
1550
+ fn e13_quickstart_human_includes_attach_commands() {
1551
+ let value = json!({
1552
+ "summary": "team started",
1553
+ "attach_commands": [
1554
+ "tmux -S /tmp/ta-x attach -t team-y:w1",
1555
+ "tmux -S /tmp/ta-x attach -t team-y:w2",
1556
+ ],
1557
+ });
1558
+ let out = quickstart_human(&value);
1559
+ assert!(out.contains("team started"), "must keep summary; got {out}");
1560
+ assert!(out.contains("attach:"), "must render attach block; got {out}");
1561
+ assert!(out.contains("team-y:w1") && out.contains("team-y:w2"), "must list each attach cmd; got {out}");
1562
+ }
1563
+
1564
+ #[test]
1565
+ fn e13_quickstart_human_summary_only_when_no_attach() {
1566
+ let value = json!({"summary": "quick-start complete"});
1567
+ assert_eq!(quickstart_human(&value), "quick-start complete");
1568
+ // 空数组也只 summary。
1569
+ let value2 = json!({"summary": "s", "attach_commands": []});
1570
+ assert_eq!(quickstart_human(&value2), "s");
1571
+ }
1572
+
1528
1573
  #[test]
1529
1574
  fn agent_pane_id_resolves_session_window_even_with_recorded_pane_id() {
1530
1575
  let state = json!({
@@ -208,11 +208,20 @@ pub(crate) fn build_profile_smoke_check_for_team(team: &std::path::Path) -> Resu
208
208
  let spec = match crate::compiler::compile_team(team) {
209
209
  Ok(spec) => spec,
210
210
  Err(error) => {
211
+ // SMOKE-1 (locate.md §"Smallest likely code touch" item 2):compile
212
+ // 失败时把 team_dir + next_action 带上,operator 才有可下手的诊断
213
+ // (不是只贴一行 reason)。
211
214
  return Ok(json!({
212
215
  "name": "profile_smoke",
213
216
  "ok": false,
214
217
  "status": "profile_invalid",
218
+ "team_dir": team.to_string_lossy().to_string(),
215
219
  "reason": error.to_string(),
220
+ "next_action": format!(
221
+ "fix the team spec at `{}` (see reason above) or re-run \
222
+ doctor with a different `<team-dir>`",
223
+ team.display()
224
+ ),
216
225
  "secret_values_printed": false,
217
226
  "checks": [],
218
227
  }));
@@ -108,6 +108,7 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
108
108
  "watch" => cmd_watch(&watch_args(args, cwd)).map(emit_result),
109
109
  "sessions" => cmd_sessions(&sessions_args(args, cwd)).map(emit_result),
110
110
  "validate" => cmd_validate(&validate_args(args, cwd)).map(emit_result),
111
+ "install-skill" => cmd_install_skill(&install_skill_args(args)?).map(emit_result),
111
112
  "profile" => cmd_profile(&profile_args(args, cwd)?).map(emit_result),
112
113
  "validate-result" if has_arg(args, "--result") => {
113
114
  eprintln!("team-agent: error: unrecognized arguments: --result");
@@ -160,6 +161,7 @@ const DISPATCH_COMMANDS: &[&str] = &[
160
161
  "watch",
161
162
  "sessions",
162
163
  "validate",
164
+ "install-skill",
163
165
  "profile",
164
166
  "validate-result",
165
167
  "collect",
@@ -230,6 +232,7 @@ fn command_help(command: Option<&str>) -> String {
230
232
  Some("watch") => "usage: team-agent watch [--workspace WORKSPACE] [--team TEAM]".to_string(),
231
233
  Some("sessions") => "usage: team-agent sessions [--workspace WORKSPACE] [--json]".to_string(),
232
234
  Some("validate") => "usage: team-agent validate [SPEC] [--json]".to_string(),
235
+ Some("install-skill") => "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]".to_string(),
233
236
  Some("profile") => "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--json]".to_string(),
234
237
  Some("validate-result") => "usage: team-agent validate-result [ENVELOPE] [--file FILE|--result JSON] [--json]".to_string(),
235
238
  Some("collect") => "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]".to_string(),
@@ -249,15 +252,164 @@ fn emit_unknown_subcommand_usage(command: &str) -> ExitCode {
249
252
  emit_usage_error(&format!(
250
253
  "argument {{codex,claude,...,doctor}}: invalid choice: '{command}' (choose from codex, claude, ..., doctor)"
251
254
  ));
255
+ // E8 (N38): 错路引导 —— 拼写近似时建议最接近的真子命令(additive,不改既有 golden 行)。
256
+ if let Some(suggestion) = nearest_subcommand(command) {
257
+ eprintln!("team-agent: did you mean `{suggestion}`?");
258
+ }
252
259
  ExitCode::Usage
253
260
  }
254
261
 
262
+ /// 在已知子命令里找与 `input` 最接近的一个(Levenshtein ≤ 阈值)。无足够接近者 → None。
263
+ fn nearest_subcommand(input: &str) -> Option<&'static str> {
264
+ let mut candidates: Vec<&'static str> = vec!["codex", "claude"];
265
+ candidates.extend_from_slice(DISPATCH_COMMANDS);
266
+ candidates.extend_from_slice(SPEC_ONLY_HELP_COMMANDS);
267
+ // 阈值随长度放宽,但短词收紧,避免 'x' 误配任何东西。
268
+ let max_distance = match input.chars().count() {
269
+ 0..=3 => 1,
270
+ 4..=6 => 2,
271
+ _ => 3,
272
+ };
273
+ candidates
274
+ .into_iter()
275
+ .map(|c| (c, levenshtein(input, c)))
276
+ .filter(|(_, d)| *d <= max_distance)
277
+ .min_by_key(|(_, d)| *d)
278
+ .map(|(c, _)| c)
279
+ }
280
+
281
+ /// 标准 Levenshtein 编辑距离(纯函数,无依赖;子命令建议用)。
282
+ fn levenshtein(a: &str, b: &str) -> usize {
283
+ let a: Vec<char> = a.chars().collect();
284
+ let b: Vec<char> = b.chars().collect();
285
+ let mut prev: Vec<usize> = (0..=b.len()).collect();
286
+ let mut curr = vec![0usize; b.len() + 1];
287
+ for (i, &ca) in a.iter().enumerate() {
288
+ curr[0] = i + 1;
289
+ for (j, &cb) in b.iter().enumerate() {
290
+ let cost = if ca == cb { 0 } else { 1 };
291
+ curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
292
+ }
293
+ std::mem::swap(&mut prev, &mut curr);
294
+ }
295
+ prev[b.len()]
296
+ }
297
+
255
298
  fn emit_usage_error(message: &str) {
256
299
  eprintln!("usage: team-agent [-h] {{codex,claude,...,doctor}} ...");
257
300
  eprintln!("team-agent: error: {message}");
258
301
  }
259
302
 
260
303
  /// `cmd_validate` delegates to runtime validate_file.
304
+ /// `install-skill` 参数(RED-1 根治:把 skill 安装单源收敛到二进制,install.mjs 调它)。
305
+ struct InstallSkillArgs {
306
+ target: crate::packaging::SkillTarget,
307
+ dest: Option<PathBuf>,
308
+ dry_run: bool,
309
+ /// `--uninstall`:删 target 的 skill 目标目录(单源,走同一 SkillTarget 表),不需 --source。
310
+ uninstall: bool,
311
+ source: Option<PathBuf>,
312
+ json: bool,
313
+ }
314
+
315
+ fn install_skill_args(args: &[String]) -> Result<InstallSkillArgs, CliError> {
316
+ let parsed = parse_args(args);
317
+ // `--target` 复用 parse_args.targets(codex|claude|copilot|all,默认 all)。
318
+ let target = match parsed.targets.as_deref() {
319
+ None | Some("all") => crate::packaging::SkillTarget::All,
320
+ Some("codex") => crate::packaging::SkillTarget::Codex,
321
+ Some("claude") => crate::packaging::SkillTarget::Claude,
322
+ Some("copilot") => crate::packaging::SkillTarget::Copilot,
323
+ Some(other) => {
324
+ return Err(CliError::Usage(format!(
325
+ "invalid --target: {other} (choose from codex, claude, copilot, all)"
326
+ )))
327
+ }
328
+ };
329
+ let uninstall = args.iter().any(|a| a == "--uninstall");
330
+ // `--source <dir>` 安装时必需(npm 包的 skills/team-agent;运行期无 CARGO_MANIFEST_DIR);
331
+ // 卸载不需要。
332
+ let source = flag_value(args, "--source").map(PathBuf::from);
333
+ if !uninstall && source.is_none() {
334
+ return Err(CliError::Usage("missing --source <skill dir>".to_string()));
335
+ }
336
+ let dest = flag_value(args, "--dest").map(PathBuf::from);
337
+ let dry_run = args.iter().any(|a| a == "--dry-run");
338
+ Ok(InstallSkillArgs {
339
+ target,
340
+ dest,
341
+ dry_run,
342
+ uninstall,
343
+ source,
344
+ json: parsed.json,
345
+ })
346
+ }
347
+
348
+ /// 取 `--flag <value>` 的值(用于 install-skill 的 --source/--dest,parse_args 不覆盖的旗标)。
349
+ fn flag_value(args: &[String], flag: &str) -> Option<String> {
350
+ args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1).cloned())
351
+ }
352
+
353
+ /// `team-agent install-skill`(RED-1 单源):repo `skills/team-agent` → `~/.codex|.claude|.copilot`。
354
+ /// install.mjs 删 JS 拷贝逻辑、改调本命令(`--target all --source <pkg>/skills/team-agent`)。
355
+ fn cmd_install_skill(args: &InstallSkillArgs) -> Result<CmdResult, CliError> {
356
+ // 卸载分支(单源:走同一 SkillTarget 表的 dest_dir;all → SINGLE_TARGETS 全集)。
357
+ if args.uninstall {
358
+ let home = std::env::var_os("HOME").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("."));
359
+ let targets: Vec<crate::packaging::SkillTarget> = match args.target {
360
+ crate::packaging::SkillTarget::All => {
361
+ crate::packaging::SkillTarget::SINGLE_TARGETS.to_vec()
362
+ }
363
+ t => vec![t],
364
+ };
365
+ let mut removed: Vec<serde_json::Value> = Vec::new();
366
+ for t in targets {
367
+ if let Some(dest) = t.dest_dir(&home) {
368
+ let existed = dest.0.exists();
369
+ if existed && !args.dry_run {
370
+ std::fs::remove_dir_all(&dest.0).map_err(|e| CliError::Runtime(e.to_string()))?;
371
+ }
372
+ removed.push(serde_json::json!({
373
+ "target": t,
374
+ "dest": dest.0.to_string_lossy(),
375
+ "removed": existed,
376
+ "dry_run": args.dry_run,
377
+ }));
378
+ }
379
+ }
380
+ return Ok(CmdResult::from_json(
381
+ serde_json::json!({"ok": true, "uninstalled": removed}),
382
+ args.json,
383
+ ));
384
+ }
385
+ let source = args
386
+ .source
387
+ .clone()
388
+ .ok_or_else(|| CliError::Usage("missing --source <skill dir>".to_string()))?;
389
+ let outcomes = crate::packaging::install::install_skill(&crate::packaging::SkillInstallOptions {
390
+ target: args.target,
391
+ dest: args.dest.clone(),
392
+ dry_run: args.dry_run,
393
+ source,
394
+ })
395
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
396
+ let installed: Vec<serde_json::Value> = outcomes
397
+ .iter()
398
+ .map(|o| {
399
+ serde_json::json!({
400
+ "target": o.target,
401
+ "dest": o.dest.0.to_string_lossy(),
402
+ "dry_run": o.dry_run,
403
+ "removed_stale": o.removed_stale.len(),
404
+ })
405
+ })
406
+ .collect();
407
+ Ok(CmdResult::from_json(
408
+ serde_json::json!({"ok": true, "installed": installed}),
409
+ args.json,
410
+ ))
411
+ }
412
+
261
413
  pub fn cmd_validate(args: &ValidateArgs) -> Result<CmdResult, CliError> {
262
414
  let spec = resolve_path(&args.spec);
263
415
  let value = if spec.is_dir() {
@@ -1437,4 +1589,27 @@ mod tests {
1437
1589
  let _ = std::fs::remove_dir_all(&cwd);
1438
1590
  let _ = std::fs::remove_dir_all(&ws);
1439
1591
  }
1592
+
1593
+ // ── E8 (N38): 未知子命令 → 最近似建议(additive,不破坏 golden invalid-choice 行) ──
1594
+ #[test]
1595
+ fn e8_unknown_subcommand_suggests_nearest_known_command() {
1596
+ // 'statu' typo → status; 'add-agen' → add-agent.
1597
+ assert_eq!(nearest_subcommand("statu"), Some("status"));
1598
+ assert_eq!(nearest_subcommand("add-agen"), Some("add-agent"));
1599
+ assert_eq!(nearest_subcommand("start-agnet"), Some("start-agent"));
1600
+ }
1601
+
1602
+ #[test]
1603
+ fn e8_unknown_subcommand_no_suggestion_when_far() {
1604
+ // 完全无关的串不应误配出任何建议。
1605
+ assert_eq!(nearest_subcommand("zzzzzzzzzz"), None);
1606
+ assert_eq!(nearest_subcommand("x"), None);
1607
+ }
1608
+
1609
+ #[test]
1610
+ fn e8_levenshtein_basic() {
1611
+ assert_eq!(levenshtein("kitten", "sitting"), 3);
1612
+ assert_eq!(levenshtein("status", "status"), 0);
1613
+ assert_eq!(levenshtein("statu", "status"), 1);
1614
+ }
1440
1615
  }