smol 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e5135b67f9f85d2de94e50d6909e98499d47405a2123ce3a2a980f350c7b70ec
4
+ data.tar.gz: 2c00bffd406225c264c9599f7f0629648e1de6a6e7596e930267a19c8ff178e5
5
+ SHA512:
6
+ metadata.gz: a4057e86b35110d3dbb88d132d34d40cd00c9678107ff633587d9cdc27e336dce92c505eabca2522a09a3d8eeff4fd650c9e16f208b067d758f1e60ea2486b16
7
+ data.tar.gz: ca4895857b5729af92894a9d097db534fa13fd10c691ebd71d71d416e575040d3296a7fd1db229f5cfca91501265f2555a628618a263e7f4df401ee7efc29eb0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2026-01-14
4
+
5
+ - Initial extraction
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Josh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,697 @@
1
+ # Smol
2
+
3
+ A dependency-free CLI and REPL framework for Ruby. Define commands, run health checks, manage configuration from environment variables.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ gem install smol
9
+ ```
10
+
11
+ Or in your Gemfile:
12
+
13
+ ```ruby
14
+ gem "smol"
15
+ ```
16
+
17
+ Or inline for single-file scripts:
18
+
19
+ ```ruby
20
+ require "bundler/inline"
21
+
22
+ gemfile do
23
+ gem "smol"
24
+ end
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ ```ruby
30
+ #!/usr/bin/env ruby
31
+ require "bundler/inline"
32
+
33
+ gemfile do
34
+ gem "smol"
35
+ end
36
+
37
+ module MyCLI
38
+ class App < Smol::App
39
+ banner "mycli v1.0"
40
+
41
+ config.setting :database, default: "production", desc: "database to use"
42
+ config.setting :verbose, default: false, type: :boolean
43
+ end
44
+
45
+ module Commands
46
+ class Greet < Smol::Command
47
+ desc "say hello"
48
+ args :name
49
+ aliases :g, :hello
50
+
51
+ def call(name)
52
+ info "hello, #{name}!"
53
+ end
54
+ end
55
+
56
+ class Status < Smol::Command
57
+ desc "run health checks"
58
+
59
+ def call
60
+ all_passed = run_checks(Checks::Database)
61
+ checks_passed?(all_passed)
62
+ end
63
+ end
64
+ end
65
+
66
+ module Checks
67
+ class Database < Smol::Check
68
+ def call
69
+ pass "connected to #{config[:database]}"
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ Smol::CLI.new(MyCLI::App, prompt: "mycli").run(ARGV)
76
+ ```
77
+
78
+ ```bash
79
+ ./mycli.rb # starts REPL
80
+ ./mycli.rb greet world # runs single command
81
+ ./mycli.rb help # shows available commands
82
+ ```
83
+
84
+ ## App
85
+
86
+ The root of your CLI. Holds configuration and registered commands.
87
+
88
+ ```ruby
89
+ module MyCLI
90
+ class App < Smol::App
91
+ banner "mycli v1.0"
92
+
93
+ config.setting :host, default: "localhost"
94
+ config.setting :port, default: 3000, type: :integer
95
+ config.setting :debug, default: false, type: :boolean, desc: "enable debug"
96
+ end
97
+ end
98
+ ```
99
+
100
+ ### Auto-registration
101
+
102
+ Commands and checks defined under your app's namespace register automatically:
103
+
104
+ ```ruby
105
+ module MyCLI
106
+ class App < Smol::App
107
+ banner "mycli"
108
+ end
109
+
110
+ module Commands
111
+ class Deploy < Smol::Command # auto-registers to MyCLI::App
112
+ desc "deploy the app"
113
+ def call
114
+ info "deploying..."
115
+ end
116
+ end
117
+ end
118
+ end
119
+ ```
120
+
121
+ The framework walks up the namespace hierarchy looking for an `App` class. Works with any nesting depth.
122
+
123
+ ### Explicit registration
124
+
125
+ For control over command order in help output, register commands explicitly:
126
+
127
+ ```ruby
128
+ module MyCLI
129
+ class App < Smol::App
130
+ banner "mycli"
131
+
132
+ register Commands::Status # appears first in help
133
+ register Commands::Deploy # appears second
134
+ register Commands::Logs # appears third
135
+ end
136
+ end
137
+ ```
138
+
139
+ Once you call `register`, auto-registration is disabled for that app. Commands appear in help in the order you register them.
140
+
141
+ Use explicit registration when:
142
+
143
+ - Help output order matters,
144
+ - you want to control which commands are exposed,
145
+ - or you're building a larger app where implicit behavior feels too magical.
146
+
147
+ ### Mode control
148
+
149
+ Enable or disable CLI and REPL modes:
150
+
151
+ ```ruby
152
+ class App < Smol::App
153
+ cli false # disable CLI mode (commands via arguments)
154
+ repl false # disable REPL mode (interactive shell)
155
+ end
156
+ ```
157
+
158
+ Both default to `true`. Disabling CLI means users must run interactively. Disabling REPL means users must pass commands as arguments.
159
+
160
+ ### Boot display
161
+
162
+ Control what REPL shows on startup:
163
+
164
+ ```ruby
165
+ class App < Smol::App
166
+ boot :help # show full command list (default)
167
+ boot :minimal # show banner and hint only
168
+ boot :none # show nothing, just the prompt
169
+ end
170
+ ```
171
+
172
+ ### History file
173
+
174
+ Command history saves to `~/.smol_{prompt}_history` by default. Override it:
175
+
176
+ ```ruby
177
+ class App < Smol::App
178
+ history_file "~/.myapp_history"
179
+ end
180
+ ```
181
+
182
+ ### App methods
183
+
184
+ | Method | Purpose |
185
+ |--------|---------|
186
+ | `banner` | text shown at top of help |
187
+ | `cli` | enable/disable CLI mode |
188
+ | `repl` | enable/disable REPL mode |
189
+ | `boot` | REPL startup display (:help, :minimal, :none) |
190
+ | `history_file` | path to command history file |
191
+ | `config` | access the Config object |
192
+ | `commands` | array of registered command classes |
193
+ | `checks` | array of registered check classes |
194
+ | `mount` | mount a sub-app at a prefix |
195
+ | `find_command(name)` | look up command by name or alias |
196
+ | `register` | explicitly register a command class |
197
+
198
+ ## Commands
199
+
200
+ Commands define the actions users run. Subclass `Smol::Command` and implement `call`.
201
+
202
+ ```ruby
203
+ class Deploy < Smol::Command
204
+ title "deploy to production"
205
+ explain "pushes code, runs migrations, restarts servers"
206
+ desc "deploy the app"
207
+ args :environment
208
+ aliases :d, :push
209
+
210
+ def call(environment)
211
+ info "deploying to #{environment}..."
212
+ done
213
+ end
214
+ end
215
+ ```
216
+
217
+ ### Class-level DSL
218
+
219
+ | Method | Purpose |
220
+ |--------|---------|
221
+ | `title` | heading shown when command runs |
222
+ | `explain` | longer description shown below title |
223
+ | `desc` | one-line description for help listing |
224
+ | `args` | positional arguments (required) |
225
+ | `option` | named arguments with flags |
226
+ | `aliases` | alternative names for the command |
227
+ | `command_name` | override the derived command name |
228
+ | `group` | group related commands in help |
229
+ | `before_action` | method to run before `call` |
230
+ | `after_action` | method to run after `call` |
231
+ | `rescue_from` | handle specific exception types |
232
+
233
+ ### Instance methods
234
+
235
+ Commands include `Smol::Output` and `Smol::Input`. Additional helpers:
236
+
237
+ | Method | Purpose |
238
+ |--------|---------|
239
+ | `config` | access app configuration |
240
+ | `app` | the app class |
241
+ | `run_checks(*classes, args: [])` | run health checks |
242
+ | `checks_passed?(result, pass_hint:, fail_hint:)` | report check results |
243
+ | `checking(name)` | announce you're checking something |
244
+ | `dropping(target)` | announce you're removing something |
245
+ | `done(hint = nil)` | announce completion |
246
+
247
+ ### Positional arguments
248
+
249
+ Define required arguments with `args`:
250
+
251
+ ```ruby
252
+ class Greet < Smol::Command
253
+ args :name, :greeting
254
+
255
+ def call(name, greeting)
256
+ info "#{greeting}, #{name}!"
257
+ end
258
+ end
259
+ ```
260
+
261
+ ```bash
262
+ ./mycli.rb greet alice hello # "hello, alice!"
263
+ ```
264
+
265
+ ### Named options
266
+
267
+ Define optional flags with `option`:
268
+
269
+ ```ruby
270
+ class Deploy < Smol::Command
271
+ args :target
272
+ option :env, short: :e, default: "staging", desc: "target environment"
273
+ option :force, short: :f, type: :boolean, default: false
274
+ option :timeout, type: :integer, default: 30
275
+
276
+ def call(target, env:, force:, timeout:)
277
+ info "deploying #{target} to #{env}"
278
+ end
279
+ end
280
+ ```
281
+
282
+ ```bash
283
+ ./mycli.rb deploy app --env=production
284
+ ./mycli.rb deploy app -e production --force
285
+ ./mycli.rb deploy app --timeout=60
286
+ ```
287
+
288
+ Supported types: `:string` (default), `:integer`, `:boolean`.
289
+
290
+ ### Command groups
291
+
292
+ Organize commands in help output:
293
+
294
+ ```ruby
295
+ class Users::List < Smol::Command
296
+ group "users"
297
+ desc "list all users"
298
+ end
299
+
300
+ class Users::Create < Smol::Command
301
+ group "users"
302
+ desc "create a user"
303
+ end
304
+ ```
305
+
306
+ Help displays grouped commands under their group heading.
307
+
308
+ ### Callbacks
309
+
310
+ Run methods before or after `call`:
311
+
312
+ ```ruby
313
+ class Deploy < Smol::Command
314
+ before_action :check_auth
315
+ before_action :validate_env
316
+ after_action :notify_team
317
+
318
+ def call(env)
319
+ info "deploying to #{env}"
320
+ true
321
+ end
322
+
323
+ private
324
+
325
+ def check_auth
326
+ return false unless authenticated? # halts if false
327
+ end
328
+
329
+ def validate_env(env)
330
+ failure "invalid env" unless %w[staging production].include?(env)
331
+ end
332
+
333
+ def notify_team(env, result:)
334
+ info "deploy #{result ? 'succeeded' : 'failed'}"
335
+ end
336
+ end
337
+ ```
338
+
339
+ - Before actions receive the same arguments as `call`
340
+ - Return `false` to halt execution
341
+ - After actions receive arguments plus `result:` with the return value
342
+
343
+ ### Error handling
344
+
345
+ Handle exceptions without crashing:
346
+
347
+ ```ruby
348
+ class Deploy < Smol::Command
349
+ rescue_from ConnectionError do |e|
350
+ failure "connection failed: #{e.message}"
351
+ end
352
+
353
+ rescue_from ValidationError, with: :handle_validation
354
+
355
+ def call
356
+ # might raise
357
+ end
358
+
359
+ private
360
+
361
+ def handle_validation(error)
362
+ warning "invalid: #{error.message}"
363
+ end
364
+ end
365
+ ```
366
+
367
+ Unhandled exceptions propagate normally.
368
+
369
+ ### Calling other commands
370
+
371
+ Commands are just Ruby classes. Call them directly:
372
+
373
+ ```ruby
374
+ class Deploy < Smol::Command
375
+ def call(env)
376
+ Commands::Preflight.new.call
377
+ info "deploying to #{env}..."
378
+ end
379
+ end
380
+ ```
381
+
382
+ ## Checks
383
+
384
+ Health checks that return pass or fail. Subclass `Smol::Check` and implement `call`.
385
+
386
+ ```ruby
387
+ class DiskSpace < Smol::Check
388
+ def call
389
+ available = check_disk_space_gb
390
+ if available > 10
391
+ pass "#{available}GB free"
392
+ else
393
+ fail "only #{available}GB free"
394
+ end
395
+ end
396
+ end
397
+ ```
398
+
399
+ ### Running checks
400
+
401
+ From a command:
402
+
403
+ ```ruby
404
+ def call
405
+ all_passed = run_checks(DiskSpace, DatabaseConnection, RedisConnection)
406
+ checks_passed?(all_passed,
407
+ pass_hint: "ready to deploy",
408
+ fail_hint: "fix issues first"
409
+ )
410
+ end
411
+ ```
412
+
413
+ ### Checks with arguments
414
+
415
+ ```ruby
416
+ class IndexExists < Smol::Check
417
+ def initialize(index_name)
418
+ @index_name = index_name
419
+ end
420
+
421
+ def call
422
+ # check @index_name exists
423
+ end
424
+ end
425
+
426
+ # in a command:
427
+ run_checks(IndexExists, args: ["users_email_idx"])
428
+ ```
429
+
430
+ ### Check methods
431
+
432
+ | Method | Purpose |
433
+ |--------|---------|
434
+ | `pass(message)` | return a passing result |
435
+ | `fail(message)` | return a failing result |
436
+ | `config` | access app configuration |
437
+
438
+ ## Configuration
439
+
440
+ Define settings on your app:
441
+
442
+ ```ruby
443
+ config.setting :database, default: "production"
444
+ config.setting :port, default: 3000, type: :integer
445
+ config.setting :verbose, default: false, type: :boolean
446
+ config.setting :timeout, default: 30, type: :integer, desc: "request timeout"
447
+ ```
448
+
449
+ ### Reading values
450
+
451
+ Settings read from environment variables first (uppercased key), then fall back to defaults:
452
+
453
+ ```bash
454
+ PORT=8080 ./mycli.rb # config[:port] => 8080
455
+ ```
456
+
457
+ ```ruby
458
+ def call
459
+ db = config[:database]
460
+ port = config[:port]
461
+ end
462
+ ```
463
+
464
+ ### Setting values at runtime
465
+
466
+ ```ruby
467
+ config.set(:database, "staging")
468
+ ```
469
+
470
+ Or via CLI:
471
+
472
+ ```bash
473
+ ./mycli.rb config:set database staging
474
+ ```
475
+
476
+ ### Viewing configuration
477
+
478
+ ```bash
479
+ ./mycli.rb config
480
+ ```
481
+
482
+ Or in REPL:
483
+
484
+ ```
485
+ mycli> config
486
+ ```
487
+
488
+ ## Output
489
+
490
+ All output goes through `Smol::Output`. Available in commands:
491
+
492
+ | Method | Purpose |
493
+ |--------|---------|
494
+ | `info(text)` | plain text |
495
+ | `success(text)` | green, bold |
496
+ | `failure(text)` | red, bold |
497
+ | `warning(text)` | yellow |
498
+ | `hint(text)` | dim |
499
+ | `header(text)` | bold |
500
+ | `desc(text)` | dim |
501
+ | `banner(text)` | red |
502
+ | `label(text)` | yellow |
503
+ | `nl` | blank line |
504
+ | `verbose(text)` | only when `VERBOSE=1` |
505
+ | `debug(text)` | only when `DEBUG=1` |
506
+ | `check_result(name, result)` | formatted pass/fail |
507
+ | `table(rows, headers:, indent:)` | formatted table |
508
+
509
+ ### Tables
510
+
511
+ ```ruby
512
+ def call
513
+ rows = [
514
+ ["alice", "admin", "active"],
515
+ ["bob", "user", "pending"]
516
+ ]
517
+ table(rows, headers: %w[name role status])
518
+ end
519
+ ```
520
+
521
+ ```
522
+ name role status
523
+ --------------------
524
+ alice admin active
525
+ bob user pending
526
+ ```
527
+
528
+ ### Verbose and debug modes
529
+
530
+ ```ruby
531
+ def call
532
+ verbose "extra detail" # only with VERBOSE=1
533
+ debug "internal state" # only with DEBUG=1
534
+ end
535
+ ```
536
+
537
+ ```bash
538
+ VERBOSE=1 ./mycli.rb command
539
+ DEBUG=1 ./mycli.rb command
540
+ ```
541
+
542
+ Or programmatically:
543
+
544
+ ```ruby
545
+ Smol.verbose = true
546
+ Smol.debug = true
547
+ ```
548
+
549
+ ### Redirecting output
550
+
551
+ For testing:
552
+
553
+ ```ruby
554
+ Smol.output = StringIO.new
555
+ Smol.input = StringIO.new("y\n")
556
+ ```
557
+
558
+ ### Logger
559
+
560
+ Standard Ruby logger for internal debugging:
561
+
562
+ ```ruby
563
+ Smol.logger.level = Logger::DEBUG
564
+ Smol.logger.debug "something"
565
+ ```
566
+
567
+ ## Input
568
+
569
+ Interactive prompts. Available in commands via `Smol::Input`:
570
+
571
+ ```ruby
572
+ def call
573
+ name = ask("project name?")
574
+ port = ask("port?", default: "3000")
575
+
576
+ if confirm("create database?", default: true)
577
+ # do it
578
+ end
579
+
580
+ env = choose("environment:", %w[dev staging prod], default: 1)
581
+ end
582
+ ```
583
+
584
+ | Method | Purpose |
585
+ |--------|---------|
586
+ | `ask(question, default:)` | text input |
587
+ | `confirm(question, default:)` | yes/no |
588
+ | `choose(question, choices, default:)` | select from list |
589
+
590
+ ## Colors
591
+
592
+ Colors use a refinement. Used internally by output methods. For custom use:
593
+
594
+ ```ruby
595
+ using Smol::Colors
596
+
597
+ puts "success".green
598
+ puts "error".red
599
+ puts "warning".yellow
600
+ puts "heading".bold
601
+ puts "muted".dim
602
+ ```
603
+
604
+ ## Running
605
+
606
+ ### CLI mode
607
+
608
+ ```bash
609
+ ./mycli.rb command arg1 arg2
610
+ ```
611
+
612
+ Exit codes:
613
+
614
+ - `0` if command returns truthy
615
+ - `1` if command returns `false` or raises
616
+
617
+ ### REPL mode
618
+
619
+ ```bash
620
+ ./mycli.rb # no arguments
621
+ ```
622
+
623
+ Built-in commands:
624
+
625
+ - `help` / `h` / `?` — list commands
626
+ - `config` / `c` — show configuration
627
+ - `config:set <key> <value>` — update config
628
+ - `exit` / `quit` / `q` — exit
629
+
630
+ Includes readline with history and tab completion. History saves to `~/.smol_{prompt}_history` by default. Configure via `history_file` in your App class.
631
+
632
+ ## Sub-apps
633
+
634
+ Mount other apps under a prefix:
635
+
636
+ ```ruby
637
+ module Admin
638
+ class App < Smol::App
639
+ banner "admin tools"
640
+ end
641
+
642
+ module Commands
643
+ class Users < Smol::Command
644
+ desc "manage users"
645
+ def call
646
+ info "listing users..."
647
+ end
648
+ end
649
+ end
650
+ end
651
+
652
+ module MyCLI
653
+ class App < Smol::App
654
+ banner "mycli"
655
+ mount Admin::App, as: "admin"
656
+ end
657
+ end
658
+ ```
659
+
660
+ CLI access with colon syntax:
661
+
662
+ ```bash
663
+ ./mycli.rb admin:users
664
+ ```
665
+
666
+ REPL access by entering the sub-app:
667
+
668
+ ```
669
+ mycli> admin
670
+ mycli:admin> users
671
+ mycli:admin> back
672
+ mycli>
673
+ ```
674
+
675
+ ## Project structure
676
+
677
+ For larger apps:
678
+
679
+ ```
680
+ my_cli/
681
+ lib/
682
+ my_cli/
683
+ app.rb # MyCLI::App
684
+ commands/
685
+ deploy.rb # MyCLI::Commands::Deploy
686
+ status.rb # MyCLI::Commands::Status
687
+ checks/
688
+ database.rb # MyCLI::Checks::Database
689
+ bin/
690
+ mycli # CLI entrypoint
691
+ ```
692
+
693
+ Commands and checks auto-register based on namespace. No manual wiring needed unless you want explicit control over ordering.
694
+
695
+ ## License
696
+
697
+ MIT