Build complex CLIs with type safety and no dependencies
Generate help text and shell completions for commands.
// Generate help for all commands
const docs = generateHelpTextForAllCommands(app);
for (const [route, helpText] of docs) {
console.log(`${route}:\n${helpText}\n`);
}
// Shell completions
const completions = await proposeCompletions(app, ["myapp", "deploy", "--e"], { process });
// Returns: [{ kind: "argument:flag", completion: "--env", brief: "Environment" }]function generateHelpTextForAllCommands(
app: Application<CommandContext>,
locale?: string
): readonly [route: string, helpText: string][]Example:
const app = buildApplication(routeMap, { name: "myapp" });
const docs = generateHelpTextForAllCommands(app);
// docs = [
// ["myapp deploy", "USAGE\n myapp deploy [OPTIONS]\n\n..."],
// ["myapp status", "USAGE\n myapp status\n\n..."]
// ]
// Write to files
for (const [route, helpText] of docs) {
const filename = route.replace(/ /g, "-") + ".txt";
await writeFile(filename, helpText);
}async function proposeCompletions<CONTEXT>(
app: Application<CONTEXT>,
rawInputs: readonly string[],
context: StricliDynamicCommandContext<CONTEXT>
): Promise<readonly InputCompletion[]>InputCompletion:
type InputCompletion = ArgumentCompletion | RoutingTargetCompletion
interface ArgumentCompletion {
kind: "argument:flag" | "argument:value"
completion: string
brief: string
}
interface RoutingTargetCompletion {
kind: "routing-target:command" | "routing-target:route-map"
completion: string
brief: string
}Examples:
// Complete command names
await proposeCompletions(app, ["myapp", "de"], { process })
// Returns: [{ kind: "routing-target:command", completion: "deploy", brief: "Deploy application" }]
// Complete flag names
await proposeCompletions(app, ["myapp", "deploy", "--e"], { process })
// Returns: [{ kind: "argument:flag", completion: "--env", brief: "Environment" }]
// Complete flag values
await proposeCompletions(app, ["myapp", "deploy", "--env", "st"], { process })
// Returns: [{ kind: "argument:value", completion: "staging", brief: "Environment" }]
// Complete aliases
await proposeCompletions(app, ["myapp", "deploy", "-"], { process })
// Returns: [
// { kind: "argument:flag", completion: "-e", brief: "Environment" },
// { kind: "argument:flag", completion: "-f", brief: "Force deployment" }
// ]const completeCmd = buildCommand({
func: async function(flags, ...inputs) {
const completions = await proposeCompletions(app, inputs, { process });
for (const completion of completions) {
this.process.stdout.write(completion.completion + "\n");
}
},
parameters: {
positional: {
kind: "array",
parameter: { brief: "Input words", parse: String }
}
},
docs: { brief: "Shell completion", hideRoute: { __complete: true } }
});
const app = buildApplication(
buildRouteMap({
routes: {
...mainRoutes,
__complete: completeCmd // Hidden completion command
},
docs: { brief: "My CLI" }
}),
{ name: "myapp" }
);_myapp_completions() {
local completions=$(myapp __complete "${COMP_WORDS[@]:1}")
COMPREPLY=($(compgen -W "$completions" -- "${COMP_WORDS[COMP_CWORD]}"))
}
complete -F _myapp_completions myapp#compdef myapp
_myapp() {
local completions=(${(f)"$(myapp __complete ${words[@]:1})"})
_describe 'command' completions
}
_myappAdd proposeCompletions to parameter definitions for dynamic completions.
// File path completions
positional: {
kind: "tuple",
parameters: [
{
brief: "File to process",
parse: String,
proposeCompletions: async () => {
const files = await readdir(".");
return files.filter(f => f.endsWith(".txt"));
}
}
]
}
// API completions with context
interface MyContext extends CommandContext {
api: { listUsers: () => Promise<string[]> };
}
positional: {
kind: "tuple",
parameters: [
{
brief: "User ID",
parse: String,
proposeCompletions: async function(this: MyContext) {
return await this.api.listUsers();
}
}
]
}Install with Tessl CLI
npx tessl i tessl/npm-stricli--core