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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +697 -0
- data/Rakefile +12 -0
- data/lib/smol/app.rb +122 -0
- data/lib/smol/app_lookup.rb +13 -0
- data/lib/smol/check.rb +70 -0
- data/lib/smol/check_result.rb +18 -0
- data/lib/smol/cli.rb +119 -0
- data/lib/smol/coercion.rb +18 -0
- data/lib/smol/colors.rb +27 -0
- data/lib/smol/command.rb +292 -0
- data/lib/smol/config.rb +48 -0
- data/lib/smol/config_display.rb +31 -0
- data/lib/smol/input.rb +60 -0
- data/lib/smol/output.rb +105 -0
- data/lib/smol/repl.rb +207 -0
- data/lib/smol/version.rb +3 -0
- data/lib/smol.rb +55 -0
- metadata +92 -0
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
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
|