[READ-ONLY] Mirror of https://github.com/bombshell-dev/tab. shell autocompletions for javascript CLIs
4

Configure Feed

Select the types of activity you want to include in your feed.

citty update

AmirSa12 (Dec 11, 2024, 5:14 PM +0330) e3be5d3b 4c7b0a18

+256 -92
+169
citty.ts
··· 1 + import { defineCommand } from "citty"; 2 + import * as zsh from "./zsh"; 3 + import * as bash from "./bash"; 4 + import * as fish from "./fish"; 5 + import * as powershell from "./powershell"; 6 + import { 7 + flagMap, 8 + Positional, 9 + positionalMap, 10 + ShellCompDirective, 11 + } from "./shared"; 12 + 13 + function quoteIfNeeded(path) { 14 + return path.includes(" ") ? `'${path}'` : path; 15 + } 16 + 17 + const execPath = process.execPath; 18 + const processArgs = process.argv.slice(1); 19 + const quotedExecPath = quoteIfNeeded(execPath); 20 + const quotedProcessArgs = processArgs.map(quoteIfNeeded); 21 + const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); 22 + const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`; 23 + 24 + export default function tab(mainCommand) { 25 + mainCommand.subCommands["complete"] = defineCommand({ 26 + meta: { 27 + name: "complete", 28 + description: "Generate shell completion scripts", 29 + }, 30 + args: { 31 + shell: { 32 + type: "positional", 33 + required: false, 34 + description: "Specify shell type", 35 + }, 36 + }, 37 + async run(ctx) { 38 + let shell: string | undefined = ctx.args.shell; 39 + if (shell?.startsWith("--")) { 40 + shell = undefined; 41 + } 42 + 43 + const extra = ctx.args._ || []; 44 + 45 + switch (shell) { 46 + case "zsh": { 47 + const script = zsh.generate(mainCommand.meta.name, x); 48 + console.log(script); 49 + break; 50 + } 51 + case "bash": { 52 + const script = bash.generate(mainCommand.meta.name, x); 53 + console.log(script); 54 + break; 55 + } 56 + case "fish": { 57 + const script = fish.generate(mainCommand.meta.name, x); 58 + console.log(script); 59 + break; 60 + } 61 + case "powershell": { 62 + const script = powershell.generate(mainCommand.meta.name, x); 63 + console.log(script); 64 + break; 65 + } 66 + default: { 67 + const args = extra; 68 + let directive = ShellCompDirective.ShellCompDirectiveDefault; 69 + const completions: string[] = []; 70 + const endsWithSpace = args[args.length - 1] === ""; 71 + 72 + if (endsWithSpace) args.pop(); 73 + let toComplete = args[args.length - 1] || ""; 74 + const previousArgs = args.slice(0, -1); 75 + 76 + let matchedCommand = mainCommand; 77 + 78 + if (previousArgs.length > 0) { 79 + const lastPrevArg = previousArgs[previousArgs.length - 1]; 80 + if (lastPrevArg.startsWith("--")) { 81 + const flagCompletion = flagMap.get(lastPrevArg); 82 + if (flagCompletion) { 83 + const flagSuggestions = await flagCompletion(previousArgs, toComplete); 84 + completions.push( 85 + ...flagSuggestions.map( 86 + (comp) => `${comp.action}\t${comp.description ?? ""}` 87 + ) 88 + ); 89 + completions.forEach((comp) => console.log(comp)); 90 + console.log(`:${directive}`); 91 + return; 92 + } 93 + } 94 + } 95 + 96 + if (toComplete.startsWith("--")) { 97 + if (toComplete === "--") { 98 + const allFlags = [...flagMap.keys()]; 99 + completions.push( 100 + ...allFlags.map( 101 + (flag) => 102 + `${flag}\t${matchedCommand.args[flag.slice(2)]?.description ?? "Option"}` 103 + ) 104 + ); 105 + } else { 106 + const flagNamePartial = toComplete.slice(2); 107 + const flagKeyPartial = `--${flagNamePartial}`; 108 + 109 + if (flagMap.has(toComplete)) { 110 + const flagCompletion = flagMap.get(toComplete); 111 + if (flagCompletion) { 112 + const flagSuggestions = await flagCompletion(previousArgs, ""); 113 + completions.push( 114 + ...flagSuggestions.map( 115 + (comp) => `${comp.action}\t${comp.description ?? ""}` 116 + ) 117 + ); 118 + } 119 + } else { 120 + const matchingFlags = [...flagMap.keys()].filter((flag) => 121 + flag.startsWith(flagKeyPartial) 122 + ); 123 + 124 + completions.push( 125 + ...matchingFlags.map( 126 + (flag) => 127 + `${flag}\t${matchedCommand.args[flag.slice(2)]?.description ?? "Option"}` 128 + ) 129 + ); 130 + } 131 + } 132 + 133 + completions.forEach((comp) => console.log(comp)); 134 + console.log(`:${directive}`); 135 + return; 136 + } 137 + 138 + if (previousArgs.length === 0) { 139 + completions.push( 140 + ...Object.keys(mainCommand.subCommands || {}) 141 + .filter((cmd) => cmd !== "complete") 142 + .map( 143 + (cmd) => 144 + `${cmd}\t${mainCommand.subCommands[cmd]?.meta.description ?? ""}` 145 + ) 146 + ); 147 + } else { 148 + const positionalCompletions = 149 + positionalMap.get(matchedCommand.meta.name) || []; 150 + for (const positional of positionalCompletions) { 151 + const suggestions = await positional.completion( 152 + previousArgs, 153 + toComplete 154 + ); 155 + completions.push( 156 + ...suggestions.map( 157 + (comp) => `${comp.action}\t${comp.description ?? ""}` 158 + ) 159 + ); 160 + } 161 + } 162 + 163 + completions.forEach((comp) => console.log(comp)); 164 + console.log(`:${directive}`); 165 + } 166 + } 167 + }, 168 + }); 169 + }
+87 -92
demo.citty.ts
··· 1 1 import { defineCommand, createMain } from "citty"; 2 - // import { tab } from "./citty"; 2 + import tab from "./citty"; 3 + import { flagMap, positionalMap } from "./shared"; 4 + 3 5 const main = defineCommand({ 4 6 meta: { 5 7 name: "vite", 6 8 description: "Vite CLI tool", 7 9 }, 8 10 args: { 9 - config: { 10 - type: "string", 11 - description: "Use specified config file", 12 - alias: "c", 13 - }, 14 - base: { 15 - type: "string", 16 - description: "Public base path (default: /)", 17 - }, 18 - logLevel: { 19 - type: "string", 20 - description: "info | warn | error | silent", 21 - alias: "l", 22 - }, 23 - clearScreen: { 24 - type: "boolean", 25 - description: "Allow/disable clear screen when logging", 26 - }, 27 - debug: { 28 - type: "string", 29 - description: "Show debug logs", 30 - alias: "d", 31 - }, 32 - filter: { 33 - type: "string", 34 - description: "Filter debug logs", 35 - alias: "f", 36 - }, 37 - mode: { 38 - type: "string", 39 - description: "Set env mode", 40 - alias: "m", 41 - }, 11 + config: { type: "string", description: "Use specified config file", alias: "c" }, 12 + base: { type: "string", description: "Public base path (default: /)" }, 13 + logLevel: { type: "string", description: "info | warn | error | silent", alias: "l" }, 14 + clearScreen: { type: "boolean", description: "Allow/disable clear screen when logging" }, 15 + debug: { type: "string", description: "Show debug logs", alias: "d" }, 16 + filter: { type: "string", description: "Filter debug logs", alias: "f" }, 17 + mode: { type: "string", description: "Set env mode", alias: "m" }, 42 18 }, 43 19 run(ctx) { 44 - if (ctx.args._?.[0] !== "complete" && devCommand.run) { 45 - devCommand.run(ctx); 46 - } else if (!devCommand.run) { 47 - console.error("Error: dev command is not defined."); 20 + const firstArg = ctx.args._?.[0]; 21 + if (firstArg && devCommand?.run) { 22 + devCommand.run({ ...ctx, args: { ...ctx.args, root: firstArg } }); 48 23 } 49 24 }, 50 25 }); ··· 55 30 description: "Start dev server", 56 31 }, 57 32 args: { 58 - root: { 59 - type: "positional", 60 - description: "Root directory", 61 - required: false, 62 - default: ".", 63 - }, 64 - host: { 65 - type: "string", 66 - description: "Specify hostname", 67 - }, 68 - port: { 69 - type: "string", 70 - description: "Specify port", 71 - }, 72 - open: { 73 - type: "boolean", 74 - description: "Open browser on startup", 75 - }, 76 - cors: { 77 - type: "boolean", 78 - description: "Enable CORS", 79 - }, 80 - strictPort: { 81 - type: "boolean", 82 - description: "Exit if specified port is already in use", 83 - }, 84 - force: { 85 - type: "boolean", 86 - description: "Force the optimizer to ignore the cache and re-bundle", 87 - }, 33 + root: { type: "positional", description: "Root directory", default: "." }, 34 + host: { type: "string", description: "Specify hostname" }, 35 + port: { type: "string", description: "Specify port" }, 36 + open: { type: "boolean", description: "Open browser on startup" }, 37 + cors: { type: "boolean", description: "Enable CORS" }, 38 + strictPort: { type: "boolean", description: "Exit if specified port is already in use" }, 39 + force: { type: "boolean", description: "Force the optimizer to ignore the cache and re-bundle" }, 88 40 }, 89 41 run({ args }) { 90 42 const { root, port, ...options } = args; 91 43 const parsedPort = port ? parseInt(port, 10) : undefined; 92 44 93 - if (args._?.[0] !== "complete") { 94 - if (!root || root === ".") { 95 - console.log("Suggested root directories:"); 96 - console.log("- src/"); 97 - console.log("- ./"); 98 - } 45 + if (!root || root === ".") { 46 + console.log("Suggested root directories:"); 47 + console.log("- src/"); 48 + console.log("- ./"); 49 + } 50 + }, 51 + }); 52 + 53 + main.subCommands = { dev: devCommand }; 54 + 55 + for (const command of [main, ...Object.values(main.subCommands)]) { 56 + const commandName = command.meta.name; 99 57 100 - const formattedOptions = { ...options, port: parsedPort }; 101 - if (formattedOptions._) { 102 - formattedOptions["--"] = formattedOptions._; 103 - delete formattedOptions._; 104 - } 58 + for (const [argName, argConfig] of Object.entries(command.args || {})) { 59 + const optionKey = `--${argName}`; 60 + if (argName === "port") { 61 + flagMap.set(optionKey, async (_, toComplete) => { 62 + const options = [ 63 + { action: "3000", description: "Development server port" }, 64 + { action: "8080", description: "Alternative port" }, 65 + ]; 66 + return toComplete 67 + ? options.filter(comp => comp.action.startsWith(toComplete)) 68 + : options; 69 + }); 70 + } 105 71 106 - const cleanedOptions = Object.fromEntries( 107 - Object.entries(formattedOptions).filter(([_, v]) => v !== undefined) 108 - ); 72 + if (argName === "host") { 73 + flagMap.set(optionKey, async (_, toComplete) => { 74 + const options = [ 75 + { action: "localhost", description: "Localhost" }, 76 + { action: "0.0.0.0", description: "All interfaces" }, 77 + ]; 78 + return toComplete 79 + ? options.filter(comp => comp.action.startsWith(toComplete)) 80 + : options; 81 + }); 82 + } 109 83 110 - console.log( 111 - `Starting dev server at ${root || "."} with options:`, 112 - cleanedOptions 113 - ); 84 + if (argName === "mode") { 85 + flagMap.set(optionKey, async (_, toComplete) => { 86 + const options = [ 87 + { action: "development", description: "Development mode" }, 88 + { action: "production", description: "Production mode" }, 89 + ]; 90 + return toComplete 91 + ? options.filter(comp => comp.action.startsWith(toComplete)) 92 + : options; 93 + }); 114 94 } 115 - }, 116 - }); 95 + } 117 96 118 - main.subCommands = { 119 - dev: devCommand, 120 - }; 97 + if (command.args) { 98 + const positionals = Object.entries(command.args) 99 + .filter(([, config]) => config.type === "positional") 100 + .map(([argName, argConfig]) => ({ 101 + value: argName, 102 + required: !!argConfig.required, 103 + completion: async (_, toComplete) => { 104 + const options = [ 105 + { action: "src/", description: "Source directory" }, 106 + { action: "./", description: "Current directory" }, 107 + ]; 108 + return toComplete 109 + ? options.filter(comp => comp.action.startsWith(toComplete)) 110 + : options; 111 + }, 112 + })); 113 + positionalMap.set(commandName, positionals); 114 + } 115 + } 121 116 122 - // tab(main); 117 + tab(main); 123 118 124 119 const cli = createMain(main); 125 120 cli();