goke 6.9.0 → 6.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/__test__/completions.test.d.ts +9 -0
  2. package/dist/__test__/completions.test.d.ts.map +1 -0
  3. package/dist/__test__/completions.test.js +774 -0
  4. package/dist/__test__/index.test.js +436 -308
  5. package/dist/__test__/just-bash.test.js +7 -7
  6. package/dist/__test__/readme-examples.test.js +149 -13
  7. package/dist/__test__/types.test-d.js +27 -0
  8. package/dist/agents.d.ts +38 -0
  9. package/dist/agents.d.ts.map +1 -0
  10. package/dist/agents.js +63 -0
  11. package/dist/completions.d.ts +88 -0
  12. package/dist/completions.d.ts.map +1 -0
  13. package/dist/completions.js +315 -0
  14. package/dist/goke.d.ts +95 -5
  15. package/dist/goke.d.ts.map +1 -1
  16. package/dist/goke.js +487 -4
  17. package/dist/index.d.ts +9 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +8 -1
  20. package/dist/just-bash.d.ts.map +1 -1
  21. package/dist/just-bash.js +1 -2
  22. package/dist/runtime-browser.d.ts +1 -1
  23. package/dist/runtime-browser.d.ts.map +1 -1
  24. package/dist/runtime-browser.js +1 -1
  25. package/dist/runtime-node.d.ts +1 -1
  26. package/dist/runtime-node.d.ts.map +1 -1
  27. package/dist/runtime-node.js +22 -13
  28. package/package.json +1 -1
  29. package/src/__test__/completions.test.ts +902 -0
  30. package/src/__test__/index.test.ts +471 -308
  31. package/src/__test__/just-bash.test.ts +7 -7
  32. package/src/__test__/readme-examples.test.ts +161 -13
  33. package/src/__test__/types.test-d.ts +27 -0
  34. package/src/agents.ts +101 -0
  35. package/src/completions.ts +363 -0
  36. package/src/goke.ts +540 -8
  37. package/src/index.ts +11 -2
  38. package/src/just-bash.ts +1 -2
  39. package/src/runtime-browser.ts +1 -1
  40. package/src/runtime-node.ts +19 -11
package/src/goke.ts CHANGED
@@ -15,6 +15,8 @@ import mri from "./mri.js"
15
15
  import { GokeError, coerceBySchema, extractJsonSchema, extractSchemaMetadata, isStandardSchema } from "./coerce.js"
16
16
  import type { StandardJSONSchemaV1 } from "./coerce.js"
17
17
  import { createJustBashCommand as createJustBashCommandBridge } from './just-bash.js'
18
+ import { COMPLETION_FLAG, generateCompletionScript, installCompletions, uninstallCompletions, detectShell, detectCompletionShell, validateShell } from './completions.js'
19
+ import type { ShellType } from './completions.js'
18
20
  import type { GokeFs } from './goke-fs.js'
19
21
  import { EventEmitter, fs as runtimeFs, openInBrowser, process } from '#runtime'
20
22
 
