textus 0.52.0 → 0.53.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 +25 -0
- data/README.md +62 -54
- data/SPEC.md +62 -187
- data/docs/architecture/README.md +88 -77
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +53 -45
- data/lib/textus/command.rb +36 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +11 -11
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +17 -9
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +39 -35
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +90 -73
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +17 -11
- data/lib/textus/manifest/schema/validator.rb +24 -22
- data/lib/textus/manifest/schema/vocabulary.rb +1 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +7 -5
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
- data/lib/textus/ports/queue.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -3
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +132 -118
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "optparse"
|
|
3
|
+
|
|
4
|
+
module Textus
|
|
5
|
+
module Surfaces
|
|
6
|
+
class CLI
|
|
7
|
+
# Subclasses must implement #call(store) and return an integer exit code.
|
|
8
|
+
# Use #emit(obj) for normal JSON output (returns 0).
|
|
9
|
+
# Subclasses that don't need a Textus store (e.g. Init) override
|
|
10
|
+
# `.needs_store?` to return false; dispatch will pass nil instead.
|
|
11
|
+
class Verb
|
|
12
|
+
class << self
|
|
13
|
+
def option(name, optspec)
|
|
14
|
+
options << [name, optspec]
|
|
15
|
+
attr_accessor(name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def options
|
|
19
|
+
@options ||= []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def needs_store?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Declarative CLI name. Reader returns the registered name (or nil
|
|
27
|
+
# for verbs that aren't directly invokable, like the abstract
|
|
28
|
+
# Verb/Group base classes). Writer registers it.
|
|
29
|
+
def command_name(name = nil)
|
|
30
|
+
if name.nil?
|
|
31
|
+
@command_name
|
|
32
|
+
else
|
|
33
|
+
@command_name = name.to_s
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Declares that this verb is a subcommand of `group_klass`. When
|
|
38
|
+
# set, the verb is NOT a top-level CLI verb — it's listed under
|
|
39
|
+
# the group's subcommands instead.
|
|
40
|
+
def parent_group(group_klass = nil)
|
|
41
|
+
if group_klass.nil?
|
|
42
|
+
@parent_group
|
|
43
|
+
else
|
|
44
|
+
@parent_group = group_klass
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inherited(subclass)
|
|
49
|
+
super
|
|
50
|
+
subclass.instance_variable_set(:@options, [])
|
|
51
|
+
subclass.instance_variable_set(:@command_name, nil)
|
|
52
|
+
subclass.instance_variable_set(:@parent_group, nil)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Recursive subclass enumeration. Ruby 3.1 ships Class#subclasses
|
|
56
|
+
# but not Class#descendants, so we expand it ourselves.
|
|
57
|
+
def descendants
|
|
58
|
+
subclasses.flat_map { |k| [k] + k.descendants }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize(stdin:, stdout:, stderr:, cwd: nil)
|
|
63
|
+
@stdin = stdin
|
|
64
|
+
@stdout = stdout
|
|
65
|
+
@stderr = stderr
|
|
66
|
+
@cwd = cwd
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse(argv)
|
|
70
|
+
fmt = "json"
|
|
71
|
+
OptionParser.new do |o|
|
|
72
|
+
self.class.options.each do |name, optspec|
|
|
73
|
+
o.on(optspec) { |v| public_send(:"#{name}=", v) }
|
|
74
|
+
end
|
|
75
|
+
o.on("--output=FMT") { |v| fmt = v }
|
|
76
|
+
o.on("--format=FMT") { |_v| raise FlagRenamed.new("--format", "--output") }
|
|
77
|
+
end.permute!(argv)
|
|
78
|
+
raise UsageError.new("only --output=json is supported in v1") unless fmt == "json"
|
|
79
|
+
|
|
80
|
+
@positional = argv.dup
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
attr_reader :positional
|
|
84
|
+
|
|
85
|
+
# Hashes get "protocol" => PROTOCOL prepended unless they already
|
|
86
|
+
# carry one (Store envelopes do). Caller's value wins on collision.
|
|
87
|
+
def emit(obj, exit_code: 0)
|
|
88
|
+
payload = obj.is_a?(Hash) ? { "protocol" => PROTOCOL }.merge(obj) : obj
|
|
89
|
+
@stdout.puts(JSON.generate(payload))
|
|
90
|
+
exit_code
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Resolves the active role for this invocation. Honors the verb's
|
|
94
|
+
# `--as` flag if declared, then TEXTUS_ROLE, then the project default.
|
|
95
|
+
# Pass `default:` to override the fallback (e.g. MCPServe uses AGENT).
|
|
96
|
+
def resolved_role(store, default: Role::DEFAULT)
|
|
97
|
+
flag = respond_to?(:as_flag) ? as_flag : nil
|
|
98
|
+
Role.resolve(flag: flag, env: ENV, root: store.root, default: default)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Builds a Command from spec + inputs and dispatches through Gate.
|
|
102
|
+
def gate_dispatch(cmd, store)
|
|
103
|
+
store.gate.dispatch(cmd)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The input stream — the source for a `cli_stdin` envelope (ADR 0068).
|
|
107
|
+
attr_reader :stdin
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "optparse"
|
|
3
|
+
|
|
4
|
+
module Textus
|
|
5
|
+
module Surfaces
|
|
6
|
+
class CLI
|
|
7
|
+
# Auto-derived verb table. Every CLI::Verb (or Group) subclass that
|
|
8
|
+
# declares `command_name "X"` and has no `parent_group` is a top-level
|
|
9
|
+
# verb. Sorted alphabetically for stable help output. Adding a new
|
|
10
|
+
# verb requires only a new file declaring its `command_name`.
|
|
11
|
+
#
|
|
12
|
+
# `k.name` gates out anonymous (Class.new) subclasses: real verbs are always
|
|
13
|
+
# named constants (generated Gen* or hand-authored classes), so this is a
|
|
14
|
+
# no-op in production but keeps throwaway test fixtures from leaking into the
|
|
15
|
+
# registry (and tripping the reconciliation guards order-dependently).
|
|
16
|
+
def self.verbs
|
|
17
|
+
Runner.install!
|
|
18
|
+
Verb.descendants
|
|
19
|
+
.select { |k| k.name && k.command_name && k.parent_group.nil? }
|
|
20
|
+
.sort_by(&:command_name)
|
|
21
|
+
.to_h { |k| [k.command_name, k] }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, cwd: Dir.pwd)
|
|
25
|
+
new(stdin: stdin, stdout: stdout, stderr: stderr, cwd: cwd).run(argv)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(stdin:, stdout:, stderr:, cwd:)
|
|
29
|
+
@stdin = stdin
|
|
30
|
+
@stdout = stdout
|
|
31
|
+
@stderr = stderr
|
|
32
|
+
@cwd = cwd
|
|
33
|
+
@root_arg = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run(argv)
|
|
37
|
+
# `--root` is a global, position-agnostic option: pull it out of argv
|
|
38
|
+
# wherever it appears so it works uniformly before OR after any verb or
|
|
39
|
+
# group (e.g. both `textus --root=X hook list` and
|
|
40
|
+
# `textus hook list --root=X`). Without this, `order!` below only sees
|
|
41
|
+
# options before the first verb token, so a trailing `--root` reached the
|
|
42
|
+
# verb's own parser and raised InvalidOption (#161 F5). TEXTUS_ROOT already
|
|
43
|
+
# works everywhere via Store.discover, so this brings the flag to parity.
|
|
44
|
+
@root_arg = extract_root!(argv)
|
|
45
|
+
|
|
46
|
+
# Define --version/--help ourselves so OptionParser doesn't intercept them
|
|
47
|
+
# with its built-in handlers (which print "version unknown" and a bare usage
|
|
48
|
+
# line, then exit before we ever reach the verb dispatch below).
|
|
49
|
+
show_version = false
|
|
50
|
+
show_help = false
|
|
51
|
+
OptionParser.new do |o|
|
|
52
|
+
o.on("--version", "-v") { show_version = true }
|
|
53
|
+
o.on("--help", "-h") { show_help = true }
|
|
54
|
+
end.order!(argv)
|
|
55
|
+
|
|
56
|
+
return @stdout.puts(VERSION) || 0 if show_version
|
|
57
|
+
return print_help || 0 if show_help
|
|
58
|
+
|
|
59
|
+
verb = argv.shift
|
|
60
|
+
raise UsageError.new("missing verb") if verb.nil?
|
|
61
|
+
|
|
62
|
+
klass = self.class.verbs[verb] or raise UsageError.new("unknown verb: #{verb}")
|
|
63
|
+
coerce_exit_code(dispatch(klass, argv))
|
|
64
|
+
rescue Textus::Error => e
|
|
65
|
+
emit_error(e)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Remove the first `--root=PATH` or `--root PATH` token from argv (anywhere)
|
|
71
|
+
# and return its value, or nil if absent. Mutates argv in place.
|
|
72
|
+
def extract_root!(argv)
|
|
73
|
+
i = argv.index { |a| a == "--root" || a.start_with?("--root=") }
|
|
74
|
+
return nil unless i
|
|
75
|
+
|
|
76
|
+
tok = argv[i]
|
|
77
|
+
if tok.start_with?("--root=")
|
|
78
|
+
argv.delete_at(i)
|
|
79
|
+
tok.delete_prefix("--root=")
|
|
80
|
+
else
|
|
81
|
+
val = argv[i + 1]
|
|
82
|
+
raise UsageError.new("--root requires a PATH") if val.nil? || val.start_with?("-")
|
|
83
|
+
|
|
84
|
+
argv.delete_at(i + 1)
|
|
85
|
+
argv.delete_at(i)
|
|
86
|
+
val
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def coerce_exit_code(value)
|
|
91
|
+
case value
|
|
92
|
+
when Integer then value
|
|
93
|
+
when true, nil then 0
|
|
94
|
+
when false then 1
|
|
95
|
+
else
|
|
96
|
+
@stderr.puts("warning: verb returned non-Integer #{value.class}; treating as 0")
|
|
97
|
+
0
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def store
|
|
102
|
+
@store ||= Store.discover(@cwd, root: @root_arg)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def dispatch(klass, argv)
|
|
106
|
+
v = klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
|
|
107
|
+
v.parse(argv)
|
|
108
|
+
v.call(klass.needs_store? ? store : nil)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def emit_error(err)
|
|
112
|
+
@stdout.puts(JSON.generate(err.to_envelope))
|
|
113
|
+
@stderr.puts("#{err.code}: #{err.message}")
|
|
114
|
+
@stderr.puts(" → #{err.hint}") if err.hint
|
|
115
|
+
err.exit_code
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def print_help
|
|
119
|
+
@stdout.puts <<~HELP
|
|
120
|
+
textus #{VERSION} — reference implementation of #{PROTOCOL}
|
|
121
|
+
|
|
122
|
+
Usage (json output is the default):
|
|
123
|
+
textus list [--prefix=KEY] [--lane=LANE]
|
|
124
|
+
textus where KEY
|
|
125
|
+
textus get KEY
|
|
126
|
+
textus put KEY --stdin --as=ROLE
|
|
127
|
+
textus propose KEY --stdin --as=ROLE
|
|
128
|
+
textus accept KEY --as=ROLE
|
|
129
|
+
textus reject KEY --as=ROLE
|
|
130
|
+
textus audit [--key=K] [--lane=LANE] [--role=R] [--verb=V] [--since=X] [--correlation-id=ID] [--limit=N]
|
|
131
|
+
textus blame KEY [--limit=N]
|
|
132
|
+
textus pulse [--since=N]
|
|
133
|
+
textus boot
|
|
134
|
+
textus doctor
|
|
135
|
+
textus drain [PREFIX] --as=ROLE
|
|
136
|
+
textus watch
|
|
137
|
+
textus jobs
|
|
138
|
+
|
|
139
|
+
textus key {delete,mv,mv-prefix,delete-prefix,uid}
|
|
140
|
+
textus rule {explain,lint,list}
|
|
141
|
+
textus schema {diff,init,migrate,show}
|
|
142
|
+
textus data {mv}
|
|
143
|
+
textus mcp {serve}
|
|
144
|
+
HELP
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
module MCP
|
|
4
|
+
# Derives the entire MCP tool surface from the per-verb contracts
|
|
5
|
+
# (ADR 0039). `tool_schemas` feeds tools/list; `call` is the generic
|
|
6
|
+
# tools/call dispatch: map JSON args -> (positional, keyword) per the
|
|
7
|
+
# contract, invoke the verb through the role scope, then shape the
|
|
8
|
+
# return value with the contract's default view. No per-tool code.
|
|
9
|
+
module Catalog
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
WRITE_VERBS = %i[
|
|
13
|
+
put propose key_delete key_mv accept reject enqueue
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
MAINTENANCE_VERBS = %i[
|
|
17
|
+
data_mv key_mv_prefix key_delete_prefix drain rule_lint
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
# Contracts of every MCP-surfaced verb, in Dispatcher order.
|
|
21
|
+
def specs
|
|
22
|
+
Textus::Action::VERBS.values
|
|
23
|
+
.select { |k| mcp_surfaced?(k) }
|
|
24
|
+
.map(&:contract)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tool_schemas
|
|
28
|
+
specs.map do |s|
|
|
29
|
+
{ name: s.verb.to_s, description: s.summary, inputSchema: s.input_schema }
|
|
30
|
+
end.freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def names
|
|
34
|
+
specs.map { |s| s.verb.to_s }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# MCP-surfaced read verbs, by Dispatcher class namespace — the agent's
|
|
38
|
+
# real read/discovery surface. `boot.agent_quickstart.read_verbs` derives
|
|
39
|
+
# from this so it can never advertise a verb the agent cannot call, nor
|
|
40
|
+
# omit one it can (ADR 0056). Excludes write/maintenance verbs by verb
|
|
41
|
+
# identity (routing may be legacy UseCases or Dispatch::Actions).
|
|
42
|
+
def read_verbs
|
|
43
|
+
Textus::Action::VERBS
|
|
44
|
+
.reject { |verb, _klass| WRITE_VERBS.include?(verb) || MAINTENANCE_VERBS.include?(verb) }
|
|
45
|
+
.select { |_verb, klass| mcp_surfaced?(klass) }
|
|
46
|
+
.keys.map(&:to_s)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# MCP-surfaced write verbs, by Dispatcher class namespace — the mirror of
|
|
50
|
+
# read_verbs for the write side. `boot.agent_quickstart.write_verbs` derives
|
|
51
|
+
# from this so it advertises bare verb names the agent can call (no `--as`/
|
|
52
|
+
# `--stdin` CLI framing), finishing the de-CLI-ing of the agent surface
|
|
53
|
+
# (ADR 0056, ADR 0057).
|
|
54
|
+
def write_verbs
|
|
55
|
+
Textus::Action::VERBS
|
|
56
|
+
.select { |verb, klass| WRITE_VERBS.include?(verb) && mcp_surfaced?(klass) }
|
|
57
|
+
.keys.map(&:to_s)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def mcp_surfaced?(klass)
|
|
61
|
+
klass.respond_to?(:contract?) && klass.contract? && klass.contract.mcp?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def call(name, session:, store:, args:) # rubocop:disable Metrics/AbcSize
|
|
65
|
+
klass = Textus::Action::VERBS[name.to_sym]
|
|
66
|
+
raise ToolError.new("unknown tool: #{name}") unless klass && mcp_surfaced?(klass)
|
|
67
|
+
|
|
68
|
+
spec = klass.contract
|
|
69
|
+
inputs = Textus::Contract::Binder.inputs_from_wire(spec, args)
|
|
70
|
+
|
|
71
|
+
invoke = lambda do |effective_inputs|
|
|
72
|
+
pos, kwargs = Textus::Contract::Binder.bind(spec, effective_inputs, session: session)
|
|
73
|
+
spec.args.select(&:positional).zip(pos).each { |a, v| kwargs[a.name] = v unless kwargs.key?(a.name) }
|
|
74
|
+
cmd_class = Textus::Gate::VERB_COMMAND.fetch(spec.verb) do
|
|
75
|
+
raise Textus::MCP::ToolError.new("unknown verb: #{spec.verb}")
|
|
76
|
+
end
|
|
77
|
+
merged = kwargs.merge(role: session.role)
|
|
78
|
+
filled = cmd_class.members.to_h { |m| [m, merged.key?(m) ? merged[m] : nil] }
|
|
79
|
+
cmd = cmd_class.new(**filled)
|
|
80
|
+
store.gate.dispatch(cmd)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
result = if spec.around
|
|
84
|
+
Textus::Contract::Around.with(spec.around, scope: store.as(session.role), inputs: inputs, session: session, &invoke)
|
|
85
|
+
else
|
|
86
|
+
invoke.call(inputs)
|
|
87
|
+
end
|
|
88
|
+
Textus::Contract::View.render(spec, :default, result, inputs)
|
|
89
|
+
rescue Textus::Contract::MissingArgs => e
|
|
90
|
+
raise ToolError.new("#{spec.verb}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
|
|
91
|
+
rescue ContractDrift, CursorExpired
|
|
92
|
+
raise
|
|
93
|
+
rescue Textus::Error => e
|
|
94
|
+
raise ToolError.new("#{name}: #{e.message}")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
module MCP
|
|
4
|
+
# Manifest fingerprint changed mid-session. Client should re-boot.
|
|
5
|
+
class ContractDrift < Textus::Error
|
|
6
|
+
JSONRPC_CODE = -32_001
|
|
7
|
+
|
|
8
|
+
def initialize(message, details: {})
|
|
9
|
+
super("contract_drift", message, details: details)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Audit cursor fell off the keep window. Client should re-boot and
|
|
14
|
+
# resume from the new latest_seq.
|
|
15
|
+
class CursorExpired < Textus::Error
|
|
16
|
+
JSONRPC_CODE = -32_002
|
|
17
|
+
|
|
18
|
+
def initialize(message, details: {})
|
|
19
|
+
super("cursor_expired", message, details: details)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Tool execution failed (validation, authorization, IO). Wraps an
|
|
24
|
+
# underlying Textus::Error or generic StandardError.
|
|
25
|
+
class ToolError < Textus::Error
|
|
26
|
+
JSONRPC_CODE = -32_000
|
|
27
|
+
|
|
28
|
+
def initialize(message, details: {})
|
|
29
|
+
super("tool_error", message, details: details)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Surfaces
|
|
5
|
+
module MCP
|
|
6
|
+
# Stdio JSON-RPC 2.0 server speaking MCP draft 2024-11-05. One line per
|
|
7
|
+
# message (NDJSON). Holds a single Session for the lifetime of stdin.
|
|
8
|
+
class Server
|
|
9
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
10
|
+
SERVER_INFO = { "name" => "textus", "version" => Textus::VERSION }.freeze
|
|
11
|
+
MAX_LINE_BYTES = 1_048_576 # 1 MB — protects against OOM from oversized tool calls
|
|
12
|
+
|
|
13
|
+
def initialize(store:, stdin: $stdin, stdout: $stdout, role: Textus::Role::DEFAULT)
|
|
14
|
+
@store = store
|
|
15
|
+
@stdin = stdin
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
@role = role
|
|
18
|
+
@session = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
@stdin.each_line do |line|
|
|
23
|
+
line = line.strip
|
|
24
|
+
next if line.empty?
|
|
25
|
+
|
|
26
|
+
handle_line(line)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def handle_line(line)
|
|
33
|
+
if line.bytesize > MAX_LINE_BYTES
|
|
34
|
+
emit_error(nil, -32_700, "message too large (#{line.bytesize} bytes, limit #{MAX_LINE_BYTES})")
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
msg = JSON.parse(line)
|
|
38
|
+
rescue JSON::ParserError => e
|
|
39
|
+
emit_error(nil, -32_700, "parse error: #{e.message}")
|
|
40
|
+
else
|
|
41
|
+
dispatch(msg)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def dispatch(msg)
|
|
45
|
+
rid = msg["id"]
|
|
46
|
+
case msg["method"]
|
|
47
|
+
when "initialize" then handle_initialize(rid, msg["params"] || {})
|
|
48
|
+
when "tools/list" then handle_tools_list(rid)
|
|
49
|
+
when "tools/call" then handle_tools_call(rid, msg["params"] || {})
|
|
50
|
+
when "ping" then emit_result(rid, {})
|
|
51
|
+
when "shutdown" then emit_result(rid, nil)
|
|
52
|
+
when "notifications/initialized" then nil
|
|
53
|
+
else emit_error(rid, -32_601, "method not found: #{msg["method"]}")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def handle_initialize(rid, _params)
|
|
58
|
+
# The acting role IS the resolved connection role (ADR 0040): the MCP
|
|
59
|
+
# transport defaults to `agent`, which can write the queue, so its
|
|
60
|
+
# propose_lane resolves directly. If a connection's role cannot propose,
|
|
61
|
+
# propose_lane is nil and the `propose` tool reports that honestly.
|
|
62
|
+
propose_lane = @store.manifest.policy.propose_lane_for(@role)
|
|
63
|
+
|
|
64
|
+
@session = Session.new(
|
|
65
|
+
role: @role,
|
|
66
|
+
cursor: @store.audit_log.latest_seq,
|
|
67
|
+
propose_lane: propose_lane,
|
|
68
|
+
contract_etag: contract_etag,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# ADR 0075: announce the connection to connect-time hooks with the
|
|
72
|
+
# resolved role. Distinct from :store_loaded (fired at Store.new under
|
|
73
|
+
# the default role, before any connection's role is known).
|
|
74
|
+
@store.steps.publish(
|
|
75
|
+
:session_opened,
|
|
76
|
+
ctx: Step::Context.new(scope: @store.as(@role)),
|
|
77
|
+
role: @role,
|
|
78
|
+
cursor: @session.cursor,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
emit_result(rid, {
|
|
82
|
+
"protocolVersion" => PROTOCOL_VERSION,
|
|
83
|
+
"serverInfo" => SERVER_INFO,
|
|
84
|
+
"capabilities" => { "tools" => {} },
|
|
85
|
+
})
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def handle_tools_list(rid)
|
|
89
|
+
emit_result(rid, { "tools" => Catalog.tool_schemas })
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def handle_tools_call(rid, params)
|
|
93
|
+
unless @session
|
|
94
|
+
emit_error(rid, -32_002, "session not initialized; call 'initialize' first")
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
name = params["name"]
|
|
99
|
+
args = params["arguments"] || {}
|
|
100
|
+
|
|
101
|
+
# ADR 0083: the contract-drift guard gates mutating verbs — every MCP
|
|
102
|
+
# verb that is NOT a pure read (Write:: + the destructive Maintenance::
|
|
103
|
+
# verbs drain/data_mv/key_*_prefix). Reads and boot bypass it (a stale
|
|
104
|
+
# read returns on-disk truth; boot re-orients). Keying on read_verbs
|
|
105
|
+
# (not write_verbs) keeps the destructive Maintenance:: verbs gated.
|
|
106
|
+
@session.check_etag!(contract_etag) unless Catalog.read_verbs.include?(name)
|
|
107
|
+
|
|
108
|
+
result = Catalog.call(name, session: @session, store: @store, args: args)
|
|
109
|
+
@session = @session.advance_cursor(@store.audit_log.latest_seq) if name == "pulse"
|
|
110
|
+
@session = @session.with(contract_etag: contract_etag) if name == "boot"
|
|
111
|
+
|
|
112
|
+
emit_result(rid, {
|
|
113
|
+
"content" => [{ "type" => "text", "text" => JSON.dump(result) }],
|
|
114
|
+
"isError" => false,
|
|
115
|
+
})
|
|
116
|
+
rescue ContractDrift => e
|
|
117
|
+
emit_error(rid, ContractDrift::JSONRPC_CODE, e.message)
|
|
118
|
+
rescue CursorExpired => e
|
|
119
|
+
emit_error(rid, CursorExpired::JSONRPC_CODE, e.message)
|
|
120
|
+
rescue ToolError => e
|
|
121
|
+
emit_error(rid, ToolError::JSONRPC_CODE, e.message)
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
emit_error(rid, -32_603, "internal: #{e.class}: #{e.message}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def contract_etag
|
|
127
|
+
Textus::Etag.for_contract(@store.root)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def emit_result(rid, result)
|
|
131
|
+
write({ "jsonrpc" => "2.0", "id" => rid, "result" => result })
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def emit_error(rid, code, message)
|
|
135
|
+
write({ "jsonrpc" => "2.0", "id" => rid, "error" => { "code" => code, "message" => message } })
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def write(obj)
|
|
139
|
+
@stdout.puts(JSON.dump(obj))
|
|
140
|
+
@stdout.flush
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surfaces
|
|
3
|
+
module MCP
|
|
4
|
+
# Kept for name stability (ADR 0039). The JSON schemas are DERIVED from
|
|
5
|
+
# per-verb contracts; this delegates to MCP::Catalog. The hand-written
|
|
6
|
+
# array is gone — a kwarg rename now updates the schema automatically (and
|
|
7
|
+
# the signature guard fails if the contract lags the use-case).
|
|
8
|
+
module ToolSchemas
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def all
|
|
12
|
+
Catalog.tool_schemas
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Surfaces
|
|
5
|
+
# Role-scoped identity carrier. Holds the acting identity (role,
|
|
6
|
+
# correlation_id, dry_run) bound to a container. All verb methods
|
|
7
|
+
# (put, get, accept, ...) are injected by textus.rb's define_method
|
|
8
|
+
# loop, which dispatches directly through Gate.
|
|
9
|
+
class RoleScope
|
|
10
|
+
attr_reader :container, :role, :correlation_id
|
|
11
|
+
|
|
12
|
+
def initialize(container:, role:, dry_run: false, correlation_id: nil)
|
|
13
|
+
@container = container
|
|
14
|
+
@role = role.to_s
|
|
15
|
+
@dry_run = dry_run
|
|
16
|
+
@correlation_id = correlation_id || SecureRandom.uuid
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def dry_run? = !!@dry_run
|
|
20
|
+
|
|
21
|
+
def with_role(role)
|
|
22
|
+
self.class.new(container: @container, role:, dry_run: @dry_run, correlation_id: @correlation_id)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def with_correlation_id(cid)
|
|
26
|
+
self.class.new(container: @container, role: @role, dry_run: @dry_run, correlation_id: cid)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def with_dry_run
|
|
30
|
+
self.class.new(container: @container, role: @role, dry_run: true, correlation_id: @correlation_id)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def hook_context
|
|
34
|
+
@hook_context ||= Textus::Step::Context.new(scope: self)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Surfaces
|
|
7
|
+
class Watcher
|
|
8
|
+
def initialize(container:)
|
|
9
|
+
@container = container
|
|
10
|
+
@queue = Textus::Ports::Queue.new(root: container.root)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def tick
|
|
14
|
+
Textus::Background::Planner::Plan.seed(
|
|
15
|
+
container: @container,
|
|
16
|
+
queue: @queue,
|
|
17
|
+
role: Textus::Role::AUTOMATION,
|
|
18
|
+
)
|
|
19
|
+
@queue.reclaim(now: Textus::Ports::Clock.new.now)
|
|
20
|
+
Textus::Background::Worker.for(container: @container, queue: @queue).drain
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run(poll: nil)
|
|
24
|
+
interval = poll || @container.manifest.data.worker_config[:poll]
|
|
25
|
+
lock = Textus::Ports::WatcherLock.new(@container.root)
|
|
26
|
+
lock.acquire
|
|
27
|
+
begin
|
|
28
|
+
loop do
|
|
29
|
+
tick
|
|
30
|
+
sleep(interval)
|
|
31
|
+
end
|
|
32
|
+
ensure
|
|
33
|
+
lock.release
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/textus/version.rb
CHANGED