@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,1798 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest'
|
|
2
|
+
import goke, { createConsole } from '../index.js'
|
|
3
|
+
import type { GokeOutputStream, GokeOptions } from '../index.js'
|
|
4
|
+
import { coerceBySchema } from '../coerce.js'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
const ANSI_RE = /\x1B\[[0-9;]*m/g
|
|
8
|
+
|
|
9
|
+
const stripAnsi = (text: string) => text.replace(ANSI_RE, '')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper: creates a GokeOutputStream that captures all written data into a string array.
|
|
13
|
+
* Access `output.lines` for raw writes, or `output.text` for the joined result.
|
|
14
|
+
*/
|
|
15
|
+
function createTestOutputStream(): GokeOutputStream & { lines: string[]; readonly text: string } {
|
|
16
|
+
const lines: string[] = []
|
|
17
|
+
return {
|
|
18
|
+
lines,
|
|
19
|
+
get text() { return stripAnsi(lines.join('')) },
|
|
20
|
+
write(data: string) { lines.push(data) },
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper: creates a goke instance with exit overridden to a no-op.
|
|
26
|
+
* This prevents process.exit(1) from killing the test runner while
|
|
27
|
+
* still allowing the original error to propagate (the framework
|
|
28
|
+
* re-throws after calling exit when exit doesn't halt execution).
|
|
29
|
+
*
|
|
30
|
+
* Tests can still use .toThrow() to assert CLI errors normally.
|
|
31
|
+
*/
|
|
32
|
+
function gokeTestable(name = '', options?: Partial<GokeOptions>) {
|
|
33
|
+
return goke(name, {
|
|
34
|
+
...options,
|
|
35
|
+
exit: () => {},
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Strip stack trace lines for stable snapshots.
|
|
41
|
+
* Keeps the error message and help hint, removes all " at ..." lines
|
|
42
|
+
* and the blank line before them, since those contain machine-specific paths.
|
|
43
|
+
*/
|
|
44
|
+
function stripStackTrace(text: string): string {
|
|
45
|
+
return text
|
|
46
|
+
.split('\n')
|
|
47
|
+
.filter(line => !line.match(/^\s+at /))
|
|
48
|
+
.join('\n')
|
|
49
|
+
.replace(/\n{2,}/g, '\n')
|
|
50
|
+
.trim()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('error formatting', () => {
|
|
54
|
+
test('unknown option prints formatted error to stderr', () => {
|
|
55
|
+
const stderr = createTestOutputStream()
|
|
56
|
+
const cli = goke('mycli', { stderr, exit: () => {} })
|
|
57
|
+
|
|
58
|
+
cli
|
|
59
|
+
.command('build', 'Build your app')
|
|
60
|
+
.option('--port <port>', 'Port')
|
|
61
|
+
.action(() => {})
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
cli.parse('node bin build --unknown'.split(' '))
|
|
65
|
+
} catch {}
|
|
66
|
+
|
|
67
|
+
expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: Unknown option \`--unknown\`"`)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('missing required option value prints formatted error to stderr', () => {
|
|
71
|
+
const stderr = createTestOutputStream()
|
|
72
|
+
const cli = goke('mycli', { stderr, exit: () => {} })
|
|
73
|
+
|
|
74
|
+
cli
|
|
75
|
+
.command('serve', 'Start server')
|
|
76
|
+
.option('--port <port>', 'Port')
|
|
77
|
+
.action(() => {})
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
cli.parse('node bin serve --port'.split(' '))
|
|
81
|
+
} catch {}
|
|
82
|
+
|
|
83
|
+
expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: option \`--port <port>\` value is missing"`)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('schema coercion error prints formatted error to stderr', () => {
|
|
87
|
+
const stderr = createTestOutputStream()
|
|
88
|
+
const cli = goke('mycli', { stderr, exit: () => {} })
|
|
89
|
+
|
|
90
|
+
cli.option('--port <port>', z.number().describe('Port'))
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
cli.parse('node bin --port abc'.split(' '))
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: Invalid value for --port: expected number, got "abc""`)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('error includes help hint when help is enabled', () => {
|
|
100
|
+
const stderr = createTestOutputStream()
|
|
101
|
+
const cli = goke('mycli', { stderr, exit: () => {} })
|
|
102
|
+
|
|
103
|
+
cli.help()
|
|
104
|
+
|
|
105
|
+
cli
|
|
106
|
+
.command('serve', 'Start server')
|
|
107
|
+
.option('--port <port>', 'Port')
|
|
108
|
+
.action(() => {})
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
cli.parse('node bin serve --port'.split(' '))
|
|
112
|
+
} catch {}
|
|
113
|
+
|
|
114
|
+
expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`
|
|
115
|
+
"error: option \`--port <port>\` value is missing
|
|
116
|
+
Run "mycli serve --help" for usage information."
|
|
117
|
+
`)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('async action error prints formatted error to stderr', async () => {
|
|
121
|
+
const stderr = createTestOutputStream()
|
|
122
|
+
let exitCode: number | undefined
|
|
123
|
+
const cli = goke('mycli', { stderr, exit: (code) => { exitCode = code } })
|
|
124
|
+
|
|
125
|
+
cli
|
|
126
|
+
.command('deploy', 'Deploy app')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
throw new Error('connection refused')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
cli.parse('node bin deploy'.split(' '))
|
|
132
|
+
|
|
133
|
+
// Wait for the async rejection to be handled
|
|
134
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
135
|
+
|
|
136
|
+
expect(exitCode).toBe(1)
|
|
137
|
+
expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: connection refused"`)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('error output includes stack trace', () => {
|
|
141
|
+
const stderr = createTestOutputStream()
|
|
142
|
+
const cli = goke('mycli', { stderr, exit: () => {} })
|
|
143
|
+
|
|
144
|
+
cli
|
|
145
|
+
.command('build', 'Build app')
|
|
146
|
+
.action(() => {})
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
cli.parse('node bin build --unknown'.split(' '))
|
|
150
|
+
} catch {}
|
|
151
|
+
|
|
152
|
+
// Verify that stderr contains "error:" prefix and a stack trace with "at" lines
|
|
153
|
+
const text = stderr.text
|
|
154
|
+
expect(text).toContain('error:')
|
|
155
|
+
expect(text).toContain('Unknown option `--unknown`')
|
|
156
|
+
expect(text).toMatch(/at /)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('double dashes', () => {
|
|
161
|
+
const cli = goke()
|
|
162
|
+
|
|
163
|
+
const { args, options } = cli.parse([
|
|
164
|
+
'node',
|
|
165
|
+
'bin',
|
|
166
|
+
'foo',
|
|
167
|
+
'bar',
|
|
168
|
+
'--',
|
|
169
|
+
'npm',
|
|
170
|
+
'test',
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
expect(args).toEqual(['foo', 'bar'])
|
|
174
|
+
expect(options['--']).toEqual(['npm', 'test'])
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('dot-nested options', () => {
|
|
178
|
+
const cli = goke()
|
|
179
|
+
|
|
180
|
+
cli
|
|
181
|
+
.option('--externals <external>', 'Add externals')
|
|
182
|
+
.option('--scale [level]', 'Scaling level')
|
|
183
|
+
|
|
184
|
+
const { options: options1 } = cli.parse(
|
|
185
|
+
`node bin --externals.env.prod production --scale`.split(' ')
|
|
186
|
+
)
|
|
187
|
+
expect(options1.externals).toEqual({ env: { prod: 'production' } })
|
|
188
|
+
expect(options1.scale).toEqual(true)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('schema-based options', () => {
|
|
192
|
+
test('schema coerces string to number', () => {
|
|
193
|
+
const cli = goke()
|
|
194
|
+
|
|
195
|
+
cli.option('--port <port>', z.number().describe('Port number'))
|
|
196
|
+
|
|
197
|
+
const { options } = cli.parse('node bin --port 3000'.split(' '))
|
|
198
|
+
expect(options.port).toBe(3000)
|
|
199
|
+
expect(typeof options.port).toBe('number')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('schema preserves string (no auto-conversion to number)', () => {
|
|
203
|
+
const cli = goke()
|
|
204
|
+
|
|
205
|
+
cli.option('--id <id>', z.string().describe('ID'))
|
|
206
|
+
|
|
207
|
+
const { options } = cli.parse('node bin --id 00123'.split(' '))
|
|
208
|
+
expect(options.id).toBe('00123')
|
|
209
|
+
expect(typeof options.id).toBe('string')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('schema coerces string to integer', () => {
|
|
213
|
+
const cli = goke()
|
|
214
|
+
|
|
215
|
+
cli.option('--count <count>', z.int().describe('Count'))
|
|
216
|
+
|
|
217
|
+
const { options } = cli.parse('node bin --count 42'.split(' '))
|
|
218
|
+
expect(options.count).toBe(42)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('schema parses JSON object', () => {
|
|
222
|
+
const cli = goke()
|
|
223
|
+
|
|
224
|
+
cli.option('--config <config>', z.looseObject({}).describe('Config'))
|
|
225
|
+
|
|
226
|
+
const { options } = cli.parse(['node', 'bin', '--config', '{"a":1}'])
|
|
227
|
+
expect(options.config).toEqual({ a: 1 })
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('schema parses JSON array', () => {
|
|
231
|
+
const cli = goke()
|
|
232
|
+
|
|
233
|
+
cli.option('--items <items>', z.array(z.unknown()).describe('Items'))
|
|
234
|
+
|
|
235
|
+
const { options } = cli.parse(['node', 'bin', '--items', '[1,2,3]'])
|
|
236
|
+
expect(options.items).toEqual([1, 2, 3])
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('schema throws on invalid number', () => {
|
|
240
|
+
const cli = gokeTestable()
|
|
241
|
+
|
|
242
|
+
cli.option('--port <port>', z.number().describe('Port number'))
|
|
243
|
+
|
|
244
|
+
expect(() => cli.parse('node bin --port abc'.split(' ')))
|
|
245
|
+
.toThrow('expected number, got "abc"')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('schema with union type ["number", "string"]', () => {
|
|
249
|
+
const cli = goke()
|
|
250
|
+
|
|
251
|
+
cli.option('--val <val>', z.union([z.number(), z.string()]).describe('Value'))
|
|
252
|
+
|
|
253
|
+
const { options: opts1 } = cli.parse('node bin --val 123'.split(' '))
|
|
254
|
+
expect(opts1.val).toBe(123)
|
|
255
|
+
|
|
256
|
+
const { options: opts2 } = cli.parse('node bin --val abc'.split(' '))
|
|
257
|
+
expect(opts2.val).toBe('abc')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('options without schema keep values as strings', () => {
|
|
261
|
+
const cli = goke()
|
|
262
|
+
|
|
263
|
+
cli.option('--port <port>', 'Port number')
|
|
264
|
+
|
|
265
|
+
// Without schema, mri no longer auto-converts — value stays as string.
|
|
266
|
+
// Use a schema to get typed values.
|
|
267
|
+
const { options } = cli.parse('node bin --port 3000'.split(' '))
|
|
268
|
+
expect(options.port).toBe('3000')
|
|
269
|
+
expect(typeof options.port).toBe('string')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('schema with default value', () => {
|
|
273
|
+
const cli = goke()
|
|
274
|
+
|
|
275
|
+
cli.option('--port [port]', z.number().default(8080).describe('Port number'))
|
|
276
|
+
|
|
277
|
+
const { options } = cli.parse('node bin'.split(' '))
|
|
278
|
+
expect(options.port).toBe(8080)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('schema on subcommand options', () => {
|
|
282
|
+
const cli = goke()
|
|
283
|
+
let result: any = {}
|
|
284
|
+
|
|
285
|
+
cli
|
|
286
|
+
.command('serve', 'Start server')
|
|
287
|
+
.option('--port <port>', z.number().describe('Port'))
|
|
288
|
+
.option('--host <host>', z.string().describe('Host'))
|
|
289
|
+
.action((options) => {
|
|
290
|
+
result = options
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
cli.parse('node bin serve --port 3000 --host localhost'.split(' '), { run: true })
|
|
294
|
+
expect(result.port).toBe(3000)
|
|
295
|
+
expect(result.host).toBe('localhost')
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
describe('no-schema behavior (mri no longer auto-converts)', () => {
|
|
300
|
+
test('numeric string stays as string without schema', () => {
|
|
301
|
+
const cli = goke()
|
|
302
|
+
cli.option('--port <port>', 'Port')
|
|
303
|
+
const { options } = cli.parse('node bin --port 3000'.split(' '))
|
|
304
|
+
expect(options.port).toBe('3000')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('leading zeros preserved without schema', () => {
|
|
308
|
+
const cli = goke()
|
|
309
|
+
cli.option('--id <id>', 'ID')
|
|
310
|
+
const { options } = cli.parse('node bin --id 00123'.split(' '))
|
|
311
|
+
expect(options.id).toBe('00123')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('phone number preserved without schema', () => {
|
|
315
|
+
const cli = goke()
|
|
316
|
+
cli.option('--phone <phone>', 'Phone')
|
|
317
|
+
const { options } = cli.parse('node bin --phone +1234567890'.split(' '))
|
|
318
|
+
expect(options.phone).toBe('+1234567890')
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('boolean flags still work without schema', () => {
|
|
322
|
+
const cli = goke()
|
|
323
|
+
cli.option('--verbose', 'Verbose')
|
|
324
|
+
const { options } = cli.parse('node bin --verbose'.split(' '))
|
|
325
|
+
expect(options.verbose).toBe(true)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('optional value flag returns true when no value given', () => {
|
|
329
|
+
const cli = goke()
|
|
330
|
+
cli.option('--format [fmt]', 'Format')
|
|
331
|
+
const { options } = cli.parse('node bin --format'.split(' '))
|
|
332
|
+
expect(options.format).toBe(true)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('optional value flag returns string when value given', () => {
|
|
336
|
+
const cli = goke()
|
|
337
|
+
cli.option('--format [fmt]', 'Format')
|
|
338
|
+
const { options } = cli.parse('node bin --format json'.split(' '))
|
|
339
|
+
expect(options.format).toBe('json')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test('hex string stays as string without schema', () => {
|
|
343
|
+
const cli = goke()
|
|
344
|
+
cli.option('--color <color>', 'Color')
|
|
345
|
+
const { options } = cli.parse('node bin --color 0xff00ff'.split(' '))
|
|
346
|
+
expect(options.color).toBe('0xff00ff')
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('scientific notation stays as string without schema', () => {
|
|
350
|
+
const cli = goke()
|
|
351
|
+
cli.option('--val <val>', 'Value')
|
|
352
|
+
const { options } = cli.parse('node bin --val 1e10'.split(' '))
|
|
353
|
+
expect(options.val).toBe('1e10')
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe('typical CLI usage examples', () => {
|
|
358
|
+
test('web server CLI with typed options', () => {
|
|
359
|
+
const cli = goke('myserver')
|
|
360
|
+
let config: any = {}
|
|
361
|
+
|
|
362
|
+
cli
|
|
363
|
+
.command('start', 'Start the web server')
|
|
364
|
+
.option('--port <port>', z.number().default(3000).describe('Port to listen on'))
|
|
365
|
+
.option('--host <host>', z.string().default('localhost').describe('Hostname to bind'))
|
|
366
|
+
.option('--workers <workers>', z.int().describe('Number of worker threads'))
|
|
367
|
+
.option('--cors', 'Enable CORS')
|
|
368
|
+
.option('--log', 'Enable logging')
|
|
369
|
+
.action((options) => { config = options })
|
|
370
|
+
|
|
371
|
+
cli.parse('node bin start --port 8080 --host 0.0.0.0 --workers 4 --cors'.split(' '), { run: true })
|
|
372
|
+
|
|
373
|
+
expect(config.port).toBe(8080)
|
|
374
|
+
expect(typeof config.port).toBe('number')
|
|
375
|
+
expect(config.host).toBe('0.0.0.0')
|
|
376
|
+
expect(config.workers).toBe(4)
|
|
377
|
+
expect(typeof config.workers).toBe('number')
|
|
378
|
+
expect(config.cors).toBe(true)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
test('web server CLI with defaults (no args)', () => {
|
|
382
|
+
const cli = goke('myserver')
|
|
383
|
+
let config: any = {}
|
|
384
|
+
|
|
385
|
+
cli
|
|
386
|
+
.command('start', 'Start the web server')
|
|
387
|
+
.option('--port [port]', z.number().default(3000).describe('Port'))
|
|
388
|
+
.option('--host [host]', z.string().default('localhost').describe('Host'))
|
|
389
|
+
.action((options) => { config = options })
|
|
390
|
+
|
|
391
|
+
cli.parse('node bin start'.split(' '), { run: true })
|
|
392
|
+
|
|
393
|
+
expect(config.port).toBe(3000)
|
|
394
|
+
expect(config.host).toBe('localhost')
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
test('database CLI with JSON config option', () => {
|
|
398
|
+
const cli = goke('dbcli')
|
|
399
|
+
let config: any = {}
|
|
400
|
+
|
|
401
|
+
cli
|
|
402
|
+
.command('migrate', 'Run database migrations')
|
|
403
|
+
.option('--connection <conn>', z.object({ host: z.string(), port: z.number() }).describe('Connection config (JSON)'))
|
|
404
|
+
.option('--dry-run', 'Preview without executing')
|
|
405
|
+
.action((options) => { config = options })
|
|
406
|
+
|
|
407
|
+
cli.parse(['node', 'bin', 'migrate', '--connection', '{"host":"localhost","port":5432}', '--dry-run'], { run: true })
|
|
408
|
+
|
|
409
|
+
expect(config.connection).toEqual({ host: 'localhost', port: 5432 })
|
|
410
|
+
expect(config.dryRun).toBe(true)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('file processing CLI with positional args + typed options', () => {
|
|
414
|
+
const cli = goke('fileproc')
|
|
415
|
+
let result: any = {}
|
|
416
|
+
|
|
417
|
+
cli
|
|
418
|
+
.command('convert <input> <output>', 'Convert file format')
|
|
419
|
+
.option('--quality <quality>', z.int().describe('Quality (0-100)'))
|
|
420
|
+
.option('--format <format>', z.enum(['png', 'jpg', 'webp']).describe('Output format'))
|
|
421
|
+
.action((input, output, options) => {
|
|
422
|
+
result = { input, output, ...options }
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
cli.parse('node bin convert photo.bmp photo.jpg --quality 85 --format jpg'.split(' '), { run: true })
|
|
426
|
+
|
|
427
|
+
expect(result.input).toBe('photo.bmp')
|
|
428
|
+
expect(result.output).toBe('photo.jpg')
|
|
429
|
+
expect(result.quality).toBe(85)
|
|
430
|
+
expect(typeof result.quality).toBe('number')
|
|
431
|
+
expect(result.format).toBe('jpg')
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
test('API client CLI preserving string IDs', () => {
|
|
435
|
+
const cli = goke('apicli')
|
|
436
|
+
let result: any = {}
|
|
437
|
+
|
|
438
|
+
cli
|
|
439
|
+
.command('get-user <userId>', 'Get user by ID')
|
|
440
|
+
.option('--fields <fields>', z.array(z.unknown()).describe('Fields to return (JSON array)'))
|
|
441
|
+
.action((userId, options) => {
|
|
442
|
+
result = { userId, ...options }
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
// userId "00123" should NOT be coerced to number 123
|
|
446
|
+
cli.parse(['node', 'bin', 'get-user', '00123', '--fields', '["name","email"]'], { run: true })
|
|
447
|
+
|
|
448
|
+
expect(result.userId).toBe('00123')
|
|
449
|
+
expect(result.fields).toEqual(['name', 'email'])
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
test('nullable option with union type', () => {
|
|
453
|
+
const cli = goke()
|
|
454
|
+
cli.option('--timeout <timeout>', z.nullable(z.number()).describe('Timeout'))
|
|
455
|
+
|
|
456
|
+
const { options: opts1 } = cli.parse('node bin --timeout 5000'.split(' '))
|
|
457
|
+
expect(opts1.timeout).toBe(5000)
|
|
458
|
+
|
|
459
|
+
// Empty string coerces to null for null type
|
|
460
|
+
const { options: opts2 } = cli.parse(['node', 'bin', '--timeout', ''])
|
|
461
|
+
expect(opts2.timeout).toBe(null)
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
describe('regression: oracle-found issues', () => {
|
|
466
|
+
test('required option with schema still throws when value missing', () => {
|
|
467
|
+
const cli = gokeTestable()
|
|
468
|
+
let actionCalled = false
|
|
469
|
+
|
|
470
|
+
cli
|
|
471
|
+
.command('serve', 'Start server')
|
|
472
|
+
.option('--port <port>', z.number().describe('Port'))
|
|
473
|
+
.action(() => { actionCalled = true })
|
|
474
|
+
|
|
475
|
+
// --port without a value should throw "value is missing"
|
|
476
|
+
expect(() => {
|
|
477
|
+
cli.parse('node bin serve --port'.split(' '), { run: true })
|
|
478
|
+
}).toThrow('value is missing')
|
|
479
|
+
expect(actionCalled).toBe(false)
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
test('repeated flags with non-array schema throws', () => {
|
|
483
|
+
const cli = gokeTestable()
|
|
484
|
+
|
|
485
|
+
cli.option('--tag <tag>', z.string().describe('Tags'))
|
|
486
|
+
|
|
487
|
+
expect(() => cli.parse('node bin --tag foo --tag bar'.split(' ')))
|
|
488
|
+
.toThrow('does not accept multiple values')
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
test('repeated flags with number schema throws', () => {
|
|
492
|
+
const cli = gokeTestable()
|
|
493
|
+
|
|
494
|
+
cli.option('--id <id>', z.number().describe('ID'))
|
|
495
|
+
|
|
496
|
+
expect(() => cli.parse('node bin --id 1 --id 2'.split(' ')))
|
|
497
|
+
.toThrow('does not accept multiple values')
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test('repeated flags with array schema collects values', () => {
|
|
501
|
+
const cli = goke()
|
|
502
|
+
|
|
503
|
+
cli.option('--tag <tag>', z.array(z.string()).describe('Tags'))
|
|
504
|
+
|
|
505
|
+
const { options } = cli.parse('node bin --tag foo --tag bar'.split(' '))
|
|
506
|
+
expect(options.tag).toEqual(['foo', 'bar'])
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
test('repeated flags with array+items schema coerces each element', () => {
|
|
510
|
+
const cli = goke()
|
|
511
|
+
|
|
512
|
+
cli.option('--id <id>', z.array(z.number()).describe('IDs'))
|
|
513
|
+
|
|
514
|
+
const { options } = cli.parse('node bin --id 1 --id 2 --id 3'.split(' '))
|
|
515
|
+
expect(options.id).toEqual([1, 2, 3])
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
test('single value with array schema wraps in array', () => {
|
|
519
|
+
const cli = goke()
|
|
520
|
+
|
|
521
|
+
cli.option('--tag <tag>', z.array(z.string()).describe('Tags'))
|
|
522
|
+
|
|
523
|
+
const { options } = cli.parse('node bin --tag foo'.split(' '))
|
|
524
|
+
expect(options.tag).toEqual(['foo'])
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
test('single value with array+number items schema wraps and coerces', () => {
|
|
528
|
+
const cli = goke()
|
|
529
|
+
|
|
530
|
+
cli.option('--id <id>', z.array(z.number()).describe('IDs'))
|
|
531
|
+
|
|
532
|
+
const { options } = cli.parse('node bin --id 42'.split(' '))
|
|
533
|
+
expect(options.id).toEqual([42])
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
test('JSON array string with array schema parses correctly', () => {
|
|
537
|
+
const cli = goke()
|
|
538
|
+
|
|
539
|
+
cli.option('--ids <ids>', z.array(z.number()).describe('IDs'))
|
|
540
|
+
|
|
541
|
+
const { options } = cli.parse(['node', 'bin', '--ids', '[1,2,3]'])
|
|
542
|
+
expect(options.ids).toEqual([1, 2, 3])
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
test('repeated flags without schema still produce array (no schema = no restriction)', () => {
|
|
546
|
+
const cli = goke()
|
|
547
|
+
|
|
548
|
+
cli.option('--tag <tag>', 'Tags')
|
|
549
|
+
|
|
550
|
+
const { options } = cli.parse('node bin --tag foo --tag bar'.split(' '))
|
|
551
|
+
expect(options.tag).toEqual(['foo', 'bar'])
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
test('const null coercion works', () => {
|
|
555
|
+
expect(coerceBySchema('', { const: null }, 'val')).toBe(null)
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
test('optional value option with schema returns undefined when no value given', () => {
|
|
559
|
+
const cli = goke()
|
|
560
|
+
|
|
561
|
+
cli.option('--count [count]', z.number().describe('Count'))
|
|
562
|
+
|
|
563
|
+
// --count without value → schema expects number, none given → undefined
|
|
564
|
+
const { options } = cli.parse('node bin --count'.split(' '))
|
|
565
|
+
expect(options.count).toBe(undefined)
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
test('optional value option without schema preserves true sentinel', () => {
|
|
569
|
+
const cli = goke()
|
|
570
|
+
|
|
571
|
+
cli.option('--count [count]', 'Count')
|
|
572
|
+
|
|
573
|
+
// Without schema, original goke behavior: true means "flag present"
|
|
574
|
+
const { options } = cli.parse('node bin --count'.split(' '))
|
|
575
|
+
expect(options.count).toBe(true)
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
test('optional value option with schema coerces when value given', () => {
|
|
579
|
+
const cli = goke()
|
|
580
|
+
|
|
581
|
+
cli.option('--count [count]', z.number().describe('Count'))
|
|
582
|
+
|
|
583
|
+
const { options } = cli.parse('node bin --count 42'.split(' '))
|
|
584
|
+
expect(options.count).toBe(42)
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
test('alias + schema coercion works', () => {
|
|
588
|
+
const cli = goke()
|
|
589
|
+
|
|
590
|
+
cli.option('-p, --port <port>', z.number().describe('Port'))
|
|
591
|
+
|
|
592
|
+
const { options } = cli.parse('node bin -p 3000'.split(' '))
|
|
593
|
+
expect(options.port).toBe(3000)
|
|
594
|
+
expect(options.p).toBe(3000)
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
test('union type ["array", "null"] with repeated flags', () => {
|
|
598
|
+
const cli = goke()
|
|
599
|
+
|
|
600
|
+
cli.option('--tags <tags>', z.nullable(z.array(z.string())).describe('Tags'))
|
|
601
|
+
|
|
602
|
+
const { options } = cli.parse('node bin --tags foo --tags bar'.split(' '))
|
|
603
|
+
expect(options.tags).toEqual(['foo', 'bar'])
|
|
604
|
+
})
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
describe('edge cases: schema + defaults interaction', () => {
|
|
608
|
+
test('default value from schema is used when option not passed', () => {
|
|
609
|
+
const cli = goke()
|
|
610
|
+
|
|
611
|
+
cli.option('--port [port]', z.number().default(8080).describe('Port'))
|
|
612
|
+
|
|
613
|
+
const { options } = cli.parse('node bin'.split(' '))
|
|
614
|
+
expect(options.port).toBe(8080)
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
test('default value is used when option not passed, schema value when passed', () => {
|
|
618
|
+
const cli = goke()
|
|
619
|
+
|
|
620
|
+
cli.option('--port [port]', z.number().default(8080).describe('Port'))
|
|
621
|
+
|
|
622
|
+
const { options: opts1 } = cli.parse('node bin'.split(' '))
|
|
623
|
+
expect(opts1.port).toBe(8080)
|
|
624
|
+
|
|
625
|
+
const { options: opts2 } = cli.parse('node bin --port 3000'.split(' '))
|
|
626
|
+
expect(opts2.port).toBe(3000)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
test('optional value + default + schema: three-way interaction', () => {
|
|
630
|
+
const cli = goke()
|
|
631
|
+
|
|
632
|
+
cli.option('--count [count]', z.number().default(10).describe('Count'))
|
|
633
|
+
|
|
634
|
+
// Not passed at all → default
|
|
635
|
+
const { options: opts1 } = cli.parse('node bin'.split(' '))
|
|
636
|
+
expect(opts1.count).toBe(10)
|
|
637
|
+
|
|
638
|
+
// Passed with value → coerced
|
|
639
|
+
const { options: opts2 } = cli.parse('node bin --count 42'.split(' '))
|
|
640
|
+
expect(opts2.count).toBe(42)
|
|
641
|
+
|
|
642
|
+
// Passed without value → undefined (sentinel replaced)
|
|
643
|
+
const { options: opts3 } = cli.parse('node bin --count'.split(' '))
|
|
644
|
+
expect(opts3.count).toBe(undefined)
|
|
645
|
+
})
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
describe('edge cases: boolean flags + schema', () => {
|
|
649
|
+
test('boolean flag (no brackets) with number schema — mri returns boolean', () => {
|
|
650
|
+
const cli = goke()
|
|
651
|
+
|
|
652
|
+
// This is a questionable usage: boolean flag + number schema
|
|
653
|
+
// mri returns true/false for boolean flags, schema tries to coerce boolean→number
|
|
654
|
+
cli.option('--verbose', z.number().describe('Verbose'))
|
|
655
|
+
|
|
656
|
+
const { options } = cli.parse('node bin --verbose'.split(' '))
|
|
657
|
+
// Boolean true → coerced to 1 by number schema
|
|
658
|
+
expect(options.verbose).toBe(1)
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
test('boolean string value with boolean schema on value option', () => {
|
|
662
|
+
const cli = goke()
|
|
663
|
+
|
|
664
|
+
cli.option('--flag <flag>', z.boolean().describe('A flag'))
|
|
665
|
+
|
|
666
|
+
const { options: opts1 } = cli.parse('node bin --flag true'.split(' '))
|
|
667
|
+
expect(opts1.flag).toBe(true)
|
|
668
|
+
|
|
669
|
+
const { options: opts2 } = cli.parse('node bin --flag false'.split(' '))
|
|
670
|
+
expect(opts2.flag).toBe(false)
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
test('invalid boolean string with boolean schema throws', () => {
|
|
674
|
+
const cli = gokeTestable()
|
|
675
|
+
|
|
676
|
+
cli.option('--flag <flag>', z.boolean().describe('A flag'))
|
|
677
|
+
|
|
678
|
+
expect(() => cli.parse('node bin --flag yes'.split(' ')))
|
|
679
|
+
.toThrow('expected true or false')
|
|
680
|
+
})
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
describe('edge cases: dot-nested options + schema', () => {
|
|
684
|
+
test('dot-nested option with number schema coerces value', () => {
|
|
685
|
+
const cli = goke()
|
|
686
|
+
|
|
687
|
+
cli.option('--config.port <port>', z.number().describe('Port'))
|
|
688
|
+
|
|
689
|
+
const { options } = cli.parse('node bin --config.port 3000'.split(' '))
|
|
690
|
+
expect(options.config).toEqual({ port: 3000 })
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
test('dot-nested default uses nested object shape', () => {
|
|
694
|
+
const cli = goke()
|
|
695
|
+
|
|
696
|
+
cli.option('--config.port [port]', z.number().default(8080).describe('Port'))
|
|
697
|
+
|
|
698
|
+
const { options } = cli.parse('node bin'.split(' '))
|
|
699
|
+
expect(options.config).toEqual({ port: 8080 })
|
|
700
|
+
})
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
describe('edge cases: kebab-case + schema', () => {
|
|
704
|
+
test('kebab-case option coerced via schema and accessible as camelCase', () => {
|
|
705
|
+
const cli = goke()
|
|
706
|
+
|
|
707
|
+
cli.option('--max-retries <count>', z.number().describe('Max retries'))
|
|
708
|
+
|
|
709
|
+
const { options } = cli.parse('node bin --max-retries 5'.split(' '))
|
|
710
|
+
expect(options.maxRetries).toBe(5)
|
|
711
|
+
expect(typeof options.maxRetries).toBe('number')
|
|
712
|
+
})
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
describe('edge cases: empty string values', () => {
|
|
716
|
+
test('empty string with string schema stays empty string', () => {
|
|
717
|
+
const cli = goke()
|
|
718
|
+
|
|
719
|
+
cli.option('--name <name>', z.string().describe('Name'))
|
|
720
|
+
|
|
721
|
+
const { options } = cli.parse(['node', 'bin', '--name', ''])
|
|
722
|
+
expect(options.name).toBe('')
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
test('empty string with number schema throws', () => {
|
|
726
|
+
const cli = gokeTestable()
|
|
727
|
+
|
|
728
|
+
cli.option('--port <port>', z.number().describe('Port'))
|
|
729
|
+
|
|
730
|
+
expect(() => cli.parse(['node', 'bin', '--port', '']))
|
|
731
|
+
.toThrow('expected number, got empty string')
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
test('empty string with nullable number schema returns null', () => {
|
|
735
|
+
const cli = goke()
|
|
736
|
+
|
|
737
|
+
cli.option('--timeout <timeout>', z.nullable(z.number()).describe('Timeout'))
|
|
738
|
+
|
|
739
|
+
const { options } = cli.parse(['node', 'bin', '--timeout', ''])
|
|
740
|
+
expect(options.timeout).toBe(null)
|
|
741
|
+
})
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
describe('edge cases: global options with schema in subcommands', () => {
|
|
745
|
+
test('global option schema applies to subcommand parsing', () => {
|
|
746
|
+
const cli = goke()
|
|
747
|
+
let result: any = {}
|
|
748
|
+
|
|
749
|
+
cli.option('--port <port>', z.number().describe('Port'))
|
|
750
|
+
|
|
751
|
+
cli
|
|
752
|
+
.command('serve', 'Start server')
|
|
753
|
+
.action((options) => { result = options })
|
|
754
|
+
|
|
755
|
+
cli.parse('node bin serve --port 3000'.split(' '), { run: true })
|
|
756
|
+
expect(result.port).toBe(3000)
|
|
757
|
+
expect(typeof result.port).toBe('number')
|
|
758
|
+
})
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
describe('edge cases: short alias + schema', () => {
|
|
762
|
+
test('short alias repeated with array schema', () => {
|
|
763
|
+
const cli = goke()
|
|
764
|
+
|
|
765
|
+
cli.option('-t, --tag <tag>', z.array(z.string()).describe('Tags'))
|
|
766
|
+
|
|
767
|
+
const { options } = cli.parse('node bin -t foo -t bar'.split(' '))
|
|
768
|
+
expect(options.tag).toEqual(['foo', 'bar'])
|
|
769
|
+
expect(options.t).toEqual(['foo', 'bar'])
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
test('short alias single value with array schema wraps', () => {
|
|
773
|
+
const cli = goke()
|
|
774
|
+
|
|
775
|
+
cli.option('-t, --tag <tag>', z.array(z.string()).describe('Tags'))
|
|
776
|
+
|
|
777
|
+
const { options } = cli.parse('node bin -t foo'.split(' '))
|
|
778
|
+
expect(options.tag).toEqual(['foo'])
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
test('short alias with number schema coerces', () => {
|
|
782
|
+
const cli = goke()
|
|
783
|
+
|
|
784
|
+
cli.option('-p, --port <port>', z.number().describe('Port'))
|
|
785
|
+
|
|
786
|
+
const { options } = cli.parse('node bin -p 8080'.split(' '))
|
|
787
|
+
expect(options.port).toBe(8080)
|
|
788
|
+
expect(options.p).toBe(8080)
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
test('short alias repeated with non-array schema throws', () => {
|
|
792
|
+
const cli = gokeTestable()
|
|
793
|
+
|
|
794
|
+
cli.option('-p, --port <port>', z.number().describe('Port'))
|
|
795
|
+
|
|
796
|
+
expect(() => cli.parse('node bin -p 3000 -p 4000'.split(' ')))
|
|
797
|
+
.toThrow('does not accept multiple values')
|
|
798
|
+
})
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
test('throw on unknown options', () => {
|
|
802
|
+
const cli = gokeTestable()
|
|
803
|
+
|
|
804
|
+
cli
|
|
805
|
+
.command('build [entry]', 'Build your app')
|
|
806
|
+
.option('--foo-bar', 'foo bar')
|
|
807
|
+
.option('--aB', 'ab')
|
|
808
|
+
.action(() => {})
|
|
809
|
+
|
|
810
|
+
expect(() => {
|
|
811
|
+
cli.parse(`node bin build app.js --fooBar --a-b --xx`.split(' '))
|
|
812
|
+
}).toThrowError('Unknown option `--xx`')
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
describe('space-separated subcommands', () => {
|
|
816
|
+
test('basic subcommand matching', () => {
|
|
817
|
+
const cli = goke()
|
|
818
|
+
let matched = ''
|
|
819
|
+
|
|
820
|
+
cli.command('mcp login', 'Login to MCP').action(() => {
|
|
821
|
+
matched = 'mcp login'
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
cli.parse(['node', 'bin', 'mcp', 'login'], { run: true })
|
|
825
|
+
expect(matched).toBe('mcp login')
|
|
826
|
+
expect(cli.matchedCommandName).toBe('mcp login')
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
test('subcommand with positional args', () => {
|
|
830
|
+
const cli = goke()
|
|
831
|
+
let receivedId = ''
|
|
832
|
+
|
|
833
|
+
cli.command('mcp getNodeXml <id>', 'Get XML for a node').action((id) => {
|
|
834
|
+
receivedId = id
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
cli.parse(['node', 'bin', 'mcp', 'getNodeXml', '123'], { run: true })
|
|
838
|
+
expect(receivedId).toBe('123')
|
|
839
|
+
expect(cli.matchedCommandName).toBe('mcp getNodeXml')
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
test('subcommand with options', () => {
|
|
843
|
+
const cli = goke()
|
|
844
|
+
let result: any = {}
|
|
845
|
+
|
|
846
|
+
cli
|
|
847
|
+
.command('mcp export <id>', 'Export something')
|
|
848
|
+
.option('--format <format>', 'Output format')
|
|
849
|
+
.action((id, options) => {
|
|
850
|
+
result = { id, format: options.format }
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
cli.parse(['node', 'bin', 'mcp', 'export', 'abc', '--format', 'json'], {
|
|
854
|
+
run: true,
|
|
855
|
+
})
|
|
856
|
+
expect(result).toEqual({ id: 'abc', format: 'json' })
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
test('greedy matching - longer commands match first', () => {
|
|
860
|
+
const cli = goke()
|
|
861
|
+
let matched = ''
|
|
862
|
+
|
|
863
|
+
cli.command('mcp', 'MCP base command').action(() => {
|
|
864
|
+
matched = 'mcp'
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
cli.command('mcp login', 'Login to MCP').action(() => {
|
|
868
|
+
matched = 'mcp login'
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
cli.parse(['node', 'bin', 'mcp', 'login'], { run: true })
|
|
872
|
+
expect(matched).toBe('mcp login')
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
test('three-level subcommand', () => {
|
|
876
|
+
const cli = goke()
|
|
877
|
+
let matched = ''
|
|
878
|
+
|
|
879
|
+
cli.command('git remote add', 'Add a remote').action(() => {
|
|
880
|
+
matched = 'git remote add'
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
cli.parse(['node', 'bin', 'git', 'remote', 'add'], { run: true })
|
|
884
|
+
expect(matched).toBe('git remote add')
|
|
885
|
+
expect(cli.matchedCommandName).toBe('git remote add')
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
test('single-word commands still work (backward compatibility)', () => {
|
|
889
|
+
const cli = goke()
|
|
890
|
+
let matched = ''
|
|
891
|
+
|
|
892
|
+
cli.command('build', 'Build the project').action(() => {
|
|
893
|
+
matched = 'build'
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
cli.parse(['node', 'bin', 'build'], { run: true })
|
|
897
|
+
expect(matched).toBe('build')
|
|
898
|
+
expect(cli.matchedCommandName).toBe('build')
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
test('subcommand does not match when args are insufficient', () => {
|
|
902
|
+
const cli = goke()
|
|
903
|
+
let matched = ''
|
|
904
|
+
|
|
905
|
+
cli.command('mcp login', 'Login to MCP').action(() => {
|
|
906
|
+
matched = 'mcp login'
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
cli.command('mcp', 'MCP base').action(() => {
|
|
910
|
+
matched = 'mcp base'
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
cli.parse(['node', 'bin', 'mcp'], { run: true })
|
|
914
|
+
expect(matched).toBe('mcp base')
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
test('default command should not match if args are prefix of another command', () => {
|
|
918
|
+
const cli = goke()
|
|
919
|
+
let matched = ''
|
|
920
|
+
|
|
921
|
+
cli.command('mcp login', 'Login to MCP').action(() => {
|
|
922
|
+
matched = 'mcp login'
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
cli.command('', 'Default command').action(() => {
|
|
926
|
+
matched = 'default'
|
|
927
|
+
})
|
|
928
|
+
|
|
929
|
+
cli.parse(['node', 'bin', 'mcp'], { run: true })
|
|
930
|
+
expect(matched).toBe('')
|
|
931
|
+
expect(cli.matchedCommand).toBeUndefined()
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
test('default command should match when args do not prefix any command', () => {
|
|
935
|
+
const cli = goke()
|
|
936
|
+
let matched = ''
|
|
937
|
+
let receivedArg = ''
|
|
938
|
+
|
|
939
|
+
cli.command('mcp login', 'Login to MCP').action(() => {
|
|
940
|
+
matched = 'mcp login'
|
|
941
|
+
})
|
|
942
|
+
|
|
943
|
+
cli.command('<file>', 'Default command').action((file) => {
|
|
944
|
+
matched = 'default'
|
|
945
|
+
receivedArg = file
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
cli.parse(['node', 'bin', 'foo'], { run: true })
|
|
949
|
+
expect(matched).toBe('default')
|
|
950
|
+
expect(receivedArg).toBe('foo')
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
test('help output with subcommands', () => {
|
|
954
|
+
let output = ''
|
|
955
|
+
const cli = goke('mycli', {
|
|
956
|
+
stdout: { write(data) { output += data } },
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
cli.command('mcp login <url>', 'Login to MCP server')
|
|
960
|
+
cli.command('mcp logout', 'Logout from MCP server')
|
|
961
|
+
cli.command('mcp status', 'Show connection status')
|
|
962
|
+
cli.command('git remote add <name> <url>', 'Add a git remote')
|
|
963
|
+
cli.command('git remote remove <name>', 'Remove a git remote')
|
|
964
|
+
cli.command('build', 'Build the project').option('--watch', 'Watch mode')
|
|
965
|
+
|
|
966
|
+
cli.help()
|
|
967
|
+
// parse with --help triggers outputHelp() internally, which writes to our captured stdout
|
|
968
|
+
cli.parse(['node', 'bin', '--help'], { run: false })
|
|
969
|
+
|
|
970
|
+
expect(stripAnsi(output)).toMatchInlineSnapshot(`
|
|
971
|
+
"mycli
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
Usage:
|
|
975
|
+
$ mycli <command> [options]
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
Commands:
|
|
979
|
+
mcp login <url> Login to MCP server
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
mcp logout Logout from MCP server
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
mcp status Show connection status
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
git remote add <name> <url> Add a git remote
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
git remote remove <name> Remove a git remote
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
build Build the project
|
|
995
|
+
|
|
996
|
+
--watch Watch mode
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
Options:
|
|
1000
|
+
-h, --help Display this message
|
|
1001
|
+
"
|
|
1002
|
+
`)
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
test('unknown subcommand shows filtered help for prefix', () => {
|
|
1006
|
+
let output = ''
|
|
1007
|
+
const cli = goke('mycli', {
|
|
1008
|
+
stdout: { write(data) { output += data } },
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
cli.command('mcp login', 'Login to MCP')
|
|
1012
|
+
cli.command('mcp logout', 'Logout from MCP')
|
|
1013
|
+
cli.command('mcp status', 'Show status')
|
|
1014
|
+
cli.command('build', 'Build project')
|
|
1015
|
+
|
|
1016
|
+
cli.help()
|
|
1017
|
+
|
|
1018
|
+
// User types "mcp nonexistent" - should show help for mcp commands
|
|
1019
|
+
cli.parse(['node', 'bin', 'mcp', 'nonexistent'], { run: true })
|
|
1020
|
+
|
|
1021
|
+
expect(cli.matchedCommand).toBeUndefined()
|
|
1022
|
+
const normalizedOutput = stripAnsi(output)
|
|
1023
|
+
expect(normalizedOutput).toContain('Unknown command: mcp nonexistent')
|
|
1024
|
+
expect(normalizedOutput).toContain('Available "mcp" commands:')
|
|
1025
|
+
expect(normalizedOutput).toContain('mcp login')
|
|
1026
|
+
expect(normalizedOutput).toContain('mcp logout')
|
|
1027
|
+
expect(normalizedOutput).toContain('mcp status')
|
|
1028
|
+
expect(normalizedOutput).not.toContain('build')
|
|
1029
|
+
})
|
|
1030
|
+
|
|
1031
|
+
test('unknown command without prefix does not show filtered help', () => {
|
|
1032
|
+
let output = ''
|
|
1033
|
+
const cli = goke('mycli', {
|
|
1034
|
+
stdout: { write(data) { output += data } },
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
cli.command('mcp login', 'Login to MCP')
|
|
1038
|
+
cli.command('build', 'Build project')
|
|
1039
|
+
|
|
1040
|
+
cli.help()
|
|
1041
|
+
|
|
1042
|
+
// User types "foo" - no commands start with "foo"
|
|
1043
|
+
cli.parse(['node', 'bin', 'foo'], { run: true })
|
|
1044
|
+
|
|
1045
|
+
// Should not show filtered help since "foo" is not a prefix of any command
|
|
1046
|
+
expect(stripAnsi(output)).not.toContain('Available "foo" commands')
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
test('unknown command without prefix outputs root help', () => {
|
|
1050
|
+
let output = ''
|
|
1051
|
+
const cli = goke('mycli', {
|
|
1052
|
+
stdout: { write(data) { output += data } },
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
cli.command('mcp login', 'Login to MCP')
|
|
1056
|
+
cli.command('build', 'Build project')
|
|
1057
|
+
|
|
1058
|
+
cli.help()
|
|
1059
|
+
|
|
1060
|
+
// User types an unknown command that does not match any prefix group
|
|
1061
|
+
cli.parse(['node', 'bin', 'something'], { run: true })
|
|
1062
|
+
|
|
1063
|
+
expect(cli.matchedCommand).toBeUndefined()
|
|
1064
|
+
expect(stripAnsi(output)).toContain('Usage:')
|
|
1065
|
+
expect(stripAnsi(output)).toContain('$ mycli <command> [options]')
|
|
1066
|
+
expect(stripAnsi(output)).toContain('mcp login')
|
|
1067
|
+
expect(stripAnsi(output)).toContain('build')
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
test('no args without default command outputs root help', () => {
|
|
1071
|
+
const stdout = createTestOutputStream()
|
|
1072
|
+
const cli = goke('mycli', { stdout })
|
|
1073
|
+
|
|
1074
|
+
cli.command('mcp login', 'Login to MCP')
|
|
1075
|
+
cli.command('build', 'Build project')
|
|
1076
|
+
cli.help()
|
|
1077
|
+
|
|
1078
|
+
cli.parse(['node', 'bin'], { run: true })
|
|
1079
|
+
|
|
1080
|
+
expect(stdout.text).toContain('Usage:')
|
|
1081
|
+
expect(stdout.text).toContain('$ mycli <command> [options]')
|
|
1082
|
+
expect(stdout.text).toContain('mcp login')
|
|
1083
|
+
expect(stdout.text).toContain('build')
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
test('prefix --help shows filtered help for matching command group', () => {
|
|
1087
|
+
let output = ''
|
|
1088
|
+
const cli = goke('mycli', {
|
|
1089
|
+
stdout: { write(data) { output += data } },
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
cli.command('mcp login', 'Login to MCP')
|
|
1093
|
+
cli.command('mcp logout', 'Logout from MCP')
|
|
1094
|
+
cli.command('mcp status', 'Show status')
|
|
1095
|
+
cli.command('build', 'Build project')
|
|
1096
|
+
|
|
1097
|
+
cli.help()
|
|
1098
|
+
cli.parse(['node', 'bin', 'mcp', '--help'], { run: true })
|
|
1099
|
+
|
|
1100
|
+
const normalizedOutput = stripAnsi(output)
|
|
1101
|
+
expect(normalizedOutput).toMatchInlineSnapshot(`
|
|
1102
|
+
"mycli
|
|
1103
|
+
|
|
1104
|
+
Available \"mcp\" commands:
|
|
1105
|
+
|
|
1106
|
+
mcp login Login to MCP
|
|
1107
|
+
mcp logout Logout from MCP
|
|
1108
|
+
mcp status Show status
|
|
1109
|
+
|
|
1110
|
+
Run \"mycli <command> --help\" for more information.
|
|
1111
|
+
"
|
|
1112
|
+
`)
|
|
1113
|
+
})
|
|
1114
|
+
})
|
|
1115
|
+
|
|
1116
|
+
describe('many commands with root command (empty string)', () => {
|
|
1117
|
+
test('root command runs when no subcommand given', () => {
|
|
1118
|
+
const cli = goke('deploy')
|
|
1119
|
+
let matched = ''
|
|
1120
|
+
|
|
1121
|
+
cli.command('', 'Deploy the current project').action(() => {
|
|
1122
|
+
matched = 'root'
|
|
1123
|
+
})
|
|
1124
|
+
|
|
1125
|
+
cli.command('init', 'Initialize project').action(() => {
|
|
1126
|
+
matched = 'init'
|
|
1127
|
+
})
|
|
1128
|
+
|
|
1129
|
+
cli.command('login', 'Authenticate').action(() => {
|
|
1130
|
+
matched = 'login'
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
cli.parse(['node', 'bin'], { run: true })
|
|
1134
|
+
expect(matched).toBe('root')
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
test('root command receives options', () => {
|
|
1138
|
+
const cli = goke('deploy')
|
|
1139
|
+
let result: any = {}
|
|
1140
|
+
|
|
1141
|
+
cli
|
|
1142
|
+
.command('', 'Deploy the current project')
|
|
1143
|
+
.option('--env <env>', z.string().default('production').describe('Target environment'))
|
|
1144
|
+
.option('--dry-run', 'Preview without deploying')
|
|
1145
|
+
.action((options) => {
|
|
1146
|
+
result = options
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
cli.command('init', 'Initialize project').action(() => {})
|
|
1150
|
+
cli.command('login', 'Authenticate').action(() => {})
|
|
1151
|
+
|
|
1152
|
+
cli.parse(['node', 'bin', '--env', 'staging', '--dry-run'], { run: true })
|
|
1153
|
+
expect(result.env).toBe('staging')
|
|
1154
|
+
expect(result.dryRun).toBe(true)
|
|
1155
|
+
})
|
|
1156
|
+
|
|
1157
|
+
test('root command uses defaults when no options given', () => {
|
|
1158
|
+
const cli = goke('deploy')
|
|
1159
|
+
let result: any = {}
|
|
1160
|
+
|
|
1161
|
+
cli
|
|
1162
|
+
.command('', 'Deploy the current project')
|
|
1163
|
+
.option('--env [env]', z.string().default('production').describe('Target environment'))
|
|
1164
|
+
.action((options) => {
|
|
1165
|
+
result = options
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
cli.command('init', 'Initialize project').action(() => {})
|
|
1169
|
+
|
|
1170
|
+
cli.parse(['node', 'bin'], { run: true })
|
|
1171
|
+
expect(result.env).toBe('production')
|
|
1172
|
+
})
|
|
1173
|
+
|
|
1174
|
+
test('subcommands take priority over root command', () => {
|
|
1175
|
+
const cli = goke('deploy')
|
|
1176
|
+
let matched = ''
|
|
1177
|
+
|
|
1178
|
+
cli.command('', 'Deploy the current project').action(() => {
|
|
1179
|
+
matched = 'root'
|
|
1180
|
+
})
|
|
1181
|
+
|
|
1182
|
+
cli.command('init', 'Initialize project').action(() => {
|
|
1183
|
+
matched = 'init'
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
cli.command('login', 'Authenticate').action(() => {
|
|
1187
|
+
matched = 'login'
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
cli.command('status', 'Show status').action(() => {
|
|
1191
|
+
matched = 'status'
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
cli.parse(['node', 'bin', 'status'], { run: true })
|
|
1195
|
+
expect(matched).toBe('status')
|
|
1196
|
+
})
|
|
1197
|
+
|
|
1198
|
+
test('subcommand with args works alongside root command', () => {
|
|
1199
|
+
const cli = goke('deploy')
|
|
1200
|
+
let rootCalled = false
|
|
1201
|
+
let logsResult: any = {}
|
|
1202
|
+
|
|
1203
|
+
cli.command('', 'Deploy').action(() => {
|
|
1204
|
+
rootCalled = true
|
|
1205
|
+
})
|
|
1206
|
+
|
|
1207
|
+
cli
|
|
1208
|
+
.command('logs <deploymentId>', 'Stream logs')
|
|
1209
|
+
.option('--follow', 'Follow output')
|
|
1210
|
+
.option('--lines [n]', z.number().default(100).describe('Number of lines'))
|
|
1211
|
+
.action((deploymentId, options) => {
|
|
1212
|
+
logsResult = { deploymentId, ...options }
|
|
1213
|
+
})
|
|
1214
|
+
|
|
1215
|
+
cli.parse(['node', 'bin', 'logs', 'abc123', '--follow', '--lines', '50'], { run: true })
|
|
1216
|
+
expect(rootCalled).toBe(false)
|
|
1217
|
+
expect(logsResult.deploymentId).toBe('abc123')
|
|
1218
|
+
expect(logsResult.follow).toBe(true)
|
|
1219
|
+
expect(logsResult.lines).toBe(50)
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
test('help shows root and all subcommands', () => {
|
|
1223
|
+
const stdout = createTestOutputStream()
|
|
1224
|
+
const cli = goke('deploy', { stdout })
|
|
1225
|
+
|
|
1226
|
+
cli
|
|
1227
|
+
.command('', 'Deploy the current project')
|
|
1228
|
+
.option('--env <env>', 'Target environment')
|
|
1229
|
+
|
|
1230
|
+
cli.command('init', 'Initialize a new project')
|
|
1231
|
+
cli.command('login', 'Authenticate with the server')
|
|
1232
|
+
cli.command('logout', 'Clear saved credentials')
|
|
1233
|
+
cli.command('status', 'Show deployment status')
|
|
1234
|
+
cli.command('logs <deploymentId>', 'Stream logs for a deployment')
|
|
1235
|
+
|
|
1236
|
+
cli.help()
|
|
1237
|
+
cli.parse(['node', 'bin', '--help'], { run: false })
|
|
1238
|
+
|
|
1239
|
+
expect(stdout.text).toContain('init')
|
|
1240
|
+
expect(stdout.text).toContain('login')
|
|
1241
|
+
expect(stdout.text).toContain('logout')
|
|
1242
|
+
expect(stdout.text).toContain('status')
|
|
1243
|
+
expect(stdout.text).toContain('logs <deploymentId>')
|
|
1244
|
+
expect(stdout.text).toContain('Initialize a new project')
|
|
1245
|
+
expect(stdout.text).toContain('Stream logs for a deployment')
|
|
1246
|
+
})
|
|
1247
|
+
|
|
1248
|
+
test('root help with many commands renders examples section after options', () => {
|
|
1249
|
+
const stdout = createTestOutputStream()
|
|
1250
|
+
const cli = goke('deploy', { stdout })
|
|
1251
|
+
|
|
1252
|
+
cli
|
|
1253
|
+
.command('', 'Deploy the current project')
|
|
1254
|
+
.option('--env <env>', 'Target environment')
|
|
1255
|
+
.option('--dry-run', 'Preview without deploying')
|
|
1256
|
+
.example('# Deploy to staging first')
|
|
1257
|
+
.example('deploy --env staging --dry-run')
|
|
1258
|
+
|
|
1259
|
+
cli.command('init', 'Initialize a new project')
|
|
1260
|
+
cli.command('login', 'Authenticate with the server')
|
|
1261
|
+
cli.command('logout', 'Clear saved credentials')
|
|
1262
|
+
cli.command('status', 'Show deployment status')
|
|
1263
|
+
cli.command('logs <deploymentId>', 'Stream logs for a deployment')
|
|
1264
|
+
|
|
1265
|
+
cli.help()
|
|
1266
|
+
cli.parse(['node', 'bin', '--help'], { run: false })
|
|
1267
|
+
|
|
1268
|
+
expect(stdout.text).toMatchInlineSnapshot(`
|
|
1269
|
+
"deploy
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
Usage:
|
|
1273
|
+
$ deploy [options]
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
Commands:
|
|
1277
|
+
deploy Deploy the current project
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
init Initialize a new project
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
login Authenticate with the server
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
logout Clear saved credentials
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
status Show deployment status
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
logs <deploymentId> Stream logs for a deployment
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
Options:
|
|
1296
|
+
--env <env> Target environment
|
|
1297
|
+
--dry-run Preview without deploying
|
|
1298
|
+
-h, --help Display this message
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
Examples:
|
|
1302
|
+
# Deploy to staging first
|
|
1303
|
+
deploy --env staging --dry-run
|
|
1304
|
+
"
|
|
1305
|
+
`)
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
test('subcommand help renders command examples at the end', () => {
|
|
1309
|
+
const stdout = createTestOutputStream()
|
|
1310
|
+
const cli = goke('deploy', { stdout, columns: 80 })
|
|
1311
|
+
|
|
1312
|
+
cli.command('', 'Deploy the current project')
|
|
1313
|
+
cli.command('init', 'Initialize a new project')
|
|
1314
|
+
cli.command('login', 'Authenticate with the server')
|
|
1315
|
+
|
|
1316
|
+
cli
|
|
1317
|
+
.command('logs <deploymentId>', 'Stream logs for a deployment')
|
|
1318
|
+
.option('--follow', 'Follow log output')
|
|
1319
|
+
.option('--lines <n>', z.number().default(100).describe('Number of lines'))
|
|
1320
|
+
.example('# Stream last 200 lines for a deployment')
|
|
1321
|
+
.example('deploy logs dep_123 --lines 200')
|
|
1322
|
+
.example('# Keep following new log lines')
|
|
1323
|
+
.example('deploy logs dep_123 --follow')
|
|
1324
|
+
|
|
1325
|
+
cli.help()
|
|
1326
|
+
cli.parse(['node', 'bin', 'logs', '--help'], { run: false })
|
|
1327
|
+
|
|
1328
|
+
expect(stdout.text).toMatchInlineSnapshot(`
|
|
1329
|
+
"deploy
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
Usage:
|
|
1333
|
+
$ deploy logs <deploymentId>
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
Options:
|
|
1337
|
+
--follow Follow log output
|
|
1338
|
+
--lines <n> Number of lines (default: 100)
|
|
1339
|
+
-h, --help Display this message
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
Description:
|
|
1343
|
+
Stream logs for a deployment
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
Examples:
|
|
1347
|
+
# Stream last 200 lines for a deployment
|
|
1348
|
+
deploy logs dep_123 --lines 200
|
|
1349
|
+
# Keep following new log lines
|
|
1350
|
+
deploy logs dep_123 --follow
|
|
1351
|
+
"
|
|
1352
|
+
`)
|
|
1353
|
+
})
|
|
1354
|
+
|
|
1355
|
+
test('root help labels default command with cli name and does not duplicate global options', () => {
|
|
1356
|
+
const stdout = createTestOutputStream()
|
|
1357
|
+
const cli = goke('deploy', { stdout })
|
|
1358
|
+
|
|
1359
|
+
cli.option('--env <env>', 'Target environment')
|
|
1360
|
+
cli
|
|
1361
|
+
.command('', 'Deploy the current project')
|
|
1362
|
+
.option('--env <env>', 'Target environment')
|
|
1363
|
+
.option('--dry-run', 'Preview without deploying')
|
|
1364
|
+
|
|
1365
|
+
cli.command('status', 'Show deployment status')
|
|
1366
|
+
|
|
1367
|
+
cli.help()
|
|
1368
|
+
cli.parse(['node', 'bin', '--help'], { run: false })
|
|
1369
|
+
|
|
1370
|
+
expect(stdout.text).toMatchInlineSnapshot(`
|
|
1371
|
+
"deploy
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
Usage:
|
|
1375
|
+
$ deploy [options]
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
Commands:
|
|
1379
|
+
deploy Deploy the current project
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
status Show deployment status
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
Options:
|
|
1386
|
+
--env <env> Target environment
|
|
1387
|
+
--dry-run Preview without deploying
|
|
1388
|
+
-h, --help Display this message
|
|
1389
|
+
"
|
|
1390
|
+
`)
|
|
1391
|
+
})
|
|
1392
|
+
|
|
1393
|
+
test('root help wraps long command descriptions snapshot', () => {
|
|
1394
|
+
const stdout = createTestOutputStream()
|
|
1395
|
+
const cli = goke('mycli', { stdout, columns: 56 })
|
|
1396
|
+
|
|
1397
|
+
cli.command(
|
|
1398
|
+
'notion-search',
|
|
1399
|
+
'Perform a semantic search over Notion workspace content and connected integrations with advanced filtering options, date filters, and creator filters.',
|
|
1400
|
+
)
|
|
1401
|
+
.option('--query <query>', 'Natural language query text to search for')
|
|
1402
|
+
.option('--limit [limit]', z.number().default(10).describe('Maximum number of results to return'))
|
|
1403
|
+
|
|
1404
|
+
cli.command(
|
|
1405
|
+
'notion-fetch',
|
|
1406
|
+
'Retrieve a Notion page or database by URL or ID and render the result in enhanced markdown format for terminal output.',
|
|
1407
|
+
).option('--id <id>', 'Notion URL or UUID to fetch')
|
|
1408
|
+
|
|
1409
|
+
cli.help()
|
|
1410
|
+
cli.parse(['node', 'bin', '--help'], { run: false })
|
|
1411
|
+
|
|
1412
|
+
expect(stdout.text).toMatchInlineSnapshot(`
|
|
1413
|
+
"mycli
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
Usage:
|
|
1417
|
+
$ mycli <command> [options]
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
Commands:
|
|
1421
|
+
notion-search Perform a semantic search over
|
|
1422
|
+
Notion workspace content and
|
|
1423
|
+
connected integrations with
|
|
1424
|
+
advanced filtering options, date
|
|
1425
|
+
filters, and creator filters.
|
|
1426
|
+
|
|
1427
|
+
--query <query> Natural language query text to
|
|
1428
|
+
search for
|
|
1429
|
+
--limit [limit] Maximum number of results to return
|
|
1430
|
+
(default: 10)
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
notion-fetch Retrieve a Notion page or database
|
|
1434
|
+
by URL or ID and render the result
|
|
1435
|
+
in enhanced markdown format for
|
|
1436
|
+
terminal output.
|
|
1437
|
+
|
|
1438
|
+
--id <id> Notion URL or UUID to fetch
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
Options:
|
|
1442
|
+
-h, --help Display this message
|
|
1443
|
+
"
|
|
1444
|
+
`)
|
|
1445
|
+
})
|
|
1446
|
+
|
|
1447
|
+
test('root help aligns command descriptions with mixed command lengths', () => {
|
|
1448
|
+
const stdout = createTestOutputStream()
|
|
1449
|
+
const cli = goke('gtui', { stdout, columns: 120 })
|
|
1450
|
+
|
|
1451
|
+
cli.command('auth login', 'Authenticate with Google (opens browser)')
|
|
1452
|
+
cli.command('auth logout', 'Remove stored credentials').option('--force', 'Skip confirmation')
|
|
1453
|
+
cli.command('mail list', 'List email threads').option('--folder [folder]', 'Folder to list')
|
|
1454
|
+
cli.command('attachment get <messageId> <attachmentId>', 'Download an attachment')
|
|
1455
|
+
|
|
1456
|
+
cli.help()
|
|
1457
|
+
cli.parse(['node', 'bin', '--help'], { run: false })
|
|
1458
|
+
|
|
1459
|
+
expect(stdout.text).toMatchInlineSnapshot(`
|
|
1460
|
+
"gtui
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
Usage:
|
|
1464
|
+
$ gtui <command> [options]
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
Commands:
|
|
1468
|
+
auth login Authenticate with Google (opens browser)
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
auth logout Remove stored credentials
|
|
1472
|
+
|
|
1473
|
+
--force Skip confirmation
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
mail list List email threads
|
|
1477
|
+
|
|
1478
|
+
--folder [folder] Folder to list
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
attachment get <messageId> <attachmentId> Download an attachment
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
Options:
|
|
1485
|
+
-h, --help Display this message
|
|
1486
|
+
"
|
|
1487
|
+
`)
|
|
1488
|
+
})
|
|
1489
|
+
|
|
1490
|
+
test('root help wraps all multi-line description lines', () => {
|
|
1491
|
+
const stdout = createTestOutputStream()
|
|
1492
|
+
const cli = goke('mycli', { stdout, columns: 64 })
|
|
1493
|
+
|
|
1494
|
+
cli.command(
|
|
1495
|
+
'notion-create',
|
|
1496
|
+
'Create a new page.\n {"title":"Example"}\n {"done":true}',
|
|
1497
|
+
)
|
|
1498
|
+
cli.help()
|
|
1499
|
+
cli.parse(['node', 'bin', '--help'], { run: false })
|
|
1500
|
+
|
|
1501
|
+
expect(stdout.text).toContain('{"title":"Example"}')
|
|
1502
|
+
expect(stdout.text).toContain('{"done":true}')
|
|
1503
|
+
})
|
|
1504
|
+
|
|
1505
|
+
test('root help snapshot when columns is undefined (no wrapping fallback)', () => {
|
|
1506
|
+
const stdout = createTestOutputStream()
|
|
1507
|
+
const originalColumns = process.stdout.columns
|
|
1508
|
+
|
|
1509
|
+
Object.defineProperty(process.stdout, 'columns', {
|
|
1510
|
+
configurable: true,
|
|
1511
|
+
value: undefined,
|
|
1512
|
+
})
|
|
1513
|
+
|
|
1514
|
+
try {
|
|
1515
|
+
const cli = goke('mycli', { stdout })
|
|
1516
|
+
|
|
1517
|
+
cli.command(
|
|
1518
|
+
'notion-search',
|
|
1519
|
+
'Perform a semantic search over Notion workspace content and connected integrations with advanced filtering options, date filters, and creator filters.',
|
|
1520
|
+
)
|
|
1521
|
+
.option('--query <query>', 'Natural language query text to search for')
|
|
1522
|
+
.option('--limit [limit]', z.number().default(10).describe('Maximum number of results to return'))
|
|
1523
|
+
|
|
1524
|
+
cli.help()
|
|
1525
|
+
cli.parse(['node', 'bin', '--help'], { run: false })
|
|
1526
|
+
|
|
1527
|
+
expect(stdout.text).toMatchInlineSnapshot(`
|
|
1528
|
+
"mycli
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
Usage:
|
|
1532
|
+
$ mycli <command> [options]
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
Commands:
|
|
1536
|
+
notion-search Perform a semantic search over Notion workspace content and connected integrations with advanced filtering options, date filters, and creator filters.
|
|
1537
|
+
|
|
1538
|
+
--query <query> Natural language query text to search for
|
|
1539
|
+
--limit [limit] Maximum number of results to return (default: 10)
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
Options:
|
|
1543
|
+
-h, --help Display this message
|
|
1544
|
+
"
|
|
1545
|
+
`)
|
|
1546
|
+
} finally {
|
|
1547
|
+
Object.defineProperty(process.stdout, 'columns', {
|
|
1548
|
+
configurable: true,
|
|
1549
|
+
value: originalColumns,
|
|
1550
|
+
})
|
|
1551
|
+
}
|
|
1552
|
+
})
|
|
1553
|
+
|
|
1554
|
+
test('many subcommands all resolve correctly', () => {
|
|
1555
|
+
const cli = goke('deploy')
|
|
1556
|
+
let matched = ''
|
|
1557
|
+
|
|
1558
|
+
cli.command('', 'Root').action(() => { matched = 'root' })
|
|
1559
|
+
cli.command('init', 'Init').action(() => { matched = 'init' })
|
|
1560
|
+
cli.command('login', 'Login').action(() => { matched = 'login' })
|
|
1561
|
+
cli.command('logout', 'Logout').action(() => { matched = 'logout' })
|
|
1562
|
+
cli.command('status', 'Status').action(() => { matched = 'status' })
|
|
1563
|
+
cli.command('logs <id>', 'Logs').action(() => { matched = 'logs' })
|
|
1564
|
+
cli.command('rollback <id>', 'Rollback').action(() => { matched = 'rollback' })
|
|
1565
|
+
cli.command('config set <key> <value>', 'Set config').action(() => { matched = 'config set' })
|
|
1566
|
+
|
|
1567
|
+
// Test each command resolves to the right one
|
|
1568
|
+
cli.parse(['node', 'bin'], { run: true })
|
|
1569
|
+
expect(matched).toBe('root')
|
|
1570
|
+
|
|
1571
|
+
matched = ''
|
|
1572
|
+
cli.parse(['node', 'bin', 'init'], { run: true })
|
|
1573
|
+
expect(matched).toBe('init')
|
|
1574
|
+
|
|
1575
|
+
matched = ''
|
|
1576
|
+
cli.parse(['node', 'bin', 'login'], { run: true })
|
|
1577
|
+
expect(matched).toBe('login')
|
|
1578
|
+
|
|
1579
|
+
matched = ''
|
|
1580
|
+
cli.parse(['node', 'bin', 'logout'], { run: true })
|
|
1581
|
+
expect(matched).toBe('logout')
|
|
1582
|
+
|
|
1583
|
+
matched = ''
|
|
1584
|
+
cli.parse(['node', 'bin', 'status'], { run: true })
|
|
1585
|
+
expect(matched).toBe('status')
|
|
1586
|
+
|
|
1587
|
+
matched = ''
|
|
1588
|
+
cli.parse(['node', 'bin', 'logs', 'dep-123'], { run: true })
|
|
1589
|
+
expect(matched).toBe('logs')
|
|
1590
|
+
|
|
1591
|
+
matched = ''
|
|
1592
|
+
cli.parse(['node', 'bin', 'rollback', 'dep-456'], { run: true })
|
|
1593
|
+
expect(matched).toBe('rollback')
|
|
1594
|
+
|
|
1595
|
+
matched = ''
|
|
1596
|
+
cli.parse(['node', 'bin', 'config', 'set', 'region', 'us-east-1'], { run: true })
|
|
1597
|
+
expect(matched).toBe('config set')
|
|
1598
|
+
})
|
|
1599
|
+
})
|
|
1600
|
+
|
|
1601
|
+
describe('stdout/stderr/argv injection', () => {
|
|
1602
|
+
test('stdout captures help output', () => {
|
|
1603
|
+
const stdout = createTestOutputStream()
|
|
1604
|
+
const cli = goke('mycli', { stdout })
|
|
1605
|
+
|
|
1606
|
+
cli.command('serve', 'Start server')
|
|
1607
|
+
cli.help()
|
|
1608
|
+
cli.parse(['node', 'bin', '--help'], { run: false })
|
|
1609
|
+
cli.outputHelp()
|
|
1610
|
+
|
|
1611
|
+
expect(stdout.text).toContain('mycli')
|
|
1612
|
+
expect(stdout.text).toContain('serve')
|
|
1613
|
+
expect(stdout.text).toContain('Start server')
|
|
1614
|
+
})
|
|
1615
|
+
|
|
1616
|
+
test('stdout captures version output', () => {
|
|
1617
|
+
const stdout = createTestOutputStream()
|
|
1618
|
+
const cli = goke('mycli', { stdout })
|
|
1619
|
+
|
|
1620
|
+
cli.version('1.2.3')
|
|
1621
|
+
cli.parse(['node', 'bin', '--version'], { run: false })
|
|
1622
|
+
cli.outputVersion()
|
|
1623
|
+
|
|
1624
|
+
expect(stdout.text).toContain('mycli/1.2.3')
|
|
1625
|
+
})
|
|
1626
|
+
|
|
1627
|
+
test('stdout captures prefix help for unknown subcommands', () => {
|
|
1628
|
+
const stdout = createTestOutputStream()
|
|
1629
|
+
const cli = goke('mycli', { stdout })
|
|
1630
|
+
|
|
1631
|
+
cli.command('mcp login', 'Login to MCP')
|
|
1632
|
+
cli.command('mcp logout', 'Logout from MCP')
|
|
1633
|
+
cli.help()
|
|
1634
|
+
|
|
1635
|
+
cli.parse(['node', 'bin', 'mcp', 'nonexistent'], { run: true })
|
|
1636
|
+
|
|
1637
|
+
expect(stdout.text).toContain('Unknown command: mcp nonexistent')
|
|
1638
|
+
expect(stdout.text).toContain('mcp login')
|
|
1639
|
+
expect(stdout.text).toContain('mcp logout')
|
|
1640
|
+
})
|
|
1641
|
+
|
|
1642
|
+
test('stderr is separate from stdout', () => {
|
|
1643
|
+
const stdout = createTestOutputStream()
|
|
1644
|
+
const stderr = createTestOutputStream()
|
|
1645
|
+
const cli = goke('mycli', { stdout, stderr })
|
|
1646
|
+
|
|
1647
|
+
cli.console.log('hello stdout')
|
|
1648
|
+
cli.console.error('hello stderr')
|
|
1649
|
+
|
|
1650
|
+
expect(stdout.text).toBe('hello stdout\n')
|
|
1651
|
+
expect(stderr.text).toBe('hello stderr\n')
|
|
1652
|
+
})
|
|
1653
|
+
|
|
1654
|
+
test('argv option is used as default in parse()', () => {
|
|
1655
|
+
const cli = goke('mycli', {
|
|
1656
|
+
argv: ['node', 'bin', 'serve', '--port', '3000'],
|
|
1657
|
+
})
|
|
1658
|
+
|
|
1659
|
+
let result: any = {}
|
|
1660
|
+
cli
|
|
1661
|
+
.command('serve', 'Start server')
|
|
1662
|
+
.option('--port <port>', z.number().describe('Port'))
|
|
1663
|
+
.action((options) => { result = options })
|
|
1664
|
+
|
|
1665
|
+
// parse() without args uses the injected argv
|
|
1666
|
+
cli.parse()
|
|
1667
|
+
|
|
1668
|
+
expect(result.port).toBe(3000)
|
|
1669
|
+
})
|
|
1670
|
+
|
|
1671
|
+
test('parse(customArgv) overrides injected argv', () => {
|
|
1672
|
+
const cli = goke('mycli', {
|
|
1673
|
+
argv: ['node', 'bin', 'serve', '--port', '3000'],
|
|
1674
|
+
})
|
|
1675
|
+
|
|
1676
|
+
let result: any = {}
|
|
1677
|
+
cli
|
|
1678
|
+
.command('serve', 'Start server')
|
|
1679
|
+
.option('--port <port>', z.number().describe('Port'))
|
|
1680
|
+
.action((options) => { result = options })
|
|
1681
|
+
|
|
1682
|
+
// Explicit argv overrides the default
|
|
1683
|
+
cli.parse(['node', 'bin', 'serve', '--port', '8080'])
|
|
1684
|
+
|
|
1685
|
+
expect(result.port).toBe(8080)
|
|
1686
|
+
})
|
|
1687
|
+
|
|
1688
|
+
test('default behavior without options uses process.stdout', () => {
|
|
1689
|
+
const cli = goke('mycli')
|
|
1690
|
+
|
|
1691
|
+
// stdout/stderr should be process.stdout/process.stderr by default
|
|
1692
|
+
expect(cli.stdout).toBe(process.stdout)
|
|
1693
|
+
expect(cli.stderr).toBe(process.stderr)
|
|
1694
|
+
})
|
|
1695
|
+
|
|
1696
|
+
test('createConsole routes log to stdout and error to stderr', () => {
|
|
1697
|
+
const stdout = createTestOutputStream()
|
|
1698
|
+
const stderr = createTestOutputStream()
|
|
1699
|
+
const con = createConsole(stdout, stderr)
|
|
1700
|
+
|
|
1701
|
+
con.log('msg1', 'msg2')
|
|
1702
|
+
con.error('err1', 'err2')
|
|
1703
|
+
|
|
1704
|
+
expect(stdout.text).toBe('msg1 msg2\n')
|
|
1705
|
+
expect(stderr.text).toBe('err1 err2\n')
|
|
1706
|
+
})
|
|
1707
|
+
|
|
1708
|
+
test('createConsole log with no args writes empty line', () => {
|
|
1709
|
+
const stdout = createTestOutputStream()
|
|
1710
|
+
const stderr = createTestOutputStream()
|
|
1711
|
+
const con = createConsole(stdout, stderr)
|
|
1712
|
+
|
|
1713
|
+
con.log()
|
|
1714
|
+
|
|
1715
|
+
expect(stdout.text).toBe('\n')
|
|
1716
|
+
})
|
|
1717
|
+
})
|
|
1718
|
+
|
|
1719
|
+
describe('schema description and default extraction', () => {
|
|
1720
|
+
test('description is extracted from schema and shown in help', () => {
|
|
1721
|
+
const stdout = createTestOutputStream()
|
|
1722
|
+
const cli = goke('mycli', { stdout })
|
|
1723
|
+
|
|
1724
|
+
cli
|
|
1725
|
+
.command('serve', 'Start server')
|
|
1726
|
+
.option('--port <port>', z.number().describe('Port to listen on'))
|
|
1727
|
+
|
|
1728
|
+
cli.help()
|
|
1729
|
+
cli.parse(['node', 'bin', 'serve', '--help'], { run: false })
|
|
1730
|
+
|
|
1731
|
+
expect(stdout.text).toContain('Port to listen on')
|
|
1732
|
+
})
|
|
1733
|
+
|
|
1734
|
+
test('default is extracted from schema and shown in help', () => {
|
|
1735
|
+
const stdout = createTestOutputStream()
|
|
1736
|
+
const cli = goke('mycli', { stdout })
|
|
1737
|
+
|
|
1738
|
+
cli
|
|
1739
|
+
.command('serve', 'Start server')
|
|
1740
|
+
.option('--port [port]', z.number().default(3000).describe('Port'))
|
|
1741
|
+
|
|
1742
|
+
cli.help()
|
|
1743
|
+
cli.parse(['node', 'bin', 'serve', '--help'], { run: false })
|
|
1744
|
+
|
|
1745
|
+
expect(stdout.text).toContain('(default: 3000)')
|
|
1746
|
+
})
|
|
1747
|
+
|
|
1748
|
+
test('deprecated options are hidden from help output', () => {
|
|
1749
|
+
const stdout = createTestOutputStream()
|
|
1750
|
+
const cli = goke('mycli', { stdout })
|
|
1751
|
+
|
|
1752
|
+
cli
|
|
1753
|
+
.command('serve', 'Start server')
|
|
1754
|
+
.option('--old <value>', z.string().meta({ deprecated: true, description: 'Old option' }))
|
|
1755
|
+
.option('--new <value>', z.string().describe('Normal option'))
|
|
1756
|
+
|
|
1757
|
+
cli.help()
|
|
1758
|
+
cli.parse(['node', 'bin', 'serve', '--help'], { run: false })
|
|
1759
|
+
|
|
1760
|
+
// Normal option should be visible
|
|
1761
|
+
expect(stdout.text).toContain('--new')
|
|
1762
|
+
expect(stdout.text).toContain('Normal option')
|
|
1763
|
+
// Deprecated option should be hidden
|
|
1764
|
+
expect(stdout.text).not.toContain('--old')
|
|
1765
|
+
expect(stdout.text).not.toContain('Old option')
|
|
1766
|
+
})
|
|
1767
|
+
|
|
1768
|
+
test('deprecated option still works for parsing (just hidden from help)', () => {
|
|
1769
|
+
const cli = gokeTestable('mycli')
|
|
1770
|
+
|
|
1771
|
+
let result: any = {}
|
|
1772
|
+
cli
|
|
1773
|
+
.command('serve', 'Start server')
|
|
1774
|
+
.option('--old <value>', z.string().meta({ deprecated: true, description: 'Old option' }))
|
|
1775
|
+
.action((options) => { result = options })
|
|
1776
|
+
|
|
1777
|
+
cli.parse(['node', 'bin', 'serve', '--old', 'legacy-value'])
|
|
1778
|
+
|
|
1779
|
+
// Deprecated option should still be parsed and usable
|
|
1780
|
+
expect(result.old).toBe('legacy-value')
|
|
1781
|
+
})
|
|
1782
|
+
|
|
1783
|
+
test('deprecated options hidden from global help', () => {
|
|
1784
|
+
const stdout = createTestOutputStream()
|
|
1785
|
+
const cli = goke('mycli', { stdout })
|
|
1786
|
+
|
|
1787
|
+
cli.option('--legacy [value]', z.string().meta({ deprecated: true, description: 'Deprecated global' }))
|
|
1788
|
+
cli.option('--current [value]', z.string().describe('Current option'))
|
|
1789
|
+
|
|
1790
|
+
cli.help()
|
|
1791
|
+
cli.parse(['node', 'bin', '--help'], { run: false })
|
|
1792
|
+
|
|
1793
|
+
expect(stdout.text).toContain('--current')
|
|
1794
|
+
expect(stdout.text).toContain('Current option')
|
|
1795
|
+
expect(stdout.text).not.toContain('--legacy')
|
|
1796
|
+
expect(stdout.text).not.toContain('Deprecated global')
|
|
1797
|
+
})
|
|
1798
|
+
})
|