goke
Build CLIs like you'd build an API. Type-safe, chainable, zero dependencies.
goke是一个TypeScript CLI框架,具有 荣誉-如API。你链 .use() 用于中间件和 .command() 对于路由-与应用于终端的REST API相同的心理模型。
import { goke } from 'goke'
import { z } from 'zod'
const cli = goke('deploy')
// middleware — runs before every command
cli
.option('--env ', z.enum(['staging', 'production']).default('staging').describe('Target environment'))
.use((options, { console }) => {
console.log(`Environment: ${options.env}`)
})
// commands — like route handlers
cli
.command('up', 'Deploy the app')
.option('--dry-run', 'Preview without deploying')
.action((options, { console, process }) => {
console.log(`Deploying from ${process.cwd}`)
})
cli
.command('logs ', 'Stream logs')
.option('--lines ', z.number().default(100).describe('Lines to tail'))
.action((id, options) => streamLogs(id, options.lines))
cli.help()
cli.completions()
cli.parse()特性
- 霍诺喜欢链锁 —
.use()对于中间件,.command()对于处理人员。以与设计REST API相同的方式构建CLI。 - Zod型安全 --将Zod模式传递给
.option()并免费获得自动强制转换、TypeScript推理和帮助文本。适用于Valibot、ArkType或任何标准模式库。 - MCP服务器分为两条线路 --将整个CLI暴露为MCP服务器
createMcpAction({ cli })每个命令都成为一个工具,为Claude Desktop、Cursor、VS Code和 任何MCP客户端. - JustBash支持 —
cli.createJustBashCommand()将CLI公开为沙盒JustBash命令。相同的操作代码,无需更改。 - 空格分隔的子命令 —
git remote add,mcp login,db migrate--多词命令开箱即用。 - 注入
{ fs, console, process }--命令接收可移植的运行时上下文。在测试中交换它,或者让JustBash用沙箱替换它。无全球副作用。 - 壳体完井 —
cli.completions()为zsh和bash添加Tab补全功能。安装一次mycli completions install;补全会自动保持最新,因为shell每次按Tab键都会调用二进制文件。 - 零运行时依赖关系 --安装
goke而无需将额外的运行时包拉入CLI。 - Agent检测 —
isAgent和detectAgent()告诉您该进程是否在AI编码代理(Claude、Cursor、Codex、Gemini等)内运行。跳过提示,更喜欢结构化输出,自动调整行为。
安装
npm install goke为AI代理安装技能
npx -y skills add remorses/goke这为AI编码代理安装了存储库技能。在这个仓库中,所提供的技能位于 skills/goke/SKILL.md.
Agent检测
goke可以检测您的CLI是否由AI编码代理运行。这对于跳过交互式提示、首选结构化输出或在代理驱动CLI时调整行为非常有用。
import { isAgent, agent, agentInfo, detectAgent } from 'goke'
if (isAgent) {
console.log(`Running inside ${agent}`) // e.g. "claude", "cursor", "codex"
}
// Re-run detection (e.g. after env changes)
const info = detectAgent()
console.log(info) // { name: "claude" } or {}检测通过扫描已知代理设置的环境变量来工作。这 AI_AGENT env-var可以显式设置为覆盖检测。
支持的代理: cursor, claude, devin, replit, gemini, codex, auggie, opencode, kiro, goose, pi
一种常见的模式是将代理检测与交互式提示回退相结合:
import { goke, isAgent } from 'goke'
import * as clack from '@clack/prompts'
cli
.command('deploy', 'Deploy the app')
.option('--env [env]', z.enum(['staging', 'production']).optional().describe('Target environment'))
.action(async (options) => {
let env = options.env
if (!env) {
if (isAgent || !process.stdin.isTTY) {
// Agent or non-TTY: fail with clear usage instead of hanging on a prompt
console.error('Missing --env. Usage: deploy --env staging|production')
process.exit(1)
}
const choice = await clack.select({
message: 'Which environment?',
options: [
{ value: 'staging', label: 'Staging' },
{ value: 'production', label: 'Production' },
],
})
if (clack.isCancel(choice)) process.exit(0)
env = choice
}
})检测逻辑从 unjs/std-env。检查的特定于代理的环境变量包括 CLAUDECODE, CURSOR_AGENT, CODEX_SANDBOX, GEMINI_CLI, OPENCODE以及其他。最后检查基于IDE的代理(Cursor、Devin、Kiro),以便在这些IDE中运行的代理首先被它们自己的环境变量检测到。
终端颜色
哥克运送一艘被出卖的船 colors 导出(picocolors API),因此使用goke构建的CLI不需要安装 picocolors, chalk, kleur或任何其他颜色库。树中的依赖性少了一个。
import { colors } from 'goke'
console.log(colors.green('success'))
console.log(colors.red('error'))
console.log(colors.bold(colors.cyan('info')))切勿安装单独的颜色库。 导入 colors 从goke代替。它支持所有标准格式化程序: bold, dim, italic, underline, red, green, yellow, blue, magenta, cyan, gray, bgRed, bgGreen等等。颜色支持从以下位置自动检测 NO_COLOR, FORCE_COLOR, --no-color, --color以及TTY状态。
用法
简单解析
使用goke作为简单的参数解析器:
import { goke } from 'goke'
import { z } from 'zod'
const cli = goke()
cli.option(
'--type [type]',
z.string().default('node').describe('Choose a project type'),
)
cli.option('--name ', 'Provide your name')
cli.command('lint [...files]', 'Lint files').action((files, options, { console, process }) => {
console.log(files, options, process.cwd)
})
cli
.command('build [entry]', 'Build your app')
.option('--minify', 'Minify output')
.example('build src/index.ts')
.example('build src/index.ts --minify')
.action(async (entry, options, { console, process }) => { // options is type safe! no need to type it
console.log(entry, options, process.env.NODE_ENV)
})
cli.example((bin) => `${bin} lint src/**/*.ts`)
// Display help message when `-h` or `--help` appears
cli.help()
cli.completions()
// Display version number when `-v` or `--version` appears
cli.version('0.0.0')
cli.parse()定义示例时,帮助输出包括 例子 部分。
帮助中的命令示例
使用 .example(...) 根据命令(或 cli)在帮助中显示用法代码片段:
import { goke } from 'goke'
const cli = goke('mycli')
cli
.command('deploy', 'Deploy current app')
.option('--env ', 'Target environment')
.example('mycli deploy --env production')
.example('mycli deploy --env staging')
.action(() => {})
cli.example((bin) => `${bin} deploy --env production`)
cli.help()
cli.completions()
cli.parse()丰富的多行命令描述(string-dedent)
当命令需要长描述时(包括项目符号、引号、内联代码和 多个示例),使用 string-dedent 以保持源代码的可读性,同时保留干净的帮助输出。
安装:
npm install string-dedent带有详细命令描述的示例:
import { goke } from 'goke'
import dedent from 'string-dedent'
const cli = goke('acme')
cli
.command(
'release ',
dedent`
Publish a versioned release to your distribution channels.
- **Validates** release metadata and changelog before publishing.
- **Builds** production artifacts with reproducible settings.
- **Tags** git history using semantic version format.
- **Publishes** to npm and creates release notes.
> Recommended flow: run with \`--dry-run\` first in CI to verify output.
Examples:
- \`acme release 2.4.0 --channel stable\`
- \`acme release 2.5.0-rc.1 --channel beta --dry-run\`
- \`acme release 3.0.0 --notes-file ./docs/releases/3.0.0.md\`
`,
)
.option('--channel ', 'Target channel: stable, beta, alpha')
.option('--notes-file
', 'Markdown file used as release notes')
.option('--dry-run', 'Preview every step without publishing')
.action((version, options, { console, process }) => {
console.log('release', version, options, process.cwd)
})
cli
.command(
'db migrate',
dedent`
Apply pending database migrations in a controlled sequence.
- Runs migrations in timestamp order.
- Stops immediately on first failure.
- Prints SQL statements when \`--verbose\` is enabled.
- Supports smoke-testing with \`--dry-run\`.
> Safety: always run this command against staging before production.
Examples:
- \`acme db migrate\`
- \`acme db migrate --target 20260210120000_add_users\`
- \`acme db migrate --dry-run --verbose\`
`,
)
.option('--target ', 'Apply up to a specific migration id')
.option('--dry-run', 'Print plan only, do not execute SQL')
.option('--verbose', 'Show each executed statement')
.action((options, { console, process }) => {
console.log('migrate', options, process.stdin)
})
cli.help()
cli.completions()
cli.parse()为什么这种模式效果很好:
dedent使模板文本在源文件中可读。- 帮助文本保持对齐,没有额外的前导空格。
- 您可以包含用户已经识别的丰富格式模式:
列表、引号和内联命令片段。
- 随着CLI的增长,长篇描述仍然可以维护。
重要!string dedent的起始行和结束行必须始终为空,否则将抛出运行时错误。永远不要拒绝non empty line.富有的 .example(...) 块与 dedent
您还可以使用 dedent 在 .example(...) 因此,示例在代码中保持可读性 在帮助输出中呈现良好。一个有用的模式是 第一行a # 评论 这就解释了这种情况。
import { goke } from 'goke'
import dedent from 'string-dedent'
const cli = goke('tuistory')
cli
.command('start', 'Start an interactive session')
.example(dedent`
# Launch and immediately check what the app shows
tuistory launch "claude" -s ai && tuistory -s ai snapshot --trim
`)
.example(dedent`
# Start a focused coding session with explicit context
tuistory start --agent code --context "Fix OAuth callback timeout"
`)
.example(dedent`
# Recover recent activity and inspect the latest run details
tuistory runs list --limit 5 && tuistory runs show --latest
`)
.action(() => {
// command implementation
})
cli
.command('deploy', 'Deploy current workspace')
.example(dedent`
# Dry-run deployment first to validate plan
tuistory deploy --env staging --dry-run
`)
.example(dedent`
# Deploy production with release notes attached
tuistory deploy --env production --notes ./docs/release.md
`)
.action(() => {
// command implementation
})
cli.help()
cli.completions()
cli.parse()笔记:
- 将每个示例集中在一个工作流程上。
- 使用第一个
#行作为人类可读的意图标签。 - 保持命令行可复制粘贴(避免占位符过多的示例)。
今天呈现的示例如下:
- 获取根帮助(
deploy --help),root/default命令中的示例出现在 例子 最后一节。 - 获取子命令帮助(
deploy logs --help),该特定子命令中的示例将单独显示 例子 最后一节。
内联快照样式输出(许多命令):
deploy
Usage:
$ deploy [options]
Commands:
deploy Deploy the current project
init Initialize a new project
login Authenticate with the server
logout Clear saved credentials
status Show deployment status
logs Stream logs for a deployment
Options:
--env Target environment
--dry-run Preview without deploying
-h, --help Display this message
Examples:
# Deploy to staging first
deploy --env staging --dry-rundeploy
Usage:
$ deploy logs
Options:
--follow Follow log output
--lines Number of lines (default: 100)
-h, --help Display this message
Description:
Stream logs for a deployment
Examples:
# Stream last 200 lines for a deployment
deploy logs dep_123 --lines 200
# Keep following new log lines
deploy logs dep_123 --follow许多带有根命令的命令
使用 '' 作为命令名,用于定义在没有给出子命令时运行的根命令。这对于具有主操作和多个子命令的CLI非常有用:
import { goke } from 'goke'
import { z } from 'zod'
const cli = goke('deploy')
// Root command — runs when user types just `deploy`
cli
.command('', 'Deploy the current project')
.option(
'--env ',
z.string().default('production').describe('Target environment'),
)
.option('--dry-run', 'Preview without deploying')
.action((options, { console, process }) => {
console.log(`Deploying to ${options.env} from ${process.cwd}...`)
})
// Subcommands
cli
.command('init', 'Initialize a new project')
.option('--template ', 'Project template')
.action((options, { console, process }) => {
console.log('Initializing project in', process.cwd)
})
cli.command('login', 'Authenticate with the server').action((options, { console, process }) => {
console.log('Opening browser for login from', process.cwd)
})
cli.command('logout', 'Clear saved credentials').action((options, { console, process }) => {
console.log('Logged out', process.env.USER)
})
cli
.command('status', 'Show deployment status')
.option('--json', 'Output as JSON')
.action((options, { console, process }) => {
console.log('Fetching status from', process.cwd)
})
cli
.command('logs ', 'Stream logs for a deployment')
.option('--follow', 'Follow log output')
.option('--lines ', z.number().default(100).describe('Number of lines'))
.action((deploymentId, options, { console, process }) => {
console.log(`Streaming logs for ${deploymentId} from ${process.cwd}...`)
})
cli.help()
cli.completions()
cli.version('1.0.0')
cli.parse()deploy # runs root command (deploy to production)
deploy --env staging --dry-run # root command with options
deploy init --template react # subcommand
deploy login # subcommand
deploy logs abc123 --follow # subcommand with args + options
deploy --help # shows all commands将大型CLI拆分为文件
随着CLI的增长,在自己的文件中定义每个命令(或命令组),并用 .use()这使每个文件都保持专注,避免了一个巨大的入口点。
// commands/deploy.ts
import { goke } from 'goke'
import { z } from 'zod'
export default goke()
.command('deploy', 'Deploy the app')
.option('--env ', z.enum(['staging', 'production']).describe('Target environment'))
.option('--dry-run', 'Preview without deploying')
.action((options, { console, process }) => {
console.log(`Deploying to ${options.env} from ${process.cwd}`)
})// commands/auth.ts — multiple commands need a variable
import { goke } from 'goke'
import { z } from 'zod'
const auth = goke()
auth
.command('login', 'Authenticate with the server')
.option('--token [token]', z.string().describe('API token (skips browser login)'))
.action(async (options, { fs, console }) => {
const token = options.token ?? await browserOAuth()
await fs.mkdir('.mycli', { recursive: true })
await fs.writeFile('.mycli/auth.json', JSON.stringify({ token }), 'utf8')
console.log('Logged in')
})
auth
.command('logout', 'Clear saved credentials')
.action(async (options, { fs, console }) => {
await fs.rm('.mycli/auth.json', { force: true })
console.log('Logged out')
})
export default auth// cli.ts
import { goke } from 'goke'
import deploy from './commands/deploy.js'
import auth from './commands/auth.js'
const cli = goke('mycli')
.use(deploy)
.use(auth)
cli.help()
cli.completions()
cli.version('1.0.0')
cli.parse()比起只导入动作函数,更喜欢这样。 当您在goke中内联定义命令时,动作回调的类型将根据 .command() 和 .option() 链。如果将操作拆分为单独的函数,则必须手动复制选项类型以保持同步。随着 .use(subCli),这些类型仍然来源于真理的来源。
// BAD: duplicated types that can drift out of sync
import type { DeployOptions } from './types.js'
export function deployAction(options: DeployOptions) { /* ... */ }
// GOOD: types are always derived from the goke chain
export default goke()
.command('deploy', 'Deploy')
.option('--env ', z.enum(['staging', 'production']).describe('Environment'))
.action((options) => {
options.env // ← always in sync, inferred from the chain
})当 .use(subCli) 仅被调用 命令 组成了父。子CLI上定义的中间件和全局选项不会被复制。改为在父CLI上定义共享中间件。
全局选项和中间件
全局选项在CLI实例上定义,并应用于所有命令。使用 .use() 注册在任何命令操作之前运行的中间件,这对于对设置日志、初始化状态或配置服务等全局选项做出反应非常有用。
更喜欢注射 { fs, console, process } 全球争论 console, process.exit,或直接 node:fs/promises 进口。它使命令更容易测试,并允许相同的命令代码在JustBash等备用运行时中运行。
process.cwd, process.stdin,以及 process.env 来自活动运行时:
- 在正常的Node.js运行中,
process.cwd和process.env反映宿主进程,同时process.stdin默认为空字符串,除非您自己注入。 - 在JustBash运行中,这些相同的字段是从沙盒执行上下文中填充的。
中间件按照注册顺序运行,在选项解析和验证之后,但在匹配的命令之前 .action() 回拨。
文件系统访问
注射 fs object是读取或写入CLI状态的推荐方式。
- 在正常的Node.js运行中,
fs默认为node:fs/promises - 在JustBash运行中,
goke在JustBash虚拟文件系统上交换兼容适配器
这使得存储风格的命令在两种环境中都能工作,而无需在运行时细节上进行分支。
cli
.command('login', 'Save auth token')
.option('--token ', z.string().describe('Auth token'))
.action(async (options, { fs, console, process }) => {
await fs.mkdir('.mycli', { recursive: true })
await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8')
console.log('saved credentials in', process.cwd)
})
cli
.command('whoami', 'Read saved auth token')
.action(async (options, { fs, console, process }) => {
const auth = await fs.readFile('.mycli/auth.json', 'utf8')
console.log(auth, process.env.USER)
})更喜欢注射 fs 用于CLI存储,而不是导入 node:fs/promises 直接内部行动。这使得该命令可以移植到JustBash,更容易测试。
路径处理
使用注射的相对路径 fs 用于常规CLI存储路径。当助手需要从当前目录解析时,注入pass process.cwd 进入那个助手,并从那里下定决心。
await fs.mkdir('.mycli', { recursive: true })
await fs.writeFile('.mycli/auth.json', json, 'utf8')
console.log('running from', process.cwd)为什么这样做:
- 在正常的Node.js运行中,相对路径会根据主机cwd解析。
- 在JustBash运行中,相同的相对路径对沙箱cwd进行解析。
process.cwd在两种环境中都反映了特定于运行时的cwd。
goke 还导出运行时类型,因此辅助函数可以使用依赖注入,而无需使用全局变量:
import { goke } from 'goke'
import type { GokeFs, GokeProcess } from 'goke'
async function saveAuthToken(args: {
fs: GokeFs
process: GokeProcess
token: string
}) {
await args.fs.mkdir('.mycli', { recursive: true })
await args.fs.writeFile('.mycli/auth.json', JSON.stringify({
token,
cwd: args.process.cwd,
}), 'utf8')
}
const cli = goke('mycli')
cli
.command('login ', 'Save auth token')
.action(async (token, options, { fs, process, console }) => {
await saveAuthToken({ fs, process, token })
console.log('saved credentials')
})import { goke } from 'goke'
import { z } from 'zod'
const cli = goke('mycli')
cli
.option('--verbose', z.boolean().default(false).describe('Enable verbose logging'))
.option('--api-url [url]', z.string().default('https://api.example.com').describe('API base URL'))
.use((options, { console, process }) => {
// options.verbose and options.apiUrl are fully typed here
if (options.verbose) {
console.log('verbose mode enabled in', process.cwd)
}
})
cli
.command('deploy ', 'Deploy to an environment')
.option('--dry-run', 'Preview without deploying')
.action((env, options, { console, process }) => {
// options includes both command options (dryRun) and global options (verbose, apiUrl)
console.log(`Deploying to ${env} via ${options.apiUrl} from ${process.cwd}`)
})
cli
.command('status', 'Show deployment status')
.action((options, { console, process }) => {
console.log('Checking status...', process.stdin)
})
cli.help()
cli.completions()
cli.parse()类型安全是位置安全的——每个 .use() callback只看到链中前面声明的选项:
cli
.option('--verbose', z.boolean().default(false).describe('Verbose'))
.use((options, { process }) => {
options.verbose // boolean — typed
process.argv // string[] — typed
process.cwd // string — typed
process.env // Record — typed
process.stdin // string — typed
options.port // TypeScript error — not declared yet
})
.option('--port
', z.number().describe('Port'))
.use((options, { console, process }) => {
options.verbose // boolean — still visible
options.port // number — now visible
console.error('ready', process.cwd)
})中间件支持异步功能。如果任何中间件是异步的,则剩余的中间件和命令操作将作为promise链接:
cli
.option('--token ', z.string().describe('API token'))
.use(async (options, { console, process }) => {
const client = await connectToApi(options.token)
globalState.client = client
console.log('connected', process.env.NODE_ENV)
})命令特定选项
您可以将选项附加到命令。
import { goke } from 'goke'
const cli = goke()
cli
.command('rm ', 'Remove a dir')
.option('-r, --recursive', 'Remove recursively')
.action((dir, options, { console, process }) => {
console.log('remove ' + dir + (options.recursive ? ' recursively' : ''), process.cwd)
})
cli.help()
cli.completions()
cli.parse()空格分隔的子命令
goke支持类似git的嵌套子命令的多字命令名:
import { goke } from 'goke'
const cli = goke('mycli')
cli.command('mcp login ', 'Login to MCP server').action((url, options, { console, process }) => {
console.log('Logging in to', url, 'from', process.cwd)
})
cli.command('mcp logout', 'Logout from MCP server').action((options, { console, process }) => {
console.log('Logged out', process.env.USER)
})
cli
.command('git remote add ', 'Add a git remote')
.action((name, url, options, { console, process }) => {
console.log('Adding remote', name, url, 'from', process.cwd)
})
cli.help()
cli.completions()
cli.parse()基于模式的类型强制
将标准模式(如Zod)作为第二个参数传递给 .option() 用于自动类型强制。从架构中提取描述和默认值:
import { goke } from 'goke'
import { z } from 'zod'
const cli = goke()
cli
.command('serve', 'Start server')
.option('--port
', z.number().describe('Port number'))
.option('--host [host]', z.string().default('localhost').describe('Hostname'))
.option('--workers ', z.int().describe('Worker count'))
.option('--tags ', z.array(z.string()).describe('Tags (repeatable)'))
.option('--verbose', 'Verbose output')
.action((options, { console, process }) => {
// options.port is number, options.host is string, etc.
console.log(options, process.env.NODE_ENV)
})
cli.parse()重要提示: 使用架构时 .default(),做 不 重复描述字符串中的默认值。框架会自动附加 (default: ) 以帮助从模式默认值输出。写作 .default(100).describe('Number of lines (default: 100)') 将显示默认值两次。
第二个参数接受任何实现 标准架构,包括:
- 萨德 v4.2+(例如。
z.number(),z.string(),z.array(z.number())) - 选项, ArkType,以及其他与标准架构兼容的库
隐藏弃用的选项
使用Zod将选项标记为已弃用 .meta({ deprecated: true })。弃用的选项在帮助输出中隐藏,但仍适用于解析,这对向后兼容性很有用。
import { goke } from 'goke'
import { z } from 'zod'
const cli = goke()
cli
.command('serve', 'Start server')
// Deprecated option: hidden from --help, still parses
.option('--old-port
', z.number().meta({ deprecated: true, description: 'Use --port instead' }))
// Current option: visible in help
.option('--port
', z.number().describe('Port number'))
.action((options, { console, process }) => {
const port = options.port ?? options.oldPort
console.log('Starting on port', port, 'from', process.cwd)
})
cli.help()
cli.completions()
cli.parse()当用户运行时 --help,不推荐的选项不会出现,但是 --old-port 3000 仍然有效。
括号
在命令名称中使用括号时,尖括号表示必需的命令参数,方括号表示可选参数。
在选项名称中使用括号时,尖括号表示需要字符串/数字值,而方括号表示该值是可选的。
可选性仅由括号语法决定,而不是由模式决定。 [square brackets] 使选项可选,而不管架构是否为 z.string() 或 z.string().optional().模式的 .optional() 从未对此进行过咨询——它只影响类型强制。这意味着 z.string() 随着 [--name] 被视为可选:如果省略了标记, options.name 是 undefined 即使模式没有 .optional().
可选值标志-- --flag vs --flag value vs省略
用方括号声明的旗帜(--host [host])有 三种不同的运行时状态,不是两个。用户可以:
- 完全省略国旗 --没有
--host完全在命令行上 - 光着身子传递旗帜 —
--host它本身没有任何价值 - 传递带有值的标志 —
--host example.com
goke通过一个单一的程序处理所有三个箱子 string | undefined 类型。没有 boolean 在联盟中,裸露的旗帜被正常化 空字符串 '' 因此调用者只处理字符串:
cli
.command('serve', 'Start the server')
.option('--host [host]', 'Optional host override')
.action((options) => {
// options.host: string | undefined
// --host → '' (flag present, no value)
// --host example.com → 'example.com'
// (omitted) → undefined
})检测每个病例:
.action((options) => {
if (options.host === undefined) {
// Flag was not passed at all — use a sensible default
console.log('using default host: localhost')
} else if (options.host === '') {
// Flag was passed bare: `--host` with no value following it
// Treat this as an explicit "opt in, but use the default/automatic value"
console.log('host flag passed with no value — enabling auto-discovery')
} else {
// Flag was passed with an explicit value
console.log(`host = ${options.host}`)
}
})在大多数情况下,你不需要三重区别 --一个简单的truthy检查将“省略”和“裸旗”折叠到同一个“回退到默认”分支中:
.action((options) => {
// `--host` bare AND omitted both fall through to the default
const host = options.host || 'localhost'
startServer({ host })
})保留 === '' 检查“无值选择加入”是否是一个有意义的信号,与“省略标志”不同——例如, --direct 意思是“自动发现Chrome实例”vs --direct ws://… 意思是“连接到此特定端点”与否 --direct 意思是“不要使用直接模式”。
重大变更通知(代码6.6.0): 以前的版本显示了裸机标志booleantrue内部astring | boolean | undefined工会,迫使每个呼叫站点都写typeof options.host === 'string' ? options.host : undefined使用的代码options.host === true要检测裸标志,必须更新为options.host === ''基于模式的可选标志.default(...)不受影响——当标志被裸传递时,默认值仍然生效。
被否定的选项
允许值为的选项 false,您需要手动指定一个否定选项:
cli
.command('build [project]', 'Build a project')
.option('--no-config', 'Disable config file')
.option('--config
', 'Use a custom config file')变量论证
命令的最后一个参数可以是可变的。要使参数可变,您必须添加 ... 到参数名称的开头:
cli
.command('build [...otherFiles]', 'Build your app')
.option('--foo', 'Foo option')
.action((entry, otherFiles, options, { console, process }) => {
console.log(entry)
console.log(otherFiles)
console.log(options, process.stdin)
})双破折号 -- (选项结束)
这 -- 代币标志着期权的结束。一切之后 -- 可通过以下方式获得 options['--'] 作为一个单独的数组,不混合到位置参数中。这使您可以区分命令自己的参数和passthrough参数——与 doppler, npm, pnpm,以及 docker.
options['--'] 是 始终在场 关于推断选项类型 string[].如果没有 -- 令牌出现在命令行上,它是空数组——你永远不需要用 || 或 ?. 或a Array.isArray 演员。
import { goke } from 'goke'
import { z } from 'zod'
import { execSync } from 'child_process'
const cli = goke('runner')
cli
.command('run ', 'Run a script with injected environment variables')
.option('--env ', z.enum(['dev', 'staging', 'production']).describe('Target environment'))
.example('# Pass extra flags to the child script via --')
.example('runner run --env staging server.js -- --port 3000 --verbose')
.action((script, options) => {
// runner run --env staging server.js -- --port 3000 --verbose
// script = 'server.js' (positional arg)
// options.env = 'staging' (runner's own option)
// options['--'] = ['--port', '3000', '--verbose'] (passthrough, always string[])
const secrets = loadSecrets(options.env)
const extraArgs = options['--'].join(' ')
execSync(`node ${script} ${extraArgs}`, {
env: { ...process.env, ...secrets },
stdio: 'inherit',
})
})
cli.help()
cli.completions()
cli.parse()runner run --env staging server.js -- --port 3000 --verbose
# ^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
# runner option positional passthrough (options['--'])没有 --,旗帜像 --port 将被解析为runner选项,并以“未知选项”失败 --port“The -- 告诉goke停止解析并单独收集其余部分。
点嵌套选项
点嵌套选项将合并为单个选项。
cli
.command('build', 'desc')
.option('--env ', 'Set envs')
.example('--env.API_SECRET xxx')
.action((options, { console, process }) => {
console.log(options, process.env.API_SECRET)
})默认命令
注册一个在没有其他命令匹配时使用的命令。
cli
.command('[...files]', 'Build files')
.option('--minimize', 'Minimize output')
.action((files, options, { console, process }) => {
console.log(files)
console.log(options.minimize, process.cwd)
})错误处理
要全局处理命令错误,请执行以下操作:
cli.parse(process.argv).catch((error) => {
const message = error instanceof Error ? error.stack : String(error)
process.stderr.write(String(message) + '\n')
process.exit(1)
})操作后流程关闭
parse() 仅在匹配的命令操作和中间件完成后解析。这使得在解析启动后台工作、本机句柄或操作期间长时间运行的子进程的CLI后添加进程关闭代码是安全的。
import { goke } from 'goke'
const cli = goke('render')
cli
.command('screenshot ', 'Render a screenshot')
.action(async (file, options, { console }) => {
await renderScreenshot(file)
console.log('saved screenshot')
})
await cli.parse(process.argv)
// The command is done here. Use manual exit only for runtimes that leave
// background handles alive after the user-visible work has completed.
setTimeout(() => process.exit(0), 500).unref()设置强制出口 之后 等待的解析调用,不在操作内部。这避免了切断仍属于该命令的未决写入、日志或清理。
使用模拟控制台进行测试并退出
因为goke得到了它的注射 { fs, console, process } 根据CLI配置的运行时依赖关系,测试可以直接覆盖它们并对调用进行断言。
import { describe, expect, test, vi } from 'vitest'
import { goke, GokeProcessExit } from 'goke'
describe('deploy command', () => {
test('writes output and exits with injected mocks', async () => {
const stdout = { write: vi.fn void>() }
const stderr = { write: vi.fn void>() }
const exit = vi.fn void>()
const cli = goke('acme', { stdout, stderr, exit })
cli
.command('deploy', 'Deploy the project')
.action((options, { console, process }) => {
console.log('deploying')
process.exit(2)
})
await expect(cli.parse(['node', 'acme', 'deploy'], { run: true }))
.rejects.toThrow(GokeProcessExit)
expect(stdout.write).toHaveBeenCalledWith('deploying\n')
expect(exit).toHaveBeenCalledWith(2)
expect(stderr.write).not.toHaveBeenCalled()
})
})直接测试命令操作
使用 .getAction() 从命令中提取键入的动作回调。这允许您在测试中直接调用操作,而无需解析argv,同时保持操作内联以确保类型安全。
import { describe, expect, test, vi } from 'vitest'
import { goke } from 'goke'
import { z } from 'zod'
const cli = goke('mycli')
const deployCmd = cli
.command('deploy', 'Deploy the app')
.option('--env ', z.enum(['staging', 'production']).describe('Target environment'))
.action((options, { console }) => {
console.log(`Deploying to ${options.env}`)
})
test('deploys to staging', () => {
const stdout = { write: vi.fn void>() }
const action = deployCmd.getAction()
action({ env: 'staging', '--': [] }, cli.createExecutionContext({ stdout }))
expect(stdout.write).toHaveBeenCalledWith('Deploying to staging\n')
})返回的函数与 .action() 回调,因此TypeScript在编译时捕获错误的参数类型。如果没有登记动作, .getAction() 投掷。
使用TypeScript
import { goke } from 'goke'
const cli = goke('my-program')不要手动键入 action 回调参数。goke根据命令签名和选项模式自动推断参数和选项类型。
import { goke } from 'goke'
import { z } from 'zod'
const cli = goke('my-program')
cli
.command('serve ', 'Start the app')
.option('--port
', z.number().default(3000).describe('Port number'))
.option('--watch', 'Watch files')
.action((entry, options, { console, process }) => {
// entry: string
// options.port: number
// options.watch: boolean
console.log(entry, options.port, options.watch, process.cwd)
})在浏览器中打开
openInBrowser 在默认浏览器中打开一个URL。在非TTY环境(CI、管道输出、代理)中,它会将URL打印到stderr,从而保持stdout对JSON和结构化输出的干净。
重要提示:openInBrowser是异步的,必须等待。没有await,在进程退出之前,浏览器可能无法打开。
import { openInBrowser } from 'goke'
await openInBrowser('https://example.com/dashboard')将goke CLI暴露给JustBash
使用 cli.createJustBashCommand() 将goke CLI作为单个JustBash自定义命令公开。JustBash命令名是单令牌可执行文件,但它们后面的goke CLI仍然可以使用多字子命令,如 child commandwithspaces.
import { goke } from 'goke'
import { z } from 'zod'
import { Bash } from 'just-bash'
const cli = goke('parent')
cli
.command('child commandwithspaces', 'Run nested command')
.option('--name ', z.string().describe('Name'))
.action((options, { console, process }) => {
console.log(`hello ${options.name} from ${process.cwd}`)
})
const bash = new Bash({
customCommands: [await cli.createJustBashCommand()],
})
await bash.exec('parent child commandwithspaces --name Tommy')更喜欢注射 { fs, console, process } 命令实现中的助手,因此相同的命令代码在常规CLI运行时和通过JustBash桥都能清晰地工作。注射 fs 默认为Node fs/promises,以及 process.cwd / process.env / process.stdin 在节点中反映主机值,但在内部反映沙盒值 createJustBashCommand().
使用真实的JustBash进行测试
当命令读取或写入文件时,通过real测试它 just-bash 使用现有的应用程序CLI。不要在测试体内定义CLI。
import { describe, expect, test } from 'vitest'
import { Bash, InMemoryFs } from 'just-bash'
import { cli } from '../src/cli'
describe('login command', () => {
test('writes auth state through the sandbox fs', async () => {
const virtualFs = new InMemoryFs()
await virtualFs.mkdir('/project', { recursive: true })
const bash = new Bash({
fs: virtualFs,
cwd: '/project',
customCommands: [await cli.createJustBashCommand()],
})
const result = await bash.exec('parent login --token Tommy')
expect(result.stdout).toBe('saved credentials\n')
expect(await virtualFs.readFile('/project/.mycli/auth.json', 'utf8')).toBe(
'{"token":"Tommy","cwd":"/project"}',
)
})
})每当CLI接触存储时,这是推荐的兼容性测试:在正常测试中运行同一CLI一次,在实际测试中运行一次 just-bash.
将CLI作为一种技能进行展示
如果使用goke构建CLI,请尽量减少技能,并将代理指向CLI帮助输出。将详细用法放在CLI代码和README中,而不是放在重复的技能文件中。
---
name: acme
description: >
acme is a deployment CLI. Always run `acme --help` before using it
to discover available commands, options, and usage examples.
---
# acme
Always run `acme --help` before using this CLI.
For subcommand details: `acme --help`用于代理友好CLI的YAML输出
当命令返回结构化数据时,在stdout上将其打印为YAML。YAML是人类可读输出和机器可处理输出之间的最佳中间地带:
- 人类可以一目了然地阅读它——按键上没有引号,标点符号噪音比JSON少。
- 代理商可以用
yq,相当于YAMLjq,以提取特定字段或过滤结果。 - 它比冗长的散文更有上下文效率:一个紧凑的YAML块在更少的令牌中传达相同的信息。
import { goke } from 'goke'
import { stringify } from 'yaml'
const cli = goke('deploy')
cli
.command('status', 'Show deployment status')
.action(async (options, { console }) => {
const status = await fetchStatus()
// Output structured data as YAML on stdout
console.log(stringify(status))
})输出示例:
deployment: prod-v2
status: running
replicas: 3
lastDeploy: "2026-01-15T10:30:00Z"
health:
cpu: 42%
memory: 1.2GB使用yq处理YAML输出
代理可以通过管道传输输出 yq 提取特定字段或过滤结果——与他们使用的方式相同 jq 使用JSON,但输出更清晰、更易读:
# Extract a single field
deploy status | yq '.deployment'
# Access nested fields
deploy status | yq '.health.cpu'
# Filter an array of results
deploy list | yq '.[] | select(.status == "running")'
# Combine multiple fields
deploy list | yq '.[] | {name: .name, status: .status}'
# Count items matching a condition
deploy list | yq '[.[] | select(.status == "error")] | length'保持stdout干净——将非YAML发送到stderr
如果命令在stdout上输出YAML,则所有无关的内容都必须转到stderr:错误消息、进度指示器、信息日志、警告。这使得stdout可以通过管道传输 yq 而不会破坏YAML解析。
cli
.command('deploy ', 'Deploy to environment')
.action(async (env, options, { console }) => {
// Progress and logs → stderr (won't pollute yq pipes)
console.error(`Deploying to ${env}...`)
console.error('Building artifacts...')
const result = await deploy(env)
// Structured result → stdout as YAML
console.log(stringify(result))
})现在代理可以干净地处理输出:
# Only the YAML result reaches yq — progress lines go to the terminal
deploy deploy production | yq '.version'如果发生错误,请抛出或写入 console.error / process.stderr,并以非零代码退出。当命令预期输出YAML时,切勿将错误文本混合到stdout中。
生成Markdown文档
generateDocs 为CLI中的每个命令创建markdown文档页面。每个页面都包含一个使用块、参数表、选项表、全局选项和示例。你会得到一个数组 { command, slug, content } 您可以根据需要将对象写入磁盘。
这对于为CLI自动生成文档并将其提供给文档平台非常有用,例如 全息照相机 或 明特利.
// scripts/generate-docs.ts
import { goke, generateDocs } from 'goke'
import { z } from 'zod'
import fs from 'node:fs'
const cli = goke('sentry')
.version('1.0.0')
.help()
.completions()
cli
.command('event view ', 'View details of a specific event')
.option('-w, --web', 'Open in browser')
.option('--spans ', z.string().default('3').describe('Span tree depth limit'))
.example('```\nsentry event view abc123\n```')
cli
.command('event list ', 'List events for an issue')
.option('-n, --limit
', z.number().default(25).describe('Number of events'))
.option('-q, --query ', 'Search query')
const pages = generateDocs({ cli })
fs.mkdirSync('docs', { recursive: true })
for (const page of pages) {
fs.writeFileSync(`docs/${page.slug}.md`, page.content)
console.log(`wrote docs/${page.slug}.md`)
}跑步 npx tsx scripts/generate-docs.ts 生成以下文件 docs/index.md, docs/event-view.md, docs/event-list.md。每个命令页看起来像这样:
# event view
View details of a specific event
## Usage
\```sh
sentry event view
\```
## Arguments
| Argument | Required | Description |
|----------|----------|-------------|
| `` | Yes | id |
## Options
| Option | Default | Description |
|--------|---------|-------------|
| `-w, --web` | - | Open in browser |
| `--spans ` | `3` | Span tree depth limit |
## Global Options
| Option | Default | Description |
|--------|---------|-------------|
| `-v, --version` | - | Display version number |
| `-h, --help` | - | Display this message |
## Examples
\```
sentry event view abc123
\```索引页面列出了所有带有链接的命令:
# sentry
Version: 1.0.0
## Commands
| Command | Description |
|---------|-------------|
| [`event view`](./event-view.md) | View details of a specific event |
| [`event list`](./event-list.md) | List events for an issue |
## Global Options
| Option | Default | Description |
|--------|---------|-------------|
| `-v, --version` | - | Display version number |
| `-h, --help` | - | Display this message |隐藏的命令和已弃用的选项将被自动排除。将此脚本添加到您的CI或 package.json 用于使文档与CLI代码保持同步的脚本。
贡献者备注
规则
- 对类型值使用基于架构的选项。
- 不要在中重复默认值
.describe(...)使用时.default().
- 不要手动键入操作回调参数;让戈克来推断它们。
- 更喜欢注射
{ fs, console, process }全球console,process.exit,或直接node:fs/promises进口。
- 使用带有注入的隐式cwd
fs用于CLI存储。当助手需要当前的cwd语义时,传递process.cwd从注入的上下文到该助手。
- 在应用程序代码中定义CLI,并在测试中导入相同的CLI;不要在兼容性测试中构建单独的CLI。
- 保持行动一致。 将操作内联到
.command().option().action()链。将其提取到单独的函数中会失去推断的类型安全性,并迫使您手动复制选项/arg类型。如果文件太大,请拆分为单独的文件,每个文件导出一个goke()实例并将其组合起来.use()。参见 将大型CLI拆分为文件。要直接测试操作,请使用.getAction()提取键入的回调。看 直接测试命令操作.
// BAD: action in a separate function — types are lost, args/options duplicated
projectsCli
.command("projects list", "List all projects")
.action((_options, ctx) => listProjectsAction(ctx));
// GOOD: action inline — types inferred from the chain
projectsCli
.command("projects list", "List all projects")
.action((_options, { console }) => {
// implementation here, fully typed
});
// GOOD: split into files with .use() when commands get large
// commands/projects.ts
export default goke()
.command("projects list", "List all projects")
.action((_options, { console }) => {
// implementation here
});版本
导入 package.json 并使用其版本字段,以便CLI自动保持同步:
import pkg from './package.json' with { type: 'json' }
cli.version(pkg.version)参考文献
CLI实例
CLI实例是通过调用 goke 功能:
import { goke } from 'goke'
const cli = goke()郭(名字?)
创建CLI实例,可选择指定将用于在帮助和版本消息中显示的程序名称。当未设置时,我们使用basename argv[1].
cli.命令(名称、描述、配置?)
- 类型:
(name: string, description: string) => Command
创建命令实例。支持空格分隔的子命令,如 mcp login.
config.allowUnknownOptions:boolean在此命令中允许未知选项。config.ignoreOptionDefaultValue:boolean不要在解析的选项中使用选项的默认值,只在帮助消息中显示它们。
cli.选项(名称、描述或模式?)
- 类型:
(name: string, descriptionOrSchema?: string | StandardJSONSchemaV1) => CLI
添加全局选项。第二个论点是:
- A. 字符串 用作描述文本
- A. 标准架构 (例如。
z.number().describe('Port'))--描述和默认值会自动从模式中提取
cli.use(回调)
- 类型:
(callback: (options: Opts, { fs, console, process }) => void | Promise) => CLI
注册一个在匹配的命令操作之前运行的中间件函数。中间件在经过选项解析和验证后,按照注册顺序运行。回调函数接收解析后的全局选项,根据所有 .option() 之前的呼叫 .use() 在链中,加上注射 { fs, console, process } 辅助对象。
cli.use(subCli)
- 类型:
(subCli: Goke) => CLI
将来自另一个goke实例的命令组合到此CLI中。上定义的所有命令 subCli 被合并到父级中。子CLI中的中间件和全局选项包括 不 复制。看 将大型CLI拆分为文件.
cli.parse(argv?)
- 类型: `(argv = process.argv) => Promise
`
解析argv,默认运行匹配的命令,并在异步中间件和操作完成后进行解析。通过 { run: false } 在不运行匹配命令的情况下解析和检查args/选项。
cli.version(版本,自定义标志?)
- 类型:
(version: string, customFlags = '-v, --version') => CLI
cli.help(回调?)
- 类型:
(callback?: HelpCallback) => CLI
cli.outHelp()
- 类型:
() => CLI
将帮助消息打印到stdout。
cli.clone(选项?)
- 类型:
(options?: GokeOptions) => Goke
使用所有命令、选项、中间件和事件侦听器创建CLI实例的深度副本。覆盖任何 GokeOptions 克隆中的(stdout、stderr、cwd、env、fs、argv、columns、exit)不会影响原始文件。主要用于从同一CLI定义运行多个隔离解析的测试:
const cli = goke('mycli')
cli.command('build', 'Build project').action((options, { console }) => {
console.log('building')
})
cli.help()
// In tests: override streams without touching the original CLI
const stdout = { write: vi.fn void>() }
const isolated = cli.clone({ stdout })
isolated.parse(['node', 'mycli', 'build'])
expect(stdout.write).toHaveBeenCalledWith('building\n')cli.helpText()
- 类型:
() => string
返回格式化的帮助字符串,不打印。可用于在文档、测试或其他编程用途中嵌入帮助文本。
生成Docs({cli})
- 类型:
(options: { cli: Goke }) => DocPage[]
为每个非隐藏命令生成markdown文档页面。返回一个数组 { command, slug, content } 物体。看 生成Markdown文档 完全使用。
const cli = goke('mycli')
cli.command('build', 'Build project')
cli.option('--watch', 'Watch mode')
cli.help()
const help = cli.helpText()
// => "mycli\n\nUsage:\n $ mycli ..."cli.用法(文本)
- 类型:
(text: string) => CLI
cli.example(示例)
- 类型:
(example: CommandExample) => CLI
命令实例
command.option()
基本相同 cli.option 但这为特定命令添加了选项。
command.action(回调)
- 类型:
(callback: ActionCallback) => Command
命令回调首先接收位置参数,然后解析选项,然后注入 { fs, console, process } 对象。比起全球,更喜欢那些注射的助手 console, process.exit,直接 node:fs/promises 导入,使命令更容易测试,并可以在JustBash等备用运行时中运行。
command.alias(name)
- 类型:
(name: string) => Command
command.allowUnknownOptions()
- 类型:
() => Command
command.hidden()
- 类型:
() => Command
在帮助输出列表中隐藏命令。直接调用时,该命令仍然匹配并运行——只有帮助显示被抑制。适用于您不想宣传的内部、已弃用或实验性命令。
cli
.command('internal-reset', 'Reset internal state')
.hidden()
.action((options, { console }) => {
console.log('reset done')
})command.example.(示例)
- 类型:
(example: CommandExample) => Command
command.用法(文本)
- 类型:
(text: string) => Command
command.helpText()
- 类型:
() => string
返回此特定命令的格式化帮助字符串,而不打印它。适用于测试或以编程方式嵌入帮助文本。
事件
听命令:
cli.on('command:foo', () => {
// Do something
})
cli.on('command:!', () => {
// Default command
})
cli.on('command:*', () => {
process.stderr.write(`Invalid command: ${cli.args.join(' ')}\n`)
process.exit(1)
})学分
许可证
麻省理工学院
