vipvot
v0.4.0
Published
A Bun-native, zero-dependency, TypeScript port of cobra.
Maintainers
Readme
vipvot
A zero-dependency TypeScript port of spf13/cobra. Built with Bun, runs on Node 20+ and Bun 1.3+.
Why
Cobra is the de-facto CLI framework for Go (Kubernetes, Hugo, gh, docker, GitHub CLI). The TypeScript ecosystem has good CLI libraries — commander, oclif, clipanion, Stricli — but none match cobra's exact shape: command groups in help, declarative flag-group constraints (mutex / required-together / one-required), inheritable persistent flags, the five-stage hook chain, POSIX/GNU pflag semantics, 33 of pflag's flag types — and a public surface that lets you port cobra Go code by mechanical rewrite.
vipvot is that shape — in TypeScript, with no runtime dependencies, and differential-tested against a real cobra binary. The build targets Bun for the single-binary bun build --compile workflow (the closest analog to go build of a cobra app), but the published ESM package runs in Node 20+ too.
Why "vipvot"?
vipvot is viper + pivot with the second i dropped. Two things at once:
- Viper — a snake-nod to cobra. cobra is an elapid; viper is a viperid; both are venomous colubrids in the same kingdom. A different family of the same animal, the same way a TypeScript port is a different language of the same library.
- Pivot — what the library actually does. It's the pivot point between Go cobra and Bun TypeScript. Cobra Go code translates into vipvot TypeScript by mechanical rewrite, and the reverse is just as direct.
&cobra.Command{...}↔Command({...}). PascalCase ↔ camelCase. struct literal ↔ factory call. A CLI written in vipvot can flip back to Go cobra (or vice versa) without rethinking the model.
So the name carries both: the snake adjacent to cobra, and the joint where bun and Go meet.
See design-docs/01-name.md for the full naming rationale.
What you get
Parser
- POSIX/GNU flag parsing — short combining (
-vvv,-xVALUE), long flags,--terminator - Universal
--no-fooboolean negation - All edge cases verified byte-for-byte against the cobra/pflag oracle
33 flag types (most of pflag — see COMPARISON.md for the missing ones: IPSlice, IPNetSlice, UintSlice, plus the niche Func / BoolFunc / TextVar / TimeVar callbacks)
- Scalars:
string,boolean,int,int8/16/32/64,uint/uint8/16/32/64,float32/64,count,duration - Slices:
stringSlice(CSV-aware),stringArray,intSlice,int32/64Slice,float32/64Slice,boolSlice,durationSlice - Maps:
stringToString,stringToInt,stringToInt64 - Network:
ip,ipMask,ipNet(CIDR) - Bytes:
bytesHex,bytesBase64
Command tree
Command({...})callable factory — nonewkeyword- Persistent (inherited) flags
- Five-stage hook chain:
persistentPreRun → preRun → run → postRun → persistentPostRun, with cobra-style ancestor-walk inheritance for the persistent stages - Args validators:
MinimumNArgs,MaximumNArgs,ExactArgs,RangeArgs,OnlyValidArgs,NoArgs,ArbitraryArgs,MatchAll - Flag-group constraints:
markFlagsRequiredTogether,markFlagsMutuallyExclusive,markFlagsOneRequired - Command groups in
--help:cmd.addGroup({ id, title })+ childgroupIdfor sectioned listings disableFlagParsingfor proxy commands;disableFlagsInUseLinefor help cosmeticsfParseErrWhitelist: { unknownFlags: true }for proxy/wrapper commands that forward argvannotationsmap for external tooling
Custom flag types
Valueinterface (set/toString/type) — pflag.Value-equivalentcmd.flags().varP(value, name, short, description)andvar()(long-only)- Help renderer reads the Value's
type()andtoString()automatically
Help & error handling
- Cobra-equivalent help format byte-for-byte (sections, alignment, type markers, defaults)
--help/-hauto-injection;<cmd> help [path]auto-dispatchsetOut/setErrwriters with parent-chain inheritancesetHelpFuncto override--helprendering;setUsageFuncto override the usage block printed on parse errors- Auto-print on error:
Error: <msg>+ usage block to stderr (cobra parity); suppress withsilenceErrors/silenceUsage - "Did you mean?" Levenshtein-based suggestions for unknown subcommands; configurable via
suggestFor/disableSuggestions/suggestionsMinimumDistance - Error wording byte-for-byte identical to pflag/cobra (verified against the oracle)
Doc generators (vipvot/doc subpath)
genMarkdown/genMarkdownTree— per-command markdowngenYaml/genYamlTree— YAML for tooling that consumes the command tree as datagenReST/genReSTTree— reStructuredTextgenMan/genManTree— roff-formatted man pages
Format pinned byte-for-byte against the cobra oracle.
Shell completion (vipvot/completion subpath)
genBashCompletion,genZshCompletion,genFishCompletion,genPowerShellCompletionvalidArgsFunction(cmd, args, prefix)for dynamic positional completionsregisterFlagCompletionFunc(name, fn)for per-flag-value completion (walks parent chain for inherited persistent flags)ShellCompDirective*constants —NoSpace,NoFileComp,FilterFileExt,FilterDirs,KeepOrderdisableDescriptionsstripsvalue\tdescto bare values for shells that don't render descriptions- Generators only loaded when imported — no bundle cost for users that don't need them
Bundle
- Core bundle: ~30 KB minified, ~8.5 KB gzipped
vipvot/docandvipvot/completionlazily loaded via subpath imports- Zero runtime dependencies
Quick taste
Cobra Go and vipvot TypeScript side-by-side — the only forced changes are the struct-literal → factory call, PascalCase → camelCase, and &variable → ref<T>().
Go:
var name string
var verbose int
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "my CLI",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("hello, %s\n", name)
return nil
},
}
func init() {
rootCmd.PersistentFlags().StringVarP(&name, "name", "n", "world", "who to greet")
rootCmd.PersistentFlags().CountVarP(&verbose, "verbose", "v", "verbose level")
rootCmd.MarkFlagRequired("name")
}TypeScript:
import { Command, ref } from "vipvot";
const name = ref("world");
const verbose = ref(0);
const rootCmd = Command({
use: "myapp",
short: "my CLI",
runE: (cmd, args) => {
console.log(`hello, ${name.value}`);
},
});
rootCmd.persistentFlags().stringVarP(name, "name", "n", "world", "who to greet");
rootCmd.persistentFlags().countVarP(verbose, "verbose", "v", "verbose level");
rootCmd.markFlagRequired("name");
await rootCmd.execute();See COMPARISON.md for the side-by-side feature table and design-docs/02-cobra-mapping.md for the full per-symbol concordance.
Worked example: a small CLI with subcommands
import { Command, ref, ExactArgs } from "vipvot";
const root = Command({
use: "todo",
short: "task tracker",
});
// `todo add <title>` — leaf command with positional arg validation
const title = ref("");
const priority = ref(0);
const add = Command({
use: "add <title>",
short: "add a task",
args: ExactArgs(1),
run: (_cmd, args) => {
title.value = args[0]!;
console.log(`added: ${title.value} (priority ${priority.value})`);
},
});
add.flags().intVarP(priority, "priority", "p", 0, "priority (0-3)");
root.addCommand(add);
// `todo list --tag urgent` — flag-driven leaf command
const tags = ref<string[]>([]);
const list = Command({
use: "list",
short: "list tasks",
run: () => {
console.log(`filtering by tags: [${tags.value.join(", ")}]`);
},
});
list.flags().stringSliceVarP(tags, "tag", "t", [], "filter by tag");
root.addCommand(list);
// Persistent flag — `--verbose` works on every subcommand
const verbose = ref(false);
root.persistentFlags().boolVarP(verbose, "verbose", "v", false, "verbose output");
await root.execute();Development
See AGENTS.md for the full developer guide.
bun install
bun test # 545+ tests; uses committed oracle fixtures — no Go needed
bun run typecheck # tsgo (TypeScript 7 native preview)
bun run lint # oxlint, default config
bun run format # oxfmtTo regenerate oracle fixtures (requires Go):
bun run oracle:build
bun run oracle:captureLicense
vipvot's source code is MIT-licensed — see LICENSE.
The shell-completion script templates in src/completion/ are adapted from cobra (Apache License 2.0). See NOTICE for the third-party attribution and licenses/cobra-LICENSE-2.0.txt for the full upstream license text.
