raindown 0.11.0 → 0.12.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: 1dffe3184c451794454149de18701ac6bd1afc2674b6a41385773d0f052bb2e5
4
- data.tar.gz: 4e49463244dffc8e9b39873e2137f072f8632daec02c314d928fe09bbb115fd5
3
+ metadata.gz: 6ea9df31d83ebabf0649ad353b75e2f2546aeb10c31aa97038bdd85f5e45fd2c
4
+ data.tar.gz: cc736f0d57e576c476629bfa7fe37405139cb1b880e86908e71e7e8d8b22926c
5
5
  SHA512:
6
- metadata.gz: 9502a0acb5cca634c884f74b8f2fc3e3a83c3fbe5bc635df7fccb779147718563a0a44679b3b3e6a1cf9da9e691026ff55953fc9a6b0aafad9cc48444eec12c6
7
- data.tar.gz: dc928865b0da23c1b3dd67d859e94a3e718fe370b3d21d9344060cd5019e82fd5cce029b56d6008632690f9f1cc6f782cd41a8c38ca6d233bd06336f29fb7077
6
+ metadata.gz: 851438f9079a944aca1dc2f97d0c5081822d0ce31df12e536ca10dce8d471233ee0879fc91b7004fcf7949159554dec7cd0d1e4aa3d50b8cee4009dbd3180766
7
+ data.tar.gz: 27863d35bb59084303144bdc0b1ea3852028a55c854e8142456c3e1d8f9ee495a3d5489dc00c91ce474bcd9eda833deb42d4f3696118bb24c25e913dabb54372
@@ -24,7 +24,7 @@ module Raindown
24
24
  # Writes back the whole file, like saving it while it's watched, with bare directives allowed, and endpoints with no
25
25
  # response run, when asked, like with --bare and -x.
26
26
  def write_back(path, bare: false, execute: false)
27
- Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path))
27
+ Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path)).changed?
28
28
  end
29
29
 
30
30
  # Runs one endpoint's request and writes its response, replacing any response it had, then raises when the response
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'json'
4
5
  require 'time'
5
6
  require 'unicode/display_width'
6
7
  require_relative 'projects'
7
8
 
8
9
  module Raindown
9
- # Blueprints from Claude Code's replies, kept in a project's .raindown/blueprints.md, which the terminal UI's Blueprints page
10
+ # Blueprints from Claude Code's replies, kept in a project's .raindown/data/blueprints.md, which the terminal UI's Blueprints page
10
11
  # shows, newest first. Each is a code block under a heading of when it was saved and its kind:
11
12
  # ## 2026-10-30T03:14:07Z · mockup
12
13
  #
@@ -32,7 +33,7 @@ module Raindown
32
33
  "#{time.sub('T', ' ').sub(/:\d\dZ\z/, ' UTC')}#{SEPARATOR}#{kind}"
33
34
  end
34
35
 
35
- # As it's saved in .raindown/blueprints.md, under when it was saved and its kind, then its description.
36
+ # As it's saved in .raindown/data/blueprints.md, under when it was saved and its kind, then its description.
36
37
  def markdown(saved_at)
37
38
  about = "#{description}\n\n" if description
38
39
  "## #{saved_at}#{SEPARATOR}#{kind}\n\n#{about}```#{kind}\n#{lines.join("\n")}\n```\n\n"
@@ -49,7 +50,7 @@ module Raindown
49
50
  def distance(other) = [(top - other.top).abs, (left + right - other.left - other.right).abs]
50
51
  end
51
52
 
52
- FILE = File.join('.raindown', 'blueprints.md')
53
+ FILE = File.join('.raindown', 'data', 'blueprints.md')
53
54
  # The kinds a code block can be marked with to be a blueprint.
54
55
  KINDS = %w[mockup diagram chart table other].freeze
55
56
  # What a blueprint's drawn with: lines, like boxes' and arrows', and blocks, like charts' bars.
@@ -79,8 +80,7 @@ module Raindown
79
80
  return [] unless File.exist?(path)
80
81
 
81
82
  lines = File.readlines(path, chomp: true)
