Schema-first CLI framework for Bun. Define typed commands once, get validated
handlers, predictable stdout, and an agent-readable @schema.
bun add github:ethan-huo/argc#v7.5.0Use release tags for downstream projects. main is the source branch and does
not commit generated declaration files; tags include dist/*.d.ts.
import { toStandardJsonSchema } from '@valibot/to-json-schema'
import * as v from 'valibot'
import { c, cli, group } from 'argc'
const s = toStandardJsonSchema
const commands = {
user: group(
{ description: 'User management' },
{
create: c
.meta({
description: 'Create a user',
examples: ['myapp user.create "{ name: \'alice\' }"'],
})
.input(
s(
v.object({
name: v.pipe(v.string(), v.minLength(2)),
role: v.optional(v.string(), 'member'),
}),
),
),
},
),
}
const app = cli(commands, {
name: 'myapp',
version: '7.5.0',
description: 'Example argc CLI',
})
await app.run({
handlers: {
'user.create': ({ input }) => ({
ok: true,
user: input,
}),
},
})$ myapp user.create "{ name: 'alice', role: 'admin' }"
ok: true
user:
name: alice
role: adminargc 7 is a clean-break typed command surface:
- Commands are addressed by dotted path:
myapp user.create - Identifier, kebab-case, and non-builtin
@segments are app commands;@schema,@run, and@completionsare reserved - Input is one quoted object literal token:
"{ name: 'alice' }" - Large input can come from a file or stdin:
@payload.jsonor- @schema,@run, and@completionsare builtins--helpand--versionare the only direct global flags- Handler return values are serialized to stdout
- Handler logs (
console.log/process.stdout) are redirected to stderr
There are no command aliases, .args(), input flags, --input, --schema,
--run, global transforms, or compatibility shims for the v1 surface.
Quote object literal input so the shell passes it as one argv token:
myapp user.create "{ name: 'alice', tags: ['admin', 'dev'] }"Use files for reusable payloads:
myapp user.create @payload.jsonUse stdin when another process generates the payload:
printf "{ name: 'alice' }" | myapp user.create -Bare braces are rejected because the shell splits them before argc can parse the payload.
Define process context with CLIOptions.context. It is validated verbatim and
injected into handlers as context.
const app = cli(commands, {
name: 'myapp',
version: '7.5.0',
context: s(
v.object({
token: v.string(),
}),
),
})Pass context with --context or ARGC_CTX:
myapp user.create "{ name: 'alice' }" --context "{ token: 'secret' }"
ARGC_CTX="{ token: 'secret' }" myapp user.create "{ name: 'alice' }"Default output is YAML. Strings print as raw text, undefined prints nothing,
and structured values use block-style YAML.
handlers: {
'user.create': ({ input }) => ({
created: input.name,
next: ['myapp user.get "{ name: \'alice\' }"'],
}),
}Use @run --json when scripting needs strict JSON.
Errors are YAML envelopes on stderr with stable error codes:
error: UNKNOWN_KEY
message: Unknown input key: nam
issues:
- path: nam
message: Unknown input key@schema is the agent-facing contract:
myapp @schema
myapp @schema .user.create
myapp @schema .user.create.inputSchema output is TypeScript-like and includes quoted object literal examples.
Command and group keys may be JavaScript identifiers, kebab-case names, or
non-builtin @ names. @schema quotes non-identifier command and input keys
when needed:
type Input = {
'content-type'?: string
}@run executes small agent-authored scripts against typed command handlers:
myapp @run "const user = await user.create({ name: 'alice' }); user" --jsonInline scripts expose command locals (user.create) plus argc. File scripts
receive only argc to avoid accidental identifier collisions:
// script.ts
export default async function main(argc) {
return argc.commands.user.create({ name: 'alice' })
}myapp @run @script.ts --jsonrun: false disables @run.
Handlers can be flat or nested:
await app.run({
handlers: {
'user.create': ({ input, context }) => ({ input, context }),
},
})
await app.run({
handlers: {
user: {
create: ({ input }) => input,
},
},
})Type split handler modules with typeof app.Handlers.
Install generated shell completions with:
myapp @completions zsh
myapp @completions bash
myapp @completions fishCompletions are path-oriented and include builtins. They do not complete input object keys as shell flags because command input is a single structured value.