goke 6.9.0 → 6.10.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.
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
 
@@ -1551,6 +1586,309 @@ class Goke<Opts = {}> extends EventEmitter {
1551
1586
  }
1552
1587
  }
1553
1588
 
1589
+ /**
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
+ * 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 as string[])
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
+
1554
1892
  /**
1555
1893
  * Parse argv
1556
1894
  */
@@ -1566,6 +1904,20 @@ class Goke<Opts = {}> extends EventEmitter {
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"
@@ -1943,8 +2295,183 @@ class Goke<Opts = {}> extends EventEmitter {
1943
2295
  }
1944
2296
  }
1945
2297
 
2298
+ // ─── Doc generation ───
2299
+
2300
+ interface DocPage {
2301
+ /** The command name, e.g. "event view". Empty string for the root CLI page. */
2302
+ command: string
2303
+ /** URL-friendly slug, e.g. "event-view". "index" for the root CLI page. */
2304
+ slug: string
2305
+ /** Full markdown content for this command's documentation page. */
2306
+ content: string
2307
+ }
2308
+
2309
+ interface GenerateDocsOptions {
2310
+ /** The Goke CLI instance to generate docs from. */
2311
+ cli: Goke<any>
2312
+ }
2313
+
2314
+ /**
2315
+ * Generate markdown documentation pages for every command in a CLI.
2316
+ *
2317
+ * Returns one `DocPage` per non-hidden command, plus a root index page
2318
+ * that lists all available commands. Each page includes an arguments table,
2319
+ * options table, global options, and examples when available.
2320
+ *
2321
+ * @example
2322
+ * ```ts
2323
+ * import { goke, generateDocs } from 'goke'
2324
+ * import fs from 'node:fs'
2325
+ *
2326
+ * const cli = goke('mycli')
2327
+ * .command('deploy <env>', 'Deploy to an environment')
2328
+ * .option('--force', 'Skip confirmation')
2329
+ *
2330
+ * const pages = generateDocs({ cli })
2331
+ * for (const page of pages) {
2332
+ * fs.writeFileSync(`docs/${page.slug}.md`, page.content)
2333
+ * }
2334
+ * ```
2335
+ */
2336
+ function generateDocs({ cli }: GenerateDocsOptions): DocPage[] {
2337
+ const pages: DocPage[] = []
2338
+
2339
+ // Collect global options (from globalCommand), excluding deprecated
2340
+ const globalOptions = cli.globalCommand.options.filter((o) => !o.deprecated)
2341
+
2342
+ // Root index page listing all commands
2343
+ const visibleCommands = cli.commands.filter((cmd) => !cmd._hidden)
2344
+ if (visibleCommands.length > 0) {
2345
+ const lines: string[] = []
2346
+ lines.push(`# ${cli.name}`)
2347
+ lines.push('')
2348
+
2349
+ const { versionNumber } = cli.globalCommand
2350
+ if (versionNumber) {
2351
+ lines.push(`Version: ${versionNumber}`)
2352
+ lines.push('')
2353
+ }
2354
+
2355
+ lines.push('## Commands')
2356
+ lines.push('')
2357
+ lines.push('| Command | Description |')
2358
+ lines.push('|---------|-------------|')
2359
+ for (const cmd of visibleCommands) {
2360
+ if (cmd.isDefaultCommand) continue
2361
+ const desc = cmd.description.split('\n')[0].trim()
2362
+ const slug = cmd.name.replace(/\s+/g, '-')
2363
+ lines.push(`| [\`${cmd.name}\`](./${slug}.md) | ${desc} |`)
2364
+ }
2365
+ lines.push('')
2366
+
2367
+ if (globalOptions.length > 0) {
2368
+ lines.push('## Global Options')
2369
+ lines.push('')
2370
+ lines.push(formatOptionsTable(globalOptions))
2371
+ lines.push('')
2372
+ }
2373
+
2374
+ pages.push({ command: '', slug: 'index', content: lines.join('\n') })
2375
+ }
2376
+
2377
+ // One page per command
2378
+ for (const cmd of visibleCommands) {
2379
+ if (cmd.isDefaultCommand) continue
2380
+ const lines: string[] = []
2381
+ const title = cmd.name
2382
+ lines.push(`# ${title}`)
2383
+ lines.push('')
2384
+
2385
+ if (cmd.description) {
2386
+ lines.push(cmd.description)
2387
+ lines.push('')
2388
+ }
2389
+
2390
+ // Usage line
2391
+ const usage = cmd.usageText || cmd.rawName
2392
+ lines.push('## Usage')
2393
+ lines.push('')
2394
+ lines.push('```sh')
2395
+ lines.push(`${cli.name} ${usage}`)
2396
+ lines.push('```')
2397
+ lines.push('')
2398
+
2399
+ // Arguments table
2400
+ if (cmd.args.length > 0) {
2401
+ lines.push('## Arguments')
2402
+ lines.push('')
2403
+ lines.push('| Argument | Required | Description |')
2404
+ lines.push('|----------|----------|-------------|')
2405
+ for (const arg of cmd.args) {
2406
+ const bracket = arg.required
2407
+ ? `<${arg.variadic ? '...' : ''}${arg.value}>`
2408
+ : `[${arg.variadic ? '...' : ''}${arg.value}]`
2409
+ const required = arg.required ? 'Yes' : 'No'
2410
+ const desc = arg.variadic ? `${arg.value} (variadic)` : arg.value
2411
+ lines.push(`| \`${bracket}\` | ${required} | ${desc} |`)
2412
+ }
2413
+ lines.push('')
2414
+ }
2415
+
2416
+ // Command-specific options
2417
+ const cmdOptions = cmd.options.filter((o) => !o.deprecated)
2418
+ if (cmdOptions.length > 0) {
2419
+ lines.push('## Options')
2420
+ lines.push('')
2421
+ lines.push(formatOptionsTable(cmdOptions))
2422
+ lines.push('')
2423
+ }
2424
+
2425
+ // Global options section
2426
+ if (globalOptions.length > 0) {
2427
+ lines.push('## Global Options')
2428
+ lines.push('')
2429
+ lines.push(formatOptionsTable(globalOptions))
2430
+ lines.push('')
2431
+ }
2432
+
2433
+ // Examples
2434
+ if (cmd.examples.length > 0) {
2435
+ lines.push('## Examples')
2436
+ lines.push('')
2437
+ for (const example of cmd.examples) {
2438
+ const text = typeof example === 'function' ? example(cli.name) : example
2439
+ // Auto-wrap in ```sh if not already fenced
2440
+ if (text.trimStart().startsWith('```')) {
2441
+ lines.push(text)
2442
+ } else {
2443
+ lines.push('```sh')
2444
+ lines.push(text)
2445
+ lines.push('```')
2446
+ }
2447
+ lines.push('')
2448
+ }
2449
+ }
2450
+
2451
+ const slug = cmd.name.replace(/\s+/g, '-')
2452
+ pages.push({ command: cmd.name, slug, content: lines.join('\n') })
2453
+ }
2454
+
2455
+ return pages
2456
+ }
2457
+
2458
+ function formatOptionsTable(options: Option[]): string {
2459
+ const lines: string[] = []
2460
+ lines.push('| Option | Default | Description |')
2461
+ lines.push('|--------|---------|-------------|')
2462
+ for (const opt of options) {
2463
+ const defaultVal = opt.default !== undefined ? `\`${String(opt.default)}\`` : '-'
2464
+ // Escape pipe characters in description for markdown tables
2465
+ const desc = opt.description.replace(/\|/g, '\\|').replace(/\n/g, ' ')
2466
+ lines.push(`| \`${opt.rawName}\` | ${defaultVal} | ${desc} |`)
2467
+ }
2468
+ return lines.join('\n')
2469
+ }
2470
+
1946
2471
  // ─── Exports ───
