caml 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89fb929361ce5de9959beac6898d64208cc7cffdbb77e8083bca78873955723a
4
- data.tar.gz: 7b4288ec82d1b423db8bed3c667e61028141f49a5577759da798a24c15280926
3
+ metadata.gz: b6c99302250394b3a09b78b2391b5ede1d3b6fa8304ab2d6dbf3733ad8f67726
4
+ data.tar.gz: 8c8c7532d97bfa544fa857238f94f3e6b104e71974714a367ddb3b418bcaefb2
5
5
  SHA512:
6
- metadata.gz: f90dbe11062eac8b849911ab4b16fe9976ecf8b912a61465a759dbfa5899e630766c122aef6635b394dd637fa876bcc26685ae0d7732943278772572445cee08
7
- data.tar.gz: da0adb379026e5459cef1804dd90c93509d7ab8d27ab8694cfd1d8bc7fd8477dd0957e97abe6c634a785a78b891e522d3f367377030f5593a3b044b56676f015
6
+ metadata.gz: 549fe339f84b6c62de4786fbbab80abdcc06145b8b8aadcfe6ff3e6cc43dba18136622f6af6abf76978ec187de7f7d88418d845e49c3c9820fb0b12ec3aa1b5c
7
+ data.tar.gz: c404fe5c3d691c00bf6ba641696df230adac242da11403986c0ae1d4a72a70e90e85a20fb49ee12e80d3a85019e5ac251beaaafdffe8761b005fc9af12225a1a
data/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [1.0.0] — 2026-05-01
6
+
7
+ First stable release.
8
+
9
+ ### Added
10
+ - Positional arguments with `{{name}}` interpolation, shell-escaped via `Shellwords`
11
+ - Typed options (string / boolean / numeric) with aliases, defaults, and per-option `execute` overrides
12
+ - Task-level command aliases via Thor's `map`
13
+ - Multi-line `execute:` arrays joined with `&&` for fail-fast semantics
14
+ - Task dependencies via `needs:`, resolved with cycle detection and dedup
15
+ - Pure orchestrator tasks (only `needs:`, no `execute:`)
16
+ - `caml init` scaffolds a starter `caml.yaml`
17
+ - `caml --version` prints the installed version
18
+ - Walks up from the current directory to find `caml.yaml`
19
+ - Friendly errors for missing or malformed YAML
20
+
21
+ ### Changed
22
+ - Replaced the `class_eval`-of-source-strings command generator with a typed pipeline (Config → Task → Runner → Plan → Thor adapter)
23
+ - Single canonical "no such task" error: `Caml::Plan::UnknownTask`
24
+ - Minimum Ruby is now 3.0
25
+ - Runtime deps declared in the gemspec; dev deps in the Gemfile
26
+
27
+ ### Security
28
+ - YAML loads via `safe_yaml` with `raise_on_unknown_tag: true`; `!ruby/object` tags are stripped, never instantiated
29
+ - Argument interpolation always shell-escapes substituted values; the only place caml shells out is `Caml::Shell#run`
30
+
31
+ ## [0.1.1]
32
+
33
+ Initial scaffolding (pre-1.0; experimental).
data/README.md CHANGED
@@ -1,78 +1,175 @@
1
- # 🐪 caml
1
+ # caml
2
2
 
3
- > Build CLI apps with YAML
3
+ > Build CLI apps from a declarative `caml.yaml` file.
4
4
 
5
- `caml` allows you to build command line applications using declarative yaml.
6
- `caml` aims to be like `make`, but by using descriptive, declarative yaml.
5
+ `caml` turns well-documented YAML tasks into a runnable CLI. Inspired by `make` and `just`, but YAML-driven.
7
6
 
8
- <!-- ## Installation -->
7
+ ## Install
9
8
 
10
- <!-- TODO -->
9
+ ```sh
10
+ gem install caml
11
+ ```
11
12
 
12
- <!-- ```sh -->
13
- <!-- gem install caml -->
14
- <!-- ``` -->
13
+ Or in a `Gemfile`:
15
14
 
