this repo has no description
at main 338 lines 10 kB view raw
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})