82
- starts = lines.each_index.select { lines[it].start_with?('## ') }
83
- starts.filter_map { |start| blueprint(lines, start) }.reverse
83
+ lines.each_index.select { lines[it].start_with?('## ') }.filter_map { blueprint(lines, it) }.reverse
84
84
  end
85
85
 
86
86
  # Saves the blueprints in Claude Code's last reply, from its Stop hook's JSON, to the project it's working in, if any,
@@ -91,7 +91,8 @@ module Raindown
91
91
  return if drawn.empty?
92
92
 
93
93
  time = Time.now.utc.iso8601
94
- File.open(File.join(root, FILE), 'a') { |file| drawn.each { file.write(it.markdown(time)) } }
94
+ path = File.join(root, FILE).tap { FileUtils.mkdir_p(File.dirname(it)) }
95
+ File.open(path, 'a') { |file| drawn.each { file.write(it.markdown(time)) } }
95
96
  end
96
97
 
97
98
  # The text's code blocks that are blueprints, each with its kind and the paragraph above it.
data/lib/raindown/cli.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
+ require 'open3'
5
+ require 'tmpdir'
4
6
  require 'trees'
5
7
  require_relative '../raindown'
6
8
  require_relative '../version'
@@ -32,7 +34,7 @@ module Raindown
32
34
  init [DIRECTORY] Create a .raindown folder for the project's config and nodes (default: current directory)
33
35
  server Keep Raindown loaded for an editor, answering a JSON request a line on stdin with its HTML,
34
36
  or its Markdown as it would be written back, like --stdin and --stdin --rewrite
35
- blueprints Save the blueprints in Claude Code's last reply to .raindown/blueprints.md, for the terminal
37
+ blueprints Save the blueprints in Claude Code's last reply to .raindown/data/blueprints.md, for the terminal
36
38
  UI's Blueprints page, as a Stop hook that's given the hook's JSON
37
39
 
38
40
  Options:
@@ -52,12 +54,8 @@ module Raindown
52
54
  Set RAINDOWN_DEBUG=1 to show where an error happened.
53
55
  TEXT
54
56
 
55
- CONFIG = <<~YAML
56
- # Raindown project settings. See https://raindown.dev/docs/config
57
- #
58
- # Leave code blocks unhighlighted, so the browser can highlight them instead:
59
- # highlight_code: false
60
- YAML
57
+ # The files "raindown init" puts in a project's .raindown folder, like "rain new" clones raindeer-template.
58
+ TEMPLATE_URL = 'https://github.com/rainpour/raindown-init'
61
59
 
62
60
  # A path opens the terminal UI, or after "watch" it's watched, and with --html or --stdin it's rendered. The flags
63
61
  # are keyword arguments, in any order, like -x for x:, and --html is the OUTPUT when it's given one.
@@ -111,16 +109,20 @@ module Raindown
111
109
  abort("raindown: #{path}: #{error.message}")
112
110
  end
113
111
 
114
- # Creates the .raindown folder that marks a project's root and holds its config.yml and nodes.
112
+ # Clones raindown-init into the .raindown folder that marks a project's root and holds its config.yml and nodes,
113
+ # keeping any files already there.
115
114
  def self.init(directory = nil)
116
115
  folder = File.join(directory || Dir.pwd, Nodes::FOLDER)
117
116
  config = File.join(folder, Config::FILE)
118
117
  return puts("#{config} already exists") if File.exist?(config)
119
118
 
120
- FileUtils.mkdir_p(folder)
121
- File.write(config, CONFIG)
122
- gitignore = File.join(folder, '.gitignore')
123
- File.write(gitignore, "#{EndpointVariables::LOCAL_FILE}\n") unless File.exist?(gitignore)
119
+ Dir.mktmpdir do |clone|
120
+ clone_template(clone)
121
+ FileUtils.mkdir_p(folder)
122
+ (Dir.children(clone) - ['.git']).each do |name|
123
+ FileUtils.cp_r(File.join(clone, name), folder) unless File.exist?(File.join(folder, name))
124
+ end
125
+ end
124
126
  puts "Created #{config}"
