@team-agent/installer 0.3.24 → 0.3.26

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 (33) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/emit.rs +1 -0
  4. package/crates/team-agent/src/cli/mod.rs +193 -7
  5. package/crates/team-agent/src/cli/send.rs +106 -0
  6. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
  7. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +270 -13
  8. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  9. package/crates/team-agent/src/cli/types.rs +7 -0
  10. package/crates/team-agent/src/coordinator/tests/energy.rs +1 -0
  11. package/crates/team-agent/src/coordinator/tests/health_sync.rs +1 -0
  12. package/crates/team-agent/src/coordinator/tests/spine.rs +1 -0
  13. package/crates/team-agent/src/coordinator/tests/takeover.rs +1 -0
  14. package/crates/team-agent/src/coordinator/tick.rs +55 -7
  15. package/crates/team-agent/src/leader/lease.rs +57 -0
  16. package/crates/team-agent/src/leader/start.rs +1 -0
  17. package/crates/team-agent/src/lifecycle/launch.rs +217 -19
  18. package/crates/team-agent/src/lifecycle/restart/agent.rs +116 -0
  19. package/crates/team-agent/src/lifecycle/restart/common.rs +66 -1
  20. package/crates/team-agent/src/lifecycle/restart.rs +19 -2
  21. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +994 -0
  22. package/crates/team-agent/src/messaging/delivery.rs +91 -1
  23. package/crates/team-agent/src/messaging/helpers.rs +64 -24
  24. package/crates/team-agent/src/messaging/tests/runtime.rs +2 -0
  25. package/crates/team-agent/src/messaging/tests/spine.rs +158 -4
  26. package/crates/team-agent/src/tmux_backend/tests.rs +414 -2
  27. package/crates/team-agent/src/tmux_backend.rs +366 -2
  28. package/crates/team-agent/src/transport/test_support.rs +1 -0
  29. package/crates/team-agent/src/transport/tests/mod.rs +1 -0
  30. package/crates/team-agent/src/transport/tests/wire.rs +2 -1
  31. package/crates/team-agent/src/transport.rs +103 -1
  32. package/npm/install.mjs +312 -39
  33. package/package.json +4 -4
package/npm/install.mjs CHANGED
@@ -6,36 +6,108 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const modulePath = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(modulePath);
10
11
  const packageRoot = path.resolve(__dirname, "..");
11
12
  const require = createRequire(import.meta.url);
12
13
  const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
13
14
  const DOCTOR_TIMEOUT_MS = 5000;
14
15
  const VERSION_SMOKE_TIMEOUT_MS = 5000;
16
+ const INSTALL_MANIFEST = "install-manifest.json";
17
+ const PATH_MARKER = "# team-agent PATH (E48)";
18
+ const WRAPPER_MARKER = "# team-agent installer wrapper";
19
+ const WRAPPER_NAMES = ["team-agent", "team_orchestrator", "team-agent-coordinator"];
15
20
 
