mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a245359792 | |||
| 19bd5a5ea8 | |||
| e262f2e574 | |||
| 180aac9f28 | |||
| aa1484264e | |||
| 2767491ce6 | |||
| efb832815a | |||
| 902516bbad | |||
| f44fb2e95f | |||
| c6b9931d6f | |||
| a92c58420b | |||
| bcf68a31f3 | |||
| e53daf05eb | |||
| 9469556671 | |||
| b93e0d586d |
@@ -6,6 +6,36 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
|
||||
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## [1.22.0] - 2026-09-08
|
||||
|
||||
### Changed
|
||||
|
||||
- **Project initialization** — `managed-agent project init` now creates `./managed-agent` by default. Use `--project .` to initialize in place. **(BREAKING)**
|
||||
- **Build confirmation** — `managed-agent project build` no longer requires confirmation and rejects `--yes`. Use `--dry-run` for a read-only preview; Publish still requires confirmation. **(BREAKING)**
|
||||
- **Managed Agent SDK** — upgrade to `0.7.1`. Build automatically associates active Agent-local resources while preserving explicit bindings, Skill versions, and File mount paths. Ambiguous Environment or Vault selections are rejected before writing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Project diagnostics** — provide actionable project-root guidance and surface the underlying Build validation error.
|
||||
- **YAML initialization paths** — show the absolute YAML path in creation messages and existing-file errors.
|
||||
|
||||
### Internal
|
||||
|
||||
- Expand project initialization and Build regression coverage, and remove the obsolete Build confirmation flag from the local lifecycle E2E test.
|
||||
|
||||
## [1.21.0] - 2026-09-07
|
||||
|
||||
### Added
|
||||
|
||||
- **Managed Agent directory projects** — initialize, validate, build, and publish local projects with Agent-scoped resource configuration and resource examples.
|
||||
- **Local project versions** — enable or disable snapshot versioning, list and preview history, and restore project files without Git.
|
||||
- **Project Workbench** — edit resources, review changes, publish updates, and manage local versions in a browser.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Playground version resolution** — check npm before launch, reuse matching local versions, and fetch the latest version when needed; retain explicit version and binary overrides.
|
||||
- **Managed Agent SDK** — upgrade to `0.7.0` and consume project workspace and version services through SDK subpath exports.
|
||||
|
||||
## [1.20.0] - 2026-09-03
|
||||
|
||||
> Managed Agents now combines YAML-first infrastructure management with direct Bailian AgentStudio resource and runtime operations.
|
||||
|
||||
@@ -6,6 +6,36 @@
|
||||
|
||||
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
|
||||
|
||||
## [1.22.0] - 2026-09-08
|
||||
|
||||
### 变更
|
||||
|
||||
- **项目初始化** —— `managed-agent project init` 默认创建 `./managed-agent` 子目录;如需原地初始化,请使用 `--project .`。**(BREAKING)**
|
||||
- **Build 确认机制** —— `managed-agent project build` 无需确认,并且不再接受 `--yes`。使用 `--dry-run` 可只读预览;Publish 仍需显式确认。**(BREAKING)**
|
||||
- **Managed Agent SDK** —— 升级至 `0.7.1`。Build 自动关联 Agent 目录下已启用的资源,保留显式引用、Skill 版本和 File 挂载路径;Environment 或 Vault 选择存在歧义时,在写入前报错。
|
||||
|
||||
### 修复
|
||||
|
||||
- **项目诊断** —— 提供可操作的项目根目录提示,并展示 Build 校验失败的具体原因。
|
||||
- **YAML 初始化路径** —— 创建成功及文件已存在的错误信息均展示 YAML 绝对路径。
|
||||
|
||||
### 内部
|
||||
|
||||
- 补充项目初始化和 Build 回归覆盖,移除本地闭环 E2E 测试中过时的 Build 确认参数。
|
||||
|
||||
## [1.21.0] - 2026-09-07
|
||||
|
||||
### 新增
|
||||
|
||||
- **Managed Agent 目录项目** —— 支持初始化、校验、构建和发布本地项目,按 Agent 组织资源配置,并提供资源示例。
|
||||
- **本地项目版本管理** —— 无需 Git 即可启停快照版本管理、查看和预览历史,以及恢复项目文件。
|
||||
- **项目 Workbench** —— 在浏览器中编辑资源、审阅变更、发布更新和管理本地版本。
|
||||
|
||||
### 变更
|
||||
|
||||
- **Playground 版本选择** —— 启动前查询 npm,复用版本一致的本地安装,否则按需下载最新版;保留显式版本和启动文件覆盖。
|
||||
- **Managed Agent SDK** —— 升级至 `0.7.0`,通过 SDK 子路径复用项目目录和版本服务。
|
||||
|
||||
## [1.20.0] - 2026-09-03
|
||||
|
||||
> Managed Agent 现在同时提供 YAML-first 基础设施管理与百炼 AgentStudio 资源、运行时 API 操作。
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- 类型由 `ParsedFlags<typeof FLAGS>` 推导;避免手写 `flags.x as number` 这类断言
|
||||
- 单 flag 必填用 `required: true`;跨 flag / 值相关校验放 `validate`
|
||||
- 默认值 fallback 写在命令实现或 `Settings` 解析层,不要重复解析 env/config
|
||||
- 需要在高风险确认前检查本地路径时,可用异步 `validate`;runtime 会在鉴权和确认前等待它完成。这里只允许本地只读检查,不写文件、不请求远端。非缺参的环境错误应抛出 `BailianError`,避免裸命令调用被当成缺参而仅显示 help。
|
||||
|
||||
### B. 鉴权 / 全局选项
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.20.0",
|
||||
"version": "1.22.0",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -140,6 +140,18 @@ import {
|
||||
managedAgentPlan,
|
||||
managedAgentApply,
|
||||
managedAgentDestroy,
|
||||
managedAgentPlayground,
|
||||
managedAgentProjectInit,
|
||||
managedAgentProjectValidate,
|
||||
managedAgentProjectBuild,
|
||||
managedAgentProjectPublish,
|
||||
managedAgentProjectWorkbench,
|
||||
managedAgentProjectVersionEnable,
|
||||
managedAgentProjectVersionDisable,
|
||||
managedAgentProjectVersionStatus,
|
||||
managedAgentProjectVersionList,
|
||||
managedAgentProjectVersionPreview,
|
||||
managedAgentProjectVersionRestore,
|
||||
managedAgentStateList,
|
||||
managedAgentStateShow,
|
||||
managedAgentStateRm,
|
||||
@@ -344,6 +356,18 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"managed-agent plan": managedAgentPlan,
|
||||
"managed-agent apply": managedAgentApply,
|
||||
"managed-agent destroy": managedAgentDestroy,
|
||||
"managed-agent playground": managedAgentPlayground,
|
||||
"managed-agent project init": managedAgentProjectInit,
|
||||
"managed-agent project validate": managedAgentProjectValidate,
|
||||
"managed-agent project build": managedAgentProjectBuild,
|
||||
"managed-agent project publish": managedAgentProjectPublish,
|
||||
"managed-agent project workbench": managedAgentProjectWorkbench,
|
||||
"managed-agent project version enable": managedAgentProjectVersionEnable,
|
||||
"managed-agent project version disable": managedAgentProjectVersionDisable,
|
||||
"managed-agent project version status": managedAgentProjectVersionStatus,
|
||||
"managed-agent project version list": managedAgentProjectVersionList,
|
||||
"managed-agent project version preview": managedAgentProjectVersionPreview,
|
||||
"managed-agent project version restore": managedAgentProjectVersionRestore,
|
||||
"managed-agent state list": managedAgentStateList,
|
||||
"managed-agent state show": managedAgentStateShow,
|
||||
"managed-agent state rm": managedAgentStateRm,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.20.0",
|
||||
"version": "1.22.0",
|
||||
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
@@ -40,7 +40,7 @@
|
||||
"check": "vp check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openagentpack/sdk": "0.5.0",
|
||||
"@openagentpack/sdk": "0.7.1",
|
||||
"bailian-cli-core": "workspace:*",
|
||||
"bailian-cli-runtime": "workspace:*",
|
||||
"boxen": "catalog:",
|
||||
|
||||
@@ -30,9 +30,11 @@ export const BAILIAN_PROVIDER = "bailian";
|
||||
|
||||
interface AgentConfigOptions {
|
||||
resolveEnv?: boolean;
|
||||
environment?: Record<string, string>;
|
||||
projectName?: string;
|
||||
statePath?: string;
|
||||
credentials?: CredentialScope;
|
||||
overrideBailianBaseUrl?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +58,9 @@ export async function resolveAgentProjectConfig(
|
||||
prepareProviderEnv();
|
||||
const resolved = await resolveProjectConfig(filePath, options);
|
||||
normalizeInterpolatedProviderBlocks(resolved.config.providers);
|
||||
injectProviderCredentials(resolved.config.providers, host);
|
||||
injectProviderCredentials(resolved.config.providers, host, {
|
||||
overrideBaseUrl: options.overrideBailianBaseUrl,
|
||||
});
|
||||
scrubCredentialEnv();
|
||||
assertBailianOnlyProviders(resolved.config.providers);
|
||||
if ((options.credentials ?? "all") !== "none") {
|
||||
|
||||
@@ -99,9 +99,10 @@ export function prepareProviderEnv(): void {
|
||||
* Override the bailian provider block with bl's authStage-resolved credential, so
|
||||
* the bailian API key is authoritatively the CLI auth chain's — never a config
|
||||
* file bare-read or a stale env value. `api_key` is replaced unconditionally
|
||||
* when a credential resolved; `base_url` / `workspace_id` are filled only when
|
||||
* the block references them and the interpolated value is empty (a literal in
|
||||
* agents.yaml is respected).
|
||||
* when a credential resolved. `base_url` is normally filled only when empty so
|
||||
* a literal in agents.yaml remains supported; directory projects pass
|
||||
* `overrideBaseUrl` so their connection always follows the Bailian CLI auth
|
||||
* chain. `workspace_id` is filled only when empty.
|
||||
*
|
||||
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
|
||||
* paths onto it verbatim; a value already ending in the suffix is left as-is.
|
||||
@@ -114,6 +115,7 @@ export function prepareProviderEnv(): void {
|
||||
export function injectProviderCredentials(
|
||||
providers: Record<string, unknown>,
|
||||
host: CredentialHost,
|
||||
options: { overrideBaseUrl?: boolean } = {},
|
||||
): void {
|
||||
const bailian = providers.bailian;
|
||||
if (!bailian || typeof bailian !== "object") return;
|
||||
@@ -121,7 +123,7 @@ export function injectProviderCredentials(
|
||||
|
||||
const cred = host.client.exportApiCredential();
|
||||
if (cred) block.api_key = cred.token;
|
||||
if ("base_url" in block && !block.base_url) {
|
||||
if ("base_url" in block && (options.overrideBaseUrl || !block.base_url)) {
|
||||
// Defensive normalization: the auth chain already normalizes base_url to
|
||||
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
|
||||
const origin = host.client.baseUrl.replace(/\/+$/, "");
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
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;
|
||||
|
||||
export interface PlaygroundLaunchOptions {
|
||||
port?: number;
|
||||
open: boolean;
|
||||
file?: string;
|
||||
project?: string;
|
||||
agent?: string;
|
||||
surface: "preview" | "workbench";
|
||||
client: Client;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
interface Launcher {
|
||||
command: string;
|
||||
args: string[];
|
||||
version?: string;
|
||||
fetched: boolean;
|
||||
}
|
||||
|
||||
interface ExistingPlayground {
|
||||
version: string;
|
||||
pid: number;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
interface PlaygroundProjectSummary {
|
||||
status?: string;
|
||||
agents?: Array<{ agent?: { id?: string } }>;
|
||||
}
|
||||
|
||||
export interface PlaygroundBrowserTarget {
|
||||
url: string;
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
export async function launchManagedAgentPlayground(
|
||||
options: PlaygroundLaunchOptions,
|
||||
): Promise<void> {
|
||||
assertSupportedNodeVersion();
|
||||
const port = options.port ?? DEFAULT_PORT;
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
|
||||
throw new BailianError(`Invalid --port '${port}'.`, ExitCode.USAGE);
|
||||
}
|
||||
const sourcePath = resolve(
|
||||
options.surface === "workbench" ? (options.project ?? ".") : (options.file ?? "agents.yaml"),
|
||||
);
|
||||
const projectId = createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
|
||||
const launcher = await resolveLauncher();
|
||||
const existing = await probeExistingPlayground(port);
|
||||
if (existing) {
|
||||
const reusable =
|
||||
existing.projectId === projectId &&
|
||||
(launcher.version === undefined || existing.version === launcher.version);
|
||||
if (reusable) {
|
||||
emitBare(`Workbench already running at http://localhost:${port} (pid ${existing.pid}).`);
|
||||
await openPlaygroundSurface(port, options.surface, options.agent, options.open);
|
||||
return;
|
||||
}
|
||||
const released = await replaceExistingPlayground(existing, port);
|
||||
if (!released) {
|
||||
throw new BailianError(
|
||||
`Could not stop the existing Workbench process (pid ${existing.pid}) on port ${port}.`,
|
||||
ExitCode.GENERAL,
|
||||
"Stop it manually or choose another --port.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const environment = buildPlaygroundEnvironment(options, port, sourcePath);
|
||||
if (launcher.fetched) {
|
||||
emitBare(`Fetching ${PLAYGROUND_PACKAGE} (first run may take a moment)...`);
|
||||
}
|
||||
const child = spawn(launcher.command, launcher.args, {
|
||||
env: environment,
|
||||
stdio: ["inherit", "pipe", "inherit"],
|
||||
});
|
||||
const removeSignalForwarding = forwardSignals(child);
|
||||
try {
|
||||
const readyPort = await waitForPlaygroundReady(child, port, 30_000, projectId);
|
||||
if (readyPort === null) {
|
||||
throw new BailianError(
|
||||
`Workbench did not become ready in time. Check the logs above, then open http://localhost:${port}.`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
emitBare(`Workbench ready at http://localhost:${readyPort}`);
|
||||
await openPlaygroundSurface(readyPort, options.surface, options.agent, options.open);
|
||||
const exitCode = await waitForChildExit(child);
|
||||
if (exitCode !== 0) {
|
||||
throw new BailianError(`Workbench exited with code ${exitCode}.`, ExitCode.GENERAL);
|
||||
}
|
||||
} finally {
|
||||
removeSignalForwarding();
|
||||
}
|
||||
}
|
||||
|
||||
export function playgroundBrowserTargetFromSummary(
|
||||
baseUrl: string,
|
||||
summary: PlaygroundProjectSummary,
|
||||
requestedAgent?: string,
|
||||
): PlaygroundBrowserTarget {
|
||||
if (summary.status !== "valid") return { url: baseUrl };
|
||||
const agentIds = (summary.agents ?? [])
|
||||
.map((entry) => entry.agent?.id?.trim())
|
||||
.filter((agentId): agentId is string => Boolean(agentId));
|
||||
const requested = requestedAgent?.trim();
|
||||
if (requested) {
|
||||
if (agentIds.includes(requested)) {
|
||||
return { url: `${baseUrl}/agents/${encodeURIComponent(requested)}/preview` };
|
||||
}
|
||||
return {
|
||||
url: baseUrl,
|
||||
warning: `Agent '${requested}' was not found. Opening the project Workbench instead.`,
|
||||
};
|
||||
}
|
||||
if (agentIds.length === 1) {
|
||||
return { url: `${baseUrl}/agents/${encodeURIComponent(agentIds[0]!)}/preview` };
|
||||
}
|
||||
if (agentIds.length > 1) {
|
||||
return {
|
||||
url: baseUrl,
|
||||
warning:
|
||||
"This project declares multiple Agents. Opening the Workbench; rerun with --agent <id> for Preview.",
|
||||
};
|
||||
}
|
||||
return { url: baseUrl };
|
||||
}
|
||||
|
||||
function assertSupportedNodeVersion(): void {
|
||||
const majorVersion = Number(process.versions.node.split(".")[0]);
|
||||
if (Number.isFinite(majorVersion) && majorVersion >= 22) return;
|
||||
throw new BailianError(
|
||||
"Managed Agent Workbench requires Node.js 22 or later.",
|
||||
ExitCode.USAGE,
|
||||
"Upgrade Node.js for Workbench; other Bailian CLI commands continue to support Node.js 18.17+.",
|
||||
);
|
||||
}
|
||||
|
||||
export async function resolveLauncher(): Promise<Launcher> {
|
||||
const explicit =
|
||||
process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN?.trim() ||
|
||||
process.env.AGENTS_PLAYGROUND_BIN?.trim();
|
||||
if (explicit) {
|
||||
if (!existsSync(explicit)) {
|
||||
throw new BailianError(
|
||||
`Configured Workbench binary does not exist: ${explicit}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
return { command: process.execPath, args: [explicit], 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,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: "npx",
|
||||
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);
|
||||
const packageJsonPath = require.resolve(`${PLAYGROUND_PACKAGE}/package.json`);
|
||||
const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
||||
version?: string;
|
||||
bin?: string | Record<string, string>;
|
||||
};
|
||||
const relativeBinary =
|
||||
typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.["agents-playground"];
|
||||
if (!relativeBinary) return undefined;
|
||||
const binaryPath = resolve(dirname(packageJsonPath), relativeBinary);
|
||||
if (!existsSync(binaryPath)) return undefined;
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: [binaryPath],
|
||||
version: manifest.version,
|
||||
fetched: false,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function findLocalPlaygroundBin(startDirectory: string): string | undefined {
|
||||
let directory = startDirectory;
|
||||
for (let depth = 0; depth < 10; depth += 1) {
|
||||
const candidate = resolve(directory, "packages/playground/dist/bin/playground.js");
|
||||
if (existsSync(candidate)) return candidate;
|
||||
const parent = dirname(directory);
|
||||
if (parent === directory) break;
|
||||
directory = parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildPlaygroundEnvironment(
|
||||
options: PlaygroundLaunchOptions,
|
||||
port: number,
|
||||
sourcePath: string,
|
||||
): NodeJS.ProcessEnv {
|
||||
const credential = options.client.exportApiCredential();
|
||||
const environment: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
AGENTS_PLAYGROUND_TOKEN: randomBytes(32).toString("hex"),
|
||||
};
|
||||
if (options.surface === "workbench") {
|
||||
delete environment.AGENTS_CONFIG_PATH;
|
||||
environment.AGENTS_PROJECT_ROOT = sourcePath;
|
||||
} else {
|
||||
delete environment.AGENTS_PROJECT_ROOT;
|
||||
environment.AGENTS_CONFIG_PATH = sourcePath;
|
||||
}
|
||||
if (credential) environment.DASHSCOPE_API_KEY = credential.token;
|
||||
const baseUrl = options.client.baseUrl.replace(/\/+$/, "");
|
||||
environment.BAILIAN_BASE_URL = baseUrl.endsWith("/api/v1/agentstudio")
|
||||
? baseUrl
|
||||
: `${baseUrl}/api/v1/agentstudio`;
|
||||
if (options.settings.workspaceId) {
|
||||
environment.BAILIAN_WORKSPACE_ID = options.settings.workspaceId;
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
async function waitForPlaygroundReady(
|
||||
child: ChildProcess,
|
||||
fallbackPort: number,
|
||||
timeoutMs: number,
|
||||
expectedProjectId: string,
|
||||
): Promise<number | null> {
|
||||
let port = fallbackPort;
|
||||
let outputBuffer = "";
|
||||
child.stdout?.on("data", (chunk: Buffer | string) => {
|
||||
process.stdout.write(chunk);
|
||||
outputBuffer += chunk.toString();
|
||||
const match = outputBuffer.match(PLAYGROUND_URL_PATTERN);
|
||||
if (match?.[1]) port = Number(match[1]);
|
||||
});
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() <= deadline) {
|
||||
if (child.exitCode !== null) return null;
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/health`, {
|
||||
signal: AbortSignal.timeout(1_000),
|
||||
});
|
||||
const body = response.ok
|
||||
? ((await response.json()) as { playground?: { project_id?: string } })
|
||||
: undefined;
|
||||
if (body?.playground?.project_id === expectedProjectId) return port;
|
||||
} catch {
|
||||
// Not ready yet.
|
||||
}
|
||||
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 300));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function probeExistingPlayground(port: number): Promise<ExistingPlayground | null> {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/health`, {
|
||||
signal: AbortSignal.timeout(2_000),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const body = (await response.json()) as {
|
||||
playground?: { version?: string; pid?: number; project_id?: string };
|
||||
};
|
||||
if (!body.playground?.pid) return null;
|
||||
return {
|
||||
version: body.playground.version ?? "unknown",
|
||||
pid: body.playground.pid,
|
||||
projectId: body.playground.project_id,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceExistingPlayground(
|
||||
existing: ExistingPlayground,
|
||||
port: number,
|
||||
): Promise<boolean> {
|
||||
emitBare(`Replacing Workbench v${existing.version} (pid ${existing.pid}) on port ${port}...`);
|
||||
try {
|
||||
process.kill(existing.pid, "SIGTERM");
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 100));
|
||||
if (!(await probeExistingPlayground(port))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function openPlaygroundSurface(
|
||||
port: number,
|
||||
surface: "preview" | "workbench",
|
||||
requestedAgent: string | undefined,
|
||||
shouldOpen: boolean,
|
||||
): Promise<void> {
|
||||
if (!shouldOpen) return;
|
||||
const target =
|
||||
surface === "workbench"
|
||||
? { url: `http://localhost:${port}` }
|
||||
: await resolvePlaygroundBrowserTarget(port, requestedAgent);
|
||||
if (target.warning) emitBare(`Warning: ${target.warning}`);
|
||||
openBrowser(target.url);
|
||||
}
|
||||
|
||||
async function resolvePlaygroundBrowserTarget(
|
||||
port: number,
|
||||
requestedAgent?: string,
|
||||
): Promise<PlaygroundBrowserTarget> {
|
||||
const baseUrl = `http://localhost:${port}`;
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/project`, {
|
||||
signal: AbortSignal.timeout(3_000),
|
||||
});
|
||||
if (!response.ok) return { url: baseUrl };
|
||||
return playgroundBrowserTargetFromSummary(
|
||||
baseUrl,
|
||||
(await response.json()) as PlaygroundProjectSummary,
|
||||
requestedAgent,
|
||||
);
|
||||
} catch {
|
||||
return { url: baseUrl };
|
||||
}
|
||||
}
|
||||
|
||||
function openBrowser(url: string): void {
|
||||
const command =
|
||||
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
||||
const args = process.platform === "win32" ? ["", url] : [url];
|
||||
try {
|
||||
spawn(command, args, {
|
||||
stdio: "ignore",
|
||||
detached: true,
|
||||
shell: process.platform === "win32",
|
||||
}).unref();
|
||||
} catch {
|
||||
emitBare(`Could not open a browser automatically. Visit ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
function forwardSignals(child: ChildProcess): () => void {
|
||||
const forwardInterrupt = () => child.kill("SIGINT");
|
||||
const forwardTerminate = () => child.kill("SIGTERM");
|
||||
process.on("SIGINT", forwardInterrupt);
|
||||
process.on("SIGTERM", forwardTerminate);
|
||||
return () => {
|
||||
process.off("SIGINT", forwardInterrupt);
|
||||
process.off("SIGTERM", forwardTerminate);
|
||||
};
|
||||
}
|
||||
|
||||
function waitForChildExit(child: ChildProcess): Promise<number> {
|
||||
if (child.exitCode !== null) return Promise.resolve(child.exitCode);
|
||||
return new Promise((resolveExit, rejectExit) => {
|
||||
child.once("exit", (exitCode) => resolveExit(exitCode ?? 0));
|
||||
child.once("error", rejectExit);
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
BailianError,
|
||||
defineCommand,
|
||||
@@ -12,6 +13,7 @@ import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
const GITIGNORE_ADDITIONS = `
|
||||
# agents
|
||||
agents.state.json
|
||||
.openagentpack/versions/
|
||||
.env
|
||||
`;
|
||||
|
||||
@@ -71,8 +73,8 @@ const INIT_FLAGS = {
|
||||
|
||||
export default defineCommand({
|
||||
description: {
|
||||
"en-US": "Create a new agents.yaml template",
|
||||
"zh-CN": "创建新的 agents.yaml 模板",
|
||||
"en-US": "Create an agents.yaml template",
|
||||
"zh-CN": "创建 agents.yaml 模板",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "[--agent-name <name>] [--file <path>] [--force]",
|
||||
@@ -86,7 +88,7 @@ export default defineCommand({
|
||||
|
||||
if (existsSync(file) && !flags.force) {
|
||||
throw new BailianError(
|
||||
`${file} already exists.`,
|
||||
`${resolve(file)} already exists.`,
|
||||
ExitCode.USAGE,
|
||||
"Pass --force to overwrite.",
|
||||
);
|
||||
@@ -127,7 +129,7 @@ export default defineCommand({
|
||||
if (format === "json") {
|
||||
emitResult({ created: file, provider: "bailian", agent: agentName }, format);
|
||||
} else {
|
||||
emitBare(`Created ${file}`);
|
||||
emitBare(`Created ${resolve(file)}`);
|
||||
emitBare(
|
||||
"Credentials: run `bl auth login --api-key <key> --base-url <url>`, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.",
|
||||
);
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
BailianError,
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import {
|
||||
commitProjectBuild,
|
||||
createDirectoryWorkspaceVersionService,
|
||||
executeProjectPublish,
|
||||
initializeDirectoryProject,
|
||||
planProjectPublish,
|
||||
previewProjectBuild,
|
||||
type ProjectBuildResolver,
|
||||
resolveDirectoryProjectRoot,
|
||||
validateDirectoryProject,
|
||||
} from "@openagentpack/sdk/project-workspace";
|
||||
import { CREDENTIALS_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts";
|
||||
import { withStdoutProtected } from "./_engine/console-capture.ts";
|
||||
import { withAgentErrors } from "./_engine/errors.ts";
|
||||
import { renderAgentFeedback } from "./_engine/feedback.ts";
|
||||
import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts";
|
||||
import { installSdkTransport } from "./_engine/transport.ts";
|
||||
import { formatResourceLabel } from "./_engine/address-utils.ts";
|
||||
|
||||
const PROJECT_FLAG = {
|
||||
project: {
|
||||
type: "string",
|
||||
valueHint: "<directory>",
|
||||
description: {
|
||||
"en-US": "Directory project root (default: current directory)",
|
||||
"zh-CN": "目录项目根路径(默认:当前目录)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const JSON_FORMAT = "json" as const;
|
||||
|
||||
export const managedAgentProjectInit = defineCommand({
|
||||
description: {
|
||||
"en-US": "Create a directory project or convert the local agents.yaml",
|
||||
"zh-CN": "创建目录项目,或转换当前 agents.yaml",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "[--project <directory>]",
|
||||
flags: {
|
||||
project: {
|
||||
...PROJECT_FLAG.project,
|
||||
description: {
|
||||
"en-US": "Directory project root (default: ./managed-agent under the current directory)",
|
||||
"zh-CN": "目录项目根路径(默认:当前目录下的 ./managed-agent)",
|
||||
},
|
||||
},
|
||||
},
|
||||
exampleArgs: ["", "--project ./my-agent", "--project ."],
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"Without --project, creates a managed-agent/ subdirectory. Enter it before running other project commands. Use --project . to initialize in place or convert the current agents.yaml; existing project files are not overwritten.",
|
||||
"zh-CN":
|
||||
"不传 --project 时创建 managed-agent/ 子目录;后续项目操作请先进入该目录。使用 --project . 可在当前目录初始化或转换 agents.yaml;不会覆盖已有项目文件。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"New projects include Skill, File, Vault, and Environment examples under each resource directory's _examples/. They are not referenced by agent.json and are excluded from Build/Publish. Copy an example outside _examples/ to enable it, then configure its Agent reference.",
|
||||
"zh-CN":
|
||||
"新项目在四类资源目录的 _examples/ 下生成配置示例,不写入 agent.json 引用,也不参与 Build/Publish。需要使用时,将示例复制到 _examples/ 外,再配置 Agent 引用。",
|
||||
},
|
||||
],
|
||||
async run(ctx) {
|
||||
const projectRoot = ctx.flags.project ?? "./managed-agent";
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
would_initialize_project: projectRoot,
|
||||
},
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = await initializeDirectoryProject({ projectRoot });
|
||||
emitResult(result, detectOutputFormat(ctx.settings.output));
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectValidate = defineCommand({
|
||||
description: { "en-US": "Validate a directory Agent project", "zh-CN": "校验目录式 Agent 项目" },
|
||||
auth: "none",
|
||||
usageArgs: "[--project <directory>]",
|
||||
flags: PROJECT_FLAG,
|
||||
exampleArgs: ["", "--project ./my-agent"],
|
||||
async run(ctx) {
|
||||
const result = await validateDirectoryProject(ctx.flags.project ?? ".");
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (format === JSON_FORMAT) emitResult(safeInspection(result), format);
|
||||
else {
|
||||
for (const diagnostic of [...result.diagnostics, ...result.warnings]) {
|
||||
emitBare(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`);
|
||||
}
|
||||
if (!result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
||||
emitBare(`Project is valid (${result.project_revision.slice(0, 12)}).`);
|
||||
}
|
||||
}
|
||||
if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
||||
throw new BailianError("Directory project validation failed.", ExitCode.GENERAL);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectBuild = defineCommand({
|
||||
description: {
|
||||
"en-US": "Organize directory source and generate the immutable Publish Build",
|
||||
"zh-CN": "整理目录源文件并生成不可变的发布 Build",
|
||||
},
|
||||
auth: "none",
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"Build writes local project files without confirmation, including inferred resource associations and migration of plaintext Vault secrets into .env. Use --dry-run to preview without writing. Publish still requires explicit confirmation before remote changes.",
|
||||
"zh-CN":
|
||||
"Build 无需确认即可写入本地项目文件,包括推断的资源关联及将 Vault 明文密钥移入 .env。使用 --dry-run 可只预览不写入。Publish 变更远端资源前仍需显式确认。",
|
||||
},
|
||||
],
|
||||
usageArgs: "[--project <directory>]",
|
||||
flags: PROJECT_FLAG,
|
||||
exampleArgs: ["", "--dry-run", "--project ./my-agent"],
|
||||
async validate(flags) {
|
||||
await withAgentErrors(async () => {
|
||||
const root = await resolveDirectoryProjectRoot(flags.project ?? ".");
|
||||
const metadata = await stat(join(root, "project.json")).catch((error: unknown) => {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
||||
throw error;
|
||||
});
|
||||
if (!metadata?.isFile()) {
|
||||
throw new BailianError(
|
||||
`Not a project root: ${root} (project.json is missing).`,
|
||||
ExitCode.USAGE,
|
||||
"Run from the directory containing project.json, or pass --project <directory>.",
|
||||
);
|
||||
}
|
||||
});
|
||||
return undefined;
|
||||
},
|
||||
async run(ctx) {
|
||||
const root = ctx.flags.project ?? ".";
|
||||
const preview = await previewProjectBuild(root);
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(safeInspection(preview), format);
|
||||
return;
|
||||
}
|
||||
if (!preview.can_build)
|
||||
throw new BailianError(
|
||||
preview.diagnostics.find((diagnostic) => diagnostic.severity === "error")?.message ??
|
||||
"Directory project is invalid and cannot be built.",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
const built = await commitProjectBuild({
|
||||
projectRoot: root,
|
||||
baseRevision: preview.project_revision,
|
||||
});
|
||||
emitResult(
|
||||
{ manifest: built.manifest, organization_moves: preview.organization_moves },
|
||||
format,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectPublish = defineCommand({
|
||||
description: {
|
||||
"en-US": "Publish the current directory-project Build and record a version",
|
||||
"zh-CN": "发布当前目录项目 Build 并记录版本",
|
||||
},
|
||||
auth: "apiKey",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This publishes the current directory-project Build and may create, update, or delete remote managed Agent resources.",
|
||||
"zh-CN": "该操作会发布当前目录项目 Build,可能创建、更新或删除远端托管 Agent 资源。",
|
||||
},
|
||||
},
|
||||
usageArgs: "[--project <directory>] [--no-refresh] [--concurrency <n>]",
|
||||
flags: {
|
||||
...PROJECT_FLAG,
|
||||
noRefresh: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Skip remote refresh before planning",
|
||||
"zh-CN": "规划前跳过远端刷新",
|
||||
},
|
||||
},
|
||||
concurrency: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: {
|
||||
"en-US": "Maximum parallel resource operations",
|
||||
"zh-CN": "最大并行资源操作数",
|
||||
},
|
||||
},
|
||||
},
|
||||
exampleArgs: ["--yes", "--project ./my-agent --yes"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
installSdkTransport(ctx);
|
||||
const root = ctx.flags.project ?? ".";
|
||||
const resolveBuild: ProjectBuildResolver = async (buildPath, options) =>
|
||||
(await resolveAgentProjectConfig(ctx, buildPath, {
|
||||
environment: options?.environment,
|
||||
overrideBailianBaseUrl: true,
|
||||
})) as unknown as Awaited<ReturnType<ProjectBuildResolver>>;
|
||||
const planned = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
planProjectPublish(root, {
|
||||
provider: "bailian",
|
||||
refresh: !ctx.flags.noRefresh,
|
||||
quiet: true,
|
||||
onFeedback: renderAgentFeedback,
|
||||
resolveBuild,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const actions = planned.planned.plan.actions.filter((action) => action.action !== "no-op");
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
project_revision: planned.project_revision,
|
||||
build_manifest: planned.build_manifest,
|
||||
plan: planned.planned.plan,
|
||||
},
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const action of actions) {
|
||||
const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
|
||||
emitBare(`${marker} ${formatResourceLabel(action.address)}`);
|
||||
}
|
||||
const result = await withAgentErrors(() =>
|
||||
withStdoutProtected(() =>
|
||||
executeProjectPublish({
|
||||
projectRoot: planned.project_root,
|
||||
expectedProjectRevision: planned.project_revision,
|
||||
expectedYamlHash: planned.build_manifest.yaml_hash,
|
||||
expectedPlanFingerprint: planned.plan_fingerprint,
|
||||
provider: "bailian",
|
||||
refresh: !ctx.flags.noRefresh,
|
||||
concurrency: ctx.flags.concurrency,
|
||||
policy: "force",
|
||||
onFeedback: renderAgentFeedback,
|
||||
resolveBuild,
|
||||
}),
|
||||
),
|
||||
);
|
||||
emitResult(result, detectOutputFormat(ctx.settings.output));
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectWorkbench = defineCommand({
|
||||
description: {
|
||||
"en-US": "Launch the directory project Workbench",
|
||||
"zh-CN": "启动目录项目 Workbench",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--project <directory>] [--port <n>] [--no-open]",
|
||||
flags: {
|
||||
...PROJECT_FLAG,
|
||||
port: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: { "en-US": "Local port (default: 4848)", "zh-CN": "本地端口(默认:4848)" },
|
||||
},
|
||||
noOpen: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Do not open a browser", "zh-CN": "不自动打开浏览器" },
|
||||
},
|
||||
},
|
||||
exampleArgs: ["", "--project ./my-agent --no-open"],
|
||||
notes: CREDENTIALS_NOTE,
|
||||
async run(ctx) {
|
||||
const root = ctx.flags.project ?? ".";
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{ would_launch: "workbench", project_root: root, port: ctx.flags.port ?? 4848 },
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await launchManagedAgentPlayground({
|
||||
project: root,
|
||||
port: ctx.flags.port ?? 4848,
|
||||
open: !ctx.flags.noOpen,
|
||||
surface: "workbench",
|
||||
client: ctx.client,
|
||||
settings: ctx.settings,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectVersionEnable = versionToggleCommand(true);
|
||||
export const managedAgentProjectVersionDisable = versionToggleCommand(false);
|
||||
|
||||
function versionToggleCommand(enabled: boolean) {
|
||||
return defineCommand({
|
||||
description: enabled
|
||||
? { "en-US": "Enable directory project versions", "zh-CN": "启用目录项目版本管理" }
|
||||
: { "en-US": "Disable directory project versions", "zh-CN": "关闭目录项目版本管理" },
|
||||
auth: "none",
|
||||
usageArgs: "[--project <directory>]",
|
||||
flags: PROJECT_FLAG,
|
||||
exampleArgs: ["", "--project ./my-agent"],
|
||||
async run(ctx) {
|
||||
const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".");
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{ would_set_enabled: enabled, status: await service.status() },
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = enabled
|
||||
? await service.enable("Enable project versions")
|
||||
: await service.disable();
|
||||
emitResult(result, detectOutputFormat(ctx.settings.output));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const managedAgentProjectVersionStatus = defineCommand({
|
||||
description: {
|
||||
"en-US": "Show directory project version status",
|
||||
"zh-CN": "显示目录项目版本状态",
|
||||
},
|
||||
auth: "none",
|
||||
usageArgs: "[--project <directory>]",
|
||||
flags: PROJECT_FLAG,
|
||||
exampleArgs: ["", "--project ./my-agent --output json"],
|
||||
async run(ctx) {
|
||||
emitResult(
|
||||
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").status(),
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectVersionList = defineCommand({
|
||||
description: { "en-US": "List directory project versions", "zh-CN": "列出目录项目版本" },
|
||||
auth: "none",
|
||||
usageArgs: "[--project <directory>] [--limit <n>] [--cursor <cursor>]",
|
||||
flags: {
|
||||
...PROJECT_FLAG,
|
||||
limit: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: { "en-US": "Maximum versions to return", "zh-CN": "最多返回的版本数" },
|
||||
},
|
||||
cursor: {
|
||||
type: "string",
|
||||
valueHint: "<cursor>",
|
||||
description: { "en-US": "Pagination cursor", "zh-CN": "分页游标" },
|
||||
},
|
||||
},
|
||||
exampleArgs: ["", "--limit 20 --output json"],
|
||||
async run(ctx) {
|
||||
emitResult(
|
||||
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").listVersions({
|
||||
limit: ctx.flags.limit,
|
||||
cursor: ctx.flags.cursor,
|
||||
}),
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const VERSION_ID_FLAG = {
|
||||
versionId: {
|
||||
type: "string",
|
||||
valueHint: "<full-version>",
|
||||
required: true,
|
||||
description: { "en-US": "Full project version ID", "zh-CN": "完整的项目版本 ID" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export const managedAgentProjectVersionPreview = defineCommand({
|
||||
description: { "en-US": "Preview a directory project version", "zh-CN": "预览目录项目历史版本" },
|
||||
auth: "none",
|
||||
usageArgs: "--version-id <full-version> [--project <directory>]",
|
||||
flags: { ...PROJECT_FLAG, ...VERSION_ID_FLAG },
|
||||
exampleArgs: ["--version-id <full-version>"],
|
||||
async run(ctx) {
|
||||
emitResult(
|
||||
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").previewVersion(
|
||||
ctx.flags.versionId,
|
||||
),
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const managedAgentProjectVersionRestore = defineCommand({
|
||||
description: {
|
||||
"en-US": "Restore a version to the project working directory",
|
||||
"zh-CN": "将历史版本恢复到项目工作目录",
|
||||
},
|
||||
auth: "none",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US":
|
||||
"This restores the full directory source to the working tree. Version history and remote State will not move.",
|
||||
"zh-CN": "该操作会将完整目录源文件恢复到工作区;版本历史和远端 State 不会移动。",
|
||||
},
|
||||
},
|
||||
usageArgs: "--version-id <full-version> [--project <directory>]",
|
||||
flags: { ...PROJECT_FLAG, ...VERSION_ID_FLAG },
|
||||
exampleArgs: ["--version-id <full-version>", "--version-id <full-version> --yes"],
|
||||
async run(ctx) {
|
||||
const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".");
|
||||
const preview = await service.previewVersion(ctx.flags.versionId);
|
||||
if (!preview.can_restore)
|
||||
throw new BailianError(
|
||||
preview.blockers[0] ?? preview.diagnostics[0]?.message ?? "Version cannot be restored.",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{ would_restore: ctx.flags.versionId, preview },
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const restored = await service.restoreVersion(ctx.flags.versionId, {
|
||||
headVersion: preview.base_head_version,
|
||||
projectRevision: preview.base_project_revision,
|
||||
});
|
||||
emitResult(restored, detectOutputFormat(ctx.settings.output));
|
||||
},
|
||||
});
|
||||
|
||||
function safeInspection(result: {
|
||||
project_root: string;
|
||||
project_revision: string;
|
||||
source_manifest_hash: string;
|
||||
yaml_hash: string;
|
||||
diagnostics: unknown[];
|
||||
warnings: unknown[];
|
||||
organization_moves: unknown[];
|
||||
canonical_yaml: string;
|
||||
before_yaml?: string;
|
||||
can_build?: boolean;
|
||||
}) {
|
||||
return {
|
||||
project_root: result.project_root,
|
||||
project_revision: result.project_revision,
|
||||
source_manifest_hash: result.source_manifest_hash,
|
||||
yaml_hash: result.yaml_hash,
|
||||
diagnostics: result.diagnostics,
|
||||
warnings: result.warnings,
|
||||
organization_moves: result.organization_moves,
|
||||
before_yaml: "before_yaml" in result ? result.before_yaml : undefined,
|
||||
after_yaml: result.canonical_yaml,
|
||||
can_build: "can_build" in result ? result.can_build : undefined,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
|
||||
import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts";
|
||||
|
||||
const PLAYGROUND_BASE_FLAGS = {
|
||||
file: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
description: {
|
||||
"en-US": "Config file path (default: agents.yaml)",
|
||||
"zh-CN": "配置文件路径(默认:agents.yaml)",
|
||||
},
|
||||
},
|
||||
port: {
|
||||
type: "number",
|
||||
valueHint: "<n>",
|
||||
description: {
|
||||
"en-US": "Local port (default: 4848)",
|
||||
"zh-CN": "本地端口(默认:4848)",
|
||||
},
|
||||
},
|
||||
noOpen: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Do not open a browser automatically",
|
||||
"zh-CN": "不自动打开浏览器",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const PLAYGROUND_FLAGS = {
|
||||
...PLAYGROUND_BASE_FLAGS,
|
||||
agent: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: {
|
||||
"en-US": "Agent to preview (required when the project declares multiple Agents)",
|
||||
"zh-CN": "要预览的 Agent(项目包含多个 Agent 时需要指定)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
const PLAYGROUND_NOTES = [
|
||||
...CREDENTIALS_NOTE,
|
||||
{
|
||||
"en-US":
|
||||
"Session Preview requires Node.js 22+ and keeps using an agents.yaml source. Directory Workbench is available under managed-agent project workbench.",
|
||||
"zh-CN":
|
||||
"会话预览需要 Node.js 22+,并继续使用 agents.yaml;目录 Workbench 位于 managed-agent project workbench。",
|
||||
},
|
||||
];
|
||||
|
||||
export const managedAgentPlayground = defineCommand({
|
||||
description: {
|
||||
"en-US": "Launch a Session Preview for an agents.yaml Agent",
|
||||
"zh-CN": "为 agents.yaml 中的 Agent 启动会话预览",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "[--file <path>] [--agent <id>] [--port <n>] [--no-open]",
|
||||
flags: PLAYGROUND_FLAGS,
|
||||
exampleArgs: ["", "--agent assistant", "--file agents.yaml --no-open"],
|
||||
notes: PLAYGROUND_NOTES,
|
||||
async run(ctx) {
|
||||
const file = ctx.flags.file ?? "agents.yaml";
|
||||
const port = ctx.flags.port ?? 4848;
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
would_launch: "playground",
|
||||
config_file: file,
|
||||
agent: ctx.flags.agent,
|
||||
port,
|
||||
open_browser: !ctx.flags.noOpen,
|
||||
},
|
||||
detectOutputFormat(ctx.settings.output),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await launchManagedAgentPlayground({
|
||||
file,
|
||||
agent: ctx.flags.agent,
|
||||
port,
|
||||
open: !ctx.flags.noOpen,
|
||||
surface: "preview",
|
||||
client: ctx.client,
|
||||
settings: ctx.settings,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -137,6 +137,20 @@ export { default as managedAgentValidate } from "./commands/managed-agent/valida
|
||||
export { default as managedAgentPlan } from "./commands/managed-agent/plan.ts";
|
||||
export { default as managedAgentApply } from "./commands/managed-agent/apply.ts";
|
||||
export { default as managedAgentDestroy } from "./commands/managed-agent/destroy.ts";
|
||||
export { managedAgentPlayground } from "./commands/managed-agent/workbench.ts";
|
||||
export {
|
||||
managedAgentProjectBuild,
|
||||
managedAgentProjectInit,
|
||||
managedAgentProjectPublish,
|
||||
managedAgentProjectValidate,
|
||||
managedAgentProjectVersionDisable,
|
||||
managedAgentProjectVersionEnable,
|
||||
managedAgentProjectVersionList,
|
||||
managedAgentProjectVersionPreview,
|
||||
managedAgentProjectVersionRestore,
|
||||
managedAgentProjectVersionStatus,
|
||||
managedAgentProjectWorkbench,
|
||||
} from "./commands/managed-agent/project.ts";
|
||||
export { default as managedAgentStateList } from "./commands/managed-agent/state-list.ts";
|
||||
export { default as managedAgentStateShow } from "./commands/managed-agent/state-show.ts";
|
||||
export { default as managedAgentStateRm } from "./commands/managed-agent/state-rm.ts";
|
||||
|
||||
@@ -104,6 +104,20 @@ test("inject:base_url 已带后缀不重复拼;非空字面量 base_url 保留",
|
||||
expect(literal.bailian.base_url).toBe("https://custom.example.com/api/v1/agentstudio");
|
||||
});
|
||||
|
||||
test("inject:目录项目可强制使用 Bailian CLI 解析出的 base_url", () => {
|
||||
const providers = {
|
||||
bailian: {
|
||||
api_key: "from-project-env",
|
||||
base_url: "https://project-env.example.com/api/v1/agentstudio",
|
||||
},
|
||||
};
|
||||
injectProviderCredentials(providers, makeHost({ apiCred: bailianCred() }), {
|
||||
overrideBaseUrl: true,
|
||||
});
|
||||
expect(providers.bailian.api_key).toBe("sk-auth-chain");
|
||||
expect(providers.bailian.base_url).toBe("https://dashscope.aliyuncs.com/api/v1/agentstudio");
|
||||
});
|
||||
|
||||
test("inject:base_url 尾斜杠被规范化,不产生双斜杠", () => {
|
||||
const providers = { bailian: { api_key: "", base_url: "" } };
|
||||
injectProviderCredentials(
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { runNodeMain } from "e2e/runner";
|
||||
import { afterEach, describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson } from "./helpers.ts";
|
||||
|
||||
const directories: string[] = [];
|
||||
|
||||
async function temporaryDirectory() {
|
||||
const directory = await realpath(await mkdtemp(join(tmpdir(), "bailian-yaml-init-")));
|
||||
directories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
for (const directory of directories.splice(0)) {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
async function runInit(directory: string, args: string[] = []) {
|
||||
return runNodeMain(
|
||||
fileURLToPath(new URL("./harness/main.ts", import.meta.url)),
|
||||
["managed-agent", "init", ...args],
|
||||
{
|
||||
cwd: directory,
|
||||
env: {
|
||||
BAILIAN_CONFIG_DIR: await temporaryDirectory(),
|
||||
BAILIAN_E2E_ROUTES: JSON.stringify([
|
||||
{ path: "managed-agent init", export: "managedAgentInit" },
|
||||
]),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
describe("e2e: managed-agent init output path", () => {
|
||||
test.each(["default", "relative", "absolute"] as const)(
|
||||
"reports the absolute YAML path for a %s output path",
|
||||
async (pathKind) => {
|
||||
const directory = await temporaryDirectory();
|
||||
const relativePath =
|
||||
pathKind === "default" ? "agents.yaml" : join("config files", "custom agents.yaml");
|
||||
const outputPath = join(directory, relativePath);
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
const args =
|
||||
pathKind === "default"
|
||||
? []
|
||||
: ["--file", pathKind === "absolute" ? outputPath : relativePath];
|
||||
|
||||
const result = await runInit(directory, args);
|
||||
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(result.stdout.split(/\r?\n/)[0]).toBe(`Created ${outputPath}`);
|
||||
expect(await readFile(outputPath, "utf8")).toContain("agents:");
|
||||
},
|
||||
);
|
||||
|
||||
test("preserves the JSON output contract", async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const result = await runInit(directory, ["--file", "custom.yaml", "--output", "json"]);
|
||||
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(parseStdoutJson(result.stdout)).toEqual({
|
||||
created: "custom.yaml",
|
||||
provider: "bailian",
|
||||
agent: "assistant",
|
||||
});
|
||||
expect((await stat(join(directory, "custom.yaml"))).isFile()).toBe(true);
|
||||
});
|
||||
|
||||
test("keeps dry-run read-only without reporting a created file", async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const result = await runInit(directory, ["--dry-run", "--output", "json"]);
|
||||
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(parseStdoutJson(result.stdout)).toEqual({
|
||||
would_create: "agents.yaml",
|
||||
provider: "bailian",
|
||||
agent: "assistant",
|
||||
would_update_gitignore: true,
|
||||
});
|
||||
expect(await stat(join(directory, "agents.yaml")).catch(() => null)).toBeNull();
|
||||
expect(await stat(join(directory, ".gitignore")).catch(() => null)).toBeNull();
|
||||
});
|
||||
|
||||
test.each(["default", "relative", "absolute"] as const)(
|
||||
"reports the absolute existing %s path without overwriting the YAML",
|
||||
async (pathKind) => {
|
||||
const directory = await temporaryDirectory();
|
||||
const relativePath =
|
||||
pathKind === "default" ? "agents.yaml" : join("config files", "custom agents.yaml");
|
||||
const outputPath = join(directory, relativePath);
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, "Keep user configuration.\n");
|
||||
const args =
|
||||
pathKind === "default"
|
||||
? []
|
||||
: ["--file", pathKind === "absolute" ? outputPath : relativePath];
|
||||
|
||||
const result = await runInit(directory, args);
|
||||
|
||||
expect(result.exitCode).toBe(2);
|
||||
expect(result.stderr).toContain(`${outputPath} already exists.`);
|
||||
expect(result.stderr).toContain("Pass --force to overwrite.");
|
||||
expect(result.stdout).not.toContain("Created ");
|
||||
expect(await readFile(outputPath, "utf8")).toBe("Keep user configuration.\n");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,247 @@
|
||||
import {
|
||||
cp,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
realpath,
|
||||
rm,
|
||||
stat,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { runNodeMain } from "e2e/runner";
|
||||
import { afterEach, describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandHelp } from "./helpers.ts";
|
||||
|
||||
const routes = {
|
||||
"managed-agent project init": "managedAgentProjectInit",
|
||||
"managed-agent project build": "managedAgentProjectBuild",
|
||||
"managed-agent project publish": "managedAgentProjectPublish",
|
||||
"managed-agent project validate": "managedAgentProjectValidate",
|
||||
} as const;
|
||||
const directories: string[] = [];
|
||||
|
||||
async function temporaryDirectory() {
|
||||
const directory = await realpath(await mkdtemp(join(tmpdir(), "bailian-project-init-")));
|
||||
directories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
for (const directory of directories.splice(0)) {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function runInit(directory: string, args: string[] = []) {
|
||||
return runProject(directory, "init", args);
|
||||
}
|
||||
|
||||
async function runProject(directory: string, subcommand: string, args: string[] = [], json = true) {
|
||||
const configRoot = await temporaryDirectory();
|
||||
return runNodeMain(
|
||||
fileURLToPath(new URL("./harness/main.ts", import.meta.url)),
|
||||
["managed-agent", "project", subcommand, ...args, ...(json ? ["--output", "json"] : [])],
|
||||
{
|
||||
cwd: directory,
|
||||
env: {
|
||||
BAILIAN_CONFIG_DIR: configRoot,
|
||||
BAILIAN_E2E_ROUTES: JSON.stringify(
|
||||
Object.entries(routes).map(([path, exportName]) => ({ path, export: exportName })),
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
describe("e2e: managed-agent project init directory defaults", () => {
|
||||
test("build links copied resources and reports ambiguous environment bindings without writing", async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const initialized = await runInit(directory);
|
||||
expect(initialized.exitCode, initialized.stderr).toBe(0);
|
||||
const root = join(directory, "managed-agent");
|
||||
for (const [section, id] of [
|
||||
["skills", "example-skill"],
|
||||
["files", "example-file"],
|
||||
["environments", "example-env"],
|
||||
["vaults", "example-vault"],
|
||||
] as const) {
|
||||
await cp(
|
||||
join(root, "agents/assistant", section, "_examples", id),
|
||||
join(root, "agents/assistant", section, id),
|
||||
{ recursive: true },
|
||||
);
|
||||
}
|
||||
const agentPath = join(root, "agents/assistant/agent.json");
|
||||
const original = await readFile(agentPath, "utf8");
|
||||
const preview = await runProject(root, "build", ["--dry-run"]);
|
||||
expect(preview.exitCode, preview.stderr).toBe(0);
|
||||
expect(parseStdoutJson<{ can_build: boolean }>(preview.stdout).can_build).toBe(true);
|
||||
expect(await readFile(agentPath, "utf8")).toBe(original);
|
||||
const built = await runProject(root, "build");
|
||||
expect(built.exitCode, built.stderr).toBe(0);
|
||||
const agent = JSON.parse(await readFile(agentPath, "utf8"));
|
||||
expect(agent).toMatchObject({
|
||||
environment: "example-env",
|
||||
vault: "example-vault",
|
||||
skills: ["example-skill"],
|
||||
files: [{ file: "example-file", mount_path: "/mnt/example.md" }],
|
||||
});
|
||||
const alternatePath = join(root, "agents/assistant/environments/alternate");
|
||||
await mkdir(alternatePath);
|
||||
await writeFile(
|
||||
join(alternatePath, "environment.json"),
|
||||
JSON.stringify({ id: "alternate", config: { type: "cloud" } }),
|
||||
);
|
||||
delete agent.environment;
|
||||
await writeFile(agentPath, JSON.stringify(agent));
|
||||
const beforeConflict = await readFile(agentPath, "utf8");
|
||||
const buildPath = join(root, ".openagentpack/build/agents.yaml");
|
||||
const beforeBuild = await readFile(buildPath, "utf8");
|
||||
for (const json of [false, true]) {
|
||||
const conflict = await runProject(root, "build", [], json);
|
||||
expect(conflict.exitCode).toBe(1);
|
||||
expect(conflict.stderr).toContain("multiple local environment resources");
|
||||
expect(conflict.stderr).toContain("Set 'environment' explicitly");
|
||||
expect(conflict.stderr).not.toMatch(/\p{Script=Han}/u);
|
||||
}
|
||||
expect(await readFile(agentPath, "utf8")).toBe(beforeConflict);
|
||||
expect(await readFile(buildPath, "utf8")).toBe(beforeBuild);
|
||||
});
|
||||
|
||||
test("build checks directories and writes without confirmation while publish stays gated", async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const initialized = await runInit(directory);
|
||||
expect(initialized.exitCode, initialized.stderr).toBe(0);
|
||||
const root = join(directory, "managed-agent");
|
||||
const nested = join(root, "agents/assistant/skills");
|
||||
for (const args of [[], ["--dry-run"]]) {
|
||||
const result = await runProject(nested, "build", args, false);
|
||||
expect(result.exitCode, result.stderr).toBe(2);
|
||||
expect(result.stderr).toContain("Not a project root:");
|
||||
expect(result.stderr).not.toMatch(/\p{Script=Han}/u);
|
||||
expect(result.stderr).toContain(`cd '${root}'`);
|
||||
expect(result.stderr).not.toContain("high-risk");
|
||||
expect(result.stderr).not.toContain("Usage:");
|
||||
}
|
||||
const explicit = await runProject(root, "build", ["--project", nested]);
|
||||
expect(explicit.exitCode).toBe(2);
|
||||
expect(explicit.stderr).toContain(`--project '${root}'`);
|
||||
const noMarker = await runProject(directory, "build", [], false);
|
||||
expect(noMarker.exitCode).toBe(2);
|
||||
expect(noMarker.stderr).toContain("project.json");
|
||||
expect(noMarker.stderr).not.toMatch(/\p{Script=Han}/u);
|
||||
expect(noMarker.stderr).not.toContain("high-risk");
|
||||
expect(await stat(join(nested, ".openagentpack")).catch(() => null)).toBeNull();
|
||||
|
||||
const preview = await runProject(root, "build", ["--dry-run"]);
|
||||
expect(preview.exitCode, preview.stderr).toBe(0);
|
||||
expect(await stat(join(root, ".openagentpack/build")).catch(() => null)).toBeNull();
|
||||
const built = await runProject(root, "build");
|
||||
expect(built.exitCode, built.stderr).toBe(0);
|
||||
expect(built.stderr).not.toContain("requires_confirmation");
|
||||
expect((await stat(join(root, ".openagentpack/build/agents.yaml"))).isFile()).toBe(true);
|
||||
const explicitValid = await runProject(nested, "build", ["--project", root]);
|
||||
expect(explicitValid.exitCode, explicitValid.stderr).toBe(0);
|
||||
const storePath = join(root, ".openagentpack/versions/project/store.json");
|
||||
const beforePublish = await readFile(storePath, "utf8");
|
||||
const publish = await runProject(root, "publish");
|
||||
expect(publish.exitCode).toBe(7);
|
||||
expect(publish.stderr).toContain("requires_confirmation");
|
||||
expect(await readFile(storePath, "utf8")).toBe(beforePublish);
|
||||
});
|
||||
|
||||
test("only publish help includes confirmation; build keeps dry-run", async () => {
|
||||
const build = await runCommandHelp(routes, ["managed-agent", "project", "build", "--help"]);
|
||||
expect(build.stderr).not.toContain("--yes");
|
||||
expect(build.stderr).toContain("--dry-run");
|
||||
const publish = await runCommandHelp(routes, ["managed-agent", "project", "publish", "--help"]);
|
||||
expect(publish.stderr).toContain("--yes");
|
||||
const directory = await temporaryDirectory();
|
||||
const removedFlag = await runProject(directory, "build", ["--yes"]);
|
||||
expect(removedFlag.exitCode).toBe(2);
|
||||
expect(removedFlag.stderr).toMatch(/Unknown flag.*--yes/);
|
||||
});
|
||||
|
||||
test("nested build and validate explain the project root without changing directories", async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const initialized = await runInit(directory);
|
||||
expect(initialized.exitCode, initialized.stderr).toBe(0);
|
||||
const root = join(directory, "managed-agent");
|
||||
const nested = join(root, "agents/assistant/skills");
|
||||
for (const command of ["build", "validate"]) {
|
||||
const result = await runProject(nested, command, ["--dry-run"]);
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain("Not a project root:");
|
||||
expect(result.stderr).not.toMatch(/\p{Script=Han}/u);
|
||||
expect(result.stderr).toContain(`cd '${root}'`);
|
||||
expect(result.stderr).toContain(`--project '${root}'`);
|
||||
expect(result.stderr).not.toContain("ERR_MODULE_NOT_FOUND");
|
||||
}
|
||||
expect(await stat(join(nested, ".openagentpack")).catch(() => null)).toBeNull();
|
||||
const corrected = await runProject(nested, "build", ["--project", root, "--dry-run"]);
|
||||
expect(corrected.exitCode, corrected.stderr).toBe(0);
|
||||
});
|
||||
|
||||
test("help describes the subdirectory default and explicit in-place initialization", async () => {
|
||||
const result = await runCommandHelp(routes, ["managed-agent", "project", "init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stderr).toContain("./managed-agent");
|
||||
expect(result.stderr).toContain("--project .");
|
||||
});
|
||||
|
||||
test("default dry-run reports the child directory without creating it", async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const result = await runInit(directory, ["--dry-run"]);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(parseStdoutJson(result.stdout)).toEqual({ would_initialize_project: "./managed-agent" });
|
||||
expect(await stat(join(directory, "managed-agent")).catch(() => null)).toBeNull();
|
||||
});
|
||||
|
||||
test("initializes only the child, ignores parent source, and rejects repeated init without overwriting", async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const parentYaml = "not a valid project declaration";
|
||||
await writeFile(join(directory, "agents.yaml"), parentYaml);
|
||||
if (process.platform !== "win32") {
|
||||
await symlink("missing-instructions.md", join(directory, "CLAUDE.md"));
|
||||
}
|
||||
const result = await runInit(directory);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const projectRoot = join(directory, "managed-agent");
|
||||
const initialized = parseStdoutJson<{
|
||||
project_root: string;
|
||||
baseline_version: string;
|
||||
converted_from_yaml: boolean;
|
||||
}>(result.stdout);
|
||||
expect(initialized.project_root).toBe(projectRoot);
|
||||
expect(initialized.baseline_version).toHaveLength(64);
|
||||
expect(initialized.converted_from_yaml).toBe(false);
|
||||
expect(await stat(join(directory, "project.json")).catch(() => null)).toBeNull();
|
||||
expect(await readFile(join(directory, "agents.yaml"), "utf8")).toBe(parentYaml);
|
||||
expect((await stat(join(projectRoot, "agents/assistant/agent.json"))).isFile()).toBe(true);
|
||||
expect(
|
||||
(await stat(join(projectRoot, ".openagentpack/versions/project/store.json"))).isFile(),
|
||||
).toBe(true);
|
||||
const instructions = join(projectRoot, "agents/assistant/instructions.md");
|
||||
await writeFile(instructions, "user changes");
|
||||
const repeated = await runInit(directory);
|
||||
expect(repeated.exitCode).not.toBe(0);
|
||||
expect(repeated.stderr).toContain("already exists");
|
||||
expect(await readFile(instructions, "utf8")).toBe("user changes");
|
||||
});
|
||||
|
||||
test("explicit paths, including dot, remain exact project roots", async () => {
|
||||
for (const target of ["custom-agent", "."]) {
|
||||
const directory = await temporaryDirectory();
|
||||
const result = await runInit(directory, ["--project", target]);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(parseStdoutJson<{ project_root: string }>(result.stdout).project_root).toBe(
|
||||
join(directory, target),
|
||||
);
|
||||
expect(await stat(join(directory, "managed-agent")).catch(() => null)).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
resolveProjectConfig,
|
||||
type ResourceAddress,
|
||||
} from "@openagentpack/sdk";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { afterEach, describe, expect, test } from "vite-plus/test";
|
||||
import { parse } from "yaml";
|
||||
import { e2eFixturesDir, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts";
|
||||
@@ -29,6 +29,13 @@ const DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES = [
|
||||
"bailian.deployment.file.mount_path.required",
|
||||
"bailian.deployment.file.mount_path.duplicate",
|
||||
];
|
||||
const projectDirectories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
for (const directory of projectDirectories.splice(0)) {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
async function writeLocalSkillDirectory(parentDirectory: string): Promise<string> {
|
||||
const skillDirectory = join(parentDirectory, "local-report");
|
||||
@@ -98,8 +105,8 @@ async function seedTrackedResources(
|
||||
/**
|
||||
* managed-agent:help / 缺参不依赖密钥;所有 mutation 命令的 --dry-run
|
||||
* 必须在构建 SDK runtime(凭证注入 / 联网 / 写盘)之前短路,因此同样不需要密钥。
|
||||
* 鉴权分层:离线命令(init/validate/state list|show|rm)auth: "none";联网命令
|
||||
* 统一 auth: "apiKey" 硬门禁(见 managed-agent-auth-chain e2e)。
|
||||
* 鉴权分层:离线命令(init/project init/validate/state list|show|rm)auth: "none";
|
||||
* 联网命令统一 auth: "apiKey" 硬门禁(见 managed-agent-auth-chain e2e)。
|
||||
* 真实集成(apply/destroy/session 流程)依赖工作区内的 agents.yaml 与远端资源,
|
||||
* 属批量场景,暂仅覆盖 dry-run 契约。
|
||||
*/
|
||||
@@ -226,6 +233,215 @@ describe("e2e: managed-agent", () => {
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--file|--yes/i);
|
||||
expect(stderr).not.toContain("--provider");
|
||||
expect(stderr).not.toContain("--ci");
|
||||
expect(stderr).not.toContain("--refresh-only");
|
||||
});
|
||||
|
||||
test("managed-agent apply 不再接受 --refresh-only", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"apply",
|
||||
"--dry-run",
|
||||
"--refresh-only",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/Unknown flag.*--refresh-only/i);
|
||||
});
|
||||
|
||||
test("managed-agent init 不再暴露 Git 仓库脚手架", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"init",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).not.toContain("--git");
|
||||
});
|
||||
|
||||
test("managed-agent project 暴露目录项目与共享版本管理子命令", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"project",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/init|validate|build|publish|workbench|version/i);
|
||||
});
|
||||
|
||||
test("managed-agent project 完成 init、validate、build 与 version status 本地闭环", async () => {
|
||||
const projectRoot = await mkdtemp(join(tmpdir(), "bailian-managed-agent-project-"));
|
||||
projectDirectories.push(projectRoot);
|
||||
const initialized = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"project",
|
||||
"init",
|
||||
"--project",
|
||||
projectRoot,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(initialized.exitCode, initialized.stderr).toBe(0);
|
||||
expect(
|
||||
parseStdoutJson<{ baseline_version?: string }>(initialized.stdout).baseline_version,
|
||||
).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(JSON.parse(await readFile(join(projectRoot, "project.json"), "utf8"))).toEqual({
|
||||
version: "1",
|
||||
});
|
||||
expect(await stat(join(projectRoot, ".env")).catch(() => null)).toBeNull();
|
||||
expect(await stat(join(projectRoot, ".gitignore")).catch(() => null)).toBeNull();
|
||||
|
||||
expect(
|
||||
JSON.parse(await readFile(join(projectRoot, "agents/assistant/agent.json"), "utf8")),
|
||||
).toEqual({ name: "Assistant", model: "qwen3.7-max" });
|
||||
for (const relativePath of [
|
||||
"skills/_examples/example-skill/skill.json",
|
||||
"skills/_examples/example-skill/SKILL.md",
|
||||
"files/_examples/example-file/file.json",
|
||||
"files/_examples/example-file/example.md",
|
||||
"vaults/_examples/example-vault/vault.json",
|
||||
"environments/_examples/example-env/environment.json",
|
||||
]) {
|
||||
expect((await stat(join(projectRoot, "agents/assistant", relativePath))).isFile()).toBe(true);
|
||||
expect(
|
||||
await readFile(join(projectRoot, "agents/assistant", relativePath, "../README.md"), "utf8"),
|
||||
).toContain("_examples/");
|
||||
}
|
||||
|
||||
const skillDirectory = join(projectRoot, "agents/assistant/skills/writer");
|
||||
const filesDirectory = join(projectRoot, "agents/assistant/files");
|
||||
const nestedFileDirectory = join(filesDirectory, "mount");
|
||||
await mkdir(skillDirectory, { recursive: true });
|
||||
await mkdir(nestedFileDirectory, { recursive: true });
|
||||
await writeFile(join(skillDirectory, "SKILL.md"), "# Writer\n", "utf8");
|
||||
await writeFile(join(filesDirectory, "brief.txt"), "Build this brief.\n", "utf8");
|
||||
await writeFile(join(nestedFileDirectory, "mount.md"), "Mount this file.\n", "utf8");
|
||||
|
||||
const validated = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"project",
|
||||
"validate",
|
||||
"--project",
|
||||
projectRoot,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(validated.exitCode, validated.stderr).toBe(0);
|
||||
expect(
|
||||
parseStdoutJson<{ diagnostics?: Array<{ severity?: string }> }>(validated.stdout).diagnostics,
|
||||
).not.toContainEqual(expect.objectContaining({ severity: "error" }));
|
||||
|
||||
const vaultDirectory = join(projectRoot, "agents/assistant/vaults/secrets");
|
||||
await mkdir(vaultDirectory, { recursive: true });
|
||||
await writeFile(
|
||||
join(vaultDirectory, "vault.json"),
|
||||
JSON.stringify({
|
||||
id: "secrets",
|
||||
display_name: "Secrets",
|
||||
credentials: [
|
||||
{
|
||||
name: "service",
|
||||
type: "environment_variable",
|
||||
secret_name: "SERVICE_TOKEN",
|
||||
secret_value: "e2e-vault-private-value",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const dryBuild = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"project",
|
||||
"build",
|
||||
"--project",
|
||||
projectRoot,
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(dryBuild.exitCode, dryBuild.stderr).toBe(0);
|
||||
expect(dryBuild.stdout + dryBuild.stderr).not.toContain("e2e-vault-private-value");
|
||||
expect(dryBuild.stdout).toContain("AGENTS_VAULT_");
|
||||
expect(await stat(join(projectRoot, ".env")).catch(() => null)).toBeNull();
|
||||
|
||||
const built = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"project",
|
||||
"build",
|
||||
"--project",
|
||||
projectRoot,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(built.exitCode, built.stderr).toBe(0);
|
||||
const generatedYaml = await readFile(
|
||||
join(projectRoot, ".openagentpack/build/agents.yaml"),
|
||||
"utf8",
|
||||
);
|
||||
expect(generatedYaml).not.toContain("example-");
|
||||
expect(generatedYaml).not.toContain("_examples");
|
||||
expect(generatedYaml).not.toContain("$" + "{SERVICE_TOKEN}");
|
||||
expect(built.stdout + built.stderr).not.toContain("e2e-vault-private-value");
|
||||
expect(await readFile(join(projectRoot, ".env"), "utf8")).toContain("e2e-vault-private-value");
|
||||
expect(await readFile(join(vaultDirectory, "vault.json"), "utf8")).not.toContain(
|
||||
"e2e-vault-private-value",
|
||||
);
|
||||
expect(
|
||||
await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8"),
|
||||
).not.toContain("e2e-vault-private-value");
|
||||
expect(await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8")).toContain(
|
||||
"assistant",
|
||||
);
|
||||
expect(await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8")).toContain(
|
||||
"provider: bailian",
|
||||
);
|
||||
expect(JSON.parse(await readFile(join(skillDirectory, "skill.json"), "utf8"))).toEqual({
|
||||
id: "writer",
|
||||
});
|
||||
expect(JSON.parse(await readFile(join(filesDirectory, "brief/file.json"), "utf8"))).toEqual({
|
||||
id: "brief",
|
||||
name: "brief.txt",
|
||||
source: "./brief.txt",
|
||||
});
|
||||
expect(JSON.parse(await readFile(join(nestedFileDirectory, "file.json"), "utf8"))).toEqual({
|
||||
id: "mount",
|
||||
name: "mount.md",
|
||||
source: "./mount.md",
|
||||
});
|
||||
const builtAgent = JSON.parse(
|
||||
await readFile(join(projectRoot, "agents/assistant/agent.json"), "utf8"),
|
||||
);
|
||||
expect(builtAgent.skills).toEqual(["writer"]);
|
||||
expect(builtAgent.files).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ file: "brief", mount_path: "/mnt/brief.txt" },
|
||||
{ file: "mount", mount_path: "/mnt/mount.md" },
|
||||
]),
|
||||
);
|
||||
|
||||
const status = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"project",
|
||||
"version",
|
||||
"status",
|
||||
"--project",
|
||||
projectRoot,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(status.exitCode, status.stderr).toBe(0);
|
||||
expect(parseStdoutJson<{ enabled?: boolean }>(status.stdout).enabled).toBe(true);
|
||||
expect(await stat(join(projectRoot, ".openagentpack/state.json")).catch(() => null)).toBeNull();
|
||||
});
|
||||
|
||||
test("managed-agent project version preview 缺少 --version-id 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"project",
|
||||
"version",
|
||||
"preview",
|
||||
"--quiet",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--version-id|Missing required/i);
|
||||
});
|
||||
|
||||
test("managed-agent apply 计划失败时在最终错误中保留首条诊断和剩余数量", async () => {
|
||||
@@ -1289,6 +1505,29 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () =>
|
||||
expect(data.provider).toBe("bailian");
|
||||
});
|
||||
|
||||
test("project workbench --dry-run 仅输出目录项目启动计划", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"project",
|
||||
"workbench",
|
||||
"--dry-run",
|
||||
"--project",
|
||||
"./agent-project",
|
||||
"--no-open",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
would_launch?: string;
|
||||
project_root?: string;
|
||||
port?: number;
|
||||
}>(stdout);
|
||||
expect(data.would_launch).toBe("workbench");
|
||||
expect(data.project_root).toBe("./agent-project");
|
||||
expect(data.port).toBe(4848);
|
||||
});
|
||||
|
||||
test("init 不再接受 --provider", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
@@ -1301,6 +1540,27 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () =>
|
||||
expect(stderr).toMatch(/Unknown flag.*--provider/i);
|
||||
});
|
||||
|
||||
test("project init --dry-run 不需要密钥且不创建目录", async () => {
|
||||
const parent = await mkdtemp(join(tmpdir(), "bailian-managed-agent-project-dry-run-"));
|
||||
projectDirectories.push(parent);
|
||||
const projectRoot = join(parent, "project");
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"project",
|
||||
"init",
|
||||
"--dry-run",
|
||||
"--project",
|
||||
projectRoot,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(
|
||||
parseStdoutJson<{ would_initialize_project?: string }>(stdout).would_initialize_project,
|
||||
).toBe(projectRoot);
|
||||
expect(await stat(projectRoot).catch(() => null)).toBeNull();
|
||||
});
|
||||
|
||||
test("apply --dry-run 仅输出计划", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
|
||||
@@ -188,6 +188,18 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
|
||||
"managed-agent plan": "managedAgentPlan",
|
||||
"managed-agent apply": "managedAgentApply",
|
||||
"managed-agent destroy": "managedAgentDestroy",
|
||||
"managed-agent playground": "managedAgentPlayground",
|
||||
"managed-agent project init": "managedAgentProjectInit",
|
||||
"managed-agent project validate": "managedAgentProjectValidate",
|
||||
"managed-agent project build": "managedAgentProjectBuild",
|
||||
"managed-agent project publish": "managedAgentProjectPublish",
|
||||
"managed-agent project workbench": "managedAgentProjectWorkbench",
|
||||
"managed-agent project version enable": "managedAgentProjectVersionEnable",
|
||||
"managed-agent project version disable": "managedAgentProjectVersionDisable",
|
||||
"managed-agent project version status": "managedAgentProjectVersionStatus",
|
||||
"managed-agent project version list": "managedAgentProjectVersionList",
|
||||
"managed-agent project version preview": "managedAgentProjectVersionPreview",
|
||||
"managed-agent project version restore": "managedAgentProjectVersionRestore",
|
||||
"managed-agent state list": "managedAgentStateList",
|
||||
"managed-agent state rm": "managedAgentStateRm",
|
||||
"managed-agent state import": "managedAgentStateImport",
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
commitPreparedProjectVersion,
|
||||
disableProjectVersioning,
|
||||
enableProjectVersioning,
|
||||
getProjectVersionStatus,
|
||||
listProjectVersions,
|
||||
prepareProjectVersion,
|
||||
previewProjectVersion,
|
||||
restoreProjectVersion,
|
||||
} from "@openagentpack/sdk/project-versions";
|
||||
import { afterEach, describe, expect, test } from "vite-plus/test";
|
||||
import { playgroundBrowserTargetFromSummary } from "../src/commands/managed-agent/_engine/playground-launcher.ts";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("managed-agent local snapshot versions", () => {
|
||||
test("uses the shared path-scoped switch and stores full YAML outside store.json", async () => {
|
||||
const root = await temporaryDirectory();
|
||||
const configPath = join(root, "agents.yaml");
|
||||
const siblingDirectory = join(root, "nested");
|
||||
const siblingConfigPath = join(siblingDirectory, "agents.yaml");
|
||||
await mkdir(siblingDirectory);
|
||||
await writeFile(configPath, projectYaml("First"));
|
||||
await writeFile(siblingConfigPath, projectYaml("Second"));
|
||||
|
||||
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
|
||||
|
||||
expect(enabled.versioning.enabled).toBe(true);
|
||||
expect(enabled.version?.message).toBe("Enable Bailian CLI versioning");
|
||||
expect((await getProjectVersionStatus(siblingConfigPath)).enabled).toBe(false);
|
||||
const storeSource = await readFile(join(root, ".openagentpack/versions/store.json"), "utf8");
|
||||
expect(storeSource).not.toContain("instructions: First");
|
||||
const snapshotSource = await readFile(
|
||||
join(root, ".openagentpack/versions/blobs", `${enabled.version!.source_hash}.yaml`),
|
||||
"utf8",
|
||||
);
|
||||
expect(snapshotSource).toContain("instructions: First");
|
||||
|
||||
await writeFile(configPath, projectYaml("First updated"));
|
||||
const repeated = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
|
||||
expect(repeated.version?.message).toBe("Enable Bailian CLI versioning");
|
||||
expect((await listProjectVersions(configPath)).versions).toHaveLength(2);
|
||||
|
||||
const disabled = await disableProjectVersioning(configPath);
|
||||
expect(disabled.enabled).toBe(false);
|
||||
});
|
||||
|
||||
test("auto-snapshots after success and restores without changing history or permissions", async () => {
|
||||
const root = await temporaryDirectory();
|
||||
const configPath = join(root, "agents.yaml");
|
||||
await writeFile(configPath, projectYaml("Version one"));
|
||||
await chmod(configPath, 0o640);
|
||||
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
|
||||
const firstVersion = enabled.version!.version_id;
|
||||
const secondSource = projectYaml("Version two");
|
||||
await writeFile(configPath, secondSource);
|
||||
|
||||
const prepared = await prepareProjectVersion(configPath, secondSource);
|
||||
const version = await commitPreparedProjectVersion(prepared!);
|
||||
const currentVersionBeforeRestore = (await getProjectVersionStatus(configPath)).head_version;
|
||||
expect(version?.message).toBe("Apply agents.yaml");
|
||||
|
||||
const preview = await previewProjectVersion(configPath, firstVersion);
|
||||
expect(preview.can_restore).toBe(true);
|
||||
expect(preview.after_yaml).toContain("Version one");
|
||||
await restoreProjectVersion(configPath, firstVersion, {
|
||||
headVersion: preview.base_head_version,
|
||||
sourceRevision: preview.base_source_revision,
|
||||
});
|
||||
|
||||
expect(await readFile(configPath, "utf8")).toContain("Version one");
|
||||
expect((await getProjectVersionStatus(configPath)).head_version).toBe(
|
||||
currentVersionBeforeRestore,
|
||||
);
|
||||
expect((await stat(configPath)).mode & 0o777).toBe(0o640);
|
||||
});
|
||||
|
||||
test("rejects abbreviated version IDs and plaintext credentials", async () => {
|
||||
const root = await temporaryDirectory();
|
||||
const configPath = join(root, "agents.yaml");
|
||||
await writeFile(configPath, projectYaml("Safe"));
|
||||
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
|
||||
await expect(previewProjectVersion(configPath, enabled.version!.short_version)).rejects.toThrow(
|
||||
/full 64-character hexadecimal/i,
|
||||
);
|
||||
|
||||
await disableProjectVersioning(configPath);
|
||||
await writeFile(
|
||||
configPath,
|
||||
projectYaml("Unsafe").replace("qoder: {}", "qoder:\n api_key: plaintext-secret"),
|
||||
);
|
||||
await expect(
|
||||
enableProjectVersioning(configPath, "Enable Bailian CLI versioning"),
|
||||
).rejects.toThrow(/environment variable reference/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed-agent Workbench policy", () => {
|
||||
test("Session Preview opens the requested Agent or falls back to Workbench", () => {
|
||||
const summary = {
|
||||
status: "valid",
|
||||
agents: [{ agent: { id: "assistant" } }, { agent: { id: "reviewer" } }],
|
||||
};
|
||||
expect(
|
||||
playgroundBrowserTargetFromSummary("http://localhost:4848", summary, "reviewer"),
|
||||
).toEqual({ url: "http://localhost:4848/agents/reviewer/preview" });
|
||||
expect(playgroundBrowserTargetFromSummary("http://localhost:4848", summary)).toEqual(
|
||||
expect.objectContaining({ url: "http://localhost:4848", warning: expect.any(String) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function temporaryDirectory(): Promise<string> {
|
||||
const directory = await mkdtemp(join(tmpdir(), "bailian-cli-local-versions-"));
|
||||
temporaryDirectories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
function projectYaml(instructions: string): string {
|
||||
return `version: "1"
|
||||
providers:
|
||||
qoder: {}
|
||||
defaults:
|
||||
provider: qoder
|
||||
agents:
|
||||
assistant:
|
||||
model: ultimate
|
||||
instructions: ${instructions}
|
||||
`;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-core",
|
||||
"version": "1.20.0",
|
||||
"version": "1.22.0",
|
||||
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -287,8 +287,10 @@ export interface Command<F extends FlagsDef = FlagsDef> {
|
||||
* Cross-flag validation, after parsing and before run. Return an error message
|
||||
* → UsageError; undefined to pass. Single-flag `required` is enforced by the
|
||||
* parser — use this for rules spanning flags or depending on a flag's *value*.
|
||||
* May be async for read-only local preflight. Runtime awaits it before auth
|
||||
* and confirmation. Do not perform remote requests or local writes here.
|
||||
*/
|
||||
validate?: (flags: ParsedFlags<F>) => string | undefined;
|
||||
validate?: (flags: ParsedFlags<F>) => string | undefined | Promise<string | undefined>;
|
||||
run: (ctx: CommandContext<F>) => Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "knowledge-studio-cli",
|
||||
"version": "1.20.0",
|
||||
"version": "1.22.0",
|
||||
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
|
||||
"keywords": [
|
||||
"alibaba-cloud",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-runtime",
|
||||
"version": "1.20.0",
|
||||
"version": "1.22.0",
|
||||
"description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -217,7 +217,7 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
|
||||
parsedFlags,
|
||||
Object.keys(res.command.flags ?? {}),
|
||||
) as ParsedFlags<FlagsDef>;
|
||||
const invalid = res.command.validate?.(ownFlags);
|
||||
const invalid = await res.command.validate?.(ownFlags);
|
||||
if (invalid) throw new UsageError(invalid);
|
||||
|
||||
// 校验通过 → 建源、解析 settings、组 ctx,进中间件执行命令。
|
||||
|
||||
Generated
+5
-5
@@ -189,8 +189,8 @@ importers:
|
||||
packages/commands:
|
||||
dependencies:
|
||||
'@openagentpack/sdk':
|
||||
specifier: 0.5.0
|
||||
version: 0.5.0
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1
|
||||
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.1':
|
||||
resolution: {integrity: sha512-OENks8UdfWFanvhJIVbqyUX1+XGavHnrxDiFx4SoJoGRXi45cOgyYtWWbG5VdDI2I5Dpo9eGNaw/JtDnkdxUcg==}
|
||||
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.1':
|
||||
dependencies:
|
||||
jszip: 3.10.1
|
||||
yaml: 2.9.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-cli
|
||||
metadata:
|
||||
version: "1.20.0"
|
||||
version: "1.22.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-finetune
|
||||
metadata:
|
||||
version: "1.20.0"
|
||||
version: "1.22.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-gen
|
||||
metadata:
|
||||
version: "1.20.0"
|
||||
version: "1.22.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
---
|
||||
name: bailian-managed-agent
|
||||
metadata:
|
||||
version: "1.20.0"
|
||||
version: "1.22.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
阿里云百炼托管 Agent 声明式基础设施与 API 命令入口:用户要创建agent、初始化 agents.yaml、校验或预览配置变更、
|
||||
创建/更新/销毁托管 Agent 或 Deployment,或查询 Agent/Environment/Skill/Vault/Deployment、管理 Session/Event/File、
|
||||
运行/暂停 Deployment 时使用 `bl managed-agent`。持久资源仍以 agents.yaml 为唯一事实源做 IaC;公开 API 能力按资源透出
|
||||
创建/更新/销毁托管 Agent 或 Deployment、在 Workbench 编辑和调试目录项目、管理本地快照版本,或查询
|
||||
Agent/Environment/Skill/Vault/Deployment、管理 Session/Event/File、运行/暂停 Deployment 时使用
|
||||
`bl managed-agent`。持久资源仍以 agents.yaml 为唯一事实源做 IaC;公开 API 能力按资源透出
|
||||
list/get/search/versions/download、数据面和运行时动作命令。apply / destroy 与破坏性 API 命令受统一高风险确认闸门保护;
|
||||
务必先展示预览再让用户确认,禁止自动添加 `--yes`。
|
||||
反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、
|
||||
@@ -47,6 +48,31 @@ for explicit confirmation before re-running with `--yes`.
|
||||
6. Destroy bl managed-agent destroy --yes # separate explicit confirmation
|
||||
```
|
||||
|
||||
## Directory projects, Workbench, and local versions
|
||||
|
||||
| Intent | Command |
|
||||
| --------------------------------------- | ------------------------------------------------------ |
|
||||
| Create or convert a directory project | `bl managed-agent project init` |
|
||||
| Validate and Build directory source | `bl managed-agent project validate` / `build` |
|
||||
| Publish the current immutable Build | `bl managed-agent project publish --yes` |
|
||||
| Launch project resource editing | `bl managed-agent project workbench` |
|
||||
| Launch one Agent Session Preview | `bl managed-agent playground --agent <id>` |
|
||||
| Enable/disable project versions | `bl managed-agent project version enable` / `disable` |
|
||||
| Inspect local version state and history | `bl managed-agent project version status` / `list` |
|
||||
| Preview or restore project source | `bl managed-agent project version preview` / `restore` |
|
||||
|
||||
- Bailian CLI and Workbench use the same `.openagentpack/versions/project` store and enable switch. Git is not required.
|
||||
- Directory projects always use Bailian. `project.json` does not declare a Provider; Build supplies the Bailian Provider configuration automatically.
|
||||
- Fresh `project init` includes complete Skill/File/Vault/Environment examples with bilingual README files under `agents/assistant/<resource-type>/_examples/`. They are not linked in `agent.json` and are excluded from Build discovery, Workbench declarations, and remote Publish. Copy a resource outside `_examples/` and configure its Agent reference to use it. Examples remain local versioned source; never put real secrets into them. / 新项目的四类资源示例默认不启用、不发布;请按 README 复制到 `_examples/` 外再配置引用,不要向示例写入真实密钥。
|
||||
- `project init`, `validate`, `build`, and version commands are local-only. Publish and Workbench resolve credentials from Bailian CLI flags, shell environment, or the active Profile; project initialization does not write credentials into the project directory.
|
||||
- Build is local-only. Publish never runs Build implicitly and consumes only a current `.openagentpack/build/agents.yaml` plus manifest.
|
||||
- Build moves literal Vault `secret_value` / `access_token` values from Agent-local or shared `vault.json` into project-root `.env`, replacing them with generated environment references. Existing references and `.env` entries are preserved; conflicts receive suffixed variable names. Preview/dry-run never write or print secrets. Publish and Workbench read the selected project's root `.env` as a fallback to inherited environment variables, even when invoked elsewhere. `.env` is private plaintext storage, excluded from local versions but not automatically ignored by Git; keep it backed up securely.
|
||||
- Build 会将 Agent 本地或共享 `vault.json` 中的明文密钥移入项目根目录 `.env`,再写回环境变量引用;保留已有引用和变量,重名时生成后缀。预览不写文件或输出密钥。`.env` 不进入版本快照,也不加密;请自行备份并加入 Git 忽略规则。
|
||||
- Agent-local File and Skill content supports Build-time association. A File may be copied directly into `agents/<agent>/files/`, or placed in `agents/<agent>/files/<id>/` when that directory contains exactly one content file; Build generates `file.json` and a `/mnt/<filename>` entry in `agent.json.files`. A directory under `agents/<agent>/skills/<id>/` containing `SKILL.md` generates `skill.json` and its `agent.json.skills` entry. Explicit JSON always wins; shared root resources remain explicit. Resources referenced by multiple Agents are promoted to the corresponding root shared directory during Build.
|
||||
- A successful Publish versions the canonical YAML and the complete project source tree, including Skill scripts/assets and binary files. Remote State is never versioned or restored.
|
||||
- `project version restore` restores source files to the working directory, invalidates Build, and does not move version history or remote State.
|
||||
- `managed-agent playground` remains the standalone `agents.yaml` Session Preview path; directory Workbench is only under `managed-agent project workbench`.
|
||||
|
||||
## Scoped single-resource create
|
||||
|
||||
以下命令都先构造 `agents.yaml` 声明,再通过 SDK 的定向 Plan/Apply 创建远端资源,不绕过 State:
|
||||
|
||||
@@ -37,8 +37,20 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `bl managed-agent file list` | API Key | List Managed Agent files | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent file search` | API Key | Search Managed Agent files | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent init` | No Auth | Create a new agents.yaml template | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent init` | No Auth | Create an agents.yaml template | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project build` | No Auth | Organize directory source and generate the immutable Publish Build | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project init` | No Auth | Create a directory project or convert the local agents.yaml | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project publish` | API Key | Publish the current directory-project Build and record a version | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project validate` | No Auth | Validate a directory Agent project | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project version disable` | No Auth | Disable directory project versions | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project version enable` | No Auth | Enable directory project versions | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project version list` | No Auth | List directory project versions | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project version preview` | No Auth | Preview a directory project version | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project version restore` | No Auth | Restore a version to the project working directory | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project version status` | No Auth | Show directory project version status | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent project workbench` | API Key | Launch the directory project Workbench | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent session create` | API Key | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
|
||||
| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | [managed-agent.md](managed-agent.md) |
|
||||
@@ -74,9 +86,9 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
|
||||
## By group
|
||||
|
||||
| Group | Commands | Reference |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `managed-agent` | `agent create`, `agent get`, `agent list`, `agent search`, `agent versions`, `apply`, `deployment create`, `deployment get`, `deployment list`, `deployment pause`, `deployment run`, `deployment runs get`, `deployment runs list`, `deployment search`, `deployment unpause`, `destroy`, `environment create`, `environment get`, `environment list`, `environment search`, `file delete`, `file download`, `file get`, `file list`, `file search`, `file upload`, `init`, `plan`, `session archive`, `session create`, `session debug`, `session delete`, `session event list`, `session event send`, `session event stream`, `session events`, `session export`, `session get`, `session list`, `session run`, `session search`, `session send`, `session update`, `skill create`, `skill download`, `skill get`, `skill list`, `skill search`, `skill versions`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `vault create`, `vault credential create`, `vault get`, `vault list`, `vault search` | [managed-agent.md](managed-agent.md) |
|
||||
| Group | Commands | Reference |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `managed-agent` | `agent create`, `agent get`, `agent list`, `agent search`, `agent versions`, `apply`, `deployment create`, `deployment get`, `deployment list`, `deployment pause`, `deployment run`, `deployment runs get`, `deployment runs list`, `deployment search`, `deployment unpause`, `destroy`, `environment create`, `environment get`, `environment list`, `environment search`, `file delete`, `file download`, `file get`, `file list`, `file search`, `file upload`, `init`, `plan`, `playground`, `project build`, `project init`, `project publish`, `project validate`, `project version disable`, `project version enable`, `project version list`, `project version preview`, `project version restore`, `project version status`, `project workbench`, `session archive`, `session create`, `session debug`, `session delete`, `session event list`, `session event send`, `session event stream`, `session events`, `session export`, `session get`, `session list`, `session run`, `session search`, `session send`, `session update`, `skill create`, `skill download`, `skill get`, `skill list`, `skill search`, `skill versions`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `vault create`, `vault credential create`, `vault get`, `vault list`, `vault search` | [managed-agent.md](managed-agent.md) |
|
||||
|
||||
## Global flags
|
||||
|
||||
|
||||
@@ -35,8 +35,20 @@ Index: [index.md](index.md)
|
||||
| `bl managed-agent file list` | API Key | List Managed Agent files |
|
||||
| `bl managed-agent file search` | API Key | Search Managed Agent files |
|
||||
| `bl managed-agent file upload` | API Key | Upload a Managed Agent file |
|
||||
| `bl managed-agent init` | No Auth | Create a new agents.yaml template |
|
||||
| `bl managed-agent init` | No Auth | Create an agents.yaml template |
|
||||
| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure |
|
||||
| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent |
|
||||
| `bl managed-agent project build` | No Auth | Organize directory source and generate the immutable Publish Build |
|
||||
| `bl managed-agent project init` | No Auth | Create a directory project or convert the local agents.yaml |
|
||||
| `bl managed-agent project publish` | API Key | Publish the current directory-project Build and record a version |
|
||||
| `bl managed-agent project validate` | No Auth | Validate a directory Agent project |
|
||||
| `bl managed-agent project version disable` | No Auth | Disable directory project versions |
|
||||
| `bl managed-agent project version enable` | No Auth | Enable directory project versions |
|
||||
| `bl managed-agent project version list` | No Auth | List directory project versions |
|
||||
| `bl managed-agent project version preview` | No Auth | Preview a directory project version |
|
||||
| `bl managed-agent project version restore` | No Auth | Restore a version to the project working directory |
|
||||
| `bl managed-agent project version status` | No Auth | Show directory project version status |
|
||||
| `bl managed-agent project workbench` | API Key | Launch the directory project Workbench |
|
||||
| `bl managed-agent session archive` | API Key | Archive a Managed Agent session |
|
||||
| `bl managed-agent session create` | API Key | Create a new session for an agent |
|
||||
| `bl managed-agent session debug` | API Key | Aggregate session diagnostics |
|
||||
@@ -1068,7 +1080,7 @@ bl managed-agent file upload --path ./report.pdf
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent init` |
|
||||
| **Description** | Create a new agents.yaml template |
|
||||
| **Description** | Create an agents.yaml template |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent init [--agent-name <name>] [--file <path>] [--force]` |
|
||||
|
||||
@@ -1126,6 +1138,372 @@ bl managed-agent plan
|
||||
bl managed-agent plan --no-refresh
|
||||
```
|
||||
|
||||
### `bl managed-agent playground`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent playground` |
|
||||
| **Description** | Launch a Session Preview for an agents.yaml Agent |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl managed-agent playground [--file <path>] [--agent <id>] [--port <n>] [--no-open]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | --------------------------------------------------------------------- |
|
||||
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
|
||||
| `--port <n>` | number | no | Local port (default: 4848) |
|
||||
| `--no-open` | switch | no | Do not open a browser automatically |
|
||||
| `--agent <id>` | string | no | Agent to preview (required when the project declares multiple Agents) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- bl managed-agent supports the Bailian provider only; configurations containing other providers are rejected.
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
- Session Preview requires Node.js 22+ and keeps using an agents.yaml source. Directory Workbench is available under managed-agent project workbench.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent playground
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent playground --agent assistant
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent playground --file agents.yaml --no-open
|
||||
```
|
||||
|
||||
### `bl managed-agent project build`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------ |
|
||||
| **Name** | `managed-agent project build` |
|
||||
| **Description** | Organize directory source and generate the immutable Publish Build |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent project build [--project <directory>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: current directory) |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Build writes local project files without confirmation, including inferred resource associations and migration of plaintext Vault secrets into .env. Use --dry-run to preview without writing. Publish still requires explicit confirmation before remote changes.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent project build
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent project build --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent project build --project ./my-agent
|
||||
```
|
||||
|
||||
### `bl managed-agent project init`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------- |
|
||||
| **Name** | `managed-agent project init` |
|
||||
| **Description** | Create a directory project or convert the local agents.yaml |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent project init [--project <directory>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------- | ------ | -------- | ----------------------------------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: ./managed-agent under the current directory) |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Without --project, creates a managed-agent/ subdirectory. Enter it before running other project commands. Use --project . to initialize in place or convert the current agents.yaml; existing project files are not overwritten.
|
||||
- New projects include Skill, File, Vault, and Environment examples under each resource directory's \_examples/. They are not referenced by agent.json and are excluded from Build/Publish. Copy an example outside \_examples/ to enable it, then configure its Agent reference.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent project init
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent project init --project ./my-agent
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent project init --project .
|
||||
```
|
||||
|
||||
### `bl managed-agent project publish`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent project publish` |
|
||||
| **Description** | Publish the current directory-project Build and record a version |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl managed-agent project publish [--project <directory>] [--no-refresh] [--concurrency <n>]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This publishes the current directory-project Build and may create, update, or delete remote managed Agent resources. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: current directory) |
|
||||
| `--no-refresh` | switch | no | Skip remote refresh before planning |
|
||||
| `--concurrency <n>` | number | no | Maximum parallel resource operations |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- bl managed-agent supports the Bailian provider only; configurations containing other providers are rejected.
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl managed-agent project publish --yes
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl managed-agent project publish --project ./my-agent --yes
|
||||
```
|
||||
|
||||
### `bl managed-agent project validate`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------- |
|
||||
| **Name** | `managed-agent project validate` |
|
||||
| **Description** | Validate a directory Agent project |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent project validate [--project <directory>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: current directory) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent project validate
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent project validate --project ./my-agent
|
||||
```
|
||||
|
||||
### `bl managed-agent project version disable`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------ |
|
||||
| **Name** | `managed-agent project version disable` |
|
||||
| **Description** | Disable directory project versions |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent project version disable [--project <directory>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: current directory) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent project version disable
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent project version disable --project ./my-agent
|
||||
```
|
||||
|
||||
### `bl managed-agent project version enable`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent project version enable` |
|
||||
| **Description** | Enable directory project versions |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent project version enable [--project <directory>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: current directory) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent project version enable
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent project version enable --project ./my-agent
|
||||
```
|
||||
|
||||
### `bl managed-agent project version list`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent project version list` |
|
||||
| **Description** | List directory project versions |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent project version list [--project <directory>] [--limit <n>] [--cursor <cursor>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: current directory) |
|
||||
| `--limit <n>` | number | no | Maximum versions to return |
|
||||
| `--cursor <cursor>` | string | no | Pagination cursor |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent project version list
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent project version list --limit 20 --output json
|
||||
```
|
||||
|
||||
### `bl managed-agent project version preview`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent project version preview` |
|
||||
| **Description** | Preview a directory project version |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent project version preview --version-id <full-version> [--project <directory>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------------- | ------ | -------- | --------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: current directory) |
|
||||
| `--version-id <full-version>` | string | yes | Full project version ID |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent project version preview --version-id <full-version>
|
||||
```
|
||||
|
||||
### `bl managed-agent project version restore`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| **Name** | `managed-agent project version restore` |
|
||||
| **Description** | Restore a version to the project working directory |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent project version restore --version-id <full-version> [--project <directory>]` |
|
||||
| **Risk** | `high` |
|
||||
| **Risk message** | This restores the full directory source to the working tree. Version history and remote State will not move. |
|
||||
|
||||
> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope.
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------------- | ------ | -------- | --------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: current directory) |
|
||||
| `--version-id <full-version>` | string | yes | Full project version ID |
|
||||
| `--yes` | switch | no | Confirm this high-risk operation |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent project version restore --version-id <full-version>
|
||||
```
|
||||
|
||||
```bash
|
||||
# Only after explicit user confirmation:
|
||||
bl managed-agent project version restore --version-id <full-version> --yes
|
||||
```
|
||||
|
||||
### `bl managed-agent project version status`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ----------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent project version status` |
|
||||
| **Description** | Show directory project version status |
|
||||
| **Authentication** | No Auth |
|
||||
| **Usage** | `bl managed-agent project version status [--project <directory>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: current directory) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent project version status
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent project version status --project ./my-agent --output json
|
||||
```
|
||||
|
||||
### `bl managed-agent project workbench`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------- |
|
||||
| **Name** | `managed-agent project workbench` |
|
||||
| **Description** | Launch the directory project Workbench |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl managed-agent project workbench [--project <directory>] [--port <n>] [--no-open]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------------- |
|
||||
| `--project <directory>` | string | no | Directory project root (default: current directory) |
|
||||
| `--port <n>` | number | no | Local port (default: 4848) |
|
||||
| `--no-open` | switch | no | Do not open a browser |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- bl managed-agent supports the Bailian provider only; configurations containing other providers are rejected.
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl managed-agent project workbench
|
||||
```
|
||||
|
||||
```bash
|
||||
bl managed-agent project workbench --project ./my-agent --no-open
|
||||
```
|
||||
|
||||
### `bl managed-agent session archive`
|
||||
|
||||
| Field | Value |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-protocol
|
||||
metadata:
|
||||
version: "1.20.0"
|
||||
version: "1.22.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-web-search
|
||||
metadata:
|
||||
version: "1.20.0"
|
||||
version: "1.22.0"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
Reference in New Issue
Block a user