125
127
  puts 'Put your own nodes in the same folder. See https://raindown.dev/docs/components'
126
128
  end
@@ -136,6 +138,11 @@ module Raindown
136
138
  class << self
137
139
  private
138
140
 
141
+ def clone_template(directory)
142
+ output, status = Open3.capture2e('git', 'clone', '--quiet', '--depth', '1', TEMPLATE_URL, directory)
143
+ abort("Couldn't clone #{TEMPLATE_URL}:\n#{output}") unless status.success?
144
+ end
145
+
139
146
  # Endpoints run while formatting, which --stdin skips, so -x can't go with it. --html - prints, like --html.
140
147
  def open_path(path, html:, stdin:, bare:, execute:)
141
148
  output = html unless [true, '-'].include?(html)
@@ -152,14 +159,13 @@ module Raindown
152
159
  Nodes.load(path)
153
160
 
154
161
  metadata = Pages.for(path)
155
- Rewriter.update(path:, bare:, execute:, metadata:)
162
+ failures = Rewriter.update(path:, bare:, execute:, metadata:).failures
156
163
  write(output, Raindown.render_file(path:, bare:, metadata:))
157
- check_endpoints(path, bare:)
164
+ check_endpoints(failures)
158
165
  end
159
166
 
160
167
  # Endpoint responses that fail their checks, like status: 200, are listed, and fail the command, for CI.
161
- def check_endpoints(path, bare:)
162
- failures = EndpointRunner.failures(content: Utils.read_markdown(path), path:, bare:)
168
+ def check_endpoints(failures)
163
169
  return if failures.empty?
164
170
 
165
171
  warn(*failures.map { "Endpoint check failed: #{it}" })
@@ -5,6 +5,8 @@ module Raindown
5
5
  module CodeBlocks
