easy_caddy 0.1.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: 15359e9854098ff355b15635597ff9979952d9224835eb31594676492fbd64d9
4
+ data.tar.gz: b3f4b62942373f0554e0d0ff92671bccb8fe7efd8ffef16038f4248c251687bb
5
+ SHA512:
6
+ metadata.gz: 2cccf4fca5dbe0eb0148998507d67666b294dcc0233c34fd83b2789e6c18c44191981dc2391b9f319067bc183729078b2985994641f9247838134c3361efbdf2
7
+ data.tar.gz: d8c66f1dba6f6d32210c2a2cbfa910a1560668a7ea5e5a84cfe9e6371142036fcd0543035b488f9b415f9e1c6ac9c58b6ac1e17cc06006e6f71a9e8c057e45bf
data/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] — 2026-06-09
9
+
10
+ ### Added
11
+
12
+ - Initial public release.
13
+ - Thor CLI `ecaddy` with commands: `setup`, `run`, `ensure`, `up`, `down`,
14
+ `list`, `edit`, `logs`, `remove`, `reload`, `status`, `doctor`, `audit`.
15
+ - Single-source-of-truth path management via `EasyCaddy::Paths` — every
16
+ filesystem path derives from the `ECADDY_HOME` env var (defaults to
17
+ `~/.config/caddy`), keeping the tool fully redirectable for tests and
18
+ multi-user setups.
19
+ - YAML registry (`ecaddy.yml`) tracking each site by name, enabled state,
20
+ and source Caddyfile path.
21
+ - Conflict detection: `*.localhost` domain collisions and
22
+ `reverse_proxy localhost:PORT` port collisions across registered
23
+ fragments, plus TCP probing of upstream ports via `ecaddy doctor`.
24
+ - Automatic rewrite of relative `output file` log paths to absolute paths
25
+ on registration, so Caddy (running as a background service detached from
26
+ the project directory) can write log files correctly.
27
+ - One-shot machine bootstrap (`ecaddy setup`): Homebrew install of Caddy,
28
+ global Caddyfile scaffold, `caddy trust` for local-CA HTTPS, and
29
+ `brew services` start. Idempotent — safe to re-run.
30
+ - Foreground `ecaddy run --site NAME` mode: registers the fragment, traps
31
+ `SIGTERM`/`SIGINT`, and unregisters on exit — designed to drop into a
32
+ Procfile alongside the Rails server.
33
+
34
+ [0.1.0]: https://github.com/pniemczyk/easy_caddy/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pawel Niemczyk
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,333 @@
1
+ # ecaddy
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/easy_caddy.svg)](https://rubygems.org/gems/easy_caddy)
4
+ [![Docs](https://img.shields.io/badge/docs-pniemczyk.github.io%2Feasy__caddy-blue)](https://pniemczyk.github.io/easy_caddy/)
5
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
6
+ [![Changelog](https://img.shields.io/badge/changelog-CHANGELOG.md-orange)](CHANGELOG.md)
7
+
8
+ **[📖 Documentation](https://pniemczyk.github.io/easy_caddy/)**  |  **[GitHub](https://github.com/pniemczyk/easy_caddy)**  |  **[Changelog](CHANGELOG.md)**
9
+
10
+ One global [Caddy](https://caddyserver.com/) for all your local Rails projects.
11
+
12
+ Instead of fighting port conflicts from multiple Caddy processes, `ecaddy` manages a single shared Caddy instance. Each project keeps its own `Caddyfile` — `ecaddy` copies it in and out of the global config on demand.
13
+
14
+ ## How it works
15
+
16
+ ```
17
+ Browser
18
+
19
+
20
+ Caddy (~/.config/caddy/Caddyfile)
21
+ │ imports sites/*.caddy
22
+ ├── fishme.localhost → localhost:3104
23
+ ├── letly.localhost → localhost:3100
24
+ └── traiderb.localhost → localhost:3106
25
+ ```
26
+
27
+ Each Rails project has its own `Caddyfile`. When you start the project, `ecaddy` copies it into `~/.config/caddy/sites/<name>.caddy` and reloads the global Caddy. When you stop, it removes the fragment and reloads again.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ gem install easy_caddy
33
+ ecaddy setup
34
+ ```
35
+
36
+ `ecaddy setup` is a one-time bootstrap that:
37
+
38
+ 1. Installs Caddy via Homebrew if not already present
39
+ 2. Scaffolds `~/.config/caddy/{sites,disabled}/`
40
+ 3. Writes the global `Caddyfile` (with `import sites/*.caddy`)
41
+ 4. Symlinks it into `/opt/homebrew/etc/Caddyfile` so `brew services` picks it up
42
+ 5. Runs `caddy trust` to install the local CA in your system keychain (makes `https://*.localhost` green in browsers)
43
+ 6. Starts Caddy as a `brew services` background service
44
+
45
+ Run `ecaddy setup` again at any time — every step is idempotent.
46
+
47
+ ### The `--site` option
48
+
49
+ Every `ecaddy` command that registers or references a project uses a **site name** — a short identifier you choose, e.g. `fishme`. This name:
50
+
51
+ - Determines the fragment filename: `~/.config/caddy/sites/fishme.caddy`
52
+ - Is used by `up`, `down`, `edit`, `remove` to target the right project
53
+ - Should be unique across all your local projects
54
+
55
+ The name is **not** read from the Caddyfile — you always supply it explicitly with `--site fishme` (short: `-s fishme`). This keeps `ecaddy` compatible with any Caddyfile content.
56
+
57
+ ## Project setup
58
+
59
+ Each project needs two things: a `Caddyfile` and a Procfile line.
60
+
61
+ ### 1. Write your project Caddyfile
62
+
63
+ Put a `Caddyfile` in your project root. Write it however you need — `ecaddy` treats it as read-only source. One automatic transform is applied on copy: relative `output file` log paths are rewritten to absolute paths so Caddy (running as a background service with no relation to your project directory) can actually write the log files.
64
+
65
+ ```caddy
66
+ # Caddyfile (in your Rails project root)
67
+
68
+ fishme.localhost {
69
+ handle /vite-dev/* {
70
+ reverse_proxy localhost:3054
71
+ }
72
+
73
+ reverse_proxy localhost:3104
74
+ tls internal
75
+
76
+ log {
77
+ level INFO
78
+ output file log/caddy.log {
79
+ roll_size 2mb
80
+ roll_keep 5
81
+ roll_keep_for 48h
82
+ }
83
+ }
84
+ }
85
+
86
+ vite.fishme.localhost {
87
+ reverse_proxy localhost:3054
88
+ tls internal
89
+ }
90
+ ```
91
+
92
+ Pick unique ports across your projects. Common pattern:
93
+
94
+ | Project | App port | Vite port |
95
+ |----------|----------|-----------|
96
+ | fishme | 3104 | 3054 |
97
+ | letly | 3100 | 3050 |
98
+ | traiderb | 3106 | 3056 |
99
+
100
+ ### 2. Add a Procfile line
101
+
102
+ ```procfile
103
+ # Procfile.dev
104
+
105
+ web: bin/rails server -p 3104
106
+ js: yarn dev
107
+ caddy: ecaddy run --config ./Caddyfile --site fishme
108
+ ```
109
+
110
+ When foreman (or overmind) starts, `ecaddy run` copies your `Caddyfile` into the global config and reloads Caddy. When you press `Ctrl-C`, it removes the fragment and reloads again — the domain disappears cleanly.
111
+
112
+ ### 3. Allow `.localhost` in Rails
113
+
114
+ ```ruby
115
+ # config/environments/development.rb
116
+
117
+ config.hosts << /.*\.localhost/
118
+ ```
119
+
120
+ ### 4. Start your project
121
+
122
+ ```bash
123
+ bin/dev
124
+ ```
125
+
126
+ Visit `https://fishme.localhost` — done.
127
+
128
+ ## Commands
129
+
130
+ ### `ecaddy setup`
131
+
132
+ One-time machine bootstrap. Install Caddy, scaffold the global config, trust the local CA, start the brew service.
133
+
134
+ ```bash
135
+ ecaddy setup
136
+ ```
137
+
138
+ ---
139
+
140
+ ### `ecaddy run`
141
+
142
+ Register a project Caddyfile, block, and unregister on shutdown. Use in `Procfile.dev`.
143
+
144
+ ```bash
145
+ ecaddy run --config ./Caddyfile --site fishme
146
+ ecaddy run -c ./Caddyfile -s fishme
147
+ ```
148
+
149
+ On `SIGTERM` or `SIGINT`, the fragment is removed and Caddy is reloaded before the process exits.
150
+
151
+ Relative `output file` log paths in the Caddyfile are automatically rewritten to absolute paths (resolved from the directory of `--config`) before the fragment is installed.
152
+
153
+ ---
154
+
155
+ ### `ecaddy ensure`
156
+
157
+ One-shot variant of `run`. Copies the Caddyfile, reloads Caddy, exits immediately. The site stays registered until you run `ecaddy down` or `ecaddy remove`.
158
+
159
+ ```bash
160
+ ecaddy ensure --config ./Caddyfile --site fishme
161
+ ```
162
+
163
+ Useful in CI or shell scripts where you want Caddy configured but don't need a foreground process.
164
+
165
+ ---
166
+
167
+ ### `ecaddy list`
168
+
169
+ Show all registered sites.
170
+
171
+ ```bash
172
+ ecaddy list
173
+ ecaddy list --format json
174
+ ```
175
+
176
+ ```
177
+ ┌──────────┬────────┬──────────────────────────────────────────────┬────────────┬──────────────────────────┐
178
+ │ Name │ Status │ Domains │ Ports │ Source │
179
+ ├──────────┼────────┼──────────────────────────────────────────────┼────────────┼──────────────────────────┤
180
+ │ fishme │ up │ fishme.localhost, vite.fishme.localhost │ 3054, 3104 │ /projects/fishme/Caddyfile │
181
+ │ letly │ down │ letly.localhost, vite.letly.localhost │ 3050, 3100 │ /projects/letly/Caddyfile │
182
+ └──────────┴────────┴──────────────────────────────────────────────┴────────────┴──────────────────────────┘
183
+ ```
184
+
185
+ ---
186
+
187
+ ### `ecaddy up NAME` / `ecaddy down NAME`
188
+
189
+ Enable or disable a registered site without removing it.
190
+
191
+ ```bash
192
+ ecaddy down fishme # moves sites/fishme.caddy → disabled/fishme.caddy, reloads
193
+ ecaddy up fishme # moves disabled/fishme.caddy → sites/fishme.caddy, reloads
194
+ ```
195
+
196
+ ---
197
+
198
+ ### `ecaddy status`
199
+
200
+ Show global Caddy state and per-site health (whether the upstream app is actually running).
201
+
202
+ ```bash
203
+ ecaddy status
204
+ ```
205
+
206
+ ```
207
+ Caddy service: running
208
+ Config: /Users/you/.config/caddy/Caddyfile
209
+
210
+ fishme up
211
+ fragment: /Users/you/.config/caddy/sites/fishme.caddy
212
+ source: /projects/fishme/Caddyfile
213
+ letly up (app not running)
214
+ fragment: /Users/you/.config/caddy/sites/letly.caddy
215
+ source: /projects/letly/Caddyfile
216
+ ```
217
+
218
+ ---
219
+
220
+ ### `ecaddy doctor`
221
+
222
+ Scan all registered sites for port/domain conflicts and dead upstreams.
223
+
224
+ ```bash
225
+ ecaddy doctor
226
+ ```
227
+
228
+ Exits `0` if all clear or only INFO findings. Exits `1` on any BLOCK.
229
+
230
+ | Severity | Meaning |
231
+ |----------|---------|
232
+ | `BLOCK` | Two sites share a port or domain — one will fail |
233
+ | `WARN` | A port is bound by an unexpected process |
234
+ | `INFO` | Upstream not listening (app not started) |
235
+
236
+ ---
237
+
238
+ ### `ecaddy edit NAME`
239
+
240
+ Open a site's fragment in `$EDITOR`. Caddy is validated and reloaded after you save.
241
+
242
+ ```bash
243
+ ecaddy edit fishme
244
+ ```
245
+
246
+ This edits the copy in `~/.config/caddy/sites/fishme.caddy`, not your project source. Re-run `ecaddy run` (or `ecaddy ensure`) to sync from your project `Caddyfile` again.
247
+
248
+ ---
249
+
250
+ ### `ecaddy remove NAME`
251
+
252
+ Remove a site's fragment and registry entry entirely.
253
+
254
+ ```bash
255
+ ecaddy remove fishme
256
+ ecaddy remove fishme --force # skip confirmation
257
+ ```
258
+
259
+ ---
260
+
261
+ ### `ecaddy reload`
262
+
263
+ Validate the global config and reload Caddy.
264
+
265
+ ```bash
266
+ ecaddy reload
267
+ ```
268
+
269
+ ---
270
+
271
+ ### `ecaddy version`
272
+
273
+ ```bash
274
+ ecaddy version
275
+ # ecaddy 0.1.0
276
+ ```
277
+
278
+ ## Global config layout
279
+
280
+ ```
281
+ ~/.config/caddy/
282
+ Caddyfile # global root: { admin ... } + import sites/*.caddy
283
+ ecaddy.yml # registry: name → { enabled, source_path }
284
+ sites/
285
+ fishme.caddy # enabled fragments — loaded by Caddy
286
+ letly.caddy
287
+ disabled/
288
+ traiderb.caddy # disabled fragments — preserved, not loaded
289
+ ```
290
+
291
+ The global `Caddyfile` is also symlinked at `/opt/homebrew/etc/Caddyfile` so `brew services start caddy` picks it up automatically.
292
+
293
+ ## Conflict detection
294
+
295
+ Before registering any Caddyfile, `ecaddy` parses it and checks:
296
+
297
+ - **Domain collision** — same `*.localhost` domain already registered by another enabled site → BLOCK
298
+ - **Port collision** — same `reverse_proxy localhost:PORT` already in use by another site → BLOCK
299
+
300
+ These checks run on `ecaddy run`, `ecaddy ensure`, and `ecaddy up`. Run `ecaddy doctor` at any time for a full cross-site audit.
301
+
302
+ ## Environment variable
303
+
304
+ Set `ECADDY_HOME` to override the config root (defaults to `~/.config/caddy`). Useful for testing:
305
+
306
+ ```bash
307
+ ECADDY_HOME=/tmp/ecaddy_test ecaddy list
308
+ ```
309
+
310
+ ## Development
311
+
312
+ ```bash
313
+ bin/setup # bundle install
314
+ bundle exec rspec # run the full spec suite
315
+ bundle exec rubocop # lint
316
+ bin/console # IRB session with easy_caddy preloaded
317
+ ```
318
+
319
+ To run the CLI against the local source without installing the gem:
320
+
321
+ ```bash
322
+ bundle exec exe/ecaddy <command>
323
+ ```
324
+
325
+ Cutting a release: bump `EasyCaddy::VERSION` in `lib/easy_caddy/version.rb`, add a `CHANGELOG.md` entry, commit, then `bundle exec rake release` — that tags the commit and pushes the gem to RubyGems (requires `gem signin` first).
326
+
327
+ ## Contributing
328
+
329
+ Bug reports and pull requests are welcome at <https://github.com/pniemczyk/easy_caddy>. Please run the spec suite and `rubocop` before opening a PR.
330
+
331
+ ## License
332
+
333
+ Released under the [MIT License](LICENSE).
data/exe/ecaddy ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/easy_caddy'
5
+
6
+ EasyCaddy::CLI.start(ARGV)
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require_relative 'paths'
5
+
6
+ module EasyCaddy
7
+ module Caddy
8
+ BINARY = 'caddy'
9
+
10
+ def self.installed?
11
+ system('which caddy > /dev/null 2>&1')
12
+ end
13
+
14
+ def self.validate(caddyfile = Paths.caddyfile)
15
+ system("#{BINARY} validate --config #{caddyfile} 2>&1")
16
+ end
17
+
18
+ def self.validate!(caddyfile = Paths.caddyfile)
19
+ return unless caddyfile.exist?
20
+
21
+ out = `#{BINARY} validate --config #{caddyfile} 2>&1`
22
+ raise "Caddy config invalid:\n#{out}" unless $CHILD_STATUS.success?
23
+ end
24
+
25
+ def self.reload(caddyfile = Paths.caddyfile)
26
+ unless caddyfile.exist?
27
+ warn ' [ecaddy] Skipping reload — global Caddyfile not found. Run `ecaddy setup` first.'
28
+ return
29
+ end
30
+
31
+ out = `#{BINARY} reload --config #{caddyfile} 2>&1`
32
+ raise "Caddy reload failed:\n#{out}" unless $CHILD_STATUS.success?
33
+ end
34
+
35
+ def self.trust
36
+ system("#{BINARY} trust")
37
+ end
38
+
39
+ def self.running?
40
+ pid = brew_service_pid
41
+ pid && pid > 0
42
+ end
43
+
44
+ def self.start_service
45
+ system('brew services start caddy')
46
+ end
47
+
48
+ def self.restart_service
49
+ system('brew services restart caddy')
50
+ end
51
+
52
+ def self.brew_service_pid
53
+ output = `brew services list 2>/dev/null | grep '^caddy '`
54
+ m = output.match(/(\d+)/)
55
+ m&.captures&.first&.to_i
56
+ end
57
+
58
+ def self.process_pid
59
+ out = `pgrep -f 'caddy run' 2>/dev/null`.strip
60
+ return nil if out.empty?
61
+
62
+ out.lines.first.to_i
63
+ end
64
+
65
+ def self.install_via_brew
66
+ system('brew install caddy')
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative 'version'
5
+ require_relative 'commands/setup'
6
+ require_relative 'commands/remove'
7
+ require_relative 'commands/edit'
8
+ require_relative 'commands/list'
9
+ require_relative 'commands/up'
10
+ require_relative 'commands/down'
11
+ require_relative 'commands/status'
12
+ require_relative 'commands/doctor'
13
+ require_relative 'commands/reload'
14
+ require_relative 'commands/ensure'
15
+ require_relative 'commands/run'
16
+ require_relative 'commands/logs'
17
+ require_relative 'commands/audit'
18
+
19
+ module EasyCaddy
20
+ class CLI < Thor
21
+ def self.exit_on_failure? = true
22
+
23
+ desc 'setup', 'One-time machine bootstrap: install Caddy, scaffold config, start service'
24
+ def setup
25
+ require 'tty-prompt'
26
+ Commands::Setup.new(prompt: TTY::Prompt.new).call
27
+ end
28
+
29
+ desc 'remove NAME', 'Remove a site from global Caddy and delete its fragment'
30
+ option :force, type: :boolean, default: false, aliases: '-f', desc: 'Skip confirmation'
31
+ def remove(name)
32
+ require 'tty-prompt'
33
+ Commands::Remove.new(name: name, force: options[:force], prompt: TTY::Prompt.new).call
34
+ end
35
+
36
+ desc 'edit NAME', 'Open a site fragment in $EDITOR and reload Caddy'
37
+ def edit(name)
38
+ Commands::Edit.new(name: name).call
39
+ end
40
+
41
+ desc 'list', 'List all registered sites'
42
+ option :format, type: :string, default: 'table', aliases: '-f', desc: 'Output format: table or json'
43
+ def list
44
+ Commands::List.new(format: options[:format]).call
45
+ end
46
+
47
+ desc 'up NAME', 'Enable a disabled site and reload Caddy'
48
+ def up(name)
49
+ Commands::Up.new(name: name).call
50
+ end
51
+
52
+ desc 'down NAME', 'Disable an enabled site and reload Caddy'
53
+ def down(name)
54
+ Commands::Down.new(name: name).call
55
+ end
56
+
57
+ desc 'status', 'Show global Caddy state and per-site health'
58
+ def status
59
+ Commands::Status.new.call
60
+ end
61
+
62
+ desc 'doctor', 'Scan for port/domain conflicts and dead upstreams'
63
+ def doctor
64
+ Commands::Doctor.new.call
65
+ end
66
+
67
+ desc 'reload', 'Validate and reload the global Caddy config'
68
+ def reload
69
+ Commands::Reload.new.call
70
+ end
71
+
72
+ desc 'ensure', 'One-shot: copy project Caddyfile into global config and exit (for scripts/CI)'
73
+ option :config, type: :string, required: true, aliases: '-c', desc: 'Path to project Caddyfile'
74
+ option :site, type: :string, required: true, aliases: '-s', desc: 'Site name (used as fragment filename)'
75
+ def ensure
76
+ Commands::Ensure.new(config_path: options[:config], site: options[:site]).call
77
+ end
78
+
79
+ desc 'run', 'Register Caddyfile; block and unregister on shutdown (for Procfile.dev)'
80
+ option :config, type: :string, required: true, aliases: '-c', desc: 'Path to project Caddyfile'
81
+ option :site, type: :string, required: true, aliases: '-s', desc: 'Site name (used as fragment filename)'
82
+ def caddy_run
83
+ Commands::Run.new(config_path: options[:config], site: options[:site]).call
84
+ end
85
+ map 'run' => :caddy_run
86
+
87
+ desc 'logs', 'Tail the Caddy log files for a site'
88
+ option :site, type: :string, required: true, aliases: '-s', desc: 'Site name'
89
+ option :lines, type: :numeric, default: 200, aliases: '-n', desc: 'Number of lines (with --no-follow)'
90
+ option :follow, type: :boolean, default: true, desc: 'Follow log (tail -F). Use --no-follow to print and exit.'
91
+ def logs
92
+ Commands::Logs.new(site: options[:site], lines: options[:lines], follow: options[:follow]).call
93
+ end
94
+
95
+ desc 'audit', 'Print a full system + TLS + site snapshot'
96
+ option :site, type: :string, aliases: '-s', desc: 'Limit to a single site'
97
+ option :fix, type: :boolean, default: false, desc: 'After report, prompt to apply a fix for each failing check'
98
+ def audit
99
+ Commands::Audit.new(site: options[:site], fix: options[:fix]).call
100
+ end
101
+
102
+ desc 'version', 'Print ecaddy version'
103
+ def version
104
+ puts "ecaddy #{EasyCaddy::VERSION}"
105
+ end
106
+ end
107
+ end