16
- const command = process.argv[2] || "install";
17
- const args = process.argv.slice(3);
18
-
19
- if (["-h", "--help", "help"].includes(command)) {
20
- printHelp();
21
- process.exit(0);
21
+ if (isCliEntrypoint()) {
22
+ main();
22
23
  }
23
24
 
24
- try {
25
- if (command === "install" || command === "update") {
26
- install(args);
27
- } else if (command === "doctor") {
28
- runDoctor(args);
29
- } else if (command === "uninstall") {
30
- uninstall(args);
31
- } else {
32
- console.error(`unknown command: ${command}`);
25
+ function main() {
26
+ const command = process.argv[2] || "install";
27
+ const args = process.argv.slice(3);
28
+
29
+ if (["-h", "--help", "help"].includes(command)) {
33
30
  printHelp();
34
- process.exit(2);
31
+ process.exit(0);
32
+ }
33
+
34
+ try {
35
+ if (command === "install" || command === "update") {
36
+ install(args);
37
+ } else if (command === "doctor") {
38
+ runDoctor(args);
39
+ } else if (command === "uninstall") {
40
+ uninstall(args);
41
+ } else {
42
+ console.error(`unknown command: ${command}`);
43
+ printHelp();
44
+ process.exit(2);
45
+ }
46
+ } catch (error) {
47
+ console.error(error instanceof Error ? error.message : String(error));
48
+ process.exit(1);
49
+ }
50
+ }
51
+
52
+ function isCliEntrypoint() {
53
+ if (!process.argv[1]) {
54
+ return false;
55
+ }
56
+ try {
57
+ return fs.realpathSync(process.argv[1]) === fs.realpathSync(modulePath);
58
+ } catch {
59
+ return path.resolve(process.argv[1]) === modulePath;
35
60
  }
36
- } catch (error) {
37
- console.error(error instanceof Error ? error.message : String(error));
38
- process.exit(1);
61
+ }
62
+
63
+ export function doctorSelfCheckVerdict(doctorBody, spawnMeta = {}) {
64
+ if (spawnMeta.error || spawnMeta.signal) {
65
+ return { kind: "advisory", blockers: [] };
66
+ }
67
+
68
+ let body;
69
+ try {
70
+ body = JSON.parse(doctorBody || "");
71
+ } catch {
72
+ return { kind: "advisory", blockers: [] };
73
+ }
74
+
75
+ const blockers = [];
76
+ // Doctor JSON source: crates/team-agent/src/cli/mod.rs:2739-2764.
77
+ if (body?.tmux?.installed === false) {
78
+ blockers.push("tmux not installed");
79
+ }
80
+ if (!body?.mcp?.server_command) {
81
+ blockers.push("MCP server command missing");
82
+ }
83
+ const profileSmokeStatus = body?.profile_smoke?.status;
84
+ const profileSmokeNonBlocking =
85
+ profileSmokeStatus === "legacy_team_invalid" || profileSmokeStatus === "not_required";
86
+ if (
87
+ body?.error === "profile_smoke_failed" ||
88
+ (body?.profile_smoke?.ok === false && !profileSmokeNonBlocking)
89
+ ) {
90
+ blockers.push("profile smoke failed");
91
+ }
92
+
93
+ if (blockers.length > 0) {
94
+ return { kind: "blockers", blockers };
95
+ }
96
+
97
+ const noContext =
98
+ body?.ok === false && body?.error === "workspace has no Team Agent spec or runtime context";
99
+ if (body?.ok === true || noContext) {
100
+ return { kind: "ok", blockers: [] };
101
+ }
102
+
103
+ return { kind: "advisory", blockers: [] };
104
+ }
105
+
106
+ export function doctorSelfCheckLine(verdict) {
107
+ if (verdict.kind === "blockers") {
108
+ return `doctor: found blockers (${verdict.blockers.join("; ")}); run team-agent doctor in your project for details`;
109
+ }
110
+ return "doctor: ok (run team-agent doctor inside a team workspace for a full report)";
39
111
  }
40
112
 
41
113
  function printHelp() {
@@ -47,7 +119,7 @@ Usage:
47
119
  npx @team-agent/installer@latest uninstall
48
120
 
49
121
  Options:
50
- --prefix <dir> wrapper install prefix, default ~/.local
122
+ --prefix <dir> fallback wrapper prefix when no writable PATH dir exists, default ~/.local
51
123
  --runtime-dir <dir> stable runtime root, default ~/.team-agent/runtime
52
124
  --purge-runtime uninstall also removes the runtime root
53
125
  `);
@@ -55,9 +127,9 @@ Options:
55
127
 
56
128
  function install(argv) {
57
129
  const opts = parseOptions(argv);
58
- const prefix = path.resolve(expandHome(opts.prefix || path.join(os.homedir(), ".local")));
59
- const binDir = path.join(prefix, "bin");
60
130
  const runtimeRoot = path.resolve(expandHome(opts.runtimeDir || path.join(os.homedir(), ".team-agent", "runtime")));
131
+ const installTarget = resolveInstallBinDir({ env: process.env, home: os.homedir(), prefix: opts.prefix });
132
+ const binDir = installTarget.binDir;
61
133
  const version = packageJson.version || "dev";
62
134
  const dest = path.join(runtimeRoot, version);
63
135
  const tmp = path.join(runtimeRoot, `.${version}.${process.pid}.tmp`);
@@ -81,13 +153,29 @@ function install(argv) {
81
153
  writeExecWrapper(path.join(binDir, "team_orchestrator"), runtimeBinary, ["mcp-server"]);
82
154
  writeExecWrapper(path.join(binDir, "team-agent-coordinator"), runtimeBinary, ["coordinator"]);
83
155
  installSkills(runtimeBinary);
156
+ writeInstallManifest(runtimeRoot, {
157
+ version,
158
+ binDir,
159
+ runtimeRoot,
160
+ runtimeBinary,
161
+ installedAt: new Date().toISOString(),
162
+ installTargetKind: installTarget.kind,
163
+ });
84
164
 
85
165
  const teamAgent = path.join(binDir, "team-agent");
86
166
  console.log(`installed: ${teamAgent}`);
167
+ if (installTarget.readyNow) {
168
+ console.log(`installed to ${binDir} (on PATH, ready now)`);
169
+ } else if (installTarget.rc?.files?.length > 0) {
170
+ console.log(`installed to ${binDir}; added ${binDir} to ${installTarget.rc.files.join(", ")}; restart terminal or open a new shell to use team-agent`);
171
+ } else if (installTarget.rc?.skipped?.length > 0) {
172
+ console.log(`installed to ${binDir}; PATH entry already present in ${installTarget.rc.skipped.join(", ")}; restart terminal or open a new shell to use team-agent`);
173
+ } else {
174
+ console.log(`installed to ${binDir}; add it to PATH to use team-agent by name`);
175
+ }
87
176
  console.log(`runtime: ${dest}`);
88
177
  console.log(`binary: ${platformBinary.packageName}`);
89
178
  console.log("skill: installed for Codex, Claude and Copilot");
90
- console.log(`PATH: ensure ${binDir} is on PATH`);
91
179
 
92
180
  // 0.3.6 hotfix · C-5 cr verdict — post-install binary smoke 门(走 `--help`
93
181
  // 子命令,因为 0.3.x CLI 现阶段没有 --version)。真跑一次 binary 才能抓住
@@ -116,11 +204,8 @@ function install(argv) {
116
204
  encoding: "utf8",
117
205
  timeout: DOCTOR_TIMEOUT_MS,
118
206
  });
119
- if (doctor.status === 0) {
120
- console.log("doctor: ok");
121
- } else {
122
- console.log("doctor: has blockers; run `team-agent doctor` after updating PATH");
123
- }
207
+ const verdict = doctorSelfCheckVerdict(doctor.stdout, doctor);
208
+ console.log(doctorSelfCheckLine(verdict));
124
209
  } finally {
125
210
  fs.rmSync(doctorWorkspace, { recursive: true, force: true });
126
211
  }
@@ -128,8 +213,8 @@ function install(argv) {
128
213
 
129
214
  function runDoctor(argv) {
130
215
  const opts = parseOptions(argv);
131
- const prefix = path.resolve(expandHome(opts.prefix || path.join(os.homedir(), ".local")));
132
- const teamAgent = path.join(prefix, "bin", "team-agent");
216
+ const runtimeRoot = path.resolve(expandHome(opts.runtimeDir || path.join(os.homedir(), ".team-agent", "runtime")));
217
+ const teamAgent = path.join(installedBinDir(runtimeRoot, opts), "team-agent");
133
218
  if (!fs.existsSync(teamAgent)) {
134
219
  console.error(`team-agent wrapper not found: ${teamAgent}`);
135
220
  process.exit(1);
@@ -148,10 +233,11 @@ function runDoctor(argv) {
148
233
 
149
234
  function uninstall(argv) {
150
235
  const opts = parseOptions(argv);
151
- const prefix = path.resolve(expandHome(opts.prefix || path.join(os.homedir(), ".local")));
236
+ const runtimeRoot = path.resolve(expandHome(opts.runtimeDir || path.join(os.homedir(), ".team-agent", "runtime")));
237
+ const binDir = installedBinDir(runtimeRoot, opts);
152
238
  // 卸载 skill 走二进制单源(同一 SkillTarget 表 codex/claude/copilot),在删 wrapper 前调
153
239
  // (删 wrapper 后 PATH 上的 team-agent 没了,但 runtime 二进制仍在;用 runtime 二进制直调)。
154
- const teamAgentBin = path.join(prefix, "bin", "team-agent");
240
+ const teamAgentBin = path.join(binDir, "team-agent");
155
241
  if (fs.existsSync(teamAgentBin)) {
156
242
  const res = spawnSync(teamAgentBin, ["install-skill", "--target", "all", "--uninstall", "--json"], {
157
243
  text: true,
@@ -162,13 +248,12 @@ function uninstall(argv) {
162
248
  console.error(`WARN: skill uninstall via binary failed (status=${res.status ?? "signal"}); skill dirs may remain under ~/.codex|.claude|.copilot/skills/team-agent`);
163
249
  }
164
250
  }
165
- for (const name of ["team-agent", "team_orchestrator", "team-agent-coordinator"]) {
166
- fs.rmSync(path.join(prefix, "bin", name), { force: true });
251
+ for (const name of WRAPPER_NAMES) {
252
+ fs.rmSync(path.join(binDir, name), { force: true });
167
253
  }
168
- console.log(`removed wrappers from ${path.join(prefix, "bin")}`);
254
+ console.log(`removed wrappers from ${binDir}`);
169
255
  console.log("removed skills from ~/.codex, ~/.claude and ~/.copilot skills/team-agent");
170
256
  if (opts.purgeRuntime) {
171
- const runtimeRoot = path.resolve(expandHome(opts.runtimeDir || path.join(os.homedir(), ".team-agent", "runtime")));
172
257
  fs.rmSync(runtimeRoot, { recursive: true, force: true });
173
258
  console.log(`removed runtime root ${runtimeRoot}`);
174
259
  } else {
@@ -197,6 +282,182 @@ function parseOptions(argv) {
197
282
  return opts;
198
283
  }
199
284
 
285
+ export function resolveInstallBinDir(options = {}) {
286
+ const env = options.env || process.env;
287
+ const home = options.home || os.homedir();
288
+ const entries = uniquePathEntries(env.PATH || "", home);
289
+ for (const entry of entries) {
290
+ if (isVersionManagedPath(entry) || !canWriteDir(entry) || hasForeignWrapper(entry)) {
291
+ continue;
292
+ }
293
+ return { binDir: entry, kind: "path", readyNow: true, rc: null };
294
+ }
295
+
296
+ for (const entry of entries) {
297
+ if (isVersionManagedPath(entry) || !isReasonableUserBinDir(entry, home) || !canWriteDir(entry) || hasForeignWrapper(entry)) {
298
+ continue;
299
+ }
300
+ return { binDir: entry, kind: "path_user", readyNow: true, rc: null };
301
+ }
302
+
303
+ const fallbackPrefix = options.prefix
304
+ ? path.resolve(expandHomeFor(options.prefix, home))
305
+ : path.join(home, ".local");
306
+ const binDir = path.join(fallbackPrefix, "bin");
307
+ fs.mkdirSync(binDir, { recursive: true });
308
+ const rc = ensureBinDirOnShellRc(binDir, { env, home });
309
+ return { binDir, kind: "shell_rc", readyNow: false, rc };
310
+ }
311
+
312
+ function uniquePathEntries(searchPath, home) {
313
+ const seen = new Set();
314
+ const entries = [];
315
+ for (const raw of searchPath.split(path.delimiter)) {
316
+ if (!raw) {
317
+ continue;
318
+ }
319
+ const resolved = path.resolve(expandHomeFor(raw, home));
320
+ if (seen.has(resolved)) {
321
+ continue;
322
+ }
323
+ seen.add(resolved);
324
+ entries.push(resolved);
325
+ }
326
+ return entries;
327
+ }
328
+
329
+ function isVersionManagedPath(dir) {
330
+ const value = dir.replace(/\\/g, "/");
331
+ return [
332
+ "/.nvm/versions/",
333
+ "/Cellar/",
334
+ "/volta/tools/image/",
335
+ "/fnm/node-versions/",
336
+ "/.asdf/installs/",
337
+ "/node_modules/.bin",
338
+ "/.npm/_npx/",
339
+ "/_npx/",
340
+ ].some((marker) => value.includes(marker));
341
+ }
342
+
343
+ function isReasonableUserBinDir(dir, home) {
344
+ const relative = path.relative(home, dir);
345
+ return Boolean(relative && !relative.startsWith("..") && !path.isAbsolute(relative));
346
+ }
347
+
348
+ function canWriteDir(dir) {
349
+ try {
350
+ const probe = path.join(dir, `.team-agent-write-test-${process.pid}-${Date.now()}`);
351
+ fs.writeFileSync(probe, "");
352
+ fs.rmSync(probe, { force: true });
353
+ return true;
354
+ } catch {
355
+ return false;
356
+ }
357
+ }
358
+
359
+ function hasForeignWrapper(binDir) {
360
+ return WRAPPER_NAMES.some((name) => {
361
+ const file = path.join(binDir, name);
362
+ return fs.existsSync(file) && !isInstallerManagedWrapper(file);
363
+ });
364
+ }
365
+
366
+ function isInstallerManagedWrapper(file) {
367
+ try {
368
+ const text = fs.readFileSync(file, "utf8");
369
+ if (text.includes(WRAPPER_MARKER)) {
370
+ return true;
371
+ }
372
+ return /^#!\/usr\/bin\/env sh\nexec '[^']+\/bin\/team-agent'(?: '[^']+')? "\$@"\n$/.test(text);
373
+ } catch {
374
+ return false;
375
+ }
376
+ }
377
+
378
+ function ensureBinDirOnShellRc(binDir, options = {}) {
379
+ const env = options.env || process.env;
380
+ const home = options.home || os.homedir();
381
+ const shell = path.basename(env.SHELL || "");
382
+ const rc = shellRcTargets(shell, home);
383
+ if (!rc) {
384
+ return { files: [], skipped: [], unsupported: true };
385
+ }
386
+ const block = rc.style === "fish"
387
+ ? `\n${PATH_MARKER}\nset -gx PATH ${shellQuote(binDir)} $PATH\n`
388
+ : `\n${PATH_MARKER}\nexport PATH=\"${escapeDoubleQuoted(binDir)}:$PATH\"\n`;
389
+ const files = [];
390
+ const skipped = [];
391
+ for (const file of rc.files) {
392
+ let existing = "";
393
+ try {
394
+ existing = fs.readFileSync(file, "utf8");
395
+ } catch {
396
+ existing = "";
397
+ }
398
+ if (existing.includes(PATH_MARKER)) {
399
+ skipped.push(file);
400
+ continue;
401
+ }
402
+ try {
403
+ fs.mkdirSync(path.dirname(file), { recursive: true });
404
+ fs.appendFileSync(file, block);
405
+ files.push(file);
406
+ } catch {
407
+ continue;
408
+ }
409
+ }
410
+ return { files, skipped, unsupported: false };
411
+ }
412
+
413
+ function shellRcTargets(shell, home) {
414
+ if (shell === "zsh") {
415
+ return { files: [path.join(home, ".zshrc")], style: "posix" };
416
+ }
417
+ if (shell === "bash") {
418
+ return { files: [path.join(home, ".bashrc"), path.join(home, ".bash_profile")], style: "posix" };
419
+ }
420
+ if (shell === "fish") {
421
+ return { files: [path.join(home, ".config", "fish", "config.fish")], style: "fish" };
422
+ }
423
+ if (!shell || shell === "sh") {
424
+ return {
425
+ files: [process.platform === "darwin" ? path.join(home, ".zshrc") : path.join(home, ".profile")],
426
+ style: "posix",
427
+ };
428
+ }
429
+ return null;
430
+ }
431
+
432
+ function installedBinDir(runtimeRoot, opts) {
433
+ const manifest = readInstallManifest(runtimeRoot);
434
+ if (typeof manifest?.binDir === "string" && manifest.binDir) {
435
+ return manifest.binDir;
436
+ }
437
+ const prefix = path.resolve(expandHome(opts.prefix || path.join(os.homedir(), ".local")));
438
+ return path.join(prefix, "bin");
439
+ }
440
+
441
+ function installManifestPath(runtimeRoot) {
442
+ return path.join(runtimeRoot, INSTALL_MANIFEST);
443
+ }
444
+
445
+ export function readInstallManifest(runtimeRoot) {
446
+ try {
447
+ return JSON.parse(fs.readFileSync(installManifestPath(runtimeRoot), "utf8"));
448
+ } catch {
449
+ return null;
450
+ }
451
+ }
452
+
453
+ export function writeInstallManifest(runtimeRoot, manifest) {
454
+ fs.mkdirSync(runtimeRoot, { recursive: true });
455
+ const file = installManifestPath(runtimeRoot);
456
+ const tmp = `${file}.${process.pid}.tmp`;
457
+ fs.writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`);
458
+ fs.renameSync(tmp, file);
459
+ }
460
+
200
461
  function resolvePlatformBinary() {
201
462
  const packageName = platformPackageName();
202
463
  if (!packageName) {
@@ -239,9 +500,13 @@ function copyExecutable(src, dest) {
239
500
  }
240
501
 
241
502
  function writeExecWrapper(file, binary, fixedArgs) {
503
+ if (fs.existsSync(file) && !isInstallerManagedWrapper(file)) {
504
+ throw new Error(`refusing to overwrite non-Team Agent installer wrapper: ${file}`);
505
+ }
242
506
  const args = fixedArgs.map(shellQuote).join(" ");
243
507
  const argPrefix = args ? `${args} ` : "";
244
508
  const content = `#!/usr/bin/env sh
509
+ ${WRAPPER_MARKER}
245
510
  exec ${shellQuote(binary)} ${argPrefix}"$@"
246
511
  `;
247
512
  fs.writeFileSync(file, content, { mode: 0o755 });
@@ -275,15 +540,23 @@ function makeDoctorWorkspace() {
275
540
  }
276
541
 
277
542
  function expandHome(value) {
543
+ return expandHomeFor(value, os.homedir());
544
+ }
545
+
546
+ function expandHomeFor(value, home) {
278
547
  if (value === "~") {
279
- return os.homedir();
548
+ return home;
280
549
  }
281
550
  if (value.startsWith("~/")) {
282
- return path.join(os.homedir(), value.slice(2));
551
+ return path.join(home, value.slice(2));
283
552
  }
284
553
  return value;
285
554
  }
286
555
 
556
+ function escapeDoubleQuoted(value) {
557
+ return String(value).replace(/["\\`$]/g, "\\$&");
558
+ }
559
+
287
560
  function shellQuote(value) {
288
561
  return `'${String(value).replace(/'/g, "'\\''")}'`;
289
562
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
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",
24
- "@team-agent/cli-darwin-x64": "0.3.24",
25
- "@team-agent/cli-linux-x64": "0.3.24"
23
+ "@team-agent/cli-darwin-arm64": "0.3.26",
24
+ "@team-agent/cli-darwin-x64": "0.3.26",
25
+ "@team-agent/cli-linux-x64": "0.3.26"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",