6
6
  FENCE = /\A {0,3}(`{3,}|~{3,})[ \t]*(\S*)/
7
7
  SNIPPET_LANGUAGES = %w[sh bash shell zsh console curl].freeze
8
+ # The line closing a fence, like ``` or ~~~~, compiled once per fence.
9
+ CLOSINGS = Hash.new { |closings, fence| closings[fence] = /\A {0,3}#{fence[0]}{#{fence.length},}[ \t]*$/ }
8
10
 
9
11
  module_function
10
12
 
@@ -13,7 +15,7 @@ module Raindown
13
15
  fence, language = lines[index]&.match(FENCE)&.captures
14
16
  return unless fence
15
17
 
16
- close = (index + 1...lines.length).find { |i| lines[i].match?(/\A {0,3}#{fence[0]}{#{fence.length},}[ \t]*$/) }
18
+ close = (index + 1...lines.length).find { |i| lines[i].match?(CLOSINGS[fence]) }
17
19
  { open: index, close:, language:, body: lines[index + 1...close].join } if close
18
20
  end
19
21
 
@@ -17,8 +17,8 @@ module Raindown
17
17
 
18
18
  def rewrite_markdown(markdown, path:, bare: false, execute: false)
19
19
  Nodes.load(path)
20
- rewritten = Rewriter.rewrite(content: markdown, path:, bare:, execute:, metadata: Pages.for(path))
21
- { output: rewritten, warnings: EndpointRunner.failures(content: rewritten, path:, bare:).map { "Endpoint check failed: #{it}" } }
20
+ rewrite = Rewriter.rewrite(content: markdown, path:, bare:, execute:, metadata: Pages.for(path))
21
+ { output: rewrite.content, warnings: rewrite.failures.map { "Endpoint check failed: #{it}" } }
22
22
  end
23
23
  end
24
24
  end
@@ -42,11 +42,6 @@ module Raindown
42
42
  new(content:, path:, bare: BareDirectives.enabled?(path:, content:, bare:), execute:).rewrite
43
43
  end
44
44
 
45
- # The file's failed checks, like "api.md:3: expected status 200, received 404", from the responses in it.
46
- def self.failures(content:, path: nil, bare: false)
47
- new(content:, path:, bare: BareDirectives.enabled?(path:, content:, bare:)).tap(&:rewrite).failures
48
- end
49
-
50
45
  def initialize(content:, path: nil, bare: false, execute: false, only: nil)
51
46
  @lines = content.lines
52
47
  @path = path
@@ -26,7 +26,7 @@ module Raindown
26
26
  end
27
27
  end
28
28
 
29
- # Whether a file is a Markdown Component, by its extension and name, so .raindown/blueprints.md isn't one.
29
+ # Whether a file is a Markdown Component, by its extension and name, so .raindown/data/blueprints.md isn't one.
30
30
  def file?(path)
31
31
  EXTENSIONS.include?(File.extname(path)) && File.basename(path, '.*').match?(NAME)
32
32
  end
@@ -13,23 +13,49 @@ module Raindown
13
13
  # folder: basics, order: 1, path: docs/_basics/_1-getting-started.md
14
14
  # Folder tags can share a folder name with "&", like docs/_1&_basics/getting-started.md.
15
15
  # An app can add its own metadata by subclassing and overriding implicit_metadata, like Raindeer's URL paths.
16
+ # Each file's front matter is only read once something lists the files. The listing and the front matter are kept for
17
+ # as long as the process runs, like "raindown server", until they change.
16
18
  class Pages
17
19
  EXTENSIONS = %w[md rd markdown raindown].freeze
18
20
  SEGMENTS = Regexp.union(['/', '&', '-'])
19
21
 
20
22
  class << self
23
+ # Each file's front matter, by its path, with the time it was modified when it was read.
24
+ def front_matters
25
+ @front_matters ||= {}
26
+ end
27
+
21
28
  # The Markdown files under a project's root, the folder with its .raindown folder or else the file's folder.
22
29
  def for(path)
23
30
  file_directory = File.dirname(File.expand_path(path))
24
31
  load(root: Nodes.find_folder(file_directory)&.then { File.dirname(it) } || file_directory)
25
32
  end
26
33
 
27
- # Lists the files under the root with Dirload, without loading them.
34
+ # Lists the files under the root, without loading them, for Dirload.
28
35
  def load(root:)
29
36
  require 'dirload'
30
- file_types = Dirload.file_paths(absolute_path: root).group_by { Dirload.extension(file_path: it) }
37
+ file_types = file_paths(root).group_by { Dirload.extension(file_path: it) }
31
38
  new(metadata: Dirload::Metadata.new(loaded_paths: {}, missed_paths: [], file_types:), root:)
32
39
  end
40
+
41
+ # The files under the root, outside hidden folders, like Dirload lists them. They're listed again once a folder
42
+ # among them has changed, which adding, removing or renaming a file or folder in it does.
43
+ def file_paths(root)
44
+ listing = listings[root]
45
+ return listing[:file_paths] if listing&.dig(:folders)&.all? { |folder, modified| modified_at(folder) == modified }
46
+
47
+ folders, file_paths = Dir["#{root}/**/*"].partition { File.directory?(it) }
48
+ listings[root] = { file_paths:, folders: [root, *folders].to_h { [it, modified_at(it)] } }
49
+ file_paths
50
+ end
51
+
52
+ def listings
53
+ @listings ||= {}
54
+ end
55
+
56
+ def modified_at(path)
57
+ File.mtime(path) if File.exist?(path)
58
+ end
33
59
  end
34
60
 
35
61
  attr_reader :root
@@ -39,10 +65,6 @@ module Raindown
39
65
  @root = root
40
66
  @render_options = render_options
41
67
  @file_paths = metadata.file_types.values_at(*EXTENSIONS).flatten.compact
42
- @metadata = {}
43
- @tags = {}
44
-
45
- @file_paths.each { |file_path| process(file_path:) }
46
68
  end
47
69
 
48
70
  # The files with any of the tags, like list(folder: 'basics') or list(tags: 'ruby'), sorted by order.
@@ -53,21 +75,46 @@ module Raindown
53
75
  end
54
76
 
55
77
  def tagged(**tags)
56
- tags.flat_map { |type, tag| @tags.dig(type.to_sym, tag) || [] }.uniq
78
+ tags.flat_map { |type, tag| self.tags.dig(type.to_sym, tag) || [] }.uniq
57
79
  end
58
80
 
59
81
  private
60
82
 
83
+ def metadata
84
+ process_files unless @metadata
85
+ @metadata
86
+ end
87
+
88
+ def tags
89
+ process_files unless @tags
90
+ @tags
91
+ end
92
+
93
+ def process_files
94
+ @metadata = {}
95
+ @tags = {}
96
+ @file_paths.each { |file_path| process(file_path:) }
97
+ end
98
+
61
99
  def listed_page(file_path)
62
- Page.new(file_path:, metadata: @metadata[file_path], render_options: @render_options)
100
+ Page.new(file_path:, metadata: metadata[file_path], render_options: @render_options)
63
101
  end
64
102
 
65
103
  def process(file_path:)
66
- metadata = implicit_metadata(file_path:).merge(FrontMatter.parse(Utils.read_markdown(file_path)).first)
104
+ metadata = implicit_metadata(file_path:).merge(front_matter(file_path))
67
105
  @metadata[file_path] = metadata
68
106
  metadata.each { |type, tag| Array(tag).each { |item| tag(type:, tag: item, file_path:) } }
69
107
  end
70
108
 
109
+ # The file's front matter, read again only when the file's changed since it was last read.
110
+ def front_matter(file_path)
111
+ modified = File.mtime(file_path)
112
+ modified_then, front_matter = self.class.front_matters[file_path]
113
+ return front_matter if modified_then == modified
114
+
115
+ FrontMatter.parse(Utils.read_markdown(file_path)).first.tap { self.class.front_matters[file_path] = [modified, it] }
116
+ end
117
+
71
118
  # The path is relative to the root, like docs/_basics/_1-getting-started.md.
72
119
  # A subclass can add or replace metadata here, like Raindeer's Rain::Pages replacing the path with a URL path.
73
120
  def implicit_metadata(file_path:)
@@ -104,8 +151,8 @@ module Raindown
104
151
 
105
152
  # Files with an order come first, by order, then the rest by path.
106
153
  def sort_key(file_path)
107
- order = @metadata[file_path][:order]
108
- [order.is_a?(Numeric) ? 0 : 1, order.is_a?(Numeric) ? order : 0, @metadata[file_path][:path].to_s]
154
+ order = metadata[file_path][:order]
155
+ [order.is_a?(Numeric) ? 0 : 1, order.is_a?(Numeric) ? order : 0, metadata[file_path][:path].to_s]
109
156
  end
110
157
  end
111
158
  end
@@ -8,26 +8,27 @@ require_relative 'utils'
8
8
  module Raindown
9
9
  # Rewrites the generated parts of a Markdown file in place before it's rendered:
10
10
  # node output, like the TOC, endpoint responses and table alignment.
11
- # Each rewriter responds to rewrite(content:, path:, bare:, execute:, metadata:) and returns the new content.
12
11
  module Rewriter
13
- REWRITERS = [NodeInjector, EndpointRunner, TableFormatter].freeze
12
+ # The content written back, and the endpoint checks it failed, like "api.md:3: expected status 200, received 404".
13
+ Rewrite = Data.define(:original, :content, :failures) do
14
+ def changed?
15
+ content != original
16
+ end
17
+ end
14
18
 
15
19
  module_function
16
20
 
17
21
  def rewrite(content:, path:, bare: false, execute: false, metadata: NoMetadata)
18
- REWRITERS.reduce(content) do |rewritten, rewriter|
19
- rewriter.rewrite(content: rewritten, path:, bare:, execute:, metadata:)
20
- end
22
+ injected = NodeInjector.rewrite(content:, path:, bare:, metadata:)
23
+ endpoints = EndpointRunner.new(content: injected, path:, bare: BareDirectives.enabled?(path:, content: injected, bare:), execute:)
24
+ Rewrite.new(original: content, content: TableFormatter.rewrite(content: endpoints.rewrite), failures: endpoints.failures)
21
25
  end
22
26
 
23
- # Returns true when the file changed.
27
+ # Writes the file only when it changed.
24
28
  def update(path:, bare: false, execute: false, metadata: NoMetadata)
25
- content = Utils.read_markdown(path)
26
- rewritten = rewrite(content:, path:, bare:, execute:, metadata:)
27
- return false if rewritten == content
28
-
29
- File.write(path, rewritten)
30
- true
29
+ rewrite(content: Utils.read_markdown(path), path:, bare:, execute:, metadata:).tap do |rewrite|
30
+ File.write(path, rewrite.content) if rewrite.changed?
31
+ end
31
32
  end
32
33
  end
33
34
  end
@@ -11,7 +11,7 @@ require_relative 'filter'
11
11
 
12
12
  module Raindown
13
13
  module TUI
14
- # The blueprints from Claude Code's replies, in the project's .raindown/blueprints.md, newest first, then those in its
14
+ # The blueprints from Claude Code's replies, in the project's .raindown/data/blueprints.md, newest first, then those in its
15
15
  # code's comments, one at a time, of
16
16
  # those its Filter's keywords match, each traced to its code: the arrows move between the boxes drawn in one, or a
17
17
  # click on one, the code each box names shows below, Space or another click moving on to
@@ -22,16 +22,16 @@ module Raindown
22
22
  end
23
23
  end
24
24
 
25
- # The feelings everyone's sorted about the code as it is now, kept in .raindown/feelings.csv with the commit they were
25
+ # The feelings everyone's sorted about the code as it is now, kept in .raindown/data/feelings.csv with the commit they were
26
26
  # felt at, which a node can count, like the README's code debt. A feeling about code that's since changed, or gone, moves to
27
- # .raindown/memories.csv, so the feelings stay few, and the history's kept.
27
+ # .raindown/data/memories.csv, so the feelings stay few, and the history's kept.
28
28
  class FeelingsLog
29
29
  TEMPERS = %w[frolic numb woe dread malice].freeze
30
30
  HEADER = %i[timestamp commit checksum location method temper note].freeze
31
31
  def initialize(root:)
32
32
  @root = root
33
- @csv = File.join(root, '.raindown', 'feelings.csv')
34
- @memories = File.join(root, '.raindown', 'memories.csv')
33
+ @csv = File.join(root, '.raindown', 'data', 'feelings.csv')
34
+ @memories = File.join(root, '.raindown', 'data', 'memories.csv')
35
35
  end
36
36
 
37
37
  # Adds the feeling, as the newest, in place of one about the method before, which moves to the memories if it was
@@ -51,9 +51,9 @@ module Raindown
51
51
 
52
52
  # The files' metadata is loaded again each time, so new and renamed files are listed.
53
53
  def rewrite
54
- changed = Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path))
55
- EndpointRunner.failures(content: Utils.read_markdown(path), path:, bare:).each { warn "Endpoint check failed: #{it}" }
56
- changed
54
+ rewrite = Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path))
55
+ rewrite.failures.each { warn "Endpoint check failed: #{it}" }
56
+ rewrite.changed?
57
57
  end
58
58
  end
59
59
  end
@@ -28,10 +28,9 @@ module Raindown
28
28
 
29
29
  # Writing back saves the file again, and that save changes nothing, so it ends there.
30
30
  def update(path)
31
- changed = Rewriter.update(path:, bare: @bare, execute: @execute, metadata: Pages.for(path))
32
- puts "[#{name}] Updated #{relative(path)}" if changed
33
- EndpointRunner.failures(content: Utils.read_markdown(path), path:, bare: @bare)
34
- .each { warn "[#{name}] Endpoint check failed: #{it}" }
31
+ rewrite = Rewriter.update(path:, bare: @bare, execute: @execute, metadata: Pages.for(path))
32
+ puts "[#{name}] Updated #{relative(path)}" if rewrite.changed?
33
+ rewrite.failures.each { warn "[#{name}] Endpoint check failed: #{it}" }
35
34
  rescue StandardError => e
36
35
  raise if ENV['RAINDOWN_DEBUG']
37
36
 
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raindown
4
- VERSION = '0.11.0'
4
+ VERSION = '0.12.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raindown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi