@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +52 -7
- package/crates/team-agent/src/cli/diagnose.rs +9 -0
- package/crates/team-agent/src/cli/emit.rs +175 -0
- package/crates/team-agent/src/cli/mod.rs +455 -63
- package/crates/team-agent/src/cli/status_port.rs +62 -0
- package/crates/team-agent/src/cli/tests/base.rs +9 -4
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +83 -1
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +10 -2
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +86 -21
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +76 -0
- package/crates/team-agent/src/cli/types.rs +3 -2
- package/crates/team-agent/src/compiler.rs +73 -50
- package/crates/team-agent/src/coordinator/tick.rs +108 -20
- package/crates/team-agent/src/db/migration.rs +17 -1
- package/crates/team-agent/src/leader/owner_bind.rs +59 -20
- package/crates/team-agent/src/lifecycle/launch.rs +378 -56
- package/crates/team-agent/src/lifecycle/restart/common.rs +4 -9
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +91 -12
- package/crates/team-agent/src/lifecycle/restart/selection.rs +6 -4
- package/crates/team-agent/src/lifecycle/tests/core.rs +238 -3
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +257 -7
- package/crates/team-agent/src/lifecycle/types.rs +2 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +29 -7
- package/crates/team-agent/src/mcp_server/tests/golden.rs +7 -5
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +5 -2
- package/crates/team-agent/src/mcp_server/tools.rs +25 -1
- package/crates/team-agent/src/mcp_server/wire.rs +11 -1
- package/crates/team-agent/src/model/paths.rs +7 -0
- package/crates/team-agent/src/model/spec.rs +23 -1
- package/crates/team-agent/src/packaging/install.rs +42 -4
- package/crates/team-agent/src/packaging/tests.rs +91 -14
- package/crates/team-agent/src/packaging/types.rs +13 -1
- package/crates/team-agent/src/provider/adapter.rs +381 -15
- package/crates/team-agent/src/state/identity.rs +29 -0
- package/crates/team-agent/src/state/selector.rs +48 -14
- package/crates/team-agent/src/tmux_backend/tests.rs +44 -0
- package/crates/team-agent/src/tmux_backend.rs +104 -9
- package/crates/team-agent/src/transport/test_support.rs +57 -4
- package/crates/team-agent/src/transport.rs +13 -0
- package/npm/install.mjs +31 -35
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +82 -5
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -133,13 +133,33 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
133
133
|
}
|
|
134
134
|
Ok(result)
|
|
135
135
|
} else {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
}
|