From 1d0beede3e54b436c0e15fab6503ef458432c528 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:30:37 +0100 Subject: [PATCH 1/2] feat: add completion config support to cac Adds completion config support and moves some things to the shared module. There will be a better way to share logic between these two even more but it hasn't yet clicked in my head. --- src/cac.ts | 22 ++++++++++++++++------ src/citty.ts | 20 ++------------------ src/shared.ts | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/cac.ts b/src/cac.ts index 58ac7fc..7c1d219 100644 --- a/src/cac.ts +++ b/src/cac.ts @@ -3,7 +3,8 @@ import * as bash from './bash'; import * as fish from './fish'; import * as powershell from './powershell'; import type { CAC } from 'cac'; -import { Completion } from './'; +import { Completion } from './index'; +import { CompletionConfig, noopHandler } from './shared'; const execPath = process.execPath; const processArgs = process.argv.slice(1); @@ -17,7 +18,10 @@ function quoteIfNeeded(path: string): string { return path.includes(' ') ? `'${path}'` : path; } -export default function tab(instance: CAC): Completion { +export default async function tab( + instance: CAC, + completionConfig?: CompletionConfig +) { const completion = new Completion(); // Add all commands and their options @@ -29,24 +33,30 @@ export default function tab(instance: CAC): Completion { arg.startsWith('[') ); // true if optional (wrapped in []) + const isRootCommand = cmd.name === '@@global@@'; + const commandCompletionConfig = isRootCommand + ? completionConfig + : completionConfig?.subCommands?.[cmd.name]; + // Add command to completion const commandName = completion.addCommand( - cmd.name === '@@global@@' ? '' : cmd.name, + isRootCommand ? '' : cmd.name, cmd.description || '', args, - async () => [] + commandCompletionConfig?.handler ?? noopHandler ); // Add command options for (const option of [...instance.globalCommand.options, ...cmd.options]) { // Extract short flag from the name if it exists (e.g., "-c, --config" -> "c") const shortFlag = option.name.match(/^-([a-zA-Z]), --/)?.[1]; + const argName = option.name.replace(/^-[a-zA-Z], --/, ''); completion.addOption( commandName, - `--${option.name.replace(/^-[a-zA-Z], --/, '')}`, // Remove the short flag part if it exists + `--${argName}`, // Remove the short flag part if it exists option.description || '', - async () => [], + commandCompletionConfig?.options?.[argName]?.handler ?? noopHandler, shortFlag ); } diff --git a/src/citty.ts b/src/citty.ts index e435b3e..e786cd1 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -3,7 +3,7 @@ import * as zsh from './zsh'; import * as bash from './bash'; import * as fish from './fish'; import * as powershell from './powershell'; -import { Completion, type Handler } from '.'; +import { Completion } from './index'; import type { ArgsDef, CommandDef, @@ -11,6 +11,7 @@ import type { SubCommandsDef, } from 'citty'; import { generateFigSpec } from './fig'; +import { CompletionConfig, noopHandler } from './shared'; function quoteIfNeeded(path: string) { return path.includes(' ') ? `'${path}'` : path; @@ -30,23 +31,6 @@ function isConfigPositional(config: CommandDef) { ); } -// TODO (43081j): use type inference some day, so we can type-check -// that the sub commands exist, the options exist, etc. -interface CompletionConfig { - handler?: Handler; - subCommands?: Record; - options?: Record< - string, - { - handler: Handler; - } - >; -} - -const noopHandler: Handler = () => { - return []; -}; - async function handleSubCommands( completion: Completion, subCommands: SubCommandsDef, diff --git a/src/shared.ts b/src/shared.ts index e69de29..23e4acf 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -0,0 +1,18 @@ +import { Handler } from './index'; + +export const noopHandler: Handler = () => { + return []; +}; + +// TODO (43081j): use type inference some day, so we can type-check +// that the sub commands exist, the options exist, etc. +export interface CompletionConfig { + handler?: Handler; + subCommands?: Record; + options?: Record< + string, + { + handler: Handler; + } + >; +} From 8c28a367c1afc271fbc3b7e48443322492690049 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Sun, 2 Mar 2025 19:57:57 +0100 Subject: [PATCH 2/2] feat: consistently export same type for all entrypoints Defines a `TabFunction` that all entrypoints export, such that we consistently support the same call signature. --- src/cac.ts | 11 +++++------ src/citty.ts | 14 ++++++++------ src/shared.ts | 7 ++++++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/cac.ts b/src/cac.ts index 7c1d219..077ca52 100644 --- a/src/cac.ts +++ b/src/cac.ts @@ -4,7 +4,7 @@ import * as fish from './fish'; import * as powershell from './powershell'; import type { CAC } from 'cac'; import { Completion } from './index'; -import { CompletionConfig, noopHandler } from './shared'; +import { noopHandler, TabFunction } from './shared'; const execPath = process.execPath; const processArgs = process.argv.slice(1); @@ -18,10 +18,7 @@ function quoteIfNeeded(path: string): string { return path.includes(' ') ? `'${path}'` : path; } -export default async function tab( - instance: CAC, - completionConfig?: CompletionConfig -) { +const tab: TabFunction = async (instance, completionConfig) => { const completion = new Completion(); // Add all commands and their options @@ -103,4 +100,6 @@ export default async function tab( }); return completion; -} +}; + +export default tab; diff --git a/src/citty.ts b/src/citty.ts index e786cd1..ad90a43 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -11,7 +11,7 @@ import type { SubCommandsDef, } from 'citty'; import { generateFigSpec } from './fig'; -import { CompletionConfig, noopHandler } from './shared'; +import { CompletionConfig, noopHandler, TabFunction } from './shared'; function quoteIfNeeded(path: string) { return path.includes(' ') ? `'${path}'` : path; @@ -92,10 +92,10 @@ async function handleSubCommands( } } -export default async function tab( - instance: CommandDef, - completionConfig?: CompletionConfig -) { +const tab: TabFunction> = async ( + instance, + completionConfig +) => { const completion = new Completion(); const meta = await resolve(instance.meta); @@ -204,7 +204,9 @@ export default async function tab( subCommands.complete = completeCommand; return completion; -} +}; + +export default tab; type Resolvable = T | Promise | (() => T) | (() => Promise); diff --git a/src/shared.ts b/src/shared.ts index 23e4acf..5f6f0ae 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,4 +1,4 @@ -import { Handler } from './index'; +import { Handler, Completion } from './index'; export const noopHandler: Handler = () => { return []; @@ -16,3 +16,8 @@ export interface CompletionConfig { } >; } + +export type TabFunction = ( + instance: T, + completionConfig?: CompletionConfig +) => Promise;