|
| 1 | +/** |
| 2 | + * Stricli + tab integration demo. |
| 3 | + * |
| 4 | + * This shows how a CLI that already implements its own completion-resolution |
| 5 | + * logic (the (a) half) can plug into tab purely for the shell-protocol / |
| 6 | + * shell-script half (the (b) half). |
| 7 | + * |
| 8 | + * The shape is: |
| 9 | + * - `<exec> complete <shell>` -> tab generates the shell script |
| 10 | + * - `<exec> complete -- <argv>` -> stricli computes completions, tab emits |
| 11 | + * them in the wire format the script reads |
| 12 | + * |
| 13 | + * Run any of: |
| 14 | + * pnpm tsx examples/demo.stricli.ts complete -- "" |
| 15 | + * pnpm tsx examples/demo.stricli.ts complete -- dev --port= |
| 16 | + * pnpm tsx examples/demo.stricli.ts complete -- dev --mode prod |
| 17 | + * pnpm tsx examples/demo.stricli.ts complete bash |
| 18 | + */ |
| 19 | +import { |
| 20 | + buildApplication, |
| 21 | + buildCommand, |
| 22 | + buildRouteMap, |
| 23 | + numberParser, |
| 24 | + proposeCompletions, |
| 25 | + type InputCompletion, |
| 26 | +} from '@stricli/core'; |
| 27 | +import { |
| 28 | + emitCompletions, |
| 29 | + script, |
| 30 | + ShellCompDirective, |
| 31 | + type Completion, |
| 32 | + type Directive, |
| 33 | +} from '../src/t'; |
| 34 | + |
| 35 | +// --- (1) Build a tiny stricli application ---------------------------------- |
| 36 | + |
| 37 | +const devCommand = buildCommand({ |
| 38 | + loader: async () => () => { |
| 39 | + /* impl not needed for completion demo */ |
| 40 | + }, |
| 41 | + parameters: { |
| 42 | + flags: { |
| 43 | + port: { |
| 44 | + kind: 'parsed', |
| 45 | + parse: numberParser, |
| 46 | + brief: 'Port to listen on', |
| 47 | + optional: true, |
| 48 | + }, |
| 49 | + mode: { |
| 50 | + kind: 'enum', |
| 51 | + values: ['development', 'production'] as const, |
| 52 | + brief: 'Build mode', |
| 53 | + optional: true, |
| 54 | + }, |
| 55 | + verbose: { |
| 56 | + kind: 'boolean', |
| 57 | + brief: 'Enable verbose logging', |
| 58 | + optional: true, |
| 59 | + }, |
| 60 | + }, |
| 61 | + }, |
| 62 | + docs: { brief: 'Start dev server' }, |
| 63 | +}); |
| 64 | + |
| 65 | +const buildCmd = buildCommand({ |
| 66 | + loader: async () => () => {}, |
| 67 | + parameters: { flags: {} }, |
| 68 | + docs: { brief: 'Build the project' }, |
| 69 | +}); |
| 70 | + |
| 71 | +const root = buildRouteMap({ |
| 72 | + routes: { dev: devCommand, build: buildCmd }, |
| 73 | + docs: { brief: 'Demo CLI using stricli for (a) and tab for (b)' }, |
| 74 | +}); |
| 75 | + |
| 76 | +const app = buildApplication(root, { |
| 77 | + name: 'demo-stricli', |
| 78 | + versionInfo: { currentVersion: '0.0.0' }, |
| 79 | +}); |
| 80 | + |
| 81 | +// --- (2) Wire up the `complete` subcommand --------------------------------- |
| 82 | + |
| 83 | +async function main() { |
| 84 | + const argv = process.argv.slice(2); |
| 85 | + |
| 86 | + if (argv[0] !== 'complete') { |
| 87 | + console.log('Demo CLI. Use "complete <shell>" or "complete -- <args>".'); |
| 88 | + return; |
| 89 | + } |
| 90 | + |
| 91 | + const second = argv[1]; |
| 92 | + const SUPPORTED_SHELLS = ['bash', 'zsh', 'fish', 'powershell'] as const; |
| 93 | + type Shell = (typeof SUPPORTED_SHELLS)[number]; |
| 94 | + |
| 95 | + // a) `complete <shell>` -> use tab to print the shell-side completion script |
| 96 | + if (second && (SUPPORTED_SHELLS as readonly string[]).includes(second)) { |
| 97 | + script( |
| 98 | + second as Shell, |
| 99 | + 'demo-stricli', |
| 100 | + 'pnpm tsx examples/demo.stricli.ts' |
| 101 | + ); |
| 102 | + return; |
| 103 | + } |
| 104 | + |
| 105 | + // b) `complete -- <args>` -> use stricli to compute completions, |
| 106 | + // then hand the finished list to tab to emit on the wire. |
| 107 | + if (second === '--') { |
| 108 | + const inputs = argv.slice(2); |
| 109 | + const stricliCompletions = await proposeCompletions(app, inputs, { |
| 110 | + process, |
| 111 | + }); |
| 112 | + |
| 113 | + const completions = stricliCompletions.map(toTabCompletion); |
| 114 | + const directive: Directive = |
| 115 | + ShellCompDirective.ShellCompDirectiveNoFileComp; |
| 116 | + emitCompletions(completions, directive); |
| 117 | + return; |
| 118 | + } |
| 119 | + |
| 120 | + console.error('Usage: complete <shell> | complete -- <args>'); |
| 121 | + process.exit(1); |
| 122 | +} |
| 123 | + |
| 124 | +/** |
| 125 | + * Map stricli's `InputCompletion` shape ({ kind, completion, brief }) to tab's |
| 126 | + * `Completion` shape ({ value, description }). The `kind` is informational |
| 127 | + * only — tab's wire format doesn't care about it. |
| 128 | + */ |
| 129 | +function toTabCompletion(c: InputCompletion): Completion { |
| 130 | + return { value: c.completion, description: c.brief }; |
| 131 | +} |
| 132 | + |
| 133 | +main().catch((err) => { |
| 134 | + console.error(err); |
| 135 | + process.exit(1); |
| 136 | +}); |
0 commit comments