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/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 +64 -0
- package/dist/__test__/readme-examples.test.js +141 -5
- 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 +92 -2
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +479 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- 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 +75 -0
- package/src/__test__/readme-examples.test.ts +153 -5
- 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 +529 -2
- package/src/index.ts +11 -2
- package/src/runtime-browser.ts +1 -1
- package/src/runtime-node.ts +19 -11
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion support for goke CLIs.
|
|
3
|
+
*
|
|
4
|
+
* Two pieces work together:
|
|
5
|
+
* 1. A hidden `--get-goke-completions` flag in the CLI binary. When present,
|
|
6
|
+
* the CLI skips normal execution and prints matching completions to stdout.
|
|
7
|
+
* 2. A shell-specific shim script installed into an fpath/completion directory.
|
|
8
|
+
* On each Tab press the shell calls the CLI binary with the flag + current words.
|
|
9
|
+
*
|
|
10
|
+
* The `installCompletions` function finds a writable completion directory and
|
|
11
|
+
* writes the shim. Since the shim calls the binary on every Tab, completions
|
|
12
|
+
* are always up-to-date with the installed CLI.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { process } from '#runtime'
|
|
16
|
+
|
|
17
|
+
// ─── Constants ───
|
|
18
|
+
|
|
19
|
+
/** The hidden flag the shell script passes to the CLI on each Tab press */
|
|
20
|
+
export const COMPLETION_FLAG = 'get-goke-completions'
|
|
21
|
+
|
|
22
|
+
// ─── Shell templates ───
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Zsh completion script template.
|
|
26
|
+
*
|
|
27
|
+
* The `#compdef` header lets zsh autoload this as an fpath function.
|
|
28
|
+
* On Tab press it calls the CLI binary with `--get-goke-completions` and
|
|
29
|
+
* all typed words. The binary returns `name:description` pairs (one per line)
|
|
30
|
+
* and zsh renders them with `_describe`.
|
|
31
|
+
*/
|
|
32
|
+
export const zshTemplate = `#compdef {{app_name}}
|
|
33
|
+
###-begin-{{app_name}}-completions-###
|
|
34
|
+
_{{app_name_safe}}_completions() {
|
|
35
|
+
local reply
|
|
36
|
+
local si=$IFS
|
|
37
|
+
IFS=$'\\n' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" GOKE_COMPLETION_SHELL=zsh {{app_path}} --get-goke-completions "\${words[@]}"))
|
|
38
|
+
IFS=$si
|
|
39
|
+
if [[ \${#reply} -gt 0 ]]; then
|
|
40
|
+
_describe 'values' reply
|
|
41
|
+
else
|
|
42
|
+
_default
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
if [[ "'\${zsh_eval_context[-1]}" == "loadautofunc" ]]; then
|
|
46
|
+
_{{app_name_safe}}_completions "$@"
|
|
47
|
+
else
|
|
48
|
+
compdef _{{app_name_safe}}_completions {{app_name}}
|
|
49
|
+
fi
|
|
50
|
+
###-end-{{app_name}}-completions-###
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Bash completion script template.
|
|
55
|
+
*
|
|
56
|
+
* On Tab press bash calls the function which invokes the CLI binary with
|
|
57
|
+
* `--get-goke-completions` and all typed words. The binary returns plain
|
|
58
|
+
* completion strings (one per line).
|
|
59
|
+
*/
|
|
60
|
+
export const bashTemplate = `###-begin-{{app_name}}-completions-###
|
|
61
|
+
_{{app_name_safe}}_completions()
|
|
62
|
+
{
|
|
63
|
+
local cur_word args type_list
|
|
64
|
+
|
|
65
|
+
cur_word="\${COMP_WORDS[COMP_CWORD]}"
|
|
66
|
+
args=("\${COMP_WORDS[@]}")
|
|
67
|
+
|
|
68
|
+
# Bash 3 compatible (no mapfile). Works on macOS default bash.
|
|
69
|
+
local IFS=$'\\n'
|
|
70
|
+
type_list=($(GOKE_COMPLETION_SHELL=bash {{app_path}} --get-goke-completions "\${args[@]}"))
|
|
71
|
+
unset IFS
|
|
72
|
+
COMPREPLY=($(compgen -W "$( printf '%q ' "\${type_list[@]}" )" -- "\${cur_word}" |
|
|
73
|
+
awk '/ / { print "\\""$0"\\"" } /^[^ ]+$/ { print $0 }'))
|
|
74
|
+
|
|
75
|
+
if [ \${#COMPREPLY[@]} -eq 0 ]; then
|
|
76
|
+
COMPREPLY=()
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
complete -o bashdefault -o default -F _{{app_name_safe}}_completions {{app_name}}
|
|
82
|
+
###-end-{{app_name}}-completions-###
|
|
83
|
+
`
|
|
84
|
+
|
|
85
|
+
// ─── Script generation ───
|
|
86
|
+
|
|
87
|
+
export type ShellType = 'zsh' | 'bash'
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detect the current shell from environment variables.
|
|
91
|
+
* Returns 'zsh', 'bash', or null if unrecognized.
|
|
92
|
+
*/
|
|
93
|
+
export function detectShell(): ShellType | null {
|
|
94
|
+
const shell = process.env.SHELL ?? ''
|
|
95
|
+
if (shell.includes('zsh')) return 'zsh'
|
|
96
|
+
if (shell.includes('bash')) return 'bash'
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate and normalize a shell value from user input.
|
|
102
|
+
* Returns a valid ShellType or throws if the value is invalid.
|
|
103
|
+
*/
|
|
104
|
+
export function validateShell(value: unknown): ShellType | undefined {
|
|
105
|
+
if (value == null || value === '') return undefined
|
|
106
|
+
if (value === 'zsh' || value === 'bash') return value
|
|
107
|
+
throw new Error(`Invalid shell "${String(value)}". Expected "zsh" or "bash".`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect which shell format to use for completion output.
|
|
112
|
+
* Prefers the explicit GOKE_COMPLETION_SHELL env var (set by the shell shim)
|
|
113
|
+
* over the login $SHELL. This prevents format mismatch when a bash shim runs
|
|
114
|
+
* on a system where $SHELL is zsh.
|
|
115
|
+
*/
|
|
116
|
+
export function detectCompletionShell(): ShellType | null {
|
|
117
|
+
const explicit = process.env.GOKE_COMPLETION_SHELL
|
|
118
|
+
if (explicit === 'zsh' || explicit === 'bash') return explicit
|
|
119
|
+
return detectShell()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Make a string safe for use as a shell function name.
|
|
124
|
+
* Replaces non-alphanumeric chars with underscores.
|
|
125
|
+
*/
|
|
126
|
+
function safeShellName(name: string): string {
|
|
127
|
+
return name.replace(/[^a-zA-Z0-9]/g, '_')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Generate a completion script for the given shell.
|
|
132
|
+
*
|
|
133
|
+
* @param shell - Target shell ('zsh' or 'bash')
|
|
134
|
+
* @param cliName - The CLI binary name (e.g. 'my-cli')
|
|
135
|
+
* @param cliPath - Full path to the CLI binary. If not provided, uses cliName.
|
|
136
|
+
*/
|
|
137
|
+
export function generateCompletionScript(
|
|
138
|
+
shell: ShellType,
|
|
139
|
+
cliName: string,
|
|
140
|
+
cliPath?: string,
|
|
141
|
+
): string {
|
|
142
|
+
const template = shell === 'zsh' ? zshTemplate : bashTemplate
|
|
143
|
+
const path = cliPath ?? cliName
|
|
144
|
+
const safeName = safeShellName(cliName)
|
|
145
|
+
|
|
146
|
+
return template
|
|
147
|
+
.replace(/{{app_name}}/g, cliName)
|
|
148
|
+
.replace(/{{app_name_safe}}/g, safeName)
|
|
149
|
+
.replace(/{{app_path}}/g, path)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── Well-known completion directories ───
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Well-known zsh fpath directories, ordered by preference.
|
|
156
|
+
* The first writable one wins.
|
|
157
|
+
*/
|
|
158
|
+
const ZSH_FPATH_CANDIDATES = [
|
|
159
|
+
// Homebrew (macOS arm64) — user-writable
|
|
160
|
+
'/opt/homebrew/share/zsh/site-functions',
|
|
161
|
+
// Homebrew (macOS x86) / Linux system-wide
|
|
162
|
+
'/usr/local/share/zsh/site-functions',
|
|
163
|
+
// OS vendor
|
|
164
|
+
'/usr/share/zsh/site-functions',
|
|
165
|
+
// User-level fallback (no sudo needed, but user must add to fpath in .zshrc)
|
|
166
|
+
`${process.env.HOME}/.zsh/completions`,
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Well-known bash completion directories, ordered by preference.
|
|
171
|
+
*/
|
|
172
|
+
const BASH_COMPLETION_CANDIDATES = [
|
|
173
|
+
// XDG standard (user-writable)
|
|
174
|
+
`${process.env.HOME}/.local/share/bash-completion/completions`,
|
|
175
|
+
// Legacy user dir
|
|
176
|
+
`${process.env.HOME}/.bash_completion.d`,
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
// ─── Installation ───
|
|
180
|
+
|
|
181
|
+
interface InstallResult {
|
|
182
|
+
/** The file path where the completion script was written */
|
|
183
|
+
path: string
|
|
184
|
+
/** The shell the script was generated for */
|
|
185
|
+
shell: ShellType
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Find a writable completion directory, generate the shell script, and write it.
|
|
190
|
+
*
|
|
191
|
+
* For zsh, scans `$fpath` directories plus well-known fallbacks.
|
|
192
|
+
* For bash, scans XDG and legacy user completion dirs.
|
|
193
|
+
*
|
|
194
|
+
* Throws if no writable directory is found.
|
|
195
|
+
*
|
|
196
|
+
* @param cliName - The CLI binary name
|
|
197
|
+
* @param cliPath - Full path to the CLI binary
|
|
198
|
+
* @param shell - Target shell. Auto-detected from $SHELL if omitted.
|
|
199
|
+
*/
|
|
200
|
+
export async function installCompletions(
|
|
201
|
+
cliName: string,
|
|
202
|
+
cliPath: string,
|
|
203
|
+
shell?: ShellType,
|
|
204
|
+
): Promise<InstallResult> {
|
|
205
|
+
const { existsSync, mkdirSync, writeFileSync, accessSync, constants } = await import('node:fs')
|
|
206
|
+
const { execSync } = await import('node:child_process')
|
|
207
|
+
const { join } = await import('node:path')
|
|
208
|
+
|
|
209
|
+
const targetShell = shell ?? detectShell()
|
|
210
|
+
if (!targetShell) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
'Could not detect shell. Set the SHELL environment variable or pass --shell explicitly.',
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const script = generateCompletionScript(targetShell, cliName, cliPath)
|
|
217
|
+
|
|
218
|
+
// Build candidate directories
|
|
219
|
+
let candidates: string[]
|
|
220
|
+
let filename: string
|
|
221
|
+
|
|
222
|
+
if (targetShell === 'zsh') {
|
|
223
|
+
filename = `_${cliName}`
|
|
224
|
+
|
|
225
|
+
// Get live $fpath from zsh if available
|
|
226
|
+
let fpathDirs: string[] = []
|
|
227
|
+
try {
|
|
228
|
+
const fpathOutput = execSync('zsh -c "echo $fpath"', {
|
|
229
|
+
encoding: 'utf-8',
|
|
230
|
+
timeout: 5000,
|
|
231
|
+
}).trim()
|
|
232
|
+
fpathDirs = fpathOutput.split(/\s+/).filter(Boolean)
|
|
233
|
+
} catch {
|
|
234
|
+
// zsh not available, fall through to well-known paths
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Deduplicate: fpath dirs first (in order), then well-known fallbacks
|
|
238
|
+
const seen = new Set<string>()
|
|
239
|
+
candidates = []
|
|
240
|
+
const userZshDir = `${process.env.HOME}/.zsh/completions`
|
|
241
|
+
for (const dir of [...fpathDirs, ...ZSH_FPATH_CANDIDATES]) {
|
|
242
|
+
if (seen.has(dir)) continue
|
|
243
|
+
seen.add(dir)
|
|
244
|
+
// Auto-create the user-level ~/.zsh/completions dir if it's a candidate
|
|
245
|
+
if (!existsSync(dir) && dir === userZshDir) {
|
|
246
|
+
try {
|
|
247
|
+
mkdirSync(dir, { recursive: true })
|
|
248
|
+
} catch {
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (existsSync(dir)) {
|
|
253
|
+
candidates.push(dir)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
filename = cliName
|
|
258
|
+
candidates = BASH_COMPLETION_CANDIDATES.filter((dir) => {
|
|
259
|
+
// For bash dirs, check parent exists or try creating
|
|
260
|
+
try {
|
|
261
|
+
if (!existsSync(dir)) {
|
|
262
|
+
mkdirSync(dir, { recursive: true })
|
|
263
|
+
}
|
|
264
|
+
return true
|
|
265
|
+
} catch {
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Find first writable directory
|
|
272
|
+
for (const dir of candidates) {
|
|
273
|
+
try {
|
|
274
|
+
accessSync(dir, constants.W_OK)
|
|
275
|
+
const filePath = join(dir, filename)
|
|
276
|
+
writeFileSync(filePath, script, 'utf-8')
|
|
277
|
+
return { path: filePath, shell: targetShell }
|
|
278
|
+
} catch {
|
|
279
|
+
// Not writable, try next
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// No writable directory found
|
|
285
|
+
const triedPaths = candidates.length > 0
|
|
286
|
+
? candidates.map((d) => ` - ${d}`).join('\n')
|
|
287
|
+
: ' (none found)'
|
|
288
|
+
|
|
289
|
+
const hint = targetShell === 'zsh'
|
|
290
|
+
? 'Create ~/.zsh/completions and add `fpath=(~/.zsh/completions $fpath)` to your .zshrc, then run this command again.'
|
|
291
|
+
: 'Create ~/.local/share/bash-completion/completions and run this command again.'
|
|
292
|
+
|
|
293
|
+
throw new Error(
|
|
294
|
+
`No writable ${targetShell} completion directory found.\n\nTried:\n${triedPaths}\n\n${hint}`,
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Remove an installed completion script.
|
|
300
|
+
*
|
|
301
|
+
* Searches the same candidate directories as `installCompletions` and removes
|
|
302
|
+
* any matching completion files found.
|
|
303
|
+
*
|
|
304
|
+
* @returns The paths that were removed, or empty array if none found.
|
|
305
|
+
*/
|
|
306
|
+
export async function uninstallCompletions(
|
|
307
|
+
cliName: string,
|
|
308
|
+
shell?: ShellType,
|
|
309
|
+
): Promise<string[]> {
|
|
310
|
+
const { existsSync, unlinkSync } = await import('node:fs')
|
|
311
|
+
const { execSync } = await import('node:child_process')
|
|
312
|
+
const { join } = await import('node:path')
|
|
313
|
+
|
|
314
|
+
const targetShell = shell ?? detectShell()
|
|
315
|
+
if (!targetShell) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
'Could not detect shell. Set the SHELL environment variable or pass --shell explicitly.',
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let candidates: string[]
|
|
322
|
+
let filename: string
|
|
323
|
+
|
|
324
|
+
if (targetShell === 'zsh') {
|
|
325
|
+
filename = `_${cliName}`
|
|
326
|
+
let fpathDirs: string[] = []
|
|
327
|
+
try {
|
|
328
|
+
const fpathOutput = execSync('zsh -c "echo $fpath"', {
|
|
329
|
+
encoding: 'utf-8',
|
|
330
|
+
timeout: 5000,
|
|
331
|
+
}).trim()
|
|
332
|
+
fpathDirs = fpathOutput.split(/\s+/).filter(Boolean)
|
|
333
|
+
} catch {
|
|
334
|
+
// zsh not available
|
|
335
|
+
}
|
|
336
|
+
const seen = new Set<string>()
|
|
337
|
+
candidates = []
|
|
338
|
+
for (const dir of [...fpathDirs, ...ZSH_FPATH_CANDIDATES]) {
|
|
339
|
+
if (!seen.has(dir)) {
|
|
340
|
+
seen.add(dir)
|
|
341
|
+
candidates.push(dir)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
filename = cliName
|
|
346
|
+
candidates = [...BASH_COMPLETION_CANDIDATES]
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const removed: string[] = []
|
|
350
|
+
for (const dir of candidates) {
|
|
351
|
+
const filePath = join(dir, filename)
|
|
352
|
+
if (existsSync(filePath)) {
|
|
353
|
+
try {
|
|
354
|
+
unlinkSync(filePath)
|
|
355
|
+
removed.push(filePath)
|
|
356
|
+
} catch {
|
|
357
|
+
// Permission denied, skip
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return removed
|
|
363
|
+
}
|