agent-trellis 0.16.0 → 0.17.0
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.
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { loadCanonicalSource } from "../core/canonical.js";
|
|
8
8
|
import type { AgentId, AgentSnapshot, CanonicalSource } from "../core/types.js";
|
|
9
|
+
import { type VersionExecutor } from "../lib/cliEntrypoints.js";
|
|
9
10
|
import { type WorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
|
|
10
11
|
/**
|
|
11
12
|
* Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
|
|
@@ -15,7 +16,7 @@ import { type WorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
|
|
|
15
16
|
* machine.
|
|
16
17
|
*/
|
|
17
18
|
export declare const DEFAULT_KNOWN_HOST_INJECTED: readonly string[];
|
|
18
|
-
export type FindingKind = "probe-failed" | "duplication" | "collision" | "drift" | "case-mismatch" | "parse-diagnostic" | "runtime-drift" | "oauth-expired" | "mcp-unreachable" | "workspace-toml-invalid";
|
|
19
|
+
export type FindingKind = "probe-failed" | "duplication" | "collision" | "drift" | "case-mismatch" | "parse-diagnostic" | "runtime-drift" | "oauth-expired" | "mcp-unreachable" | "workspace-toml-invalid" | "cli-version-split";
|
|
19
20
|
export interface Finding {
|
|
20
21
|
kind: FindingKind;
|
|
21
22
|
/** Omitted for a finding that names more than one agent (drift). */
|
|
@@ -52,6 +53,10 @@ export interface RunDoctorOptions {
|
|
|
52
53
|
* seam. CLI discovery walks up from cwd via `findWorkspaceRoot`;
|
|
53
54
|
* null = no workspace layer. */
|
|
54
55
|
workspaceDir?: string | null;
|
|
56
|
+
/** PATH to scan for `trellis` entrypoints; defaults to the real one.
|
|
57
|
+
* Test seam, with `execVersion`. */
|
|
58
|
+
pathEnv?: string;
|
|
59
|
+
execVersion?: VersionExecutor;
|
|
55
60
|
}
|
|
56
61
|
export interface OAuthHealthEntry {
|
|
57
62
|
server: string;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -17,6 +17,7 @@ import { ALL_AGENTS, isOAuthAuth, oauthOwner } from "../core/types.js";
|
|
|
17
17
|
import { classifyStoredToken, remediationFor } from "../lib/oauth/renew.js";
|
|
18
18
|
import { readToken } from "../lib/oauth/store.js";
|
|
19
19
|
import { findWorkspaceRoot } from "../lib/workspace.js";
|
|
20
|
+
import { detectCliVersionSplit, findCliEntrypoints } from "../lib/cliEntrypoints.js";
|
|
20
21
|
import { collectWorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
|
|
21
22
|
/**
|
|
22
23
|
* Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
|
|
@@ -48,6 +49,9 @@ const EXIT_NONZERO_KINDS = new Set([
|
|
|
48
49
|
// *agent*'s own startup regardless of ownership — catch it here before
|
|
49
50
|
// the next session does (trellis-doctor-workspace-toml-validation).
|
|
50
51
|
"workspace-toml-invalid",
|
|
52
|
+
// A stale `trellis` that wins on PATH silently changes behavior (e.g.
|
|
53
|
+
// rejecting config keys a newer copy accepts) — trellis-doctor-cli-version-split.
|
|
54
|
+
"cli-version-split",
|
|
51
55
|
]);
|
|
52
56
|
/** Exported so tests can assert on this resolution directly, without
|
|
53
57
|
* routing through real per-agent probes `runDoctor` also runs. */
|
|
@@ -66,6 +70,12 @@ export function resolveKnownHostInjected(opts) {
|
|
|
66
70
|
export async function runDoctor(opts = {}) {
|
|
67
71
|
const workspaceDir = opts.workspaceDir !== undefined ? opts.workspaceDir : findWorkspaceRoot(process.cwd());
|
|
68
72
|
const report = await collectDoctorReport(opts.homeDir, resolveKnownHostInjected(opts), opts.probeMcp, workspaceDir);
|
|
73
|
+
// CLI boundary only: spawning `trellis --version` belongs here, not in
|
|
74
|
+
// collectDoctorReport, so library callers and their tests never depend
|
|
75
|
+
// on the developer's real PATH.
|
|
76
|
+
const split = detectCliVersionSplit(findCliEntrypoints({ pathEnv: opts.pathEnv, execVersion: opts.execVersion }));
|
|
77
|
+
if (split)
|
|
78
|
+
report.findings.push({ kind: "cli-version-split", message: split });
|
|
69
79
|
const exitCode = report.findings.some((f) => EXIT_NONZERO_KINDS.has(f.kind)) ? 1 : 0;
|
|
70
80
|
if (opts.json) {
|
|
71
81
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locates every `trellis` executable on PATH and compares their versions
|
|
3
|
+
* (trellis-doctor-cli-version-split). Read-only: it never edits, removes
|
|
4
|
+
* or upgrades an entrypoint — a wrapper script may be deliberately
|
|
5
|
+
* maintained by the user, so Trellis only reports.
|
|
6
|
+
*/
|
|
7
|
+
export interface CliEntrypoint {
|
|
8
|
+
path: string;
|
|
9
|
+
version: string;
|
|
10
|
+
}
|
|
11
|
+
export type VersionExecutor = (path: string) => string | undefined;
|
|
12
|
+
/** Runs `<path> --version` via argv (no shell). `undefined` on any
|
|
13
|
+
* failure or unrecognizable output, so a broken entry is skipped rather
|
|
14
|
+
* than reported as a split. */
|
|
15
|
+
export declare const defaultExecVersion: VersionExecutor;
|
|
16
|
+
export declare function findCliEntrypoints(opts?: {
|
|
17
|
+
pathEnv?: string;
|
|
18
|
+
execVersion?: VersionExecutor;
|
|
19
|
+
name?: string;
|
|
20
|
+
}): CliEntrypoint[];
|
|
21
|
+
/** A finding message when two or more entries disagree; `undefined`
|
|
22
|
+
* otherwise. The first entry is the one a bare `trellis` runs. */
|
|
23
|
+
export declare function detectCliVersionSplit(entries: readonly CliEntrypoint[]): string | undefined;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locates every `trellis` executable on PATH and compares their versions
|
|
3
|
+
* (trellis-doctor-cli-version-split). Read-only: it never edits, removes
|
|
4
|
+
* or upgrades an entrypoint — a wrapper script may be deliberately
|
|
5
|
+
* maintained by the user, so Trellis only reports.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
9
|
+
import { delimiter, join } from "node:path";
|
|
10
|
+
const VERSION_RE = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/;
|
|
11
|
+
/** Runs `<path> --version` via argv (no shell). `undefined` on any
|
|
12
|
+
* failure or unrecognizable output, so a broken entry is skipped rather
|
|
13
|
+
* than reported as a split. */
|
|
14
|
+
export const defaultExecVersion = (path) => {
|
|
15
|
+
try {
|
|
16
|
+
const out = execFileSync(path, ["--version"], { encoding: "utf-8", timeout: 5_000, stdio: ["ignore", "pipe", "ignore"] });
|
|
17
|
+
return VERSION_RE.exec(out)?.[0];
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function isExecutableFile(path) {
|
|
24
|
+
try {
|
|
25
|
+
if (!statSync(path).isFile())
|
|
26
|
+
return false;
|
|
27
|
+
accessSync(path, constants.X_OK);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function findCliEntrypoints(opts = {}) {
|
|
35
|
+
const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
|
|
36
|
+
const execVersion = opts.execVersion ?? defaultExecVersion;
|
|
37
|
+
const name = opts.name ?? "trellis";
|
|
38
|
+
const seenDirs = new Set();
|
|
39
|
+
const seenReal = new Set();
|
|
40
|
+
const entries = [];
|
|
41
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
42
|
+
if (!dir || seenDirs.has(dir))
|
|
43
|
+
continue;
|
|
44
|
+
seenDirs.add(dir);
|
|
45
|
+
const candidate = join(dir, name);
|
|
46
|
+
if (!isExecutableFile(candidate))
|
|
47
|
+
continue;
|
|
48
|
+
let real;
|
|
49
|
+
try {
|
|
50
|
+
real = realpathSync(candidate);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (seenReal.has(real))
|
|
56
|
+
continue;
|
|
57
|
+
seenReal.add(real);
|
|
58
|
+
// Spawn the PATH-visible path, not the resolved target: a shim's
|
|
59
|
+
// behavior depends on the name it is invoked by.
|
|
60
|
+
const version = execVersion(candidate);
|
|
61
|
+
if (version)
|
|
62
|
+
entries.push({ path: candidate, version });
|
|
63
|
+
}
|
|
64
|
+
return entries;
|
|
65
|
+
}
|
|
66
|
+
/** A finding message when two or more entries disagree; `undefined`
|
|
67
|
+
* otherwise. The first entry is the one a bare `trellis` runs. */
|
|
68
|
+
export function detectCliVersionSplit(entries) {
|
|
69
|
+
if (entries.length < 2)
|
|
70
|
+
return undefined;
|
|
71
|
+
if (new Set(entries.map((e) => e.version)).size < 2)
|
|
72
|
+
return undefined;
|
|
73
|
+
const lines = entries.map((e, i) => ` ${i === 0 ? "▶ runs" : " also"} ${e.path} ${e.version}`);
|
|
74
|
+
return [
|
|
75
|
+
`${entries.length} \`trellis\` entrypoints on PATH report different versions — a bare \`trellis\` runs ${entries[0].path} (${entries[0].version}), so upgrading another copy has no effect:`,
|
|
76
|
+
...lines,
|
|
77
|
+
"Reconcile them (upgrade or remove the stale one, or reorder PATH) — Trellis does not modify entrypoints.",
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-trellis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "A unified Code Agent Runtime for Skills, MCP, Memory, and shared instructions across Claude Code, Codex, Kiro, pi, Kimi Code, and ZCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Paul Leo",
|