mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
fix(managed-agent): check latest Playground version before launch
Resolve the requested npm version before reusing local builds or running processes, fetch mismatched versions on demand, and preserve explicit binary overrides. Add launcher regression tests and sync the SDK 0.7.0 lockfile.
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { execFile, spawn, type ChildProcess } from "node:child_process";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { BailianError, type Client, ExitCode, type Settings } from "bailian-cli-core";
|
||||
import { emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const PLAYGROUND_PACKAGE = "@openagentpack/playground";
|
||||
const execFileAsync = promisify(execFile);
|
||||
const DEFAULT_PORT = 4848;
|
||||
const PLAYGROUND_URL_PATTERN = /running at http:\/\/localhost:(\d+)/i;
|
||||
|
||||
@@ -56,7 +58,7 @@ export async function launchManagedAgentPlayground(
|
||||
options.surface === "workbench" ? (options.project ?? ".") : (options.file ?? "agents.yaml"),
|
||||
);
|
||||
const projectId = createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
||||
const launcher = resolveLauncher();
|
||||
const launcher = await resolveLauncher();
|
||||
const existing = await probeExistingPlayground(port);
|
||||
if (existing) {
|
||||
const reusable =
|
||||
@@ -147,7 +149,7 @@ function assertSupportedNodeVersion(): void {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveLauncher(): Launcher {
|
||||
export async function resolveLauncher(): Promise<Launcher> {
|
||||
const explicit =
|
||||
process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN?.trim() ||
|
||||
process.env.AGENTS_PLAYGROUND_BIN?.trim();
|
||||
@@ -161,23 +163,69 @@ function resolveLauncher(): Launcher {
|
||||
return { command: process.execPath, args: [explicit], fetched: false };
|
||||
}
|
||||
|
||||
const installed = resolveInstalledPlayground();
|
||||
if (installed) return installed;
|
||||
|
||||
const monorepoBinary = findLocalPlaygroundBin(process.cwd());
|
||||
if (monorepoBinary) {
|
||||
return { command: process.execPath, args: [monorepoBinary], fetched: false };
|
||||
const requestedVersion = process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION?.trim() || "latest";
|
||||
const { stdout } = await execFileAsync(
|
||||
"npm",
|
||||
[
|
||||
"view",
|
||||
`${PLAYGROUND_PACKAGE}@${requestedVersion}`,
|
||||
"version",
|
||||
"--json",
|
||||
"--prefer-online",
|
||||
"--fetch-retries=0",
|
||||
"--fetch-timeout=10000",
|
||||
],
|
||||
{ timeout: 15_000, maxBuffer: 1024 * 1024 },
|
||||
);
|
||||
let resolvedVersion: unknown;
|
||||
try {
|
||||
resolvedVersion = JSON.parse(stdout);
|
||||
} catch {
|
||||
resolvedVersion = undefined;
|
||||
}
|
||||
if (
|
||||
typeof resolvedVersion !== "string" ||
|
||||
!/^\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/.test(resolvedVersion)
|
||||
) {
|
||||
throw new BailianError(
|
||||
"npm did not return a single valid Playground version. Specify an exact version or dist-tag. / npm 未返回唯一有效的 Playground 版本号。请指定精确版本或 dist-tag。",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
const installed = resolveInstalledPlayground();
|
||||
if (installed?.version === resolvedVersion) return installed;
|
||||
|
||||
const monorepoBinary = findLocalPlaygroundBin(process.cwd());
|
||||
if (
|
||||
monorepoBinary &&
|
||||
readPlaygroundVersion(resolve(dirname(monorepoBinary), "../../package.json")) ===
|
||||
resolvedVersion
|
||||
) {
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: [monorepoBinary],
|
||||
version: resolvedVersion,
|
||||
fetched: false,
|
||||
};
|
||||
}
|
||||
|
||||
const requestedVersion = process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION?.trim() || "latest";
|
||||
return {
|
||||
command: "npx",
|
||||
args: ["-y", `${PLAYGROUND_PACKAGE}@${requestedVersion}`],
|
||||
version: requestedVersion === "latest" ? undefined : requestedVersion,
|
||||
args: ["-y", `${PLAYGROUND_PACKAGE}@${resolvedVersion}`],
|
||||
version: resolvedVersion,
|
||||
fetched: true,
|
||||
};
|
||||
}
|
||||
|
||||
function readPlaygroundVersion(manifestPath: string): string | undefined {
|
||||
try {
|
||||
return (JSON.parse(readFileSync(manifestPath, "utf8")) as { version?: string }).version;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveInstalledPlayground(): Launcher | undefined {
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vite-plus/test";
|
||||
|
||||
const registry = vi.hoisted(() => ({
|
||||
version: '"0.7.0"',
|
||||
error: null as Error | null,
|
||||
manifest: "",
|
||||
query: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:child_process", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("node:child_process")>()),
|
||||
execFile: (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: unknown,
|
||||
callback: (error: Error | null, output: { stdout: string }) => void,
|
||||
) => {
|
||||
registry.query(command, args, options);
|
||||
callback(registry.error, { stdout: registry.version });
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("node:module", () => ({
|
||||
createRequire: () => ({ resolve: () => registry.manifest }),
|
||||
}));
|
||||
|
||||
import { resolveLauncher } from "../src/commands/managed-agent/_engine/playground-launcher.ts";
|
||||
|
||||
let directory: string;
|
||||
let binary: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
directory = await mkdtemp(join(tmpdir(), "playground-launcher-"));
|
||||
binary = join(directory, "playground.js");
|
||||
registry.manifest = join(directory, "package.json");
|
||||
registry.version = '"0.7.0"';
|
||||
registry.error = null;
|
||||
registry.query.mockClear();
|
||||
await writeFile(binary, "");
|
||||
vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN", "");
|
||||
vi.stubEnv("AGENTS_PLAYGROUND_BIN", "");
|
||||
vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION", "");
|
||||
vi.spyOn(process, "cwd").mockReturnValue(directory);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function install(version: string) {
|
||||
await writeFile(registry.manifest, JSON.stringify({ version, bin: "playground.js" }));
|
||||
}
|
||||
|
||||
test("checks latest on every resolution and reuses only the matching installed version", async () => {
|
||||
await install("0.7.0");
|
||||
expect(await resolveLauncher()).toMatchObject({
|
||||
args: [binary],
|
||||
version: "0.7.0",
|
||||
fetched: false,
|
||||
});
|
||||
registry.version = '"0.8.0"';
|
||||
expect(await resolveLauncher()).toMatchObject({
|
||||
command: "npx",
|
||||
args: ["-y", "@openagentpack/playground@0.8.0"],
|
||||
version: "0.8.0",
|
||||
fetched: true,
|
||||
});
|
||||
expect(registry.query).toHaveBeenCalledTimes(2);
|
||||
expect(registry.query).toHaveBeenCalledWith(
|
||||
"npm",
|
||||
expect.arrayContaining([
|
||||
"view",
|
||||
"@openagentpack/playground@latest",
|
||||
"--prefer-online",
|
||||
"--fetch-timeout=10000",
|
||||
]),
|
||||
expect.objectContaining({ timeout: 15_000 }),
|
||||
);
|
||||
});
|
||||
|
||||
test("downloads the resolved exact version when local installation is old or missing", async () => {
|
||||
for (const version of ["0.6.0", undefined]) {
|
||||
if (version) await install(version);
|
||||
else await rm(registry.manifest);
|
||||
expect(await resolveLauncher()).toMatchObject({
|
||||
command: "npx",
|
||||
args: ["-y", "@openagentpack/playground@0.7.0"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("explicit version overrides an incompatible installed version", async () => {
|
||||
await install("0.6.0");
|
||||
vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION", "0.7.0");
|
||||
expect(await resolveLauncher()).toMatchObject({ fetched: true, version: "0.7.0" });
|
||||
expect(registry.query.mock.calls[0]?.[1]).toContain("@openagentpack/playground@0.7.0");
|
||||
});
|
||||
|
||||
test("explicit binary remains an offline development override", async () => {
|
||||
vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN", binary);
|
||||
expect(await resolveLauncher()).toMatchObject({ args: [binary], fetched: false });
|
||||
expect(registry.query).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("registry failure does not silently reuse an old installation", async () => {
|
||||
await install("0.6.0");
|
||||
registry.error = new Error("registry unavailable");
|
||||
await expect(resolveLauncher()).rejects.toThrow("registry unavailable");
|
||||
});
|
||||
|
||||
test("rejects malformed or ambiguous registry metadata with localized diagnostics", async () => {
|
||||
for (const output of ["invalid", '["0.6.0","0.7.0"]', '"--unsafe"']) {
|
||||
registry.version = output;
|
||||
await expect(resolveLauncher()).rejects.toThrow("single valid Playground version");
|
||||
await expect(resolveLauncher()).rejects.toThrow("唯一有效的 Playground 版本号");
|
||||
}
|
||||
});
|
||||
|
||||
test("local source builds must also match the resolved version", async () => {
|
||||
const packageRoot = join(directory, "packages/playground");
|
||||
const sourceBinary = join(packageRoot, "dist/bin/playground.js");
|
||||
await mkdir(join(packageRoot, "dist/bin"), { recursive: true });
|
||||
await writeFile(sourceBinary, "");
|
||||
await writeFile(join(packageRoot, "package.json"), JSON.stringify({ version: "0.6.0" }));
|
||||
expect(await resolveLauncher()).toMatchObject({ fetched: true });
|
||||
await writeFile(join(packageRoot, "package.json"), JSON.stringify({ version: "0.7.0" }));
|
||||
expect(await resolveLauncher()).toMatchObject({
|
||||
args: [sourceBinary],
|
||||
version: "0.7.0",
|
||||
fetched: false,
|
||||
});
|
||||
});
|
||||
Generated
+5
-5
@@ -189,8 +189,8 @@ importers:
|
||||
packages/commands:
|
||||
dependencies:
|
||||
'@openagentpack/sdk':
|
||||
specifier: 0.5.0
|
||||
version: 0.5.0
|
||||
specifier: 0.7.0
|
||||
version: 0.7.0
|
||||
bailian-cli-core:
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
@@ -1124,8 +1124,8 @@ packages:
|
||||
'@emnapi/core': ^1.7.1
|
||||
'@emnapi/runtime': ^1.7.1
|
||||
|
||||
'@openagentpack/sdk@0.5.0':
|
||||
resolution: {integrity: sha512-9mNMvPWuoiK5NQVFN5i2qAhUAFSmw9jO8xrMDXXfa6eSu0XlEAPZzR6DDg4HdhW+ex60KgA+fcNA0h0LRF552A==}
|
||||
'@openagentpack/sdk@0.7.0':
|
||||
resolution: {integrity: sha512-MjyKIFvPi4aT1TIcvGUB0bnD4Tly9aMCncjI9+EYG6CPWpuGO4DGj+4zlfBCx1fvtD1G1w4Zi0m8A8ZodqkK9Q==}
|
||||
engines: {node: '>=18.17.0'}
|
||||
|
||||
'@oxc-project/runtime@0.129.0':
|
||||
@@ -4287,7 +4287,7 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@openagentpack/sdk@0.5.0':
|
||||
'@openagentpack/sdk@0.7.0':
|
||||
dependencies:
|
||||
jszip: 3.10.1
|
||||
yaml: 2.9.0
|
||||
|
||||
Reference in New Issue
Block a user