@brianli/kimaki 0.4.72-brianli.1
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/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,1362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goke — a cac-inspired CLI framework.
|
|
3
|
+
*
|
|
4
|
+
* This file contains the entire core framework:
|
|
5
|
+
* - Option: CLI option parsing (flags, required/optional values)
|
|
6
|
+
* - Command / GlobalCommand: command definition, help/version output
|
|
7
|
+
* - Goke: main CLI class with parsing, matching, and execution
|
|
8
|
+
* - GokeOutputStream / GokeConsole / GokeOptions: injectable I/O
|
|
9
|
+
* - createConsole: factory for console-like objects from output streams
|
|
10
|
+
* - Utility functions: string helpers, bracket parsing, dot-prop access
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { EventEmitter } from 'events'
|
|
14
|
+
import pc from 'picocolors'
|
|
15
|
+
import mri from "./mri.js"
|
|
16
|
+
import { GokeError, coerceBySchema, extractJsonSchema, extractSchemaMetadata, isStandardSchema } from "./coerce.js"
|
|
17
|
+
import type { StandardJSONSchemaV1 } from "./coerce.js"
|
|
18
|
+
|
|
19
|
+
// ─── Node.js platform constants ───
|
|
20
|
+
|
|
21
|
+
const processArgs = process.argv
|
|
22
|
+
const platformInfo = `${process.platform}-${process.arch} node-${process.version}`
|
|
23
|
+
|
|
24
|
+
// ─── Utility functions ───
|
|
25
|
+
|
|
26
|
+
const removeBrackets = (v: string) => v.replace(/[<[].+/, '').trim()
|
|
27
|
+
|
|
28
|
+
const findAllBrackets = (v: string) => {
|
|
29
|
+
const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g
|
|
30
|
+
const SQUARE_BRACKET_RE_GLOBAL = /\[([^\]]+)\]/g
|
|
31
|
+
|
|
32
|
+
const res: CommandArg[] = []
|
|
33
|
+
|
|
34
|
+
const parse = (match: string[]) => {
|
|
35
|
+
let variadic = false
|
|
36
|
+
let value = match[1]
|
|
37
|
+
if (value.startsWith('...')) {
|
|
38
|
+
value = value.slice(3)
|
|
39
|
+
variadic = true
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
required: match[0].startsWith('<'),
|
|
43
|
+
value,
|
|
44
|
+
variadic
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let angledMatch
|
|
49
|
+
while ((angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v))) {
|
|
50
|
+
res.push(parse(angledMatch))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let squareMatch
|
|
54
|
+
while ((squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v))) {
|
|
55
|
+
res.push(parse(squareMatch))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return res
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface MriOptionsConfig {
|
|
62
|
+
alias: { [k: string]: string[] }
|
|
63
|
+
boolean: string[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const getMriOptions = (options: Option[]) => {
|
|
67
|
+
const result: MriOptionsConfig = { alias: {}, boolean: [] }
|
|
68
|
+
|
|
69
|
+
for (const option of options) {
|
|
70
|
+
// We do not set default values in mri options
|
|
71
|
+
// Since its type (typeof) will be used to cast parsed arguments.
|
|
72
|
+
// Which mean `--foo foo` will be parsed as `{foo: true}` if we have `{default:{foo: true}}`
|
|
73
|
+
|
|
74
|
+
// Set alias
|
|
75
|
+
if (option.names.length > 1) {
|
|
76
|
+
result.alias[option.names[0]] = option.names.slice(1)
|
|
77
|
+
}
|
|
78
|
+
// Set boolean
|
|
79
|
+
if (option.isBoolean) {
|
|
80
|
+
result.boolean.push(option.names[0])
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const maxVisibleLength = (arr: string[]) => {
|
|
88
|
+
return arr.reduce((max, value) => {
|
|
89
|
+
return Math.max(max, visibleLength(value))
|
|
90
|
+
}, 0)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const ANSI_RE = /\x1B\[[0-9;]*m/g
|
|
94
|
+
|
|
95
|
+
const visibleLength = (value: string) => value.replace(ANSI_RE, '').length
|
|
96
|
+
|
|
97
|
+
const commandGreen = (value: string) => pc.bold(pc.greenBright(value))
|
|
98
|
+
|
|
99
|
+
const optionBlue = (value: string) => pc.bold(pc.blueBright(value))
|
|
100
|
+
|
|
101
|
+
const padRight = (str: string, length: number) => {
|
|
102
|
+
return visibleLength(str) >= length ? str : `${str}${' '.repeat(length - visibleLength(str))}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const wrapLine = (line: string, width: number) => {
|
|
106
|
+
if (width <= 0 || visibleLength(line) <= width) {
|
|
107
|
+
return [line]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const words = line.trim().split(/\s+/)
|
|
111
|
+
const wrapped: string[] = []
|
|
112
|
+
let current = ''
|
|
113
|
+
|
|
114
|
+
for (const word of words) {
|
|
115
|
+
const next = current ? `${current} ${word}` : word
|
|
116
|
+
if (visibleLength(next) <= width) {
|
|
117
|
+
current = next
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (current) {
|
|
122
|
+
wrapped.push(current)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (visibleLength(word) <= width) {
|
|
126
|
+
current = word
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let remaining = word
|
|
131
|
+
while (visibleLength(remaining) > width) {
|
|
132
|
+
wrapped.push(remaining.slice(0, width))
|
|
133
|
+
remaining = remaining.slice(width)
|
|
134
|
+
}
|
|
135
|
+
current = remaining
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (current) {
|
|
139
|
+
wrapped.push(current)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return wrapped
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const wrapDescription = (text: string, width: number) => {
|
|
146
|
+
const maxWidth = Math.max(20, width)
|
|
147
|
+
return text
|
|
148
|
+
.split('\n')
|
|
149
|
+
.flatMap((line) => {
|
|
150
|
+
if (line.trim() === '') {
|
|
151
|
+
return ['']
|
|
152
|
+
}
|
|
153
|
+
return wrapLine(line, maxWidth)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const formatWrappedDescription = (text: string, width: number, indent: number) => {
|
|
158
|
+
const lines = wrapDescription(text, width)
|
|
159
|
+
.map((line) => (line ? pc.dim(line) : line))
|
|
160
|
+
if (lines.length === 0) {
|
|
161
|
+
return ''
|
|
162
|
+
}
|
|
163
|
+
return [
|
|
164
|
+
lines[0],
|
|
165
|
+
...lines.slice(1).map((line) => `${' '.repeat(indent)}${line}`),
|
|
166
|
+
].join('\n')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const optionDescriptionText = (option: Option) => {
|
|
170
|
+
const defaultText = option.default === undefined
|
|
171
|
+
? ''
|
|
172
|
+
: ` ${pc.cyan(`(default: ${String(option.default)})`)}`
|
|
173
|
+
return `${option.description}${defaultText}`.trim()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const camelcase = (input: string) => {
|
|
177
|
+
return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => {
|
|
178
|
+
return p1 + p2.toUpperCase()
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const setDotProp = (
|
|
183
|
+
obj: { [k: string]: any },
|
|
184
|
+
keys: string[],
|
|
185
|
+
val: any
|
|
186
|
+
) => {
|
|
187
|
+
let i = 0
|
|
188
|
+
let length = keys.length
|
|
189
|
+
let t = obj
|
|
190
|
+
let x
|
|
191
|
+
for (; i < length; ++i) {
|
|
192
|
+
x = t[keys[i]]
|
|
193
|
+
t = t[keys[i]] =
|
|
194
|
+
i === length - 1
|
|
195
|
+
? val
|
|
196
|
+
: x != null
|
|
197
|
+
? x
|
|
198
|
+
: !!~keys[i + 1].indexOf('.') || !(+keys[i + 1] > -1)
|
|
199
|
+
? {}
|
|
200
|
+
: []
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const getFileName = (input: string) => {
|
|
205
|
+
const m = /([^\\\/]+)$/.exec(input)
|
|
206
|
+
return m ? m[1] : ''
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const camelcaseOptionName = (name: string) => {
|
|
210
|
+
// Camelcase the option name
|
|
211
|
+
// Don't camelcase anything after the dot `.`
|
|
212
|
+
return name
|
|
213
|
+
.split('.')
|
|
214
|
+
.map((v, i) => {
|
|
215
|
+
return i === 0 ? camelcase(v) : v
|
|
216
|
+
})
|
|
217
|
+
.join('.')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── Option ───
|
|
221
|
+
|
|
222
|
+
class Option {
|
|
223
|
+
/** Option name */
|
|
224
|
+
name: string
|
|
225
|
+
/** Option name and aliases */
|
|
226
|
+
names: string[]
|
|
227
|
+
isBoolean?: boolean
|
|
228
|
+
// `required` will be a boolean for options with brackets
|
|
229
|
+
required?: boolean
|
|
230
|
+
/** Description text for help output */
|
|
231
|
+
description: string
|
|
232
|
+
/** Default value for this option */
|
|
233
|
+
default?: unknown
|
|
234
|
+
/** Standard JSON Schema V1 schema for type coercion and inference */
|
|
235
|
+
schema?: StandardJSONSchemaV1
|
|
236
|
+
/** Whether this option is deprecated (hidden from help output) */
|
|
237
|
+
deprecated?: boolean
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create an option.
|
|
241
|
+
* @param rawName - The raw option string, e.g. '--port <port>', '-v, --verbose'
|
|
242
|
+
* @param descriptionOrSchema - Either a description string or a StandardJSONSchemaV1 schema.
|
|
243
|
+
* When a schema is provided, description and default are extracted from the JSON Schema.
|
|
244
|
+
*/
|
|
245
|
+
constructor(
|
|
246
|
+
public rawName: string,
|
|
247
|
+
descriptionOrSchema?: string | StandardJSONSchemaV1,
|
|
248
|
+
) {
|
|
249
|
+
if (typeof descriptionOrSchema === 'string') {
|
|
250
|
+
this.description = descriptionOrSchema
|
|
251
|
+
} else if (descriptionOrSchema && isStandardSchema(descriptionOrSchema)) {
|
|
252
|
+
this.schema = descriptionOrSchema
|
|
253
|
+
const meta = extractSchemaMetadata(descriptionOrSchema)
|
|
254
|
+
this.description = meta.description ?? ''
|
|
255
|
+
if (meta.default !== undefined) {
|
|
256
|
+
this.default = meta.default
|
|
257
|
+
}
|
|
258
|
+
if (meta.deprecated) {
|
|
259
|
+
this.deprecated = true
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
this.description = ''
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// You may use cli.option('--env.* [value]', 'desc') to denote a dot-nested option
|
|
266
|
+
rawName = rawName.replace(/\.\*/g, '')
|
|
267
|
+
|
|
268
|
+
this.names = removeBrackets(rawName)
|
|
269
|
+
.split(',')
|
|
270
|
+
.map((v: string) => {
|
|
271
|
+
let name = v.trim().replace(/^-{1,2}/, '')
|
|
272
|
+
return camelcaseOptionName(name)
|
|
273
|
+
})
|
|
274
|
+
.sort((a, b) => (a.length > b.length ? 1 : -1)) // Sort names
|
|
275
|
+
|
|
276
|
+
// Use the longest name (last one) as actual option name
|
|
277
|
+
this.name = this.names[this.names.length - 1]
|
|
278
|
+
|
|
279
|
+
if (rawName.includes('<')) {
|
|
280
|
+
this.required = true
|
|
281
|
+
} else if (rawName.includes('[')) {
|
|
282
|
+
this.required = false
|
|
283
|
+
} else {
|
|
284
|
+
// No arg needed, it's boolean flag
|
|
285
|
+
this.isBoolean = true
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Command ───
|
|
291
|
+
|
|
292
|
+
// Type-level helpers for inferring option names and types
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Converts a kebab-case string to camelCase at the type level.
|
|
296
|
+
* "--foo-bar <val>" → name "foo-bar" → camelCase "fooBar"
|
|
297
|
+
*/
|
|
298
|
+
type CamelCase<S extends string> =
|
|
299
|
+
S extends `${infer L}-${infer R}`
|
|
300
|
+
? `${L}${CamelCase<Capitalize<R>>}`
|
|
301
|
+
: S
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Extracts the long option name from a raw option string.
|
|
305
|
+
* "-p, --port <port>" → "port"
|
|
306
|
+
* "--foo-bar <val>" → "fooBar"
|
|
307
|
+
* "--verbose" → "verbose"
|
|
308
|
+
*/
|
|
309
|
+
type ExtractOptionName<S extends string> =
|
|
310
|
+
// Match: --name <value> or --name [value] or --name
|
|
311
|
+
S extends `${string}--${infer Name} <${string}>` ? CamelCase<Name> :
|
|
312
|
+
S extends `${string}--${infer Name} [${string}]` ? CamelCase<Name> :
|
|
313
|
+
S extends `${string}--${infer Name}` ? CamelCase<Name> :
|
|
314
|
+
string
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Determines if an option takes a required value (<...>) vs optional ([...]) vs boolean flag.
|
|
318
|
+
*/
|
|
319
|
+
type IsOptionalOption<S extends string> =
|
|
320
|
+
S extends `${string}<${string}>` ? false :
|
|
321
|
+
true
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Infer the output type from a StandardTypedV1-compatible schema.
|
|
325
|
+
*/
|
|
326
|
+
type InferSchemaOutput<S> =
|
|
327
|
+
S extends { readonly "~standard": { readonly types?: { readonly output: infer O } } } ? O : unknown
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Build the option type entry for a single .option() call.
|
|
331
|
+
* Required options (<...>) produce a required key.
|
|
332
|
+
* Optional options ([...]) and boolean flags produce an optional key.
|
|
333
|
+
*/
|
|
334
|
+
type OptionEntry<RawName extends string, Schema> =
|
|
335
|
+
IsOptionalOption<RawName> extends true
|
|
336
|
+
? { [K in ExtractOptionName<RawName>]?: InferSchemaOutput<Schema> }
|
|
337
|
+
: { [K in ExtractOptionName<RawName>]: InferSchemaOutput<Schema> }
|
|
338
|
+
|
|
339
|
+
interface CommandArg {
|
|
340
|
+
required: boolean
|
|
341
|
+
value: string
|
|
342
|
+
variadic: boolean
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
interface HelpSection {
|
|
346
|
+
title?: string
|
|
347
|
+
body: string
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
interface CommandConfig {
|
|
351
|
+
allowUnknownOptions?: boolean
|
|
352
|
+
ignoreOptionDefaultValue?: boolean
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
type HelpCallback = (sections: HelpSection[]) => void | HelpSection[]
|
|
356
|
+
|
|
357
|
+
type CommandExample = ((bin: string) => string) | string
|
|
358
|
+
|
|
359
|
+
class Command {
|
|
360
|
+
options: Option[]
|
|
361
|
+
aliasNames: string[]
|
|
362
|
+
/* Parsed command name */
|
|
363
|
+
name: string
|
|
364
|
+
args: CommandArg[]
|
|
365
|
+
commandAction?: (...args: any[]) => any
|
|
366
|
+
usageText?: string
|
|
367
|
+
versionNumber?: string
|
|
368
|
+
examples: CommandExample[]
|
|
369
|
+
helpCallback?: HelpCallback
|
|
370
|
+
globalCommand?: GlobalCommand
|
|
371
|
+
|
|
372
|
+
constructor(
|
|
373
|
+
public rawName: string,
|
|
374
|
+
public description: string,
|
|
375
|
+
public config: CommandConfig = {},
|
|
376
|
+
public cli: Goke
|
|
377
|
+
) {
|
|
378
|
+
this.options = []
|
|
379
|
+
this.aliasNames = []
|
|
380
|
+
this.name = removeBrackets(rawName)
|
|
381
|
+
this.args = findAllBrackets(rawName)
|
|
382
|
+
this.examples = []
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
usage(text: string) {
|
|
386
|
+
this.usageText = text
|
|
387
|
+
return this
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
allowUnknownOptions() {
|
|
391
|
+
this.config.allowUnknownOptions = true
|
|
392
|
+
return this
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
ignoreOptionDefaultValue() {
|
|
396
|
+
this.config.ignoreOptionDefaultValue = true
|
|
397
|
+
return this
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
version(version: string, customFlags = '-v, --version') {
|
|
401
|
+
this.versionNumber = version
|
|
402
|
+
this.option(customFlags, 'Display version number')
|
|
403
|
+
return this
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
example(example: CommandExample) {
|
|
407
|
+
this.examples.push(example)
|
|
408
|
+
return this
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Add an option for this command.
|
|
413
|
+
*
|
|
414
|
+
* The second argument is either a description string or a StandardJSONSchemaV1
|
|
415
|
+
* schema. When a schema is provided, description and default are extracted from
|
|
416
|
+
* the JSON Schema automatically.
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```ts
|
|
420
|
+
* // With Zod schema (description + default extracted from schema):
|
|
421
|
+
* cmd.option('--port <port>', z.number().describe('Port number'))
|
|
422
|
+
*
|
|
423
|
+
* // Without schema (plain description, values are raw strings/booleans):
|
|
424
|
+
* cmd.option('--verbose', 'Verbose output')
|
|
425
|
+
* ```
|
|
426
|
+
*/
|
|
427
|
+
option<
|
|
428
|
+
RawName extends string,
|
|
429
|
+
S extends StandardJSONSchemaV1
|
|
430
|
+
>(rawName: RawName, schema: S): Command & { __opts: OptionEntry<RawName, S> }
|
|
431
|
+
option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): this
|
|
432
|
+
option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): any {
|
|
433
|
+
const option = new Option(rawName, descriptionOrSchema)
|
|
434
|
+
this.options.push(option)
|
|
435
|
+
return this
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
alias(name: string) {
|
|
439
|
+
this.aliasNames.push(name)
|
|
440
|
+
return this
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
action(callback: (...args: any[]) => any) {
|
|
444
|
+
this.commandAction = callback
|
|
445
|
+
return this
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
isMatched(args: string[]): { matched: boolean; consumedArgs: number } {
|
|
449
|
+
const nameParts = this.name.split(' ').filter(Boolean)
|
|
450
|
+
|
|
451
|
+
if (nameParts.length === 0) {
|
|
452
|
+
return { matched: false, consumedArgs: 0 }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (args.length < nameParts.length) {
|
|
456
|
+
return { matched: false, consumedArgs: 0 }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
for (let i = 0; i < nameParts.length; i++) {
|
|
460
|
+
if (nameParts[i] !== args[i]) {
|
|
461
|
+
if (i === 0 && this.aliasNames.includes(args[i])) {
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
return { matched: false, consumedArgs: 0 }
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return { matched: true, consumedArgs: nameParts.length }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
get isDefaultCommand() {
|
|
472
|
+
return this.name === '' || this.aliasNames.includes('!')
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
get isGlobalCommand(): boolean {
|
|
476
|
+
return this instanceof GlobalCommand
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Check if an option is registered in this command
|
|
481
|
+
* @param name Option name
|
|
482
|
+
*/
|
|
483
|
+
hasOption(name: string) {
|
|
484
|
+
name = name.split('.')[0]
|
|
485
|
+
return this.options.find((option) => {
|
|
486
|
+
return option.names.includes(name)
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
outputHelp() {
|
|
491
|
+
const { name, commands } = this.cli
|
|
492
|
+
const {
|
|
493
|
+
versionNumber,
|
|
494
|
+
options: globalOptions,
|
|
495
|
+
helpCallback,
|
|
496
|
+
} = this.cli.globalCommand
|
|
497
|
+
|
|
498
|
+
let sections: HelpSection[] = [
|
|
499
|
+
{
|
|
500
|
+
body: pc.bold(pc.cyan(`${name}${versionNumber ? `/${versionNumber}` : ''}`)),
|
|
501
|
+
},
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
sections.push({
|
|
505
|
+
title: 'Usage',
|
|
506
|
+
body: ` ${pc.green('$')} ${pc.bold(name)} ${this.usageText || this.rawName || '[options]'}`,
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
const showCommands =
|
|
510
|
+
(this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0
|
|
511
|
+
const terminalWidth = Math.max(this.cli.columns, 40)
|
|
512
|
+
|
|
513
|
+
if (showCommands) {
|
|
514
|
+
const commandRows = commands.map((command) => {
|
|
515
|
+
const displayName = command.rawName.trim() === '' ? name : command.rawName
|
|
516
|
+
// Hide deprecated options from subcommand help output
|
|
517
|
+
const displayOptions = command.isDefaultCommand ? [] : command.options.filter((o) => !o.deprecated)
|
|
518
|
+
return {
|
|
519
|
+
command,
|
|
520
|
+
displayName,
|
|
521
|
+
displayOptions,
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
const longestCommandNameLength = maxVisibleLength(
|
|
526
|
+
commandRows.map((row) => row.displayName)
|
|
527
|
+
)
|
|
528
|
+
const longestCommandOptions = commandRows
|
|
529
|
+
.flatMap((row) => row.displayOptions.map((option) => option.rawName))
|
|
530
|
+
const longestCommandOptionNameLength = maxVisibleLength(longestCommandOptions)
|
|
531
|
+
const commandDescriptionColumn = 2 + longestCommandNameLength + 2
|
|
532
|
+
const optionDescriptionColumn = 4 + longestCommandOptionNameLength + 2
|
|
533
|
+
const sharedDescriptionColumn = Math.max(commandDescriptionColumn, optionDescriptionColumn)
|
|
534
|
+
const descriptionWidth = terminalWidth - sharedDescriptionColumn
|
|
535
|
+
|
|
536
|
+
sections.push({
|
|
537
|
+
title: 'Commands',
|
|
538
|
+
body: commandRows
|
|
539
|
+
.map(({ command, displayName, displayOptions }) => {
|
|
540
|
+
const commandDescription = formatWrappedDescription(
|
|
541
|
+
command.description,
|
|
542
|
+
descriptionWidth,
|
|
543
|
+
sharedDescriptionColumn,
|
|
544
|
+
)
|
|
545
|
+
const commandPrefix = ` ${pc.bold(commandGreen(displayName))}`
|
|
546
|
+
const commandPadding = ' '.repeat(
|
|
547
|
+
Math.max(2, sharedDescriptionColumn - (2 + visibleLength(displayName)))
|
|
548
|
+
)
|
|
549
|
+
const headerLine = commandDescription
|
|
550
|
+
? `${commandPrefix}${commandPadding}${commandDescription}`
|
|
551
|
+
: commandPrefix
|
|
552
|
+
|
|
553
|
+
if (displayOptions.length === 0) {
|
|
554
|
+
return headerLine
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const optionLines = displayOptions
|
|
558
|
+
.map((option) => {
|
|
559
|
+
const optionDescription = formatWrappedDescription(
|
|
560
|
+
optionDescriptionText(option),
|
|
561
|
+
descriptionWidth,
|
|
562
|
+
sharedDescriptionColumn,
|
|
563
|
+
)
|
|
564
|
+
const optionPrefix = ` ${optionBlue(option.rawName)}`
|
|
565
|
+
const optionPadding = ' '.repeat(
|
|
566
|
+
Math.max(2, sharedDescriptionColumn - (4 + visibleLength(option.rawName)))
|
|
567
|
+
)
|
|
568
|
+
return optionDescription
|
|
569
|
+
? `${optionPrefix}${optionPadding}${optionDescription}`
|
|
570
|
+
: optionPrefix
|
|
571
|
+
})
|
|
572
|
+
.join('\n')
|
|
573
|
+
|
|
574
|
+
return `${headerLine}\n\n${optionLines}`
|
|
575
|
+
})
|
|
576
|
+
.join('\n\n\n'),
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const defaultCommandOptions = this.isGlobalCommand
|
|
581
|
+
? commands
|
|
582
|
+
.filter((command) => command.isDefaultCommand)
|
|
583
|
+
.flatMap((command) => command.options)
|
|
584
|
+
: []
|
|
585
|
+
|
|
586
|
+
const mergedGlobalAndDefaultOptions = [...globalOptions]
|
|
587
|
+
const mergedOptionNames = new Set(globalOptions.map((option) => option.name))
|
|
588
|
+
for (const option of defaultCommandOptions) {
|
|
589
|
+
if (!mergedOptionNames.has(option.name)) {
|
|
590
|
+
mergedGlobalAndDefaultOptions.push(option)
|
|
591
|
+
mergedOptionNames.add(option.name)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const mergedCommandAndGlobalOptions = [...this.options]
|
|
596
|
+
const mergedCommandOptionNames = new Set(this.options.map((option) => option.name))
|
|
597
|
+
for (const option of globalOptions || []) {
|
|
598
|
+
if (!mergedCommandOptionNames.has(option.name)) {
|
|
599
|
+
mergedCommandAndGlobalOptions.push(option)
|
|
600
|
+
mergedCommandOptionNames.add(option.name)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
let options = this.isGlobalCommand
|
|
605
|
+
? mergedGlobalAndDefaultOptions
|
|
606
|
+
: mergedCommandAndGlobalOptions
|
|
607
|
+
if (!this.isGlobalCommand && !this.isDefaultCommand) {
|
|
608
|
+
options = options.filter((option) => option.name !== 'version')
|
|
609
|
+
}
|
|
610
|
+
// Hide deprecated options from help output
|
|
611
|
+
options = options.filter((option) => !option.deprecated)
|
|
612
|
+
if (options.length > 0) {
|
|
613
|
+
const longestOptionNameLength = maxVisibleLength(
|
|
614
|
+
options.map((option) => option.rawName)
|
|
615
|
+
)
|
|
616
|
+
const descriptionColumn = 2 + longestOptionNameLength + 2
|
|
617
|
+
const descriptionWidth = terminalWidth - descriptionColumn
|
|
618
|
+
sections.push({
|
|
619
|
+
title: 'Options',
|
|
620
|
+
body: options
|
|
621
|
+
.map((option) => {
|
|
622
|
+
const optionLabel = padRight(option.rawName, longestOptionNameLength)
|
|
623
|
+
const description = formatWrappedDescription(
|
|
624
|
+
optionDescriptionText(option),
|
|
625
|
+
descriptionWidth,
|
|
626
|
+
descriptionColumn,
|
|
627
|
+
)
|
|
628
|
+
return description
|
|
629
|
+
? ` ${optionBlue(optionLabel)} ${description}`
|
|
630
|
+
: ` ${optionBlue(optionLabel)}`
|
|
631
|
+
})
|
|
632
|
+
.join('\n'),
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Show full description for specific commands (not global/default)
|
|
637
|
+
if (!this.isGlobalCommand && !this.isDefaultCommand && this.description) {
|
|
638
|
+
const descriptionLines = wrapDescription(this.description, terminalWidth - 2)
|
|
639
|
+
sections.push({
|
|
640
|
+
title: 'Description',
|
|
641
|
+
body: descriptionLines
|
|
642
|
+
.map((line) => (line ? ` ${pc.dim(line)}` : ''))
|
|
643
|
+
.join('\n'),
|
|
644
|
+
})
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (this.examples.length > 0) {
|
|
648
|
+
sections.push({
|
|
649
|
+
title: 'Examples',
|
|
650
|
+
body: this.examples
|
|
651
|
+
.map((example) => {
|
|
652
|
+
if (typeof example === 'function') {
|
|
653
|
+
return example(name)
|
|
654
|
+
}
|
|
655
|
+
return example
|
|
656
|
+
})
|
|
657
|
+
.join('\n'),
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (helpCallback) {
|
|
662
|
+
sections = helpCallback(sections) || sections
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
this.cli.console.log(
|
|
666
|
+
sections
|
|
667
|
+
.map((section) => {
|
|
668
|
+
return section.title
|
|
669
|
+
? `${pc.bold(pc.blue(section.title))}:\n${section.body}`
|
|
670
|
+
: section.body
|
|
671
|
+
})
|
|
672
|
+
.join('\n\n\n')
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
outputVersion() {
|
|
677
|
+
const { name } = this.cli
|
|
678
|
+
const { versionNumber } = this.cli.globalCommand
|
|
679
|
+
if (versionNumber) {
|
|
680
|
+
this.cli.console.log(`${name}/${versionNumber} ${platformInfo}`)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
checkRequiredArgs() {
|
|
685
|
+
const minimalArgsCount = this.args.filter((arg) => arg.required).length
|
|
686
|
+
|
|
687
|
+
if (this.cli.args.length < minimalArgsCount) {
|
|
688
|
+
throw new GokeError(
|
|
689
|
+
`missing required args for command \`${this.rawName}\``
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Check if the parsed options contain any unknown options
|
|
696
|
+
*
|
|
697
|
+
* Exit and output error when true
|
|
698
|
+
*/
|
|
699
|
+
checkUnknownOptions() {
|
|
700
|
+
const { options, globalCommand } = this.cli
|
|
701
|
+
|
|
702
|
+
if (!this.config.allowUnknownOptions) {
|
|
703
|
+
for (const name of Object.keys(options)) {
|
|
704
|
+
if (
|
|
705
|
+
name !== '--' &&
|
|
706
|
+
!this.hasOption(name) &&
|
|
707
|
+
!globalCommand.hasOption(name)
|
|
708
|
+
) {
|
|
709
|
+
throw new GokeError(
|
|
710
|
+
`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Check if the required string-type options exist
|
|
719
|
+
*/
|
|
720
|
+
checkOptionValue() {
|
|
721
|
+
const { options: parsedOptions, globalCommand } = this.cli
|
|
722
|
+
const options = [...globalCommand.options, ...this.options]
|
|
723
|
+
for (const option of options) {
|
|
724
|
+
// Resolve the full dot-path to get the actual value.
|
|
725
|
+
// For "config.port", traverse parsedOptions.config.port instead of just parsedOptions.config.
|
|
726
|
+
const keys = option.name.split('.')
|
|
727
|
+
let value: unknown = parsedOptions
|
|
728
|
+
for (const key of keys) {
|
|
729
|
+
if (value != null && typeof value === 'object') {
|
|
730
|
+
value = (value as Record<string, unknown>)[key]
|
|
731
|
+
} else {
|
|
732
|
+
value = undefined
|
|
733
|
+
break
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// Check required option value
|
|
737
|
+
if (option.required) {
|
|
738
|
+
if (value === true || value === false) {
|
|
739
|
+
throw new GokeError(`option \`${option.rawName}\` value is missing`)
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
class GlobalCommand extends Command {
|
|
747
|
+
constructor(cli: Goke) {
|
|
748
|
+
super('@@global@@', '', {}, cli)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ─── I/O interfaces ───
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Output stream interface, modeled after Node's process.stdout / process.stderr.
|
|
756
|
+
* Requires only a `write` method that accepts a string.
|
|
757
|
+
*/
|
|
758
|
+
interface GokeOutputStream {
|
|
759
|
+
write(data: string): void
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Console-like object returned by `createConsole`.
|
|
764
|
+
* Provides `log` and `error` methods that route output through
|
|
765
|
+
* the configured GokeOutputStream instances.
|
|
766
|
+
*/
|
|
767
|
+
interface GokeConsole {
|
|
768
|
+
log(...args: unknown[]): void
|
|
769
|
+
error(...args: unknown[]): void
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Options for configuring a Goke CLI instance.
|
|
774
|
+
*/
|
|
775
|
+
interface GokeOptions {
|
|
776
|
+
/** Custom stdout stream. Defaults to process.stdout */
|
|
777
|
+
stdout?: GokeOutputStream
|
|
778
|
+
/** Custom stderr stream. Defaults to process.stderr */
|
|
779
|
+
stderr?: GokeOutputStream
|
|
780
|
+
/** Custom argv array. Defaults to process.argv */
|
|
781
|
+
argv?: string[]
|
|
782
|
+
/** Terminal width used to wrap help output. Defaults to process.stdout.columns, or Infinity when unavailable */
|
|
783
|
+
columns?: number
|
|
784
|
+
/**
|
|
785
|
+
* Custom exit function called on CLI errors (unknown option, missing value, etc.).
|
|
786
|
+
* Defaults to process.exit. Set to a no-op or throw to prevent exit in tests.
|
|
787
|
+
*/
|
|
788
|
+
exit?: (code: number) => void
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Creates a console-like object that writes to the given output streams.
|
|
793
|
+
*
|
|
794
|
+
* Joins arguments with a space and appends a newline, then writes to the
|
|
795
|
+
* provided stream. Does not support format specifiers like `%d` — only
|
|
796
|
+
* simple string concatenation via `String()` conversion.
|
|
797
|
+
*/
|
|
798
|
+
function createConsole(stdout: GokeOutputStream, stderr: GokeOutputStream): GokeConsole {
|
|
799
|
+
return {
|
|
800
|
+
log(...args: unknown[]) {
|
|
801
|
+
stdout.write(args.map(String).join(' ') + '\n')
|
|
802
|
+
},
|
|
803
|
+
error(...args: unknown[]) {
|
|
804
|
+
stderr.write(args.map(String).join(' ') + '\n')
|
|
805
|
+
},
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ─── Error formatting ───
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Format an error for CLI output.
|
|
813
|
+
* Prints a red "error:" prefix with the message, followed by a dimmed stack trace.
|
|
814
|
+
*/
|
|
815
|
+
function formatCliError(err: Error): string {
|
|
816
|
+
const lines: string[] = []
|
|
817
|
+
lines.push(`${pc.red(pc.bold('error:'))} ${err.message}`)
|
|
818
|
+
if (err.stack) {
|
|
819
|
+
// Extract just the stack frames (skip the first line which is the message)
|
|
820
|
+
const stackLines = err.stack.split('\n').slice(1)
|
|
821
|
+
if (stackLines.length > 0) {
|
|
822
|
+
lines.push('')
|
|
823
|
+
lines.push(pc.red(pc.dim(stackLines.join('\n'))))
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return lines.join('\n')
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ─── Goke (main CLI class) ───
|
|
830
|
+
|
|
831
|
+
interface ParsedArgv {
|
|
832
|
+
args: ReadonlyArray<string>
|
|
833
|
+
options: {
|
|
834
|
+
[k: string]: any
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
class Goke extends EventEmitter {
|
|
839
|
+
/** The program name to display in help and version message */
|
|
840
|
+
name: string
|
|
841
|
+
commands: Command[]
|
|
842
|
+
globalCommand: GlobalCommand
|
|
843
|
+
matchedCommand?: Command
|
|
844
|
+
matchedCommandName?: string
|
|
845
|
+
/**
|
|
846
|
+
* Raw CLI arguments
|
|
847
|
+
*/
|
|
848
|
+
rawArgs: string[]
|
|
849
|
+
/**
|
|
850
|
+
* Parsed CLI arguments
|
|
851
|
+
*/
|
|
852
|
+
args: ParsedArgv['args']
|
|
853
|
+
/**
|
|
854
|
+
* Parsed CLI options, camelCased
|
|
855
|
+
*/
|
|
856
|
+
options: ParsedArgv['options']
|
|
857
|
+
|
|
858
|
+
showHelpOnExit?: boolean
|
|
859
|
+
showVersionOnExit?: boolean
|
|
860
|
+
|
|
861
|
+
/** Output stream for normal output (help, version, etc.) */
|
|
862
|
+
readonly stdout: GokeOutputStream
|
|
863
|
+
/** Output stream for error output */
|
|
864
|
+
readonly stderr: GokeOutputStream
|
|
865
|
+
/** Console-like object that routes through stdout/stderr */
|
|
866
|
+
readonly console: GokeConsole
|
|
867
|
+
/** Terminal width used to wrap help output text */
|
|
868
|
+
readonly columns: number
|
|
869
|
+
/** Exit function called on CLI errors. Defaults to process.exit */
|
|
870
|
+
readonly exit: (code: number) => void
|
|
871
|
+
|
|
872
|
+
#defaultArgv: string[]
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* @param name The program name to display in help and version message
|
|
876
|
+
* @param options Configuration for stdout, stderr, and argv
|
|
877
|
+
*/
|
|
878
|
+
constructor(name = '', options?: GokeOptions) {
|
|
879
|
+
super()
|
|
880
|
+
this.name = name
|
|
881
|
+
this.commands = []
|
|
882
|
+
this.rawArgs = []
|
|
883
|
+
this.args = []
|
|
884
|
+
this.options = {}
|
|
885
|
+
this.stdout = options?.stdout ?? process.stdout
|
|
886
|
+
this.stderr = options?.stderr ?? process.stderr
|
|
887
|
+
this.console = createConsole(this.stdout, this.stderr)
|
|
888
|
+
this.columns = options?.columns ?? process.stdout.columns ?? Number.POSITIVE_INFINITY
|
|
889
|
+
this.exit = options?.exit ?? ((code: number) => process.exit(code))
|
|
890
|
+
this.#defaultArgv = options?.argv ?? processArgs
|
|
891
|
+
this.globalCommand = new GlobalCommand(this)
|
|
892
|
+
this.globalCommand.usage('<command> [options]')
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Add a global usage text.
|
|
897
|
+
*
|
|
898
|
+
* This is not used by sub-commands.
|
|
899
|
+
*/
|
|
900
|
+
usage(text: string) {
|
|
901
|
+
this.globalCommand.usage(text)
|
|
902
|
+
return this
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Add a sub-command
|
|
907
|
+
*/
|
|
908
|
+
command(rawName: string, description?: string, config?: CommandConfig) {
|
|
909
|
+
const command = new Command(rawName, description || '', config, this)
|
|
910
|
+
command.globalCommand = this.globalCommand
|
|
911
|
+
this.commands.push(command)
|
|
912
|
+
return command
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Add a global CLI option.
|
|
917
|
+
*
|
|
918
|
+
* Which is also applied to sub-commands.
|
|
919
|
+
*/
|
|
920
|
+
option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1) {
|
|
921
|
+
this.globalCommand.option(rawName, descriptionOrSchema as any)
|
|
922
|
+
return this
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Show help message when `-h, --help` flags appear.
|
|
927
|
+
*
|
|
928
|
+
*/
|
|
929
|
+
help(callback?: HelpCallback) {
|
|
930
|
+
this.globalCommand.option('-h, --help', 'Display this message')
|
|
931
|
+
this.globalCommand.helpCallback = callback
|
|
932
|
+
this.showHelpOnExit = true
|
|
933
|
+
return this
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Show version number when `-v, --version` flags appear.
|
|
938
|
+
*
|
|
939
|
+
*/
|
|
940
|
+
version(version: string, customFlags = '-v, --version') {
|
|
941
|
+
this.globalCommand.version(version, customFlags)
|
|
942
|
+
this.showVersionOnExit = true
|
|
943
|
+
return this
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Add a global example.
|
|
948
|
+
*
|
|
949
|
+
* This example added here will not be used by sub-commands.
|
|
950
|
+
*/
|
|
951
|
+
example(example: CommandExample) {
|
|
952
|
+
this.globalCommand.example(example)
|
|
953
|
+
return this
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Output the corresponding help message
|
|
958
|
+
* When a sub-command is matched, output the help message for the command
|
|
959
|
+
* Otherwise output the global one.
|
|
960
|
+
*
|
|
961
|
+
*/
|
|
962
|
+
outputHelp() {
|
|
963
|
+
if (this.matchedCommand) {
|
|
964
|
+
this.matchedCommand.outputHelp()
|
|
965
|
+
} else {
|
|
966
|
+
this.globalCommand.outputHelp()
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Output help for commands matching a prefix.
|
|
972
|
+
* Used when user types "mcp nonexistent" and we have "mcp login", "mcp status", etc.
|
|
973
|
+
*/
|
|
974
|
+
outputHelpForPrefix(prefix: string, matchingCommands: Command[], fromHelpFlag = false) {
|
|
975
|
+
const { versionNumber } = this.globalCommand
|
|
976
|
+
|
|
977
|
+
this.console.log(`${this.name}${versionNumber ? `/${versionNumber}` : ''}`)
|
|
978
|
+
this.console.log()
|
|
979
|
+
if (!fromHelpFlag) {
|
|
980
|
+
this.console.log(
|
|
981
|
+
`Unknown command: ${this.args.join(' ')}`
|
|
982
|
+
)
|
|
983
|
+
this.console.log()
|
|
984
|
+
}
|
|
985
|
+
this.console.log(`Available "${prefix}" commands:`)
|
|
986
|
+
this.console.log()
|
|
987
|
+
|
|
988
|
+
const longestName = Math.max(...matchingCommands.map((c) => c.rawName.length))
|
|
989
|
+
for (const cmd of matchingCommands) {
|
|
990
|
+
const firstLine = cmd.description.split('\n')[0].trim()
|
|
991
|
+
this.console.log(` ${cmd.rawName.padEnd(longestName)} ${firstLine}`)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
this.console.log()
|
|
995
|
+
this.console.log(`Run "${this.name} <command> --help" for more information.`)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Output the version number.
|
|
1000
|
+
*
|
|
1001
|
+
*/
|
|
1002
|
+
outputVersion() {
|
|
1003
|
+
this.globalCommand.outputVersion()
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
private setParsedInfo(
|
|
1007
|
+
{ args, options }: ParsedArgv,
|
|
1008
|
+
matchedCommand?: Command,
|
|
1009
|
+
matchedCommandName?: string
|
|
1010
|
+
) {
|
|
1011
|
+
this.args = args
|
|
1012
|
+
this.options = options
|
|
1013
|
+
if (matchedCommand) {
|
|
1014
|
+
this.matchedCommand = matchedCommand
|
|
1015
|
+
}
|
|
1016
|
+
if (matchedCommandName) {
|
|
1017
|
+
this.matchedCommandName = matchedCommandName
|
|
1018
|
+
}
|
|
1019
|
+
return this
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
unsetMatchedCommand() {
|
|
1023
|
+
this.matchedCommand = undefined
|
|
1024
|
+
this.matchedCommandName = undefined
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Handle a CLI error by formatting it and writing to stderr.
|
|
1029
|
+
* For GokeError / coercion errors, also includes a help hint.
|
|
1030
|
+
*/
|
|
1031
|
+
private handleCliError(err: Error): void {
|
|
1032
|
+
this.console.error(formatCliError(err))
|
|
1033
|
+
|
|
1034
|
+
// Add help hint when help is enabled
|
|
1035
|
+
if (this.showHelpOnExit) {
|
|
1036
|
+
const cmdName = this.matchedCommandName
|
|
1037
|
+
? `${this.name} ${this.matchedCommandName} --help`
|
|
1038
|
+
: `${this.name} --help`
|
|
1039
|
+
this.console.error(`\nRun "${cmdName}" for usage information.`)
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Parse argv
|
|
1045
|
+
*/
|
|
1046
|
+
parse(
|
|
1047
|
+
argv = this.#defaultArgv,
|
|
1048
|
+
{
|
|
1049
|
+
/** Whether to run the action for matched command */
|
|
1050
|
+
run = true,
|
|
1051
|
+
} = {}
|
|
1052
|
+
): ParsedArgv {
|
|
1053
|
+
this.rawArgs = argv
|
|
1054
|
+
if (!this.name) {
|
|
1055
|
+
this.name = argv[1] ? getFileName(argv[1]) : 'cli'
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
let shouldParse = true
|
|
1059
|
+
|
|
1060
|
+
// Sort by name length (longest first) so "mcp login" matches before "mcp"
|
|
1061
|
+
const sortedCommands = [...this.commands].sort((a, b) => {
|
|
1062
|
+
const aLength = a.name.split(' ').filter(Boolean).length
|
|
1063
|
+
const bLength = b.name.split(' ').filter(Boolean).length
|
|
1064
|
+
return bLength - aLength
|
|
1065
|
+
})
|
|
1066
|
+
|
|
1067
|
+
// Search sub-commands — mri() can throw coercion errors, catch them
|
|
1068
|
+
try {
|
|
1069
|
+
for (const command of sortedCommands) {
|
|
1070
|
+
const parsed = this.mri(argv.slice(2), command)
|
|
1071
|
+
|
|
1072
|
+
const result = command.isMatched(parsed.args as string[])
|
|
1073
|
+
if (result.matched) {
|
|
1074
|
+
shouldParse = false
|
|
1075
|
+
const matchedCommandName = parsed.args.slice(0, result.consumedArgs).join(' ')
|
|
1076
|
+
const parsedInfo = {
|
|
1077
|
+
...parsed,
|
|
1078
|
+
args: parsed.args.slice(result.consumedArgs),
|
|
1079
|
+
}
|
|
1080
|
+
this.setParsedInfo(parsedInfo, command, matchedCommandName)
|
|
1081
|
+
this.emit(`command:${matchedCommandName}`, command)
|
|
1082
|
+
break // Stop after first match (greedy matching)
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (shouldParse) {
|
|
1087
|
+
// Search the default command
|
|
1088
|
+
for (const command of this.commands) {
|
|
1089
|
+
if (command.name === '') {
|
|
1090
|
+
// Check if any argument is a prefix of an existing command
|
|
1091
|
+
// If so, don't match the default command (user probably mistyped a subcommand)
|
|
1092
|
+
const parsed = this.mri(argv.slice(2), command)
|
|
1093
|
+
const firstArg = parsed.args[0]
|
|
1094
|
+
if (firstArg) {
|
|
1095
|
+
const isPrefixOfCommand = this.commands.some((cmd) => {
|
|
1096
|
+
if (cmd.name === '') return false
|
|
1097
|
+
const cmdParts = cmd.name.split(' ')
|
|
1098
|
+
return cmdParts[0] === firstArg
|
|
1099
|
+
})
|
|
1100
|
+
if (isPrefixOfCommand) {
|
|
1101
|
+
// Don't match default command - let it fall through to "unknown command"
|
|
1102
|
+
continue
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
shouldParse = false
|
|
1106
|
+
this.setParsedInfo(parsed, command)
|
|
1107
|
+
this.emit(`command:!`, command)
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (shouldParse) {
|
|
1113
|
+
const parsed = this.mri(argv.slice(2))
|
|
1114
|
+
this.setParsedInfo(parsed)
|
|
1115
|
+
}
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
if (err instanceof GokeError) {
|
|
1118
|
+
this.handleCliError(err)
|
|
1119
|
+
this.exit(1)
|
|
1120
|
+
}
|
|
1121
|
+
throw err
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (this.options.help && this.showHelpOnExit) {
|
|
1125
|
+
if (!this.matchedCommand && this.args[0]) {
|
|
1126
|
+
const firstArg = this.args[0]
|
|
1127
|
+
const matchingCommands = this.commands.filter((cmd) => {
|
|
1128
|
+
if (cmd.name === '') return false
|
|
1129
|
+
const cmdParts = cmd.name.split(' ')
|
|
1130
|
+
return cmdParts[0] === firstArg
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
if (matchingCommands.length > 0) {
|
|
1134
|
+
this.outputHelpForPrefix(firstArg, matchingCommands, true)
|
|
1135
|
+
} else {
|
|
1136
|
+
this.outputHelp()
|
|
1137
|
+
}
|
|
1138
|
+
} else {
|
|
1139
|
+
this.outputHelp()
|
|
1140
|
+
}
|
|
1141
|
+
run = false
|
|
1142
|
+
this.unsetMatchedCommand()
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {
|
|
1146
|
+
this.outputVersion()
|
|
1147
|
+
run = false
|
|
1148
|
+
this.unsetMatchedCommand()
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const parsedArgv = { args: this.args, options: this.options }
|
|
1152
|
+
|
|
1153
|
+
if (run) {
|
|
1154
|
+
this.runMatchedCommand()
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (!this.matchedCommand && this.args[0] && !(this.options.help && this.showHelpOnExit)) {
|
|
1158
|
+
this.emit('command:*')
|
|
1159
|
+
|
|
1160
|
+
// If the first arg is a prefix of existing commands but no command matched,
|
|
1161
|
+
// show help automatically (user likely mistyped a subcommand)
|
|
1162
|
+
if (this.showHelpOnExit) {
|
|
1163
|
+
const firstArg = this.args[0]
|
|
1164
|
+
const matchingCommands = this.commands.filter((cmd) => {
|
|
1165
|
+
if (cmd.name === '') return false
|
|
1166
|
+
const cmdParts = cmd.name.split(' ')
|
|
1167
|
+
return cmdParts[0] === firstArg
|
|
1168
|
+
})
|
|
1169
|
+
if (matchingCommands.length > 0) {
|
|
1170
|
+
// Show help for commands starting with this prefix
|
|
1171
|
+
this.outputHelpForPrefix(firstArg, matchingCommands)
|
|
1172
|
+
} else {
|
|
1173
|
+
// Unknown command with no matching prefix: show root help
|
|
1174
|
+
this.outputHelp()
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (
|
|
1180
|
+
!this.matchedCommand &&
|
|
1181
|
+
this.args.length === 0 &&
|
|
1182
|
+
this.showHelpOnExit &&
|
|
1183
|
+
!(this.options.help && this.showHelpOnExit)
|
|
1184
|
+
) {
|
|
1185
|
+
this.outputHelp()
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return parsedArgv
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
private mri(
|
|
1192
|
+
argv: string[],
|
|
1193
|
+
/** Matched command */ command?: Command
|
|
1194
|
+
): ParsedArgv {
|
|
1195
|
+
// All added options
|
|
1196
|
+
const cliOptions = [
|
|
1197
|
+
...this.globalCommand.options,
|
|
1198
|
+
...(command ? command.options : []),
|
|
1199
|
+
]
|
|
1200
|
+
const mriOptions = getMriOptions(cliOptions)
|
|
1201
|
+
|
|
1202
|
+
// Extract everything after `--` since mri doesn't support it
|
|
1203
|
+
let argsAfterDoubleDashes: string[] = []
|
|
1204
|
+
const doubleDashesIndex = argv.indexOf('--')
|
|
1205
|
+
if (doubleDashesIndex > -1) {
|
|
1206
|
+
argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1)
|
|
1207
|
+
argv = argv.slice(0, doubleDashesIndex)
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
let parsed = mri(argv, mriOptions)
|
|
1211
|
+
parsed = Object.keys(parsed).reduce(
|
|
1212
|
+
(res, name) => {
|
|
1213
|
+
return {
|
|
1214
|
+
...res,
|
|
1215
|
+
[camelcaseOptionName(name)]: parsed[name],
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
{ _: [] }
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
const args = parsed._
|
|
1222
|
+
|
|
1223
|
+
const options: { [k: string]: any } = {
|
|
1224
|
+
'--': argsAfterDoubleDashes,
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Set option default value
|
|
1228
|
+
const ignoreDefault =
|
|
1229
|
+
command && command.config.ignoreOptionDefaultValue
|
|
1230
|
+
? command.config.ignoreOptionDefaultValue
|
|
1231
|
+
: this.globalCommand.config.ignoreOptionDefaultValue
|
|
1232
|
+
|
|
1233
|
+
// Build a map of option name → JSON Schema for schema-backed options
|
|
1234
|
+
const schemaMap = new Map<string, { jsonSchema: Record<string, unknown>; optionName: string }>()
|
|
1235
|
+
|
|
1236
|
+
for (const cliOption of cliOptions) {
|
|
1237
|
+
if (!ignoreDefault && cliOption.default !== undefined) {
|
|
1238
|
+
for (const name of cliOption.names) {
|
|
1239
|
+
// Use setDotProp so dot-nested defaults (e.g. "config.port") produce
|
|
1240
|
+
// nested objects ({ config: { port: ... } }) instead of flat keys.
|
|
1241
|
+
const keys = name.split('.')
|
|
1242
|
+
setDotProp(options, keys, cliOption.default)
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Extract JSON Schema from StandardJSONSchemaV1-compatible schema
|
|
1247
|
+
if (cliOption.schema) {
|
|
1248
|
+
const jsonSchema = extractJsonSchema(cliOption.schema)
|
|
1249
|
+
if (jsonSchema) {
|
|
1250
|
+
schemaMap.set(cliOption.name, { jsonSchema, optionName: cliOption.name })
|
|
1251
|
+
// Also register aliases so we can look up by any name
|
|
1252
|
+
for (const alias of cliOption.names) {
|
|
1253
|
+
schemaMap.set(alias, { jsonSchema, optionName: cliOption.name })
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Build sets of option names for sentinel detection.
|
|
1260
|
+
//
|
|
1261
|
+
// When mri returns `true` for value-taking options, it means "flag present, no value given".
|
|
1262
|
+
// For required options (<...>), the sentinel is preserved so checkOptionValue() throws.
|
|
1263
|
+
// For optional options ([...]) with a schema, we replace `true` with `undefined`.
|
|
1264
|
+
const requiredValueOptions = new Set<string>()
|
|
1265
|
+
const optionalValueOptions = new Set<string>()
|
|
1266
|
+
for (const cliOption of cliOptions) {
|
|
1267
|
+
if (cliOption.required === true) {
|
|
1268
|
+
for (const name of cliOption.names) {
|
|
1269
|
+
requiredValueOptions.add(name)
|
|
1270
|
+
}
|
|
1271
|
+
} else if (cliOption.required === false) {
|
|
1272
|
+
for (const name of cliOption.names) {
|
|
1273
|
+
optionalValueOptions.add(name)
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Set option values (support dot-nested property name)
|
|
1279
|
+
// Apply schema-based coercion for options with schemas
|
|
1280
|
+
for (const key of Object.keys(parsed)) {
|
|
1281
|
+
if (key !== '_') {
|
|
1282
|
+
const keys = key.split('.')
|
|
1283
|
+
let value = parsed[key]
|
|
1284
|
+
|
|
1285
|
+
// Apply schema coercion if this option has a schema.
|
|
1286
|
+
// When value is boolean `true` and the option takes a value, it's mri's sentinel
|
|
1287
|
+
// for "flag present, no value given":
|
|
1288
|
+
// - Required options (<...>): preserve `true` so checkOptionValue() throws
|
|
1289
|
+
// - Optional options ([...]) with schema: replace with `undefined` (no typed value)
|
|
1290
|
+
// - Optional options ([...]) without schema: preserve `true` (original goke behavior)
|
|
1291
|
+
const schemaInfo = schemaMap.get(key)
|
|
1292
|
+
if (schemaInfo && value !== undefined) {
|
|
1293
|
+
if (value === true && requiredValueOptions.has(key)) {
|
|
1294
|
+
// Keep sentinel for checkOptionValue() to detect
|
|
1295
|
+
} else if (value === true && optionalValueOptions.has(key)) {
|
|
1296
|
+
// Optional value not given — schema expects a typed value, so return undefined
|
|
1297
|
+
value = undefined
|
|
1298
|
+
} else {
|
|
1299
|
+
value = coerceBySchema(value, schemaInfo.jsonSchema, schemaInfo.optionName)
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
setDotProp(options, keys, value)
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
return {
|
|
1308
|
+
args,
|
|
1309
|
+
options,
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
runMatchedCommand() {
|
|
1314
|
+
const { args, options, matchedCommand: command } = this
|
|
1315
|
+
|
|
1316
|
+
if (!command || !command.commandAction) return
|
|
1317
|
+
|
|
1318
|
+
try {
|
|
1319
|
+
command.checkUnknownOptions()
|
|
1320
|
+
command.checkOptionValue()
|
|
1321
|
+
command.checkRequiredArgs()
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
if (err instanceof GokeError) {
|
|
1324
|
+
this.handleCliError(err)
|
|
1325
|
+
this.exit(1)
|
|
1326
|
+
}
|
|
1327
|
+
throw err
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const actionArgs: any[] = []
|
|
1331
|
+
command.args.forEach((arg, index) => {
|
|
1332
|
+
if (arg.variadic) {
|
|
1333
|
+
actionArgs.push(args.slice(index))
|
|
1334
|
+
} else {
|
|
1335
|
+
actionArgs.push(args[index])
|
|
1336
|
+
}
|
|
1337
|
+
})
|
|
1338
|
+
actionArgs.push(options)
|
|
1339
|
+
|
|
1340
|
+
const result = command.commandAction.apply(this, actionArgs)
|
|
1341
|
+
|
|
1342
|
+
// If the action returns a promise, catch async errors
|
|
1343
|
+
if (result && typeof result === 'object' && typeof result.catch === 'function') {
|
|
1344
|
+
result.catch((err: unknown) => {
|
|
1345
|
+
if (err instanceof Error) {
|
|
1346
|
+
this.handleCliError(err)
|
|
1347
|
+
} else {
|
|
1348
|
+
this.console.error(`${pc.red(pc.bold('error:'))} ${String(err)}`)
|
|
1349
|
+
}
|
|
1350
|
+
this.exit(1)
|
|
1351
|
+
})
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
return result
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// ─── Exports ───
|
|
1359
|
+
|
|
1360
|
+
export type { GokeOutputStream, GokeConsole, GokeOptions }
|
|
1361
|
+
export { createConsole, Command }
|
|
1362
|
+
export default Goke
|