1import { Effect, Option } from "effect"
2import { Argument, Command, Flag } from "effect/cli"
3import type { TestActions } from "../services/TestActions.ts"
4import { logAction } from "../services/TestActions.ts"
5
6// Deeply nested admin commands
7const 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
33const 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
63const users = Command.make("users").pipe(
64 Command.withDescription("User management commands"),
65 Command.withSubcommands([usersList, usersCreate])
66)
67
68const 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
88const 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
106const 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
119const 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
132const 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
167const 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
187const 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
220const 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
245const 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
261const 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
277const 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
289const 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
306const 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
317const 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
336const 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
365const 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
382const 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
397const 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
414export 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
437export const run = Command.runWith(ComprehensiveCli, {
438 version: "1.0.0"
439})