16
- ## Usage
15
+ ```ruby
16
+ gem 'caml'
17
+ ```
17
18
 
18
- Running the command without any arguments displays the commands defined in the `caml.yaml` file:
19
+ ## Quickstart
19
20
 
20
21
  ```sh
21
- bin/caml
22
+ caml init # scaffold a starter caml.yaml
23
+ caml # list tasks
24
+ caml hello # run the starter task
22
25
  ```
23
26
 
24
- ## Declaring commands
27
+ ## Tasks
25
28
 
26
- `caml` reads a file called `caml.yaml` in the current directory and converts those commands into a unified CLI command.
27
-
28
- The basic structure is to have a `command`, which has a `desc` and an `execute` for the bash command to execute.
29
+ A task has a name, a `desc`, and an `execute` shell command:
29
30
 
30
31
  ```yaml
31
- command:
32
- desc: Command description
33
- execute: script.sh
32
+ test:
33
+ desc: Run the test suite
34
+ execute: bundle exec rspec
34
35
  ```
35
36
 
36
- This yaml will create the following command:
37
+ `caml` and `caml help` list every task with its description.
38
+
39
+ ## Multi-line execute
40
+
41
+ Pass a list to run multiple steps. Steps run with fail-fast semantics — the first non-zero exit aborts:
37
42
 
38
43
  ```yaml
39
- bin/caml command # Command description
44
+ setup:
45
+ desc: Install and migrate
46
+ execute:
47
+ - bundle install
48
+ - bin/rails db:migrate
40
49
  ```
41
50
 
42
- And it will run any bash command defined.
51
+ ## Arguments
43
52
 
44
- Arguments may be added under `args` in a nested fashion as displayed below.
53
+ Positional arguments substitute into the `execute` template via `{{name}}`. Values are shell-escaped:
45
54
 
46
55
  ```yaml
47
- command:
56
+ greet:
57
+ desc: Say hello to someone
48
58
  args:
49
- one:
50
- desc: First argument
51
- type: string
52
- two:
53
- desc: Second argument
59
+ name:
60
+ desc: Person to greet
54
61
  type: string
62
+ execute: echo Hello, {{name}}!
63
+ ```
64
+
65
+ ```sh
66
+ caml greet world
67
+ # Hello, world!
55
68
  ```
56
69
 
57
- ## Examples
70
+ ## Options
71
+
72
+ Flags with type, optional aliases, default value, and an optional override `execute`:
58
73
 
59
74
  ```yaml
60
75
  build:
61
- desc: Build our project
62
- execute: make
63
- clean:
64
- desc: Clean our project
65
- execute: make clean
76
+ desc: Build the project
77
+ opts:
78
+ target:
79
+ type: string
80
+ default: dist
81
+ aliases:
82
+ - t
83
+ desc: Output directory
84
+ verbose:
85
+ type: boolean
86
+ aliases:
87
+ - v
88
+ desc: Print verbose output
89
+ execute: make build TARGET={{target}}
90
+ ```
91
+
92
+ ```sh
93
+ caml build --target release
94
+ caml build -t release -v
66
95
  ```
67
96
 
97
+ ### Option-driven dispatch
98
+
99
+ When a boolean option has its own `execute`, that command runs instead of the task's default:
100
+
101
+ ```yaml
102
+ start:
103
+ desc: Start the app
104
+ opts:
105
+ background:
106
+ type: boolean
107
+ aliases:
108
+ - b
109
+ desc: Run as a daemon
110
+ execute: app start --daemon
111
+ execute: app start
112
+ ```
113
+
114
+ ```sh
115
+ caml start # app start
116
+ caml start --background # app start --daemon
117
+ ```
118
+
119
+ ## Aliases
120
+
121
+ Add shortcut names for a task:
122
+
123
+ ```yaml
124
+ test:
125
+ desc: Run the test suite
126
+ aliases:
127
+ - t
128
+ execute: bundle exec rspec
129
+ ```
130
+
131
+ ```sh
132
+ caml t # same as caml test
133
+ ```
134
+
135
+ ## Dependencies
136
+
137
+ A task can declare prerequisites with `needs`. Deps run in declared order, each at most once per invocation; failures abort the run:
138
+
68
139
  ```yaml
69
- build:
70
- desc: Bundle
71
- execute: bundle install
72
- migrate:
73
- desc: Migrate the test database
74
- execute: rails db:migrate RAILS_ENV=test
75
140
  test:
76
141
  desc: Run tests
77
- execute: rspec
142
+ execute: bundle exec rspec
143
+
144
+ lint:
145
+ desc: Check style
146
+ execute: bundle exec rubocop
147
+
148
+ ci:
149
+ desc: Lint and test
150
+ needs:
151
+ - lint
152
+ - test
153
+ ```
154
+
155
+ ```sh
156
+ caml ci # runs lint, then test
78
157
  ```
