textus 0.22.0 → 0.26.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/ARCHITECTURE.md +148 -45
- data/CHANGELOG.md +102 -0
- data/README.md +1 -1
- data/SPEC.md +12 -12
- data/docs/conventions.md +10 -0
- data/lib/textus/application/caps.rb +49 -0
- data/lib/textus/application/context.rb +2 -2
- data/lib/textus/application/envelope/reader.rb +44 -0
- data/lib/textus/application/{writes/envelope_io.rb → envelope/writer.rb} +24 -50
- data/lib/textus/application/maintenance/key_delete_prefix.rb +44 -0
- data/lib/textus/application/maintenance/key_mv_prefix.rb +57 -0
- data/lib/textus/application/maintenance/migrate.rb +59 -0
- data/lib/textus/application/maintenance/rule_lint.rb +65 -0
- data/lib/textus/application/maintenance/zone_mv.rb +60 -0
- data/lib/textus/application/maintenance.rb +17 -0
- data/lib/textus/application/projection.rb +12 -10
- data/lib/textus/application/read/audit.rb +106 -0
- data/lib/textus/application/read/blame.rb +91 -0
- data/lib/textus/application/read/deps.rb +34 -0
- data/lib/textus/application/read/freshness.rb +110 -0
- data/lib/textus/application/read/get.rb +75 -0
- data/lib/textus/application/read/get_or_refresh.rb +63 -0
- data/lib/textus/application/read/list.rb +25 -0
- data/lib/textus/application/read/policy_explain.rb +47 -0
- data/lib/textus/application/read/published.rb +25 -0
- data/lib/textus/application/read/pulse.rb +101 -0
- data/lib/textus/application/read/rdeps.rb +35 -0
- data/lib/textus/application/read/schema_envelope.rb +26 -0
- data/lib/textus/application/read/stale.rb +23 -0
- data/lib/textus/application/read/uid.rb +30 -0
- data/lib/textus/application/read/validate_all.rb +32 -0
- data/lib/textus/application/{reads → read}/validator.rb +2 -2
- data/lib/textus/application/read/where.rb +26 -0
- data/lib/textus/application/use_case.rb +22 -0
- data/lib/textus/application/write/accept.rb +102 -0
- data/lib/textus/application/{writes → write}/authority_gate.rb +3 -3
- data/lib/textus/application/write/delete.rb +45 -0
- data/lib/textus/application/{writes → write}/materializer.rb +14 -15
- data/lib/textus/application/write/mv.rb +118 -0
- data/lib/textus/application/write/publish.rb +96 -0
- data/lib/textus/application/write/put.rb +49 -0
- data/lib/textus/application/write/refresh_all.rb +63 -0
- data/lib/textus/application/{refresh/orchestrator.rb → write/refresh_orchestrator.rb} +32 -8
- data/lib/textus/application/write/refresh_worker.rb +134 -0
- data/lib/textus/application/write/reject.rb +62 -0
- data/lib/textus/boot.rb +27 -29
- data/lib/textus/builder/pipeline.rb +3 -3
- data/lib/textus/cli/group/mcp.rb +9 -0
- data/lib/textus/cli/group/zone.rb +9 -0
- data/lib/textus/cli/verb/accept.rb +1 -1
- data/lib/textus/cli/verb/audit.rb +2 -2
- data/lib/textus/cli/verb/blame.rb +1 -1
- data/lib/textus/cli/verb/boot.rb +1 -1
- data/lib/textus/cli/verb/build.rb +2 -2
- data/lib/textus/cli/verb/delete.rb +1 -1
- data/lib/textus/cli/verb/deps.rb +1 -1
- data/lib/textus/cli/verb/doctor.rb +1 -1
- data/lib/textus/cli/verb/freshness.rb +1 -1
- data/lib/textus/cli/verb/get.rb +1 -1
- data/lib/textus/cli/verb/hook_run.rb +3 -4
- data/lib/textus/cli/verb/hooks.rb +11 -14
- data/lib/textus/cli/verb/key_delete.rb +24 -0
- data/lib/textus/cli/verb/list.rb +1 -1
- data/lib/textus/cli/verb/mcp_serve.rb +17 -0
- data/lib/textus/cli/verb/migrate.rb +18 -0
- data/lib/textus/cli/verb/mv.rb +11 -3
- data/lib/textus/cli/verb/published.rb +1 -1
- data/lib/textus/cli/verb/pulse.rb +1 -1
- data/lib/textus/cli/verb/put.rb +8 -6
- data/lib/textus/cli/verb/rdeps.rb +1 -1
- data/lib/textus/cli/verb/refresh.rb +1 -1
- data/lib/textus/cli/verb/refresh_stale.rb +1 -1
- data/lib/textus/cli/verb/reject.rb +1 -1
- data/lib/textus/cli/verb/rule_explain.rb +1 -1
- data/lib/textus/cli/verb/rule_lint.rb +18 -0
- data/lib/textus/cli/verb/schema.rb +1 -1
- data/lib/textus/cli/verb/uid.rb +1 -1
- data/lib/textus/cli/verb/where.rb +1 -1
- data/lib/textus/cli/verb/zone_mv.rb +19 -0
- data/lib/textus/cli/verb.rb +4 -4
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
- data/lib/textus/doctor/check/hooks.rb +4 -3
- data/lib/textus/doctor/check/illegal_keys.rb +2 -2
- data/lib/textus/doctor/check/intake_registration.rb +2 -2
- data/lib/textus/doctor/check/manifest_files.rb +2 -2
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/refresh_locks.rb +2 -2
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/schemas.rb +2 -2
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check/templates.rb +2 -2
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +5 -3
- data/lib/textus/doctor.rb +24 -27
- data/lib/textus/domain/authorizer.rb +4 -4
- data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
- data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
- data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
- data/lib/textus/domain/staleness/generator_check.rb +2 -2
- data/lib/textus/domain/staleness/intake_check.rb +2 -2
- data/lib/textus/domain/staleness.rb +1 -1
- data/lib/textus/hooks/builtin.rb +14 -14
- data/lib/textus/hooks/context.rb +13 -13
- data/lib/textus/hooks/error_log.rb +32 -0
- data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
- data/lib/textus/hooks/loader.rb +29 -3
- data/lib/textus/hooks/rpc_registry.rb +77 -0
- data/lib/textus/infra/audit_subscriber.rb +6 -7
- data/lib/textus/infra/refresh/detached.rb +1 -1
- data/lib/textus/key/path.rb +7 -3
- data/lib/textus/manifest/data.rb +78 -0
- data/lib/textus/manifest/entry/base.rb +4 -4
- data/lib/textus/manifest/entry/derived.rb +4 -5
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/manifest/policy.rb +48 -0
- data/lib/textus/manifest/resolver.rb +14 -14
- data/lib/textus/manifest/rules.rb +1 -1
- data/lib/textus/manifest.rb +53 -111
- data/lib/textus/mcp/errors.rb +32 -0
- data/lib/textus/mcp/server.rb +127 -0
- data/lib/textus/mcp/session.rb +31 -0
- data/lib/textus/mcp/tool_schemas.rb +71 -0
- data/lib/textus/mcp/tools.rb +129 -0
- data/lib/textus/mcp.rb +6 -0
- data/lib/textus/schema/tools.rb +14 -10
- data/lib/textus/session.rb +84 -0
- data/lib/textus/store.rb +14 -9
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +8 -1
- metadata +61 -36
- data/lib/textus/application/reads/audit.rb +0 -94
- data/lib/textus/application/reads/blame.rb +0 -82
- data/lib/textus/application/reads/deps.rb +0 -26
- data/lib/textus/application/reads/freshness.rb +0 -88
- data/lib/textus/application/reads/get.rb +0 -67
- data/lib/textus/application/reads/get_or_refresh.rb +0 -51
- data/lib/textus/application/reads/list.rb +0 -17
- data/lib/textus/application/reads/policy_explain.rb +0 -39
- data/lib/textus/application/reads/published.rb +0 -17
- data/lib/textus/application/reads/pulse.rb +0 -63
- data/lib/textus/application/reads/rdeps.rb +0 -27
- data/lib/textus/application/reads/schema_envelope.rb +0 -18
- data/lib/textus/application/reads/stale.rb +0 -15
- data/lib/textus/application/reads/uid.rb +0 -23
- data/lib/textus/application/reads/validate_all.rb +0 -24
- data/lib/textus/application/reads/where.rb +0 -18
- data/lib/textus/application/refresh/all.rb +0 -52
- data/lib/textus/application/refresh/worker.rb +0 -116
- data/lib/textus/application/writes/accept.rb +0 -89
- data/lib/textus/application/writes/delete.rb +0 -33
- data/lib/textus/application/writes/mv.rb +0 -105
- data/lib/textus/application/writes/publish.rb +0 -81
- data/lib/textus/application/writes/put.rb +0 -37
- data/lib/textus/application/writes/reject.rb +0 -50
- data/lib/textus/infra/event_bus.rb +0 -27
- data/lib/textus/operations.rb +0 -176
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require "timeout"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Application
|
|
5
|
+
module Write
|
|
6
|
+
module RefreshWorker
|
|
7
|
+
FETCH_TIMEOUT_SECONDS = 30
|
|
8
|
+
|
|
9
|
+
def self.call(*, session:, ctx:, caps:, **)
|
|
10
|
+
Impl.new(
|
|
11
|
+
ctx: ctx, caps: caps,
|
|
12
|
+
rpc: session.rpc,
|
|
13
|
+
writer: session.envelope_writer,
|
|
14
|
+
hook_context: session.hook_context
|
|
15
|
+
).call(*, **)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Impl
|
|
19
|
+
def initialize(ctx:, caps:, rpc:, writer:, hook_context:)
|
|
20
|
+
@ctx = ctx
|
|
21
|
+
@caps = caps
|
|
22
|
+
@manifest = caps.manifest
|
|
23
|
+
@writer = writer
|
|
24
|
+
@events = caps.events
|
|
25
|
+
@rpc = rpc
|
|
26
|
+
@authorizer = caps.authorizer
|
|
27
|
+
@hook_context = hook_context
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# call(key) is the primary entry; run is kept as an alias for
|
|
31
|
+
# Orchestrator and RefreshAll which call worker.run(key).
|
|
32
|
+
def call(key)
|
|
33
|
+
run(key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run(key)
|
|
37
|
+
res = @manifest.resolver.resolve(key)
|
|
38
|
+
mentry = res.entry
|
|
39
|
+
path = res.path
|
|
40
|
+
remaining = res.remaining
|
|
41
|
+
raise UsageError.new("no intake declared for '#{key}'") unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
42
|
+
|
|
43
|
+
before_etag = File.exist?(path) ? Etag.for_file(path) : nil
|
|
44
|
+
result = fetch_with_events(key, mentry, remaining)
|
|
45
|
+
persist_and_notify(key, mentry, result, before_etag)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def fetch_timeout_for(key)
|
|
51
|
+
rule = @manifest.rules.for(key)
|
|
52
|
+
rule&.refresh&.fetch_timeout_seconds || FETCH_TIMEOUT_SECONDS
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fetch_with_events(key, mentry, remaining)
|
|
56
|
+
@events.publish(:refresh_started, ctx: @hook_context, key: key, mode: :sync)
|
|
57
|
+
call_intake(key, mentry, remaining)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def call_intake(key, mentry, remaining)
|
|
61
|
+
timeout = fetch_timeout_for(key)
|
|
62
|
+
Timeout.timeout(timeout) do
|
|
63
|
+
@rpc.invoke(:resolve_intake, mentry.handler,
|
|
64
|
+
caps: @caps,
|
|
65
|
+
config: mentry.config,
|
|
66
|
+
args: { trigger_key: key, leaf_segments: remaining || [] })
|
|
67
|
+
end
|
|
68
|
+
rescue Timeout::Error
|
|
69
|
+
@events.publish(:refresh_failed, ctx: @hook_context, key: key,
|
|
70
|
+
error_class: "Timeout::Error",
|
|
71
|
+
error_message: "intake '#{mentry.handler}' exceeded #{timeout}s")
|
|
72
|
+
raise UsageError.new("intake '#{mentry.handler}' exceeded #{timeout}s timeout")
|
|
73
|
+
rescue Textus::Error => e
|
|
74
|
+
@events.publish(:refresh_failed, ctx: @hook_context, key: key, error_class: e.class.name,
|
|
75
|
+
error_message: e.message)
|
|
76
|
+
raise
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
@events.publish(:refresh_failed, ctx: @hook_context, key: key, error_class: e.class.name,
|
|
79
|
+
error_message: e.message)
|
|
80
|
+
raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def persist_and_notify(key, mentry, result, before_etag)
|
|
84
|
+
normalized = RefreshWorker.send(:normalize_action_result, result, format: mentry.format)
|
|
85
|
+
@authorizer.authorize_write!(mentry, role: @ctx.role)
|
|
86
|
+
envelope = @writer.put(
|
|
87
|
+
key,
|
|
88
|
+
mentry: mentry,
|
|
89
|
+
payload: Textus::Application::Envelope::Writer::Payload.new(
|
|
90
|
+
meta: normalized[:meta], body: normalized[:body], content: normalized[:content],
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
change = detect_change(before_etag, envelope)
|
|
94
|
+
@events.publish(:entry_refreshed, ctx: @hook_context, key: key, envelope: envelope, change: change) unless change == :unchanged
|
|
95
|
+
envelope
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def detect_change(before_etag, envelope)
|
|
99
|
+
if before_etag.nil? then :created
|
|
100
|
+
elsif envelope.etag == before_etag then :unchanged
|
|
101
|
+
else :updated
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.normalize_action_result(res, format:)
|
|
107
|
+
res = res.transform_keys(&:to_s) if res.is_a?(Hash)
|
|
108
|
+
res ||= {}
|
|
109
|
+
meta_val = res["_meta"]
|
|
110
|
+
body = res["body"]
|
|
111
|
+
content = res["content"]
|
|
112
|
+
|
|
113
|
+
case format
|
|
114
|
+
when "markdown" then { meta: meta_val || {}, body: body.to_s, content: nil }
|
|
115
|
+
when "text" then { meta: {}, body: body.to_s, content: nil }
|
|
116
|
+
when "json", "yaml"
|
|
117
|
+
if !content.nil?
|
|
118
|
+
{ meta: meta_val || {}, body: nil, content: content }
|
|
119
|
+
elsif !body.nil?
|
|
120
|
+
{ meta: {}, body: body.to_s, content: nil }
|
|
121
|
+
else
|
|
122
|
+
raise Textus::UsageError.new("intake for #{format} returned neither content nor body")
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
raise Textus::UsageError.new("unknown format #{format.inspect}")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
private_class_method :normalize_action_result
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
Textus::Application::UseCase.register(:refresh, Textus::Application::Write::RefreshWorker, caps: :write)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require_relative "authority_gate"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Application
|
|
5
|
+
module Write
|
|
6
|
+
module Reject
|
|
7
|
+
def self.call(*, session:, ctx:, caps:, **)
|
|
8
|
+
Impl.new(
|
|
9
|
+
ctx: ctx, caps: caps,
|
|
10
|
+
writer: session.envelope_writer,
|
|
11
|
+
hook_context: session.hook_context
|
|
12
|
+
).call(*, **)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Impl
|
|
16
|
+
include AuthorityGate
|
|
17
|
+
|
|
18
|
+
def initialize(ctx:, caps:, writer:, hook_context:)
|
|
19
|
+
@ctx = ctx
|
|
20
|
+
@caps = caps
|
|
21
|
+
@manifest = caps.manifest
|
|
22
|
+
@writer = writer
|
|
23
|
+
@events = caps.events
|
|
24
|
+
@authorizer = caps.authorizer
|
|
25
|
+
@hook_context = hook_context
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call(pending_key)
|
|
29
|
+
assert_accept_authority!("reject")
|
|
30
|
+
|
|
31
|
+
mentry = @manifest.resolver.resolve(pending_key).entry
|
|
32
|
+
unless mentry.in_proposal_zone?
|
|
33
|
+
raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
env = Textus::Application::Read::Get::Impl.new(
|
|
37
|
+
ctx: @ctx, caps: @caps,
|
|
38
|
+
).call(pending_key)
|
|
39
|
+
proposal = env.meta&.dig("proposal") or
|
|
40
|
+
raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
41
|
+
target_key = proposal["target_key"] or
|
|
42
|
+
raise ProposalError.new("proposal missing target_key")
|
|
43
|
+
|
|
44
|
+
Textus::Application::Write::Delete::Impl.new(
|
|
45
|
+
ctx: @ctx, caps: @caps, writer: @writer,
|
|
46
|
+
hook_context: @hook_context
|
|
47
|
+
).call(pending_key, suppress_events: true)
|
|
48
|
+
|
|
49
|
+
@events.publish(:proposal_rejected,
|
|
50
|
+
ctx: @hook_context,
|
|
51
|
+
key: pending_key,
|
|
52
|
+
target_key: target_key)
|
|
53
|
+
|
|
54
|
+
{ "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
Textus::Application::UseCase.register(:reject, Textus::Application::Write::Reject, caps: :write)
|
data/lib/textus/boot.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Textus
|
|
|
26
26
|
"edit files in identity/working zones, then 'textus put KEY --as=#{name}'"
|
|
27
27
|
end,
|
|
28
28
|
proposer: lambda do |name, manifest|
|
|
29
|
-
authority = manifest.roles_with_kind(:accept_authority).first || "accept_authority"
|
|
29
|
+
authority = manifest.policy.roles_with_kind(:accept_authority).first || "accept_authority"
|
|
30
30
|
"propose changes by writing review.* entries with --as=#{name} and a 'proposal:' frontmatter block; " \
|
|
31
31
|
"the #{authority} role runs 'textus accept' to apply"
|
|
32
32
|
end,
|
|
@@ -39,7 +39,7 @@ module Textus
|
|
|
39
39
|
}.freeze
|
|
40
40
|
|
|
41
41
|
def self.write_flows_for(manifest)
|
|
42
|
-
manifest.role_mapping.each_with_object({}) do |(name, kind), acc|
|
|
42
|
+
manifest.policy.role_mapping.each_with_object({}) do |(name, kind), acc|
|
|
43
43
|
tmpl = WRITE_FLOW_TEMPLATES[kind]
|
|
44
44
|
acc[name] = tmpl.call(name, manifest) if tmpl
|
|
45
45
|
end
|
|
@@ -120,11 +120,11 @@ module Textus
|
|
|
120
120
|
"summary" => "delta since cursor — changed entries, stale, pending review, doctor summary" },
|
|
121
121
|
].freeze
|
|
122
122
|
|
|
123
|
-
def self.agent_quickstart(manifest,
|
|
124
|
-
proposer_roles = manifest.roles_with_kind(:proposer)
|
|
123
|
+
def self.agent_quickstart(manifest, session)
|
|
124
|
+
proposer_roles = manifest.policy.roles_with_kind(:proposer)
|
|
125
125
|
agent_role = proposer_roles.first
|
|
126
126
|
|
|
127
|
-
writable_zones = manifest.zones.each_with_object([]) do |(zname, writers), acc|
|
|
127
|
+
writable_zones = manifest.data.zones.each_with_object([]) do |(zname, writers), acc|
|
|
128
128
|
acc << zname if agent_role && writers.include?(agent_role)
|
|
129
129
|
end
|
|
130
130
|
|
|
@@ -135,7 +135,7 @@ module Textus
|
|
|
135
135
|
"write_verbs" => agent_role ? ["put KEY --as=#{agent_role} --stdin"] : [],
|
|
136
136
|
"writable_zones" => writable_zones,
|
|
137
137
|
"propose_zone" => propose_zone,
|
|
138
|
-
"latest_seq" =>
|
|
138
|
+
"latest_seq" => session.write_caps.audit_log.latest_seq,
|
|
139
139
|
}
|
|
140
140
|
end
|
|
141
141
|
|
|
@@ -144,29 +144,30 @@ module Textus
|
|
|
144
144
|
"role_resolution" => {
|
|
145
145
|
"summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
|
|
146
146
|
"default 'human'",
|
|
147
|
-
"roles" => manifest.role_mapping.keys,
|
|
147
|
+
"roles" => manifest.policy.role_mapping.keys,
|
|
148
148
|
"ref" => "SPEC.md §5",
|
|
149
149
|
},
|
|
150
150
|
)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
def self.run(
|
|
153
|
+
def self.run(session)
|
|
154
|
+
manifest = session.read_caps.manifest
|
|
154
155
|
{
|
|
155
156
|
"protocol" => PROTOCOL_ID,
|
|
156
|
-
"store_root" =>
|
|
157
|
-
"zones" => zones_for(
|
|
158
|
-
"entries" => entries_for(
|
|
159
|
-
"hooks" => hooks_for(
|
|
160
|
-
"write_flows" => write_flows_for(
|
|
157
|
+
"store_root" => session.read_caps.root,
|
|
158
|
+
"zones" => zones_for(manifest),
|
|
159
|
+
"entries" => entries_for(manifest),
|
|
160
|
+
"hooks" => hooks_for(session),
|
|
161
|
+
"write_flows" => write_flows_for(manifest),
|
|
161
162
|
"cli_verbs" => CLI_VERBS.map(&:dup),
|
|
162
|
-
"agent_protocol" => agent_protocol(
|
|
163
|
-
"agent_quickstart" => agent_quickstart(
|
|
163
|
+
"agent_protocol" => agent_protocol(manifest),
|
|
164
|
+
"agent_quickstart" => agent_quickstart(manifest, session),
|
|
164
165
|
"docs" => { "spec" => "SPEC.md", "example" => "examples/claude-plugin/" },
|
|
165
166
|
}
|
|
166
167
|
end
|
|
167
168
|
|
|
168
|
-
def self.zones_for(
|
|
169
|
-
|
|
169
|
+
def self.zones_for(manifest)
|
|
170
|
+
manifest.data.zones.map do |name, writers|
|
|
170
171
|
row = { "name" => name, "writers" => Array(writers) }
|
|
171
172
|
purpose = ZONE_PURPOSES[name]
|
|
172
173
|
row["purpose"] = purpose if purpose
|
|
@@ -174,9 +175,9 @@ module Textus
|
|
|
174
175
|
end
|
|
175
176
|
end
|
|
176
177
|
|
|
177
|
-
def self.entries_for(
|
|
178
|
-
|
|
179
|
-
derived =
|
|
178
|
+
def self.entries_for(manifest)
|
|
179
|
+
manifest.data.entries.map do |e|
|
|
180
|
+
derived = manifest.policy.zone_kinds(e.zone).include?(:generator)
|
|
180
181
|
{
|
|
181
182
|
"key" => e.key,
|
|
182
183
|
"zone" => e.zone,
|
|
@@ -192,16 +193,13 @@ module Textus
|
|
|
192
193
|
end
|
|
193
194
|
end
|
|
194
195
|
|
|
195
|
-
def self.hooks_for(
|
|
196
|
-
bus = store.bus
|
|
196
|
+
def self.hooks_for(session)
|
|
197
197
|
sections = {}
|
|
198
|
-
Hooks::
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
sections[event.to_s] = bus.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
|
|
204
|
-
end
|
|
198
|
+
Hooks::RpcRegistry::EVENTS.each_key do |event|
|
|
199
|
+
sections[event.to_s] = session.rpc.names(event).map(&:to_s).sort
|
|
200
|
+
end
|
|
201
|
+
Hooks::EventBus::EVENTS.each_key do |event|
|
|
202
|
+
sections[event.to_s] = session.events.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
|
|
205
203
|
end
|
|
206
204
|
sections
|
|
207
205
|
end
|
|
@@ -63,7 +63,7 @@ module Textus
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# rubocop:disable Metrics/ParameterLists
|
|
66
|
-
def self.run(mentry:, manifest:, reader:, lister:,
|
|
66
|
+
def self.run(mentry:, manifest:, reader:, lister:, rpc:, template_loader:,
|
|
67
67
|
transform_context: nil, inject_boot: nil)
|
|
68
68
|
# 1. Load sources + project + reduce
|
|
69
69
|
data =
|
|
@@ -72,7 +72,7 @@ module Textus
|
|
|
72
72
|
reader: reader,
|
|
73
73
|
spec: mentry.source.to_h.transform_keys(&:to_s),
|
|
74
74
|
lister: lister,
|
|
75
|
-
|
|
75
|
+
rpc: rpc,
|
|
76
76
|
transform_context: transform_context,
|
|
77
77
|
).run
|
|
78
78
|
else
|
|
@@ -86,7 +86,7 @@ module Textus
|
|
|
86
86
|
bytes = klass.new(template_loader: template_loader).call(mentry: mentry, data: data)
|
|
87
87
|
|
|
88
88
|
# 3. Write (idempotent: skip if only generated_at would differ)
|
|
89
|
-
target_path = Key::Path.resolve(manifest, mentry)
|
|
89
|
+
target_path = Key::Path.resolve(manifest.data, mentry)
|
|
90
90
|
FileUtils.mkdir_p(File.dirname(target_path))
|
|
91
91
|
write_if_changed(target_path, bytes, mentry.format)
|
|
92
92
|
|
|
@@ -14,8 +14,8 @@ module Textus
|
|
|
14
14
|
option :limit, "--limit=N"
|
|
15
15
|
|
|
16
16
|
def call(store)
|
|
17
|
-
ops =
|
|
18
|
-
since_time = since && Textus::Application::
|
|
17
|
+
ops = session_for(store)
|
|
18
|
+
since_time = since && Textus::Application::Read::Audit.parse_since(since, now: ops.ctx.now)
|
|
19
19
|
rows = ops.audit(
|
|
20
20
|
key: key_filter,
|
|
21
21
|
zone: zone,
|
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
|
|
9
9
|
def call(store)
|
|
10
10
|
key = positional.shift or raise UsageError.new("blame requires a key")
|
|
11
|
-
rows =
|
|
11
|
+
rows = session_for(store).blame(key: key, limit: limit&.to_i)
|
|
12
12
|
emit({ "verb" => "blame", "key" => key, "rows" => rows })
|
|
13
13
|
end
|
|
14
14
|
end
|
data/lib/textus/cli/verb/boot.rb
CHANGED
|
@@ -8,8 +8,8 @@ module Textus
|
|
|
8
8
|
|
|
9
9
|
def call(store)
|
|
10
10
|
Textus::Infra::BuildLock.with(root: store.root) do
|
|
11
|
-
role = store.manifest.roles_with_kind(:generator).first || "builder"
|
|
12
|
-
ops =
|
|
11
|
+
role = store.manifest.policy.roles_with_kind(:generator).first || "builder"
|
|
12
|
+
ops = store.session(role: role)
|
|
13
13
|
result = ops.publish(prefix: prefix)
|
|
14
14
|
emit(result)
|
|
15
15
|
end
|
data/lib/textus/cli/verb/deps.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
|
|
9
9
|
def call(store)
|
|
10
10
|
check_list = checks&.split(",")&.map(&:strip)
|
|
11
|
-
res = Textus::Doctor.run(store, checks: check_list)
|
|
11
|
+
res = Textus::Doctor.run(Textus::Session.for(store), checks: check_list)
|
|
12
12
|
emit(res, exit_code: res["ok"] ? 0 : 1)
|
|
13
13
|
end
|
|
14
14
|
end
|
data/lib/textus/cli/verb/get.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
|
|
9
9
|
def call(store)
|
|
10
10
|
key = positional.shift or raise UsageError.new("get requires a key")
|
|
11
|
-
result =
|
|
11
|
+
result = session_for(store).get_or_refresh(key)
|
|
12
12
|
raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
|
|
13
13
|
|
|
14
14
|
emit(result.to_h_for_wire)
|
|
@@ -27,15 +27,14 @@ module Textus
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
30
|
-
callable = store.bus.rpc_callable(:resolve_intake, name)
|
|
31
30
|
|
|
32
31
|
begin
|
|
33
|
-
Timeout.timeout(Textus::Application::
|
|
34
|
-
|
|
32
|
+
Timeout.timeout(Textus::Application::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS) do
|
|
33
|
+
store.rpc.invoke(:resolve_intake, name, caps: nil, config: {}, args: args)
|
|
35
34
|
end
|
|
36
35
|
rescue Timeout::Error
|
|
37
36
|
raise UsageError.new(
|
|
38
|
-
"hook run '#{name}' exceeded #{Textus::Application::
|
|
37
|
+
"hook run '#{name}' exceeded #{Textus::Application::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
39
38
|
)
|
|
40
39
|
rescue Textus::Error
|
|
41
40
|
raise
|
|
@@ -16,22 +16,19 @@ module Textus
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
rows = []
|
|
19
|
-
Textus::Hooks::
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
row["keys"] = Array(h[:keys]) if h[:keys]
|
|
30
|
-
rows << row
|
|
31
|
-
end
|
|
19
|
+
Textus::Hooks::RpcRegistry::EVENTS.each_key do |event|
|
|
20
|
+
store.rpc.names(event).each do |name|
|
|
21
|
+
rows << { "event" => event.to_s, "mode" => "rpc", "name" => name.to_s }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
Textus::Hooks::EventBus::EVENTS.each_key do |event|
|
|
25
|
+
store.events.pubsub_handlers(event).each do |h|
|
|
26
|
+
row = { "event" => event.to_s, "mode" => "pubsub", "name" => h[:name].to_s }
|
|
27
|
+
row["keys"] = Array(h[:keys]) if h[:keys]
|
|
28
|
+
rows << row
|
|
32
29
|
end
|
|
33
30
|
end
|
|
34
|
-
store.manifest.entries.each do |e|
|
|
31
|
+
store.manifest.data.entries.each do |e|
|
|
35
32
|
(e.respond_to?(:events) ? e.events : {}).each do |evt, defs|
|
|
36
33
|
Array(defs).each do |defn|
|
|
37
34
|
next unless defn["exec"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class KeyDelete < Verb
|
|
5
|
+
command_name "delete"
|
|
6
|
+
parent_group Group::Key
|
|
7
|
+
|
|
8
|
+
option :as_flag, "--as=ROLE"
|
|
9
|
+
option :dry_run, "--dry-run"
|
|
10
|
+
option :prefix, "--prefix"
|
|
11
|
+
|
|
12
|
+
def call(store)
|
|
13
|
+
if prefix
|
|
14
|
+
p = positional.shift or raise UsageError.new("key delete --prefix requires <prefix>")
|
|
15
|
+
emit(session_for(store).key_delete_prefix(prefix: p, dry_run: dry_run || false).to_h)
|
|
16
|
+
else
|
|
17
|
+
key = positional.shift or raise UsageError.new("key delete requires <key>")
|
|
18
|
+
emit(session_for(store).delete(key))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/textus/cli/verb/list.rb
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
# Launches the MCP stdio server in the current process. Blocks on
|
|
5
|
+
# stdin; never returns until stdin closes.
|
|
6
|
+
class MCPServe < Verb
|
|
7
|
+
command_name "serve"
|
|
8
|
+
parent_group Group::MCP
|
|
9
|
+
|
|
10
|
+
def call(store)
|
|
11
|
+
Textus::MCP::Server.new(store: store, stdin: @stdin, stdout: @stdout).run
|
|
12
|
+
0
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Migrate < Verb
|
|
5
|
+
command_name "migrate"
|
|
6
|
+
|
|
7
|
+
option :as_flag, "--as=ROLE"
|
|
8
|
+
option :dry_run, "--dry-run"
|
|
9
|
+
|
|
10
|
+
def call(store)
|
|
11
|
+
path = positional.shift or raise UsageError.new("migrate requires <plan.yaml>")
|
|
12
|
+
plan_yaml = File.read(path)
|
|
13
|
+
emit(session_for(store).migrate(plan_yaml: plan_yaml, dry_run: dry_run || false).to_h)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/textus/cli/verb/mv.rb
CHANGED
|
@@ -7,11 +7,19 @@ module Textus
|
|
|
7
7
|
|
|
8
8
|
option :as_flag, "--as=ROLE"
|
|
9
9
|
option :dry_run, "--dry-run"
|
|
10
|
+
option :prefix, "--prefix"
|
|
10
11
|
|
|
11
12
|
def call(store)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if prefix
|
|
14
|
+
from_p = positional.shift or raise UsageError.new("mv --prefix requires <from-prefix> <to-prefix>")
|
|
15
|
+
to_p = positional.shift or raise UsageError.new("mv --prefix requires <from-prefix> <to-prefix>")
|
|
16
|
+
emit(session_for(store).key_mv_prefix(from_prefix: from_p, to_prefix: to_p,
|
|
17
|
+
dry_run: dry_run || false).to_h)
|
|
18
|
+
else
|
|
19
|
+
old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
20
|
+
new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
21
|
+
emit(session_for(store).mv(old_key, new_key, dry_run: dry_run || false))
|
|
22
|
+
end
|
|
15
23
|
end
|
|
16
24
|
end
|
|
17
25
|
end
|