textus 0.3.0 → 0.5.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 +79 -1
- data/README.md +22 -18
- data/SPEC.md +49 -35
- data/docs/architecture.md +63 -28
- data/lib/textus/audit_log.rb +46 -11
- data/lib/textus/builder.rb +3 -3
- data/lib/textus/{builtin_fetchers.rb → builtin_actions.rb} +16 -11
- data/lib/textus/cli/accept.rb +13 -0
- data/lib/textus/cli/action.rb +51 -0
- data/lib/textus/cli/build.rb +11 -0
- data/lib/textus/cli/delete.rb +14 -0
- data/lib/textus/cli/deprecated_alias.rb +31 -0
- data/lib/textus/cli/deps.rb +10 -0
- data/lib/textus/cli/doctor.rb +13 -0
- data/lib/textus/cli/extension_group.rb +9 -0
- data/lib/textus/cli/extensions.rb +49 -0
- data/lib/textus/cli/get.rb +10 -0
- data/lib/textus/cli/group.rb +51 -0
- data/lib/textus/cli/init.rb +12 -0
- data/lib/textus/cli/intro.rb +9 -0
- data/lib/textus/cli/key_group.rb +10 -0
- data/lib/textus/cli/list.rb +12 -0
- data/lib/textus/cli/migrate.rb +41 -0
- data/lib/textus/cli/migrate_keys.rb +19 -0
- data/lib/textus/cli/mv.rb +20 -0
- data/lib/textus/cli/published.rb +9 -0
- data/lib/textus/cli/put.rb +48 -0
- data/lib/textus/cli/rdeps.rb +10 -0
- data/lib/textus/cli/refresh.rb +13 -0
- data/lib/textus/cli/schema.rb +10 -0
- data/lib/textus/cli/schema_diff.rb +15 -0
- data/lib/textus/cli/schema_group.rb +33 -0
- data/lib/textus/cli/schema_init.rb +19 -0
- data/lib/textus/cli/schema_migrate.rb +19 -0
- data/lib/textus/cli/stale.rb +12 -0
- data/lib/textus/cli/uid.rb +15 -0
- data/lib/textus/cli/verb.rb +62 -0
- data/lib/textus/cli/where.rb +10 -0
- data/lib/textus/cli.rb +65 -347
- data/lib/textus/doctor.rb +103 -32
- data/lib/textus/entry/json.rb +6 -4
- data/lib/textus/entry/markdown.rb +4 -4
- data/lib/textus/entry/text.rb +3 -3
- data/lib/textus/entry/yaml.rb +6 -4
- data/lib/textus/entry.rb +2 -2
- data/lib/textus/errors.rb +2 -2
- data/lib/textus/extension_registry.rb +22 -9
- data/lib/textus/extensions.rb +6 -2
- data/lib/textus/init.rb +6 -5
- data/lib/textus/intro.rb +11 -9
- data/lib/textus/manifest.rb +11 -215
- data/lib/textus/manifest_entry.rb +185 -0
- data/lib/textus/migrate_v2.rb +27 -0
- data/lib/textus/projection.rb +1 -1
- data/lib/textus/proposal.rb +3 -3
- data/lib/textus/refresh.rb +21 -20
- data/lib/textus/schema_tools.rb +8 -8
- data/lib/textus/store/events.rb +31 -0
- data/lib/textus/store/mover.rb +118 -0
- data/lib/textus/store/staleness.rb +142 -0
- data/lib/textus/store/validator.rb +53 -0
- data/lib/textus/store.rb +50 -355
- data/lib/textus/store_view.rb +11 -2
- data/lib/textus/version.rb +2 -2
- data/lib/textus.rb +39 -1
- metadata +39 -2
data/lib/textus/builder.rb
CHANGED
|
@@ -96,14 +96,14 @@ module Textus
|
|
|
96
96
|
"from" => Array(mentry.projection&.fetch("select", nil)).compact,
|
|
97
97
|
},
|
|
98
98
|
}
|
|
99
|
-
Entry.for_format("markdown").serialize(
|
|
99
|
+
Entry.for_format("markdown").serialize(meta: frontmatter, body: body)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
# Text: projection -> template -> text.serialize(body). No frontmatter, no _meta.
|
|
103
103
|
def build_text(mentry, data)
|
|
104
104
|
data = data.merge("intro" => Intro.run(@store)) if mentry.inject_intro
|
|
105
105
|
body = render_template!(mentry, data)
|
|
106
|
-
Entry.for_format("text").serialize(
|
|
106
|
+
Entry.for_format("text").serialize(meta: {}, body: body)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# JSON / YAML pipeline. Templateless = default; template = escape hatch.
|
|
@@ -127,7 +127,7 @@ module Textus
|
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
final = inject_meta(content, mentry)
|
|
130
|
-
strategy.serialize(
|
|
130
|
+
strategy.serialize(meta: {}, body: "", content: final)
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def render_template!(mentry, data)
|
|
@@ -4,31 +4,35 @@ require "yaml"
|
|
|
4
4
|
require "rexml/document"
|
|
5
5
|
|
|
6
6
|
module Textus
|
|
7
|
-
module
|
|
7
|
+
module BuiltinActions
|
|
8
8
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
9
9
|
def self.register_all
|
|
10
|
-
Textus.
|
|
10
|
+
Textus.action(:json) do |config:, store:, args:|
|
|
11
11
|
_ = store
|
|
12
|
+
_ = args
|
|
12
13
|
data = JSON.parse(config["bytes"].to_s)
|
|
13
|
-
{
|
|
14
|
+
{ _meta: {}, body: YAML.dump(data) }
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
Textus.
|
|
17
|
+
Textus.action(:csv) do |config:, store:, args:|
|
|
17
18
|
_ = store
|
|
19
|
+
_ = args
|
|
18
20
|
rows = CSV.parse(config["bytes"].to_s, headers: true).map(&:to_h)
|
|
19
|
-
{
|
|
21
|
+
{ _meta: {}, body: YAML.dump(rows) }
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
Textus.
|
|
24
|
+
Textus.action(:"markdown-links") do |config:, store:, args:|
|
|
23
25
|
_ = store
|
|
26
|
+
_ = args
|
|
24
27
|
links = config["bytes"].to_s.scan(%r{\[([^\]]+)\]\((https?://[^)\s]+)\)}).map do |text, href|
|
|
25
28
|
{ "text" => text, "href" => href }
|
|
26
29
|
end
|
|
27
|
-
{
|
|
30
|
+
{ _meta: {}, body: YAML.dump(links) }
|
|
28
31
|
end
|
|
29
32
|
|
|
30
|
-
Textus.
|
|
33
|
+
Textus.action(:"ical-events") do |config:, store:, args:|
|
|
31
34
|
_ = store
|
|
35
|
+
_ = args
|
|
32
36
|
events = []
|
|
33
37
|
current = nil
|
|
34
38
|
config["bytes"].to_s.each_line do |line|
|
|
@@ -42,11 +46,12 @@ module Textus
|
|
|
42
46
|
current[Regexp.last_match(1).downcase] = Regexp.last_match(2) if current
|
|
43
47
|
end
|
|
44
48
|
end
|
|
45
|
-
{
|
|
49
|
+
{ _meta: {}, body: YAML.dump(events) }
|
|
46
50
|
end
|
|
47
51
|
|
|
48
|
-
Textus.
|
|
52
|
+
Textus.action(:rss) do |config:, store:, args:|
|
|
49
53
|
_ = store
|
|
54
|
+
_ = args
|
|
50
55
|
doc = REXML::Document.new(config["bytes"].to_s)
|
|
51
56
|
items = doc.elements.to_a("//item").map do |item|
|
|
52
57
|
{
|
|
@@ -55,7 +60,7 @@ module Textus
|
|
|
55
60
|
"pubDate" => item.elements["pubDate"]&.text,
|
|
56
61
|
}
|
|
57
62
|
end
|
|
58
|
-
{
|
|
63
|
+
{ _meta: {}, body: YAML.dump(items) }
|
|
59
64
|
end
|
|
60
65
|
end
|
|
61
66
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Accept < Verb
|
|
4
|
+
option :as_flag, "--as=ROLE"
|
|
5
|
+
|
|
6
|
+
def call(store)
|
|
7
|
+
key = positional.shift or raise UsageError.new("accept requires a key")
|
|
8
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
9
|
+
emit(store.accept(key, as: role))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Action < Verb
|
|
4
|
+
prepend DeprecatedAliasMixin
|
|
5
|
+
|
|
6
|
+
def self.deprecated_name = "action"
|
|
7
|
+
def self.replacement_path = "extension run"
|
|
8
|
+
|
|
9
|
+
def parse(argv)
|
|
10
|
+
@raw_argv = argv
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(store)
|
|
14
|
+
name = @raw_argv.shift
|
|
15
|
+
raise UsageError.new("action requires a name") if name.nil?
|
|
16
|
+
|
|
17
|
+
as_flag = nil
|
|
18
|
+
args = {}
|
|
19
|
+
@raw_argv.each do |tok|
|
|
20
|
+
case tok
|
|
21
|
+
when /\A--as=(.+)\z/ then as_flag = ::Regexp.last_match(1)
|
|
22
|
+
when /\A--format=/ then next
|
|
23
|
+
when /\A--([\w-]+)=(.*)\z/ then args[::Regexp.last_match(1)] = ::Regexp.last_match(2)
|
|
24
|
+
else
|
|
25
|
+
raise UsageError.new("unknown arg to 'action #{name}': #{tok}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
30
|
+
callable = store.registry.action(name)
|
|
31
|
+
view = StoreView.new(store, writable: true, as: role)
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
Timeout.timeout(Textus::Refresh::ACTION_TIMEOUT_SECONDS) do
|
|
35
|
+
callable.call(config: {}, store: view, args: args)
|
|
36
|
+
end
|
|
37
|
+
rescue Timeout::Error
|
|
38
|
+
raise UsageError.new(
|
|
39
|
+
"action '#{name}' exceeded #{Textus::Refresh::ACTION_TIMEOUT_SECONDS}s timeout",
|
|
40
|
+
)
|
|
41
|
+
rescue Textus::Error
|
|
42
|
+
raise
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
raise UsageError.new("action '#{name}' raised: #{e.class}: #{e.message}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
emit({ "action" => name, "ok" => true })
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Delete < Verb
|
|
4
|
+
option :as_flag, "--as=ROLE"
|
|
5
|
+
option :if_etag, "--if-etag=E"
|
|
6
|
+
|
|
7
|
+
def call(store)
|
|
8
|
+
key = positional.shift or raise UsageError.new("delete requires a key")
|
|
9
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
10
|
+
emit(store.delete(key, if_etag: if_etag, as: role))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
module DeprecatedAliasMixin
|
|
4
|
+
def self.prepended(base)
|
|
5
|
+
base.extend(ClassMethods)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def deprecated_name
|
|
10
|
+
raise NotImplementedError.new("#{self}.deprecated_name must be defined")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def replacement_path
|
|
14
|
+
raise NotImplementedError.new("#{self}.replacement_path must be defined")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_writer :deprecated_alias
|
|
19
|
+
|
|
20
|
+
def call(store)
|
|
21
|
+
if @deprecated_alias
|
|
22
|
+
@stderr.puts(
|
|
23
|
+
"textus: '#{self.class.deprecated_name}' is deprecated; " \
|
|
24
|
+
"use 'textus #{self.class.replacement_path}' instead. Removed in 0.6.",
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class DoctorVerb < Verb
|
|
4
|
+
option :checks, "--check=NAME"
|
|
5
|
+
|
|
6
|
+
def call(store)
|
|
7
|
+
check_list = checks&.split(",")&.map(&:strip)
|
|
8
|
+
res = Textus::Doctor.run(store, checks: check_list)
|
|
9
|
+
emit(res, exit_code: res["ok"] ? 0 : 1)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Extensions < Verb
|
|
4
|
+
prepend DeprecatedAliasMixin
|
|
5
|
+
|
|
6
|
+
def self.deprecated_name = "extensions"
|
|
7
|
+
def self.replacement_path = "extension list"
|
|
8
|
+
|
|
9
|
+
option :kind, "--kind=K"
|
|
10
|
+
|
|
11
|
+
def call(store) # rubocop:disable Metrics/AbcSize
|
|
12
|
+
# When invoked as the flat alias `textus extensions list`, the positional "list"
|
|
13
|
+
# is still present. When invoked via `textus extension list` (group), it has been
|
|
14
|
+
# consumed by the group dispatcher — both are valid.
|
|
15
|
+
subcommand = positional.first
|
|
16
|
+
if subcommand
|
|
17
|
+
raise UsageError.new("extensions requires 'list'") unless subcommand == "list"
|
|
18
|
+
|
|
19
|
+
positional.shift
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
rows = []
|
|
23
|
+
rows += store.registry.action_names.map { |n| { "kind" => "action", "name" => n.to_s } }
|
|
24
|
+
rows += store.registry.doctor_check_names.map { |n| { "kind" => "doctor_check", "name" => n.to_s } }
|
|
25
|
+
rows += store.registry.reducer_names.map { |n| { "kind" => "reducer", "name" => n.to_s } }
|
|
26
|
+
store.registry.hook_events.each do |evt|
|
|
27
|
+
store.registry.hooks(evt).each do |h|
|
|
28
|
+
rows << { "kind" => "hook", "event" => evt.to_s, "name" => h[:name].to_s }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
store.manifest.entries.each do |e|
|
|
32
|
+
e.events.each do |evt, defs|
|
|
33
|
+
Array(defs).each do |defn|
|
|
34
|
+
next unless defn["exec"]
|
|
35
|
+
|
|
36
|
+
rows << {
|
|
37
|
+
"kind" => "hook", "event" => evt.to_s, "exec" => defn["exec"],
|
|
38
|
+
"key" => e.key, "as" => defn["as"] || "script"
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
rows.select! { |r| r["kind"] == kind } if kind
|
|
44
|
+
|
|
45
|
+
emit({ "extensions" => rows })
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Group < Verb
|
|
4
|
+
class << self
|
|
5
|
+
def subcommands
|
|
6
|
+
@subcommands ||= {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def cli_name
|
|
10
|
+
@cli_name || raise("subclass must define cli_name")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_writer :cli_name
|
|
14
|
+
|
|
15
|
+
def inherited(subclass)
|
|
16
|
+
super
|
|
17
|
+
subclass.instance_variable_set(:@subcommands, {})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def needs_store?
|
|
21
|
+
# Delegate to the matched subcommand at parse time; default true.
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse(argv)
|
|
27
|
+
subname = argv.shift
|
|
28
|
+
if subname.nil?
|
|
29
|
+
raise UsageError.new(
|
|
30
|
+
"#{self.class.cli_name} requires a subcommand: #{self.class.subcommands.keys.join(", ")}",
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@sub_klass = self.class.subcommands[subname]
|
|
35
|
+
unless @sub_klass
|
|
36
|
+
raise UsageError.new(
|
|
37
|
+
"unknown #{self.class.cli_name} subcommand '#{subname}'. " \
|
|
38
|
+
"Valid: #{self.class.subcommands.keys.join(", ")}",
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@sub = @sub_klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
|
|
43
|
+
@sub.parse(argv)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call(store)
|
|
47
|
+
@sub.call(@sub_klass.needs_store? ? store : nil)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Migrate < Verb
|
|
4
|
+
# Does not need a store — the target manifest may still be textus/1.
|
|
5
|
+
def self.needs_store? = false
|
|
6
|
+
|
|
7
|
+
def call(_store)
|
|
8
|
+
target = positional.shift or raise UsageError.new("migrate requires a target (e.g. 'v2')")
|
|
9
|
+
raise UsageError.new("unknown migration target: #{target.inspect}; supported: v2") unless target == "v2"
|
|
10
|
+
|
|
11
|
+
# Locate the .textus directory the same way Store.discover does.
|
|
12
|
+
root = find_textus_root
|
|
13
|
+
emit(Textus::MigrateV2.run(root))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def find_textus_root
|
|
19
|
+
explicit = ENV.fetch("TEXTUS_ROOT", nil)
|
|
20
|
+
if explicit
|
|
21
|
+
abs = File.expand_path(explicit)
|
|
22
|
+
return abs if File.directory?(abs)
|
|
23
|
+
|
|
24
|
+
raise IoError.new("no textus store at #{abs}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
dir = File.expand_path(@cwd)
|
|
28
|
+
loop do
|
|
29
|
+
candidate = File.join(dir, ".textus")
|
|
30
|
+
return candidate if File.directory?(candidate) && File.exist?(File.join(candidate, "manifest.yaml"))
|
|
31
|
+
|
|
32
|
+
parent = File.dirname(dir)
|
|
33
|
+
break if parent == dir
|
|
34
|
+
|
|
35
|
+
dir = parent
|
|
36
|
+
end
|
|
37
|
+
raise IoError.new("no .textus directory found from #{@cwd}")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class MigrateKeysVerb < Verb
|
|
4
|
+
prepend DeprecatedAliasMixin
|
|
5
|
+
|
|
6
|
+
def self.deprecated_name = "migrate-keys"
|
|
7
|
+
def self.replacement_path = "key migrate"
|
|
8
|
+
|
|
9
|
+
option :write, "--write"
|
|
10
|
+
option :dry_run, "--dry-run"
|
|
11
|
+
|
|
12
|
+
def call(store)
|
|
13
|
+
effective_write = write && !dry_run
|
|
14
|
+
res = Textus::MigrateKeys.run(store, write: effective_write || false)
|
|
15
|
+
emit(res, exit_code: res["ok"] ? 0 : 1)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Mv < Verb
|
|
4
|
+
prepend DeprecatedAliasMixin
|
|
5
|
+
|
|
6
|
+
def self.deprecated_name = "mv"
|
|
7
|
+
def self.replacement_path = "key mv"
|
|
8
|
+
|
|
9
|
+
option :as_flag, "--as=ROLE"
|
|
10
|
+
option :dry_run, "--dry-run"
|
|
11
|
+
|
|
12
|
+
def call(store)
|
|
13
|
+
old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
14
|
+
new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
15
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
16
|
+
emit(store.mv(old_key, new_key, as: role, dry_run: dry_run || false))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Put < Verb
|
|
4
|
+
option :as_flag, "--as=ROLE"
|
|
5
|
+
option :use_stdin, "--stdin"
|
|
6
|
+
option :action_name, "--action=NAME"
|
|
7
|
+
|
|
8
|
+
def call(store) # rubocop:disable Metrics/AbcSize
|
|
9
|
+
key = positional.shift or raise UsageError.new("put requires a key")
|
|
10
|
+
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
11
|
+
|
|
12
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
13
|
+
|
|
14
|
+
raw = @stdin.read
|
|
15
|
+
payload =
|
|
16
|
+
if action_name
|
|
17
|
+
callable = store.registry.action(action_name)
|
|
18
|
+
result =
|
|
19
|
+
begin
|
|
20
|
+
Timeout.timeout(Textus::Refresh::ACTION_TIMEOUT_SECONDS) do
|
|
21
|
+
callable.call(config: { "bytes" => raw }, store: Textus::StoreView.new(store), args: {})
|
|
22
|
+
end
|
|
23
|
+
rescue Timeout::Error
|
|
24
|
+
raise UsageError.new(
|
|
25
|
+
"action '#{action_name}' exceeded #{Textus::Refresh::ACTION_TIMEOUT_SECONDS}s timeout",
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
basename = key.split(".").last
|
|
29
|
+
{
|
|
30
|
+
"_meta" => {
|
|
31
|
+
"name" => basename,
|
|
32
|
+
"last_refreshed_at" => Time.now.utc.iso8601,
|
|
33
|
+
"actioned_with" => action_name,
|
|
34
|
+
}.merge(result[:_meta] || result["_meta"] || result[:frontmatter] || result["frontmatter"] || {}),
|
|
35
|
+
"body" => result[:body] || result["body"] || "",
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
JSON.parse(raw)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
meta = payload["_meta"] || payload["frontmatter"] || {}
|
|
42
|
+
body = payload["body"] || ""
|
|
43
|
+
if_etag = payload["if_etag"]
|
|
44
|
+
emit(store.put(key, meta: meta, body: body, if_etag: if_etag, as: role))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class RefreshVerb < Verb
|
|
4
|
+
option :as_flag, "--as=ROLE"
|
|
5
|
+
|
|
6
|
+
def call(store)
|
|
7
|
+
key = positional.shift or raise UsageError.new("refresh requires a key")
|
|
8
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
9
|
+
emit(Textus::Refresh.call(store, key, as: role))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class SchemaDiff < Verb
|
|
4
|
+
prepend DeprecatedAliasMixin
|
|
5
|
+
|
|
6
|
+
def self.deprecated_name = "schema-diff"
|
|
7
|
+
def self.replacement_path = "schema diff"
|
|
8
|
+
|
|
9
|
+
def call(store)
|
|
10
|
+
name = positional.shift or raise UsageError.new("schema-diff NAME")
|
|
11
|
+
emit(Textus::SchemaTools.diff(store, name: name))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class SchemaGroup < Group
|
|
4
|
+
self.cli_name = "schema"
|
|
5
|
+
subcommands["show"] = SchemaVerb
|
|
6
|
+
subcommands["init"] = SchemaInit
|
|
7
|
+
subcommands["diff"] = SchemaDiff
|
|
8
|
+
subcommands["migrate"] = SchemaMigrate
|
|
9
|
+
|
|
10
|
+
# Back-compat: `textus schema KEY` (dotted key, no subcommand word).
|
|
11
|
+
# If the first positional looks like a dotted key, treat it as `schema show KEY`.
|
|
12
|
+
def parse(argv)
|
|
13
|
+
first = argv.first
|
|
14
|
+
if first && dotted_key?(first)
|
|
15
|
+
@stderr.puts(
|
|
16
|
+
"textus: 'schema KEY' is deprecated; use 'textus schema show KEY' instead. Removed in 0.6.",
|
|
17
|
+
)
|
|
18
|
+
argv.unshift("show")
|
|
19
|
+
end
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def dotted_key?(token)
|
|
26
|
+
return false if token.start_with?("-")
|
|
27
|
+
return false unless token.include?(".")
|
|
28
|
+
|
|
29
|
+
token.split(".").all? { |seg| seg.match?(Manifest::KEY_SEGMENT) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class SchemaInit < Verb
|
|
4
|
+
prepend DeprecatedAliasMixin
|
|
5
|
+
|
|
6
|
+
def self.deprecated_name = "schema-init"
|
|
7
|
+
def self.replacement_path = "schema init"
|
|
8
|
+
|
|
9
|
+
option :from_key, "--from=KEY"
|
|
10
|
+
|
|
11
|
+
def call(store)
|
|
12
|
+
name = positional.shift or raise UsageError.new("schema-init NAME")
|
|
13
|
+
raise UsageError.new("schema-init requires --from=KEY") unless from_key
|
|
14
|
+
|
|
15
|
+
emit(Textus::SchemaTools.init(store, name: name, from: from_key))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|