158
+
159
+ A task with only `needs:` and no `execute` is a pure orchestrator — useful for grouping.
160
+
161
+ ## Discovery
162
+
163
+ `caml` walks up from the current directory to find a `caml.yaml`, just like `git`. You can run it from any subdirectory of your project.
164
+
165
+ ## Built-in commands
166
+
167
+ | Command | Description |
168
+ |-------------------|----------------------------------------------------|
169
+ | `caml init` | Scaffold a starter `caml.yaml` |
170
+ | `caml --version` | Print the installed version |
171
+ | `caml help [cmd]` | Show help for a specific task (its args and opts) |
172
+
173
+ ## License
174
+
175
+ MIT — see [LICENSE](LICENSE).
data/bin/caml CHANGED
@@ -1,64 +1,30 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'thor'
4
- require_relative '../lib/caml/caml'
3
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
4
+ require 'caml'
5
5
 
6
- # TODO:
7
- # x Yaml
8
- # x Metaprogramming
9
- # _ Thor commands, options, arguments, etc.
10
- # _ Add bash and ruby commands
11
- # _ Turn into gem
12
- # _ Distribute via CMM gem server
13
- # _ Make some noise
14
- # _ Make it better with Rust
15
- # _ Distribute a release on GHE
16
- # _ Make some more noise
17
-
18
-
19
- class CamlCli < Thor
20
- def self.load
21
- Caml::Config.new.directives.each do |entry|
22
- directive = build_directive(entry)
23
- args = build_args(directive['args'])
24
- opts = build_opts(directive['opts'])
25
- command = build_command(directive.merge({'args' => args, 'opts' => opts}))
26
- # puts command
27
- # puts ''
28
- class_eval command.to_s
29
- end
30
- end
31
-
32
- private
33
-
34
- def self.build_directive(entry)
35
- entry.last.merge({'name' => entry.first, 'args' => entry.last['args'] || [], 'opts' => entry.last['opts'] || []})
36
- end
37
-
38
- def self.build_args(args)
39
- args.reject { |arg| arg.empty? }
40
- .map { |name, arg| arg.merge({'name' => name}) }
41
- .map { |arg| Caml::Argument.new(arg) }
42
- end
43
-
44
- def self.build_opts(opts)
45
- opts.reject { |opt| opt.empty? }
46
- .map { |name, opt| opt.merge({'name' => name}) }
47
- .map { |opt| Caml::Option.new(opt) }
48
- end
6
+ if %w[--version -v].include?(ARGV.first)
7
+ puts Caml::VERSION
8
+ exit 0
9
+ end
49
10
 
50
- def self.build_command(directive)
51
- Caml::Command.new(directive)
52
- end
11
+ config_path = Caml::Config.discover(Dir.getwd)
53
12
 
54
- no_commands do
55
- def execute
56
- successful = system yield
57
- exit successful
58
- end
13
+ if config_path
14
+ begin
15
+ config = Caml::Config.load(config_path)
16
+ rescue Caml::Config::Malformed => e
17
+ warn "caml: #{e.message}"
18
+ exit 1
59
19
  end
60
20
 
