@fission-ai/openspec
Version:
AI-native system for spec-driven development
250 lines (244 loc) • 8.71 kB
JavaScript
import { ZSH_DYNAMIC_HELPERS } from '../templates/zsh-templates.js';
/**
* Generates Zsh completion scripts for the OpenSpec CLI.
* Follows Zsh completion system conventions using the _openspec function.
*/
export class ZshGenerator {
shell = 'zsh';
/**
* Generate a Zsh completion script
*
* @param commands - Command definitions to generate completions for
* @returns Zsh completion script as a string
*/
generate(commands) {
// Build command list using push() for loop clarity
const commandLines = [];
for (const cmd of commands) {
const escapedDesc = this.escapeDescription(cmd.description);
commandLines.push(` '${cmd.name}:${escapedDesc}'`);
}
const commandList = commandLines.join('\n');
// Build command cases using push() for loop clarity
const commandCaseLines = [];
for (const cmd of commands) {
commandCaseLines.push(` ${cmd.name})`);
commandCaseLines.push(` _openspec_${this.sanitizeFunctionName(cmd.name)}`);
commandCaseLines.push(' ;;');
}
const commandCases = commandCaseLines.join('\n');
// Build command functions using push() for loop clarity
const commandFunctionLines = [];
for (const cmd of commands) {
commandFunctionLines.push(...this.generateCommandFunction(cmd));
commandFunctionLines.push('');
}
const commandFunctions = commandFunctionLines.join('\n');
// Dynamic completion helpers from template
const helpers = ZSH_DYNAMIC_HELPERS;
// Assemble final script with template literal
return `#compdef openspec
# Zsh completion script for OpenSpec CLI
# Auto-generated - do not edit manually
_openspec() {
local context state line
typeset -A opt_args
local -a commands
commands=(
${commandList}
)
_arguments -C \\
"1: :->command" \\
"*::arg:->args"
case $state in
command)
_describe "openspec command" commands
;;
args)
case $words[1] in
${commandCases}
esac
;;
esac
}
${commandFunctions}
${helpers}
compdef _openspec openspec
`;
}
/**
* Generate completion function for a specific command
*/
generateCommandFunction(cmd) {
const funcName = `_openspec_${this.sanitizeFunctionName(cmd.name)}`;
const lines = [];
lines.push(`${funcName}() {`);
// If command has subcommands, handle them
if (cmd.subcommands && cmd.subcommands.length > 0) {
lines.push(' local context state line');
lines.push(' typeset -A opt_args');
lines.push('');
lines.push(' local -a subcommands');
lines.push(' subcommands=(');
for (const subcmd of cmd.subcommands) {
const escapedDesc = this.escapeDescription(subcmd.description);
lines.push(` '${subcmd.name}:${escapedDesc}'`);
}
lines.push(' )');
lines.push('');
lines.push(' _arguments -C \\');
// Add command flags
for (const flag of cmd.flags) {
lines.push(' ' + this.generateFlagSpec(flag) + ' \\');
}
lines.push(' "1: :->subcommand" \\');
lines.push(' "*::arg:->args"');
lines.push('');
lines.push(' case $state in');
lines.push(' subcommand)');
lines.push(' _describe "subcommand" subcommands');
lines.push(' ;;');
lines.push(' args)');
lines.push(' case $words[1] in');
for (const subcmd of cmd.subcommands) {
lines.push(` ${subcmd.name})`);
lines.push(` _openspec_${this.sanitizeFunctionName(cmd.name)}_${this.sanitizeFunctionName(subcmd.name)}`);
lines.push(' ;;');
}
lines.push(' esac');
lines.push(' ;;');
lines.push(' esac');
}
else {
// Command without subcommands
lines.push(' _arguments \\');
// Add flags
for (const flag of cmd.flags) {
lines.push(' ' + this.generateFlagSpec(flag) + ' \\');
}
// Add positional argument completion
if (cmd.acceptsPositional) {
const positionalSpec = this.generatePositionalSpec(cmd.positionalType);
lines.push(' ' + positionalSpec);
}
else {
// Remove trailing backslash from last flag
if (lines[lines.length - 1].endsWith(' \\')) {
lines[lines.length - 1] = lines[lines.length - 1].slice(0, -2);
}
}
}
lines.push('}');
// Generate subcommand functions if they exist
if (cmd.subcommands) {
for (const subcmd of cmd.subcommands) {
lines.push('');
lines.push(...this.generateSubcommandFunction(cmd.name, subcmd));
}
}
return lines;
}
/**
* Generate completion function for a subcommand
*/
generateSubcommandFunction(parentName, subcmd) {
const funcName = `_openspec_${this.sanitizeFunctionName(parentName)}_${this.sanitizeFunctionName(subcmd.name)}`;
const lines = [];
lines.push(`${funcName}() {`);
lines.push(' _arguments \\');
// Add flags
for (const flag of subcmd.flags) {
lines.push(' ' + this.generateFlagSpec(flag) + ' \\');
}
// Add positional argument completion
if (subcmd.acceptsPositional) {
const positionalSpec = this.generatePositionalSpec(subcmd.positionalType);
lines.push(' ' + positionalSpec);
}
else {
// Remove trailing backslash from last flag
if (lines[lines.length - 1].endsWith(' \\')) {
lines[lines.length - 1] = lines[lines.length - 1].slice(0, -2);
}
}
lines.push('}');
return lines;
}
/**
* Generate flag specification for _arguments
*/
generateFlagSpec(flag) {
const parts = [];
// Handle mutually exclusive short and long forms
if (flag.short) {
parts.push(`'(-${flag.short} --${flag.name})'{-${flag.short},--${flag.name}}'`);
}
else {
parts.push(`'--${flag.name}`);
}
// Add description
const escapedDesc = this.escapeDescription(flag.description);
parts.push(`[${escapedDesc}]`);
// Add value completion if flag takes a value
if (flag.takesValue) {
if (flag.values && flag.values.length > 0) {
// Provide specific value completions
const valueList = flag.values.map(v => this.escapeValue(v)).join(' ');
parts.push(`:value:(${valueList})`);
}
else {
// Generic value placeholder
parts.push(':value:');
}
}
// Close the quote (needed for both short and long forms)
parts.push("'");
return parts.join('');
}
/**
* Generate positional argument specification
*/
generatePositionalSpec(positionalType) {
switch (positionalType) {
case 'change-id':
return "'*: :_openspec_complete_changes'";
case 'spec-id':
return "'*: :_openspec_complete_specs'";
case 'change-or-spec-id':
return "'*: :_openspec_complete_items'";
case 'path':
return "'*:path:_files'";
case 'shell':
return "'*:shell:(zsh bash fish powershell)'";
default:
return "'*: :_default'";
}
}
/**
* Escape special characters in descriptions
*/
escapeDescription(desc) {
return desc
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\[/g, '\\[')
.replace(/]/g, '\\]')
.replace(/:/g, '\\:');
}
/**
* Escape special characters in values
*/
escapeValue(value) {
return value
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/ /g, '\\ ');
}
/**
* Sanitize command names for use in function names
*/
sanitizeFunctionName(name) {
return name.replace(/-/g, '_');
}
}
//# sourceMappingURL=zsh-generator.js.map