eve-app-cli

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Eve App CLI

Eve App CLI

Build domain-specific CLIs for Eve-compatible apps so agents interact via commands instead of raw REST calls.
为兼容Eve的应用构建特定领域的CLI工具,让Agent通过命令而非原始REST调用进行交互。

Why

设计初衷

Agents waste 3-5 LLM calls per REST interaction on URL construction, JSON quoting, auth headers, and error parsing. A CLI reduces this to 1 call:
bash
undefined
Agent在每次REST交互中,会在URL构建、JSON转义、认证头和错误解析上浪费3-5次LLM调用。使用CLI可将其减少至1次调用:
bash
undefined

Before (3-5 calls, error-prone)

之前(3-5次调用,易出错)

curl -X POST "$EVE_APP_API_URL_API/projects/$PID/changesets"
-H "Content-Type: application/json"
-H "Authorization: Bearer $EVE_JOB_TOKEN"
-d @/tmp/changeset.json
curl -X POST "$EVE_APP_API_URL_API/projects/$PID/changesets"
-H "Content-Type: application/json"
-H "Authorization: Bearer $EVE_JOB_TOKEN"
-d @/tmp/changeset.json

After (1 call, self-documenting)

之后(1次调用,自带文档)

eden changeset create --project $PID --file /tmp/changeset.json
undefined
eden changeset create --project $PID --file /tmp/changeset.json
undefined

Quick Start

快速开始

1. Create the CLI Package

1. 创建CLI包

your-app/
  cli/
    src/
      index.ts          # Entry point
      client.ts         # API client (reads env vars)
      commands/
        projects.ts     # Domain commands
    bin/
      your-app          # Built artifact (single-file bundle)
    package.json
    tsconfig.json
your-app/
  cli/
    src/
      index.ts          # 入口文件
      client.ts         # API客户端(读取环境变量)
      commands/
        projects.ts     # 领域命令
    bin/
      your-app          # 构建产物(单文件打包)
    package.json
    tsconfig.json

2. Implement the API Client

2. 实现API客户端

typescript
// cli/src/client.ts — Copy this, change SERVICE name
const SERVICE = 'API';

export function getApiUrl(): string {
  const url = process.env[`EVE_APP_API_URL_${SERVICE}`];
  if (!url) {
    console.error(`Error: EVE_APP_API_URL_${SERVICE} not set.`);
    console.error('Are you running inside an Eve job with with_apis: [api]?');
    process.exit(1);
  }
  return url;
}

export async function api<T = unknown>(
  method: string,
  path: string,
  body?: unknown,
): Promise<T> {
  const url = getApiUrl();
  const token = process.env.EVE_JOB_TOKEN;
  const res = await fetch(`${url}${path}`, {
    method,
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({} as Record<string, string>));
    console.error(`${method} ${path}${res.status}: ${err.message || res.statusText}`);
    process.exit(1);
  }
  return res.json() as Promise<T>;
}
typescript
// cli/src/client.ts — 复制此代码,修改SERVICE名称
const SERVICE = 'API';

export function getApiUrl(): string {
  const url = process.env[`EVE_APP_API_URL_${SERVICE}`];
  if (!url) {
    console.error(`Error: EVE_APP_API_URL_${SERVICE} not set.`);
    console.error('Are you running inside an Eve job with with_apis: [api]?');
    process.exit(1);
  }
  return url;
}

export async function api<T = unknown>(
  method: string,
  path: string,
  body?: unknown,
): Promise<T> {
  const url = getApiUrl();
  const token = process.env.EVE_JOB_TOKEN;
  const res = await fetch(`${url}${path}`, {
    method,
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({} as Record<string, string>));
    console.error(`${method} ${path}${res.status}: ${err.message || res.statusText}`);
    process.exit(1);
  }
  return res.json() as Promise<T>;
}

3. Define Commands

3. 定义命令

typescript
// cli/src/index.ts
import { Command } from 'commander';
import { api } from './client.js';
import { readFile } from 'node:fs/promises';