21
+ runner = Caml::Runner.new(tasks: config.tasks, shell: Caml::Shell.new)
22
+ Caml::CLI.register(config: config, runner: runner)
23
+ elsif !ARGV.empty? && !%w[init help -h --help].include?(ARGV.first)
24
+ warn 'caml: no caml.yaml found in this directory or any parent.'
25
+ warn 'Run `caml init` to scaffold one.'
26
+ exit 1
61
27
  end
62
28
 
63
- CamlCli.load
64
- CamlCli.start(ARGV)
29
+ result = Caml::CLI.start(ARGV)
30
+ exit(result == false ? 1 : 0)
@@ -0,0 +1,11 @@
1
+ module Caml
2
+ class Argument
3
+ attr_reader :name, :desc, :type
4
+
5
+ def initialize(name:, desc:, type: 'string')
6
+ @name = name
7
+ @desc = desc
8
+ @type = type
9
+ end
10
+ end
11
+ end
data/lib/caml/cli.rb ADDED
@@ -0,0 +1,62 @@
1
+ require 'thor'
2
+
3
+ module Caml
4
+ class CLI < Thor
5
+ THOR_TYPES = {
6
+ 'string' => :string,
7
+ 'boolean' => :boolean,
8
+ 'numeric' => :numeric
9
+ }.freeze
10
+
11
+ def self.exit_on_failure?
12
+ true
13
+ end
14
+
15
+ desc 'init', 'Scaffold a starter caml.yaml in the current directory'
16
+ def init
17
+ target = File.join(Dir.pwd, 'caml.yaml')
18
+ Init.scaffold(target)
19
+ say "Created #{target}", :green
20
+ rescue Init::Conflict => e
21
+ say "caml init: #{e.message}", :red
22
+ exit 1
23
+ end
24
+
25
+ def self.register(config:, runner:)
26
+ config.tasks.each { |task| register_task(task, runner) }
27
+ end
28
+
29
+ def self.register_task(task, runner)
30
+ task.opts.each { |opt| register_option(opt) }
31
+ desc usage(task), task.desc.to_s
32
+ task.aliases.each { |alias_name| map alias_name => task.name.to_sym }
33
+ define_dispatcher(task, runner)
34
+ end
35
+
36
+ def self.define_dispatcher(task, runner)
37
+ define_method(task.name) do |*positional|
38
+ args_hash = task.args.each_with_index.to_h { |arg, i| [arg.name, positional[i]] }
39
+ runner.run(task.name, args: args_hash, opts: options.to_h)
40
+ end
41
+ end
42
+
43
+ def self.register_option(opt)
44
+ attrs = {
45
+ type: thor_type(opt.type),
46
+ desc: opt.desc.to_s,
47
+ aliases: opt.aliases.map { |a| "-#{a}" }
48
+ }
49
+ attrs[:default] = opt.default unless opt.default.nil?
50
+ method_option opt.name, **attrs
51
+ end
52
+
53
+ def self.thor_type(yaml_type)
54
+ THOR_TYPES.fetch(yaml_type, :string)
55
+ end
56
+
57
+ def self.usage(task)
58
+ positional = task.args.map { |arg| arg.name.upcase }.join(' ')
59
+ positional.empty? ? task.name : "#{task.name} #{positional}"
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,98 @@
1
+ require 'safe_yaml/load'
2
+
3
+ module Caml
4
+ class Config
5
+ class NotFound < StandardError
6
+ end
7
+
8
+ class Malformed < StandardError
9
+ end
10
+
11
+ attr_reader :tasks
12
+
13
+ def self.load(path)
14
+ raise NotFound, "caml.yaml not found at #{path}" unless File.file?(path)
15
+
16
+ raw = parse(File.read(path), path)
17
+ from_raw(raw)
18
+ end
19
+
20
+ def self.discover(start_dir)
21
+ dir = File.expand_path(start_dir)
22
+ loop do
23
+ candidate = File.join(dir, 'caml.yaml')
24
+ return candidate if File.file?(candidate)
25
+
26
+ parent = File.dirname(dir)
27
+ return nil if parent == dir
28
+
29
+ dir = parent
30
+ end
31
+ end
32
+
33
+ def self.from_tasks(tasks)
34
+ new(tasks)
35
+ end
36
+
37
+ def initialize(tasks)
38
+ @tasks = tasks
39
+ end
40
+
41
+ def self.parse(content, path)
42
+ SafeYAML.load(content, nil, safe: true, raise_on_unknown_tag: true) || {}
43
+ rescue Psych::SyntaxError, SafeYAML::UnknownTagError => e
44
+ raise Malformed, "could not parse #{path}: #{e.message.sub(/\A\([^)]*\):\s*/, '')}"
45
+ end
46
+ private_class_method :parse
47
+
48
+ def self.from_raw(raw)
49
+ tasks = raw.map { |name, body| build_task(name, body || {}) }
50
+ new(tasks)
51
+ end
52
+ private_class_method :from_raw
53
+
54
+ def self.build_task(name, body)
55
+ Task.new(
56
+ name: name,
57
+ desc: body['desc'],
58
+ execute: body['execute'],
59
+ args: build_args(body['args']),
60
+ opts: build_opts(body['opts']),
61
+ aliases: body['aliases'] || [],
62
+ needs: body['needs'] || []
63
+ )
64
+ end
65
+ private_class_method :build_task
66
+
67
+ def self.build_args(raw)
68
+ return [] unless raw
69
+
70
+ raw.map { |name, fields| build_argument(name, fields || {}) }
71
+ end
72
+ private_class_method :build_args
73
+
74
+ def self.build_argument(name, fields)
75
+ attrs = { name: name, desc: fields['desc'] }
76
+ attrs[:type] = fields['type'] if fields['type']
77
+ Argument.new(**attrs)
78
+ end
79
+ private_class_method :build_argument
80
+
81
+ def self.build_opts(raw)
82
+ return [] unless raw
83
+
84
+ raw.map { |name, fields| build_option(name, fields || {}) }
85
+ end
86
+ private_class_method :build_opts
87
+
88
+ def self.build_option(name, fields)
89
+ attrs = { name: name, desc: fields['desc'] }
90
+ attrs[:type] = fields['type'] if fields['type']
91
+ attrs[:aliases] = fields['aliases'] if fields['aliases']
92
+ attrs[:default] = fields['default'] unless fields['default'].nil?
93
+ attrs[:execute] = fields['execute'] if fields['execute']
94
+ Option.new(**attrs)
95
+ end
96
+ private_class_method :build_option
97
+ end
98
+ end
data/lib/caml/init.rb ADDED
@@ -0,0 +1,18 @@
1
+ module Caml
2
+ module Init
3
+ class Conflict < StandardError
4
+ end
5
+
6
+ TEMPLATE = <<~YAML.freeze
7
+ hello:
8
+ desc: A starter task — replace me
9
+ execute: echo Hello, caml!
10
+ YAML
11
+
12
+ def self.scaffold(path)
13
+ raise Conflict, "#{path} already exists" if File.exist?(path)
14
+
15
+ File.write(path, TEMPLATE)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ require 'shellwords'
2
+
3
+ module Caml
4
+ module Interpolation
5
+ class Missing < StandardError
6
+ end
7
+
8
+ TOKEN = /\{\{(\w+)\}\}/
9
+
10
+ def self.apply(template, values)
11
+ template.gsub(TOKEN) do
12
+ name = Regexp.last_match(1)
13
+ raise Missing, "no value for {{#{name}}}" unless values.key?(name)
14
+
15
+ Shellwords.escape(values[name].to_s)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module Caml
2
+ class Option
3
+ attr_reader :name, :desc, :type, :aliases, :default, :execute
4
+
5
+ def initialize(name:, desc:, type: 'boolean', aliases: [], default: nil, execute: nil)
6
+ @name = name
7
+ @desc = desc
8
+ @type = type
9
+ @aliases = aliases
10
+ @default = default
11
+ @execute = execute
12
+ end
13
+ end
14
+ end
data/lib/caml/plan.rb ADDED
@@ -0,0 +1,39 @@
1
+ module Caml
2
+ module Plan
3
+ class Cycle < StandardError
4
+ end
5
+
6
+ class UnknownTask < StandardError
7
+ end
8
+
9
+ def self.resolve(target_name, tasks)
10
+ index = tasks.to_h { |task| [task.name, task] }
11
+ state = { ordered: [], visiting: [], visited: {} }
12
+ visit(target_name, index, state)
13
+ state[:ordered]
14
+ end
15
+
16
+ def self.visit(name, index, state)
17
+ return if state[:visited][name]
18
+
19
+ raise_if_cycle(state, name)
20
+ task = index[name] or raise UnknownTask, "no such task: #{name}"
21
+
22
+ state[:visiting] << name
23
+ task.needs.each { |dep| visit(dep, index, state) }
24
+ state[:visiting].pop
25
+
26
+ state[:visited][name] = true
27
+ state[:ordered] << task
28
+ end
29
+ private_class_method :visit
30
+
31
+ def self.raise_if_cycle(state, name)
32
+ return unless state[:visiting].include?(name)
33
+
34
+ path = state[:visiting].drop_while { |n| n != name } + [name]
35
+ raise Cycle, "cycle detected: #{path.join(' -> ')}"
36
+ end
37
+ private_class_method :raise_if_cycle
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ module Caml
2
+ class Runner
3
+ def initialize(tasks:, shell:)
4
+ @tasks = tasks
5
+ @shell = shell
6
+ end
7
+
8
+ def run(name, args: {}, opts: {})
9
+ plan = Plan.resolve(name, @tasks)
10
+ plan.each do |task|
11
+ bindings = task.name == name ? { args: args, opts: opts } : { args: {}, opts: {} }
12
+ return false unless execute_task(task, **bindings)
13
+ end
14
+ true
15
+ end
16
+
17
+ private
18
+
19
+ def execute_task(task, args:, opts:)
20
+ return true if task.execute.nil? || Array(task.execute).empty?
21
+
22
+ command = build_command(task, args, opts)
23
+ @shell.run(command)
24
+ end
25
+
26
+ def build_command(task, args, opts)
27
+ execute = effective_execute(task, opts)
28
+ bindings = args.merge(opts)
29
+ Array(execute).map { |step| Interpolation.apply(step, bindings) }.join(' && ')
30
+ end
31
+
32
+ def effective_execute(task, opts)
33
+ override = task.opts.find { |opt| opt.execute && opts[opt.name] }
34
+ override ? override.execute : task.execute
35
+ end
36
+ end
37
+ end
data/lib/caml/shell.rb ADDED
@@ -0,0 +1,7 @@
1
+ module Caml
2
+ class Shell
3
+ def run(command)
4
+ system(command) ? true : false
5
+ end
6
+ end
7
+ end
data/lib/caml/task.rb ADDED
@@ -0,0 +1,15 @@
1
+ module Caml
2
+ class Task
3
+ attr_reader :name, :desc, :execute, :args, :opts, :aliases, :needs
4
+
5
+ def initialize(name:, desc:, execute:, args: [], opts: [], aliases: [], needs: [])
6
+ @name = name
7
+ @desc = desc
8
+ @execute = execute
9
+ @args = args
10
+ @opts = opts
11
+ @aliases = aliases
12
+ @needs = needs
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Caml
2
+ VERSION = '1.0.0'.freeze
3
+ end
data/lib/caml.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'caml/version'
2
+ require 'caml/argument'
3
+ require 'caml/option'
4
+ require 'caml/task'
5
+ require 'caml/interpolation'
6
+ require 'caml/config'
7
+ require 'caml/plan'
8
+ require 'caml/shell'
9
+ require 'caml/runner'
10
+ require 'caml/init'
11
+ require 'caml/cli'
12
+
13
+ module Caml
14
+ end
metadata CHANGED
@@ -1,63 +1,99 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian Johnson
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-04-21 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: rspec
13
+ name: base64
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: safe_yaml
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 1.0.5
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '1.0'
17
43
  - - ">="
