From 3905a86e7dedf18784a87ce2eb3b72bb04f9045a Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Mon, 7 Jul 2025 18:58:49 +0900 Subject: [PATCH 01/22] feat: completion plugin --- design/6.completion-plugin.md | 264 ++++++++++++++++++++ package.json | 1 + packages/gunshi/src/cli/core.ts | 16 +- packages/gunshi/src/definition.ts | 1 + packages/gunshi/src/types.ts | 6 + packages/gunshi/src/utils.ts | 4 +- packages/plugin-completion/package.json | 2 + packages/plugin-completion/src/index.ts | 198 ++++++++++++++- packages/plugin-completion/src/types.ts | 40 +++ packages/plugin-completion/tsdown.config.ts | 1 + pnpm-lock.yaml | 14 ++ 11 files changed, 533 insertions(+), 14 deletions(-) create mode 100644 design/6.completion-plugin.md create mode 100644 packages/plugin-completion/src/types.ts diff --git a/design/6.completion-plugin.md b/design/6.completion-plugin.md new file mode 100644 index 000000000..86752fcd5 --- /dev/null +++ b/design/6.completion-plugin.md @@ -0,0 +1,264 @@ +# @bombshell-dev/tabを使用したplugin-completionの設計(修正版) + +PluginContextのaddCommandを活用して、`complete`サブコマンドベースの補完サーバーを実装します。 + +## 1. プラグイン構造 + +```typescript +// packages/plugin-completion/src/index.ts +import { plugin, type PluginWithExtension } from '@gunshi/plugin' +import { Completion, script } from '@bombshell-dev/tab' +import type { Command, LazyCommand } from 'gunshi' + +export interface CompletionCommandContext { + // 補完インスタンスへのアクセス + completion: Completion + // カスタム補完ハンドラの登録 + registerHandler(commandPath: string[], handler: CompletionHandler): void +} + +type CompletionHandler = (args: { + previousArgs: string[] + toComplete: string + endsWithSpace: boolean +}) => Promise> +``` + +## 2. プラグイン実装 + +```typescript +export default function completion(): PluginWithExtension { + const completion = new Completion() + const customHandlers = new Map() + + return plugin({ + id: 'g:completion', + name: 'completion', + + setup(ctx) { + // 1. completeサブコマンドを追加(補完サーバー) + ctx.addCommand('complete', { + name: 'complete', + description: 'Handle shell completions (internal use)', + run: async cmdCtx => { + // "--"以降の引数を取得 + const argsToComplete = cmdCtx.rest + + // @bombshell-dev/tabで補完を解析 + const results = await completion.parse(argsToComplete) + + // 結果を出力(シェルが読み取る形式) + results.items.forEach(item => { + console.log(item.value) + if (item.description) { + console.log(`:${item.description}`) + } + }) + + // ディレクティブを出力 + console.log(`\n${results.directive}`) + } + }) + + // 2. completionサブコマンドを追加(スクリプト生成) + ctx.addCommand('completion', { + name: 'completion', + description: 'Generate shell completion script', + args: { + shell: { + type: 'string', + description: 'Target shell (bash|zsh|fish)', + default: detectShell() + } + }, + run: async cmdCtx => { + const shell = cmdCtx.values.shell as string + const execName = cmdCtx.env.name || 'cli' + const execPath = process.argv[1] + + // シェルスクリプトを生成 + const scripts = script(execName, execPath) + + if (!(shell in scripts)) { + throw new Error(`Unsupported shell: ${shell}`) + } + + cmdCtx.log(scripts[shell as keyof typeof scripts]) + } + }) + }, + + extension(ctx) { + // 3. gunshiのコマンド構造を@bombshell-dev/tabに変換 + setupCompletions(completion, ctx.pluginContext, customHandlers) + + return { + completion, + registerHandler: (commandPath, handler) => { + const key = commandPath.join(':') + customHandlers.set(key, handler) + } + } + } + }) +} +``` + +## 3. コマンド構造の変換 + +```typescript +function setupCompletions( + completion: Completion, + pluginContext: PluginContext, + customHandlers: Map +) { + const subCommands = pluginContext.subCommands + + // ルートレベルの補完 + completion.addCommand('', 'Root command', async ({ toComplete, previousArgs }) => { + // completeコマンド自体は補完候補から除外 + if (previousArgs.length === 0) { + const commands = Array.from(subCommands.keys()) + .filter(cmd => cmd !== 'complete' && cmd !== 'completion') + .filter(cmd => cmd.startsWith(toComplete)) + .map(cmd => ({ + value: cmd, + description: subCommands.get(cmd)?.description + })) + + return commands + } + + return [] + }) + + // 各サブコマンドの補完を設定 + for (const [name, command] of subCommands) { + if (name === 'complete' || name === 'completion') continue + + setupCommandCompletion(completion, name, command, customHandlers) + } + + // グローバルオプションの補完 + for (const [name, schema] of pluginContext.globalOptions) { + completion.addOption('', `--${name}`, schema.description || '', async () => { + return getValueCompletions(schema) + }) + } +} + +function setupCommandCompletion( + completion: Completion, + name: string, + command: Command | LazyCommand, + customHandlers: Map +) { + completion.addCommand(name, command.description || '', async args => { + // カスタムハンドラがあれば使用 + const handler = customHandlers.get(name) + if (handler) { + return handler(args) + } + + // デフォルトはオプションの補完 + return [] + }) + + // コマンドのオプションを補完に追加 + if ('args' in command && command.args) { + for (const [argName, argSchema] of Object.entries(command.args)) { + completion.addOption(name, `--${argName}`, argSchema.description || '', async () => { + return getValueCompletions(argSchema) + }) + } + } +} + +function getValueCompletions(schema: any) { + if (schema.type === 'boolean') { + return [ + { value: 'true', description: 'Enable' }, + { value: 'false', description: 'Disable' } + ] + } + + if (schema.enum) { + return schema.enum.map((value: string) => ({ value })) + } + + return [] +} +``` + +## 4. 使用例 + +```typescript +// ユーザーのCLIアプリケーション +import { cli } from 'gunshi' +import completion from '@gunshi/plugin-completion' + +const comp = completion() + +await cli(process.argv.slice(2), { + name: 'my-cli', + description: 'My CLI tool', + plugins: [comp], + subCommands: new Map([ + [ + 'build', + { + description: 'Build the project', + args: { + target: { + type: 'string', + enum: ['dev', 'prod'], + description: 'Build target' + } + }, + run: async ctx => { + // カスタム補完を登録 + if (ctx.extensions['g:completion']) { + ctx.extensions['g:completion'].registerHandler(['build'], async ({ toComplete }) => { + // プロジェクトファイルから動的に補完候補を生成 + const projects = await getProjects() + return projects + .filter(p => p.startsWith(toComplete)) + .map(p => ({ value: p, description: 'Project' })) + }) + } + } + } + ] + ]) +}) +``` + +## 5. シェルセットアップ + +```bash +# 1. 補完スクリプトを生成 +$ my-cli completion zsh > ~/.my-cli-completion.zsh + +# 2. シェル設定に追加 +$ echo 'source ~/.my-cli-completion.zsh' >> ~/.zshrc + +# 3. 補完が動作 +$ my-cli bu[TAB] +build -- Build the project + +$ my-cli build --target [TAB] +dev -- Development +prod -- Production +``` + +## 6. 内部動作フロー + +1. ユーザーがTabキーを押す +2. シェルが`my-cli complete -- build --ta`を実行 +3. gunshiが`complete`サブコマンドを実行 +4. @bombshell-dev/tabのCompletionが引数を解析 +5. 登録されたハンドラから補完候補を生成 +6. 結果を標準出力に出力 +7. シェルが補完候補を表示 + +この設計により、環境変数に依存せず、クリーンなサブコマンドベースの補完サーバーを実装できます。 diff --git a/package.json b/package.json index 01234342e..1af4c87fd 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "build:docs": "pnpm -r build:docs", "build:gunshi": "pnpm -F gunshi build", "build:plugin": "pnpm -F @gunshi/bone build", + "build:plugin:completion": "pnpm -F @gunshi/plugin-completion build", "build:plugin:global": "pnpm -F @gunshi/plugin-global build", "build:plugin:i18n": "pnpm -F @gunshi/plugin-i18n build", "build:plugin:renderer": "pnpm -F @gunshi/plugin-renderer build", diff --git a/packages/gunshi/src/cli/core.ts b/packages/gunshi/src/cli/core.ts index ad8c48a9c..603b5d20d 100644 --- a/packages/gunshi/src/cli/core.ts +++ b/packages/gunshi/src/cli/core.ts @@ -131,16 +131,20 @@ function resolveArguments( function createInitialSubCommands( options: CliOptions, - entry: Command | CommandRunner | LazyCommand + entryCmd: Command | CommandRunner | LazyCommand ): Map | LazyCommand> { const subCommands = new Map(options.subCommands) // add entry command to sub commands if there are sub commands if (options.subCommands || subCommands.size > 0) { - if (isLazyCommand(entry)) { - if (entry.commandName) subCommands.set(entry.commandName, entry as LazyCommand) - } else if (typeof entry === 'object' && entry.name) { - subCommands.set(entry.name, entry as Command) + if (isLazyCommand(entryCmd)) { + if (entryCmd.commandName) { + entryCmd.entry = true + subCommands.set(entryCmd.commandName, entryCmd as LazyCommand) + } + } else if (typeof entryCmd === 'object' && entryCmd.name) { + entryCmd.entry = true + subCommands.set(entryCmd.name, entryCmd as Command) } } @@ -209,7 +213,7 @@ async function resolveCommand( } else { // inline command (command runner) return { - command: { run: entry as CommandRunner } as Command, + command: { run: entry as CommandRunner, entry: true } as Command, callMode: 'entry' } } diff --git a/packages/gunshi/src/definition.ts b/packages/gunshi/src/definition.ts index 70a67149b..171507a27 100644 --- a/packages/gunshi/src/definition.ts +++ b/packages/gunshi/src/definition.ts @@ -169,6 +169,7 @@ export function lazy( lazyCommand.args = definition.args lazyCommand.examples = definition.examples lazyCommand.internal = definition.internal + lazyCommand.entry = definition.entry // @ts-ignore - resource property is now provided by plugin-i18n if ('resource' in definition) { // @ts-ignore diff --git a/packages/gunshi/src/types.ts b/packages/gunshi/src/types.ts index 59754576e..277f12623 100644 --- a/packages/gunshi/src/types.ts +++ b/packages/gunshi/src/types.ts @@ -414,6 +414,12 @@ export interface Command * @since v0.27.0 */ internal?: boolean + /** + * Whether this command is an entry command. + * @default undefined + * @since v0.27.0 + */ + entry?: boolean } /** diff --git a/packages/gunshi/src/utils.ts b/packages/gunshi/src/utils.ts index 54ced530c..4ee33955c 100644 --- a/packages/gunshi/src/utils.ts +++ b/packages/gunshi/src/utils.ts @@ -31,7 +31,8 @@ export async function resolveLazyCommand { + return [] +} /** * completion plugin for gunshi */ -export default function completion(): PluginWithoutExtension { +export default function completion( + options: CompletionConfig = {} +): PluginWithoutExtension { + const completion = new Completion() + return plugin({ - id: 'g:completion', + id: pluginId, name: 'completion', - setup(_ctx) { - // TODO(kazupon): implement completion plugin logic - } + async setup(ctx) { + /** + * add command for completion script generation + */ + + const completeName = 'complete' + ctx.addCommand(completeName, { + name: completeName, + // TODO(kazupon): support description localization + description: 'Generate shell completion script', + args: { + shell: { + type: 'positional', + // TODO(kazupon): support description localization + description: + 'Shell type to generate completion script (zsh, bash, fish, powershell, fig)' + } + }, + run: async cmdCtx => { + if (!cmdCtx.env.name) { + throw new Error('cli name is not defined.') + } + + let shell: string | undefined = cmdCtx._[0] + if (shell === '--') { + shell = undefined + } + + if (shell === undefined) { + const extra = cmdCtx._.slice(cmdCtx._.indexOf('--') + 1) + completion.parse(extra) + } else { + script(shell as Parameters[0], cmdCtx.env.name, quoteExec()) + } + } + }) + + /** + * setup bombshell completion + */ + + const entry = [...ctx.subCommands].map(([_, cmd]) => cmd).find(cmd => cmd.entry) + if (!entry) { + throw new Error( + 'No entry command found. Please ensure that an entry command is defined in the plugin context.' + ) + } + + // setup root level completion + const isPositional = hasPositional(await resolveLazyCommand(entry)) + const root = '' + // TODO(kazupon): more tweaking for root completion + completion.addCommand( + root, + entry.description || '', + isPositional ? [false] : [], + NOOP_HANDLER + ) + + const args = entry.args || (Object.create(null) as Args) + for (const [name, schema] of Object.entries(args)) { + if (schema.type === 'positional') { + continue // skip positional arguments on subcommands + } + // TODO(kazupon): more tweaking for root option completion + completion.addOption( + root, + `--${name}`, + schema.description || '', + NOOP_HANDLER, + schema.short + ) + } + + handleSubCommands(completion, ctx.subCommands, options.subCommands) + }, + + extension: async (_ctx, _cmd) => {} }) } + +function detectRuntime(): 'bun' | 'deno' | 'node' | 'unknown' { + if (globalThis.process !== undefined && globalThis.process.release?.name === 'node') { + return 'node' + } + // @ts-ignore -- NOTE: ignore, because development env is node.js + if (globalThis.Deno !== undefined) { + return 'deno' + } + // @ts-ignore -- NOTE: ignore, because development env is node.js + if (globalThis.Bun !== undefined) { + return 'bun' + } + return 'unknown' +} + +function quoteIfNeeded(path: string): string { + return path.includes(' ') ? `'${path}'` : path +} + +function quoteExec(): string { + const runtime = detectRuntime() + switch (runtime) { + case 'node': { + const execPath = process.execPath + const processArgs = process.argv.slice(1) + const quotedExecPath = quoteIfNeeded(execPath) + // eslint-disable-next-line unicorn/no-array-callback-reference + const quotedProcessArgs = processArgs.map(quoteIfNeeded) + // eslint-disable-next-line unicorn/no-array-callback-reference + const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded) + return `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}` + } + case 'deno': { + throw new Error('deno not implemented yet, welcome contributions :)') + } + case 'bun': { + throw new Error('deno not implemented yet, welcome contributions :)') + } + default: { + throw new Error('Unsupported runtime for completion script generation.') + } + } +} + +async function handleSubCommands( + completion: Completion, + subCommands: PluginContext['subCommands'], + configs: CompletionConfig['subCommands'] = {} +) { + for (const [name, cmd] of subCommands) { + if (cmd.internal || cmd.entry) { + continue // skip entry or internal command + } + const resolvedCmd = await resolveLazyCommand(cmd) + if (!resolvedCmd.description) { + throw new Error( + `Command "${name}" does not have a description. Please ensure that all commands have a description defined.` + ) + } + const isPositional = hasPositional(resolvedCmd) + // TODO(kazupon): more tweaking for subcommand completion + const handler = configs[name] || NOOP_HANDLER + const commandName = completion.addCommand( + name, + resolvedCmd.description, + isPositional ? [false] : [], + handler + ) + + const args = cmd.args || (Object.create(null) as Args) + for (const [name, schema] of Object.entries(args)) { + if (schema.type === 'positional') { + continue // skip positional arguments on subcommands + } + // TODO(kazupon): more tweaking for subcommand option completion + completion.addOption( + commandName, + `--${name}`, + schema.description || '', + NOOP_HANDLER, + schema.short + ) + } + } +} + +function hasPositional(cmd: Command | LazyCommand) { + return cmd.args && Object.values(cmd.args).some(arg => arg.type === 'positional') +} diff --git a/packages/plugin-completion/src/types.ts b/packages/plugin-completion/src/types.ts new file mode 100644 index 000000000..ebc46935c --- /dev/null +++ b/packages/plugin-completion/src/types.ts @@ -0,0 +1,40 @@ +/** + * @author kazuya kawaguchi (a.k.a. kazupon) + * @license MIT + */ + +import { namespacedId, PLUGIN_PREFIX } from '@gunshi/shared' + +import type { Handler } from '@bombsh/tab' +import type { GenerateNamespacedKey } from '@gunshi/shared' + +/** + * The unique identifier for the completion plugin. + */ +export const pluginId: GenerateNamespacedKey<'completion', typeof PLUGIN_PREFIX> = + namespacedId('completion') + +/** + * Type representing the unique identifier for the completion plugin. + */ +export type PluginId = typeof pluginId + +/** + * Extended command context which provides utilities via completion plugin. + * These utilities are available via `CommandContext.extensions['g:completion']`. + */ +export interface CompletionCommandContext {} + +/** + * Configuration for the completion plugin. + */ +export interface CompletionConfig { + /** + * The entry point handler. + */ + entry?: Handler + /** + * The handlers for subcommands. + */ + subCommands?: Record +} diff --git a/packages/plugin-completion/tsdown.config.ts b/packages/plugin-completion/tsdown.config.ts index 36adf282d..473a3abdf 100644 --- a/packages/plugin-completion/tsdown.config.ts +++ b/packages/plugin-completion/tsdown.config.ts @@ -7,6 +7,7 @@ const config: ReturnType = defineConfig({ clean: true, publint: true, dts: true, + noExternal: ['@bombsh/tab'], external: ['@gunshi/plugin'], hooks: { 'build:done': lintJsrExports() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3aa58fb98..dc2499bfa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -276,10 +276,16 @@ importers: packages/plugin-completion: dependencies: + '@bombsh/tab': + specifier: git+ssh://git@github.com:bombshell-dev/tab.git + version: tab@https://codeload.github.com/bombshell-dev/tab/tar.gz/6c42c55f46157ae97e2c059039e21ee412b48138 '@gunshi/plugin': specifier: workspace:* version: link:../plugin devDependencies: + '@gunshi/shared': + specifier: workspace:* + version: link:../shared deno: specifier: 'catalog:' version: 2.4.0 @@ -4497,6 +4503,10 @@ packages: resolution: {integrity: sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==} engines: {node: ^14.18.0 || >=16.0.0} + tab@https://codeload.github.com/bombshell-dev/tab/tar.gz/6c42c55f46157ae97e2c059039e21ee412b48138: + resolution: {tarball: https://codeload.github.com/bombshell-dev/tab/tar.gz/6c42c55f46157ae97e2c059039e21ee412b48138} + version: 0.0.0 + tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -9259,6 +9269,10 @@ snapshots: dependencies: '@pkgr/core': 0.2.4 + tab@https://codeload.github.com/bombshell-dev/tab/tar.gz/6c42c55f46157ae97e2c059039e21ee412b48138: + dependencies: + mri: 1.2.0 + tabbable@6.2.0: {} tapable@2.2.1: {} From bbf4355d6c7868e82177684cc6a0616c540db852 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Mon, 7 Jul 2025 19:00:15 +0900 Subject: [PATCH 02/22] remove --- design/6.completion-plugin.md | 264 ---------------------------------- 1 file changed, 264 deletions(-) delete mode 100644 design/6.completion-plugin.md diff --git a/design/6.completion-plugin.md b/design/6.completion-plugin.md deleted file mode 100644 index 86752fcd5..000000000 --- a/design/6.completion-plugin.md +++ /dev/null @@ -1,264 +0,0 @@ -# @bombshell-dev/tabを使用したplugin-completionの設計(修正版) - -PluginContextのaddCommandを活用して、`complete`サブコマンドベースの補完サーバーを実装します。 - -## 1. プラグイン構造 - -```typescript -// packages/plugin-completion/src/index.ts -import { plugin, type PluginWithExtension } from '@gunshi/plugin' -import { Completion, script } from '@bombshell-dev/tab' -import type { Command, LazyCommand } from 'gunshi' - -export interface CompletionCommandContext { - // 補完インスタンスへのアクセス - completion: Completion - // カスタム補完ハンドラの登録 - registerHandler(commandPath: string[], handler: CompletionHandler): void -} - -type CompletionHandler = (args: { - previousArgs: string[] - toComplete: string - endsWithSpace: boolean -}) => Promise> -``` - -## 2. プラグイン実装 - -```typescript -export default function completion(): PluginWithExtension { - const completion = new Completion() - const customHandlers = new Map() - - return plugin({ - id: 'g:completion', - name: 'completion', - - setup(ctx) { - // 1. completeサブコマンドを追加(補完サーバー) - ctx.addCommand('complete', { - name: 'complete', - description: 'Handle shell completions (internal use)', - run: async cmdCtx => { - // "--"以降の引数を取得 - const argsToComplete = cmdCtx.rest - - // @bombshell-dev/tabで補完を解析 - const results = await completion.parse(argsToComplete) - - // 結果を出力(シェルが読み取る形式) - results.items.forEach(item => { - console.log(item.value) - if (item.description) { - console.log(`:${item.description}`) - } - }) - - // ディレクティブを出力 - console.log(`\n${results.directive}`) - } - }) - - // 2. completionサブコマンドを追加(スクリプト生成) - ctx.addCommand('completion', { - name: 'completion', - description: 'Generate shell completion script', - args: { - shell: { - type: 'string', - description: 'Target shell (bash|zsh|fish)', - default: detectShell() - } - }, - run: async cmdCtx => { - const shell = cmdCtx.values.shell as string - const execName = cmdCtx.env.name || 'cli' - const execPath = process.argv[1] - - // シェルスクリプトを生成 - const scripts = script(execName, execPath) - - if (!(shell in scripts)) { - throw new Error(`Unsupported shell: ${shell}`) - } - - cmdCtx.log(scripts[shell as keyof typeof scripts]) - } - }) - }, - - extension(ctx) { - // 3. gunshiのコマンド構造を@bombshell-dev/tabに変換 - setupCompletions(completion, ctx.pluginContext, customHandlers) - - return { - completion, - registerHandler: (commandPath, handler) => { - const key = commandPath.join(':') - customHandlers.set(key, handler) - } - } - } - }) -} -``` - -## 3. コマンド構造の変換 - -```typescript -function setupCompletions( - completion: Completion, - pluginContext: PluginContext, - customHandlers: Map -) { - const subCommands = pluginContext.subCommands - - // ルートレベルの補完 - completion.addCommand('', 'Root command', async ({ toComplete, previousArgs }) => { - // completeコマンド自体は補完候補から除外 - if (previousArgs.length === 0) { - const commands = Array.from(subCommands.keys()) - .filter(cmd => cmd !== 'complete' && cmd !== 'completion') - .filter(cmd => cmd.startsWith(toComplete)) - .map(cmd => ({ - value: cmd, - description: subCommands.get(cmd)?.description - })) - - return commands - } - - return [] - }) - - // 各サブコマンドの補完を設定 - for (const [name, command] of subCommands) { - if (name === 'complete' || name === 'completion') continue - - setupCommandCompletion(completion, name, command, customHandlers) - } - - // グローバルオプションの補完 - for (const [name, schema] of pluginContext.globalOptions) { - completion.addOption('', `--${name}`, schema.description || '', async () => { - return getValueCompletions(schema) - }) - } -} - -function setupCommandCompletion( - completion: Completion, - name: string, - command: Command | LazyCommand, - customHandlers: Map -) { - completion.addCommand(name, command.description || '', async args => { - // カスタムハンドラがあれば使用 - const handler = customHandlers.get(name) - if (handler) { - return handler(args) - } - - // デフォルトはオプションの補完 - return [] - }) - - // コマンドのオプションを補完に追加 - if ('args' in command && command.args) { - for (const [argName, argSchema] of Object.entries(command.args)) { - completion.addOption(name, `--${argName}`, argSchema.description || '', async () => { - return getValueCompletions(argSchema) - }) - } - } -} - -function getValueCompletions(schema: any) { - if (schema.type === 'boolean') { - return [ - { value: 'true', description: 'Enable' }, - { value: 'false', description: 'Disable' } - ] - } - - if (schema.enum) { - return schema.enum.map((value: string) => ({ value })) - } - - return [] -} -``` - -## 4. 使用例 - -```typescript -// ユーザーのCLIアプリケーション -import { cli } from 'gunshi' -import completion from '@gunshi/plugin-completion' - -const comp = completion() - -await cli(process.argv.slice(2), { - name: 'my-cli', - description: 'My CLI tool', - plugins: [comp], - subCommands: new Map([ - [ - 'build', - { - description: 'Build the project', - args: { - target: { - type: 'string', - enum: ['dev', 'prod'], - description: 'Build target' - } - }, - run: async ctx => { - // カスタム補完を登録 - if (ctx.extensions['g:completion']) { - ctx.extensions['g:completion'].registerHandler(['build'], async ({ toComplete }) => { - // プロジェクトファイルから動的に補完候補を生成 - const projects = await getProjects() - return projects - .filter(p => p.startsWith(toComplete)) - .map(p => ({ value: p, description: 'Project' })) - }) - } - } - } - ] - ]) -}) -``` - -## 5. シェルセットアップ - -```bash -# 1. 補完スクリプトを生成 -$ my-cli completion zsh > ~/.my-cli-completion.zsh - -# 2. シェル設定に追加 -$ echo 'source ~/.my-cli-completion.zsh' >> ~/.zshrc - -# 3. 補完が動作 -$ my-cli bu[TAB] -build -- Build the project - -$ my-cli build --target [TAB] -dev -- Development -prod -- Production -``` - -## 6. 内部動作フロー - -1. ユーザーがTabキーを押す -2. シェルが`my-cli complete -- build --ta`を実行 -3. gunshiが`complete`サブコマンドを実行 -4. @bombshell-dev/tabのCompletionが引数を解析 -5. 登録されたハンドラから補完候補を生成 -6. 結果を標準出力に出力 -7. シェルが補完候補を表示 - -この設計により、環境変数に依存せず、クリーンなサブコマンドベースの補完サーバーを実装できます。 From 91e168cff2e8eb96268cfa9026245586bb21fedb Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Mon, 7 Jul 2025 19:09:21 +0900 Subject: [PATCH 03/22] fix: tweak completion options --- packages/plugin-completion/src/index.ts | 9 ++++++--- packages/plugin-completion/src/types.ts | 22 ++++++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/plugin-completion/src/index.ts b/packages/plugin-completion/src/index.ts index 9a3294df0..7a751864a 100644 --- a/packages/plugin-completion/src/index.ts +++ b/packages/plugin-completion/src/index.ts @@ -16,7 +16,7 @@ import type { PluginContext, PluginWithoutExtension } from '@gunshi/plugin' -import type { CompletionCommandContext, CompletionConfig } from './types.ts' +import type { CompletionCommandContext, CompletionOptions } from './types.ts' export * from './types.ts' @@ -28,8 +28,9 @@ const NOOP_HANDLER: Handler = () => { * completion plugin for gunshi */ export default function completion( - options: CompletionConfig = {} + options: CompletionOptions = {} ): PluginWithoutExtension { + const config = options.config || {} const completion = new Completion() return plugin({ @@ -110,7 +111,7 @@ export default function completion( ) } - handleSubCommands(completion, ctx.subCommands, options.subCommands) + handleSubCommands(completion, ctx.subCommands, config.subCommands) }, extension: async (_ctx, _cmd) => {} @@ -161,6 +162,8 @@ function quoteExec(): string { } } +type CompletionConfig = NonNullable + async function handleSubCommands( completion: Completion, subCommands: PluginContext['subCommands'], diff --git a/packages/plugin-completion/src/types.ts b/packages/plugin-completion/src/types.ts index ebc46935c..d74c42a3d 100644 --- a/packages/plugin-completion/src/types.ts +++ b/packages/plugin-completion/src/types.ts @@ -26,15 +26,17 @@ export type PluginId = typeof pluginId export interface CompletionCommandContext {} /** - * Configuration for the completion plugin. + * Completion plugin options. */ -export interface CompletionConfig { - /** - * The entry point handler. - */ - entry?: Handler - /** - * The handlers for subcommands. - */ - subCommands?: Record +export interface CompletionOptions { + config?: { + /** + * The entry point handler. + */ + entry?: Handler + /** + * The handlers for subcommands. + */ + subCommands?: Record + } } From d94fcfd3d970b84b9dc0fac0d4bdb8b5d2b61fd7 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Tue, 8 Jul 2025 18:32:23 +0900 Subject: [PATCH 04/22] fix: first testing --- eslint.config.ts | 1 + .../plugin-completion/examples/demo.node.ts | 73 ++++++ .../src/__snapshots__/index.test.ts.snap | 233 ++++++++++++++++++ packages/plugin-completion/src/index.test.ts | 28 +++ packages/plugin-completion/src/index.ts | 48 ++-- pnpm-lock.yaml | 3 + 6 files changed, 369 insertions(+), 17 deletions(-) create mode 100644 packages/plugin-completion/examples/demo.node.ts create mode 100644 packages/plugin-completion/src/__snapshots__/index.test.ts.snap create mode 100644 packages/plugin-completion/src/index.test.ts diff --git a/eslint.config.ts b/eslint.config.ts index b3486f3bc..29c514fd4 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -33,6 +33,7 @@ const config: ReturnType = defineConfig( './bench/**', './playground/**', './packages/docs/**', + './packages/**/examples/**', './**/test/**', './**/src/**/*.test.ts', './**/src/**/*.test-d.ts' diff --git a/packages/plugin-completion/examples/demo.node.ts b/packages/plugin-completion/examples/demo.node.ts new file mode 100644 index 000000000..8a6ab1380 --- /dev/null +++ b/packages/plugin-completion/examples/demo.node.ts @@ -0,0 +1,73 @@ +import { cli, define } from 'gunshi' +import completion from '../src/index.ts' + +const entry = define({ + // name: 'root', + args: { + config: { + type: 'string', + description: 'Use specified config file', + short: 'c' + }, + mode: { + type: 'string', + description: 'Set env mode', + short: 'm' + }, + logLevel: { + type: 'string', + description: 'info | warn | error | silent', + short: 'l' + } + }, + run: _ctx => {} +}) + +const dev = define({ + name: 'dev', + description: 'Start dev server', + args: { + host: { + type: 'string', + description: 'Specify hostname', + short: 'H' + }, + port: { + type: 'string', + description: 'Specify port', + short: 'p' + } + }, + run: () => {} +}) + +const build = define({ + name: 'build', + description: 'Build project', + run: () => {} +}) + +const lint = define({ + name: 'lint', + description: 'Lint project', + args: { + files: { + type: 'positional', + description: 'Files to lint' + } + }, + run: () => {} +}) + +const subCommands = new Map>() +subCommands.set('dev', dev) +subCommands.set('build', build) +subCommands.set('lint', lint) + +await cli(process.argv.slice(2), entry, { + name: 'vite', + version: '0.0.0', + description: 'Vite CLI', + subCommands, + plugins: [completion()] +}) diff --git a/packages/plugin-completion/src/__snapshots__/index.test.ts.snap b/packages/plugin-completion/src/__snapshots__/index.test.ts.snap new file mode 100644 index 000000000..6155b8b0b --- /dev/null +++ b/packages/plugin-completion/src/__snapshots__/index.test.ts.snap @@ -0,0 +1,233 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`shell option 1`] = ` +"#compdef vite +compdef _vite vite + +# zsh completion for vite -*- shell-script -*- + +__vite_debug() { + local file="$BASH_COMP_DEBUG_FILE" + if [[ -n \${file} ]]; then + echo "$*" >> "\${file}" + fi +} + +_vite() { + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + local shellCompDirectiveKeepOrder=32 + + local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder + local -a completions + + __vite_debug "\\n========= starting completion logic ==========" + __vite_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]}" ) + __vite_debug "Truncated words[*]: \${words[*]}," + + lastParam=\${words[-1]} + lastChar=\${lastParam[-1]} + __vite_debug "lastParam: \${lastParam}, lastChar: \${lastChar}" + + # For zsh, when completing a flag with an = (e.g., vite -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, ensuring arguments are quoted for eval + local -a args_to_quote=("\${(@)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. + __vite_debug "Adding extra empty parameter" + args_to_quote+=("") + fi + + # Use Zsh's (q) flag to quote each argument safely for eval + local quoted_args=("\${(@q)args_to_quote}") + + # Join the main command and the quoted arguments into a single string for eval + requestComp="/Users/kazuya.kawaguchi/.nvm/versions/node/v24.1.0/bin/node --require /Users/kazuya.kawaguchi/Library/Caches/pnpm/dlx/63065c1d2ea4575c940b7fbae672cb49cb081ceb23893d56e604d77d95c0d918/197e8eb9974-3a3f/node_modules/.pnpm/tsx@4.20.3/node_modules/tsx/dist/preflight.cjs --import file:///Users/kazuya.kawaguchi/Library/Caches/pnpm/dlx/63065c1d2ea4575c940b7fbae672cb49cb081ceb23893d56e604d77d95c0d918/197e8eb9974-3a3f/node_modules/.pnpm/tsx@4.20.3/node_modules/tsx/dist/loader.mjs /Users/kazuya.kawaguchi/Projects/my/gunshi/packages/plugin-completion/examples/demo.node.ts complete -- \${quoted_args[*]}" + + __vite_debug "About to call: eval \${requestComp}" + + # Use eval to handle any environment variables and such + out=$(eval \${requestComp} 2>/dev/null) + __vite_debug "completion output: \${out}" + + # Extract the directive integer following a : from the last line + local lastLine + while IFS=' +' read -r line; do + lastLine=\${line} + done < <(printf "%s +" "\${out[@]}") + __vite_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. + __vite_debug "No directive found. Setting to default" + directive=0 + fi + + __vite_debug "directive: \${directive}" + __vite_debug "completions: \${out}" + __vite_debug "flagPrefix: \${flagPrefix}" + + if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then + __vite_debug "Completion received error. Ignoring completions." + return + fi + + local activeHelpMarker="%" + local endIndex=\${#activeHelpMarker} + local startIndex=$((\${#activeHelpMarker}+1)) + local hasActiveHelp=0 + while IFS=' +' read -r comp; do + # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) + if [ "\${comp[1,$endIndex]}" = "$activeHelpMarker" ];then + __vite_debug "ActiveHelp found: $comp" + comp="\${comp[$startIndex,-1]}" + if [ -n "$comp" ]; then + compadd -x "\${comp}" + __vite_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/:} + + __vite_debug "Adding completion: \${comp}" + completions+=\${comp} + lastComp=$comp + fi + done < <(printf "%s +" "\${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 + __vite_debug "Adding activeHelp delimiter" + compadd -x "--" + hasActiveHelp=0 + fi + fi + + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then + __vite_debug "Activating nospace." + noSpace="-S ''" + fi + + if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then + __vite_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}" + + __vite_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 + __vite_debug "Listing directories in $subdir" + pushd "\${subdir}" >/dev/null 2>&1 + else + __vite_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 + __vite_debug "Calling _describe" + if eval _describe $keepOrder "completions" completions -Q \${flagPrefix} \${noSpace}; then + __vite_debug "_describe found some completions" + + # Return the success of having called _describe + return 0 + else + __vite_debug "_describe did not find completions." + __vite_debug "Checking if we should do file completion." + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + __vite_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 + __vite_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]}" = "_vite" ]; then + _vite +fi + +" +`; + +exports[`termination only 1`] = ` +"dev Start dev server +build Build project +lint Lint project +:4 +" +`; diff --git a/packages/plugin-completion/src/index.test.ts b/packages/plugin-completion/src/index.test.ts new file mode 100644 index 000000000..76c94c208 --- /dev/null +++ b/packages/plugin-completion/src/index.test.ts @@ -0,0 +1,28 @@ +import { exec } from 'node:child_process' +import { expect, test } from 'vitest' + +function runCommand(command: string): Promise { + return new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + reject(stderr) + } else { + resolve(stdout) + } + }) + }) +} + +test('shell option', async () => { + const output = await runCommand( + `pnpx tsx packages/plugin-completion/examples/demo.node.ts complete zsh` + ) + expect(output).toMatchSnapshot() +}) + +test('termination only', async () => { + const output = await runCommand( + `pnpx tsx packages/plugin-completion/examples/demo.node.ts complete --` + ) + expect(output).toMatchSnapshot() +}) diff --git a/packages/plugin-completion/src/index.ts b/packages/plugin-completion/src/index.ts index 7a751864a..a6eb60061 100644 --- a/packages/plugin-completion/src/index.ts +++ b/packages/plugin-completion/src/index.ts @@ -20,6 +20,8 @@ import type { CompletionCommandContext, CompletionOptions } from './types.ts' export * from './types.ts' +const TERMINATOR = '--' + const NOOP_HANDLER: Handler = () => { return [] } @@ -47,26 +49,28 @@ export default function completion( name: completeName, // TODO(kazupon): support description localization description: 'Generate shell completion script', - args: { - shell: { - type: 'positional', - // TODO(kazupon): support description localization - description: - 'Shell type to generate completion script (zsh, bash, fish, powershell, fig)' - } - }, + // args: { + // shell: { + // type: 'string', + // // @ts-ignore -- TOOD: `args-tokens` will be updated to support `shell` type + // required: false, + // // TODO(kazupon): support description localization + // description: + // 'shell type to generate completion script (zsh, bash, fish, fig, powershell)', + // } + // }, run: async cmdCtx => { if (!cmdCtx.env.name) { throw new Error('cli name is not defined.') } - let shell: string | undefined = cmdCtx._[0] - if (shell === '--') { + let shell: string | undefined = cmdCtx._[1] + if (shell === TERMINATOR) { shell = undefined } if (shell === undefined) { - const extra = cmdCtx._.slice(cmdCtx._.indexOf('--') + 1) + const extra = cmdCtx._.slice(cmdCtx._.indexOf(TERMINATOR) + 1) completion.parse(extra) } else { script(shell as Parameters[0], cmdCtx.env.name, quoteExec()) @@ -74,6 +78,12 @@ export default function completion( } }) + /** + * disable header renderer + */ + + ctx.decorateHeaderRenderer(async (_baseRenderer, _cmdCtx) => '') + /** * setup bombshell completion */ @@ -86,7 +96,7 @@ export default function completion( } // setup root level completion - const isPositional = hasPositional(await resolveLazyCommand(entry)) + const isPositional = hasPositional(await resolveLazyCommand(entry as Command)) const root = '' // TODO(kazupon): more tweaking for root completion completion.addCommand( @@ -119,6 +129,7 @@ export default function completion( } function detectRuntime(): 'bun' | 'deno' | 'node' | 'unknown' { + // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` if (globalThis.process !== undefined && globalThis.process.release?.name === 'node') { return 'node' } @@ -141,13 +152,16 @@ function quoteExec(): string { const runtime = detectRuntime() switch (runtime) { case 'node': { - const execPath = process.execPath - const processArgs = process.argv.slice(1) + // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` + const execPath = globalThis.process.execPath + // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` + const processArgs = globalThis.process.argv.slice(1) const quotedExecPath = quoteIfNeeded(execPath) // eslint-disable-next-line unicorn/no-array-callback-reference const quotedProcessArgs = processArgs.map(quoteIfNeeded) + // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` // eslint-disable-next-line unicorn/no-array-callback-reference - const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded) + const quotedProcessExecArgs = globalThis.process.execArgv.map(quoteIfNeeded) return `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}` } case 'deno': { @@ -170,8 +184,8 @@ async function handleSubCommands( configs: CompletionConfig['subCommands'] = {} ) { for (const [name, cmd] of subCommands) { - if (cmd.internal || cmd.entry) { - continue // skip entry or internal command + if (cmd.internal || cmd.entry || name === 'complete') { + continue // skip entry / internal command / completion command itself } const resolvedCmd = await resolveLazyCommand(cmd) if (!resolvedCmd.description) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc2499bfa..f5ab9b346 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,6 +289,9 @@ importers: deno: specifier: 'catalog:' version: 2.4.0 + gunshi: + specifier: workspace:* + version: link:../gunshi jsr: specifier: 'catalog:' version: 0.13.5 From 75b8dffc9c696100ed713aba3e4698eaa62cb053 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Wed, 9 Jul 2025 01:52:09 +0900 Subject: [PATCH 05/22] test: add more test cases --- packages/gunshi/src/cli/core.ts | 5 +- .../src/__snapshots__/index.test.ts.snap | 301 +++++------------- packages/plugin-completion/src/index.test.ts | 34 +- 3 files changed, 112 insertions(+), 228 deletions(-) diff --git a/packages/gunshi/src/cli/core.ts b/packages/gunshi/src/cli/core.ts index 603b5d20d..7edb0ac13 100644 --- a/packages/gunshi/src/cli/core.ts +++ b/packages/gunshi/src/cli/core.ts @@ -142,9 +142,10 @@ function createInitialSubCommands( entryCmd.entry = true subCommands.set(entryCmd.commandName, entryCmd as LazyCommand) } - } else if (typeof entryCmd === 'object' && entryCmd.name) { + // } else if (typeof entryCmd === 'object' && entryCmd.name) { + } else if (typeof entryCmd === 'object') { entryCmd.entry = true - subCommands.set(entryCmd.name, entryCmd as Command) + subCommands.set(entryCmd.name || '', entryCmd as Command) } } diff --git a/packages/plugin-completion/src/__snapshots__/index.test.ts.snap b/packages/plugin-completion/src/__snapshots__/index.test.ts.snap index 6155b8b0b..7d29ea274 100644 --- a/packages/plugin-completion/src/__snapshots__/index.test.ts.snap +++ b/packages/plugin-completion/src/__snapshots__/index.test.ts.snap @@ -1,230 +1,95 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`shell option 1`] = ` -"#compdef vite -compdef _vite vite - -# zsh completion for vite -*- shell-script -*- - -__vite_debug() { - local file="$BASH_COMP_DEBUG_FILE" - if [[ -n \${file} ]]; then - echo "$*" >> "\${file}" - fi -} - -_vite() { - local shellCompDirectiveError=1 - local shellCompDirectiveNoSpace=2 - local shellCompDirectiveNoFileComp=4 - local shellCompDirectiveFilterFileExt=8 - local shellCompDirectiveFilterDirs=16 - local shellCompDirectiveKeepOrder=32 - - local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder - local -a completions - - __vite_debug "\\n========= starting completion logic ==========" - __vite_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]}" ) - __vite_debug "Truncated words[*]: \${words[*]}," - - lastParam=\${words[-1]} - lastChar=\${lastParam[-1]} - __vite_debug "lastParam: \${lastParam}, lastChar: \${lastChar}" - - # For zsh, when completing a flag with an = (e.g., vite -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, ensuring arguments are quoted for eval - local -a args_to_quote=("\${(@)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. - __vite_debug "Adding extra empty parameter" - args_to_quote+=("") - fi - - # Use Zsh's (q) flag to quote each argument safely for eval - local quoted_args=("\${(@q)args_to_quote}") - - # Join the main command and the quoted arguments into a single string for eval - requestComp="/Users/kazuya.kawaguchi/.nvm/versions/node/v24.1.0/bin/node --require /Users/kazuya.kawaguchi/Library/Caches/pnpm/dlx/63065c1d2ea4575c940b7fbae672cb49cb081ceb23893d56e604d77d95c0d918/197e8eb9974-3a3f/node_modules/.pnpm/tsx@4.20.3/node_modules/tsx/dist/preflight.cjs --import file:///Users/kazuya.kawaguchi/Library/Caches/pnpm/dlx/63065c1d2ea4575c940b7fbae672cb49cb081ceb23893d56e604d77d95c0d918/197e8eb9974-3a3f/node_modules/.pnpm/tsx@4.20.3/node_modules/tsx/dist/loader.mjs /Users/kazuya.kawaguchi/Projects/my/gunshi/packages/plugin-completion/examples/demo.node.ts complete -- \${quoted_args[*]}" - - __vite_debug "About to call: eval \${requestComp}" - - # Use eval to handle any environment variables and such - out=$(eval \${requestComp} 2>/dev/null) - __vite_debug "completion output: \${out}" - - # Extract the directive integer following a : from the last line - local lastLine - while IFS=' -' read -r line; do - lastLine=\${line} - done < <(printf "%s -" "\${out[@]}") - __vite_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. - __vite_debug "No directive found. Setting to default" - directive=0 - fi - - __vite_debug "directive: \${directive}" - __vite_debug "completions: \${out}" - __vite_debug "flagPrefix: \${flagPrefix}" - - if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then - __vite_debug "Completion received error. Ignoring completions." - return - fi - - local activeHelpMarker="%" - local endIndex=\${#activeHelpMarker} - local startIndex=$((\${#activeHelpMarker}+1)) - local hasActiveHelp=0 - while IFS=' -' read -r comp; do - # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) - if [ "\${comp[1,$endIndex]}" = "$activeHelpMarker" ];then - __vite_debug "ActiveHelp found: $comp" - comp="\${comp[$startIndex,-1]}" - if [ -n "$comp" ]; then - compadd -x "\${comp}" - __vite_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/:} - - __vite_debug "Adding completion: \${comp}" - completions+=\${comp} - lastComp=$comp - fi - done < <(printf "%s -" "\${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 - __vite_debug "Adding activeHelp delimiter" - compadd -x "--" - hasActiveHelp=0 - fi - fi - - if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then - __vite_debug "Activating nospace." - noSpace="-S ''" - fi - - if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then - __vite_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}" - - __vite_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 - __vite_debug "Listing directories in $subdir" - pushd "\${subdir}" >/dev/null 2>&1 - else - __vite_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 - __vite_debug "Calling _describe" - if eval _describe $keepOrder "completions" completions -Q \${flagPrefix} \${noSpace}; then - __vite_debug "_describe found some completions" - - # Return the success of having called _describe - return 0 - else - __vite_debug "_describe did not find completions." - __vite_debug "Checking if we should do file completion." - if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then - __vite_debug "deactivating file completion" +exports[`subcommand only 1`] = ` +"dev Start dev server +:4 +" +`; - # 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 - __vite_debug "Activating file completion" +exports[`subcommand with long option 1`] = ` +"--port Specify port +:4 +" +`; - # 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 +exports[`subcommand with long option and value 1`] = ` +"completion command context [Object: null prototype] { + name: 'complete', + description: 'Generate shell completion script', + omitted: false, + callMode: 'subCommand', + env: [Object: null prototype] { + name: 'vite', + description: 'Vite CLI', + version: '0.0.0', + cwd: undefined, + usageSilent: false, + subCommands: Map(5) { + 'dev' => [Object], + 'build' => [Object], + 'lint' => [Object], + '' => [Object], + 'complete' => [Object] + }, + leftMargin: 2, + middleMargin: 10, + usageOptionType: false, + usageOptionValue: true, + renderHeader: [Function: renderer], + renderUsage: [Function: renderer], + renderValidationErrors: [Function: renderer], + plugins: [ [AsyncFunction] ] + }, + args: [Object: null prototype] { + help: [Object: null prototype] { + type: 'boolean', + short: 'h', + description: 'Display this help message' + }, + version: [Object: null prototype] { + type: 'boolean', + short: 'v', + description: 'Display this version' + } + }, + values: [Object: null prototype] {}, + positionals: [ 'complete' ], + rest: [ 'dev', '--port=3' ], + _: [ 'complete', '--', 'dev', '--port=3' ], + tokens: [ + { kind: 'positional', index: 0, value: 'complete' }, + { kind: 'option-terminator', index: 1 }, + { kind: 'positional', index: 2, value: 'dev' }, + { kind: 'positional', index: 3, value: '--port=3' } + ], + toKebab: undefined, + log: [Function: log], + validationError: undefined, + extensions: [Object: null prototype] { + 'g:global': { + showVersion: [Function: showVersion], + showHeader: [AsyncFunction: showHeader], + showUsage: [AsyncFunction: showUsage], + showValidationErrors: [AsyncFunction: showValidationErrors] + }, + 'g:renderer': { + text: [AsyncFunction: text], + loadCommands: [AsyncFunction: loadCommands] + }, + 'g:completion': undefined + } } +:4 +" +`; -# don't run the completion function when being sourced or eval-ed -if [ "\${funcstack[1]}" = "_vite" ]; then - _vite -fi - +exports[`subcommand with short option 1`] = ` +"-H Specify hostname +:4 " `; -exports[`termination only 1`] = ` +exports[`termination 1`] = ` "dev Start dev server build Build project lint Lint project diff --git a/packages/plugin-completion/src/index.test.ts b/packages/plugin-completion/src/index.test.ts index 76c94c208..08288e73d 100644 --- a/packages/plugin-completion/src/index.test.ts +++ b/packages/plugin-completion/src/index.test.ts @@ -13,16 +13,34 @@ function runCommand(command: string): Promise { }) } -test('shell option', async () => { - const output = await runCommand( - `pnpx tsx packages/plugin-completion/examples/demo.node.ts complete zsh` - ) +const SCRIPT = `pnpx tsx packages/plugin-completion/examples/demo.node.ts complete` + +test('termination', async () => { + const output = await runCommand(`${SCRIPT} --`) + expect(output).toMatchSnapshot() +}) + +test.todo('default command option', async () => { + const output = await runCommand(`${SCRIPT} --config --`) + expect(output).toMatchSnapshot() +}) + +test('subcommand only', async () => { + const output = await runCommand(`${SCRIPT} -- dev`) + expect(output).toMatchSnapshot() +}) + +test('subcommand with long option', async () => { + const output = await runCommand(`${SCRIPT} -- dev --port`) + expect(output).toMatchSnapshot() +}) + +test('subcommand with short option', async () => { + const output = await runCommand(`${SCRIPT} -- dev -H`) expect(output).toMatchSnapshot() }) -test('termination only', async () => { - const output = await runCommand( - `pnpx tsx packages/plugin-completion/examples/demo.node.ts complete --` - ) +test.todo('subcommand with long option and value', async () => { + const output = await runCommand(`${SCRIPT} -- dev --port=3`) expect(output).toMatchSnapshot() }) From b52d184a3c0af93541c8d8b37b10e1ca0e39845a Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Wed, 9 Jul 2025 02:33:39 +0900 Subject: [PATCH 06/22] test: add more cases --- .../plugin-completion/examples/demo.node.ts | 2 +- .../src/__snapshots__/index.test.ts.snap | 110 ++++++------------ packages/plugin-completion/src/index.test.ts | 37 ++++-- packages/plugin-completion/src/index.ts | 2 +- 4 files changed, 63 insertions(+), 88 deletions(-) diff --git a/packages/plugin-completion/examples/demo.node.ts b/packages/plugin-completion/examples/demo.node.ts index 8a6ab1380..793cb04d5 100644 --- a/packages/plugin-completion/examples/demo.node.ts +++ b/packages/plugin-completion/examples/demo.node.ts @@ -33,7 +33,7 @@ const dev = define({ short: 'H' }, port: { - type: 'string', + type: 'number', description: 'Specify port', short: 'p' } diff --git a/packages/plugin-completion/src/__snapshots__/index.test.ts.snap b/packages/plugin-completion/src/__snapshots__/index.test.ts.snap index 7d29ea274..5e93506db 100644 --- a/packages/plugin-completion/src/__snapshots__/index.test.ts.snap +++ b/packages/plugin-completion/src/__snapshots__/index.test.ts.snap @@ -1,98 +1,58 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`subcommand only 1`] = ` +exports[`default command inputing 1`] = ` +"--config Use specified config file +--mode Set env mode +--logLevel info | warn | error | silent +:4 +" +`; + +exports[`default command long option 1`] = ` +"--config Use specified config file +:4 +" +`; + +exports[`global option 1`] = ` +":4 +" +`; + +exports[`no input 1`] = ` "dev Start dev server +build Build project +lint Lint project :4 " `; -exports[`subcommand with long option 1`] = ` +exports[`subcommand long option 1`] = ` "--port Specify port :4 " `; -exports[`subcommand with long option and value 1`] = ` -"completion command context [Object: null prototype] { - name: 'complete', - description: 'Generate shell completion script', - omitted: false, - callMode: 'subCommand', - env: [Object: null prototype] { - name: 'vite', - description: 'Vite CLI', - version: '0.0.0', - cwd: undefined, - usageSilent: false, - subCommands: Map(5) { - 'dev' => [Object], - 'build' => [Object], - 'lint' => [Object], - '' => [Object], - 'complete' => [Object] - }, - leftMargin: 2, - middleMargin: 10, - usageOptionType: false, - usageOptionValue: true, - renderHeader: [Function: renderer], - renderUsage: [Function: renderer], - renderValidationErrors: [Function: renderer], - plugins: [ [AsyncFunction] ] - }, - args: [Object: null prototype] { - help: [Object: null prototype] { - type: 'boolean', - short: 'h', - description: 'Display this help message' - }, - version: [Object: null prototype] { - type: 'boolean', - short: 'v', - description: 'Display this version' - } - }, - values: [Object: null prototype] {}, - positionals: [ 'complete' ], - rest: [ 'dev', '--port=3' ], - _: [ 'complete', '--', 'dev', '--port=3' ], - tokens: [ - { kind: 'positional', index: 0, value: 'complete' }, - { kind: 'option-terminator', index: 1 }, - { kind: 'positional', index: 2, value: 'dev' }, - { kind: 'positional', index: 3, value: '--port=3' } - ], - toKebab: undefined, - log: [Function: log], - validationError: undefined, - extensions: [Object: null prototype] { - 'g:global': { - showVersion: [Function: showVersion], - showHeader: [AsyncFunction: showHeader], - showUsage: [AsyncFunction: showUsage], - showValidationErrors: [AsyncFunction: showValidationErrors] - }, - 'g:renderer': { - text: [AsyncFunction: text], - loadCommands: [AsyncFunction: loadCommands] - }, - 'g:completion': undefined - } -} +exports[`subcommand only 1`] = ` +"dev Start dev server :4 " `; -exports[`subcommand with short option 1`] = ` -"-H Specify hostname +exports[`subcommand option inputing 1`] = ` +"--host Specify hostname +--port Specify port :4 " `; -exports[`termination 1`] = ` -"dev Start dev server -build Build project -lint Lint project +exports[`subcommand short option 1`] = ` +"-H Specify hostname :4 " `; + +exports[`subcommand unknown option 1`] = ` +":4 +" +`; diff --git a/packages/plugin-completion/src/index.test.ts b/packages/plugin-completion/src/index.test.ts index 08288e73d..6bec12dcc 100644 --- a/packages/plugin-completion/src/index.test.ts +++ b/packages/plugin-completion/src/index.test.ts @@ -13,34 +13,49 @@ function runCommand(command: string): Promise { }) } -const SCRIPT = `pnpx tsx packages/plugin-completion/examples/demo.node.ts complete` +const SCRIPT = `pnpx tsx packages/plugin-completion/examples/demo.node.ts complete --` -test('termination', async () => { +test('no input', async () => { + const output = await runCommand(`${SCRIPT}`) + expect(output).toMatchSnapshot() +}) + +test('default command inputing', async () => { const output = await runCommand(`${SCRIPT} --`) expect(output).toMatchSnapshot() }) -test.todo('default command option', async () => { - const output = await runCommand(`${SCRIPT} --config --`) +test('default command long option', async () => { + const output = await runCommand(`${SCRIPT} --config`) expect(output).toMatchSnapshot() }) test('subcommand only', async () => { - const output = await runCommand(`${SCRIPT} -- dev`) + const output = await runCommand(`${SCRIPT} dev`) + expect(output).toMatchSnapshot() +}) + +test('subcommand option inputing', async () => { + const output = await runCommand(`${SCRIPT} dev --`) + expect(output).toMatchSnapshot() +}) + +test('subcommand long option', async () => { + const output = await runCommand(`${SCRIPT} dev --port`) expect(output).toMatchSnapshot() }) -test('subcommand with long option', async () => { - const output = await runCommand(`${SCRIPT} -- dev --port`) +test('subcommand short option', async () => { + const output = await runCommand(`${SCRIPT} dev -H`) expect(output).toMatchSnapshot() }) -test('subcommand with short option', async () => { - const output = await runCommand(`${SCRIPT} -- dev -H`) +test('subcommand unknown option', async () => { + const output = await runCommand(`${SCRIPT} dev --unknown`) expect(output).toMatchSnapshot() }) -test.todo('subcommand with long option and value', async () => { - const output = await runCommand(`${SCRIPT} -- dev --port=3`) +test.todo('subcommand long option and value', async () => { + const output = await runCommand(`${SCRIPT} dev --port=3`) expect(output).toMatchSnapshot() }) diff --git a/packages/plugin-completion/src/index.ts b/packages/plugin-completion/src/index.ts index a6eb60061..f4d390536 100644 --- a/packages/plugin-completion/src/index.ts +++ b/packages/plugin-completion/src/index.ts @@ -81,7 +81,7 @@ export default function completion( /** * disable header renderer */ - + // TODO(kazupon): we might be change this to a more flexible way ctx.decorateHeaderRenderer(async (_baseRenderer, _cmdCtx) => '') /** From 6bfe964657e37f65de593f7ce5f817892905f02c Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Wed, 9 Jul 2025 16:31:40 +0900 Subject: [PATCH 07/22] test: add test cases from bombsh/tab --- .../plugin-completion/examples/demo.node.ts | 55 +++++++- .../src/__snapshots__/index.test.ts.snap | 90 +++++++++++-- packages/plugin-completion/src/index.test.ts | 121 +++++++++++++----- packages/plugin-completion/src/index.ts | 21 ++- packages/plugin-completion/src/types.ts | 14 +- 5 files changed, 245 insertions(+), 56 deletions(-) diff --git a/packages/plugin-completion/examples/demo.node.ts b/packages/plugin-completion/examples/demo.node.ts index 793cb04d5..131b5cc2b 100644 --- a/packages/plugin-completion/examples/demo.node.ts +++ b/packages/plugin-completion/examples/demo.node.ts @@ -69,5 +69,58 @@ await cli(process.argv.slice(2), entry, { version: '0.0.0', description: 'Vite CLI', subCommands, - plugins: [completion()] + plugins: [ + completion({ + config: { + entry: { + args: { + config: { + handler: () => [ + { value: 'vite.config.ts', description: 'Vite config file' }, + { value: 'vite.config.js', description: 'Vite config file' } + ] + }, + mode: { + handler: () => [ + { value: 'development', description: 'Development mode' }, + { value: 'production', description: 'Production mode' } + ] + }, + logLevel: { + handler: () => [ + { value: 'info', description: 'Info level' }, + { value: 'warn', description: 'Warn level' }, + { value: 'error', description: 'Error level' }, + { value: 'silent', description: 'Silent level' } + ] + } + } + }, + subCommands: { + lint: { + handler: () => [ + { value: 'main.ts', description: 'Main file' }, + { value: 'index.ts', description: 'Index file' } + ] + }, + dev: { + args: { + port: { + handler: () => [ + { value: '3000', description: 'Development server port' }, + { value: '8080', description: 'Alternative port' } + ] + }, + host: { + handler: () => [ + { value: 'localhost', description: 'Localhost' }, + { value: '0.0.0.0', description: 'All interfaces' } + ] + } + } + } + } + } + }) + ] }) diff --git a/packages/plugin-completion/src/__snapshots__/index.test.ts.snap b/packages/plugin-completion/src/__snapshots__/index.test.ts.snap index 5e93506db..d91dcfc2d 100644 --- a/packages/plugin-completion/src/__snapshots__/index.test.ts.snap +++ b/packages/plugin-completion/src/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`default command inputing 1`] = ` +exports[`default command > suggest duplicate options 1`] = ` "--config Use specified config file --mode Set env mode --logLevel info | warn | error | silent @@ -8,14 +8,38 @@ exports[`default command inputing 1`] = ` " `; -exports[`default command long option 1`] = ` +exports[`default command > suggest duplicate options for short option 1`] = ` "--config Use specified config file +--mode Set env mode +--logLevel info | warn | error | silent :4 " `; -exports[`global option 1`] = ` -":4 +exports[`default command > suggest for inputing 1`] = ` +"--config Use specified config file +--mode Set env mode +--logLevel info | warn | error | silent +:4 +" +`; + +exports[`default command > suggest for long option 1`] = ` +"--config Use specified config file +:4 +" +`; + +exports[`default command > suggest for short option 1`] = ` +"-c Use specified config file +:4 +" +`; + +exports[`default command > suggest value if option values correctly 1`] = ` +"vite.config.ts Vite config file +vite.config.js Vite config file +:4 " `; @@ -27,32 +51,78 @@ lint Lint project " `; -exports[`subcommand long option 1`] = ` +exports[`positional arguments > multiple positional arguments when ending with part of the value 1`] = ` +"index.ts Index file +:4 +" +`; + +exports[`positional arguments > single positional argument when ending with space 1`] = ` +"main.ts Main file +index.ts Index file +:4 +" +`; + +exports[`positional arguments > suggest multiple positional arguments when ending with space 1`] = ` +"main.ts Main file +index.ts Index file +:4 +" +`; + +exports[`subcommand > keep suggesting the --port option if user typed partial but didn't end with space 1`] = ` "--port Specify port :4 " `; -exports[`subcommand only 1`] = ` +exports[`subcommand > not handle if unknown option 1`] = ` +":4 +" +`; + +exports[`subcommand > resolve value if long option and value 1`] = ` +"--port=3000 Development server port +:4 +" +`; + +exports[`subcommand > suggest for command only 1`] = ` "dev Start dev server :4 " `; -exports[`subcommand option inputing 1`] = ` +exports[`subcommand > suggest for long option 1`] = ` +"--port Specify port +:4 +" +`; + +exports[`subcommand > suggest for option inputing 1`] = ` "--host Specify hostname --port Specify port :4 " `; -exports[`subcommand short option 1`] = ` +exports[`subcommand > suggest for short option 1`] = ` "-H Specify hostname :4 " `; -exports[`subcommand unknown option 1`] = ` -":4 +exports[`subcommand > suggest if user ends with space after \`--port\` 1`] = ` +"3000 Development server port +8080 Alternative port +:4 +" +`; + +exports[`subcommand > user typed \`--port=\` and hasn't typed a space or value yet 1`] = ` +"--port=3000 Development server port +--port=8080 Alternative port +:4 " `; diff --git a/packages/plugin-completion/src/index.test.ts b/packages/plugin-completion/src/index.test.ts index 6bec12dcc..693d6421c 100644 --- a/packages/plugin-completion/src/index.test.ts +++ b/packages/plugin-completion/src/index.test.ts @@ -1,5 +1,5 @@ import { exec } from 'node:child_process' -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' function runCommand(command: string): Promise { return new Promise((resolve, reject) => { @@ -20,42 +20,103 @@ test('no input', async () => { expect(output).toMatchSnapshot() }) -test('default command inputing', async () => { - const output = await runCommand(`${SCRIPT} --`) - expect(output).toMatchSnapshot() -}) +describe('default command', () => { + test('suggest for inputing', async () => { + const output = await runCommand(`${SCRIPT} --`) + expect(output).toMatchSnapshot() + }) -test('default command long option', async () => { - const output = await runCommand(`${SCRIPT} --config`) - expect(output).toMatchSnapshot() -}) + test('suggest for long option', async () => { + const output = await runCommand(`${SCRIPT} --config`) + expect(output).toMatchSnapshot() + }) -test('subcommand only', async () => { - const output = await runCommand(`${SCRIPT} dev`) - expect(output).toMatchSnapshot() -}) + test('suggest duplicate options', async () => { + const output = await runCommand(`${SCRIPT} --config vite.config.js --`) + expect(output).toMatchSnapshot() + }) -test('subcommand option inputing', async () => { - const output = await runCommand(`${SCRIPT} dev --`) - expect(output).toMatchSnapshot() -}) + test('suggest value if option values correctly', async () => { + const output = await runCommand(`${SCRIPT} --config vite.config`) + expect(output).toMatchSnapshot() + }) -test('subcommand long option', async () => { - const output = await runCommand(`${SCRIPT} dev --port`) - expect(output).toMatchSnapshot() -}) + test('suggest for short option', async () => { + const output = await runCommand(`${SCRIPT} -c `) + expect(output).toMatchSnapshot() + }) -test('subcommand short option', async () => { - const output = await runCommand(`${SCRIPT} dev -H`) - expect(output).toMatchSnapshot() + test('suggest duplicate options for short option', async () => { + const output = await runCommand(`${SCRIPT} -c vite.config.js --`) + expect(output).toMatchSnapshot() + }) }) -test('subcommand unknown option', async () => { - const output = await runCommand(`${SCRIPT} dev --unknown`) - expect(output).toMatchSnapshot() +describe('subcommand', () => { + test('suggest for command only', async () => { + const output = await runCommand(`${SCRIPT} dev`) + expect(output).toMatchSnapshot() + }) + + test('suggest for option inputing', async () => { + const output = await runCommand(`${SCRIPT} dev --`) + expect(output).toMatchSnapshot() + }) + + test('suggest for long option', async () => { + const output = await runCommand(`${SCRIPT} dev --port`) + expect(output).toMatchSnapshot() + }) + + test('suggest for short option', async () => { + const output = await runCommand(`${SCRIPT} dev -H`) + expect(output).toMatchSnapshot() + }) + + test('not handle if unknown option', async () => { + const output = await runCommand(`${SCRIPT} dev --unknown`) + expect(output).toMatchSnapshot() + }) + + test('resolve value if long option and value', async () => { + const output = await runCommand(`${SCRIPT} dev --port=3`) + expect(output).toMatchSnapshot() + }) + + test('suggest if user ends with space after `--port`', async () => { + const output = await runCommand(`${SCRIPT} dev --port ""`) + expect(output).toMatchSnapshot() + }) + + test(`keep suggesting the --port option if user typed partial but didn't end with space`, async () => { + const output = await runCommand(`${SCRIPT} dev --po`) + expect(output).toMatchSnapshot() + }) + + test("user typed `--port=` and hasn't typed a space or value yet", async () => { + const output = await runCommand(`${SCRIPT} dev --port=`) + expect(output).toMatchSnapshot() + }) + + test.todo('suggest short option with equals sign', async () => { + const output = await runCommand(`${SCRIPT} dev -p=3`) + expect(output).toMatchSnapshot() + }) }) -test.todo('subcommand long option and value', async () => { - const output = await runCommand(`${SCRIPT} dev --port=3`) - expect(output).toMatchSnapshot() +describe('positional arguments', () => { + test('suggest multiple positional arguments when ending with space', async () => { + const output = await runCommand(`${SCRIPT} lint ""`) + expect(output).toMatchSnapshot() + }) + + test('multiple positional arguments when ending with part of the value', async () => { + const output = await runCommand(`${SCRIPT} lint ind`) + expect(output).toMatchSnapshot() + }) + + test('single positional argument when ending with space', async () => { + const output = await runCommand(`${SCRIPT} lint main.ts ""`) + expect(output).toMatchSnapshot() + }) }) diff --git a/packages/plugin-completion/src/index.ts b/packages/plugin-completion/src/index.ts index f4d390536..44f324a4e 100644 --- a/packages/plugin-completion/src/index.ts +++ b/packages/plugin-completion/src/index.ts @@ -16,7 +16,7 @@ import type { PluginContext, PluginWithoutExtension } from '@gunshi/plugin' -import type { CompletionCommandContext, CompletionOptions } from './types.ts' +import type { CompletionCommandContext, CompletionConfig, CompletionOptions } from './types.ts' export * from './types.ts' @@ -101,7 +101,7 @@ export default function completion( // TODO(kazupon): more tweaking for root completion completion.addCommand( root, - entry.description || '', + entry.description ?? '', isPositional ? [false] : [], NOOP_HANDLER ) @@ -115,8 +115,8 @@ export default function completion( completion.addOption( root, `--${name}`, - schema.description || '', - NOOP_HANDLER, + schema.description ?? '', + config.entry?.args?.[name]?.handler ?? NOOP_HANDLER, schema.short ) } @@ -176,12 +176,10 @@ function quoteExec(): string { } } -type CompletionConfig = NonNullable - async function handleSubCommands( completion: Completion, subCommands: PluginContext['subCommands'], - configs: CompletionConfig['subCommands'] = {} + configs: Record = {} ) { for (const [name, cmd] of subCommands) { if (cmd.internal || cmd.entry || name === 'complete') { @@ -195,12 +193,11 @@ async function handleSubCommands( } const isPositional = hasPositional(resolvedCmd) // TODO(kazupon): more tweaking for subcommand completion - const handler = configs[name] || NOOP_HANDLER const commandName = completion.addCommand( name, - resolvedCmd.description, + resolvedCmd.description ?? '', isPositional ? [false] : [], - handler + configs?.[name]?.handler ?? NOOP_HANDLER ) const args = cmd.args || (Object.create(null) as Args) @@ -212,8 +209,8 @@ async function handleSubCommands( completion.addOption( commandName, `--${name}`, - schema.description || '', - NOOP_HANDLER, + schema.description ?? '', + configs[commandName]?.args?.[name]?.handler ?? NOOP_HANDLER, schema.short ) } diff --git a/packages/plugin-completion/src/types.ts b/packages/plugin-completion/src/types.ts index d74c42a3d..9fad999bb 100644 --- a/packages/plugin-completion/src/types.ts +++ b/packages/plugin-completion/src/types.ts @@ -25,18 +25,26 @@ export type PluginId = typeof pluginId */ export interface CompletionCommandContext {} +/** + * Completion configuration, which structure is similar `bombsh/tab`'s `CompletionConfig`. + */ +export interface CompletionConfig { + handler?: Handler + args?: Record +} + /** * Completion plugin options. */ export interface CompletionOptions { config?: { /** - * The entry point handler. + * The entry point completion configuration. */ - entry?: Handler + entry?: CompletionConfig /** * The handlers for subcommands. */ - subCommands?: Record + subCommands?: Record } } From 4043a088b3c37fb6fe7ca5a53ad86f26cca0477d Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Wed, 9 Jul 2025 17:46:23 +0900 Subject: [PATCH 08/22] refactor: allow command name loosy --- .../gunshi/src/__snapshots__/cli.test.ts.snap | 26 ++++++++++++++++--- packages/gunshi/src/cli.test.ts | 11 ++++++-- packages/gunshi/src/cli/core.ts | 25 +++++++++--------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/packages/gunshi/src/__snapshots__/cli.test.ts.snap b/packages/gunshi/src/__snapshots__/cli.test.ts.snap index 41b53314b..f5fce310e 100644 --- a/packages/gunshi/src/__snapshots__/cli.test.ts.snap +++ b/packages/gunshi/src/__snapshots__/cli.test.ts.snap @@ -57,7 +57,7 @@ OPTIONS: exports[`auto generate usage > loosely sub commands > command2 1`] = ` "USAGE: - COMMAND command2 + my-cli command2 OPTIONS: -h, --help Display this help message @@ -68,7 +68,16 @@ OPTIONS: exports[`auto generate usage > loosely sub commands > main 1`] = ` "USAGE: - COMMAND + my-cli [command1] + my-cli + +COMMANDS: + command2 + command1 + +For more info, run any command with the \`--help\` flag: + my-cli command2 --help + my-cli command1 --help OPTIONS: -h, --help Display this help message @@ -79,7 +88,16 @@ OPTIONS: exports[`auto generate usage > loosely sub commands 1`] = ` "USAGE: - COMMAND + my-cli [command1] + my-cli + +COMMANDS: + command2 + command1 + +For more info, run any command with the \`--help\` flag: + my-cli command2 --help + my-cli command1 --help OPTIONS: -h, --help Display this help message @@ -87,7 +105,7 @@ OPTIONS: -f, --foo USAGE: - COMMAND command2 + my-cli command2 OPTIONS: -h, --help Display this help message diff --git a/packages/gunshi/src/cli.test.ts b/packages/gunshi/src/cli.test.ts index 32a170a03..149c24e5e 100644 --- a/packages/gunshi/src/cli.test.ts +++ b/packages/gunshi/src/cli.test.ts @@ -390,6 +390,10 @@ describe('auto generate usage', () => { test('loosely sub commands', async () => { const utils = await import('./utils.ts') const log = defineMockLog(utils) + const meta = { + name: 'my-cli', + renderHeader: null // no header + } const entryArgs = { foo: { @@ -398,6 +402,7 @@ describe('auto generate usage', () => { } } satisfies Args const entry = { + name: 'command1', args: entryArgs, run: vi.fn() } satisfies Command> @@ -417,8 +422,10 @@ describe('auto generate usage', () => { const subCommands = new Map() subCommands.set('command2', command2) - expect(await cli(['-h'], entry, { subCommands })).toMatchSnapshot('main') - expect(await cli(['command2', '-h'], entry, { subCommands })).toMatchSnapshot('command2') + expect(await cli(['-h'], entry, { ...meta, subCommands })).toMatchSnapshot('main') + expect(await cli(['command2', '-h'], entry, { ...meta, subCommands })).toMatchSnapshot( + 'command2' + ) const message = log() expect(message).toMatchSnapshot() diff --git a/packages/gunshi/src/cli/core.ts b/packages/gunshi/src/cli/core.ts index 7edb0ac13..c42b6585d 100644 --- a/packages/gunshi/src/cli/core.ts +++ b/packages/gunshi/src/cli/core.ts @@ -136,17 +136,12 @@ function createInitialSubCommands( const subCommands = new Map(options.subCommands) // add entry command to sub commands if there are sub commands - if (options.subCommands || subCommands.size > 0) { - if (isLazyCommand(entryCmd)) { - if (entryCmd.commandName) { - entryCmd.entry = true - subCommands.set(entryCmd.commandName, entryCmd as LazyCommand) - } - // } else if (typeof entryCmd === 'object' && entryCmd.name) { - } else if (typeof entryCmd === 'object') { - entryCmd.entry = true - subCommands.set(entryCmd.name || '', entryCmd as Command) - } + if ( + (options.subCommands || subCommands.size > 0) && + (isLazyCommand(entryCmd) || typeof entryCmd === 'object') + ) { + entryCmd.entry = true + subCommands.set(resolveEntryName(entryCmd as LazyCommand | Command), entryCmd) } return subCommands @@ -256,8 +251,12 @@ async function resolveCommand( } } -function resolveEntryName(entry: Command): string { - return entry.name || ANONYMOUS_COMMAND_NAME +function resolveEntryName( + entry: Command | LazyCommand +): string { + return isLazyCommand(entry) + ? entry.commandName || ANONYMOUS_COMMAND_NAME + : entry.name || ANONYMOUS_COMMAND_NAME } function getPluginExtensions(plugins: Plugin[]): Record { From 7015e036778e02c3a5ac3013610f10c5f6367dd6 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Wed, 9 Jul 2025 17:48:42 +0900 Subject: [PATCH 09/22] refactor: wrong extension identifier --- packages/plugin-global/src/decorator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-global/src/decorator.ts b/packages/plugin-global/src/decorator.ts index 38fa45542..40fe37273 100644 --- a/packages/plugin-global/src/decorator.ts +++ b/packages/plugin-global/src/decorator.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { pluginId as I18n } from './types.ts' +import { pluginId as Global } from './types.ts' import type { CommandDecorator, DefaultGunshiParams } from '@gunshi/plugin' import type { GlobalCommandContext } from './extension.ts' @@ -22,7 +22,7 @@ const decorator: CommandDecorator<{ values, validationError, extensions: { - [I18n]: { showVersion, showHeader, showUsage, showValidationErrors } + [Global]: { showVersion, showHeader, showUsage, showValidationErrors } } } = ctx From b09fd395d6f9a9f162fc95df5ce32a6704c87ddb Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 10 Jul 2025 11:23:36 +0900 Subject: [PATCH 10/22] refactor: extract localization function --- packages/plugin-i18n/src/types.ts | 20 +---- packages/plugin-renderer/src/index.ts | 51 +------------ packages/plugin-renderer/src/types.ts | 11 +-- packages/plugin-renderer/src/usage.ts | 15 +--- packages/shared/src/index.ts | 1 + packages/shared/src/localize.test.ts | 101 ++++++++++++++++++++++++++ packages/shared/src/localize.ts | 68 +++++++++++++++++ packages/shared/src/types.ts | 16 +++- packages/shared/src/utils.ts | 16 ++++ 9 files changed, 212 insertions(+), 87 deletions(-) create mode 100644 packages/shared/src/localize.test.ts create mode 100644 packages/shared/src/localize.ts diff --git a/packages/plugin-i18n/src/types.ts b/packages/plugin-i18n/src/types.ts index 815db4dcc..267833289 100644 --- a/packages/plugin-i18n/src/types.ts +++ b/packages/plugin-i18n/src/types.ts @@ -13,19 +13,14 @@ import type { GunshiParamsConstraint, NormalizeToGunshiParams } from '@gunshi/plugin' -import { - ARG_PREFIX, - CommandArgKeys, - CommandBuiltinKeys, - namespacedId, - PLUGIN_PREFIX -} from '@gunshi/shared' +import { ARG_PREFIX, CommandBuiltinKeys, namespacedId, PLUGIN_PREFIX } from '@gunshi/shared' import type { BuiltinResourceKeys, GenerateNamespacedKey, KeyOfArgs, - RemovedIndex + RemovedIndex, + Translation } from '@gunshi/shared' /** @@ -56,14 +51,7 @@ export interface I18nCommandContext = DefaultGunshiP * - For custom keys: returns an empty string ('') * - For built-in keys (prefixed with '_:'): returns the key itself */ - translate: < - T extends string = CommandBuiltinKeys, - O = CommandArgKeys, - K = CommandBuiltinKeys | O | T - >( - key: K, - values?: Record - ) => string + translate: Translation } /** diff --git a/packages/plugin-renderer/src/index.ts b/packages/plugin-renderer/src/index.ts index 3852d17c3..b11385fe4 100644 --- a/packages/plugin-renderer/src/index.ts +++ b/packages/plugin-renderer/src/index.ts @@ -30,18 +30,10 @@ */ import { plugin } from '@gunshi/plugin' -import { - ARG_NEGATABLE_PREFIX, - ARG_PREFIX_AND_KEY_SEPARATOR, - BUILD_IN_PREFIX_AND_KEY_SEPARATOR, - DefaultResource, - namespacedId, - resolveExamples, - resolveLazyCommand -} from '@gunshi/shared' +import { localizable, namespacedId, resolveLazyCommand } from '@gunshi/shared' import { renderHeader } from './header.ts' import { pluginId as id } from './types.ts' -import { makeShortLongOptionPair, renderUsage } from './usage.ts' +import { renderUsage } from './usage.ts' import { renderValidationErrors } from './validation.ts' import type { @@ -54,7 +46,7 @@ import type { PluginWithExtension } from '@gunshi/plugin' import type { I18nCommandContext } from '@gunshi/plugin-i18n' -import type { CommandArgKeys, CommandBuiltinKeys } from '@gunshi/shared' +import type { CommandBuiltinKeys } from '@gunshi/shared' import type { PluginId, UsageRendererCommandContext } from './types.ts' export { renderHeader } from './header.ts' @@ -111,43 +103,8 @@ export default function renderer(): PluginWithExtension !cmd.internal).filter(Boolean)) } - async function text< - T extends string = CommandBuiltinKeys, - O = CommandArgKeys, - K = CommandBuiltinKeys | O | T - >(key: K, values: Record = Object.create(null)): Promise { - if (i18n) { - return i18n.translate(key, values) - } else { - if ((key as string).startsWith(BUILD_IN_PREFIX_AND_KEY_SEPARATOR)) { - const resKey = (key as string).slice(BUILD_IN_PREFIX_AND_KEY_SEPARATOR.length) - return DefaultResource[resKey as keyof typeof DefaultResource] || (key as string) - } else if ((key as string).startsWith(ARG_PREFIX_AND_KEY_SEPARATOR)) { - let argKey = (key as string).slice(ARG_PREFIX_AND_KEY_SEPARATOR.length) - let negatable = false - if (argKey.startsWith(ARG_NEGATABLE_PREFIX)) { - argKey = argKey.slice(ARG_NEGATABLE_PREFIX.length) - negatable = true - } - const schema = ctx.args[argKey as keyof typeof ctx.args] - return negatable && schema.type === 'boolean' && schema.negatable - ? `${DefaultResource['NEGATABLE']} ${makeShortLongOptionPair(schema, argKey, ctx.toKebab)}` - : schema.description || '' - } else { - // if the key is a built-in key 'description' and 'examples', return empty string, because the these keys are resolved by the renderer itself. - if (key === 'description') { - return '' - } else if (key === 'examples') { - return await resolveExamples(ctx, cmd.examples) - } else { - return key as string - } - } - } - } - return { - text, + text: localizable(ctx, cmd, i18n?.translate), loadCommands } }, diff --git a/packages/plugin-renderer/src/types.ts b/packages/plugin-renderer/src/types.ts index bb01fa4bf..3fa1be15a 100644 --- a/packages/plugin-renderer/src/types.ts +++ b/packages/plugin-renderer/src/types.ts @@ -6,7 +6,7 @@ import { namespacedId, PLUGIN_PREFIX } from '@gunshi/shared' import type { Command, DefaultGunshiParams, GunshiParams } from '@gunshi/plugin' -import type { CommandArgKeys, CommandBuiltinKeys, GenerateNamespacedKey } from '@gunshi/shared' +import type { CommandBuiltinKeys, GenerateNamespacedKey, Localization } from '@gunshi/shared' /** * The unique identifier for usage renderer plugin. @@ -28,14 +28,7 @@ export interface UsageRendererCommandContext = Defau /** * Render the text message */ - text: < - T extends string = CommandBuiltinKeys, - O = CommandArgKeys, - K = CommandBuiltinKeys | O | T - >( - key: K, - values?: Record - ) => Promise + text: Localization /** * Load commands * @returns A list of commands loaded from the command loader plugin. diff --git a/packages/plugin-renderer/src/usage.ts b/packages/plugin-renderer/src/usage.ts index 1d34bfd79..0ad3c79f4 100644 --- a/packages/plugin-renderer/src/usage.ts +++ b/packages/plugin-renderer/src/usage.ts @@ -8,6 +8,7 @@ import { COMMON_ARGS, resolveExamples as _resolvedExamples, kebabnize, + makeShortLongOptionPair, resolveArgKey, resolveBuiltInKey } from '@gunshi/shared' @@ -324,20 +325,6 @@ async function generateOptionsSymbols< : '' } -export function makeShortLongOptionPair( - schema: ArgSchema, - name: string, - toKebab?: boolean -): string { - // Convert camelCase to kebab-case for display in help text if toKebab is true - const displayName = toKebab || schema.toKebab ? kebabnize(name) : name - let key = `--${displayName}` - if (schema.short) { - key = `-${schema.short}, ${key}` - } - return key -} - /** * Get optional arguments pairs for usage * @param ctx A {@link CommandContext | command context} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index c76baa7c2..7b03488b9 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -5,6 +5,7 @@ export * from 'gunshi/utils' export * from './constants.ts' +export * from './localize.ts' export { default as DefaultResource } from './resource.ts' export * from './utils.ts' diff --git a/packages/shared/src/localize.test.ts b/packages/shared/src/localize.test.ts new file mode 100644 index 000000000..126fd5603 --- /dev/null +++ b/packages/shared/src/localize.test.ts @@ -0,0 +1,101 @@ +import { define } from 'gunshi' +import { createCommandContext } from 'gunshi/context' +import { describe, expect, test, vi } from 'vitest' +import { localizable } from './localize.ts' +import { resolveArgKey, resolveBuiltInKey } from './utils.ts' + +const LANG_RESOURCES = { + description: 'これはcommand1の説明です', + foo: 'foo引数の説明' +} as Record + +async function setup() { + const command1 = define({ + name: 'command1', + description: 'Command 1 description', + args: { + foo: { + type: 'string', + description: 'Foo argument description', + short: 'f' + }, + bar: { + type: 'boolean', + description: 'Bar argument description', + negatable: true + } + }, + examples: () => { + return `command1 --foo value --no-bar` + } + }) + + const ctx = await createCommandContext({ + args: command1.args!, + values: {}, + positionals: [], + rest: [], + argv: [], + tokens: [], + omitted: false, + callMode: 'subCommand', + command: command1, + extensions: {}, + cliOptions: {} + }) + + return { ctx, command1 } +} + +test('with translation function', async () => { + const mockTranslate = vi + .fn() + .mockImplementation((key: string, _values: Record): string => { + return LANG_RESOURCES[key] || key + }) + + const { ctx, command1 } = await setup() + const localize = localizable(ctx, command1, mockTranslate) + + expect(await localize('description')).toEqual(LANG_RESOURCES['description']) + expect(await localize('foo')).toEqual(LANG_RESOURCES['foo']) + expect(await localize('nonexistent_key')).toEqual('nonexistent_key') +}) + +describe('without translation function', () => { + test('gunshi built-in keys', async () => { + const { ctx, command1 } = await setup() + const localize = localizable(ctx, command1) + + expect(await localize(resolveBuiltInKey('USAGE'))).toEqual('USAGE') + expect(await localize(resolveBuiltInKey('help'))).toEqual('Display this help message') + }) + + test('gunshi args keys', async () => { + const { ctx, command1 } = await setup() + const localize = localizable(ctx, command1) + + // normal argument + expect(await localize(resolveArgKey>('foo'))).toEqual( + 'Foo argument description' + ) + // negatable argument + expect(await localize(resolveArgKey>('no-bar'))).toEqual( + 'Negatable of --bar' + ) + // non-existent argument + expect(await localize(resolveArgKey('test'))).toEqual('test') + }) + + test('other keys', async () => { + const { ctx, command1 } = await setup() + const localize = localizable(ctx, command1) + + // `description` key + expect(await localize('description')).toEqual('') + // `examples` key + expect(await localize('examples')).toEqual('command1 --foo value --no-bar') + // other keys + expect(await localize('other_key')).toEqual('other_key') + }) +}) diff --git a/packages/shared/src/localize.ts b/packages/shared/src/localize.ts new file mode 100644 index 000000000..9a5d90972 --- /dev/null +++ b/packages/shared/src/localize.ts @@ -0,0 +1,68 @@ +/** + * @author kazuya kawaguchi (a.k.a. kazupon) + * @license MIT + */ + +import { + ARG_NEGATABLE_PREFIX, + ARG_PREFIX_AND_KEY_SEPARATOR, + BUILD_IN_PREFIX_AND_KEY_SEPARATOR +} from './constants.ts' +import DefaultResource from './resource.ts' +import { makeShortLongOptionPair, resolveExamples } from './utils.ts' + +import type { Command, CommandContext, DefaultGunshiParams, GunshiParams } from 'gunshi' +import type { CommandArgKeys, CommandBuiltinKeys, Translation } from './types.ts' + +export interface Localization< + T extends string = CommandBuiltinKeys, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + G extends GunshiParams = DefaultGunshiParams +> { + , K = CommandBuiltinKeys | O | T>( + key: K, + values?: Record + ): Promise +} + +export function localizable( + ctx: CommandContext, + cmd: Command, + translate?: Translation +): Localization { + async function localize(key: string, values?: Record): Promise { + if (translate) { + return translate(key as T, values) + } else { + if ((key as string).startsWith(BUILD_IN_PREFIX_AND_KEY_SEPARATOR)) { + const resKey = (key as string).slice(BUILD_IN_PREFIX_AND_KEY_SEPARATOR.length) + return DefaultResource[resKey as keyof typeof DefaultResource] || (key as string) + } else if ((key as string).startsWith(ARG_PREFIX_AND_KEY_SEPARATOR)) { + let argKey = (key as string).slice(ARG_PREFIX_AND_KEY_SEPARATOR.length) + let negatable = false + if (argKey.startsWith(ARG_NEGATABLE_PREFIX)) { + argKey = argKey.slice(ARG_NEGATABLE_PREFIX.length) + negatable = true + } + const schema = ctx.args[argKey as keyof typeof ctx.args] + if (!schema) { + return argKey + } + return negatable && schema.type === 'boolean' && schema.negatable + ? `${DefaultResource['NEGATABLE']} ${makeShortLongOptionPair(schema, argKey, ctx.toKebab)}` + : schema.description || '' + } else { + // if the key is a built-in key 'description' and 'examples', return empty string, because the these keys are resolved by the user. + if (key === 'description') { + return '' + } else if (key === 'examples') { + return await resolveExamples(ctx, cmd.examples) + } else { + return key as string + } + } + } + } + + return localize as Localization +} diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 2c19dc2c5..88ca4c354 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -5,7 +5,7 @@ import { ARG_PREFIX, BUILT_IN_KEY_SEPARATOR, BUILT_IN_PREFIX } from './constants.ts' -import type { Args } from 'gunshi' +import type { Args, DefaultGunshiParams, GunshiParams } from 'gunshi' type RemoveIndexSignature = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] @@ -69,3 +69,17 @@ export type CommandArgKeys = GenerateNamespacedKey< KeyOfArgs>, typeof ARG_PREFIX > + +/** + * Translation fucntion interface + */ +export interface Translation< + T extends string = CommandBuiltinKeys, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + G extends GunshiParams = DefaultGunshiParams +> { + , K = CommandBuiltinKeys | O | T>( + key: K, + values?: Record + ): string +} diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 3705275c4..fc51abe54 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -3,10 +3,12 @@ * @license MIT */ +import { kebabnize } from 'gunshi/utils' import { ARG_PREFIX, BUILT_IN_KEY_SEPARATOR, BUILT_IN_PREFIX, PLUGIN_PREFIX } from './constants.ts' import type { Args, + ArgSchema, CommandContext, CommandExamplesFetcher, DefaultGunshiParams, @@ -49,3 +51,17 @@ export function namespacedId( ): GenerateNamespacedKey { return `${PLUGIN_PREFIX}${BUILT_IN_KEY_SEPARATOR}${id}` } + +export function makeShortLongOptionPair( + schema: ArgSchema, + name: string, + toKebab?: boolean +): string { + // Convert camelCase to kebab-case for display in help text if toKebab is true + const displayName = toKebab || schema.toKebab ? kebabnize(name) : name + let key = `--${displayName}` + if (schema.short) { + key = `-${schema.short}, ${key}` + } + return key +} From ffde558313444d3091df1cf09fe3a26afb2236d7 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 10 Jul 2025 11:29:21 +0900 Subject: [PATCH 11/22] fix: support localizable completion --- packages/gunshi/src/plugin.ts | 2 + packages/plugin-completion/package.json | 3 + packages/plugin-completion/src/index.ts | 122 +++++++++++++++--------- pnpm-lock.yaml | 6 +- 4 files changed, 84 insertions(+), 49 deletions(-) diff --git a/packages/gunshi/src/plugin.ts b/packages/gunshi/src/plugin.ts index e6f2dafc1..02af39248 100644 --- a/packages/gunshi/src/plugin.ts +++ b/packages/gunshi/src/plugin.ts @@ -23,6 +23,8 @@ * @license MIT */ +export { COMMAND_OPTIONS_DEFAULT } from './constants.ts' +export { createCommandContext } from './context.ts' export { plugin } from './plugin/core.ts' export type { PluginContext } from './plugin/context.ts' diff --git a/packages/plugin-completion/package.json b/packages/plugin-completion/package.json index 61b3ebe3b..10aa73429 100644 --- a/packages/plugin-completion/package.json +++ b/packages/plugin-completion/package.json @@ -61,6 +61,9 @@ "@bombsh/tab": "git+ssh://git@github.com:bombshell-dev/tab.git", "@gunshi/plugin": "workspace:*" }, + "peerDependencies": { + "@gunshi/plugin-i18n": "workspace:*" + }, "devDependencies": { "@gunshi/shared": "workspace:*", "deno": "catalog:", diff --git a/packages/plugin-completion/src/index.ts b/packages/plugin-completion/src/index.ts index 44f324a4e..3f1b99fb7 100644 --- a/packages/plugin-completion/src/index.ts +++ b/packages/plugin-completion/src/index.ts @@ -4,18 +4,24 @@ */ import { Completion, script } from '@bombsh/tab' -import { plugin } from '@gunshi/plugin' -import { resolveLazyCommand } from '@gunshi/shared' +import { + COMMAND_OPTIONS_DEFAULT, + createCommandContext as _createCommandContext, + plugin +} from '@gunshi/plugin' +import { localizable, namespacedId, resolveArgKey, resolveLazyCommand } from '@gunshi/shared' import { pluginId } from './types.ts' import type { Handler } from '@bombsh/tab' import type { Args, Command, + CommandContext, LazyCommand, PluginContext, - PluginWithoutExtension + PluginWithExtension } from '@gunshi/plugin' +import type { I18nCommandContext } from '@gunshi/plugin-i18n' import type { CompletionCommandContext, CompletionConfig, CompletionOptions } from './types.ts' export * from './types.ts' @@ -26,12 +32,14 @@ const NOOP_HANDLER: Handler = () => { return [] } +const i18nPluginId = namespacedId('i18n') + /** * completion plugin for gunshi */ export default function completion( options: CompletionOptions = {} -): PluginWithoutExtension { +): PluginWithExtension { const config = options.config || {} const completion = new Completion() @@ -39,6 +47,8 @@ export default function completion( id: pluginId, name: 'completion', + dependencies: [{ id: i18nPluginId, optional: true }], + async setup(ctx) { /** * add command for completion script generation @@ -49,19 +59,9 @@ export default function completion( name: completeName, // TODO(kazupon): support description localization description: 'Generate shell completion script', - // args: { - // shell: { - // type: 'string', - // // @ts-ignore -- TOOD: `args-tokens` will be updated to support `shell` type - // required: false, - // // TODO(kazupon): support description localization - // description: - // 'shell type to generate completion script (zsh, bash, fish, fig, powershell)', - // } - // }, run: async cmdCtx => { if (!cmdCtx.env.name) { - throw new Error('cli name is not defined.') + throw new Error('your cli name is not defined.') } let shell: string | undefined = cmdCtx._[1] @@ -71,7 +71,7 @@ export default function completion( if (shell === undefined) { const extra = cmdCtx._.slice(cmdCtx._.indexOf(TERMINATOR) + 1) - completion.parse(extra) + await completion.parse(extra) } else { script(shell as Parameters[0], cmdCtx.env.name, quoteExec()) } @@ -83,48 +83,78 @@ export default function completion( */ // TODO(kazupon): we might be change this to a more flexible way ctx.decorateHeaderRenderer(async (_baseRenderer, _cmdCtx) => '') + }, - /** - * setup bombshell completion - */ + // TODO(kazupon): type inference with plugin function type parameter + extension: (_ctx, _cmd): CompletionCommandContext => { + return {} as CompletionCommandContext + }, + + /** + * setup bombshell completion with `onExtension` hook + */ - const entry = [...ctx.subCommands].map(([_, cmd]) => cmd).find(cmd => cmd.entry) + onExtension: async (ctx, cmd) => { + // TODO(kazupon): type inference with plugin function type parameter, more improvements! + const extensions = ctx.extensions as unknown as { [i18nPluginId]: I18nCommandContext } + const i18n = extensions[i18nPluginId] + const subCommands = ctx.env.subCommands as ReadonlyMap + + const entry = [...subCommands].map(([_, cmd]) => cmd).find(cmd => cmd.entry) if (!entry) { - throw new Error( - 'No entry command found. Please ensure that an entry command is defined in the plugin context.' - ) + throw new Error('entry command not found.') } + const entryCtx = await createCommandContext(entry) + const localizeDescription = localizable( + entryCtx as unknown as CommandContext, + cmd, + i18n ? i18n.translate : undefined + ) + // setup root level completion const isPositional = hasPositional(await resolveLazyCommand(entry as Command)) const root = '' - // TODO(kazupon): more tweaking for root completion completion.addCommand( root, - entry.description ?? '', + (await localizeDescription('description')) || entry.description || '', isPositional ? [false] : [], NOOP_HANDLER ) const args = entry.args || (Object.create(null) as Args) - for (const [name, schema] of Object.entries(args)) { + for (const [key, schema] of Object.entries(args)) { if (schema.type === 'positional') { continue // skip positional arguments on subcommands } // TODO(kazupon): more tweaking for root option completion completion.addOption( root, - `--${name}`, - schema.description ?? '', - config.entry?.args?.[name]?.handler ?? NOOP_HANDLER, + `--${key}`, + (await localizeDescription(resolveArgKey(key))) || schema.description || '', + config.entry?.args?.[key]?.handler || NOOP_HANDLER, schema.short ) } - handleSubCommands(completion, ctx.subCommands, config.subCommands) - }, + await handleSubCommands(completion, subCommands, config.subCommands, i18n) + } + }) +} - extension: async (_ctx, _cmd) => {} +async function createCommandContext(cmd: Command | LazyCommand): Promise { + return await _createCommandContext({ + args: cmd.args || (Object.create(null) as Args), + values: Object.create(null), + positionals: [], + rest: [], + argv: [], + tokens: [], + omitted: false, + callMode: cmd.entry ? 'entry' : 'subCommand', + command: cmd, + extensions: Object.create(null), + cliOptions: COMMAND_OPTIONS_DEFAULT }) } @@ -171,7 +201,7 @@ function quoteExec(): string { throw new Error('deno not implemented yet, welcome contributions :)') } default: { - throw new Error('Unsupported runtime for completion script generation.') + throw new Error('Unsupported your javascript runtime for completion script generation.') } } } @@ -179,38 +209,38 @@ function quoteExec(): string { async function handleSubCommands( completion: Completion, subCommands: PluginContext['subCommands'], - configs: Record = {} + configs: Record = {}, + i18n?: I18nCommandContext | undefined ) { for (const [name, cmd] of subCommands) { if (cmd.internal || cmd.entry || name === 'complete') { continue // skip entry / internal command / completion command itself } + const resolvedCmd = await resolveLazyCommand(cmd) - if (!resolvedCmd.description) { - throw new Error( - `Command "${name}" does not have a description. Please ensure that all commands have a description defined.` - ) - } + const ctx = await createCommandContext(resolvedCmd) + const localizeDescription = localizable(ctx, resolvedCmd, i18n ? i18n.translate : undefined) + const isPositional = hasPositional(resolvedCmd) // TODO(kazupon): more tweaking for subcommand completion const commandName = completion.addCommand( name, - resolvedCmd.description ?? '', + (await localizeDescription('description')) || resolvedCmd.description || '', isPositional ? [false] : [], - configs?.[name]?.handler ?? NOOP_HANDLER + configs?.[name]?.handler || NOOP_HANDLER ) - const args = cmd.args || (Object.create(null) as Args) - for (const [name, schema] of Object.entries(args)) { + const args = resolvedCmd.args || (Object.create(null) as Args) + for (const [key, schema] of Object.entries(args)) { if (schema.type === 'positional') { continue // skip positional arguments on subcommands } // TODO(kazupon): more tweaking for subcommand option completion completion.addOption( commandName, - `--${name}`, - schema.description ?? '', - configs[commandName]?.args?.[name]?.handler ?? NOOP_HANDLER, + `--${key}`, + (await localizeDescription(resolveArgKey(key))) || schema.description || '', + configs[commandName]?.args?.[key]?.handler || NOOP_HANDLER, schema.short ) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5ab9b346..98e5d8ae8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -282,6 +282,9 @@ importers: '@gunshi/plugin': specifier: workspace:* version: link:../plugin + '@gunshi/plugin-i18n': + specifier: workspace:* + version: link:../plugin-i18n devDependencies: '@gunshi/shared': specifier: workspace:* @@ -289,9 +292,6 @@ importers: deno: specifier: 'catalog:' version: 2.4.0 - gunshi: - specifier: workspace:* - version: link:../gunshi jsr: specifier: 'catalog:' version: 0.13.5 From 15a8b5756b7e8abef4262e3ec10a4cc409b96f57 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 10 Jul 2025 13:41:01 +0900 Subject: [PATCH 12/22] fix: typo --- .../plugin-completion/src/__snapshots__/index.test.ts.snap | 4 ++-- packages/plugin-completion/src/index.test.ts | 4 ++-- packages/shared/src/types.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/plugin-completion/src/__snapshots__/index.test.ts.snap b/packages/plugin-completion/src/__snapshots__/index.test.ts.snap index d91dcfc2d..ccf6ddad1 100644 --- a/packages/plugin-completion/src/__snapshots__/index.test.ts.snap +++ b/packages/plugin-completion/src/__snapshots__/index.test.ts.snap @@ -16,7 +16,7 @@ exports[`default command > suggest duplicate options for short option 1`] = ` " `; -exports[`default command > suggest for inputing 1`] = ` +exports[`default command > suggest for inputting 1`] = ` "--config Use specified config file --mode Set env mode --logLevel info | warn | error | silent @@ -100,7 +100,7 @@ exports[`subcommand > suggest for long option 1`] = ` " `; -exports[`subcommand > suggest for option inputing 1`] = ` +exports[`subcommand > suggest for option inputting 1`] = ` "--host Specify hostname --port Specify port :4 diff --git a/packages/plugin-completion/src/index.test.ts b/packages/plugin-completion/src/index.test.ts index 693d6421c..120aae347 100644 --- a/packages/plugin-completion/src/index.test.ts +++ b/packages/plugin-completion/src/index.test.ts @@ -21,7 +21,7 @@ test('no input', async () => { }) describe('default command', () => { - test('suggest for inputing', async () => { + test('suggest for inputting', async () => { const output = await runCommand(`${SCRIPT} --`) expect(output).toMatchSnapshot() }) @@ -58,7 +58,7 @@ describe('subcommand', () => { expect(output).toMatchSnapshot() }) - test('suggest for option inputing', async () => { + test('suggest for option inputting', async () => { const output = await runCommand(`${SCRIPT} dev --`) expect(output).toMatchSnapshot() }) diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 88ca4c354..fdd07322e 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -71,7 +71,7 @@ export type CommandArgKeys = GenerateNamespacedKey< > /** - * Translation fucntion interface + * Translation function interface */ export interface Translation< T extends string = CommandBuiltinKeys, From 6dc6170ac0ff35be2e037298907e5d149a839069 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 10 Jul 2025 16:11:39 +0900 Subject: [PATCH 13/22] refactor: cli options default constant name --- packages/gunshi/src/cli/core.ts | 4 ++-- packages/gunshi/src/constants.ts | 2 +- packages/gunshi/src/context.ts | 4 ++-- packages/gunshi/src/plugin.ts | 2 +- packages/plugin-completion/src/index.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/gunshi/src/cli/core.ts b/packages/gunshi/src/cli/core.ts index c42b6585d..ba8109344 100644 --- a/packages/gunshi/src/cli/core.ts +++ b/packages/gunshi/src/cli/core.ts @@ -4,7 +4,7 @@ */ import { parseArgs, resolveArgs } from 'args-tokens' -import { ANONYMOUS_COMMAND_NAME, COMMAND_OPTIONS_DEFAULT, NOOP } from '../constants.ts' +import { ANONYMOUS_COMMAND_NAME, CLI_OPTIONS_DEFAULT, NOOP } from '../constants.ts' import { createCommandContext } from '../context.ts' import { createDecorators } from '../decorators.ts' import { createPluginContext } from '../plugin/context.ts' @@ -155,7 +155,7 @@ function normalizeCliOptions( // get the latest sub commands from plugin context (already includes entry command) const subCommands = new Map(pluginContext.subCommands) - const resolvedOptions = Object.assign(create>(), COMMAND_OPTIONS_DEFAULT, options, { + const resolvedOptions = Object.assign(create>(), CLI_OPTIONS_DEFAULT, options, { subCommands }) as CliOptions diff --git a/packages/gunshi/src/constants.ts b/packages/gunshi/src/constants.ts index b596c6193..65ec551c8 100644 --- a/packages/gunshi/src/constants.ts +++ b/packages/gunshi/src/constants.ts @@ -9,7 +9,7 @@ export const ANONYMOUS_COMMAND_NAME = '(anonymous)' export const NOOP: () => void = () => {} -export const COMMAND_OPTIONS_DEFAULT: CliOptions = { +export const CLI_OPTIONS_DEFAULT: CliOptions = { name: undefined, description: undefined, version: undefined, diff --git a/packages/gunshi/src/context.ts b/packages/gunshi/src/context.ts index f4366d8c2..f152899d5 100644 --- a/packages/gunshi/src/context.ts +++ b/packages/gunshi/src/context.ts @@ -15,7 +15,7 @@ * @license MIT */ -import { ANONYMOUS_COMMAND_NAME, COMMAND_OPTIONS_DEFAULT, NOOP } from './constants.ts' +import { ANONYMOUS_COMMAND_NAME, CLI_OPTIONS_DEFAULT, NOOP } from './constants.ts' import { create, deepFreeze, isLazyCommand, log } from './utils.ts' import type { @@ -148,7 +148,7 @@ export async function createCommandContext< * setup the environment */ - const env = Object.assign(create>(), COMMAND_OPTIONS_DEFAULT, cliOptions) + const env = Object.assign(create>(), CLI_OPTIONS_DEFAULT, cliOptions) /** * create the command context diff --git a/packages/gunshi/src/plugin.ts b/packages/gunshi/src/plugin.ts index 02af39248..7c159aeaa 100644 --- a/packages/gunshi/src/plugin.ts +++ b/packages/gunshi/src/plugin.ts @@ -23,7 +23,7 @@ * @license MIT */ -export { COMMAND_OPTIONS_DEFAULT } from './constants.ts' +export { CLI_OPTIONS_DEFAULT } from './constants.ts' export { createCommandContext } from './context.ts' export { plugin } from './plugin/core.ts' diff --git a/packages/plugin-completion/src/index.ts b/packages/plugin-completion/src/index.ts index 3f1b99fb7..a0c5fb35e 100644 --- a/packages/plugin-completion/src/index.ts +++ b/packages/plugin-completion/src/index.ts @@ -5,7 +5,7 @@ import { Completion, script } from '@bombsh/tab' import { - COMMAND_OPTIONS_DEFAULT, + CLI_OPTIONS_DEFAULT, createCommandContext as _createCommandContext, plugin } from '@gunshi/plugin' @@ -154,7 +154,7 @@ async function createCommandContext(cmd: Command | LazyCommand): Promise Date: Thu, 10 Jul 2025 16:16:36 +0900 Subject: [PATCH 14/22] rerfactor: split some utilities --- packages/plugin-completion/src/index.ts | 71 +----------------------- packages/plugin-completion/src/utils.ts | 72 +++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 69 deletions(-) create mode 100644 packages/plugin-completion/src/utils.ts diff --git a/packages/plugin-completion/src/index.ts b/packages/plugin-completion/src/index.ts index a0c5fb35e..25422c057 100644 --- a/packages/plugin-completion/src/index.ts +++ b/packages/plugin-completion/src/index.ts @@ -4,13 +4,10 @@ */ import { Completion, script } from '@bombsh/tab' -import { - CLI_OPTIONS_DEFAULT, - createCommandContext as _createCommandContext, - plugin -} from '@gunshi/plugin' +import { plugin } from '@gunshi/plugin' import { localizable, namespacedId, resolveArgKey, resolveLazyCommand } from '@gunshi/shared' import { pluginId } from './types.ts' +import { createCommandContext, quoteExec } from './utils.ts' import type { Handler } from '@bombsh/tab' import type { @@ -142,70 +139,6 @@ export default function completion( }) } -async function createCommandContext(cmd: Command | LazyCommand): Promise { - return await _createCommandContext({ - args: cmd.args || (Object.create(null) as Args), - values: Object.create(null), - positionals: [], - rest: [], - argv: [], - tokens: [], - omitted: false, - callMode: cmd.entry ? 'entry' : 'subCommand', - command: cmd, - extensions: Object.create(null), - cliOptions: CLI_OPTIONS_DEFAULT - }) -} - -function detectRuntime(): 'bun' | 'deno' | 'node' | 'unknown' { - // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` - if (globalThis.process !== undefined && globalThis.process.release?.name === 'node') { - return 'node' - } - // @ts-ignore -- NOTE: ignore, because development env is node.js - if (globalThis.Deno !== undefined) { - return 'deno' - } - // @ts-ignore -- NOTE: ignore, because development env is node.js - if (globalThis.Bun !== undefined) { - return 'bun' - } - return 'unknown' -} - -function quoteIfNeeded(path: string): string { - return path.includes(' ') ? `'${path}'` : path -} - -function quoteExec(): string { - const runtime = detectRuntime() - switch (runtime) { - case 'node': { - // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` - const execPath = globalThis.process.execPath - // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` - const processArgs = globalThis.process.argv.slice(1) - const quotedExecPath = quoteIfNeeded(execPath) - // eslint-disable-next-line unicorn/no-array-callback-reference - const quotedProcessArgs = processArgs.map(quoteIfNeeded) - // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` - // eslint-disable-next-line unicorn/no-array-callback-reference - const quotedProcessExecArgs = globalThis.process.execArgv.map(quoteIfNeeded) - return `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}` - } - case 'deno': { - throw new Error('deno not implemented yet, welcome contributions :)') - } - case 'bun': { - throw new Error('deno not implemented yet, welcome contributions :)') - } - default: { - throw new Error('Unsupported your javascript runtime for completion script generation.') - } - } -} - async function handleSubCommands( completion: Completion, subCommands: PluginContext['subCommands'], diff --git a/packages/plugin-completion/src/utils.ts b/packages/plugin-completion/src/utils.ts new file mode 100644 index 000000000..f3c73e01f --- /dev/null +++ b/packages/plugin-completion/src/utils.ts @@ -0,0 +1,72 @@ +/** + * @author kazuya kawaguchi (a.k.a. kazupon) + * @license MIT + */ + +import { CLI_OPTIONS_DEFAULT, createCommandContext as _createCommandContext } from '@gunshi/plugin' + +import type { Args, Command, CommandContext, LazyCommand } from '@gunshi/plugin' + +export async function createCommandContext(cmd: Command | LazyCommand): Promise { + return await _createCommandContext({ + args: cmd.args || (Object.create(null) as Args), + values: Object.create(null), + positionals: [], + rest: [], + argv: [], + tokens: [], + omitted: false, + callMode: cmd.entry ? 'entry' : 'subCommand', + command: cmd, + extensions: Object.create(null), + cliOptions: CLI_OPTIONS_DEFAULT + }) +} + +function detectRuntime(): 'bun' | 'deno' | 'node' | 'unknown' { + // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` + if (globalThis.process !== undefined && globalThis.process.release?.name === 'node') { + return 'node' + } + // @ts-ignore -- NOTE: ignore, because development env is node.js + if (globalThis.Deno !== undefined) { + return 'deno' + } + // @ts-ignore -- NOTE: ignore, because development env is node.js + if (globalThis.Bun !== undefined) { + return 'bun' + } + return 'unknown' +} + +function quoteIfNeeded(path: string): string { + return path.includes(' ') ? `'${path}'` : path +} + +export function quoteExec(): string { + const runtime = detectRuntime() + switch (runtime) { + case 'node': { + // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` + const execPath = globalThis.process.execPath + // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` + const processArgs = globalThis.process.argv.slice(1) + const quotedExecPath = quoteIfNeeded(execPath) + // eslint-disable-next-line unicorn/no-array-callback-reference + const quotedProcessArgs = processArgs.map(quoteIfNeeded) + // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` + // eslint-disable-next-line unicorn/no-array-callback-reference + const quotedProcessExecArgs = globalThis.process.execArgv.map(quoteIfNeeded) + return `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}` + } + case 'deno': { + throw new Error('deno not implemented yet, welcome contributions :)') + } + case 'bun': { + throw new Error('deno not implemented yet, welcome contributions :)') + } + default: { + throw new Error('Unsupported your javascript runtime for completion script generation.') + } + } +} From e3d26350ba990f7fac4e9c9450cddaadf1d60a3f Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 10 Jul 2025 17:00:57 +0900 Subject: [PATCH 15/22] fix: support completion handler localization --- packages/plugin-completion/src/index.ts | 36 +++++++++++++++++++------ packages/plugin-completion/src/types.ts | 19 +++++++++++-- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/packages/plugin-completion/src/index.ts b/packages/plugin-completion/src/index.ts index 25422c057..3cdf11273 100644 --- a/packages/plugin-completion/src/index.ts +++ b/packages/plugin-completion/src/index.ts @@ -19,13 +19,18 @@ import type { PluginWithExtension } from '@gunshi/plugin' import type { I18nCommandContext } from '@gunshi/plugin-i18n' -import type { CompletionCommandContext, CompletionConfig, CompletionOptions } from './types.ts' +import type { + CompletionCommandContext, + CompletionConfig, + CompletionHandler, + CompletionOptions +} from './types.ts' export * from './types.ts' const TERMINATOR = '--' -const NOOP_HANDLER: Handler = () => { +const NOOP_HANDLER = () => { return [] } @@ -124,12 +129,14 @@ export default function completion( if (schema.type === 'positional') { continue // skip positional arguments on subcommands } - // TODO(kazupon): more tweaking for root option completion completion.addOption( root, `--${key}`, (await localizeDescription(resolveArgKey(key))) || schema.description || '', - config.entry?.args?.[key]?.handler || NOOP_HANDLER, + toBombshellCompletionHandler( + config.entry?.args?.[key]?.handler || NOOP_HANDLER, + i18n ? toLocale(i18n.locale) : undefined + ), schema.short ) } @@ -155,12 +162,14 @@ async function handleSubCommands( const localizeDescription = localizable(ctx, resolvedCmd, i18n ? i18n.translate : undefined) const isPositional = hasPositional(resolvedCmd) - // TODO(kazupon): more tweaking for subcommand completion const commandName = completion.addCommand( name, (await localizeDescription('description')) || resolvedCmd.description || '', isPositional ? [false] : [], - configs?.[name]?.handler || NOOP_HANDLER + toBombshellCompletionHandler( + configs?.[name]?.handler || NOOP_HANDLER, + i18n ? toLocale(i18n.locale) : undefined + ) ) const args = resolvedCmd.args || (Object.create(null) as Args) @@ -168,12 +177,14 @@ async function handleSubCommands( if (schema.type === 'positional') { continue // skip positional arguments on subcommands } - // TODO(kazupon): more tweaking for subcommand option completion completion.addOption( commandName, `--${key}`, (await localizeDescription(resolveArgKey(key))) || schema.description || '', - configs[commandName]?.args?.[key]?.handler || NOOP_HANDLER, + toBombshellCompletionHandler( + configs[commandName]?.args?.[key]?.handler || NOOP_HANDLER, + i18n ? toLocale(i18n.locale) : undefined + ), schema.short ) } @@ -183,3 +194,12 @@ async function handleSubCommands( function hasPositional(cmd: Command | LazyCommand) { return cmd.args && Object.values(cmd.args).some(arg => arg.type === 'positional') } + +function toLocale(locale: string | Intl.Locale): Intl.Locale { + return locale instanceof Intl.Locale ? locale : new Intl.Locale(locale) +} + +function toBombshellCompletionHandler(handler: CompletionHandler, locale?: Intl.Locale): Handler { + return (previousArgs, toComplete, endWithSpace) => + handler({ previousArgs, toComplete, endWithSpace, locale }) +} diff --git a/packages/plugin-completion/src/types.ts b/packages/plugin-completion/src/types.ts index 9fad999bb..37641b4ac 100644 --- a/packages/plugin-completion/src/types.ts +++ b/packages/plugin-completion/src/types.ts @@ -25,12 +25,27 @@ export type PluginId = typeof pluginId */ export interface CompletionCommandContext {} +/** + * Parameters for {@link CompletionHandler | the completion handler}. + */ +export interface CompletionParams { + previousArgs: Parameters[0] + toComplete: Parameters[1] + endWithSpace: Parameters[2] + locale?: Intl.Locale +} + +/** + * The handler for completion. + */ +export type CompletionHandler = (params: CompletionParams) => ReturnType + /** * Completion configuration, which structure is similar `bombsh/tab`'s `CompletionConfig`. */ export interface CompletionConfig { - handler?: Handler - args?: Record + handler?: CompletionHandler + args?: Record } /** From 98ebb832369c3cb87d58fa6e299724293caceed5 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 10 Jul 2025 23:22:25 +0900 Subject: [PATCH 16/22] Revert "fix: support completion handler localization" (because, plugin-i18n need more improvemnt for multiple command context env) This reverts commit e3d26350ba990f7fac4e9c9450cddaadf1d60a3f. --- packages/plugin-completion/src/index.ts | 36 ++++++------------------- packages/plugin-completion/src/types.ts | 19 ++----------- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/packages/plugin-completion/src/index.ts b/packages/plugin-completion/src/index.ts index 3cdf11273..25422c057 100644 --- a/packages/plugin-completion/src/index.ts +++ b/packages/plugin-completion/src/index.ts @@ -19,18 +19,13 @@ import type { PluginWithExtension } from '@gunshi/plugin' import type { I18nCommandContext } from '@gunshi/plugin-i18n' -import type { - CompletionCommandContext, - CompletionConfig, - CompletionHandler, - CompletionOptions -} from './types.ts' +import type { CompletionCommandContext, CompletionConfig, CompletionOptions } from './types.ts' export * from './types.ts' const TERMINATOR = '--' -const NOOP_HANDLER = () => { +const NOOP_HANDLER: Handler = () => { return [] } @@ -129,14 +124,12 @@ export default function completion( if (schema.type === 'positional') { continue // skip positional arguments on subcommands } + // TODO(kazupon): more tweaking for root option completion completion.addOption( root, `--${key}`, (await localizeDescription(resolveArgKey(key))) || schema.description || '', - toBombshellCompletionHandler( - config.entry?.args?.[key]?.handler || NOOP_HANDLER, - i18n ? toLocale(i18n.locale) : undefined - ), + config.entry?.args?.[key]?.handler || NOOP_HANDLER, schema.short ) } @@ -162,14 +155,12 @@ async function handleSubCommands( const localizeDescription = localizable(ctx, resolvedCmd, i18n ? i18n.translate : undefined) const isPositional = hasPositional(resolvedCmd) + // TODO(kazupon): more tweaking for subcommand completion const commandName = completion.addCommand( name, (await localizeDescription('description')) || resolvedCmd.description || '', isPositional ? [false] : [], - toBombshellCompletionHandler( - configs?.[name]?.handler || NOOP_HANDLER, - i18n ? toLocale(i18n.locale) : undefined - ) + configs?.[name]?.handler || NOOP_HANDLER ) const args = resolvedCmd.args || (Object.create(null) as Args) @@ -177,14 +168,12 @@ async function handleSubCommands( if (schema.type === 'positional') { continue // skip positional arguments on subcommands } + // TODO(kazupon): more tweaking for subcommand option completion completion.addOption( commandName, `--${key}`, (await localizeDescription(resolveArgKey(key))) || schema.description || '', - toBombshellCompletionHandler( - configs[commandName]?.args?.[key]?.handler || NOOP_HANDLER, - i18n ? toLocale(i18n.locale) : undefined - ), + configs[commandName]?.args?.[key]?.handler || NOOP_HANDLER, schema.short ) } @@ -194,12 +183,3 @@ async function handleSubCommands( function hasPositional(cmd: Command | LazyCommand) { return cmd.args && Object.values(cmd.args).some(arg => arg.type === 'positional') } - -function toLocale(locale: string | Intl.Locale): Intl.Locale { - return locale instanceof Intl.Locale ? locale : new Intl.Locale(locale) -} - -function toBombshellCompletionHandler(handler: CompletionHandler, locale?: Intl.Locale): Handler { - return (previousArgs, toComplete, endWithSpace) => - handler({ previousArgs, toComplete, endWithSpace, locale }) -} diff --git a/packages/plugin-completion/src/types.ts b/packages/plugin-completion/src/types.ts index 37641b4ac..9fad999bb 100644 --- a/packages/plugin-completion/src/types.ts +++ b/packages/plugin-completion/src/types.ts @@ -25,27 +25,12 @@ export type PluginId = typeof pluginId */ export interface CompletionCommandContext {} -/** - * Parameters for {@link CompletionHandler | the completion handler}. - */ -export interface CompletionParams { - previousArgs: Parameters[0] - toComplete: Parameters[1] - endWithSpace: Parameters[2] - locale?: Intl.Locale -} - -/** - * The handler for completion. - */ -export type CompletionHandler = (params: CompletionParams) => ReturnType - /** * Completion configuration, which structure is similar `bombsh/tab`'s `CompletionConfig`. */ export interface CompletionConfig { - handler?: CompletionHandler - args?: Record + handler?: Handler + args?: Record } /** From 47bb8b008ad629af130a94506694eaa6174faa0b Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 10 Jul 2025 23:33:51 +0900 Subject: [PATCH 17/22] fix: disable i18n for completion, it has still not ready yet --- packages/plugin-completion/package.json | 3 -- packages/plugin-completion/src/index.ts | 69 ++++++++++++++----------- packages/plugin-completion/src/utils.ts | 39 +++++++------- pnpm-lock.yaml | 3 -- 4 files changed, 58 insertions(+), 56 deletions(-) diff --git a/packages/plugin-completion/package.json b/packages/plugin-completion/package.json index 10aa73429..61b3ebe3b 100644 --- a/packages/plugin-completion/package.json +++ b/packages/plugin-completion/package.json @@ -61,9 +61,6 @@ "@bombsh/tab": "git+ssh://git@github.com:bombshell-dev/tab.git", "@gunshi/plugin": "workspace:*" }, - "peerDependencies": { - "@gunshi/plugin-i18n": "workspace:*" - }, "devDependencies": { "@gunshi/shared": "workspace:*", "deno": "catalog:", diff --git a/packages/plugin-completion/src/index.ts b/packages/plugin-completion/src/index.ts index 25422c057..16d830b63 100644 --- a/packages/plugin-completion/src/index.ts +++ b/packages/plugin-completion/src/index.ts @@ -5,20 +5,12 @@ import { Completion, script } from '@bombsh/tab' import { plugin } from '@gunshi/plugin' -import { localizable, namespacedId, resolveArgKey, resolveLazyCommand } from '@gunshi/shared' +import { resolveLazyCommand } from '@gunshi/shared' import { pluginId } from './types.ts' -import { createCommandContext, quoteExec } from './utils.ts' +import { quoteExec } from './utils.ts' import type { Handler } from '@bombsh/tab' -import type { - Args, - Command, - CommandContext, - LazyCommand, - PluginContext, - PluginWithExtension -} from '@gunshi/plugin' -import type { I18nCommandContext } from '@gunshi/plugin-i18n' +import type { Args, Command, LazyCommand, PluginContext, PluginWithExtension } from '@gunshi/plugin' import type { CompletionCommandContext, CompletionConfig, CompletionOptions } from './types.ts' export * from './types.ts' @@ -29,7 +21,8 @@ const NOOP_HANDLER: Handler = () => { return [] } -const i18nPluginId = namespacedId('i18n') +// NOTE(kazupon): we should use plugin-i18n for completion localization, but it is not ready yet. +// const i18nPluginId = namespacedId('i18n') /** * completion plugin for gunshi @@ -44,7 +37,8 @@ export default function completion( id: pluginId, name: 'completion', - dependencies: [{ id: i18nPluginId, optional: true }], + // NOTE(kazupon): disable dependencies for now, because plugin-i18n is not still ready yet for completion + // dependencies: [{ id: i18nPluginId, optional: true }], async setup(ctx) { /** @@ -91,10 +85,12 @@ export default function completion( * setup bombshell completion with `onExtension` hook */ - onExtension: async (ctx, cmd) => { + onExtension: async (ctx, _cmd) => { // TODO(kazupon): type inference with plugin function type parameter, more improvements! - const extensions = ctx.extensions as unknown as { [i18nPluginId]: I18nCommandContext } - const i18n = extensions[i18nPluginId] + + // NOTE(kazupon): we should use plugin-i18n for completion localization, but it is not ready yet. + // const extensions = ctx.extensions as unknown as { [i18nPluginId]: I18nCommandContext } + // const i18n = extensions[i18nPluginId] const subCommands = ctx.env.subCommands as ReadonlyMap const entry = [...subCommands].map(([_, cmd]) => cmd).find(cmd => cmd.entry) @@ -102,19 +98,22 @@ export default function completion( throw new Error('entry command not found.') } - const entryCtx = await createCommandContext(entry) - const localizeDescription = localizable( - entryCtx as unknown as CommandContext, - cmd, - i18n ? i18n.translate : undefined - ) + // NOTE(kazupon): we should use localizeDescription here, but it is not ready yet. + // const entryCtx = await createCommandContext(entry) + // const localizeDescription = localizable( + // entryCtx as unknown as CommandContext, + // cmd, + // i18n ? i18n.translate : undefined + // ) // setup root level completion const isPositional = hasPositional(await resolveLazyCommand(entry as Command)) const root = '' completion.addCommand( root, - (await localizeDescription('description')) || entry.description || '', + entry.description || '', + // NOTE(kazupon): we should use localizeDescription here, but it is not ready yet. + // (await localizeDescription('description')) || entry.description || '', isPositional ? [false] : [], NOOP_HANDLER ) @@ -128,13 +127,15 @@ export default function completion( completion.addOption( root, `--${key}`, - (await localizeDescription(resolveArgKey(key))) || schema.description || '', + schema.description || '', + // NOTE(kazupon): we should use localizeDescription here, but it is not ready yet. + // (await localizeDescription(resolveArgKey(key))) || schema.description || '', config.entry?.args?.[key]?.handler || NOOP_HANDLER, schema.short ) } - await handleSubCommands(completion, subCommands, config.subCommands, i18n) + await handleSubCommands(completion, subCommands, config.subCommands /* , i18n*/) } }) } @@ -142,8 +143,9 @@ export default function completion( async function handleSubCommands( completion: Completion, subCommands: PluginContext['subCommands'], - configs: Record = {}, - i18n?: I18nCommandContext | undefined + configs: Record = {} + // NOTE(kazupon): we should use i18n for subcommand localization, but it is not ready yet. + // i18n?: I18nCommandContext | undefined ) { for (const [name, cmd] of subCommands) { if (cmd.internal || cmd.entry || name === 'complete') { @@ -151,14 +153,17 @@ async function handleSubCommands( } const resolvedCmd = await resolveLazyCommand(cmd) - const ctx = await createCommandContext(resolvedCmd) - const localizeDescription = localizable(ctx, resolvedCmd, i18n ? i18n.translate : undefined) + // NOTE(kazupon): we should use localizeDescription here, but it is not ready yet. + // const ctx = await createCommandContext(resolvedCmd) + // const localizeDescription = localizable(ctx, resolvedCmd, i18n ? i18n.translate : undefined) const isPositional = hasPositional(resolvedCmd) // TODO(kazupon): more tweaking for subcommand completion const commandName = completion.addCommand( name, - (await localizeDescription('description')) || resolvedCmd.description || '', + resolvedCmd.description || '', + // NOTE(kazupon): we should use localizeDescription here, but it is not ready yet. + // (await localizeDescription('description')) || resolvedCmd.description || '', isPositional ? [false] : [], configs?.[name]?.handler || NOOP_HANDLER ) @@ -172,7 +177,9 @@ async function handleSubCommands( completion.addOption( commandName, `--${key}`, - (await localizeDescription(resolveArgKey(key))) || schema.description || '', + schema.description || '', + // NOTE(kazupon): we should use localizeDescription here, but it is not ready yet. + // (await localizeDescription(resolveArgKey(key))) || schema.description || '', configs[commandName]?.args?.[key]?.handler || NOOP_HANDLER, schema.short ) diff --git a/packages/plugin-completion/src/utils.ts b/packages/plugin-completion/src/utils.ts index f3c73e01f..302d0358a 100644 --- a/packages/plugin-completion/src/utils.ts +++ b/packages/plugin-completion/src/utils.ts @@ -3,25 +3,26 @@ * @license MIT */ -import { CLI_OPTIONS_DEFAULT, createCommandContext as _createCommandContext } from '@gunshi/plugin' - -import type { Args, Command, CommandContext, LazyCommand } from '@gunshi/plugin' - -export async function createCommandContext(cmd: Command | LazyCommand): Promise { - return await _createCommandContext({ - args: cmd.args || (Object.create(null) as Args), - values: Object.create(null), - positionals: [], - rest: [], - argv: [], - tokens: [], - omitted: false, - callMode: cmd.entry ? 'entry' : 'subCommand', - command: cmd, - extensions: Object.create(null), - cliOptions: CLI_OPTIONS_DEFAULT - }) -} +// NOTE(kazupon): comment out, because it is not used yet. +// import { CLI_OPTIONS_DEFAULT, createCommandContext as _createCommandContext } from '@gunshi/plugin' +// +// import type { Args, Command, CommandContext, LazyCommand } from '@gunshi/plugin' +// +// async function createCommandContext(cmd: Command | LazyCommand): Promise { +// return await _createCommandContext({ +// args: cmd.args || (Object.create(null) as Args), +// values: Object.create(null), +// positionals: [], +// rest: [], +// argv: [], +// tokens: [], +// omitted: false, +// callMode: cmd.entry ? 'entry' : 'subCommand', +// command: cmd, +// extensions: Object.create(null), +// cliOptions: CLI_OPTIONS_DEFAULT +// }) +// } function detectRuntime(): 'bun' | 'deno' | 'node' | 'unknown' { // @ts-ignore -- NOTE: ignore, because `process` will detect ts compile error on `deno check` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98e5d8ae8..dc2499bfa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -282,9 +282,6 @@ importers: '@gunshi/plugin': specifier: workspace:* version: link:../plugin - '@gunshi/plugin-i18n': - specifier: workspace:* - version: link:../plugin-i18n devDependencies: '@gunshi/shared': specifier: workspace:* From 2aa2796029ddb73dd122d014c36f9185b1bb8bf7 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Thu, 10 Jul 2025 23:50:03 +0900 Subject: [PATCH 18/22] fix --- packages/plugin-completion/package.json | 3 ++- packages/shared/jsr.json | 1 + pnpm-lock.yaml | 12 ++++-------- scripts/jsr.ts | 4 ++-- tsconfig.json | 1 + 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/plugin-completion/package.json b/packages/plugin-completion/package.json index 61b3ebe3b..a4c335c60 100644 --- a/packages/plugin-completion/package.json +++ b/packages/plugin-completion/package.json @@ -55,7 +55,7 @@ "build": "tsdown", "lint:jsr": "jsr publish --dry-run --allow-dirty", "prepack": "pnpm build", - "typecheck:deno": "deno check ./src" + "typecheck:deno": "deno check --import-map=../../importmap.json ./src" }, "dependencies": { "@bombsh/tab": "git+ssh://git@github.com:bombshell-dev/tab.git", @@ -63,6 +63,7 @@ }, "devDependencies": { "@gunshi/shared": "workspace:*", + "@types/node": "catalog:", "deno": "catalog:", "jsr": "catalog:", "jsr-exports-lint": "catalog:", diff --git a/packages/shared/jsr.json b/packages/shared/jsr.json index d4210c0d9..a09b5c4fd 100644 --- a/packages/shared/jsr.json +++ b/packages/shared/jsr.json @@ -8,6 +8,7 @@ "publish": { "include": ["src/**/*.ts", "package.json", "jsr.json", "README.md", "CHANGELOG.md", "LICENSE"], "exclude": [ + "examples/**/*.ts", "src/**/*.test.ts", "src/**/*.test-d.ts", "**/__snapshots__/**", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc2499bfa..0875d7d5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,6 +286,9 @@ importers: '@gunshi/shared': specifier: workspace:* version: link:../shared + '@types/node': + specifier: 'catalog:' + version: 22.16.0 deno: specifier: 'catalog:' version: 2.4.0 @@ -1919,9 +1922,6 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@22.15.29': - resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} - '@types/node@22.16.0': resolution: {integrity: sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==} @@ -6138,10 +6138,6 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@22.15.29': - dependencies: - undici-types: 6.21.0 - '@types/node@22.16.0': dependencies: undici-types: 6.21.0 @@ -6753,7 +6749,7 @@ snapshots: bun-types@1.2.18(@types/react@19.1.8): dependencies: - '@types/node': 22.15.29 + '@types/node': 22.16.0 '@types/react': 19.1.8 byte-size@9.0.1: {} diff --git a/scripts/jsr.ts b/scripts/jsr.ts index a2f94c38d..4fa9f80fc 100644 --- a/scripts/jsr.ts +++ b/scripts/jsr.ts @@ -47,6 +47,7 @@ function updatePkgJson(pkg: string, json: Record): Record): Record Date: Thu, 10 Jul 2025 23:52:58 +0900 Subject: [PATCH 19/22] fix: knip error --- knip.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/knip.config.ts b/knip.config.ts index 277bb1848..f3e619a75 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -9,6 +9,9 @@ export default { 'packages/gunshi': { entry: ['src/constants.ts'] }, + 'packages/plugin-completion': { + ignore: ['examples/**/*.ts'] + }, 'packages/docs': { entry: ['src/.vitepress/config.ts', 'src/.vitepress/theme/index.ts'] } From b359fc6e5a78e0317b5964189e68b3cac820ed47 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Fri, 11 Jul 2025 14:16:31 +0900 Subject: [PATCH 20/22] fix: adjust for deno --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 59562be74..d2fc4341f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -46,7 +46,8 @@ "@gunshi/plugin-renderer": ["./packages/plugin-renderer/src/index.ts"], "@gunshi/plugin-completion": ["./packages/plugin-completion/src/index.ts"], "@gunshi/plugin-i18n": ["./packages/plugin-i18n/src/index.ts"], - "@gunshi/plugin-global": ["./packages/plugin-global/src/index.ts"] + "@gunshi/plugin-global": ["./packages/plugin-global/src/index.ts"], + "@bombsh/tab": ["./packages/plugin-completion/node_modules/@bombsh/tab"] } /* Specify a set of entries that re-map imports to additional lookup locations. */, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ From 5601e31d9fc79164735c1d3e609ae052482f8483 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Fri, 11 Jul 2025 14:27:26 +0900 Subject: [PATCH 21/22] fix: typo --- packages/plugin-completion/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-completion/src/utils.ts b/packages/plugin-completion/src/utils.ts index 302d0358a..6ddf72e7e 100644 --- a/packages/plugin-completion/src/utils.ts +++ b/packages/plugin-completion/src/utils.ts @@ -64,7 +64,7 @@ export function quoteExec(): string { throw new Error('deno not implemented yet, welcome contributions :)') } case 'bun': { - throw new Error('deno not implemented yet, welcome contributions :)') + throw new Error('bun not implemented yet, welcome contributions :)') } default: { throw new Error('Unsupported your javascript runtime for completion script generation.') From 55e5a98acd250361f5e86a2cef54711592517d7d Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Tue, 15 Jul 2025 17:39:49 +0900 Subject: [PATCH 22/22] fix: change to pkg.pr.new using --- packages/plugin-completion/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- tsconfig.json | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/plugin-completion/package.json b/packages/plugin-completion/package.json index a4c335c60..9cd9dae1d 100644 --- a/packages/plugin-completion/package.json +++ b/packages/plugin-completion/package.json @@ -58,7 +58,7 @@ "typecheck:deno": "deno check --import-map=../../importmap.json ./src" }, "dependencies": { - "@bombsh/tab": "git+ssh://git@github.com:bombshell-dev/tab.git", + "@bombsh/tab": "https://pkg.pr.new/bombshell-dev/tab@main", "@gunshi/plugin": "workspace:*" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0875d7d5e..0add6303e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -277,8 +277,8 @@ importers: packages/plugin-completion: dependencies: '@bombsh/tab': - specifier: git+ssh://git@github.com:bombshell-dev/tab.git - version: tab@https://codeload.github.com/bombshell-dev/tab/tar.gz/6c42c55f46157ae97e2c059039e21ee412b48138 + specifier: https://pkg.pr.new/bombshell-dev/tab@main + version: tab@https://pkg.pr.new/bombshell-dev/tab@main '@gunshi/plugin': specifier: workspace:* version: link:../plugin @@ -4503,8 +4503,8 @@ packages: resolution: {integrity: sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==} engines: {node: ^14.18.0 || >=16.0.0} - tab@https://codeload.github.com/bombshell-dev/tab/tar.gz/6c42c55f46157ae97e2c059039e21ee412b48138: - resolution: {tarball: https://codeload.github.com/bombshell-dev/tab/tar.gz/6c42c55f46157ae97e2c059039e21ee412b48138} + tab@https://pkg.pr.new/bombshell-dev/tab@main: + resolution: {tarball: https://pkg.pr.new/bombshell-dev/tab@main} version: 0.0.0 tabbable@6.2.0: @@ -9265,7 +9265,7 @@ snapshots: dependencies: '@pkgr/core': 0.2.4 - tab@https://codeload.github.com/bombshell-dev/tab/tar.gz/6c42c55f46157ae97e2c059039e21ee412b48138: + tab@https://pkg.pr.new/bombshell-dev/tab@main: dependencies: mri: 1.2.0 diff --git a/tsconfig.json b/tsconfig.json index d2fc4341f..59562be74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -46,8 +46,7 @@ "@gunshi/plugin-renderer": ["./packages/plugin-renderer/src/index.ts"], "@gunshi/plugin-completion": ["./packages/plugin-completion/src/index.ts"], "@gunshi/plugin-i18n": ["./packages/plugin-i18n/src/index.ts"], - "@gunshi/plugin-global": ["./packages/plugin-global/src/index.ts"], - "@bombsh/tab": ["./packages/plugin-completion/node_modules/@bombsh/tab"] + "@gunshi/plugin-global": ["./packages/plugin-global/src/index.ts"] } /* Specify a set of entries that re-map imports to additional lookup locations. */, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */