this repo has no description
1import assert from 'node:assert/strict'
2import { describe, test } from 'node:test'
3import { fileURLToPath } from 'node:url'
4import {
5 type Command,
6 type CommandPath,
7 createCLI,
8 findCommand,
9 parseArgs,
10 runCLI,
11 showHelp,
12} from './index.ts'
13
14const __dirname = fileURLToPath(new URL('.', import.meta.url))
15
16const mockCommand: Command = {
17 config: {
18 description: 'Test command',
19 flags: {
20 verbose: {
21 type: 'boolean',
22 description: 'Verbose output',
23 default: false,
24 },
25 },
26 args: {
27 name: {
28 description: 'Name argument',
29 required: true,
30 },
31 },
32 },
33 handler: async () => {},
34}
35
36const mockCommands: CommandPath[] = [
37 {
38 path: 'greet',
39 command: mockCommand,
40 },
41 {
42 path: 'blueprints',
43 command: mockCommand,
44 },
45 {
46 path: 'blueprints/inspect',
47 command: mockCommand,
48 },
49 {
50 path: 'functions/inspect',
51 command: mockCommand,
52 },
53]
54
55describe('shell-casing', () => {
56 describe('findCommand', () => {
57 test('should find command by exact path', () => {
58 const result = findCommand(mockCommands, ['greet'])
59 assert.ok(result)
60 assert.strictEqual(result.config.description, 'Test command')
61 })
62
63 test('should find nested command by path', () => {
64 const result = findCommand(mockCommands, ['blueprints', 'inspect'])
65 assert.ok(result)
66 assert.strictEqual(result.config.description, 'Test command')
67 })
68
69 test('should return null for non-existent command', () => {
70 const result = findCommand(mockCommands, ['nonexistent'])
71 assert.strictEqual(result, null)
72 })
73
74 test('should return null for empty path', () => {
75 const result = findCommand(mockCommands, [])
76 assert.strictEqual(result, null)
77 })
78
79 test('should return null for partial path', () => {
80 const result = findCommand(mockCommands, ['blueprints'])
81 assert.ok(result) // blueprints exists as a command
82 })
83 })
84
85 describe('parseArgs', () => {
86 test('should parse simple command', () => {
87 const result = parseArgs(mockCommands, ['greet'])
88 assert.deepStrictEqual(result.command, ['greet'])
89 assert.deepStrictEqual(result.args, {})
90 assert.deepStrictEqual(result.flags, {})
91 assert.ok(result.target)
92 })
93
94 test('should parse nested command', () => {
95 const result = parseArgs(mockCommands, ['blueprints', 'inspect'])
96 assert.deepStrictEqual(result.command, ['blueprints', 'inspect'])
97 assert.deepStrictEqual(result.args, {})
98 assert.deepStrictEqual(result.flags, {})
99 assert.ok(result.target)
100 })
101
102 test('should parse command with flags', () => {
103 const result = parseArgs(mockCommands, [
104 'greet',
105 '--verbose',
106 '--name=John',
107 ])
108 assert.deepStrictEqual(result.command, ['greet'])
109 assert.deepStrictEqual(result.flags, { verbose: true, name: 'John' })
110 assert.deepStrictEqual(result.args, {})
111 assert.ok(result.target)
112 })
113
114 test('should parse command with arguments', () => {
115 const result = parseArgs(mockCommands, ['greet', 'John', 'Doe'])
116 assert.deepStrictEqual(result.command, ['greet'])
117 assert.deepStrictEqual(result.args, { name: 'John' })
118 assert.deepStrictEqual(result.flags, {})
119 assert.ok(result.target)
120 })
121
122 test('should find longest matching command path', () => {
123 const result = parseArgs(mockCommands, [
124 'blueprints',
125 'inspect',
126 'my-blueprint',
127 ])
128 assert.deepStrictEqual(result.command, ['blueprints', 'inspect'])
129 assert.deepStrictEqual(result.args, { name: 'my-blueprint' })
130 assert.ok(result.target)
131 })
132
133 test('should handle empty argv', () => {
134 const result = parseArgs(mockCommands, [])
135 assert.deepStrictEqual(result.command, [])
136 assert.deepStrictEqual(result.args, {})
137 assert.deepStrictEqual(result.flags, {})
138 assert.strictEqual(result.target, null)
139 })
140
141 test('should handle mixed flags and args', () => {
142 const result = parseArgs(mockCommands, [
143 'greet',
144 '--verbose',
145 'John',
146 '--formal',
147 ])
148 assert.deepStrictEqual(result.command, ['greet'])
149 assert.deepStrictEqual(result.flags, { verbose: 'John', formal: true })
150 assert.deepStrictEqual(result.args, {})
151 assert.ok(result.target)
152 })
153 })
154
155 describe('showHelp', () => {
156 test('should display commands with proper indentation', () => {
157 // snipe console.log output
158 const originalLog = console.log
159 const logs: string[] = []
160 console.log = (...args: string[]) => {
161 logs.push(args.join(' '))
162 }
163
164 try {
165 showHelp(mockCommands)
166
167 assert.ok(logs.some((log) => log.includes('greet - Test command')))
168 assert.ok(logs.some((log) => log.includes('blueprints - Test command')))
169 assert.ok(
170 logs.some((log) => log.includes('blueprints/inspect - Test command')),
171 )
172 assert.ok(
173 logs.some((log) => log.includes('functions/inspect - Test command')),
174 )
175
176 const greetLog = logs.find((log) => log.includes('greet'))
177 const blueprintsLog = logs.find((log) => log.includes('blueprints'))
178 const inspectLog = logs.find((log) =>
179 log.includes('blueprints/inspect'),
180 )
181
182 assert.ok(greetLog)
183 assert.ok(blueprintsLog)
184 assert.ok(inspectLog)
185
186 // root commands should have no indentation
187 assert.strictEqual(greetLog?.startsWith('greet'), true)
188 assert.strictEqual(blueprintsLog?.startsWith('blueprints'), true)
189
190 // nested commands should have indentation
191 assert.strictEqual(inspectLog?.startsWith(' blueprints/inspect'), true)
192 } finally {
193 console.log = originalLog
194 }
195 })
196 })
197
198 describe('createCLI', () => {
199 test('should load commands from directory', async () => {
200 const commands = await createCLI({
201 commandsDir: 'examples/rt-cli/commands',
202 baseDir: __dirname,
203 })
204
205 assert.ok(Array.isArray(commands))
206 assert.ok(commands.length > 0)
207
208 for (const cmd of commands) {
209 assert.ok(cmd.path)
210 assert.ok(cmd.command.config)
211 assert.ok(cmd.command.handler)
212 assert.ok(typeof cmd.command.config.description === 'string')
213 }
214 })
215
216 test('should handle non-existent directory gracefully', async () => {
217 const commands = await createCLI({
218 commandsDir: 'nonexistent',
219 baseDir: __dirname,
220 })
221
222 assert.deepStrictEqual(commands, [])
223 })
224 })
225
226 describe('runCLI', () => {
227 test('should show help when no command provided', async () => {
228 const logs: string[] = []
229 const originalLog = console.log
230 console.log = (...args: string[]) => {
231 logs.push(args.join(' '))
232 }
233
234 try {
235 await runCLI(mockCommands, [])
236
237 assert.ok(logs.some((log) => log.includes('Available commands:')))
238 assert.ok(logs.some((log) => log.includes('greet - Test command')))
239 } finally {
240 console.log = originalLog
241 }
242 })
243
244 test('should execute valid command', async () => {
245 let executed = false
246 const testCommand: Command = {
247 ...mockCommand,
248 handler: async () => {
249 executed = true
250 },
251 }
252
253 const testCommands: CommandPath[] = [
254 {
255 path: 'test',
256 command: testCommand,
257 },
258 ]
259
260 await runCLI(testCommands, ['test'])
261 assert.strictEqual(executed, true)
262 })
263
264 test('should show subcommands for directory paths', async () => {
265 // commands where 'blueprints' is not a command, just a directory
266 const directoryCommands: CommandPath[] = [
267 { path: 'greet', command: mockCommand },
268 { path: 'blueprints/inspect', command: mockCommand },
269 { path: 'blueprints/create', command: mockCommand },
270 ]
271
272 const commandPath = 'blueprints'
273 const subCommands = directoryCommands.filter((cmd) =>
274 cmd.path.startsWith(`${commandPath}/`),
275 )
276
277 assert.strictEqual(subCommands.length, 2)
278 assert.ok(subCommands.some((cmd) => cmd.path === 'blueprints/inspect'))
279 assert.ok(subCommands.some((cmd) => cmd.path === 'blueprints/create'))
280 })
281
282 test('should throw error for invalid command', async () => {
283 // 'nonexistent' is not a command and not a directory
284 const testCommands: CommandPath[] = [
285 { path: 'greet', command: mockCommand },
286 ]
287
288 try {
289 await runCLI(testCommands, ['nonexistent'])
290 assert.fail('Expected error to be thrown')
291 } catch (error) {
292 assert.ok(error instanceof Error)
293 assert.ok(error.message.includes('Command not found: nonexistent'))
294 }
295 })
296
297 test('should throw error when no commands available', async () => {
298 await assert.rejects(async () => {
299 await runCLI([], ['greet'])
300 }, /No commands available/)
301 })
302 })
303
304 describe('edge cases', () => {
305 test('should handle commands with same name in different directories', () => {
306 const duplicateCommands: CommandPath[] = [
307 { path: 'inspect', command: mockCommand },
308 { path: 'blueprints/inspect', command: mockCommand },
309 { path: 'functions/inspect', command: mockCommand },
310 ]
311
312 // should find the longest matching path
313 const result = parseArgs(duplicateCommands, ['blueprints', 'inspect'])
314 assert.deepStrictEqual(result.command, ['blueprints', 'inspect'])
315 assert.ok(result.target)
316 })
317
318 test('should handle flags with various types', () => {
319 const result = parseArgs(mockCommands, [
320 'greet',
321 '--string=hello',
322 '--number=42',
323 '--boolean',
324 ])
325 assert.deepStrictEqual(result.flags, {
326 string: 'hello',
327 number: 42,
328 boolean: true,
329 })
330 })
331
332 test('should handle short flags', () => {
333 const result = parseArgs(mockCommands, ['greet', '-v', '-n', 'John'])
334 assert.deepStrictEqual(result.flags, { v: true, n: 'John' })
335 assert.deepStrictEqual(result.args, {})
336 })
337 })
338})