1947
2472
 
1948
- export type { GokeOutputStream, GokeConsole, GokeOptions, GokeProcess, GokeExecutionContext, GokeExecutionContextOverride, GokeFs }
1949
- export { createConsole, Command, GokeProcessExit, openInBrowser }
2473
+ export type { GokeOutputStream, GokeConsole, GokeOptions, GokeProcess, GokeExecutionContext, GokeExecutionContextOverride, GokeFs, DocPage, GenerateDocsOptions }
2474
+ export { createConsole, Command, GokeProcessExit, openInBrowser, generateDocs }
2475
+ export type { ShellType }
2476
+ export { generateCompletionScript, installCompletions, uninstallCompletions, detectShell, detectCompletionShell, validateShell }
1950
2477
  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"
@@ -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
 
@@ -2,7 +2,7 @@
2
2
  * Node.js runtime bindings for goke core.
3
3
  */
4
4
 
5
- import { execSync } from 'child_process'
5
+ import { exec } from 'child_process'
6
6
  import { EventEmitter } from 'events'
7
7
  import * as nodeFs from 'node:fs/promises'
8
8
  import type { GokeFs } from './goke-fs.js'
@@ -10,22 +10,30 @@ import type { GokeFs } from './goke-fs.js'
10
10
  const process = globalThis.process
11
11
  const fs: GokeFs = nodeFs
12
12
 
13
- function openInBrowser(url: string): void {
13
+ async function openInBrowser(url: string): Promise<void> {
14
14
  if (!process.stdout.isTTY) {
15
- process.stdout.write(url + '\n')
15
+ process.stderr.write(url + '\n')
16
16
  return
17
17
  }
18
18
 
19
+ let cmd: string
20
+ if (process.platform === 'darwin') {
21
+ cmd = `open ${JSON.stringify(url)}`
22
+ } else if (process.platform === 'win32') {
23
+ cmd = `start "" ${JSON.stringify(url)}`
24
+ } else {
25
+ cmd = `xdg-open ${JSON.stringify(url)}`
26
+ }
27
+
19
28
  try {
20
- if (process.platform === 'darwin') {
21
- execSync(`open ${JSON.stringify(url)}`, { stdio: 'ignore' })
22
- } else if (process.platform === 'win32') {
23
- execSync(`start "" ${JSON.stringify(url)}`, { stdio: 'ignore' })
24
- } else {
25
- execSync(`xdg-open ${JSON.stringify(url)}`, { stdio: 'ignore' })
26
- }
29
+ await new Promise<void>((resolve, reject) => {
30
+ exec(cmd, { stdio: 'ignore' } as any, (err) => {
31
+ if (err) reject(err)
32
+ else resolve()
33
+ })
34
+ })
27
35
  } catch {
28
- process.stdout.write(url + '\n')
36
+ process.stderr.write(url + '\n')
29
37
  }
30
38
  }
31
39