const program = new Command();
program.name('myapp').description('My App CLI').version('1.0.0');

program.command('items')
  .command('list')
  .option('--json', 'JSON output')
  .action(async (opts) => {
    const items = await api('GET', '/items');
    if (opts.json) return console.log(JSON.stringify(items, null, 2));
    for (const i of items) console.log(`${i.id}  ${i.name}`);
  });

program.command('items')
  .command('create')
  .requiredOption('--file <path>', 'JSON file')
  .action(async (opts) => {
    const body = JSON.parse(await readFile(opts.file, 'utf8'));
    const result = await api('POST', '/items', body);
    console.log(`Created: ${result.id}`);
  });

program.parse();
typescript
// cli/src/index.ts
import { Command } from 'commander';
import { api } from './client.js';
import { readFile } from 'node:fs/promises';

const program = new Command();
program.name('myapp').description('My App CLI').version('1.0.0');

program.command('items')
  .command('list')
  .option('--json', 'JSON输出')
  .action(async (opts) => {
    const items = await api('GET', '/items');
    if (opts.json) return console.log(JSON.stringify(items, null, 2));
    for (const i of items) console.log(`${i.id}  ${i.name}`);
  });

program.command('items')
  .command('create')
  .requiredOption('--file <path>', 'JSON文件')
  .action(async (opts) => {
    const body = JSON.parse(await readFile(opts.file, 'utf8'));
    const result = await api('POST', '/items', body);
    console.log(`Created: ${result.id}`);
  });

program.parse();

4. Bundle for Zero-Dependency Distribution

4. 打包为零依赖可执行文件

Create a build script (
cli/build.mjs
):
javascript
import { build } from 'esbuild';
import { readFile, writeFile, chmod } from 'node:fs/promises';

await build({
  entryPoints: ['cli/src/index.ts'],
  bundle: true,
  platform: 'node',
  target: 'node20',
  format: 'cjs',          // CJS — commander uses require() internally
  outfile: 'cli/bin/myapp',
});

// Prepend shebang (esbuild banner escapes the !)
const code = await readFile('cli/bin/myapp', 'utf8');
await writeFile('cli/bin/myapp', '#!/usr/bin/env node\n' + code);
await chmod('cli/bin/myapp', 0o755);
Add to
package.json
:
json
{
  "scripts": {
    "build": "node build.mjs"
  }
}
Important: Do NOT set
"type": "module"
in
package.json
— it causes
require()
errors at runtime. Use
.mjs
extension for the build script instead.
创建构建脚本(
cli/build.mjs
):
javascript
import { build } from 'esbuild';
import { readFile, writeFile, chmod } from 'node:fs/promises';

await build({
  entryPoints: ['cli/src/index.ts'],
  bundle: true,
  platform: 'node',
  target: 'node20',
  format: 'cjs',          // CJS — commander内部使用require()
  outfile: 'cli/bin/myapp',
});

// 添加shebang(esbuild的banner会转义!)
const code = await readFile('cli/bin/myapp', 'utf8');
await writeFile('cli/bin/myapp', '#!/usr/bin/env node\n' + code);
await chmod('cli/bin/myapp', 0o755);
package.json
中添加:
json
{
  "scripts": {
    "build": "node build.mjs"
  }
}
重要提示: 不要在
package.json
中设置
"type": "module"
——这会导致运行时出现
require()
错误。请改用
.mjs
扩展名作为构建脚本。

5. Declare in Manifest

5. 在清单中声明

yaml
undefined
yaml
undefined

.eve/manifest.yaml

.eve/manifest.yaml

services: api: build: context: ./apps/api ports: [3000] x-eve: api_spec: type: openapi cli: name: myapp # Binary name on $PATH bin: cli/bin/myapp # Path relative to repo root

