textus 0.4.0 → 0.8.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 +4 -4
- data/CHANGELOG.md +147 -2
- data/README.md +38 -28
- data/SPEC.md +84 -147
- data/docs/architecture.md +82 -28
- data/lib/textus/builder/pipeline.rb +56 -0
- data/lib/textus/builder/renderer/json.rb +42 -0
- data/lib/textus/builder/renderer/markdown.rb +22 -0
- data/lib/textus/builder/renderer/text.rb +14 -0
- data/lib/textus/builder/renderer/yaml.rb +42 -0
- data/lib/textus/builder/renderer.rb +17 -0
- data/lib/textus/builder.rb +9 -114
- data/lib/textus/cli/group/hook.rb +11 -0
- data/lib/textus/cli/group/key.rb +12 -0
- data/lib/textus/cli/group/schema.rb +13 -0
- data/lib/textus/cli/group.rb +51 -0
- data/lib/textus/cli/verb/accept.rb +15 -0
- data/lib/textus/cli/verb/build.rb +13 -0
- data/lib/textus/cli/verb/delete.rb +16 -0
- data/lib/textus/cli/verb/deps.rb +12 -0
- data/lib/textus/cli/verb/doctor.rb +15 -0
- data/lib/textus/cli/verb/get.rb +12 -0
- data/lib/textus/cli/verb/hook_run.rb +48 -0
- data/lib/textus/cli/verb/hooks.rb +50 -0
- data/lib/textus/cli/verb/init.rb +14 -0
- data/lib/textus/cli/verb/intro.rb +11 -0
- data/lib/textus/cli/verb/list.rb +14 -0
- data/lib/textus/cli/verb/migrate_keys.rb +16 -0
- data/lib/textus/cli/verb/mv.rb +17 -0
- data/lib/textus/cli/verb/published.rb +11 -0
- data/lib/textus/cli/verb/put.rb +50 -0
- data/lib/textus/cli/verb/rdeps.rb +12 -0
- data/lib/textus/cli/verb/refresh.rb +15 -0
- data/lib/textus/cli/verb/schema.rb +12 -0
- data/lib/textus/cli/verb/schema_diff.rb +12 -0
- data/lib/textus/cli/verb/schema_init.rb +16 -0
- data/lib/textus/cli/verb/schema_migrate.rb +16 -0
- data/lib/textus/cli/verb/stale.rb +14 -0
- data/lib/textus/cli/verb/uid.rb +12 -0
- data/lib/textus/cli/verb/where.rb +12 -0
- data/lib/textus/cli/verb.rb +62 -0
- data/lib/textus/cli.rb +44 -385
- data/lib/textus/doctor/check/audit_log.rb +50 -0
- data/lib/textus/doctor/check/hooks.rb +29 -0
- data/lib/textus/doctor/check/illegal_keys.rb +49 -0
- data/lib/textus/doctor/check/manifest_files.rb +38 -0
- data/lib/textus/doctor/check/schema_violations.rb +22 -0
- data/lib/textus/doctor/check/schemas.rb +26 -0
- data/lib/textus/doctor/check/sentinels.rb +57 -0
- data/lib/textus/doctor/check/templates.rb +26 -0
- data/lib/textus/doctor/check/unowned_schema_fields.rb +34 -0
- data/lib/textus/doctor/check.rb +30 -0
- data/lib/textus/doctor.rb +29 -264
- data/lib/textus/entry/base.rb +30 -0
- data/lib/textus/entry/json.rb +11 -5
- data/lib/textus/entry/markdown.rb +5 -5
- data/lib/textus/entry/text.rb +4 -4
- data/lib/textus/entry/yaml.rb +11 -5
- data/lib/textus/entry.rb +2 -7
- data/lib/textus/envelope.rb +30 -0
- data/lib/textus/errors.rb +2 -2
- data/lib/textus/hooks/builtin.rb +70 -0
- data/lib/textus/hooks/dispatcher.rb +49 -0
- data/lib/textus/hooks/loader.rb +26 -0
- data/lib/textus/hooks/registry.rb +73 -0
- data/lib/textus/init.rb +14 -11
- data/lib/textus/intro.rb +16 -18
- data/lib/textus/key/distance.rb +55 -0
- data/lib/textus/key/grammar.rb +33 -0
- data/lib/textus/key/path.rb +17 -0
- data/lib/textus/manifest/entry.rb +199 -0
- data/lib/textus/manifest.rb +20 -254
- data/lib/textus/migrate_keys.rb +1 -1
- data/lib/textus/projection.rb +6 -5
- data/lib/textus/proposal.rb +4 -4
- data/lib/textus/refresh.rb +17 -17
- data/lib/textus/schema/tools.rb +89 -0
- data/lib/textus/store/audit_log.rb +71 -0
- data/lib/textus/store/mover.rb +121 -0
- data/lib/textus/store/reader.rb +67 -0
- data/lib/textus/store/staleness.rb +133 -0
- data/lib/textus/store/validator.rb +56 -0
- data/lib/textus/store/view.rb +29 -0
- data/lib/textus/store/writer.rb +132 -0
- data/lib/textus/store.rb +26 -527
- data/lib/textus/version.rb +2 -2
- data/lib/textus.rb +14 -29
- metadata +78 -8
- data/lib/textus/audit_log.rb +0 -32
- data/lib/textus/builtin_actions.rb +0 -68
- data/lib/textus/extension_registry.rb +0 -61
- data/lib/textus/extensions.rb +0 -33
- data/lib/textus/key_distance.rb +0 -53
- data/lib/textus/schema_tools.rb +0 -87
- data/lib/textus/store_view.rb +0 -27
data/lib/textus/cli.rb
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require "optparse"
|
|
3
|
-
require "time"
|
|
4
|
-
require "timeout"
|
|
5
|
-
require "yaml"
|
|
6
3
|
|
|
7
4
|
module Textus
|
|
8
|
-
# rubocop:disable Metrics/ClassLength
|
|
9
5
|
class CLI
|
|
6
|
+
# verb name → Verb subclass. Adding a new verb is a one-line entry here
|
|
7
|
+
# plus a new file under lib/textus/cli/.
|
|
8
|
+
VERBS = {
|
|
9
|
+
"accept" => Verb::Accept,
|
|
10
|
+
"build" => Verb::Build,
|
|
11
|
+
"delete" => Verb::Delete,
|
|
12
|
+
"deps" => Verb::Deps,
|
|
13
|
+
"doctor" => Verb::Doctor,
|
|
14
|
+
"get" => Verb::Get,
|
|
15
|
+
"hook" => Group::Hook,
|
|
16
|
+
"init" => Verb::Init,
|
|
17
|
+
"intro" => Verb::Intro,
|
|
18
|
+
"key" => Group::Key,
|
|
19
|
+
"list" => Verb::List,
|
|
20
|
+
"published" => Verb::Published,
|
|
21
|
+
"put" => Verb::Put,
|
|
22
|
+
"rdeps" => Verb::Rdeps,
|
|
23
|
+
"refresh" => Verb::Refresh,
|
|
24
|
+
"schema" => Group::Schema,
|
|
25
|
+
"stale" => Verb::Stale,
|
|
26
|
+
"where" => Verb::Where,
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
10
29
|
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, cwd: Dir.pwd)
|
|
11
30
|
new(stdin: stdin, stdout: stdout, stderr: stderr, cwd: cwd).run(argv)
|
|
12
31
|
end
|
|
@@ -19,44 +38,19 @@ module Textus
|
|
|
19
38
|
@root_arg = nil
|
|
20
39
|
end
|
|
21
40
|
|
|
22
|
-
def run(argv)
|
|
23
|
-
OptionParser.new
|
|
24
|
-
o.on("--root=PATH") { |v| @root_arg = v }
|
|
25
|
-
end.order!(argv)
|
|
41
|
+
def run(argv)
|
|
42
|
+
OptionParser.new { |o| o.on("--root=PATH") { |v| @root_arg = v } }.order!(argv)
|
|
26
43
|
verb = argv.shift
|
|
27
44
|
raise UsageError.new("missing verb") if verb.nil?
|
|
28
45
|
|
|
29
46
|
case verb
|
|
30
|
-
when "list" then verb_list(argv)
|
|
31
|
-
when "where" then verb_where(argv)
|
|
32
|
-
when "get" then verb_get(argv)
|
|
33
|
-
when "put" then verb_put(argv)
|
|
34
|
-
when "schema" then verb_schema(argv)
|
|
35
|
-
when "stale" then verb_stale(argv)
|
|
36
|
-
when "delete" then verb_delete(argv)
|
|
37
|
-
when "validate-all" then verb_validate_all(argv)
|
|
38
|
-
when "build" then verb_build(argv)
|
|
39
|
-
when "deps" then verb_deps(argv)
|
|
40
|
-
when "rdeps" then verb_rdeps(argv)
|
|
41
|
-
when "published" then verb_published(argv)
|
|
42
|
-
when "accept" then verb_accept(argv)
|
|
43
|
-
when "init" then verb_init(argv)
|
|
44
|
-
when "schema-init" then verb_schema_init(argv)
|
|
45
|
-
when "schema-diff" then verb_schema_diff(argv)
|
|
46
|
-
when "schema-migrate" then verb_schema_migrate(argv)
|
|
47
|
-
when "action" then verb_action(argv)
|
|
48
|
-
when "refresh" then verb_refresh(argv)
|
|
49
|
-
when "extensions" then verb_extensions(argv)
|
|
50
|
-
when "migrate-keys" then verb_migrate_keys(argv)
|
|
51
|
-
when "mv" then verb_mv(argv)
|
|
52
|
-
when "uid" then verb_uid(argv)
|
|
53
|
-
when "doctor" then verb_doctor(argv)
|
|
54
|
-
when "intro" then verb_intro(argv)
|
|
55
47
|
when "--version", "-v" then @stdout.puts(VERSION)
|
|
56
48
|
0
|
|
57
49
|
when "--help", "-h" then print_help
|
|
58
50
|
0
|
|
59
|
-
else
|
|
51
|
+
else
|
|
52
|
+
klass = VERBS[verb] or raise UsageError.new("unknown verb: #{verb}")
|
|
53
|
+
dispatch(klass, argv)
|
|
60
54
|
end
|
|
61
55
|
rescue Textus::Error => e
|
|
62
56
|
emit_error(e)
|
|
@@ -68,348 +62,10 @@ module Textus
|
|
|
68
62
|
@store ||= Store.discover(@cwd, root: @root_arg)
|
|
69
63
|
end
|
|
70
64
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end.permute!(argv)
|
|
76
|
-
raise UsageError.new("only --format=json is supported in v1") unless fmt == "json"
|
|
77
|
-
|
|
78
|
-
fmt
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def parse_prefix!(argv)
|
|
82
|
-
prefix = nil
|
|
83
|
-
OptionParser.new do |o|
|
|
84
|
-
o.on("--prefix=KEY") { |v| prefix = v }
|
|
85
|
-
o.on("--zone=Z") {}
|
|
86
|
-
o.on("--format=FMT") {}
|
|
87
|
-
end.permute!(argv)
|
|
88
|
-
prefix
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def parse_prefix_and_zone!(argv)
|
|
92
|
-
prefix = nil
|
|
93
|
-
zone = nil
|
|
94
|
-
fmt = "text"
|
|
95
|
-
OptionParser.new do |o|
|
|
96
|
-
o.on("--prefix=KEY") { |v| prefix = v }
|
|
97
|
-
o.on("--zone=Z") { |v| zone = v }
|
|
98
|
-
o.on("--format=FMT") { |v| fmt = v }
|
|
99
|
-
end.permute!(argv)
|
|
100
|
-
raise UsageError.new("only --format=json is supported in v1") unless fmt == "json"
|
|
101
|
-
|
|
102
|
-
[prefix, zone]
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def verb_list(argv)
|
|
106
|
-
prefix, zone = parse_prefix_and_zone!(argv)
|
|
107
|
-
emit({ "protocol" => PROTOCOL, "entries" => store.list(prefix: prefix, zone: zone) })
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def verb_where(argv)
|
|
111
|
-
key = argv.shift or raise UsageError.new("where requires a key")
|
|
112
|
-
parse_format!(argv)
|
|
113
|
-
emit(store.where(key))
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def verb_get(argv)
|
|
117
|
-
key = argv.shift or raise UsageError.new("get requires a key")
|
|
118
|
-
parse_format!(argv)
|
|
119
|
-
emit(store.get(key))
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def verb_schema(argv)
|
|
123
|
-
key = argv.shift or raise UsageError.new("schema requires a key")
|
|
124
|
-
parse_format!(argv)
|
|
125
|
-
emit(store.schema_envelope(key))
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def verb_stale(argv)
|
|
129
|
-
prefix, zone = parse_prefix_and_zone!(argv)
|
|
130
|
-
emit(store.stale(prefix: prefix, zone: zone))
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def verb_put(argv) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
134
|
-
key = argv.shift or raise UsageError.new("put requires a key")
|
|
135
|
-
as_flag = nil
|
|
136
|
-
use_stdin = false
|
|
137
|
-
action_name = nil
|
|
138
|
-
OptionParser.new do |o|
|
|
139
|
-
o.on("--stdin") { use_stdin = true }
|
|
140
|
-
o.on("--as=ROLE") { |v| as_flag = v }
|
|
141
|
-
o.on("--action=NAME") { |v| action_name = v }
|
|
142
|
-
o.on("--format=FMT") {}
|
|
143
|
-
end.permute!(argv)
|
|
144
|
-
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
145
|
-
|
|
146
|
-
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
147
|
-
|
|
148
|
-
raw = @stdin.read
|
|
149
|
-
payload =
|
|
150
|
-
if action_name
|
|
151
|
-
callable = store.registry.action(action_name)
|
|
152
|
-
result =
|
|
153
|
-
begin
|
|
154
|
-
Timeout.timeout(Textus::Refresh::ACTION_TIMEOUT_SECONDS) do
|
|
155
|
-
callable.call(config: { "bytes" => raw }, store: Textus::StoreView.new(store), args: {})
|
|
156
|
-
end
|
|
157
|
-
rescue Timeout::Error
|
|
158
|
-
raise UsageError.new(
|
|
159
|
-
"action '#{action_name}' exceeded #{Textus::Refresh::ACTION_TIMEOUT_SECONDS}s timeout",
|
|
160
|
-
)
|
|
161
|
-
end
|
|
162
|
-
basename = key.split(".").last
|
|
163
|
-
{
|
|
164
|
-
"frontmatter" => {
|
|
165
|
-
"name" => basename,
|
|
166
|
-
"last_refreshed_at" => Time.now.utc.iso8601,
|
|
167
|
-
"actioned_with" => action_name,
|
|
168
|
-
}.merge(result[:frontmatter] || result["frontmatter"] || {}),
|
|
169
|
-
"body" => result[:body] || result["body"] || "",
|
|
170
|
-
}
|
|
171
|
-
else
|
|
172
|
-
JSON.parse(raw)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
fm = payload["frontmatter"] || {}
|
|
176
|
-
body = payload["body"] || ""
|
|
177
|
-
if_etag = payload["if_etag"]
|
|
178
|
-
emit(store.put(key, frontmatter: fm, body: body, if_etag: if_etag, as: role))
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def verb_delete(argv)
|
|
182
|
-
key = argv.shift or raise UsageError.new("delete requires a key")
|
|
183
|
-
as_flag = nil
|
|
184
|
-
if_etag = nil
|
|
185
|
-
OptionParser.new do |o|
|
|
186
|
-
o.on("--as=ROLE") { |v| as_flag = v }
|
|
187
|
-
o.on("--if-etag=E") { |v| if_etag = v }
|
|
188
|
-
o.on("--format=FMT") {}
|
|
189
|
-
end.permute!(argv)
|
|
190
|
-
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
191
|
-
emit(store.delete(key, if_etag: if_etag, as: role))
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def verb_validate_all(argv)
|
|
195
|
-
parse_format!(argv)
|
|
196
|
-
res = store.validate_all
|
|
197
|
-
@stdout.puts(JSON.generate(res))
|
|
198
|
-
res["ok"] ? 0 : 1
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def verb_build(argv)
|
|
202
|
-
prefix = nil
|
|
203
|
-
OptionParser.new do |o|
|
|
204
|
-
o.on("--prefix=K") { |v| prefix = v }
|
|
205
|
-
o.on("--format=FMT") {}
|
|
206
|
-
end.permute!(argv)
|
|
207
|
-
res = Textus::Builder.new(store).build(prefix: prefix)
|
|
208
|
-
@stdout.puts(JSON.generate(res))
|
|
209
|
-
0
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def verb_deps(argv)
|
|
213
|
-
key = argv.shift or raise UsageError.new("deps requires a key")
|
|
214
|
-
parse_format!(argv)
|
|
215
|
-
emit({ "protocol" => Textus::PROTOCOL, "key" => key, "deps" => store.deps(key) })
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def verb_rdeps(argv)
|
|
219
|
-
key = argv.shift or raise UsageError.new("rdeps requires a key")
|
|
220
|
-
parse_format!(argv)
|
|
221
|
-
emit({ "protocol" => Textus::PROTOCOL, "key" => key, "rdeps" => store.rdeps(key) })
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def verb_schema_init(argv)
|
|
225
|
-
name = argv.shift or raise UsageError.new("schema-init NAME")
|
|
226
|
-
from_key = nil
|
|
227
|
-
OptionParser.new do |o|
|
|
228
|
-
o.on("--from=KEY") { |v| from_key = v }
|
|
229
|
-
o.on("--format=FMT") {}
|
|
230
|
-
end.permute!(argv)
|
|
231
|
-
raise UsageError.new("schema-init requires --from=KEY") unless from_key
|
|
232
|
-
|
|
233
|
-
emit(Textus::SchemaTools.init(store, name: name, from: from_key))
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def verb_schema_diff(argv)
|
|
237
|
-
name = argv.shift or raise UsageError.new("schema-diff NAME")
|
|
238
|
-
parse_format!(argv)
|
|
239
|
-
emit(Textus::SchemaTools.diff(store, name: name))
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def verb_schema_migrate(argv)
|
|
243
|
-
name = argv.shift or raise UsageError.new("schema-migrate NAME")
|
|
244
|
-
rename = nil
|
|
245
|
-
OptionParser.new do |o|
|
|
246
|
-
o.on("--rename=O:N") { |v| rename = v }
|
|
247
|
-
o.on("--format=FMT") {}
|
|
248
|
-
end.permute!(argv)
|
|
249
|
-
raise UsageError.new("schema-migrate requires --rename=OLD:NEW") unless rename
|
|
250
|
-
|
|
251
|
-
emit(Textus::SchemaTools.migrate(store, name: name, rename: rename))
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def verb_init(argv)
|
|
255
|
-
OptionParser.new do |o|
|
|
256
|
-
o.on("--format=FMT") {}
|
|
257
|
-
end.permute!(argv)
|
|
258
|
-
target = File.join(@cwd, ".textus")
|
|
259
|
-
res = Textus::Init.run(target)
|
|
260
|
-
@stdout.puts(JSON.generate(res))
|
|
261
|
-
0
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def verb_accept(argv)
|
|
265
|
-
key = argv.shift or raise UsageError.new("accept requires a key")
|
|
266
|
-
as_flag = nil
|
|
267
|
-
OptionParser.new do |o|
|
|
268
|
-
o.on("--as=ROLE") { |v| as_flag = v }
|
|
269
|
-
o.on("--format=FMT") {}
|
|
270
|
-
end.permute!(argv)
|
|
271
|
-
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
272
|
-
emit(store.accept(key, as: role))
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def verb_action(argv)
|
|
276
|
-
name = argv.shift
|
|
277
|
-
raise UsageError.new("action requires a name") if name.nil?
|
|
278
|
-
|
|
279
|
-
as_flag = nil
|
|
280
|
-
args = {}
|
|
281
|
-
argv.each do |tok|
|
|
282
|
-
case tok
|
|
283
|
-
when /\A--as=(.+)\z/ then as_flag = ::Regexp.last_match(1)
|
|
284
|
-
when /\A--format=/ then next
|
|
285
|
-
when /\A--([\w-]+)=(.*)\z/ then args[::Regexp.last_match(1)] = ::Regexp.last_match(2)
|
|
286
|
-
else
|
|
287
|
-
raise UsageError.new("unknown arg to 'action #{name}': #{tok}")
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
292
|
-
callable = store.registry.action(name)
|
|
293
|
-
view = StoreView.new(store, writable: true, as: role)
|
|
294
|
-
|
|
295
|
-
begin
|
|
296
|
-
Timeout.timeout(Textus::Refresh::ACTION_TIMEOUT_SECONDS) do
|
|
297
|
-
callable.call(config: {}, store: view, args: args)
|
|
298
|
-
end
|
|
299
|
-
rescue Timeout::Error
|
|
300
|
-
raise UsageError.new(
|
|
301
|
-
"action '#{name}' exceeded #{Textus::Refresh::ACTION_TIMEOUT_SECONDS}s timeout",
|
|
302
|
-
)
|
|
303
|
-
rescue Textus::Error
|
|
304
|
-
raise
|
|
305
|
-
rescue StandardError => e
|
|
306
|
-
raise UsageError.new("action '#{name}' raised: #{e.class}: #{e.message}")
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
emit({ "protocol" => Textus::PROTOCOL, "action" => name, "ok" => true })
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def verb_refresh(argv)
|
|
313
|
-
key = argv.shift or raise UsageError.new("refresh requires a key")
|
|
314
|
-
as_flag = nil
|
|
315
|
-
OptionParser.new do |o|
|
|
316
|
-
o.on("--as=ROLE") { |v| as_flag = v }
|
|
317
|
-
o.on("--format=FMT") {}
|
|
318
|
-
end.permute!(argv)
|
|
319
|
-
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
320
|
-
emit(Textus::Refresh.call(store, key, as: role))
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
def verb_extensions(argv) # rubocop:disable Metrics/AbcSize
|
|
324
|
-
subcommand = argv.shift
|
|
325
|
-
raise UsageError.new("extensions requires 'list'") unless subcommand == "list"
|
|
326
|
-
|
|
327
|
-
kind = nil
|
|
328
|
-
OptionParser.new do |o|
|
|
329
|
-
o.on("--kind=K") { |v| kind = v }
|
|
330
|
-
o.on("--format=FMT") {}
|
|
331
|
-
end.permute!(argv)
|
|
332
|
-
|
|
333
|
-
rows = []
|
|
334
|
-
rows += store.registry.action_names.map { |n| { "kind" => "action", "name" => n.to_s } }
|
|
335
|
-
rows += store.registry.doctor_check_names.map { |n| { "kind" => "doctor_check", "name" => n.to_s } }
|
|
336
|
-
rows += store.registry.reducer_names.map { |n| { "kind" => "reducer", "name" => n.to_s } }
|
|
337
|
-
store.registry.hook_events.each do |evt|
|
|
338
|
-
store.registry.hooks(evt).each do |h|
|
|
339
|
-
rows << { "kind" => "hook", "event" => evt.to_s, "name" => h[:name].to_s }
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
store.manifest.entries.each do |e|
|
|
343
|
-
e.events.each do |evt, defs|
|
|
344
|
-
Array(defs).each do |defn|
|
|
345
|
-
next unless defn["exec"]
|
|
346
|
-
|
|
347
|
-
rows << {
|
|
348
|
-
"kind" => "hook", "event" => evt.to_s, "exec" => defn["exec"],
|
|
349
|
-
"key" => e.key, "as" => defn["as"] || "script"
|
|
350
|
-
}
|
|
351
|
-
end
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
rows.select! { |r| r["kind"] == kind } if kind
|
|
355
|
-
|
|
356
|
-
emit({ "protocol" => Textus::PROTOCOL, "extensions" => rows })
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
def verb_migrate_keys(argv)
|
|
360
|
-
write = false
|
|
361
|
-
OptionParser.new do |o|
|
|
362
|
-
o.on("--dry-run") { write = false }
|
|
363
|
-
o.on("--write") { write = true }
|
|
364
|
-
o.on("--format=FMT") {}
|
|
365
|
-
end.permute!(argv)
|
|
366
|
-
res = Textus::MigrateKeys.run(store, write: write)
|
|
367
|
-
@stdout.puts(JSON.generate(res))
|
|
368
|
-
res["ok"] ? 0 : 1
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
def verb_mv(argv)
|
|
372
|
-
old_key = argv.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
373
|
-
new_key = argv.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
374
|
-
as_flag = nil
|
|
375
|
-
dry_run = false
|
|
376
|
-
OptionParser.new do |o|
|
|
377
|
-
o.on("--as=ROLE") { |v| as_flag = v }
|
|
378
|
-
o.on("--dry-run") { dry_run = true }
|
|
379
|
-
o.on("--format=FMT") {}
|
|
380
|
-
end.permute!(argv)
|
|
381
|
-
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
382
|
-
emit(store.mv(old_key, new_key, as: role, dry_run: dry_run))
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
def verb_uid(argv)
|
|
386
|
-
key = argv.shift or raise UsageError.new("uid requires a key")
|
|
387
|
-
parse_format!(argv)
|
|
388
|
-
emit({ "protocol" => PROTOCOL, "key" => key, "uid" => store.uid(key) })
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
def verb_intro(argv)
|
|
392
|
-
parse_format!(argv)
|
|
393
|
-
emit(Textus::Intro.run(store))
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
def verb_doctor(argv)
|
|
397
|
-
OptionParser.new do |o|
|
|
398
|
-
o.on("--format=FMT") {}
|
|
399
|
-
end.permute!(argv)
|
|
400
|
-
res = Textus::Doctor.run(store)
|
|
401
|
-
@stdout.puts(JSON.generate(res))
|
|
402
|
-
res["ok"] ? 0 : 1
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
def verb_published(argv)
|
|
406
|
-
parse_format!(argv)
|
|
407
|
-
emit({ "protocol" => Textus::PROTOCOL, "published" => store.published })
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
def emit(obj)
|
|
411
|
-
@stdout.puts(JSON.generate(obj))
|
|
412
|
-
0
|
|
65
|
+
def dispatch(klass, argv)
|
|
66
|
+
v = klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
|
|
67
|
+
v.parse(argv)
|
|
68
|
+
v.call(klass.needs_store? ? store : nil)
|
|
413
69
|
end
|
|
414
70
|
|
|
415
71
|
def emit_error(err)
|
|
@@ -423,16 +79,19 @@ module Textus
|
|
|
423
79
|
@stdout.puts <<~HELP
|
|
424
80
|
textus #{VERSION} — reference implementation of #{PROTOCOL}
|
|
425
81
|
|
|
426
|
-
Usage:
|
|
427
|
-
textus list [--prefix=KEY] --
|
|
428
|
-
textus where KEY
|
|
429
|
-
textus get KEY
|
|
430
|
-
textus put KEY --stdin [--
|
|
431
|
-
textus
|
|
432
|
-
textus
|
|
433
|
-
textus
|
|
82
|
+
Usage (json output is the default; --format=json accepted for back-compat):
|
|
83
|
+
textus list [--prefix=KEY] [--zone=Z]
|
|
84
|
+
textus where KEY
|
|
85
|
+
textus get KEY
|
|
86
|
+
textus put KEY --stdin [--fetch=NAME] --as=ROLE
|
|
87
|
+
textus stale [--prefix=KEY] [--zone=Z]
|
|
88
|
+
textus doctor
|
|
89
|
+
textus intro
|
|
90
|
+
|
|
91
|
+
textus key {mv,uid,migrate}
|
|
92
|
+
textus schema {show,init,diff,migrate}
|
|
93
|
+
textus hook {list,run}
|
|
434
94
|
HELP
|
|
435
95
|
end
|
|
436
96
|
end
|
|
437
|
-
# rubocop:enable Metrics/ClassLength
|
|
438
97
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Doctor
|
|
5
|
+
class Check
|
|
6
|
+
class AuditLog < Check
|
|
7
|
+
def call
|
|
8
|
+
out = []
|
|
9
|
+
path = File.join(store.root, "audit.log")
|
|
10
|
+
return out unless File.exist?(path)
|
|
11
|
+
|
|
12
|
+
File.foreach(path).with_index(1) do |line, lineno| # rubocop:disable Metrics/BlockLength
|
|
13
|
+
stripped = line.chomp
|
|
14
|
+
next if stripped.empty?
|
|
15
|
+
|
|
16
|
+
if stripped.start_with?("{")
|
|
17
|
+
begin
|
|
18
|
+
JSON.parse(stripped)
|
|
19
|
+
rescue JSON::ParserError => e
|
|
20
|
+
out << {
|
|
21
|
+
"code" => "audit.parse_error",
|
|
22
|
+
"level" => "warning",
|
|
23
|
+
"subject" => "#{path}:#{lineno}",
|
|
24
|
+
"message" => "audit log line #{lineno} is invalid JSON: #{e.message}",
|
|
25
|
+
"fix" => "inspect #{path} at line #{lineno} and remove the corrupted row",
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
# Legacy TSV (pre-0.5): read-only support retained for on-disk logs
|
|
30
|
+
# written by older textus versions. Never written by current code.
|
|
31
|
+
# Minimum 6 fields.
|
|
32
|
+
fields = stripped.split("\t")
|
|
33
|
+
next if fields.length >= 6
|
|
34
|
+
|
|
35
|
+
out << {
|
|
36
|
+
"code" => "audit.parse_error",
|
|
37
|
+
"level" => "warning",
|
|
38
|
+
"subject" => "#{path}:#{lineno}",
|
|
39
|
+
"message" => "audit log line #{lineno} has #{fields.length} fields " \
|
|
40
|
+
"(expected >=6 for legacy TSV; consider migrating to NDJSON)",
|
|
41
|
+
"fix" => "inspect #{path} at line #{lineno} and remove the corrupted row",
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
out
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class Hooks < Check
|
|
5
|
+
def call
|
|
6
|
+
out = []
|
|
7
|
+
dir = File.join(store.root, "hooks")
|
|
8
|
+
return out unless File.directory?(dir)
|
|
9
|
+
|
|
10
|
+
Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
11
|
+
registry = Textus::Hooks::Registry.new
|
|
12
|
+
Textus.with_registry(registry) do
|
|
13
|
+
load(f)
|
|
14
|
+
end
|
|
15
|
+
rescue StandardError, ScriptError => e
|
|
16
|
+
out << {
|
|
17
|
+
"code" => "hook.load_failed",
|
|
18
|
+
"level" => "error",
|
|
19
|
+
"subject" => File.basename(f),
|
|
20
|
+
"message" => "#{e.class}: #{e.message}",
|
|
21
|
+
"fix" => "open #{f} and fix the syntax/load error",
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
out
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class IllegalKeys < Check
|
|
5
|
+
def call
|
|
6
|
+
out = []
|
|
7
|
+
store.manifest.entries.each do |entry|
|
|
8
|
+
next unless entry.nested
|
|
9
|
+
|
|
10
|
+
base = File.join(store.root, "zones", entry.path)
|
|
11
|
+
next unless File.directory?(base)
|
|
12
|
+
|
|
13
|
+
walk_nested(base) do |abs_path, is_dir|
|
|
14
|
+
basename = File.basename(abs_path)
|
|
15
|
+
stem = is_dir ? basename : basename.sub(/#{Regexp.escape(File.extname(basename))}\z/, "")
|
|
16
|
+
next if stem.match?(Key::Grammar::SEGMENT)
|
|
17
|
+
|
|
18
|
+
proposed = Textus::MigrateKeys.normalize(stem)
|
|
19
|
+
out << {
|
|
20
|
+
"code" => "key.illegal",
|
|
21
|
+
"level" => "error",
|
|
22
|
+
"subject" => abs_path,
|
|
23
|
+
"path" => abs_path,
|
|
24
|
+
"proposed_key" => proposed,
|
|
25
|
+
"message" => "illegal key segment '#{stem}' at #{abs_path}",
|
|
26
|
+
"fix" => "run 'textus key migrate --dry-run' then '--write' to rename to '#{proposed}'",
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
out
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def walk_nested(root, &block)
|
|
36
|
+
Dir.each_child(root) do |name|
|
|
37
|
+
abs = File.join(root, name)
|
|
38
|
+
if File.directory?(abs)
|
|
39
|
+
walk_nested(abs, &block)
|
|
40
|
+
yield abs, true
|
|
41
|
+
else
|
|
42
|
+
yield abs, false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class ManifestFiles < Check
|
|
5
|
+
def call
|
|
6
|
+
out = []
|
|
7
|
+
store.manifest.entries.each do |entry|
|
|
8
|
+
next if entry.nested
|
|
9
|
+
|
|
10
|
+
path = leaf_path_for(entry)
|
|
11
|
+
next if File.exist?(path)
|
|
12
|
+
|
|
13
|
+
out << {
|
|
14
|
+
"code" => "manifest.missing_file",
|
|
15
|
+
"level" => "info",
|
|
16
|
+
"subject" => entry.key,
|
|
17
|
+
"message" => "declared entry has no file on disk at #{path}",
|
|
18
|
+
"fix" => "create the entry with 'textus put #{entry.key} --stdin --as=<role>' " \
|
|
19
|
+
"(or leave empty if not yet authored)",
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
out
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def leaf_path_for(entry)
|
|
28
|
+
primary_ext = Entry.for_format(entry.format).extensions.first
|
|
29
|
+
if File.extname(entry.path) == ""
|
|
30
|
+
File.join(store.root, "zones", entry.path + primary_ext)
|
|
31
|
+
else
|
|
32
|
+
File.join(store.root, "zones", entry.path)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class SchemaViolations < Check
|
|
5
|
+
def call
|
|
6
|
+
res = store.validate_all
|
|
7
|
+
res["violations"].map do |v|
|
|
8
|
+
fix = v["expected"] &&
|
|
9
|
+
"field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
|
|
10
|
+
{
|
|
11
|
+
"code" => v["code"],
|
|
12
|
+
"level" => "error",
|
|
13
|
+
"subject" => v["key"],
|
|
14
|
+
"message" => v["message"] || "#{v["code"]} on #{v["key"]}",
|
|
15
|
+
"fix" => fix,
|
|
16
|
+
}.compact
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class Schemas < Check
|
|
5
|
+
def call
|
|
6
|
+
out = []
|
|
7
|
+
store.manifest.entries.each do |entry|
|
|
8
|
+
next if entry.schema.nil?
|
|
9
|
+
|
|
10
|
+
sp = File.join(store.root, "schemas", "#{entry.schema}.yaml")
|
|
11
|
+
next if File.exist?(sp)
|
|
12
|
+
|
|
13
|
+
out << {
|
|
14
|
+
"code" => "schema.missing",
|
|
15
|
+
"level" => "error",
|
|
16
|
+
"subject" => entry.key,
|
|
17
|
+
"message" => "schema '#{entry.schema}' not found at #{sp}",
|
|
18
|
+
"fix" => "create the schema file or run 'textus schema init #{entry.schema} --from=<key>'",
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
out
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|