diff --git a/bash.ts b/bash.ts index a91a084..3f6a4f1 100644 --- a/bash.ts +++ b/bash.ts @@ -1,2 +1,2 @@ -export function generate(name: string, includeDescription: boolean) { +export function generate(name: string, exec: string) { } diff --git a/cac.ts b/cac.ts index 5d266f0..e4639d2 100644 --- a/cac.ts +++ b/cac.ts @@ -1,695 +1,262 @@ -import fs from "fs/promises"; -import cac, { Command } from "cac"; -import path from "path"; - -const cli = cac("cac"); +import { CAC } from "cac"; +import * as zsh from "./zsh"; +import * as bash from "./bash"; +import * as fish from "./fish"; +import * as powershell from "./powershell"; +import { + flagMap, + Positional, + positionalMap, + ShellCompDirective, +} from "./shared"; const execPath = process.execPath; const processArgs = process.argv.slice(1); const x = `${execPath} ${process.execArgv.join(" ")} ${processArgs[0]}`; -cli.option("--type [type]", "Choose a project type", { - default: "node", -}); -cli.option("--name ", "Provide your name"); -cli.option("--name23 ", "Provide your name23"); - -cli.command("start").option("--port ", "your port"); - -cli.command("help"); - -cli.command("help config").option("--foo", "foo option"); - -cli.command("test dev").option("--foo", "foo option"); - -cli - .command("deploy [version] [...files]") - .action((environment, version) => { - console.log(`Deploying to ${environment} environment, version ${version}`); - }); - -cli - .command("complete") - .action(() => console.log(genZshComp("my_command", true))); - -type Positional = { - required: boolean; - variadic: boolean; - completion: Callback; -}; - -const positionalMap = new Map(); - -const flagMap = new Map(); - -cli - .command("__complete [...args]") - .action(async (_, { ["--"]: args }: { ["--"]: string[] }) => { - cli.showHelpOnExit = false; - let directive = ShellCompDirective.ShellCompDirectiveDefault; - - const endsWithSpace = args[args.length - 1] === ""; - if (endsWithSpace) { - args = args.slice(0, -1); // Remove the empty string - } - - let toComplete = args[args.length - 1] || ""; - const previousArgs = args.slice(0, -1); - - const completions: string[] = []; - - cli.unsetMatchedCommand(); - cli.parse([execPath, processArgs[0], ...previousArgs], { - run: false, - }); - - const command = cli.matchedCommand ?? cli.globalCommand; - - const options = [ - ...new Set([...(command?.options ?? []), ...cli.globalCommand.options]), - ]; - console.log(options) - - let isCompletingFlagValue = false; - let flagName = ""; - let option: (typeof options)[number] | null = null; - - if (toComplete.startsWith("--")) { - // Long option - flagName = toComplete.slice(2); - const equalsIndex = flagName.indexOf("="); - if (equalsIndex !== -1 && !endsWithSpace) { - // Option with '=', get the name before '=' - flagName = flagName.slice(0, equalsIndex); - toComplete = toComplete.slice(toComplete.indexOf("=") + 1); - } else if (!endsWithSpace) { - // If not ending with space, still typing option name - flagName = ""; +export default function tab(instance: CAC): void { + instance.command("complete [shell]").action(async (shell, extra) => { + switch (shell) { + case "zsh": { + const script = zsh.generate(instance.name, x); + console.log(script); + break; } - } else if (toComplete.startsWith("-") && toComplete.length > 1) { - // Short option - flagName = toComplete.slice(1); - if (!endsWithSpace) { - // Still typing option name - flagName = ""; + case "bash": { + const script = bash.generate(instance.name, x); + console.log(script); + break; } - } - - if (flagName) { - option = options.find((o) => o.names.includes(flagName)) ?? null; - if (option && !option.isBoolean) { - isCompletingFlagValue = true; + case "fish": { + const script = fish.generate(instance.name, x); + console.log(script); + break; } - } - - if (isCompletingFlagValue) { - const flagCompletionFn = flagMap.get(`${command.name} ${option?.name}`); - // console.log( - // "flagCompletionFn", - // `${command.name} ${option?.name}`, - // flagCompletionFn, - // ); - - if (flagCompletionFn) { - // Call custom completion function for the flag - const comps = await flagCompletionFn(previousArgs, toComplete); - completions.push( - ...comps.map((comp) => `${comp.action}\t${comp.description}`), - ); - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - } else { - // Default completion (e.g., file completion) - directive = ShellCompDirective.ShellCompDirectiveDefault; + case "powershell": { + const script = powershell.generate(instance.name, x); + console.log(script); + break; } - } else if (toComplete.startsWith("-") && !endsWithSpace) { - const flag = toComplete.replace(/^-+/, ""); // Remove leading '-' + default: { + const args: string[] = extra["--"]; - // Determine options to suggest - let optionsToSuggest = options.filter((o) => o.names.some((name) => name.startsWith(flag)) && !cli.options[o.name]); - // TODO: --type does not show up because there's a default for it in the cli object + instance.showHelpOnExit = false; + let directive = ShellCompDirective.ShellCompDirectiveDefault; - const requiredOptions = optionsToSuggest.filter( - (o) => o.required, - ); + const endsWithSpace = args[args.length - 1] === ""; + if (endsWithSpace) { + args.pop(); + } - console.log(flag, optionsToSuggest) - if (requiredOptions.length) { - // Required options not yet specified - optionsToSuggest = requiredOptions; - } + let toComplete = args[args.length - 1] || ""; + const previousArgs = args.slice(0, -1); - if (optionsToSuggest.length > 0) { - completions.push( - ...optionsToSuggest.map((o) => `--${o.name}\t${o.description}`), - ); - } + const completions: string[] = []; - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - } else { - cli.parse([execPath, processArgs[0], ...previousArgs, toComplete], { - run: false, - }); - const fullCommandName = args - .filter((arg) => !arg.startsWith("-")) - .join(" "); + instance.unsetMatchedCommand(); + instance.parse([execPath, processArgs[0], ...previousArgs], { + run: false, + }); - for (const c of cli.commands) { - const fullCommandParts = fullCommandName.split(" "); - const commandParts: { type: "command"; value: string }[] = c.name - .split(" ") - .map((part) => ({ type: "command", value: part })); - const args: { - type: "positional"; - position: number; - value: Positional; - }[] = - positionalMap.get(c.name)?.map((arg, i) => ({ - type: "positional", - position: i, - value: arg, - })) ?? []; - const parts = [...commandParts, ...args]; + const command = instance.matchedCommand ?? instance.globalCommand; - for (let i = 0; i < parts.length; i++) { - const fullCommandPart = fullCommandParts[i]; - const part = parts[i]; + const options = [ + ...new Set([ + ...(command?.options ?? []), + ...instance.globalCommand.options, + ]), + ]; - if (part.type === "command") { - // Command part matching - if (part.value === fullCommandPart) { - // Command part matches user input, continue to next part - continue; - } else if ( - !fullCommandPart || - part.value.startsWith(fullCommandPart) - ) { - // User is typing this command part, provide completion - completions.push(`${part.value}\t${c.description}`); - } - // Command part does not match, break - break; - } else if (part.type === "positional") { - const positional = part.value; - // Positional argument handling - if (part.value.variadic) { - const comps = await positional.completion( - previousArgs, - toComplete, - ); - completions.push( - ...comps.map( - (comp) => `${comp.action}\t${comp.description ?? ""}`, - ), - ); - break; - } - if (typeof fullCommandPart !== "undefined") { - // User has provided input for this positional argument - if (i === fullCommandParts.length - 1 && !endsWithSpace) { - console.log(i, fullCommandParts); - // User is still typing this positional argument, provide completions - const comps = await positional.completion( - previousArgs, - toComplete, - ); - completions.push( - ...comps.map( - (comp) => `${comp.action}\t${comp.description ?? ""}`, - ), - ); - break; - } else { - // Positional argument is already provided, move to next - previousArgs.push(fullCommandPart); - continue; - } - } else { - // User has not provided input for this positional argument - const comps = await positional.completion( - previousArgs, - toComplete, - ); - completions.push( - ...comps.map( - (comp) => `${comp.action}\t${comp.description ?? ""}`, - ), - ); - break; - } + let isCompletingFlagValue = false; + let flagName = ""; + let option: (typeof options)[number] | null = null; + const lastArg = previousArgs[previousArgs.length - 1] + + console.log({ lastArg, toComplete, endsWithSpace, flagName }); + // TODO: fix for "--port 3" + if (toComplete.startsWith("--")) { + // Long option + flagName = toComplete.slice(2); + const equalsIndex = flagName.indexOf("="); + if (equalsIndex !== -1 && !endsWithSpace) { + // Option with '=', get the name before '=' + flagName = flagName.slice(0, equalsIndex); + toComplete = toComplete.slice(toComplete.indexOf("=") + 1); + } else if (!endsWithSpace) { + // If not ending with space, still typing option name + flagName = ""; + } else { + toComplete = "" + } + } else if (toComplete.startsWith("-") && toComplete.length > 1) { + // Short option + flagName = toComplete.slice(1); + if (!endsWithSpace) { + // Still typing option name + flagName = ""; } } - // for (let i = 0; i < parts.length; i++) { - // const fullCommandPart = fullCommandParts[i]; - // const part = parts[i]; - // const isLastPart = i === parts.length - 1; - - // if (part.type === "command") { - // if (part.value === fullCommandPart) { - // // if (commandPart !== toComplete) { - // continue; - - // // } - // } - // if (!fullCommandPart || part.value?.includes(fullCommandPart)) { - // completions.push(`${part.value}\t${c.description}`); - // break; - // } - // } else { - // let positional: Positional | null = null; - // if (isLastPart) { - // if (part.value.variadic) { - // positional = part.value; - // } - // } else { - // const isEmpty = typeof fullCommandPart === "undefined"; - // console.log(part.value); - // console.log({ - // previousArgs, - // toComplete, - // fullCommandPart, - // endsWithSpace, - // isEmpty, - // }); - - // if (endsWithSpace) { - // if (fullCommandPart) { - // positional = args[part.position + 1].value; - // previousArgs.push(toComplete); - // toComplete = ""; - // console.log("here"); - // } else { - // positional = part.value; - // } - // } else { - // if (fullCommandPart) { - // positional = part.value; - // } - // } - // } - - // if (positional) { - // const comps = await positional.completion( - // previousArgs, - // toComplete, - // ); - // completions.push( - // ...comps.map( - // (comp) => `${comp.action}\t${comp.description ?? ""}`, - // ), - // ); - // break; - // } - // if (!fullCommandPart) { - // break; - // } - - // // console.log(fullCommandParts, parts) - - // // console.log(endsWithSpace, fullCommandPart) - // // console.log(part.value, part.position, toComplete, fullCommandParts) - // const index = i + part.position; - // // console.log(i, part.position, index) - // // TODO: fix this - - // continue; - // } - - // // console.log("remaining", commandPart, fullCommandPart); - // break; - // } - - // if ( - // baseCommand === fullCommandName && - // lastPart.startsWith(toComplete) - // ) { - // completions.push(`${lastPart}\t${c.description}`); - // } - } - } - - // Output completions - for (const comp of completions) { - console.log(comp.split("\n")[0].trim()); - } - console.log(`:${directive}`); - console.error(`Completion ended with directive: ${directive}`); - }); + if (flagName) { + option = options.find((o) => o.names.includes(flagName)) ?? null; + if (option && !option.isBoolean) { + isCompletingFlagValue = true; + } + } -type Completion = { - action: string; - description?: string; -}; + if (isCompletingFlagValue) { + const flagCompletionFn = flagMap.get( + `${command.name} ${option?.name}`, + ); -type Callback = ( - previousArgs: string[], - toComplete: string, -) => Completion[] | Promise; + if (flagCompletionFn) { + // Call custom completion function for the flag + const comps = await flagCompletionFn(previousArgs, toComplete); + completions.push( + ...comps.map((comp) => `${comp.action}\t${comp.description}`), + ); + directive = ShellCompDirective.ShellCompDirectiveNoFileComp; + } else { + // Default completion (e.g., file completion) + directive = ShellCompDirective.ShellCompDirectiveDefault; + } + } else if (toComplete.startsWith("-") && !endsWithSpace) { + const flag = toComplete.replace(/^-+/, ""); // Remove leading '-' + + // Determine options to suggest + let optionsToSuggest = options.filter((o) => { + const equalToDefault = + "default" in o.config && + instance.options[o.name] === o.config.default; + return ( + o.names.some((name) => name.startsWith(flag)) && + !(instance.options[o.name] && !equalToDefault) + ); + }); + + const requiredOptions = optionsToSuggest.filter((o) => o.required); + + if (requiredOptions.length) { + // Required options not yet specified + optionsToSuggest = requiredOptions; + } -for (const c of [cli.globalCommand, ...cli.commands]) { - for (const o of [...cli.globalCommand.options, ...c.options]) { - if (o.rawName === "--type [type]") { - flagMap.set(`${c.name} ${o.name}`, () => { - return [ - { - action: "standalone", - description: "Standalone type", - }, - { - action: "complex", - description: "Complex type", - }, - ]; - }); - } - } + if (optionsToSuggest.length > 0) { + completions.push( + ...optionsToSuggest.map((o) => `--${o.name}\t${o.description}`), + ); + } - // console.log(c.args); - if (c.name === "deploy") { - const positionals: Positional[] = []; - positionalMap.set(c.name, positionals); - for (const arg of c.args) { - const completion: Callback = async () => { - if (arg.value === "environment") { - return [ - { - action: "node", - }, - { - action: "deno", - }, - ]; - } - if (arg.value === "version") { - return [ - { - action: "0.0.0", - }, + directive = ShellCompDirective.ShellCompDirectiveNoFileComp; + } else { + instance.parse( + [execPath, processArgs[0], ...previousArgs, toComplete], { - action: "0.0.1", + run: false, }, - ]; - } - if (arg.value === "files") { - const currentDir = process.cwd(); - const files = await fs.readdir(currentDir); - const jsonFiles = files.filter( - (file) => path.extname(file).toLowerCase() === ".json", ); - return jsonFiles.map((file) => ({ action: file })); + const fullCommandName = args + .filter((arg) => !arg.startsWith("-")) + .join(" "); + + for (const c of instance.commands) { + if (c.name === "complete") { + // avoid showing completions for the completion server + continue; + } + const fullCommandParts = fullCommandName.split(" "); + const commandParts: { type: "command"; value: string }[] = c.name + .split(" ") + .map((part) => ({ type: "command", value: part })); + const args: { + type: "positional"; + position: number; + value: Positional; + }[] = + positionalMap.get(c.name)?.map((arg, i) => ({ + type: "positional", + position: i, + value: arg, + })) ?? []; + const parts = [...commandParts, ...args]; + + for (let i = 0; i < parts.length; i++) { + const fullCommandPart = fullCommandParts[i]; + const part = parts[i]; + + if (part.type === "command") { + // Command part matching + if (part.value === fullCommandPart) { + // Command part matches user input, continue to next part + continue; + } else if ( + !fullCommandPart || + part.value.startsWith(fullCommandPart) + ) { + // User is typing this command part, provide completion + completions.push(`${part.value}\t${c.description}`); + } + // Command part does not match, break + break; + } else if (part.type === "positional") { + const positional = part.value; + // Positional argument handling + if (part.value.variadic) { + const comps = await positional.completion( + previousArgs, + toComplete, + ); + completions.push( + ...comps.map( + (comp) => `${comp.action}\t${comp.description ?? ""}`, + ), + ); + break; + } + if (typeof fullCommandPart !== "undefined") { + // User has provided input for this positional argument + if (i === fullCommandParts.length - 1 && !endsWithSpace) { + // User is still typing this positional argument, provide completions + const comps = await positional.completion( + previousArgs, + toComplete, + ); + completions.push( + ...comps.map( + (comp) => `${comp.action}\t${comp.description ?? ""}`, + ), + ); + break; + } else { + // Positional argument is already provided, move to next + previousArgs.push(fullCommandPart); + continue; + } + } else { + // User has not provided input for this positional argument + const comps = await positional.completion( + previousArgs, + toComplete, + ); + completions.push( + ...comps.map( + (comp) => `${comp.action}\t${comp.description ?? ""}`, + ), + ); + break; + } + } + } + } } - return []; - }; - positionals.push({ - required: arg.required, - variadic: arg.variadic, - completion, - }); + // Output completions + for (const comp of completions) { + console.log(comp.split("\n")[0].trim()); + } + console.log(`:${directive}`); + console.error(`Completion ended with directive: ${directive}`); + } } - } -} - -cli.version("0.0.0"); -cli.help(); - -// console.log('cli', cli) - -// ShellCompRequestCmd is the name of the hidden command that is used to request -// completion results from the program. It is used by the shell completion scripts. -const ShellCompRequestCmd: string = "__complete"; - -// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request -// completion results without their description. It is used by the shell completion scripts. -const ShellCompNoDescRequestCmd: string = "__completeNoDesc"; - -// ShellCompDirective is a bit map representing the different behaviors the shell -// can be instructed to have once completions have been provided. -const ShellCompDirective = { - // ShellCompDirectiveError indicates an error occurred and completions should be ignored. - ShellCompDirectiveError: 1 << 0, - - // ShellCompDirectiveNoSpace indicates that the shell should not add a space - // after the completion even if there is a single completion provided. - ShellCompDirectiveNoSpace: 1 << 1, - - // ShellCompDirectiveNoFileComp indicates that the shell should not provide - // file completion even when no completion is provided. - ShellCompDirectiveNoFileComp: 1 << 2, - - // ShellCompDirectiveFilterFileExt indicates that the provided completions - // should be used as file extension filters. - // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() - // is a shortcut to using this directive explicitly. The BashCompFilenameExt - // annotation can also be used to obtain the same behavior for flags. - ShellCompDirectiveFilterFileExt: 1 << 3, - - // ShellCompDirectiveFilterDirs indicates that only directory names should - // be provided in file completion. To request directory names within another - // directory, the returned completions should specify the directory within - // which to search. The BashCompSubdirsInDir annotation can be used to - // obtain the same behavior but only for flags. - ShellCompDirectiveFilterDirs: 1 << 4, - - // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order - // in which the completions are provided. - ShellCompDirectiveKeepOrder: 1 << 5, - - // =========================================================================== - - // All directives using iota (or equivalent in Go) should be above this one. - // For internal use. - shellCompDirectiveMaxValue: 1 << 6, - - // ShellCompDirectiveDefault indicates to let the shell perform its default - // behavior after completions have been provided. - // This one must be last to avoid messing up the iota count. - ShellCompDirectiveDefault: 0, -}; - -// console.log({ -// execPath, -// args: processArgs, -// x, -// }); -// console.error(x) - -function genZshComp(name: string, includeDesc: boolean) { - let compCmd = ShellCompRequestCmd; - if (!includeDesc) { - compCmd = ShellCompNoDescRequestCmd; - } - - return `#compdef ${name} -compdef _${name} ${name} - -# zsh completion for ${name} -*- shell-script -*- - -__${name}_debug() { - local file="$BASH_COMP_DEBUG_FILE" - if [[ -n \${file} ]]; then - echo "$*" >> "\${file}" - fi -} - -_${name}() { - local shellCompDirectiveError=${ShellCompDirective.ShellCompDirectiveError} - local shellCompDirectiveNoSpace=${ShellCompDirective.ShellCompDirectiveNoSpace} - local shellCompDirectiveNoFileComp=${ShellCompDirective.ShellCompDirectiveNoFileComp} - local shellCompDirectiveFilterFileExt=${ShellCompDirective.ShellCompDirectiveFilterFileExt} - local shellCompDirectiveFilterDirs=${ShellCompDirective.ShellCompDirectiveFilterDirs} - local shellCompDirectiveKeepOrder=${ShellCompDirective.ShellCompDirectiveKeepOrder} - - local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder - local -a completions - - __${name}_debug "\\n========= starting completion logic ==========" - __${name}_debug "CURRENT: \${CURRENT}, words[*]: \${words[*]}" - - # The user could have moved the cursor backwards on the command-line. - # We need to trigger completion from the \$CURRENT location, so we need - # to truncate the command-line (\$words) up to the \$CURRENT location. - # (We cannot use \$CURSOR as its value does not work when a command is an alias.) - words=( "\${=words[1,CURRENT]}" ) - __${name}_debug "Truncated words[*]: \${words[*]}," - - lastParam=\${words[-1]} - lastChar=\${lastParam[-1]} - __${name}_debug "lastParam: \${lastParam}, lastChar: \${lastChar}" - - # For zsh, when completing a flag with an = (e.g., ${name} -n=) - # completions must be prefixed with the flag - setopt local_options BASH_REMATCH - if [[ "\${lastParam}" =~ '-.*=' ]]; then - # We are dealing with a flag with an = - flagPrefix="-P \${BASH_REMATCH}" - fi - - # Prepare the command to obtain completions - requestComp="${x} ${compCmd} -- \${words[2,-1]}" - if [ "\${lastChar}" = "" ]; then - # If the last parameter is complete (there is a space following it) - # We add an extra empty parameter so we can indicate this to the go completion code. - __${name}_debug "Adding extra empty parameter" - requestComp="\${requestComp} ''" - fi - - __${name}_debug "About to call: eval \${requestComp}" - - # Use eval to handle any environment variables and such - out=\$(eval \${requestComp} 2>/dev/null) - __${name}_debug "completion output: \${out}" - - # Extract the directive integer following a : from the last line - local lastLine - while IFS='\n' read -r line; do - lastLine=\${line} - done < <(printf "%s\n" "\${out[@]}") - __${name}_debug "last line: \${lastLine}" - - if [ "\${lastLine[1]}" = : ]; then - directive=\${lastLine[2,-1]} - # Remove the directive including the : and the newline - local suffix - (( suffix=\${#lastLine}+2)) - out=\${out[1,-\$suffix]} - else - # There is no directive specified. Leave \$out as is. - __${name}_debug "No directive found. Setting to default" - directive=0 - fi - - __${name}_debug "directive: \${directive}" - __${name}_debug "completions: \${out}" - __${name}_debug "flagPrefix: \${flagPrefix}" - - if [ \$((directive & shellCompDirectiveError)) -ne 0 ]; then - __${name}_debug "Completion received error. Ignoring completions." - return - fi - - local activeHelpMarker="%" - local endIndex=\${#activeHelpMarker} - local startIndex=\$((\${#activeHelpMarker}+1)) - local hasActiveHelp=0 - while IFS='\n' read -r comp; do - # Check if this is an activeHelp statement (i.e., prefixed with \$activeHelpMarker) - if [ "\${comp[1,\$endIndex]}" = "\$activeHelpMarker" ];then - __${name}_debug "ActiveHelp found: \$comp" - comp="\${comp[\$startIndex,-1]}" - if [ -n "\$comp" ]; then - compadd -x "\${comp}" - __${name}_debug "ActiveHelp will need delimiter" - hasActiveHelp=1 - fi - continue - fi - - if [ -n "\$comp" ]; then - # If requested, completions are returned with a description. - # The description is preceded by a TAB character. - # For zsh's _describe, we need to use a : instead of a TAB. - # We first need to escape any : as part of the completion itself. - comp=\${comp//:/\\:} - - local tab="\$(printf '\\t')" - comp=\${comp//\$tab/:} - - __${name}_debug "Adding completion: \${comp}" - completions+=\${comp} - lastComp=\$comp - fi - done < <(printf "%s\n" "\${out[@]}") - - # Add a delimiter after the activeHelp statements, but only if: - # - there are completions following the activeHelp statements, or - # - file completion will be performed (so there will be choices after the activeHelp) - if [ \$hasActiveHelp -eq 1 ]; then - if [ \${#completions} -ne 0 ] || [ \$((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then - __${name}_debug "Adding activeHelp delimiter" - compadd -x "--" - hasActiveHelp=0 - fi - fi - - if [ \$((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then - __${name}_debug "Activating nospace." - noSpace="-S ''" - fi - - if [ \$((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then - __${name}_debug "Activating keep order." - keepOrder="-V" - fi - - if [ \$((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then - # File extension filtering - local filteringCmd - filteringCmd='_files' - for filter in \${completions[@]}; do - if [ \${filter[1]} != '*' ]; then - # zsh requires a glob pattern to do file filtering - filter="\\*.\$filter" - fi - filteringCmd+=" -g \$filter" - done - filteringCmd+=" \${flagPrefix}" - - __${name}_debug "File filtering command: \$filteringCmd" - _arguments '*:filename:'"\$filteringCmd" - elif [ \$((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then - # File completion for directories only - local subdir - subdir="\${completions[1]}" - if [ -n "\$subdir" ]; then - __${name}_debug "Listing directories in \$subdir" - pushd "\${subdir}" >/dev/null 2>&1 - else - __${name}_debug "Listing directories in ." - fi - - local result - _arguments '*:dirname:_files -/'" \${flagPrefix}" - result=\$? - if [ -n "\$subdir" ]; then - popd >/dev/null 2>&1 - fi - return \$result - else - __${name}_debug "Calling _describe" - if eval _describe \$keepOrder "completions" completions -Q \${flagPrefix} \${noSpace}; then - __${name}_debug "_describe found some completions" - - # Return the success of having called _describe - return 0 - else - __${name}_debug "_describe did not find completions." - __${name}_debug "Checking if we should do file completion." - if [ \$((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then - __${name}_debug "deactivating file completion" - - # We must return an error code here to let zsh know that there were no - # completions found by _describe; this is what will trigger other - # matching algorithms to attempt to find completions. - # For example zsh can match letters in the middle of words. - return 1 - else - # Perform file completion - __${name}_debug "Activating file completion" - - # We must return the result of this command, so it must be the - # last command, or else we must store its result to return it. - _arguments '*:filename:_files'" \${flagPrefix}" - fi - fi - fi -} - -# don't run the completion function when being sourced or eval-ed -if [ "\${funcstack[1]}" = "_${name}" ]; then - _${name} -fi -`; + }); } - -cli.parse(); diff --git a/demo.ts b/demo.ts index affcca7..22be912 100644 --- a/demo.ts +++ b/demo.ts @@ -1,94 +1,236 @@ import fs from "fs/promises"; -import cac, { CAC } from "cac"; -import { Callback, flagMap, Positional, positionalMap } from "./shared"; +import cac from "cac"; +import { + Callback, + Completion, + flagMap, + Positional, + positionalMap, +} from "./shared"; import path from "path"; +import tab from "./cac"; -const cli = cac("cac"); +// const cli = cac("cac"); -cli.option("--type [type]", "Choose a project type", { - default: "node", -}); -cli.option("--name ", "Provide your name"); -cli.option("--name23 ", "Provide your name23"); +// cli.option("--type [type]", "Choose a project type", { +// default: "node", +// }); +// cli.option("--name ", "Provide your name"); +// cli.option("--name23 ", "Provide your name23"); -cli.command("start").option("--port ", "your port"); +// cli.command("start").option("--port ", "your port"); -cli.command("help"); +// cli.command("help"); -cli.command("help config").option("--foo", "foo option"); +// cli.command("help config").option("--foo", "foo option"); -cli.command("test dev").option("--foo", "foo option"); +// cli.command("test dev").option("--foo", "foo option"); -cli - .command("deploy [version] [...files]") +// cli.command("deploy [version] [...files]"); -cli.version("0.0.0"); -cli.help(); +// cli.version("0.0.0"); +// cli.help(); -cli.parse(); +// for (const c of [cli.globalCommand, ...cli.commands]) { +// for (const o of [...cli.globalCommand.options, ...c.options]) { +// if (o.rawName === "--type [type]") { +// flagMap.set(`${c.name} ${o.name}`, () => { +// return [ +// { +// action: "standalone", +// description: "Standalone type", +// }, +// { +// action: "complex", +// description: "Complex type", +// }, +// ]; +// }); +// } +// } + +// // console.log(c.args); +// if (c.name === "deploy") { +// const positionals: Positional[] = []; +// positionalMap.set(c.name, positionals); +// for (const arg of c.args) { +// const completion: Callback = async () => { +// if (arg.value === "environment") { +// return [ +// { +// action: "node", +// }, +// { +// action: "deno", +// }, +// ]; +// } +// if (arg.value === "version") { +// return [ +// { +// action: "0.0.0", +// }, +// { +// action: "0.0.1", +// }, +// ]; +// } +// if (arg.value === "files") { +// const currentDir = process.cwd(); +// const files = await fs.readdir(currentDir); +// const jsonFiles = files.filter( +// (file) => path.extname(file).toLowerCase() === ".json", +// ); +// return jsonFiles.map((file) => ({ action: file })); +// } +// return []; +// }; + +// positionals.push({ +// required: arg.required, +// variadic: arg.variadic, +// completion, +// }); +// } +// } +// } + +const cli = cac("vite"); // Using 'vite' as the CLI tool name + +// Custom converters (placeholders) +function convertBase(value) { + return value; +} + +function convertHost(value) { + return value; +} +// Global options +cli + .option("-c, --config ", `[string] use specified config file`) + .option("--base ", `[string] public base path (default: /)`, { + type: [convertBase], + }) + .option("-l, --logLevel ", `[string] info | warn | error | silent`) + .option("--clearScreen", `[boolean] allow/disable clear screen when logging`) + .option("-d, --debug [feat]", `[string | boolean] show debug logs`) + .option("-f, --filter ", `[string] filter debug logs`) + .option("-m, --mode ", `[string] set env mode`); + +// Dev command +cli + .command("[root]", "start dev server") // default command + .alias("serve") // the command is called 'serve' in Vite's API + .alias("dev") // alias to align with the script name + .option("--host [host]", `[string] specify hostname`, { type: [convertHost] }) + .option("--port ", `[number] specify port`) + .option("--open [path]", `[boolean | string] open browser on startup`) + .option("--cors", `[boolean] enable CORS`) + .option("--strictPort", `[boolean] exit if specified port is already in use`) + .option( + "--force", + `[boolean] force the optimizer to ignore the cache and re-bundle`, + ) + .action((root, options) => { + console.log(`Starting dev server at ${root || "."} with options:`, options); + }); +// Build positional completions for each command using command.args for (const c of [cli.globalCommand, ...cli.commands]) { + // Handle options for (const o of [...cli.globalCommand.options, ...c.options]) { - if (o.rawName === "--type [type]") { - flagMap.set(`${c.name} ${o.name}`, () => { + const optionKey = `${c.name} ${o.name}`; + + if (o.rawName.includes("--logLevel ")) { + // Completion for --logLevel + flagMap.set(optionKey, async (previousArgs, toComplete) => { + return [ + { action: "info", description: "Info level logging" }, + { action: "warn", description: "Warning level logging" }, + { action: "error", description: "Error level logging" }, + { action: "silent", description: "No logging" }, + ].filter((comp) => comp.action.startsWith(toComplete)); + }); + } + + if (o.rawName.includes("--mode ")) { + // Completion for --mode + flagMap.set(optionKey, async (previousArgs, toComplete) => { + return [ + { action: "production", description: "Production mode" }, + { action: "development", description: "Development mode" }, + { action: "staging", description: "Staging mode" }, + ].filter((comp) => comp.action.startsWith(toComplete)); + }); + } + + if (o.rawName.includes("--port ")) { + // Completion for --port + flagMap.set(optionKey, async (previousArgs, toComplete) => { + console.log({previousArgs, toComplete}) return [ - { - action: "standalone", - description: "Standalone type", - }, - { - action: "complex", - description: "Complex type", - }, - ]; + { action: "3000", description: "Development server port" }, + { action: "8080", description: "Alternative port" }, + { action: "80", description: "HTTP port" }, + { action: "443", description: "HTTPS port" }, + { action: "5000", description: "Common backend port" }, + ].filter((comp) => comp.action.startsWith(toComplete)); + }); + } + + if (o.rawName.includes("--host [host]")) { + // Completion for --host + flagMap.set(optionKey, async (previousArgs, toComplete) => { + return [ + { action: "localhost", description: "Localhost" }, + { action: "0.0.0.0", description: "All interfaces" }, + { action: "127.0.0.1", description: "Loopback interface" }, + ].filter((comp) => comp.action.startsWith(toComplete)); + }); + } + + if (o.rawName.includes("--config ")) { + // Completion for --config + flagMap.set(optionKey, async (previousArgs, toComplete) => { + const configFiles = ["vite.config.ts", "vite.config.js"].filter( + (file) => file.startsWith(toComplete), + ); + return configFiles.map((file) => ({ action: file })); }); } + + // Add more option completions as needed } - // console.log(c.args); - if (c.name === "deploy") { - const positionals: Positional[] = []; - positionalMap.set(c.name, positionals); - for (const arg of c.args) { - const completion: Callback = async () => { - if (arg.value === "environment") { - return [ - { - action: "node", - }, - { - action: "deno", - }, - ]; - } - if (arg.value === "version") { - return [ - { - action: "0.0.0", - }, - { - action: "0.0.1", - }, - ]; - } - if (arg.value === "files") { + // Handle positional arguments + if (c.args && c.args.length > 0) { + const positionals = c.args.map((arg) => ({ + required: arg.required, + variadic: arg.variadic, + value: arg.value, + completion: async (previousArgs, toComplete) => { + if (arg.value === "root") { + // Completion for the [root] positional argument in the dev command const currentDir = process.cwd(); - const files = await fs.readdir(currentDir); - const jsonFiles = files.filter( - (file) => path.extname(file).toLowerCase() === ".json", - ); - return jsonFiles.map((file) => ({ action: file })); + const dirs = await fs.readdir(currentDir); + const dirCompletions: Completion[] = []; + for (const dir of dirs) { + const dirPath = path.join(currentDir, dir); + const stat = await fs.lstat(dirPath); + if (stat.isDirectory() && dir.startsWith(toComplete)) { + dirCompletions.push({ action: dir, description: "" }); + } + } + return dirCompletions; } return []; - }; + }, + })); - positionals.push({ - required: arg.required, - variadic: arg.variadic, - completion, - }); - } + positionalMap.set(c.name, positionals); } } +tab(cli); +cli.parse(); diff --git a/fish.ts b/fish.ts index a91a084..3f6a4f1 100644 --- a/fish.ts +++ b/fish.ts @@ -1,2 +1,2 @@ -export function generate(name: string, includeDescription: boolean) { +export function generate(name: string, exec: string) { } diff --git a/powershell.ts b/powershell.ts index a91a084..3f6a4f1 100644 --- a/powershell.ts +++ b/powershell.ts @@ -1,2 +1,2 @@ -export function generate(name: string, includeDescription: boolean) { +export function generate(name: string, exec: string) { } diff --git a/shared.ts b/shared.ts index 134123a..5925d23 100644 --- a/shared.ts +++ b/shared.ts @@ -53,7 +53,7 @@ export const ShellCompDirective = { -type Completion = { +export type Completion = { action: string; description?: string; }; diff --git a/zsh.ts b/zsh.ts index 5f938be..e7bcc5e 100644 --- a/zsh.ts +++ b/zsh.ts @@ -1,11 +1,6 @@ -import { ShellCompDirective, ShellCompNoDescRequestCmd, ShellCompRequestCmd } from "./shared"; - -export function generate(name: string, exec: string, includeDescription: boolean) { - let compCmd = ShellCompRequestCmd; - if (!includeDescription) { - compCmd = ShellCompNoDescRequestCmd; - } +import { ShellCompDirective } from "./shared"; +export function generate(name: string, exec: string) { return `#compdef ${name} compdef _${name} ${name} @@ -52,7 +47,7 @@ _${name}() { fi # Prepare the command to obtain completions - requestComp="${exec} ${compCmd} -- \${words[2,-1]}" + requestComp="${exec} complete -- \${words[2,-1]}" if [ "\${lastChar}" = "" ]; then # If the last parameter is complete (there is a space following it) # We add an extra empty parameter so we can indicate this to the go completion code.