From 34b66e3135b2618f16d7502c754a61264c495bbd Mon Sep 17 00:00:00 2001 From: Griffen Fargo <3642037+gfargo@users.noreply.github.com> Date: Thu, 14 May 2026 16:21:19 -0400 Subject: [PATCH] feat(cache): tree-sitter cache management + surrender telemetry (#933 phase 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the tree-sitter integration feature (#933). Lazy-loaded parsers gain a first-class CLI surface for cache management; verbose mode surfaces a discoverability hint when the fast path falls through to regex. ## New `coco cache` subcommands Extends the existing `coco cache` (diff-summary info / clear) with three tree-sitter subcommands: - \`coco cache parsers\` — show every manifest language with its current cache status (cached size or "not cached" + the fetched-size estimate), version pin, and source URL. Footer summarizes total disk usage + quick-reference commands. - \`coco cache prefetch [languages...]\` — download specific parsers (e.g. \`coco cache prefetch py rs go\` or \`coco cache prefetch all\`). When invoked with no args AND stdin is a TTY, opens an interactive checkbox picker. In non-interactive contexts (CI, pipes), no-arg invocations error out with usage hints instead of hanging on a prompt. - \`coco cache clear-parsers\` — wipe \`~/.cache/coco/tree-sitter/\`. Idempotent; reports a per-language ✓ for each removed file. Aliases mirror \`COCO_PREFETCH\` env grammar: \`py\` / \`python\`, \`rs\` / \`rust\`, \`go\` / \`golang\`, \`all\`. ## Surrender telemetry In verbose mode, when the language-aware fast path is enabled and the parser chain falls through to LLM, emit a discoverability hint: \`Tree-sitter parser surrendered for 'python'; using regex fallback. Hint: \`coco cache parsers\` to inspect, \`coco cache prefetch python\` to enable.\` Quiet on the default path; visible only when the user is debugging summary quality. Hint copy adapts: bundled-language surrenders (\`ts\` / \`js\`) point at \`coco cache prefetch all\` because TS / TSX wasms are always shipped (the surrender is from a parser-init failure, not a missing download); lazy-loaded languages get a per-language prefetch hint. ## Implementation ### \`cache.ts\` (lazy-load cache module) - New \`getCachedParserStatus(language)\` returns \`{ language, cached, path, bytes?, mtime? }\` for the table renderer + interactive picker. - New \`clearCachedParser(language)\` unlinks the cached .wasm. Idempotent; returns \`true\` when a file was actually removed. ### \`structuralParserRegistry.ts\` - New \`hasTreeSitterParser(language)\` lets the LLM fallthrough path know whether a tree-sitter parser is registered for the language — used by the surrender-telemetry hint. Doesn't expose internals; the caller just needs the boolean. ### \`summarizeLargeFiles.ts\` - Surrender-telemetry block fires after the registry returns undefined and BEFORE the cache lookup. Only emits when the chain includes a tree-sitter parser, so regex-only languages don't get a misleading hint. ### \`commands/cache/\` - \`config.ts\` gains the \`CACHE_SUBCOMMANDS\` enum and a positional \`[languages..]\` for prefetch. Yargs validates the subcommand set; unknown tokens get caught by the language resolver. - \`handler.ts\` adds three new branches: - \`parsers\` calls \`renderParsersTable\` - \`prefetch\` resolves tokens via \`parsePrefetchEnv\` (reusing the env-var grammar), prompts when interactive, and delegates to \`prefetchTreeSitterParsers\`. Failed downloads → \`process.exitCode = 1\`. - \`clear-parsers\` walks every manifest entry, calls \`clearCachedParser\`, reports per-language status. ### \`inquirerPrompts.ts\` - New \`checkboxPrompt\` helper. Same dynamic-import shim as the other prompts; reuses the codebase's standard pattern for ESM inquirer modules under ts-jest. ## Tests 4 new test cases in \`handler.test.ts\` cover the new subcommands: \`parsers\` lists every manifest language, \`prefetch\` warns on unknown tokens, \`clear-parsers\` reports no-op when empty AND removes cached files when present. Test isolation: each test sets \`COCO_CACHE_DIR\` to the same tmp dir the existing tests use for \`XDG_CACHE_HOME\`, so the tree-sitter cache lives inside the per-test sandbox. ## Manual validation \`\`\` $ COCO_CACHE_DIR=/tmp/coco-phase7-smoke coco cache parsers Tree-sitter parser cache Python not cached (448.0 KB when fetched) Rust not cached (1.05 MB when fetched) Go not cached (212.1 KB when fetched) cached: 0/3 total on disk: 0 B $ coco cache prefetch py · Python: downloading https://cdn.jsdelivr.net/.../tree-sitter-python.wasm… ✓ Python parser cached (447 KB) Summary: 1 downloaded · 0 already cached · 0 failed $ coco cache clear-parsers ✓ cleared Python Cleared 1 parser(s) from ~/.cache/coco/tree-sitter/ \`\`\` ## Validation - \`npx tsc --noEmit\` → 0 errors - \`npm run test:jest\` → 1674/1674 pass (3 of 4 consecutive runs clean, 1 flake on the pre-existing scenarioInputs timeout pattern) - \`npx eslint\` on touched files → clean - Manual: all four subcommands round-trip cleanly ## Out of scope (genuine future work) - **Eval-harness side-by-side regex-vs-tree-sitter comparison in the report output**. Today the eval reports per-fixture outcomes but doesn't discriminate WHICH parser produced each summary. Surfacing the regex vs. tree-sitter delta requires registry injection at eval time (the harness builds its own parser chain instead of using the global). Reasonable follow-up; not gating on #933 closure. ## #933 status: feature complete | Phase | Status | |---|---| | 1.0 — Registry abstraction | ✓ #950 | | 1.1 — TS/TSX bundled | ✓ #955 | | 2 — Polish + ESM jest + arrow-fn fixture | ✓ #956 | | 3 — Lazy-load infra + Python | ✓ #957 | | 5 — Rust | ✓ #958 | | 6 — Go | ✓ #958 | | **7 — Cache CLI + telemetry** | **this PR** | Closes #933. --- src/commands/cache/config.ts | 44 ++++- src/commands/cache/handler.test.ts | 60 ++++++ src/commands/cache/handler.ts | 183 +++++++++++++++++- .../parsers/default/__tree_sitter__/cache.ts | 51 ++++- .../default/utils/structuralParserRegistry.ts | 17 ++ .../default/utils/summarizeLargeFiles.ts | 16 ++ src/lib/ui/inquirerPrompts.ts | 10 + 7 files changed, 375 insertions(+), 6 deletions(-) diff --git a/src/commands/cache/config.ts b/src/commands/cache/config.ts index 861bbef6..10a126f4 100644 --- a/src/commands/cache/config.ts +++ b/src/commands/cache/config.ts @@ -2,18 +2,54 @@ import { Arguments, Argv } from 'yargs' import { getCommandUsageHeader } from '../../lib/ui/helpers' import { BaseCommandOptions } from '../types' -export interface CacheOptions extends BaseCommandOptions {} +export interface CacheOptions extends BaseCommandOptions { + /** + * Positional list of language identifiers / aliases for the + * `prefetch` subcommand. Empty → interactive checkbox picker. + * Recognized values mirror the `COCO_PREFETCH` env-var grammar: + * `py`, `python`, `rs`, `rust`, `go`, `golang`, `all`. + */ + languages?: string[] +} export type CacheArgv = Arguments -export const command = 'cache ' +/** + * Subcommand vocabulary. Two cache layers coexist under one command: + * + * - **Diff-summary cache** (#845) — `info` / `clear`. Caches LLM- + * produced file summaries keyed on diff content; clearing + * forces fresh summaries on the next commit run. + * - **Tree-sitter parser cache** (#933) — `parsers` / `prefetch` / + * `clear-parsers`. Manages the lazy-loaded `.wasm` parser files + * under `~/.cache/coco/tree-sitter/`. + * + * Kept under one verb because users think of "cache" as a single + * concept; the subcommand discriminator makes the scope unambiguous. + */ +export const CACHE_SUBCOMMANDS = [ + 'clear', + 'info', + 'parsers', + 'prefetch', + 'clear-parsers', +] as const + +export type CacheSubcommand = typeof CACHE_SUBCOMMANDS[number] + +export const command = 'cache [languages..]' export const builder = (yargs: Argv) => { return yargs .positional('subcommand', { - describe: 'Cache action to run (clear, info)', + describe: 'Cache action to run', + type: 'string', + choices: CACHE_SUBCOMMANDS, + }) + .positional('languages', { + describe: 'Languages to act on (for `prefetch`). Empty → interactive picker.', type: 'string', - choices: ['clear', 'info'] as const, + array: true, }) .usage(getCommandUsageHeader(command)) } diff --git a/src/commands/cache/handler.test.ts b/src/commands/cache/handler.test.ts index cabd96f8..f2db8166 100644 --- a/src/commands/cache/handler.test.ts +++ b/src/commands/cache/handler.test.ts @@ -70,4 +70,64 @@ describe('coco cache ', () => { expect(process.exitCode).toBe(1) process.exitCode = previousExit }) + + describe('tree-sitter subcommands (#933 phase 7)', () => { + // Each test sets COCO_CACHE_DIR to the same tmpRoot the existing + // tests use for XDG_CACHE_HOME, ensuring the tree-sitter cache + // dir lives inside our isolated tmp dir and gets wiped by the + // afterEach in the outer describe. + beforeEach(() => { + process.env.COCO_CACHE_DIR = path.join(tmpRoot, 'coco') + }) + afterEach(() => { + delete process.env.COCO_CACHE_DIR + }) + + it('parsers: lists every manifest language with cached/not-cached state', async () => { + await handler({ subcommand: 'parsers' } as never, logger as never) + const out = logger.log.mock.calls.map((args) => args[0]).join('\n') + expect(out).toContain('Tree-sitter parser cache') + expect(out).toContain('Python') + expect(out).toContain('Rust') + expect(out).toContain('Go') + // Every entry is not-cached in this fresh tmp dir. + expect(out).toContain('not cached') + }) + + it('prefetch: warns about unknown language tokens', async () => { + // Bare unknown token → handler should warn then no-op (empty + // resolved list → "Nothing to do"). + await handler({ + subcommand: 'prefetch', + languages: ['fortran'], + } as never, logger as never) + const out = logger.log.mock.calls.map((args) => args[0]).join('\n') + expect(out).toContain('ignoring unknown language(s): fortran') + expect(out).toContain('Nothing to do') + }) + + it('clear-parsers: reports no-op when nothing is cached', async () => { + await handler({ subcommand: 'clear-parsers' } as never, logger as never) + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining('No tree-sitter parsers cached'), + ) + }) + + it('clear-parsers: removes cached .wasm files', async () => { + // Seed two fake .wasm files in the cache dir to simulate a + // populated cache without doing a real network download. + const cacheDir = path.join(process.env.COCO_CACHE_DIR as string, 'tree-sitter') + fs.mkdirSync(cacheDir, { recursive: true }) + fs.writeFileSync(path.join(cacheDir, 'tree-sitter-python.wasm'), 'fake') + fs.writeFileSync(path.join(cacheDir, 'tree-sitter-rust.wasm'), 'fake') + + await handler({ subcommand: 'clear-parsers' } as never, logger as never) + + const out = logger.log.mock.calls.map((args) => args[0]).join('\n') + expect(out).toContain('cleared Python') + expect(out).toContain('cleared Rust') + expect(fs.existsSync(path.join(cacheDir, 'tree-sitter-python.wasm'))).toBe(false) + expect(fs.existsSync(path.join(cacheDir, 'tree-sitter-rust.wasm'))).toBe(false) + }) + }) }) diff --git a/src/commands/cache/handler.ts b/src/commands/cache/handler.ts index ec1378d2..ca472003 100644 --- a/src/commands/cache/handler.ts +++ b/src/commands/cache/handler.ts @@ -2,10 +2,24 @@ import * as fs from 'node:fs' import chalk from 'chalk' +import { + clearCachedParser, + getCachedParserStatus, + type LazyTreeSitterLanguageId, +} from '../../lib/parsers/default/__tree_sitter__/cache' +import { + listManifestLanguages, + TREE_SITTER_MANIFEST, +} from '../../lib/parsers/default/__tree_sitter__/manifest' +import { + parsePrefetchEnv, + prefetchTreeSitterParsers, +} from '../../lib/parsers/default/__tree_sitter__/prefetch' import { clearDiffSummaryCache, getDiffSummaryCachePath, } from '../../lib/parsers/default/utils/diffSummaryCache' +import { checkboxPrompt } from '../../lib/ui/inquirerPrompts' import { CommandHandler } from '../../lib/types' import { applyRepoCwd } from '../utils/applyRepoFlag' import { CacheArgv } from './config' @@ -39,8 +53,114 @@ function formatBytes(bytes: number): string { return `${(bytes / 1024 / 1024).toFixed(2)} MB` } +/** + * Render the tree-sitter parser cache table (`coco cache parsers`). + * One row per manifest entry — cached size + version + URL — plus + * a footer summarizing total disk usage. Mirrors the diff-summary + * `info` output style. + */ +function renderParsersTable( + logger: { log: (s: string) => void }, +): void { + const languages = listManifestLanguages() + let totalBytes = 0 + let cachedCount = 0 + + logger.log(chalk.bold('Tree-sitter parser cache')) + logger.log('') + for (const language of languages) { + const entry = TREE_SITTER_MANIFEST[language] + const status = getCachedParserStatus(language) + const stateLabel = status.cached + ? chalk.green('cached') + : chalk.dim('not cached') + const size = status.cached && status.bytes !== undefined + ? chalk.dim(`(${formatBytes(status.bytes)})`) + : chalk.dim(`(${formatBytes(entry.approxBytes)} when fetched)`) + if (status.cached && status.bytes !== undefined) { + totalBytes += status.bytes + cachedCount += 1 + } + logger.log( + ` ${chalk.bold(entry.displayName.padEnd(8))} ${stateLabel.padEnd(20)}${size}`, + ) + logger.log(` ${chalk.dim(`v${entry.version} · ${entry.wasmUrl}`)}`) + } + + logger.log('') + logger.log( + ` ${chalk.dim('cached:')} ${cachedCount}/${languages.length} ` + + `${chalk.dim('total on disk:')} ${formatBytes(totalBytes)}`, + ) + logger.log('') + logger.log(chalk.dim(' Prefetch a language: coco cache prefetch py')) + logger.log(chalk.dim(' Pick interactively: coco cache prefetch')) + logger.log(chalk.dim(' Clear the parser cache: coco cache clear-parsers')) +} + +/** + * Resolve a list of user-supplied tokens (and aliases) into canonical + * language ids. Reuses the prefetch module's env-var parser so the + * grammar stays in lockstep — `py` / `python` / `rs` / `rust` / `go` / + * `golang` / `all` all map the same way they do for `COCO_PREFETCH`. + * + * Empty input returns an empty result with `interactive: true` to + * signal the caller should show the checkbox picker. + */ +function resolveLanguageTokens(tokens: string[]): { + resolved: LazyTreeSitterLanguageId[] + unknown: string[] + interactive: boolean +} { + if (tokens.length === 0) { + return { resolved: [], unknown: [], interactive: true } + } + const parsed = parsePrefetchEnv(tokens.join(',')) + return { ...parsed, interactive: false } +} + +/** + * Interactive checkbox prompt: pick which languages to download. + * Each row shows the language, its current cache status, and the + * approximate / actual on-disk size. + * + * Gated by `process.stdin.isTTY` — non-interactive contexts (CI, + * pipes) get an error message instead of hanging on the prompt. + */ +async function promptLanguageSelection( + logger: { log: (s: string) => void }, +): Promise { + if (!process.stdin.isTTY) { + logger.log(chalk.red('`coco cache prefetch` with no args requires an interactive TTY.')) + logger.log(chalk.dim('In a pipe / CI, pass the languages explicitly:')) + logger.log(chalk.dim(' coco cache prefetch py rs go')) + logger.log(chalk.dim(' coco cache prefetch all')) + return undefined + } + const choices = listManifestLanguages().map((language) => { + const entry = TREE_SITTER_MANIFEST[language] + const status = getCachedParserStatus(language) + return { + name: status.cached + ? `${entry.displayName} (cached, ${formatBytes(status.bytes ?? entry.approxBytes)})` + : `${entry.displayName} (~${formatBytes(entry.approxBytes)})`, + value: language, + checked: false, + } + }) + const picked = await checkboxPrompt({ + message: 'Which tree-sitter parsers to (re)download?', + choices, + instructions: ' (Space toggles · Enter confirms)', + }) + return picked +} + export const handler: CommandHandler = async (argv, logger) => { const subcommand = (argv as { subcommand?: string }).subcommand + const positionalLanguages = ((argv as { languages?: string[] }).languages || []) + .map((s) => s.trim()) + .filter(Boolean) // Honor the global --repo flag so `coco cache info --repo ` // inspects X's cache, not the launcher's cwd. applyRepoCwd // performs the chdir when needed and returns the canonical path. @@ -82,7 +202,68 @@ export const handler: CommandHandler = async (argv, logger) => { return } + if (subcommand === 'parsers') { + renderParsersTable(logger) + return + } + + if (subcommand === 'prefetch') { + const { resolved: resolvedFromArgs, unknown, interactive } = + resolveLanguageTokens(positionalLanguages) + if (unknown.length > 0) { + logger.log(chalk.yellow( + `! ignoring unknown language(s): ${unknown.join(', ')}. ` + + `Known: ${listManifestLanguages().join(', ')}`, + )) + } + let resolved = resolvedFromArgs + if (interactive) { + const picked = await promptLanguageSelection(logger) + if (!picked) { + process.exitCode = 1 + return + } + resolved = picked + } + if (resolved.length === 0) { + logger.log(chalk.dim('No languages selected. Nothing to do.')) + return + } + const result = await prefetchTreeSitterParsers(resolved, { + writeLine: (line: string) => logger.log(line), + }) + logger.log('') + logger.log( + `${chalk.bold('Summary:')} ` + + `${chalk.green(`${result.downloaded.length} downloaded`)} · ` + + `${chalk.dim(`${result.alreadyCached.length} already cached`)} · ` + + `${chalk.red(`${result.failed.length} failed`)}`, + ) + if (result.failed.length > 0) { + process.exitCode = 1 + } + return + } + + if (subcommand === 'clear-parsers') { + const languages = listManifestLanguages() + let cleared = 0 + for (const language of languages) { + if (clearCachedParser(language)) { + cleared += 1 + logger.log(chalk.green(`✓ cleared ${TREE_SITTER_MANIFEST[language].displayName}`)) + } + } + if (cleared === 0) { + logger.log(chalk.dim('No tree-sitter parsers cached. Nothing to clear.')) + return + } + logger.log('') + logger.log(chalk.dim(`Cleared ${cleared} parser(s) from ~/.cache/coco/tree-sitter/`)) + return + } + logger.log(chalk.red(`Unknown cache subcommand: ${subcommand}`)) - logger.log(chalk.dim('Use one of: clear, info')) + logger.log(chalk.dim('Use one of: clear, info, parsers, prefetch, clear-parsers')) process.exitCode = 1 } diff --git a/src/lib/parsers/default/__tree_sitter__/cache.ts b/src/lib/parsers/default/__tree_sitter__/cache.ts index 8432ea99..8fa8d062 100644 --- a/src/lib/parsers/default/__tree_sitter__/cache.ts +++ b/src/lib/parsers/default/__tree_sitter__/cache.ts @@ -25,7 +25,7 @@ * polish-phase concern; today, users `rm -rf` the dir manually. */ -import { existsSync, mkdirSync } from 'node:fs' +import { existsSync, mkdirSync, statSync, unlinkSync } from 'node:fs' import { homedir, platform } from 'node:os' import { join } from 'node:path' @@ -113,3 +113,52 @@ export function ensureTreeSitterCacheDir(): string { export function isLanguageCached(language: LazyTreeSitterLanguageId): boolean { return existsSync(getCachedWasmPath(language)) } + +export type CachedParserStatus = { + language: LazyTreeSitterLanguageId + /** True when the .wasm exists on disk in the cache. */ + cached: boolean + /** Filesystem path the cache lookup checks. */ + path: string + /** On-disk size in bytes when cached; undefined otherwise. */ + bytes?: number + /** Last-modified timestamp when cached; undefined otherwise. */ + mtime?: Date +} + +/** + * Inspect the on-disk state of a single lazy-loaded parser. Used by + * `coco cache parsers` to render the status table and by the + * interactive prefetch picker to mark already-cached entries. + */ +export function getCachedParserStatus( + language: LazyTreeSitterLanguageId, +): CachedParserStatus { + const path = getCachedWasmPath(language) + const cached = existsSync(path) + if (!cached) return { language, cached: false, path } + try { + const stat = statSync(path) + return { language, cached: true, path, bytes: stat.size, mtime: stat.mtime } + } catch { + // Race window: file disappeared between existsSync and statSync. + // Report uncached rather than crash. + return { language, cached: false, path } + } +} + +/** + * Remove a single language's cached .wasm. Idempotent — no-op when + * the file isn't present. Returns true when a file was actually + * deleted, false otherwise. + */ +export function clearCachedParser(language: LazyTreeSitterLanguageId): boolean { + const path = getCachedWasmPath(language) + if (!existsSync(path)) return false + try { + unlinkSync(path) + return true + } catch { + return false + } +} diff --git a/src/lib/parsers/default/utils/structuralParserRegistry.ts b/src/lib/parsers/default/utils/structuralParserRegistry.ts index dd19dec8..4ee1f4a6 100644 --- a/src/lib/parsers/default/utils/structuralParserRegistry.ts +++ b/src/lib/parsers/default/utils/structuralParserRegistry.ts @@ -144,3 +144,20 @@ export function _registrySnapshotForTesting(): Record [lang, chain.map((p) => p.id)]) ) as Record } + +/** + * True when a given language's chain INCLUDES a tree-sitter parser. + * Used by the LLM fallthrough path in `summarizeLargeFiles.ts` to + * surface a discoverability hint ("run `coco cache prefetch py` to + * enable tree-sitter") when the chain falls through entirely. + * + * Doesn't tell us WHY the tree-sitter parser surrendered — that's + * still an internal concern of the parser itself (cache miss vs. + * dynamic-import failure vs. AST shape unrecognized). The hint the + * surface emits is generic enough to cover all of those cases. + */ +export function hasTreeSitterParser(language: StructuralLanguageId): boolean { + const chain = REGISTRY[language] + if (!chain) return false + return chain.some((parser) => parser.id === 'tree-sitter') +} diff --git a/src/lib/parsers/default/utils/summarizeLargeFiles.ts b/src/lib/parsers/default/utils/summarizeLargeFiles.ts index bedeee1e..b9938343 100644 --- a/src/lib/parsers/default/utils/summarizeLargeFiles.ts +++ b/src/lib/parsers/default/utils/summarizeLargeFiles.ts @@ -15,6 +15,7 @@ import { isPythonFile } from './pythonStructuralDiff' import { isRustFile } from './rustStructuralDiff' import { dispatchStructuralParser, + hasTreeSitterParser, type StructuralLanguageId, } from './structuralParserRegistry' import { detectTsLanguage } from './tsStructuralDiff' @@ -176,6 +177,21 @@ async function summarizeFileDiff( tokenCount: tokenizer(structuralSummary), } } + // Surrender telemetry (#933 phase 7). When the chain INCLUDES + // a tree-sitter parser but it surrendered (cache empty, AST + // unrecognized, dynamic import failed), emit a discoverability + // hint. Lazy-loaded languages benefit most from this — users + // who haven't run `coco cache prefetch ` see the nudge + // and know how to enable the better extractor. Bundled + // languages (ts/tsx) hit this branch too when the AST didn't + // recognize the diff shape; the hint is harmless there. + if (hasTreeSitterParser(language)) { + logger.verbose( + ` - ${fileDiff.file}: tree-sitter parser surrendered for '${language}'; using regex fallback. ` + + `Hint: \`coco cache parsers\` to inspect, \`coco cache prefetch ${language === 'ts' || language === 'js' ? 'all' : language}\` to enable.`, + { color: 'gray' } + ) + } } } diff --git a/src/lib/ui/inquirerPrompts.ts b/src/lib/ui/inquirerPrompts.ts index e550fb4c..7e641342 100644 --- a/src/lib/ui/inquirerPrompts.ts +++ b/src/lib/ui/inquirerPrompts.ts @@ -1,4 +1,5 @@ import type { + checkbox as inquirerCheckbox, confirm as inquirerConfirm, editor as inquirerEditor, input as inquirerInput, @@ -7,6 +8,7 @@ import type { } from '@inquirer/prompts' type InquirerPromptsModule = { + checkbox: typeof inquirerCheckbox confirm: typeof inquirerConfirm editor: typeof inquirerEditor input: typeof inquirerInput @@ -67,3 +69,11 @@ export async function selectPrompt( return (select as (config: unknown, context?: unknown) => Promise)(...args) } + +export async function checkboxPrompt( + ...args: [config: unknown, context?: unknown] +): Promise { + const { checkbox } = await loadInquirerPrompts() + + return (checkbox as (config: unknown, context?: unknown) => Promise)(...args) +}