The platform automatically makes the CLI available to agents that have `with_apis: [api]`.
services: api: build: context: ./apps/api ports: [3000] x-eve: api_spec: type: openapi cli: name: myapp # $PATH中的二进制文件名 bin: cli/bin/myapp # 相对于代码库根目录的路径

平台会自动向包含`with_apis: [api]`的Agent提供该CLI工具。

Design Rules

设计规则

Command Structure

命令结构

Map CLI commands to your domain, not HTTP endpoints:
undefined
将CLI命令映射到你的业务领域,而非HTTP端点:
undefined

Good — domain vocabulary

推荐 — 使用领域术语

eden map show eden changeset create --file data.json eden changeset accept CS-45
eden map show eden changeset create --file data.json eden changeset accept CS-45

Bad — HTTP vocabulary

不推荐 — 使用HTTP术语

eden get /projects/123/map eden post /changesets --body data.json
undefined
eden get /projects/123/map eden post /changesets --body data.json
undefined

Output Contract

输出约定

  • Default: human-readable (tables, summaries)
  • --json
    : machine-readable JSON on stdout
  • Errors: stderr, exit code 1, actionable message
bash
eden projects list              # Table: ID  NAME  CREATED
eden projects list --json       # [{"id":"...","name":"..."}]
eden changeset accept BAD-ID    # stderr: "Changeset BAD-ID not found"
  • 默认输出:人类可读格式(表格、摘要)
  • --json
    参数
    :标准输出为机器可读的JSON
  • 错误信息:输出到标准错误,退出码为1,附带可操作提示
bash
eden projects list              # 表格格式:ID  NAME  CREATED
eden projects list --json       # [{"id":"...","name":"..."}]
eden changeset accept BAD-ID    # 标准错误输出: "Changeset BAD-ID not found"

Auto-Detection Pattern

自动检测模式

When only one resource exists, auto-detect instead of requiring flags:
typescript
async function autoDetectProject(): Promise<string> {
  const projects = await api('GET', '/projects');
  if (projects.length === 1) return projects[0].id;
  if (projects.length === 0) {
    console.error('No projects found.');
    process.exit(1);
  }
  console.error('Multiple projects. Use --project <id>:');
  for (const p of projects) console.error(`  ${p.id}  ${p.name}`);
  process.exit(1);
}
当仅存在一个资源时,自动检测资源,无需用户指定参数:
typescript
async function autoDetectProject(): Promise<string> {
  const projects = await api('GET', '/projects');
  if (projects.length === 1) return projects[0].id;
  if (projects.length === 0) {
    console.error('未找到任何项目。');
    process.exit(1);
  }
  console.error('存在多个项目,请使用--project <id>参数指定:');
  for (const p of projects) console.error(`  ${p.id}  ${p.name}`);
  process.exit(1);
}

Progressive Help

渐进式帮助

Every command and subcommand has
--help
:
$ eden --help
Eden story map CLI

Commands:
  projects    Manage projects
  map         View story map
  changeset   Create and review changesets
  persona     Manage personas
  question    Manage questions
  search      Search the map
  export      Export project data

$ eden changeset --help
Commands:
  create   Create a changeset from JSON file
  accept   Accept a pending changeset
  reject   Reject a pending changeset
  list     List changesets for a project
每个命令和子命令都支持
--help
参数:
$ eden --help
Eden story map CLI

Commands:
  projects    管理项目
  map         查看故事地图
  changeset   创建并评审变更集
  persona     管理角色
  question    管理问题
  search      搜索地图
  export      导出项目数据

$ eden changeset --help
Commands:
  create   从JSON文件创建变更集
  accept   接受待处理的变更集
  reject   拒绝待处理的变更集
  list     列出项目的变更集

Environment Variables

环境变量

The CLI reads these from the environment (injected automatically by Eve):
VariablePurposeSet By
EVE_APP_API_URL_{SERVICE}
Base URL of the app APIPlatform (
--with-apis
)
EVE_JOB_TOKEN
Bearer auth tokenPlatform (per job)
EVE_PROJECT_ID
Eve platform project IDPlatform
EVE_ORG_ID
Eve platform org IDPlatform
The CLI never requires manual configuration.
CLI从环境中读取以下变量(由Eve自动注入):
变量名用途设置方
EVE_APP_API_URL_{SERVICE}
应用API的基础URL平台(
--with-apis
参数)
EVE_JOB_TOKEN
Bearer认证令牌平台(每个任务)
EVE_PROJECT_ID
Eve平台的项目ID平台
EVE_ORG_ID
Eve平台的组织ID平台
CLI无需手动配置。

Testing Locally

本地测试

Set env vars and run directly:
bash
export EVE_APP_API_URL_API=http://localhost:3000
export EVE_JOB_TOKEN=$(eve auth token)
设置环境变量后直接运行:
bash
export EVE_APP_API_URL_API=http://localhost:3000
export EVE_JOB_TOKEN=$(eve auth token)

Test individual commands

测试单个命令

./cli/bin/myapp projects list ./cli/bin/myapp items create --file test-data.json
undefined
./cli/bin/myapp projects list ./cli/bin/myapp items create --file test-data.json
undefined

Bundling Details

打包细节

Use esbuild to produce a single file with zero runtime dependencies:
  • --bundle
    inlines all imports (including
    commander
    )
  • --platform=node
    targets Node.js built-ins
  • --target=node20
    matches Eve runner environment
  • --format=cjs
    uses CommonJS (commander uses
    require()
    internally)
  • Shebang prepended separately (esbuild
    --banner
    escapes
    !
    in
    #!/usr/bin/env
    )
  • Result: 50-200KB single file, no
    node_modules
    needed at runtime
Commit
cli/bin/myapp
to the repo so it's available immediately after clone.
使用esbuild生成单文件、零运行时依赖的可执行文件:
  • --bundle
    :内联所有导入(包括
    commander
  • --platform=node
    :针对Node.js内置模块
  • --target=node20
    :匹配Eve运行环境
  • --format=cjs
    :使用CommonJS(commander内部使用
    require()
  • 单独添加shebang(esbuild的
    --banner
    会转义
    #!/usr/bin/env
    中的
    !
  • 结果:50-200KB的单文件,运行时无需
    node_modules
cli/bin/myapp
提交到代码库,以便克隆后即可直接使用。

Image-Based Distribution (Compiled CLIs)

基于镜像的分发(编译型CLI)

For Go, Rust, or other compiled CLIs:
yaml
services:
  api:
    x-eve:
      cli:
        name: myapp
        image: ghcr.io/org/myapp-cli:latest
Build a Docker image with the CLI binary at
/cli/bin/myapp
:
dockerfile
FROM rust:1.77 AS build
COPY . .
RUN cargo build --release

FROM busybox:stable
COPY --from=build /app/target/release/myapp /cli/bin/myapp
The platform injects it via init container (same pattern as toolchains, ~2-5s latency).
对于Go、Rust或其他编译型语言开发的CLI:
yaml
services:
  api:
    x-eve:
      cli:
        name: myapp
        image: ghcr.io/org/myapp-cli:latest
构建Docker镜像,将CLI二进制文件放置在
/cli/bin/myapp
路径下:
dockerfile
FROM rust:1.77 AS build
COPY . .
RUN cargo build --release

FROM busybox:stable
COPY --from=build /app/target/release/myapp /cli/bin/myapp
平台会通过初始化容器注入该CLI(与工具链模式相同,延迟约2-5秒)。

See Also

相关链接

  • references/app-cli.md
    in eve-read-eve-docs for the full technical reference
  • references/manifest.md
    for manifest schema details
  • references/eve-sdk.md
    for the Eve Auth SDK (server-side token verification)
  • eve-read-eve-docs中的
    references/app-cli.md
    :完整技术参考文档
  • references/manifest.md
    :清单文件Schema细节
  • references/eve-sdk.md
    :Eve认证SDK(服务端令牌验证)