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.
- package/dist/__test__/completions.test.d.ts +9 -0
- package/dist/__test__/completions.test.d.ts.map +1 -0
- package/dist/__test__/completions.test.js +774 -0
- package/dist/__test__/index.test.js +436 -308
- package/dist/__test__/just-bash.test.js +7 -7
- package/dist/__test__/readme-examples.test.js +149 -13
- package/dist/__test__/types.test-d.js +27 -0
- package/dist/agents.d.ts +38 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +63 -0
- package/dist/completions.d.ts +88 -0
- package/dist/completions.d.ts.map +1 -0
- package/dist/completions.js +315 -0
- package/dist/goke.d.ts +95 -5
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +487 -4
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/just-bash.d.ts.map +1 -1
- package/dist/just-bash.js +1 -2
- package/dist/runtime-browser.d.ts +1 -1
- package/dist/runtime-browser.d.ts.map +1 -1
- package/dist/runtime-browser.js +1 -1
- package/dist/runtime-node.d.ts +1 -1
- package/dist/runtime-node.d.ts.map +1 -1
- package/dist/runtime-node.js +22 -13
- package/package.json +1 -1
- package/src/__test__/completions.test.ts +902 -0
- package/src/__test__/index.test.ts +471 -308
- package/src/__test__/just-bash.test.ts +7 -7
- package/src/__test__/readme-examples.test.ts +161 -13
- package/src/__test__/types.test-d.ts +27 -0
- package/src/agents.ts +101 -0
- package/src/completions.ts +363 -0
- package/src/goke.ts +540 -8
- package/src/index.ts +11 -2
- package/src/just-bash.ts +1 -2
- package/src/runtime-browser.ts +1 -1
- 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
|
|
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
|
-
*
|
|
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
|
|
327
|
-
await cloned.runMatchedCommand()
|
|
326
|
+
await cloned.parse(argv)
|
|
328
327
|
const result = output.getResult()
|
|
329
328
|
return {
|
|
330
329
|
stdout: result.stdout,
|
package/src/runtime-browser.ts
CHANGED
|
@@ -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
|
|