18
44
  - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
45
+ version: 1.0.5
46
+ - !ruby/object:Gem::Dependency
47
+ name: thor
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '1.0'
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 1.0.1
56
+ type: :runtime
21
57
  prerelease: false
22
58
  version_requirements: !ruby/object:Gem::Requirement
23
59
  requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.0'
24
63
  - - ">="
25
64
  - !ruby/object:Gem::Version
26
- version: '0'
27
- description: "# \U0001F42A caml\n\n> Build CLI apps with YAML\n\n`caml` allows you
28
- to build command line applications using declarative yaml.\n`caml` aims to be like
29
- `make`, but by using descriptive, declarative yaml.\n\n<!-- ## Installation -->\n\n<!--
30
- TODO -->\n\n<!-- ```sh -->\n<!-- gem install caml -->\n<!-- ``` -->\n\n## Usage\n\nRunning
31
- the command without any arguments displays the commands defined in the `caml.yaml`
32
- file:\n\n```sh\nbin/caml\n```\n\n## Declaring commands\n\n`caml` reads a file called
33
- `caml.yaml` in the current directory and converts those commands into a unified
34
- CLI command.\n\nThe basic structure is to have a `command`, which has a `desc` and
35
- an `execute` for the bash command to execute.\n\n```yaml\ncommand:\n desc: Command
36
- description\n execute: script.sh\n```\n\nThis yaml will create the following command:\n\n```yaml\nbin/caml
37
- command # Command description\n```\n\nAnd it will run any bash command defined.\n\nArguments
38
- may be added under `args` in a nested fashion as displayed below.\n\n```yaml\ncommand:\n
39
- \ args:\n one:\n desc: First argument\n type: string\n two:\n desc:
40
- Second argument\n type: string\n```\n\n## Examples\n\n```yaml\nbuild:\n desc:
41
- Build our project\n execute: make\nclean:\n desc: Clean our project\n execute:
42
- make clean\n```\n\n```yaml\nbuild:\n desc: Bundle\n execute: bundle install\nmigrate:\n
43
- \ desc: Migrate the test database\n execute: rails db:migrate RAILS_ENV=test\ntest:\n
44
- \ desc: Run tests\n execute: rspec\n```\n"
65
+ version: 1.0.1
66
+ description: caml turns well-documented YAML tasks into a runnable CLI. Inspired by
67
+ make and just, but YAML-driven.
45
68
  email: tacoda@hey.com
46
69
  executables:
47
70
  - caml
48
71
  extensions: []
49
72
  extra_rdoc_files: []
50
73
  files:
74
+ - CHANGELOG.md
51
75
  - LICENSE
52
76
  - README.md
53
77
  - bin/caml
54
- - lib/caml/caml.rb
55
- - spec/caml/caml_spec.rb
78
+ - lib/caml.rb
79
+ - lib/caml/argument.rb
80
+ - lib/caml/cli.rb
81
+ - lib/caml/config.rb
82
+ - lib/caml/init.rb
83
+ - lib/caml/interpolation.rb
84
+ - lib/caml/option.rb
85
+ - lib/caml/plan.rb
86
+ - lib/caml/runner.rb
87
+ - lib/caml/shell.rb
88
+ - lib/caml/task.rb
89
+ - lib/caml/version.rb
56
90
  homepage: https://github.com/tacoda/caml
57
91
  licenses:
58
92
  - MIT
59
- metadata: {}
60
- post_install_message:
93
+ metadata:
94
+ source_code_uri: https://github.com/tacoda/caml
95
+ changelog_uri: https://github.com/tacoda/caml/blob/main/CHANGELOG.md
96
+ rubygems_mfa_required: 'true'
61
97
  rdoc_options: []
62
98
  require_paths:
63
99
  - lib
@@ -65,16 +101,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
65
101
  requirements:
66
102
  - - ">="
67
103
  - !ruby/object:Gem::Version
68
- version: '1.9'
104
+ version: '3.0'
69
105
  required_rubygems_version: !ruby/object:Gem::Requirement
70
106
  requirements:
71
107
  - - ">="
72
108
  - !ruby/object:Gem::Version
73
109
  version: '0'
74
110
  requirements: []
75
- rubygems_version: 3.4.10
76
- signing_key:
111
+ rubygems_version: 4.0.3
77
112
  specification_version: 4
78
- summary: Build CLI apps using YAML
79
- test_files:
80
- - spec/caml/caml_spec.rb
113
+ summary: Build CLI apps from a declarative caml.yaml file
114
+ test_files: []
data/lib/caml/caml.rb DELETED
@@ -1,107 +0,0 @@
1
- require 'safe_yaml/load'
2
-
3
- module Caml
4
- # def self.wrap(object)
5
- # if object.nil?
6
- # []
7
- # elsif object.respond_to?(:to_ary)
8
- # object.to_ary || [object]
9
- # else
10
- # [object]
11
- # end
12
- # end
13
-
14
- class Config
15
- def initialize
16
- # Recursively load (prompt to init if it doesn't exist)
17
- SafeYAML::OPTIONS[:default_mode] = :safe
18
- @config = YAML.load_file(File.join(Dir.getwd, 'caml.yaml'))
19
- end
20
-
21
- def directives
22
- @config
23
- end
24
-
25
- def to_s
26
- inspect
27
- end
28
- end
29
-
30
- class Command
31
- attr_reader :name, :desc, :aliases, :args, :opts, :execute
32
-
33
- def initialize(options = {})
34
- @name = options['name']
35
- @desc = options['desc']
36
- @aliases = options['aliases']
37
- @args = options['args']
38
- @opts = options['opts']
39
- @execute = options['execute']
40
- end
41
-
42
- def dispatcher
43
- if opts.empty?
44
- executor
45
- else
46
- first_clause = "if options[:#{opts.first.name.to_sym}]\nexecute { \"#{opts.first.execute}\" }\n"
47
- # else_clause = directive.execute.nil? ? "#\n" : "else\nexecute { \"#{wrap(directive.execute).join(';')}\" }\nend"
48
- # clauses = directive.options.drop(1).map { |option| "elsif options[:#{option.name.to_sym}]\nexecute { \"#{wrap(option.execute).join(';')}\" }\n" }.join
49
- # dispatcher = first_clause + clauses + else_clause
50
- puts first_clause
51
- executor
52
- end
53
- end
54
-
55
- def executor
56
- if execute.respond_to?(:join)
57
- <<-EOS
58
- execute do
59
- <<-COMMANDS
60
- #{execute.join("\n")}
61
- COMMANDS
62
- end
63
- EOS
64
- else
65
- "execute { \"#{execute}\" }\n"
66
- end
67
- end
68
-
69
- def to_s
70
- descriptor = "desc '#{name}', '#{desc}'\n"
71
- options = opts.empty? ? '' : opts.join("\n") + "\n"
72
- signature = "def #{name}\n" # TODO: args
73
- close_scope = "end\n"
74
- descriptor + options + signature + executor + close_scope
75
- end
76
- end
77
-
78
- class Option
79
- attr_reader :name, :type, :desc, :aliases, :execute
80
-
81
- def initialize(options = {})
82
- @name = options['name']
83
- @type = options['type']
84
- @desc = options['desc']
85
- @aliases = options['aliases']
86
- @execute = options['execute']
87
- end
88
-
89
- def to_s
90
- "option :#{name}, type: :#{type}, desc: '#{desc}'"
91
- end
92
- end
93
-
94
- class Argument
95
- attr_reader :name, :type, :desc, :value
96
-
97
- def initialize(options ={})
98
- @name = options['name']
99
- @desc = options['desc']
100
- @type = options['type']
101
- end
102
-
103
- def to_s
104
- inspect
105
- end
106
- end
107
- end
@@ -1,9 +0,0 @@
1
- require 'caml/caml'
2
-
3
- module Caml
4
- describe Caml do
5
- it 'works' do
6
- expect(1).to eq(1)
7
- end
8
- end
9
- end