| 1 | import { Effect, Option } from "effect" |
| 2 | import { Argument, Command, Flag } from "effect/cli" |
| 3 | import type { TestActions } from "../services/TestActions.ts" |
| 4 | import { logAction } from "../services/TestActions.ts" |
| 5 | |
| 6 | // Deeply nested admin commands |
| 7 | const usersList = Command.make("list", { |
| 8 | // Optional option with default |
| 9 | format: Flag.String("format").pipe( |
| 10 | Flag.withDescription("Output format (json, table, csv)"), |
| 11 | Flag.withDefault("table") |
| 12 | ), |
| 13 | // Boolean flag |
| 14 | active: Flag.Boolean("active").pipe( |
| 15 | Flag.withDescription("Show only active users"), |
| 16 | Flag.withDefault(false) |
| 17 | ), |
| 18 | // Option with both short and long aliases |
| 19 | verbose: Flag.Boolean("verbose").pipe( |
| 20 | Flag.withAlias("v"), |
| 21 | Flag.withDescription("Show detailed information"), |
| 22 | Flag.withDefault(false) |
| 23 | ) |
| 24 | }, (config) => |
| 25 | logAction("users list", { |
| 26 | format: config.format, |
| 27 | active: config.active, |
| 28 | verbose: config.verbose |
| 29 | })).pipe( |
| 30 | Command.withDescription("List all users in the system") |
| 31 | ) |
| 32 | |
| 33 | const usersCreate = Command.make("create", { |
| 34 | // Required positional argument |
| 35 | username: Argument.String("username").pipe( |
| 36 | Argument.withDescription("Username for the new user") |
| 37 | ), |
| 38 | // Optional positional argument |
| 39 | email: Argument.String("email").pipe( |
| 40 | Argument.withDescription("Email address (optional)"), |
| 41 | Argument.optional |
| 42 | ), |
| 43 | // Required option |
| 44 | role: Flag.String("role").pipe( |
| 45 | Flag.withDescription("User role (admin, user, guest)") |
| 46 | ), |
| 47 | // Boolean with explicit value support |
| 48 | notify: Flag.Boolean("notify").pipe( |
| 49 | Flag.withAlias("n"), |
| 50 | Flag.withDescription("Send notification email"), |
| 51 | Flag.withDefault(false) |
| 52 | ) |
| 53 | }, (config) => |
| 54 | logAction("users create", { |
| 55 | username: config.username, |
| 56 | email: config.email, |
| 57 | role: config.role, |
| 58 | notify: config.notify |
| 59 | })).pipe( |
| 60 | Command.withDescription("Create a new user account") |
| 61 | ) |
| 62 | |
| 63 | const users = Command.make("users").pipe( |
| 64 | Command.withDescription("User management commands"), |
| 65 | Command.withSubcommands([usersList, usersCreate]) |
| 66 | ) |
| 67 | |
| 68 | const configSet = Command.make("set", { |
| 69 | // Variadic positional arguments |
| 70 | pairs: Argument.String("key=value").pipe( |
| 71 | Argument.withDescription("Configuration key-value pairs"), |
| 72 | Argument.variadic({ min: 1 }) |
| 73 | ), |
| 74 | // File path option |
| 75 | file: Flag.File("config-file").pipe( |
| 76 | Flag.withAlias("f"), |
| 77 | Flag.withDescription("Write to specific config file"), |
| 78 | Flag.optional |
| 79 | ) |
| 80 | }, (config) => |
| 81 | logAction("config set", { |
| 82 | pairs: config.pairs, |
| 83 | file: config.file |
| 84 | })).pipe( |
| 85 | Command.withDescription("Set configuration values") |
| 86 | ) |
| 87 | |
| 88 | const configGet = Command.make("get", { |
| 89 | // Single required positional |
| 90 | key: Argument.String("key").pipe( |
| 91 | Argument.withDescription("Configuration key to retrieve") |
| 92 | ), |
| 93 | // Options with different types |
| 94 | source: Flag.String("source").pipe( |
| 95 | Flag.withDescription("Configuration source (local, global, system)"), |
| 96 | Flag.withDefault("local") |
| 97 | ) |
| 98 | }, (config) => |
| 99 | logAction("config get", { |
| 100 | key: config.key, |
| 101 | source: config.source |
| 102 | })).pipe( |
| 103 | Command.withDescription("Get configuration value") |
| 104 | ) |
| 105 | |
| 106 | const config = Command.make("config").pipe( |
| 107 | Command.withSharedFlags({ |
| 108 | // Parent command options shared with config subcommands |
| 109 | profile: Flag.String("profile").pipe( |
| 110 | Flag.withAlias("p"), |
| 111 | Flag.withDescription("Configuration profile to use"), |
| 112 | Flag.optional |
| 113 | ) |
| 114 | }), |
| 115 | Command.withDescription("Manage application configuration"), |
| 116 | Command.withSubcommands([configSet, configGet]) |
| 117 | ) |
| 118 | |
| 119 | const admin = Command.make("admin").pipe( |
| 120 | Command.withSharedFlags({ |
| 121 | // Boolean that can be set to false explicitly |
| 122 | sudo: Flag.Boolean("sudo").pipe( |
| 123 | Flag.withDescription("Run with elevated privileges"), |
| 124 | Flag.withDefault(false) |
| 125 | ) |
| 126 | }), |
| 127 | Command.withDescription("Administrative commands"), |
| 128 | Command.withSubcommands([users, config]) |
| 129 | ) |
| 130 | |
| 131 | // File operations commands |
| 132 | const copy = Command.make("copy", { |
| 133 | // Multiple required positional arguments (do not require actual filesystem presence in tests) |
| 134 | source: Argument.File("source", { mustExist: false }).pipe( |
| 135 | Argument.withDescription("Source file or directory") |
| 136 | ), |
| 137 | destination: Argument.File("destination", { mustExist: false }).pipe( |
| 138 | Argument.withDescription("Destination path") |
| 139 | ), |
| 140 | // Boolean flags with short aliases |
| 141 | recursive: Flag.Boolean("recursive").pipe( |
| 142 | Flag.withAlias("r"), |
| 143 | Flag.withDescription("Copy directories recursively"), |
| 144 | Flag.withDefault(false) |
| 145 | ), |
| 146 | force: Flag.Boolean("force").pipe( |
| 147 | Flag.withAlias("f"), |
| 148 | Flag.withDescription("Overwrite existing files"), |
| 149 | Flag.withDefault(false) |
| 150 | ), |
| 151 | // Integer option |
| 152 | buffer: Flag.Int("buffer-size").pipe( |
| 153 | Flag.withDescription("Buffer size in KB"), |
| 154 | Flag.withDefault(64) |
| 155 | ) |
| 156 | }, (config) => |
| 157 | logAction("copy", { |
| 158 | source: config.source, |
| 159 | destination: config.destination, |
| 160 | recursive: config.recursive, |
| 161 | force: config.force, |
| 162 | bufferSize: config.buffer |
| 163 | })).pipe( |
| 164 | Command.withDescription("Copy files or directories") |
| 165 | ) |
| 166 | |
| 167 | const move = Command.make("move", { |
| 168 | // Variadic sources with at least 2 items |
| 169 | paths: Argument.String("paths").pipe( |
| 170 | Argument.withDescription("Source path(s) and destination"), |
| 171 | Argument.variadic({ min: 2 }) |
| 172 | ), |
| 173 | // Options |
| 174 | interactive: Flag.Boolean("interactive").pipe( |
| 175 | Flag.withAlias("i"), |
| 176 | Flag.withDescription("Prompt before overwrite"), |
| 177 | Flag.withDefault(false) |
| 178 | ) |
| 179 | }, (config) => |
| 180 | logAction("move", { |
| 181 | paths: config.paths, |
| 182 | interactive: config.interactive |
| 183 | })).pipe( |
| 184 | Command.withDescription("Move or rename files") |
| 185 | ) |
| 186 | |
| 187 | const remove = Command.make("remove", { |
| 188 | // Variadic with no upper limit |
| 189 | files: Argument.String("files").pipe( |
| 190 | Argument.withDescription("Files to remove"), |
| 191 | Argument.variadic({ min: 1 }) |
| 192 | ), |
| 193 | // Multiple boolean options |
| 194 | recursive: Flag.Boolean("recursive").pipe( |
| 195 | Flag.withAlias("r"), |
| 196 | Flag.withDescription("Remove directories and contents"), |
| 197 | Flag.withDefault(false) |
| 198 | ), |
| 199 | force: Flag.Boolean("force").pipe( |
| 200 | Flag.withAlias("f"), |
| 201 | Flag.withDescription("Force removal without prompts"), |
| 202 | Flag.withDefault(false) |
| 203 | ), |
| 204 | verbose: Flag.Boolean("verbose").pipe( |
| 205 | Flag.withAlias("v"), |
| 206 | Flag.withDescription("Explain what is being done"), |
| 207 | Flag.withDefault(false) |
| 208 | ) |
| 209 | }, (config) => |
| 210 | logAction("remove", { |
| 211 | files: config.files, |
| 212 | recursive: config.recursive, |
| 213 | force: config.force, |
| 214 | verbose: config.verbose |
| 215 | })).pipe( |
| 216 | Command.withDescription("Remove files or directories") |
| 217 | ) |
| 218 | |
| 219 | // Build command for testing option aliases |
| 220 | const build = Command.make("build", { |
| 221 | output: Flag.String("output").pipe( |
| 222 | Flag.withAlias("o"), |
| 223 | Flag.withDescription("Output directory") |
| 224 | ), |
| 225 | verbose: Flag.Boolean("verbose").pipe( |
| 226 | Flag.withAlias("v"), |
| 227 | Flag.withDescription("Enable verbose output"), |
| 228 | Flag.withDefault(false) |
| 229 | ), |
| 230 | configFile: Flag.String("config-file").pipe( |
| 231 | Flag.withAlias("f"), |
| 232 | Flag.withDescription("Configuration file path"), |
| 233 | Flag.optional |
| 234 | ) |
| 235 | }, (config) => |
| 236 | logAction("build", { |
| 237 | output: config.output, |
| 238 | verbose: config.verbose, |
| 239 | config: Option.getOrElse(config.configFile, () => "none") |
| 240 | })).pipe( |
| 241 | Command.withDescription("Build the project") |
| 242 | ) |
| 243 | |
| 244 | // Git-style commands for testing subcommands and context sharing |
| 245 | const gitClone = Command.make("clone", { |
| 246 | repository: Argument.String("repository").pipe( |
| 247 | Argument.withDescription("Repository URL or path") |
| 248 | ), |
| 249 | branch: Flag.String("branch").pipe( |
| 250 | Flag.withDefault("main"), |
| 251 | Flag.withDescription("Branch to clone") |
| 252 | ) |
| 253 | }, (config) => |
| 254 | logAction("git clone", { |
| 255 | repository: config.repository, |
| 256 | branch: config.branch |
| 257 | })).pipe( |
| 258 | Command.withDescription("Clone a repository") |
| 259 | ) |
| 260 | |
| 261 | const gitAdd = Command.make("add", { |
| 262 | files: Argument.String("files").pipe( |
| 263 | Argument.withDescription("Files to add") |
| 264 | ), |
| 265 | update: Flag.Boolean("update").pipe( |
| 266 | Flag.withDescription("Update tracked files"), |
| 267 | Flag.withDefault(false) |
| 268 | ) |
| 269 | }, (config) => |
| 270 | logAction("git add", { |
| 271 | files: config.files, |
| 272 | update: config.update |
| 273 | })).pipe( |
| 274 | Command.withDescription("Add files to staging") |
| 275 | ) |
| 276 | |
| 277 | const gitStatus = Command.make("status", { |
| 278 | short: Flag.Boolean("short").pipe( |
| 279 | Flag.withDescription("Show short format"), |
| 280 | Flag.withDefault(false) |
| 281 | ) |
| 282 | }, (config) => |
| 283 | logAction("git status", { |
| 284 | short: config.short |
| 285 | })).pipe( |
| 286 | Command.withDescription("Show repository status") |
| 287 | ) |
| 288 | |
| 289 | const git = Command.make("git").pipe( |
| 290 | Command.withSharedFlags({ |
| 291 | verbose: Flag.Boolean("verbose").pipe( |
| 292 | Flag.withDescription("Enable verbose output"), |
| 293 | Flag.withDefault(false) |
| 294 | ) |
| 295 | }), |
| 296 | Command.withHandler((config) => |
| 297 | logAction("git", { |
| 298 | verbose: config.verbose |
| 299 | }) |
| 300 | ), |
| 301 | Command.withDescription("Git version control"), |
| 302 | Command.withSubcommands([gitClone, gitAdd, gitStatus]) |
| 303 | ) |
| 304 | |
| 305 | // Commands for testing error handling |
| 306 | const testRequired = Command.make("test-required", { |
| 307 | required: Flag.String("required").pipe( |
| 308 | Flag.withDescription("A required option for testing") |
| 309 | ) |
| 310 | }, (config) => |
| 311 | logAction("test-required", { |
| 312 | required: config.required |
| 313 | })).pipe( |
| 314 | Command.withDescription("Test command with required option") |
| 315 | ) |
| 316 | |
| 317 | const testFailing: Command.Command< |
| 318 | "test-failing", |
| 319 | { readonly input: string }, |
| 320 | {}, |
| 321 | string, |
| 322 | TestActions |
| 323 | > = Command.make("test-failing", { |
| 324 | input: Flag.String("input").pipe( |
| 325 | Flag.withDescription("Input that will cause handler to fail") |
| 326 | ) |
| 327 | }, (config) => |
| 328 | Effect.gen(function*() { |
| 329 | yield* logAction("test-failing", { input: config.input }) |
| 330 | return yield* Effect.fail("Handler error") |
| 331 | })).pipe( |
| 332 | Command.withDescription("Test command that always fails") |
| 333 | ) |
| 334 | |
| 335 | // Deploy command for testing complex nested structures |
| 336 | const deployCommand = Command.make("deploy", { |
| 337 | service: Argument.String("service").pipe( |
| 338 | Argument.withDescription("Service to deploy") |
| 339 | ), |
| 340 | environment: Argument.String("environment").pipe( |
| 341 | Argument.withDescription("Target environment") |
| 342 | ), |
| 343 | database: { |
| 344 | host: Flag.String("db-host").pipe( |
| 345 | Flag.withDescription("Database host") |
| 346 | ), |
| 347 | port: Flag.Int("db-port").pipe( |
| 348 | Flag.withDescription("Database port") |
| 349 | ) |
| 350 | }, |
| 351 | dryRun: Flag.Boolean("dry-run").pipe( |
| 352 | Flag.withDescription("Perform a dry run"), |
| 353 | Flag.withDefault(false) |
| 354 | ) |
| 355 | }, (config) => |
| 356 | logAction("deploy", { |
| 357 | service: config.service, |
| 358 | environment: config.environment, |
| 359 | database: config.database, |
| 360 | dryRun: config.dryRun |
| 361 | })).pipe( |
| 362 | Command.withDescription("Deploy a service") |
| 363 | ) |
| 364 | |
| 365 | const app = Command.make("app").pipe( |
| 366 | Command.withSharedFlags({ |
| 367 | env: Flag.String("env").pipe( |
| 368 | Flag.withDescription("Environment setting"), |
| 369 | Flag.optional |
| 370 | ) |
| 371 | }), |
| 372 | Command.withHandler((config) => |
| 373 | logAction("app", { |
| 374 | env: config.env |
| 375 | }) |
| 376 | ), |
| 377 | Command.withDescription("Application management"), |
| 378 | Command.withSubcommands([deployCommand]) |
| 379 | ) |
| 380 | |
| 381 | // Service command for nested context sharing tests |
| 382 | const serviceCommand = Command.make("service").pipe( |
| 383 | Command.withSharedFlags({ |
| 384 | name: Flag.String("name").pipe( |
| 385 | Flag.withDescription("Service name") |
| 386 | ) |
| 387 | }), |
| 388 | Command.withHandler((config) => |
| 389 | logAction("service", { |
| 390 | name: config.name |
| 391 | }) |
| 392 | ), |
| 393 | Command.withDescription("Service management"), |
| 394 | Command.withSubcommands([deployCommand]) |
| 395 | ) |
| 396 | |
| 397 | const appWithService = Command.make("app-nested").pipe( |
| 398 | Command.withSharedFlags({ |
| 399 | env: Flag.String("env").pipe( |
| 400 | Flag.withDescription("Environment setting") |
| 401 | ) |
| 402 | }), |
| 403 | Command.withHandler((config) => |
| 404 | logAction("app-nested", { |
| 405 | env: config.env |
| 406 | }) |
| 407 | ), |
| 408 | Command.withDescription("Application with nested services"), |
| 409 | Command.withSubcommands([serviceCommand]) |
| 410 | ) |
| 411 | |
| 412 | // Main command with global options |
| 413 | // Note: No handler on root command - running with no args should show help |
| 414 | export const ComprehensiveCli = Command.make("mycli").pipe( |
| 415 | Command.withSharedFlags({ |
| 416 | // Global options available to all subcommands |
| 417 | debug: Flag.Boolean("debug").pipe( |
| 418 | Flag.withAlias("d"), |
| 419 | Flag.withDescription("Enable debug logging"), |
| 420 | Flag.withDefault(false) |
| 421 | ), |
| 422 | config: Flag.File("config").pipe( |
| 423 | Flag.withAlias("c"), |
| 424 | Flag.withDescription("Path to configuration file"), |
| 425 | Flag.optional |
| 426 | ), |
| 427 | quiet: Flag.Boolean("quiet").pipe( |
| 428 | Flag.withAlias("q"), |
| 429 | Flag.withDescription("Suppress non-error output"), |
| 430 | Flag.withDefault(false) |
| 431 | ) |
| 432 | }), |
| 433 | Command.withDescription("A comprehensive CLI tool demonstrating all features"), |
| 434 | Command.withSubcommands([admin, copy, move, remove, build, git, testRequired, testFailing, app, appWithService]) |
| 435 | ) |
| 436 | |
| 437 | export const run = Command.runWith(ComprehensiveCli, { |
| 438 | version: "1.0.0" |
| 439 | }) |