@@ -630,10 +632,43 @@ class Command<RawName extends string = string, Opts = {}> {
630
632
  action(
631
633
  callback: (...args: ActionArgs<RawName, Opts>) => unknown | Promise<unknown>,
632
634
  ): this {
635
+ // Give anonymous functions a name derived from the command so stack traces
636
+ // show e.g. "command:deploy" instead of "<anonymous>"
637
+ if (!callback.name) {
638
+ const label = this.name ? `command:${this.name}` : 'command:default'
639
+ Object.defineProperty(callback, 'name', { value: label })
640
+ }
633
641
  this.commandAction = callback
634
642
  return this
635
643
  }
636
644
 
645
+ /**
646
+ * Return the registered action callback with full type safety.
647
+ *
648
+ * Use this in tests to call the action directly without parsing argv.
649
+ * The returned function has the same typed signature as the `.action()` callback:
650
+ * `(..positionalArgs, options, executionContext) => unknown | Promise<unknown>`
651
+ *
652
+ * Throws if no action has been registered on this command.
653
+ *
654
+ * @example
655
+ * ```ts
656
+ * const cmd = cli
657
+ * .command('deploy', 'Deploy')
658
+ * .option('--env <env>', z.enum(['staging', 'production']))
659
+ * .action((options, { console }) => console.log(options.env))
660
+ *
661
+ * const action = cmd.getAction()
662
+ * action({ env: 'staging', '--': [] }, cli.createExecutionContext({ stdout }))
663
+ * ```
664
+ */
665
+ getAction(): (...args: ActionArgs<RawName, Opts>) => unknown | Promise<unknown> {
666
+ if (!this.commandAction) {
667
+ throw new GokeError(`No action registered on command "${this.name || '(default)'}"`)
668
+ }
669
+ return this.commandAction
670
+ }
671
+
637
672
  isMatched(args: string[]): { matched: boolean; consumedArgs: number } {
638
673
  const nameParts = this.name.split(' ').filter(Boolean)
639
674
 
@@ -1195,7 +1230,7 @@ class Goke<Opts = {}> extends EventEmitter {
1195
1230
  this.stderr = options?.stderr ?? process.stderr
1196
1231
  this.console = createConsole(this.stdout, this.stderr)
1197
1232
  this.columns = options?.columns ?? process.stdout.columns ?? Number.POSITIVE_INFINITY
1198
- this.exit = options?.exit ?? ((code: number) => process.exit(code))
1233
+ this.exit = options?.exit ?? ((code) => process.exit(code))
1199
1234
  this.#defaultArgv = options?.argv ?? processArgs
1200
1235
  this.globalCommand = new GlobalCommand(this)
1201
1236
  this.globalCommand.usage('<command> [options]')
@@ -1398,7 +1433,7 @@ class Goke<Opts = {}> extends EventEmitter {
1398
1433
  *
1399
1434
  * // main.ts
1400
1435
  * import { selfhostCli } from './selfhost.js'
1401
- * goke('mycli')
1436
+ * await goke('mycli')
1402
1437
  * .use(selfhostCli)
1403
1438
  * .help()
1404
1439
  * .parse(process.argv)
@@ -1552,20 +1587,337 @@ class Goke<Opts = {}> extends EventEmitter {
1552
1587
  }
1553
1588
 
1554
1589
  /**
1555
- * Parse argv
1590
+ * Register shell completion commands: `completions install` and `completions uninstall`.
1591
+ *
1592
+ * Also wires the hidden `--get-goke-completions` flag that shell scripts call
1593
+ * on each Tab press. When this flag is detected during `parse()`, the CLI
1594
+ * prints matching completions to stdout and exits immediately.
1595
+ *
1596
+ * @example
1597
+ * ```ts
1598
+ * await goke('mycli')
1599
+ * .help()
1600
+ * .completions()
1601
+ * .command('deploy', 'Deploy the app')
1602
+ * .parse(process.argv)
1603
+ *
1604
+ * // Then the user runs:
1605
+ * // mycli completions install
1606
+ * // mycli dep<TAB> → mycli deploy
1607
+ * ```
1608
+ */
1609
+ completions() {
1610
+ this.command('completions install', 'Install shell completions')
1611
+ .option('--shell [shell]', 'Target shell (zsh or bash). Auto-detected if omitted.')
1612
+ .action(async (options, { console, process: proc }) => {
1613
+ const shell = validateShell(options.shell)
1614
+ const cliPath = proc.argv[1] ?? this.name
1615
+ const result = await installCompletions(this.name, cliPath, shell)
1616
+ console.log(`Wrote ${result.shell} completions to ${result.path}`)
1617
+ if (result.shell === 'zsh') {
1618
+ console.log('Restart your shell or run: autoload -Uz compinit && compinit')
1619
+ } else {
1620
+ console.log('Restart your shell to enable completions.')
1621
+ }
1622
+ })
1623
+
1624
+ this.command('completions uninstall', 'Remove shell completions')
1625
+ .option('--shell [shell]', 'Target shell (zsh or bash). Auto-detected if omitted.')
1626
+ .action(async (options, { console }) => {
1627
+ const shell = validateShell(options.shell)
1628
+ const removed = await uninstallCompletions(this.name, shell)
1629
+ if (removed.length > 0) {
1630
+ for (const p of removed) {
1631
+ console.log(`Removed ${p}`)
1632
+ }
1633
+ } else {
1634
+ console.log('No completion files found to remove.')
1635
+ }
1636
+ })
1637
+
1638
+ this.command('completions script', 'Print the completion script to stdout')
1639
+ .option('--shell [shell]', 'Target shell (zsh or bash). Auto-detected if omitted.')
1640
+ .action((options, { console, process: proc }) => {
1641
+ const shell = validateShell(options.shell) ?? detectShell()
1642
+ if (!shell) {
1643
+ throw new GokeError(
1644
+ 'Could not detect shell. Set the SHELL environment variable or pass --shell explicitly.',
1645
+ )
1646
+ }
1647
+ const cliPath = proc.argv[1] ?? this.name
1648
+ const script = generateCompletionScript(shell, this.name, cliPath)
1649
+ console.log(script)
1650
+ })
1651
+
1652
+ return this
1653
+ }
1654
+
1655
+ /**
1656
+ * Compute completions for the given args (as received from the shell script).
1657
+ *
1658
+ * Returns an array of completion strings. For zsh, each entry is `name:description`.
1659
+ * For bash, each entry is just the name.
1660
+ *
1661
+ * @internal Used by parse() when --get-goke-completions is detected.
1662
+ */
1663
+ getCompletions(argv: string[]): string[] {
1664
+ // argv comes from the shell: ["my-cli", "dep", ""] or ["my-cli", "deploy", "--"]
1665
+ // Strip the binary name (first element, which is the CLI name itself)
1666
+ const args = argv.slice(1)
1667
+ const current = args.length > 0 ? args[args.length - 1] : ''
1668
+ const previous = args.slice(0, -1)
1669
+
1670
+ // Use GOKE_COMPLETION_SHELL (set by the shell shim) over $SHELL to avoid
1671
+ // format mismatch when e.g. a bash shim runs on a machine where $SHELL is zsh.
1672
+ const isZsh = detectCompletionShell() === 'zsh'
1673
+
1674
+ const completions: string[] = []
1675
+ const escapeColon = (s: string) => s.replace(/:/g, '\\:')
1676
+
1677
+ // Extract the long --flag from an option's rawName string.
1678
+ // rawName is like "--dry-run", "-v, --verbose", "--port <port>"
1679
+ // Returns the original kebab-case flag including dashes.
1680
+ const getLongFlag = (option: Option): string => {
1681
+ const parts = removeBrackets(option.rawName).split(',').map((s) => s.trim())
1682
+ // Prefer the -- prefixed part; fall back to the last part (short-only flags like -x)
1683
+ const longPart = parts.find((p) => p.startsWith('--')) ?? parts[parts.length - 1]
1684
+ return longPart.startsWith('-') ? longPart : `--${longPart}`
1685
+ }
1686
+
1687
+ // Check if the previous token is a non-boolean option expecting a value.
1688
+ // In that case we should NOT suggest more flags or commands; let the shell
1689
+ // fall back to file completion or return nothing.
1690
+ const isAwaitingOptionValue = (): boolean => {
1691
+ if (previous.length === 0) return false
1692
+ const lastToken = previous[previous.length - 1]
1693
+ if (!lastToken.startsWith('-')) return false
1694
+
1695
+ // Find the option matching this token across all registered options
1696
+ const allOptions = [
1697
+ ...this.globalCommand.options,
1698
+ ...this.commands.flatMap((c) => c.options),
1699
+ ]
1700
+ const tokenName = camelcaseOptionName(lastToken.replace(/^-{1,2}/, ''))
1701
+ for (const option of allOptions) {
1702
+ if (option.names.includes(tokenName)) {
1703
+ // If it takes a value (required or optional) and is not boolean, we're awaiting a value
1704
+ return !option.isBoolean && option.required !== undefined
1705
+ }
1706
+ }
1707
+ return false
1708
+ }
1709
+
1710
+ // If the previous token is a non-boolean option, don't suggest anything.
1711
+ // Let the shell fall back to file completion.
1712
+ if (!current.startsWith('-') && isAwaitingOptionValue()) {
1713
+ return []
1714
+ }
1715
+
1716
+ // Helper to push an option as a completion entry
1717
+ const pushOption = (option: Option) => {
1718
+ const flag = getLongFlag(option)
1719
+ if (isZsh && option.description) {
1720
+ completions.push(`${escapeColon(flag)}:${escapeColon(option.description)}`)
1721
+ } else {
1722
+ completions.push(flag)
1723
+ }
1724
+ }
1725
+
1726
+ // Check if any alias of an option has already been used
1727
+ const isOptionUsed = (option: Option, usedOptions: Set<string>): boolean => {
1728
+ return option.names.some((name) => usedOptions.has(name))
1729
+ }
1730
+
1731
+ // Try to match a command from the previous words
1732
+ let matchedCommand: Command | undefined
1733
+ let consumedArgs = 0
1734
+
1735
+ // Sort by name length (longest first) for greedy matching
1736
+ const sortedCommands = [...this.commands].sort((a, b) => {
1737
+ const aLen = a.name.split(' ').filter(Boolean).length
1738
+ const bLen = b.name.split(' ').filter(Boolean).length
1739
+ return bLen - aLen
1740
+ })
1741
+
1742
+ for (const command of sortedCommands) {
1743
+ const result = command.isMatched(previous)
1744
+ if (result.matched) {
1745
+ matchedCommand = command
1746
+ consumedArgs = result.consumedArgs
1747
+ break
1748
+ }
1749
+ }
1750
+
1751
+ if (matchedCommand) {
1752
+ // We matched a command, suggest its options
1753
+ const usedOptions = new Set(
1754
+ previous.slice(consumedArgs)
1755
+ .filter((a) => a.startsWith('-'))
1756
+ .map((a) => a.replace(/^-{1,2}/, ''))
1757
+ .map(camelcaseOptionName),
1758
+ )
1759
+
1760
+ const allOptions = [...(matchedCommand.globalCommand?.options ?? []), ...matchedCommand.options]
1761
+
1762
+ for (const option of allOptions) {
1763
+ if (option.deprecated) continue
1764
+ // Skip already-used options (check all aliases, not just the primary name)
1765
+ if (option.isBoolean && isOptionUsed(option, usedOptions)) continue
1766
+
1767
+ const flag = getLongFlag(option)
1768
+
1769
+ if (current.startsWith('-')) {
1770
+ if (!flag.startsWith(current)) continue
1771
+ } else if (current !== '') {
1772
+ continue
1773
+ }
1774
+
1775
+ pushOption(option)
1776
+ }
1777
+
1778
+ // If current word doesn't start with -, also suggest subcommands that extend this one
1779
+ if (!current.startsWith('-')) {
1780
+ const prefix = matchedCommand.name ? matchedCommand.name + ' ' : ''
1781
+ for (const cmd of this.commands) {
1782
+ if (cmd._hidden) continue
1783
+ if (cmd === matchedCommand) continue
1784
+ if (cmd.name.startsWith(prefix) && cmd.name !== matchedCommand.name) {
1785
+ const sub = cmd.name.slice(prefix.length).split(' ')[0]
1786
+ if (sub.startsWith(current)) {
1787
+ if (isZsh) {
1788
+ const desc = cmd.description.split('\n')[0].trim()
1789
+ completions.push(desc ? `${escapeColon(sub)}:${escapeColon(desc)}` : sub)
1790
+ } else {
1791
+ completions.push(sub)
1792
+ }
1793
+ }
1794
+ }
1795
+ }
1796
+ }
1797
+ } else {
1798
+ // No command matched yet, suggest commands
1799
+ // Check if some previous words partially match a multi-word command prefix
1800
+ const prevJoined = previous.join(' ')
1801
+
1802
+ for (const command of this.commands) {
1803
+ if (command._hidden) continue
1804
+ if (command.isDefaultCommand) continue
1805
+
1806
+ const cmdName = command.name
1807
+ const cmdParts = cmdName.split(' ').filter(Boolean)
1808
+
1809
+ if (cmdParts.length === 0) continue
1810
+
1811
+ // For single-word commands, just check prefix against current
1812
+ if (cmdParts.length === 1) {
1813
+ if (previous.length === 0 && cmdParts[0].startsWith(current)) {
1814
+ if (isZsh) {
1815
+ const desc = command.description.split('\n')[0].trim()
1816
+ completions.push(desc ? `${escapeColon(cmdParts[0])}:${escapeColon(desc)}` : cmdParts[0])
1817
+ } else {
1818
+ completions.push(cmdParts[0])
1819
+ }
1820
+ }
1821
+ continue
1822
+ }
1823
+
1824
+ // Multi-word commands: check if previous matches the prefix parts
1825
+ const matchPrefix = cmdParts.slice(0, -1).join(' ')
1826
+ const lastPart = cmdParts[cmdParts.length - 1]
1827
+ if (prevJoined === matchPrefix && lastPart.startsWith(current)) {
1828
+ if (isZsh) {
1829
+ const desc = command.description.split('\n')[0].trim()
1830
+ completions.push(desc ? `${escapeColon(lastPart)}:${escapeColon(desc)}` : lastPart)
1831
+ } else {
1832
+ completions.push(lastPart)
1833
+ }
1834
+ }
1835
+ }
1836
+
1837
+ // Also suggest first words of multi-word commands when at root level
1838
+ if (previous.length === 0) {
1839
+ const seenFirstWords = new Set<string>()
1840
+ for (const command of this.commands) {
1841
+ if (command._hidden || command.isDefaultCommand) continue
1842
+ const firstWord = command.name.split(' ')[0]
1843
+ if (!firstWord || seenFirstWords.has(firstWord)) continue
1844
+ // Skip if already added as a single-word command above
1845
+ if (completions.some((c) => {
1846
+ const name = c.split(':')[0].replace(/\\:/g, ':')
1847
+ return name === firstWord
1848
+ })) continue
1849
+ seenFirstWords.add(firstWord)
1850
+ if (firstWord.startsWith(current)) {
1851
+ // For first words of multi-word commands, no description (it's a prefix, not a full command)
1852
+ completions.push(firstWord)
1853
+ }
1854
+ }
1855
+ }
1856
+
1857
+ // Also include default/root command options at root level
1858
+ // (commands with name '' that have their own options)
1859
+ const defaultCommands = this.commands.filter((c) => c.isDefaultCommand)
1860
+ const defaultOptions = defaultCommands.flatMap((c) => c.options)
1861
+
1862
+ // Suggest global options + default command options when current starts with -
1863
+ if (current.startsWith('-') || current === '') {
1864
+ const globalAndDefaultOptions = [...this.globalCommand.options, ...defaultOptions]
1865
+ const seen = new Set<string>()
1866
+
1867
+ for (const option of globalAndDefaultOptions) {
1868
+ if (option.deprecated) continue
1869
+ if (seen.has(option.name)) continue
1870
+ seen.add(option.name)
1871
+
1872
+ const flag = getLongFlag(option)
1873
+
1874
+ if (current.startsWith('-')) {
1875
+ if (!flag.startsWith(current)) continue
1876
+ } else if (current !== '') {
1877
+ continue
1878
+ }
1879
+
1880
+ // Only suggest options when current is - prefixed or empty and no commands matched
1881
+ if (current === '' && completions.length > 0 && !current.startsWith('-')) continue
1882
+
1883
+ pushOption(option)
1884
+ }
1885
+ }
1886
+ }
1887
+
1888
+ // Deduplicate
1889
+ return [...new Set(completions)]
1890
+ }
1891
+
1892
+ /**
1893
+ * Parse argv and await the matched command when run is enabled.
1556
1894
  */
1557
- parse(
1895
+ async parse(
1558
1896
  argv = this.#defaultArgv,
1559
1897
  {
1560
1898
  /** Whether to run the action for matched command */
1561
1899
  run = true,
1562
1900
  } = {}
1563
- ): ParsedArgv {
1901
+ ): Promise<ParsedArgv> {
1564
1902
  this.rawArgs = argv
1565
1903
  if (!this.name) {
1566
1904
  this.name = argv[1] ? getFileName(argv[1]) : 'cli'
1567
1905
  }
1568
1906
 
1907
+ // Intercept --get-goke-completions before any command matching/validation.
1908
+ // The shell completion script passes this flag on every Tab press.
1909
+ const completionFlagIndex = argv.indexOf(`--${COMPLETION_FLAG}`)
1910
+ if (completionFlagIndex !== -1) {
1911
+ // Everything after the flag is the words typed so far
1912
+ const completionArgs = argv.slice(completionFlagIndex + 1)
1913
+ const completions = this.getCompletions(completionArgs)
1914
+ for (const c of completions) {
1915
+ this.stdout.write(c + '\n')
1916
+ }
1917
+ this.exit(0)
1918
+ return { args: [], options: {} }
1919
+ }
1920
+
1569
1921
  let shouldParse = true
1570
1922
 
1571
1923
  // Sort by name length (longest first) so "mcp login" matches before "mcp"
@@ -1612,6 +1964,11 @@ class Goke<Opts = {}> extends EventEmitter {
1612
1964
  // Don't match default command - let it fall through to "unknown command"
1613
1965
  continue
1614
1966
  }
1967
+ // Default command defines no positional args but user passed args;
1968
+ // skip matching so unknown args fall through to "unknown command"
1969
+ if (command.args.length === 0) {
1970
+ continue
1971
+ }
1615
1972
  }
1616
1973
  shouldParse = false
1617
1974
  this.setParsedInfo(parsed, command)
@@ -1662,7 +2019,7 @@ class Goke<Opts = {}> extends EventEmitter {
1662
2019
  const parsedArgv = { args: this.args, options: this.options }
1663
2020
 
1664
2021
  if (run) {
1665
- this.runMatchedCommand()
2022
+ await this.runMatchedCommand()
1666
2023
  }
1667
2024
 
1668
2025
  if (!this.matchedCommand && this.args[0] && !(this.options.help && this.showHelpOnExit)) {
@@ -1943,8 +2300,183 @@ class Goke<Opts = {}> extends EventEmitter {
1943
2300
  }
1944
2301
  }
1945
2302
 
2303
+ // ─── Doc generation ───
2304
+
2305
+ interface DocPage {
2306
+ /** The command name, e.g. "event view". Empty string for the root CLI page. */
2307
+ command: string
2308
+ /** URL-friendly slug, e.g. "event-view". "index" for the root CLI page. */
2309
+ slug: string
2310
+ /** Full markdown content for this command's documentation page. */
2311
+ content: string
2312
+ }
2313
+
2314
+ interface GenerateDocsOptions {
2315
+ /** The Goke CLI instance to generate docs from. */
2316
+ cli: Goke<any>
2317
+ }
2318
+
2319
+ /**
2320
+ * Generate markdown documentation pages for every command in a CLI.
2321
+ *
2322
+ * Returns one `DocPage` per non-hidden command, plus a root index page
2323
+ * that lists all available commands. Each page includes an arguments table,
2324
+ * options table, global options, and examples when available.
2325
+ *
2326
+ * @example
2327
+ * ```ts
2328
+ * import { goke, generateDocs } from 'goke'
2329
+ * import fs from 'node:fs'
2330
+ *
2331
+ * const cli = goke('mycli')
2332
+ * .command('deploy <env>', 'Deploy to an environment')
2333
+ * .option('--force', 'Skip confirmation')
2334
+ *
2335
+ * const pages = generateDocs({ cli })
2336
+ * for (const page of pages) {
2337
+ * fs.writeFileSync(`docs/${page.slug}.md`, page.content)
2338
+ * }
2339
+ * ```
2340
+ */
2341
+ function generateDocs({ cli }: GenerateDocsOptions): DocPage[] {
2342
+ const pages: DocPage[] = []
2343
+
2344
+ // Collect global options (from globalCommand), excluding deprecated
2345
+ const globalOptions = cli.globalCommand.options.filter((o) => !o.deprecated)
2346
+
2347
+ // Root index page listing all commands
2348
+ const visibleCommands = cli.commands.filter((cmd) => !cmd._hidden)
2349
+ if (visibleCommands.length > 0) {
2350
+ const lines: string[] = []
2351
+ lines.push(`# ${cli.name}`)
2352
+ lines.push('')
2353
+
2354
+ const { versionNumber } = cli.globalCommand
2355
+ if (versionNumber) {
2356
+ lines.push(`Version: ${versionNumber}`)
2357
+ lines.push('')
2358
+ }
2359
+
2360
+ lines.push('## Commands')
2361
+ lines.push('')
2362
+ lines.push('| Command | Description |')
2363
+ lines.push('|---------|-------------|')
2364
+ for (const cmd of visibleCommands) {
2365
+ if (cmd.isDefaultCommand) continue
2366
+ const desc = cmd.description.split('\n')[0].trim()
2367
+ const slug = cmd.name.replace(/\s+/g, '-')
2368
+ lines.push(`| [\`${cmd.name}\`](./${slug}.md) | ${desc} |`)
2369
+ }
2370
+ lines.push('')
2371
+
2372
+ if (globalOptions.length > 0) {
2373
+ lines.push('## Global Options')
2374
+ lines.push('')
2375
+ lines.push(formatOptionsTable(globalOptions))
2376
+ lines.push('')
2377
+ }
2378
+
2379
+ pages.push({ command: '', slug: 'index', content: lines.join('\n') })
2380
+ }
2381
+
2382
+ // One page per command
2383
+ for (const cmd of visibleCommands) {
2384
+ if (cmd.isDefaultCommand) continue
2385
+ const lines: string[] = []
2386
+ const title = cmd.name
2387
+ lines.push(`# ${title}`)
2388
+ lines.push('')
2389
+
2390
+ if (cmd.description) {
2391
+ lines.push(cmd.description)
2392
+ lines.push('')
2393
+ }
2394
+
2395
+ // Usage line
2396
+ const usage = cmd.usageText || cmd.rawName
2397
+ lines.push('## Usage')
2398
+ lines.push('')
2399
+ lines.push('```sh')
2400
+ lines.push(`${cli.name} ${usage}`)
2401
+ lines.push('```')
2402
+ lines.push('')
2403
+
2404
+ // Arguments table
2405
+ if (cmd.args.length > 0) {
2406
+ lines.push('## Arguments')
2407
+ lines.push('')
2408
+ lines.push('| Argument | Required | Description |')
2409
+ lines.push('|----------|----------|-------------|')
2410
+ for (const arg of cmd.args) {
2411
+ const bracket = arg.required
2412
+ ? `<${arg.variadic ? '...' : ''}${arg.value}>`
2413
+ : `[${arg.variadic ? '...' : ''}${arg.value}]`
2414
+ const required = arg.required ? 'Yes' : 'No'
2415
+ const desc = arg.variadic ? `${arg.value} (variadic)` : arg.value
2416
+ lines.push(`| \`${bracket}\` | ${required} | ${desc} |`)
2417
+ }
2418
+ lines.push('')
2419
+ }
2420
+
2421
+ // Command-specific options
2422
+ const cmdOptions = cmd.options.filter((o) => !o.deprecated)
2423
+ if (cmdOptions.length > 0) {
2424
+ lines.push('## Options')
2425
+ lines.push('')
2426
+ lines.push(formatOptionsTable(cmdOptions))
2427
+ lines.push('')
2428
+ }
2429
+
2430
+ // Global options section
2431
+ if (globalOptions.length > 0) {
2432
+ lines.push('## Global Options')
2433
+ lines.push('')
2434
+ lines.push(formatOptionsTable(globalOptions))
2435
+ lines.push('')
2436
+ }
2437
+
2438
+ // Examples
2439
+ if (cmd.examples.length > 0) {
2440
+ lines.push('## Examples')
2441
+ lines.push('')
2442
+ for (const example of cmd.examples) {
2443
+ const text = typeof example === 'function' ? example(cli.name) : example
2444
+ // Auto-wrap in ```sh if not already fenced
2445
+ if (text.trimStart().startsWith('```')) {
2446
+ lines.push(text)
2447
+ } else {
2448
+ lines.push('```sh')
2449
+ lines.push(text)
2450
+ lines.push('```')
2451
+ }
2452
+ lines.push('')
2453
+ }
2454
+ }
2455
+
2456
+ const slug = cmd.name.replace(/\s+/g, '-')
2457
+ pages.push({ command: cmd.name, slug, content: lines.join('\n') })
2458
+ }
2459
+
2460
+ return pages
2461
+ }
2462
+
2463
+ function formatOptionsTable(options: Option[]): string {
2464
+ const lines: string[] = []
2465
+ lines.push('| Option | Default | Description |')
2466
+ lines.push('|--------|---------|-------------|')
2467
+ for (const opt of options) {
2468
+ const defaultVal = opt.default !== undefined ? `\`${String(opt.default)}\`` : '-'
2469
+ // Escape pipe characters in description for markdown tables
2470
+ const desc = opt.description.replace(/\|/g, '\\|').replace(/\n/g, ' ')
2471
+ lines.push(`| \`${opt.rawName}\` | ${defaultVal} | ${desc} |`)
2472
+ }
2473
+ return lines.join('\n')
2474
+ }
2475
+
1946
2476
  // ─── Exports ───
1947
2477
 
1948
- export type { GokeOutputStream, GokeConsole, GokeOptions, GokeProcess, GokeExecutionContext, GokeExecutionContextOverride, GokeFs }
1949
- export { createConsole, Command, GokeProcessExit, openInBrowser }
2478
+ export type { GokeOutputStream, GokeConsole, GokeOptions, GokeProcess, GokeExecutionContext, GokeExecutionContextOverride, GokeFs, DocPage, GenerateDocsOptions }
2479
+ export { createConsole, Command, GokeProcessExit, openInBrowser, generateDocs }
2480
+ export type { ShellType }
2481
+ export { generateCompletionScript, installCompletions, uninstallCompletions, detectShell, detectCompletionShell, validateShell }
1950
2482
  export default Goke
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import Goke from "./goke.js"
2
2
  import type { GokeOptions } from "./goke.js"
3
3
  import { Command } from "./goke.js"
4
+ import pc from "./picocolors.js"
4
5
 
5
6
  /**
6
7
  * @param name The program name to display in help and version message
@@ -8,9 +9,17 @@ import { Command } from "./goke.js"
8
9
  */
9
10
  const goke = (name = '', options?: GokeOptions) => new Goke(name, options)
10
11
 
12
+ /**
13
+ * Vendored picocolors instance for terminal colors.
14
+ * Import this instead of installing picocolors, chalk, or any other color library.
15
+ */
16
+ export const colors = pc
17
+
11
18
  export default goke
12
19
  export { goke, Goke, Command }
13
- export { createConsole, GokeProcessExit, openInBrowser } from "./goke.js"
14
- export type { GokeOutputStream, GokeConsole, GokeExecutionContext, GokeExecutionContextOverride, GokeFs, GokeOptions, GokeProcess } from "./goke.js"
20
+ export { createConsole, GokeProcessExit, openInBrowser, generateDocs, generateCompletionScript, installCompletions, uninstallCompletions, detectShell } from "./goke.js"
21
+ export type { GokeOutputStream, GokeConsole, GokeExecutionContext, GokeExecutionContextOverride, GokeFs, GokeOptions, GokeProcess, DocPage, GenerateDocsOptions, ShellType } from "./goke.js"
15
22
  export type { StandardTypedV1, StandardJSONSchemaV1, JsonSchema } from "./coerce.js"
16
23
  export { GokeError, coerceBySchema, extractJsonSchema, wrapJsonSchema, isStandardSchema, extractSchemaMetadata } from "./coerce.js"
24
+ export { detectAgent, agentInfo, agent, isAgent } from "./agents.js"
25
+ export type { AgentName, AgentInfo } from "./agents.js"
package/src/just-bash.ts CHANGED
@@ -323,8 +323,7 @@ export function createJustBashCommand(
323
323
  cloned.name = name
324
324
 
325
325
  try {
326
- cloned.parse(argv, { run: false })
327
- await cloned.runMatchedCommand()
326
+ await cloned.parse(argv)
328
327
  const result = output.getResult()
329
328
  return {
330
329
  stdout: result.stdout,
@@ -86,7 +86,7 @@ const fs: GokeFs = {
86
86
  writeFile: createUnsupportedFsMethod<GokeFs['writeFile']>('writeFile'),
87
87
  }
88
88
 
89
- function openInBrowser(_url: string): void {
89
+ async function openInBrowser(_url: string): Promise<void> {
90
90
  // Browser builds should decide how to surface URLs themselves.
91
91
  }
92
92