rakit 0.1.4 → 0.1.6
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 +4 -4
- data/exe/rakit +104 -0
- data/lib/generated/data_pb.rb +1 -2
- data/lib/generated/rakit.azure_pb.rb +26 -0
- data/lib/generated/{example_pb.rb → rakit.example_pb.rb} +4 -3
- data/lib/generated/rakit.file_pb.rb +22 -0
- data/lib/generated/rakit.shell_pb.rb +22 -0
- data/lib/generated/rakit.static_web_server_pb.rb +20 -0
- data/lib/generated/rakit.word_count_pb.rb +22 -0
- data/lib/rakit/azure/dev_ops.rb +1 -1
- data/lib/rakit/cli/file.rb +163 -0
- data/lib/rakit/cli/word_count.rb +131 -0
- data/lib/rakit/file.rb +181 -0
- data/lib/rakit/gem.rb +87 -14
- data/lib/rakit/git.rb +10 -4
- data/lib/rakit/hugo.rb +43 -0
- data/lib/rakit/protobuf.rb +11 -9
- data/lib/rakit/shell.rb +1 -1
- data/lib/rakit/static_web_server.rb +226 -0
- data/lib/rakit/word_count.rb +122 -0
- data/lib/rakit.rb +5 -2
- metadata +25 -9
- data/lib/generated/azure.devops_pb.rb +0 -25
- data/lib/generated/shell_pb.rb +0 -22
- data/lib/rakit/data.rb +0 -173
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "generated/rakit.static_web_server_pb"
|
|
5
|
+
|
|
6
|
+
module Rakit
|
|
7
|
+
# Publish static sites into a configurable root (~/.rakit/wwwroot), regenerate a root index, and control a local HTTP server (start/stop/running).
|
|
8
|
+
# Host and port are configurable (host default 127.0.0.1, port 5099) and returned regardless of server state for URL building.
|
|
9
|
+
# CLI: +rakit static-web-server+ (start|stop|running|publish|view). See specs/003-static-web-server/contracts/ruby-api.md and specs/007-wwwroot-docs-hugo-view/contracts/ruby-api.md.
|
|
10
|
+
module StaticWebServer
|
|
11
|
+
SITE_NAME_REGEX = /\A[a-z0-9\-]+\z/.freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# @return [String] Static root directory (default ~/.rakit/wwwroot). Used by publish and server lifecycle.
|
|
15
|
+
attr_accessor :root
|
|
16
|
+
# @return [String] Host for building URLs (default 127.0.0.1). Returned regardless of server running state.
|
|
17
|
+
attr_accessor :host
|
|
18
|
+
# @return [Integer] Default port for start (default 5099). Used when no override passed to start; returned regardless of server running state.
|
|
19
|
+
attr_accessor :port
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
self.root = ::File.expand_path("~/.rakit/wwwroot")
|
|
23
|
+
self.host = "127.0.0.1"
|
|
24
|
+
self.port = 5099
|
|
25
|
+
|
|
26
|
+
# T005: Validate site name; raise ArgumentError before any filesystem write.
|
|
27
|
+
# Single segment (e.g. "mysite") or multi-segment path (e.g. "louparslow/rakit"); each segment must match SITE_NAME_REGEX.
|
|
28
|
+
def self.validate_site_name!(site_name)
|
|
29
|
+
validate_site_path!(site_name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.validate_site_path!(site_name)
|
|
33
|
+
return if site_name.nil? || site_name.to_s.empty?
|
|
34
|
+
site_name.to_s.split(::File::SEPARATOR).each do |seg|
|
|
35
|
+
next if seg.empty?
|
|
36
|
+
unless seg.match?(SITE_NAME_REGEX)
|
|
37
|
+
raise ArgumentError, "site_name segment must be lowercase alphanumeric and dashes only (e.g. my-site); got: #{seg.inspect}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# T006: PID file path for start/stop/running (research: ~/.rakit/static_web_server.pid).
|
|
43
|
+
def self.pid_file_path
|
|
44
|
+
::File.expand_path("~/.rakit/static_web_server.pid")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# T007: Server binary (miniserve) on PATH; used by start. Returns path or nil.
|
|
48
|
+
def self.server_binary
|
|
49
|
+
@server_binary ||= begin
|
|
50
|
+
path = ENV["RAKIT_STATIC_SERVER_BINARY"]
|
|
51
|
+
return path if path && !path.empty?
|
|
52
|
+
which("miniserve")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.server_binary=(path)
|
|
57
|
+
@server_binary = path
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# T008: Create root directory if missing; used before publish and start.
|
|
61
|
+
def self.ensure_root
|
|
62
|
+
FileUtils.mkdir_p(root)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Publish static content from source_directory to root/site_name (atomic copy), then regenerate root index.
|
|
66
|
+
# @param site_name [String] Single segment (e.g. "mysite") or path (e.g. "louparslow/rakit"); each segment must match \\A[a-z0-9\-]+\\z.
|
|
67
|
+
# @param source_directory [String] Existing directory path; contents are copied (no traversal outside allowed paths).
|
|
68
|
+
# @return [true] on success.
|
|
69
|
+
# @raise [ArgumentError] for invalid site_name, missing/invalid source_directory, or root not writable.
|
|
70
|
+
def self.publish(site_name, source_directory)
|
|
71
|
+
validate_site_path!(site_name)
|
|
72
|
+
src = ::File.expand_path(source_directory)
|
|
73
|
+
raise ArgumentError, "source_directory does not exist: #{source_directory}" unless ::File.exist?(src)
|
|
74
|
+
raise ArgumentError, "source_directory is not a directory: #{source_directory}" unless ::File.directory?(src)
|
|
75
|
+
|
|
76
|
+
ensure_root
|
|
77
|
+
unless ::File.writable?(root)
|
|
78
|
+
raise ArgumentError, "root directory is not writable: #{root}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
target = ::File.join(root, site_name)
|
|
82
|
+
temp_name = ".tmp_#{site_name.to_s.gsub(::File::SEPARATOR, '_')}_#{Process.pid}_#{rand(1_000_000)}"
|
|
83
|
+
temp = ::File.join(root, temp_name)
|
|
84
|
+
FileUtils.mkdir_p(temp)
|
|
85
|
+
FileUtils.cp_r(::File.join(src, "."), temp)
|
|
86
|
+
FileUtils.mkdir_p(::File.dirname(target))
|
|
87
|
+
FileUtils.rm_rf(target)
|
|
88
|
+
FileUtils.mv(temp, target)
|
|
89
|
+
regenerate_root_index
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Whether the server process is currently running (PID file + process check). No side effects.
|
|
94
|
+
# @return [Boolean] true if server is running, false otherwise.
|
|
95
|
+
def self.running?
|
|
96
|
+
path = pid_file_path
|
|
97
|
+
return false unless ::File.file?(path)
|
|
98
|
+
pid = ::File.read(path).strip.to_i
|
|
99
|
+
return false if pid <= 0
|
|
100
|
+
Process.getpgid(pid)
|
|
101
|
+
true
|
|
102
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Start background server serving root. Idempotent: if already running, no-op.
|
|
107
|
+
# @param options [Hash] :port (optional) override default port.
|
|
108
|
+
# @return [true] if server is running (started or already running).
|
|
109
|
+
# @raise [RuntimeError] if port in use or server binary (miniserve) not found.
|
|
110
|
+
def self.start(options = {})
|
|
111
|
+
return true if running?
|
|
112
|
+
ensure_root
|
|
113
|
+
p = options[:port] || port
|
|
114
|
+
raise "port #{p} is already in use; change port or stop the other process" if port_in_use?(p)
|
|
115
|
+
bin = server_binary
|
|
116
|
+
raise "static server binary (miniserve) not found on PATH; install miniserve or set RAKIT_STATIC_SERVER_BINARY" unless bin
|
|
117
|
+
root_path = ::File.expand_path(root)
|
|
118
|
+
pid = spawn(bin, root_path, "--port", p.to_s, out: ::File::NULL, err: ::File::NULL)
|
|
119
|
+
Process.detach(pid)
|
|
120
|
+
FileUtils.mkdir_p(::File.dirname(pid_file_path))
|
|
121
|
+
::File.write(pid_file_path, pid.to_s)
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
STOP_TIMEOUT = 5
|
|
126
|
+
|
|
127
|
+
# Gracefully terminate server process; remove PID file. Fail-fast if not running.
|
|
128
|
+
# @return [true] if stopped.
|
|
129
|
+
# @raise [RuntimeError] if server was not running (no PID file or process not found).
|
|
130
|
+
def self.stop
|
|
131
|
+
path = pid_file_path
|
|
132
|
+
raise "server is not running (no PID file at #{path})" unless ::File.file?(path)
|
|
133
|
+
pid = ::File.read(path).strip.to_i
|
|
134
|
+
raise "server is not running (invalid PID in #{path})" if pid <= 0
|
|
135
|
+
begin
|
|
136
|
+
Process.getpgid(pid)
|
|
137
|
+
rescue Errno::ESRCH
|
|
138
|
+
::File.delete(path)
|
|
139
|
+
raise "server is not running (process #{pid} not found)"
|
|
140
|
+
end
|
|
141
|
+
Process.kill(:TERM, pid)
|
|
142
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + STOP_TIMEOUT
|
|
143
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
144
|
+
begin
|
|
145
|
+
Process.wait(pid, Process::WNOHANG)
|
|
146
|
+
break
|
|
147
|
+
rescue Errno::ECHILD
|
|
148
|
+
break
|
|
149
|
+
end
|
|
150
|
+
sleep 0.1
|
|
151
|
+
end
|
|
152
|
+
begin
|
|
153
|
+
Process.getpgid(pid)
|
|
154
|
+
Process.kill(:KILL, pid)
|
|
155
|
+
Process.wait(pid)
|
|
156
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
157
|
+
# already gone
|
|
158
|
+
end
|
|
159
|
+
::File.delete(path) if ::File.file?(path)
|
|
160
|
+
true
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Open a path on the static server in the default browser. Ensures server is running (starts if not), builds URL, launches browser.
|
|
164
|
+
# @param relative_path [String] URL path (e.g. "/louparslow/rakit/" or "louparslow/rakit"); normalized to one leading slash.
|
|
165
|
+
# @return [true] on success.
|
|
166
|
+
# @raise [RuntimeError] if browser cannot be launched (e.g. headless), with message e.g. "Could not launch browser; display required?"
|
|
167
|
+
def self.view(relative_path)
|
|
168
|
+
start unless running?
|
|
169
|
+
path = relative_path.to_s.strip
|
|
170
|
+
path = "/#{path}" unless path.empty? || path.start_with?("/")
|
|
171
|
+
path = "/" if path.empty?
|
|
172
|
+
url = "http://#{host}:#{port}#{path}"
|
|
173
|
+
launch_browser(url)
|
|
174
|
+
true
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# T017: Write root/index.html listing all site subdirectories alphabetically with links.
|
|
178
|
+
def self.regenerate_root_index
|
|
179
|
+
entries = Dir.entries(root).sort.select do |e|
|
|
180
|
+
e != "." && e != ".." && e != "index.html" && !e.start_with?(".") && ::File.directory?(::File.join(root, e))
|
|
181
|
+
end
|
|
182
|
+
html = ["<!DOCTYPE html>", "<html><head><meta charset=\"utf-8\"><title>Published sites</title></head><body>", "<h1>Published sites</h1>", "<ul>"]
|
|
183
|
+
entries.each { |name| html << "<li><a href=\"/#{name}/\">#{name}</a></li>" }
|
|
184
|
+
html << "</ul></body></html>"
|
|
185
|
+
::File.write(::File.join(root, "index.html"), html.join("\n"))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
class << self
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def which(cmd)
|
|
192
|
+
exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
|
|
193
|
+
ENV["PATH"].to_s.split(::File::PATH_SEPARATOR).each do |dir|
|
|
194
|
+
next if dir.empty?
|
|
195
|
+
exts.each do |ext|
|
|
196
|
+
exe = ::File.join(dir, "#{cmd}#{ext}")
|
|
197
|
+
return exe if ::File.executable?(exe) && !::File.directory?(exe)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def port_in_use?(p)
|
|
204
|
+
require "socket"
|
|
205
|
+
TCPServer.new("127.0.0.1", p).close
|
|
206
|
+
false
|
|
207
|
+
rescue Errno::EADDRINUSE
|
|
208
|
+
true
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def launch_browser(url)
|
|
212
|
+
cmd = case ::RbConfig::CONFIG["host_os"]
|
|
213
|
+
when /darwin|mac os/i
|
|
214
|
+
["open", url]
|
|
215
|
+
when /mswin|mingw|windows/i
|
|
216
|
+
["cmd", "/c", "start", "", url]
|
|
217
|
+
else
|
|
218
|
+
["xdg-open", url]
|
|
219
|
+
end
|
|
220
|
+
system(*cmd, out: ::File::NULL, err: ::File::NULL)
|
|
221
|
+
return true if $?.success?
|
|
222
|
+
raise "Could not launch browser; display required?"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "set"
|
|
5
|
+
require "generated/rakit.word_count_pb"
|
|
6
|
+
|
|
7
|
+
module Rakit
|
|
8
|
+
# Schema-driven token frequency counting over JSON content (keys or values).
|
|
9
|
+
# CLI: +rakit word-count+ (--json-keys). See specs/004-word-count/contracts/ruby-api.md.
|
|
10
|
+
module WordCount
|
|
11
|
+
class << self
|
|
12
|
+
# @param request [Rakit::Generated::WordCountRequest]
|
|
13
|
+
# @return [Rakit::Generated::WordCountResult]
|
|
14
|
+
def count(request)
|
|
15
|
+
json_str = resolve_json_input(request)
|
|
16
|
+
return json_str if json_str.is_a?(Rakit::Generated::WordCountResult)
|
|
17
|
+
|
|
18
|
+
data = JSON.parse(json_str)
|
|
19
|
+
config = request.config || Rakit::Generated::WordCountConfig.new
|
|
20
|
+
keys = extract_keys(data)
|
|
21
|
+
tokens = keys.flat_map { |k| normalize_and_split(k, config) }
|
|
22
|
+
counts = filter_count_sort(tokens, config)
|
|
23
|
+
total = tokens.size
|
|
24
|
+
unique = counts.size
|
|
25
|
+
|
|
26
|
+
Rakit::Generated::WordCountResult.new(
|
|
27
|
+
success: true,
|
|
28
|
+
message: "",
|
|
29
|
+
counts: counts.map { |tok, cnt| Rakit::Generated::TokenCount.new(token: tok, count: cnt) },
|
|
30
|
+
exit_code: 0,
|
|
31
|
+
stderr: "",
|
|
32
|
+
total_tokens: total,
|
|
33
|
+
unique_tokens: unique
|
|
34
|
+
)
|
|
35
|
+
rescue JSON::ParserError => e
|
|
36
|
+
Rakit::Generated::WordCountResult.new(
|
|
37
|
+
success: false,
|
|
38
|
+
message: e.message,
|
|
39
|
+
exit_code: 1,
|
|
40
|
+
stderr: e.message
|
|
41
|
+
)
|
|
42
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
43
|
+
Rakit::Generated::WordCountResult.new(
|
|
44
|
+
success: false,
|
|
45
|
+
message: e.message,
|
|
46
|
+
exit_code: 1,
|
|
47
|
+
stderr: e.message
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# T004: Recursive key extraction from JSON structure. Returns array of key strings (duplicates preserved).
|
|
52
|
+
def extract_keys(obj)
|
|
53
|
+
case obj
|
|
54
|
+
when Hash
|
|
55
|
+
obj.keys.map(&:to_s) + obj.values.flat_map { |v| extract_keys(v) }
|
|
56
|
+
when Array
|
|
57
|
+
obj.flat_map { |v| extract_keys(v) }
|
|
58
|
+
else
|
|
59
|
+
[]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# T005: Normalize and split one key into tokens. Pipeline: case -> snake/kebab -> camelCase.
|
|
64
|
+
def normalize_and_split(token_string, config)
|
|
65
|
+
return [] if token_string.nil? || !token_string.is_a?(String)
|
|
66
|
+
|
|
67
|
+
s = token_string.dup
|
|
68
|
+
s = s.downcase if config.case_insensitive
|
|
69
|
+
parts = [s]
|
|
70
|
+
parts = parts.flat_map { |p| p.split(/_|-/) } if config.split_snake_kebab
|
|
71
|
+
parts = parts.flat_map { |p| p.split(/(?=[A-Z])/).reject(&:empty?) } if config.split_camel_case
|
|
72
|
+
parts.map(&:downcase).reject(&:empty?)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# T006: Filter (min_length, stopwords), count, sort (count desc, token asc), top_n.
|
|
76
|
+
def filter_count_sort(tokens, config)
|
|
77
|
+
min_len = config.min_token_length || 0
|
|
78
|
+
stopwords = config.stopwords || []
|
|
79
|
+
normalized_stop = config.case_insensitive ? stopwords.map(&:downcase).to_set : stopwords.to_set
|
|
80
|
+
|
|
81
|
+
filtered = tokens.select do |t|
|
|
82
|
+
compare_t = config.case_insensitive ? t.downcase : t
|
|
83
|
+
t.length >= min_len && !normalized_stop.include?(compare_t)
|
|
84
|
+
end
|
|
85
|
+
freq = filtered.tally
|
|
86
|
+
sorted = freq.sort_by { |token, count| [-count, token] }
|
|
87
|
+
top_n = config.top_n || 0
|
|
88
|
+
top_n.positive? ? sorted.take(top_n) : sorted
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def resolve_json_input(request)
|
|
94
|
+
json = request.json
|
|
95
|
+
return error_result("No JSON source", 1) unless json
|
|
96
|
+
|
|
97
|
+
if json.json_file && !json.json_file.empty?
|
|
98
|
+
path = json.json_file
|
|
99
|
+
return error_result("File not found: #{path}", 1) unless ::File.file?(path)
|
|
100
|
+
return error_result("File not readable: #{path}", 1) unless ::File.readable?(path)
|
|
101
|
+
return ::File.read(path)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if json.inline_json && !json.inline_json.empty?
|
|
105
|
+
return json.inline_json
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# stdin: caller must set inline_json with the read content (per contracts/ruby-api.md)
|
|
109
|
+
error_result("No JSON source (set json_file or inline_json)", 1)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def error_result(message, exit_code)
|
|
113
|
+
Rakit::Generated::WordCountResult.new(
|
|
114
|
+
success: false,
|
|
115
|
+
message: message,
|
|
116
|
+
exit_code: exit_code,
|
|
117
|
+
stderr: message
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/rakit.rb
CHANGED
|
@@ -22,9 +22,12 @@ require_relative "rakit/gem"
|
|
|
22
22
|
require_relative "rakit/git"
|
|
23
23
|
require_relative "rakit/task"
|
|
24
24
|
require_relative "rakit/protobuf"
|
|
25
|
-
# Defer loading so rake tasks that don't need Shell
|
|
25
|
+
# Defer loading so rake tasks that don't need Shell (e.g. clobber) work without google-protobuf.
|
|
26
26
|
autoload :Shell, "rakit/shell"
|
|
27
|
-
|
|
27
|
+
require_relative "rakit/static_web_server"
|
|
28
|
+
require_relative "rakit/hugo"
|
|
29
|
+
require_relative "rakit/word_count"
|
|
30
|
+
require_relative "rakit/file"
|
|
28
31
|
require_relative "rakit/azure/dev_ops"
|
|
29
32
|
|
|
30
33
|
def run(cmd)
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rakit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rakit
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -41,16 +41,22 @@ dependencies:
|
|
|
41
41
|
name: google-protobuf
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- - "
|
|
44
|
+
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
46
|
version: '3.25'
|
|
47
|
+
- - "<"
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '6'
|
|
47
50
|
type: :runtime
|
|
48
51
|
prerelease: false
|
|
49
52
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
53
|
requirements:
|
|
51
|
-
- - "
|
|
54
|
+
- - ">="
|
|
52
55
|
- !ruby/object:Gem::Version
|
|
53
56
|
version: '3.25'
|
|
57
|
+
- - "<"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '6'
|
|
54
60
|
- !ruby/object:Gem::Dependency
|
|
55
61
|
name: rubyzip
|
|
56
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,22 +99,32 @@ dependencies:
|
|
|
93
99
|
- - "~>"
|
|
94
100
|
- !ruby/object:Gem::Version
|
|
95
101
|
version: '5.0'
|
|
96
|
-
executables:
|
|
102
|
+
executables:
|
|
103
|
+
- rakit
|
|
97
104
|
extensions: []
|
|
98
105
|
extra_rdoc_files: []
|
|
99
106
|
files:
|
|
100
|
-
-
|
|
107
|
+
- exe/rakit
|
|
101
108
|
- lib/generated/data_pb.rb
|
|
102
|
-
- lib/generated/
|
|
103
|
-
- lib/generated/
|
|
109
|
+
- lib/generated/rakit.azure_pb.rb
|
|
110
|
+
- lib/generated/rakit.example_pb.rb
|
|
111
|
+
- lib/generated/rakit.file_pb.rb
|
|
112
|
+
- lib/generated/rakit.shell_pb.rb
|
|
113
|
+
- lib/generated/rakit.static_web_server_pb.rb
|
|
114
|
+
- lib/generated/rakit.word_count_pb.rb
|
|
104
115
|
- lib/rakit.rb
|
|
105
116
|
- lib/rakit/azure/dev_ops.rb
|
|
106
|
-
- lib/rakit/
|
|
117
|
+
- lib/rakit/cli/file.rb
|
|
118
|
+
- lib/rakit/cli/word_count.rb
|
|
119
|
+
- lib/rakit/file.rb
|
|
107
120
|
- lib/rakit/gem.rb
|
|
108
121
|
- lib/rakit/git.rb
|
|
122
|
+
- lib/rakit/hugo.rb
|
|
109
123
|
- lib/rakit/protobuf.rb
|
|
110
124
|
- lib/rakit/shell.rb
|
|
125
|
+
- lib/rakit/static_web_server.rb
|
|
111
126
|
- lib/rakit/task.rb
|
|
127
|
+
- lib/rakit/word_count.rb
|
|
112
128
|
homepage: https://gitlab.com/gems/rakit
|
|
113
129
|
licenses:
|
|
114
130
|
- MIT
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
-
# source: azure.devops.proto
|
|
4
|
-
|
|
5
|
-
require "google/protobuf"
|
|
6
|
-
|
|
7
|
-
descriptor_data = "\n\x12\x61zure.devops.proto\x12\x0brakit.azure\"L\n\x08Pipeline\x12\x0b\n\x03org\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0bpipeline_id\x18\x03 \x01(\x05\x12\r\n\x05token\x18\x04 \x01(\t\"R\n\x18GetPipelineResultRequest\x12\'\n\x08pipeline\x18\x01 \x01(\x0b\x32\x15.rakit.azure.Pipeline\x12\r\n\x05token\x18\x02 \x01(\t\"C\n\x0ePipelineStatus\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x03(\t\x12\x10\n\x08warnings\x18\x03 \x03(\t\"8\n\x0ePipelineResult\x12&\n\x04runs\x18\x01 \x03(\x0b\x32\x18.rakit.azure.PipelineRun\"\x97\x01\n\x0bPipelineRun\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\t\x12\x0e\n\x06result\x18\x04 \x01(\t\x12\x14\n\x0c\x63reated_date\x18\x05 \x01(\t\x12\x15\n\rfinished_date\x18\x06 \x01(\t\x12\"\n\x06stages\x18\x07 \x03(\x0b\x32\x12.rakit.azure.Stage\"&\n\x05Issue\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\"G\n\x03Job\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\"\n\x06issues\x18\x03 \x03(\x0b\x32\x12.rakit.azure.Issue\"i\n\x05Stage\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\x1e\n\x04jobs\x18\x03 \x03(\x0b\x32\x10.rakit.azure.Job\x12\"\n\x06issues\x18\x04 \x03(\x0b\x32\x12.rakit.azure.Issue\"`\n\x0eTimelineRecord\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0e\n\x06result\x18\x03 \x01(\t\x12\"\n\x06issues\x18\x04 \x03(\x0b\x32\x12.rakit.azure.Issue\"\xa8\x01\n\x14PipelineResultDetail\x12\x12\n\nsuccessful\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x01(\t\x12\x10\n\x08warnings\x18\x03 \x01(\t\x12%\n\x03run\x18\x04 \x01(\x0b\x32\x18.rakit.azure.PipelineRun\x12\x33\n\x0e\x66\x61iled_records\x18\x05 \x03(\x0b\x32\x1b.rakit.azure.TimelineRecord2i\n\x0ePipelineServer\x12W\n\x11GetPipelineResult\x12%.rakit.azure.GetPipelineResultRequest\x1a\x1b.rakit.azure.PipelineResultB\x0f\xea\x02\x0cRakit::Azureb\x06proto3"
|
|
8
|
-
|
|
9
|
-
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
10
|
-
pool.add_serialized_file(descriptor_data)
|
|
11
|
-
|
|
12
|
-
module Rakit
|
|
13
|
-
module Azure
|
|
14
|
-
Pipeline = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Pipeline").msgclass
|
|
15
|
-
GetPipelineResultRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.GetPipelineResultRequest").msgclass
|
|
16
|
-
PipelineStatus = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineStatus").msgclass
|
|
17
|
-
PipelineResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineResult").msgclass
|
|
18
|
-
PipelineRun = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineRun").msgclass
|
|
19
|
-
Issue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Issue").msgclass
|
|
20
|
-
Job = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Job").msgclass
|
|
21
|
-
Stage = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Stage").msgclass
|
|
22
|
-
TimelineRecord = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.TimelineRecord").msgclass
|
|
23
|
-
PipelineResultDetail = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineResultDetail").msgclass
|
|
24
|
-
end
|
|
25
|
-
end
|
data/lib/generated/shell_pb.rb
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
-
# source: shell.proto
|
|
4
|
-
|
|
5
|
-
require 'google/protobuf'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
descriptor_data = "\n\x0bshell.proto\x12\x0brakit.shell\"\x9a\x02\n\x07\x43ommand\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x02 \x03(\t\x12\x19\n\x11working_directory\x18\x03 \x01(\t\x12\x17\n\x0ftimeout_seconds\x18\x04 \x01(\x05\x12\x1a\n\x12\x65xpected_exit_code\x18\x05 \x01(\x05\x12\x17\n\x0f\x65xpected_stdout\x18\x06 \x01(\t\x12\x17\n\x0f\x65xpected_stderr\x18\x07 \x01(\t\x12<\n\x13\x61\x63\x63\x65ptance_criteria\x18\x08 \x03(\x0b\x32\x1f.rakit.shell.AcceptanceCriteria\x12\x13\n\x0b\x65xit_status\x18\t \x01(\x05\x12\x0e\n\x06stdout\x18\n \x01(\t\x12\x0e\n\x06stderr\x18\x0b \x01(\t\"1\n\x12\x41\x63\x63\x65ptanceCriteria\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"-\n\nTestResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x03(\t\"b\n\rFormatRequest\x12%\n\x07\x63ommand\x18\x01 \x01(\x0b\x32\x14.rakit.shell.Command\x12*\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x1a.rakit.shell.CommandFormat\" \n\x0e\x46ormatResponse\x12\x0e\n\x06output\x18\x01 \x01(\t*Z\n\rCommandFormat\x12\x1e\n\x1a\x43OMMAND_FORMAT_UNSPECIFIED\x10\x00\x12\x0c\n\x08ONE_LINE\x10\x01\x12\x0e\n\nMULTI_LINE\x10\x02\x12\x0b\n\x07\x43OMPACT\x10\x03\x32\xc1\x01\n\x0e\x43ommandService\x12\x35\n\x07\x45xecute\x12\x14.rakit.shell.Command\x1a\x14.rakit.shell.Command\x12\x35\n\x04Test\x12\x14.rakit.shell.Command\x1a\x17.rakit.shell.TestResult\x12\x41\n\x06\x46ormat\x12\x1a.rakit.shell.FormatRequest\x1a\x1b.rakit.shell.FormatResponseB\x0f\xea\x02\x0cRakit::Shellb\x06proto3"
|
|
9
|
-
|
|
10
|
-
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
-
pool.add_serialized_file(descriptor_data)
|
|
12
|
-
|
|
13
|
-
module Rakit
|
|
14
|
-
module Shell
|
|
15
|
-
Command = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.Command").msgclass
|
|
16
|
-
AcceptanceCriteria = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.AcceptanceCriteria").msgclass
|
|
17
|
-
TestResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.TestResult").msgclass
|
|
18
|
-
FormatRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.FormatRequest").msgclass
|
|
19
|
-
FormatResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.FormatResponse").msgclass
|
|
20
|
-
CommandFormat = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.CommandFormat").enummodule
|
|
21
|
-
end
|
|
22
|
-
end
|
data/lib/rakit/data.rb
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
|
-
require "generated/data_pb"
|
|
6
|
-
|
|
7
|
-
module Rakit
|
|
8
|
-
module Data
|
|
9
|
-
# Export all stored messages from DataService.data_dir to export_dir in the given format.
|
|
10
|
-
# If there are no .pb files under data_dir, no files are created.
|
|
11
|
-
#
|
|
12
|
-
# @param export_dir [String] target directory (created if needed)
|
|
13
|
-
# @param export_format [Rakit::Data::ExportFormat] PROTOBUF_BINARY_FILES (mirror .pb layout),
|
|
14
|
-
# PROTOBUF_JSON_FILES (same layout with .json), or PROTOBUF_BINARY_ZIPPED (single data.zip)
|
|
15
|
-
# @return [void]
|
|
16
|
-
# @raise [ArgumentError] if export_format is not a supported ExportFormat value
|
|
17
|
-
def self.export(export_dir, export_format)
|
|
18
|
-
base = DataService.data_dir
|
|
19
|
-
export_dir = File.expand_path(export_dir)
|
|
20
|
-
FileUtils.mkdir_p(export_dir)
|
|
21
|
-
|
|
22
|
-
pb_paths = Dir.glob(File.join(base, "**", "*.pb"))
|
|
23
|
-
return if pb_paths.empty?
|
|
24
|
-
|
|
25
|
-
case export_format
|
|
26
|
-
when ExportFormat::PROTOBUF_BINARY_FILES
|
|
27
|
-
_export_binary_files(pb_paths, base, export_dir)
|
|
28
|
-
when ExportFormat::PROTOBUF_JSON_FILES
|
|
29
|
-
_export_json_files(pb_paths, base, export_dir)
|
|
30
|
-
when ExportFormat::PROTOBUF_BINARY_ZIPPED
|
|
31
|
-
_export_binary_zipped(pb_paths, base, export_dir)
|
|
32
|
-
else
|
|
33
|
-
raise ArgumentError, "unsupported export_format: #{export_format.inspect}"
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def self._rel_parts(base, path)
|
|
38
|
-
rel = path[(base.end_with?(File::SEPARATOR) ? base.length : base.length + 1)..]
|
|
39
|
-
parts = rel.split(File::SEPARATOR)
|
|
40
|
-
type_name = parts[0..-2].join("::")
|
|
41
|
-
unique_name = File.basename(parts[-1], ".pb")
|
|
42
|
-
[type_name, unique_name, rel]
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def self._export_binary_files(pb_paths, base, export_dir)
|
|
46
|
-
pb_paths.each do |path|
|
|
47
|
-
type_name, _unique_name, rel = _rel_parts(base, path)
|
|
48
|
-
message = DataService.load(type_name, File.basename(path, ".pb"))
|
|
49
|
-
out_path = File.join(export_dir, rel)
|
|
50
|
-
FileUtils.mkdir_p(File.dirname(out_path))
|
|
51
|
-
File.binwrite(out_path, message.class.encode(message))
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def self._export_json_files(pb_paths, base, export_dir)
|
|
56
|
-
pb_paths.each do |path|
|
|
57
|
-
type_name, unique_name, rel = _rel_parts(base, path)
|
|
58
|
-
message = DataService.load(type_name, unique_name)
|
|
59
|
-
out_rel = rel.sub(/\.pb\z/, ".json")
|
|
60
|
-
out_path = File.join(export_dir, out_rel)
|
|
61
|
-
FileUtils.mkdir_p(File.dirname(out_path))
|
|
62
|
-
File.write(out_path, message.class.encode_json(message))
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def self._export_binary_zipped(pb_paths, base, export_dir)
|
|
67
|
-
require "zip"
|
|
68
|
-
zip_path = File.join(export_dir, "data.zip")
|
|
69
|
-
FileUtils.rm_f(zip_path)
|
|
70
|
-
Zip::File.open(zip_path, Zip::File::CREATE) do |zip|
|
|
71
|
-
pb_paths.each do |path|
|
|
72
|
-
type_name, unique_name, rel = _rel_parts(base, path)
|
|
73
|
-
message = DataService.load(type_name, unique_name)
|
|
74
|
-
zip.get_output_stream(rel) { |io| io.write(message.class.encode(message)) }
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Persist and retrieve protobuf message instances under a configurable data root.
|
|
80
|
-
#
|
|
81
|
-
# Storage layout: root is {data_dir} (default +~/.rakit/data+). Path for a message is
|
|
82
|
-
# +data_dir/TYPE_PATH/unique_name.pb+. TYPE_PATH is the Ruby class name with +::+ replaced by
|
|
83
|
-
# +File::SEPARATOR+ (e.g. +Rakit::Shell::Command+ → +Rakit/Shell/Command+). File content is
|
|
84
|
-
# binary protobuf; no character encoding.
|
|
85
|
-
module DataService
|
|
86
|
-
# @return [String] current data root (default: expanded +~/.rakit/data+)
|
|
87
|
-
def self.data_dir
|
|
88
|
-
@data_dir ||= File.expand_path("~/.rakit/data")
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# @param path [String] set the data root for subsequent operations (e.g. tests use a temp dir)
|
|
92
|
-
# @return [void]
|
|
93
|
-
def self.data_dir=(path)
|
|
94
|
-
@data_dir = path
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Store a proto message under a unique name.
|
|
98
|
-
# @param message [Object] instance of a generated protobuf message class
|
|
99
|
-
# @param unique_name [String] non-empty, must not contain path separators or +..+
|
|
100
|
-
# @return [void]
|
|
101
|
-
# @raise [ArgumentError] if unique_name is empty/blank or contains path traversal
|
|
102
|
-
# @raise [Errno::EACCES] etc. on permission failure
|
|
103
|
-
def self.store(message, unique_name)
|
|
104
|
-
_validate_unique_name!(unique_name)
|
|
105
|
-
klass = message.class
|
|
106
|
-
path = _path(klass.name, unique_name)
|
|
107
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
108
|
-
File.binwrite(path, klass.encode(message))
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Load a stored message.
|
|
112
|
-
# @param type_name [String] Ruby class name (e.g. +"Rakit::Shell::Command"+)
|
|
113
|
-
# @param unique_name [String] same rules as for store
|
|
114
|
-
# @return [Object] decoded message instance
|
|
115
|
-
# @raise [ArgumentError] if unique_name invalid
|
|
116
|
-
# @raise [NameError] if type_name is not a valid constant
|
|
117
|
-
# @raise [Errno::ENOENT] if the file does not exist
|
|
118
|
-
def self.load(type_name, unique_name)
|
|
119
|
-
_validate_unique_name!(unique_name)
|
|
120
|
-
klass = Object.const_get(type_name.to_s)
|
|
121
|
-
path = _path(klass.name, unique_name.to_s)
|
|
122
|
-
raise Errno::ENOENT, path unless File.file?(path)
|
|
123
|
-
|
|
124
|
-
klass.decode(File.binread(path))
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Remove a stored message by type and unique name (no-op if file absent).
|
|
128
|
-
# @param type_name [String] Ruby class name
|
|
129
|
-
# @param unique_name [String] same rules as for store
|
|
130
|
-
# @return [void]
|
|
131
|
-
# @raise [ArgumentError] if unique_name invalid
|
|
132
|
-
def self.remove(type_name, unique_name)
|
|
133
|
-
_validate_unique_name!(unique_name)
|
|
134
|
-
path = _path(type_name.to_s, unique_name.to_s)
|
|
135
|
-
File.delete(path) if File.file?(path)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Return unique names (without +.pb+) for the given type.
|
|
139
|
-
# @param type_name [String] Ruby class name for directory resolution
|
|
140
|
-
# @return [Array<String>] empty if directory missing or no .pb files
|
|
141
|
-
# @raise [NameError] if type_name is not a valid constant
|
|
142
|
-
# @raise [ArgumentError] if type_name yields empty path segments (e.g. from _dir_for_type)
|
|
143
|
-
def self.get_names(type_name)
|
|
144
|
-
dir = _dir_for_type(type_name.to_s)
|
|
145
|
-
return [] unless File.directory?(dir)
|
|
146
|
-
|
|
147
|
-
Dir.children(dir).select { |f| File.file?(File.join(dir, f)) && f.end_with?(".pb") }.map { |f| f.chomp(".pb") }
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def self._validate_unique_name!(unique_name)
|
|
151
|
-
u = unique_name.to_s
|
|
152
|
-
raise ArgumentError, "unique_name must be a non-empty string" if u.strip.empty?
|
|
153
|
-
if u.include?("/") || u.include?("\\") || u.include?("..")
|
|
154
|
-
raise ArgumentError, "unique_name must not contain path separators or '..'"
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def self._path(type_name, unique_name)
|
|
159
|
-
dir = _dir_for_type(type_name)
|
|
160
|
-
File.join(dir, "#{unique_name}.pb")
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# PACKAGE_PATH/MESSAGE_NAME: e.g. Rakit::Shell::Command -> Rakit/Shell/Command
|
|
164
|
-
def self._dir_for_type(type_name)
|
|
165
|
-
parts = type_name.split("::")
|
|
166
|
-
raise ArgumentError, "type_name must be a qualified constant path" if parts.empty?
|
|
167
|
-
|
|
168
|
-
relative = parts.join(File::SEPARATOR)
|
|
169
|
-
File.join(data_dir, relative)
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|