openclacky 1.3.6 → 1.3.7
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 +30 -0
- data/lib/clacky/agent.rb +8 -1
- data/lib/clacky/agent_profile.rb +121 -53
- data/lib/clacky/brand_config.rb +309 -6
- data/lib/clacky/cli.rb +4 -57
- data/lib/clacky/default_extensions/coding/ext.yml +18 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
- data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
- data/lib/clacky/default_extensions/general/ext.yml +18 -0
- data/lib/clacky/default_extensions/git/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
- data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
- data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
- data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
- data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
- data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
- data/lib/clacky/extension/api_loader.rb +136 -0
- data/lib/clacky/extension/cli_commands.rb +226 -0
- data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
- data/lib/clacky/extension/hook_loader.rb +77 -0
- data/lib/clacky/extension/loader.rb +483 -0
- data/lib/clacky/extension/packager.rb +226 -0
- data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
- data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
- data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
- data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
- data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
- data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
- data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
- data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
- data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
- data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
- data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
- data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
- data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
- data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
- data/lib/clacky/extension/scaffold.rb +55 -0
- data/lib/clacky/extension/verifier.rb +196 -0
- data/lib/clacky/identity.rb +70 -0
- data/lib/clacky/locales/en.rb +19 -1
- data/lib/clacky/locales/zh.rb +19 -1
- data/lib/clacky/platform_http_client.rb +18 -15
- data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
- data/lib/clacky/server/channel.rb +6 -0
- data/lib/clacky/server/http_server.rb +313 -144
- data/lib/clacky/server/session_registry.rb +9 -2
- data/lib/clacky/skill_loader.rb +28 -1
- data/lib/clacky/ui2/ui_controller.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +748 -573
- data/lib/clacky/web/app.js +63 -15
- data/lib/clacky/web/auth.js +2 -0
- data/lib/clacky/web/components/notify.js +2 -0
- data/lib/clacky/web/components/sidebar.js +1 -5
- data/lib/clacky/web/core/ext.js +316 -79
- data/lib/clacky/web/features/backup/store.js +1 -0
- data/lib/clacky/web/features/backup/view.js +0 -1
- data/lib/clacky/web/features/brand/view.js +13 -5
- data/lib/clacky/web/features/extensions/store.js +184 -0
- data/lib/clacky/web/features/extensions/view.js +389 -0
- data/lib/clacky/web/features/new-session/store.js +186 -0
- data/lib/clacky/web/features/new-session/view.js +319 -0
- data/lib/clacky/web/features/share/view.js +63 -14
- data/lib/clacky/web/features/skills/store.js +3 -1
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/features/tasks/view.js +12 -0
- data/lib/clacky/web/features/trash/store.js +24 -6
- data/lib/clacky/web/features/trash/view.js +46 -9
- data/lib/clacky/web/features/workspace/store.js +2 -0
- data/lib/clacky/web/features/workspace/view.js +0 -1
- data/lib/clacky/web/i18n.js +118 -106
- data/lib/clacky/web/index.html +138 -188
- data/lib/clacky/web/sessions.js +64 -269
- data/lib/clacky/web/settings.js +1 -1
- data/lib/clacky/web/skills.js +2 -0
- data/lib/clacky/web/theme.js +2 -0
- data/lib/clacky/web/ws-dispatcher.js +1 -0
- data/lib/clacky/web/ws.js +2 -0
- data/lib/clacky.rb +21 -10
- metadata +55 -24
- data/benchmark/runner.rb +0 -441
- data/lib/clacky/api_extension_loader.rb +0 -168
- data/lib/clacky/default_agents/SOUL.md +0 -3
- data/lib/clacky/default_agents/USER.md +0 -1
- data/lib/clacky/default_agents/coding/profile.yml +0 -5
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_agents/general/profile.yml +0 -2
- data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
- data/lib/clacky/web/features/creator/store.js +0 -81
- data/lib/clacky/web/features/creator/view.js +0 -380
- /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
# `clacky ext <subcommand>` — manage extension containers (ext.yml).
|
|
8
|
+
#
|
|
9
|
+
# Containers contribute panels (WebUI) and api (HTTP backends). This command
|
|
10
|
+
# group scaffolds, verifies, lists and hot-reloads them.
|
|
11
|
+
class CliExtensionCommands < Thor
|
|
12
|
+
def self.exit_on_failure?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "new ID", "Scaffold a runnable hello-panel container at ~/.clacky/ext/local/ID/"
|
|
17
|
+
method_option :full, type: :boolean, default: false,
|
|
18
|
+
desc: "Generate a kitchen-sink container exercising all 7 contributes types"
|
|
19
|
+
def new(id)
|
|
20
|
+
path = Clacky::ExtensionScaffold.new_container(id, full: options[:full])
|
|
21
|
+
puts "Created extension container: #{path}"
|
|
22
|
+
if options[:full]
|
|
23
|
+
puts "It contributes panels/api/skills/agents/channels/patches/hooks. Read its README.md, run `clacky ext verify`, then reload the WebUI."
|
|
24
|
+
else
|
|
25
|
+
puts "Reload the WebUI page to see the panel. Edits to view.js and handler.rb take effect on the next request — no restart needed."
|
|
26
|
+
end
|
|
27
|
+
rescue ArgumentError => e
|
|
28
|
+
warn "Error: #{e.message}"
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "verify", "Resolve all containers across layers and report units + structured issues"
|
|
33
|
+
def verify
|
|
34
|
+
result = Clacky::ExtensionLoader.load_all
|
|
35
|
+
|
|
36
|
+
if result.units.empty? && result.errors.empty?
|
|
37
|
+
puts "No extension containers found."
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
result.units.each do |u|
|
|
42
|
+
case u.kind
|
|
43
|
+
when :panel
|
|
44
|
+
attach = Array(u.spec["attach"])
|
|
45
|
+
suffix = attach.empty? ? "" : ", attach=#{attach.inspect}"
|
|
46
|
+
puts "[OK] #{u.ext_id}/#{u.id} (panel#{suffix}, #{u.layer})"
|
|
47
|
+
when :api
|
|
48
|
+
puts "[OK] #{u.ext_id} (api → /api/ext/#{u.ext_id}/, #{u.layer})"
|
|
49
|
+
else
|
|
50
|
+
puts "[OK] #{u.ext_id}/#{u.id} (#{u.kind}, #{u.layer})"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
issues = Clacky::ExtensionVerifier.verify(result)
|
|
55
|
+
issues.each do |issue|
|
|
56
|
+
tag = issue.level == :error ? "[ERR]" : "[WARN]"
|
|
57
|
+
unit = issue.unit ? " #{issue.unit}" : ""
|
|
58
|
+
loc = issue.file ? " [#{issue.file}]" : ""
|
|
59
|
+
hint = issue.hint ? "\n hint: #{issue.hint}" : ""
|
|
60
|
+
puts "#{tag} #{issue.ext}#{unit} (#{issue.code}) — #{issue.message}#{loc}#{hint}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
exit 1 if issues.any? { |i| i.level == :error }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
desc "list", "List resolved containers and their contributed units"
|
|
67
|
+
def list
|
|
68
|
+
result = Clacky::ExtensionLoader.load_all
|
|
69
|
+
|
|
70
|
+
if result.units.empty?
|
|
71
|
+
puts "No extension units resolved."
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
grouped = result.units.group_by(&:ext_id)
|
|
76
|
+
grouped.each do |ext_id, units|
|
|
77
|
+
layer = units.first.layer
|
|
78
|
+
origin = units.first.origin
|
|
79
|
+
puts "#{ext_id} (#{layer}, origin=#{origin})"
|
|
80
|
+
units.each do |u|
|
|
81
|
+
case u.kind
|
|
82
|
+
when :panel
|
|
83
|
+
attach = Array(u.spec["attach"])
|
|
84
|
+
suffix = attach.empty? ? "" : " attach=#{attach.inspect}"
|
|
85
|
+
puts " panel #{u.id}#{suffix}"
|
|
86
|
+
when :api
|
|
87
|
+
puts " api → /api/ext/#{ext_id}/"
|
|
88
|
+
else
|
|
89
|
+
puts " #{u.kind.to_s.ljust(6)} #{u.id}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result.errors.each do |e|
|
|
95
|
+
puts "[ERR] #{e.ext_id} #{e.unit} — #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
desc "pack ID", "Package a local container into a distributable zip"
|
|
100
|
+
method_option :out, type: :string, desc: "Output directory for the zip (default: current dir)"
|
|
101
|
+
def pack(id)
|
|
102
|
+
out = options[:out] || Dir.pwd
|
|
103
|
+
res = Clacky::ExtensionPackager.pack(id, out_dir: out)
|
|
104
|
+
puts "Packed #{res.ext_id} → #{res.path}"
|
|
105
|
+
rescue Clacky::ExtensionPackager::Error => e
|
|
106
|
+
warn "Error: #{e.message}"
|
|
107
|
+
exit 1
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
desc "install SOURCE", "Install a packed container (local zip path or http(s) URL) into the installed layer"
|
|
111
|
+
method_option :force, type: :boolean, default: false, desc: "Overwrite an already-installed extension of the same id"
|
|
112
|
+
def install(source)
|
|
113
|
+
res = Clacky::ExtensionPackager.install(source, force: options[:force])
|
|
114
|
+
puts "Installed #{res.ext_id} → #{res.path}"
|
|
115
|
+
puts "Reload the WebUI page to see any panels. No restart needed."
|
|
116
|
+
rescue Clacky::ExtensionPackager::Error => e
|
|
117
|
+
warn "Error: #{e.message}"
|
|
118
|
+
exit 1
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
desc "publish ID", "Pack a local container and publish it to the OpenClacky marketplace"
|
|
122
|
+
method_option :force, type: :boolean, default: false, desc: "Publish a new version of an already-published extension"
|
|
123
|
+
method_option :status, type: :string, desc: "Publish status: draft or published"
|
|
124
|
+
method_option :changelog, type: :string, desc: "Release notes for this version"
|
|
125
|
+
def publish(id)
|
|
126
|
+
unless Clacky::Identity.load.bound?
|
|
127
|
+
warn "Error: this device is not bound to a platform account. Authorize it first to publish."
|
|
128
|
+
exit 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
brand = Clacky::BrandConfig.load
|
|
132
|
+
Dir.mktmpdir("clacky-ext-publish") do |tmp|
|
|
133
|
+
res = Clacky::ExtensionPackager.pack(id, out_dir: tmp)
|
|
134
|
+
zip_data = File.binread(res.path)
|
|
135
|
+
|
|
136
|
+
result = brand.upload_extension!(
|
|
137
|
+
res.ext_id, zip_data,
|
|
138
|
+
force: options[:force],
|
|
139
|
+
status: options[:status],
|
|
140
|
+
changelog: options[:changelog]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if result[:success]
|
|
144
|
+
ext = result[:extension] || {}
|
|
145
|
+
ver = (ext["latest_version"] || {})["version"]
|
|
146
|
+
puts "Published #{res.ext_id}#{ver ? " v#{ver}" : ""} → status=#{ext["status"]}"
|
|
147
|
+
elsif result[:already_exists]
|
|
148
|
+
warn "Error: #{res.ext_id} already published. Re-run with --force to publish a new version."
|
|
149
|
+
exit 1
|
|
150
|
+
else
|
|
151
|
+
warn "Error: #{result[:error]}"
|
|
152
|
+
exit 1
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
rescue Clacky::ExtensionPackager::Error => e
|
|
156
|
+
warn "Error: #{e.message}"
|
|
157
|
+
exit 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
desc "published", "List extensions you have published to the marketplace"
|
|
161
|
+
def published
|
|
162
|
+
brand = Clacky::BrandConfig.load
|
|
163
|
+
result = brand.fetch_my_extensions!
|
|
164
|
+
|
|
165
|
+
unless result[:success]
|
|
166
|
+
warn "Error: #{result[:error]}"
|
|
167
|
+
exit 1
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
extensions = result[:extensions]
|
|
171
|
+
if extensions.empty?
|
|
172
|
+
puts "You have not published any extensions yet."
|
|
173
|
+
return
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
extensions.each do |ext|
|
|
177
|
+
ver = (ext["latest_version"] || {})["version"] || ext["version"]
|
|
178
|
+
units = (ext["units"] || {}).map { |k, v| "#{v} #{k}" }.join(", ")
|
|
179
|
+
puts "#{ext["name"]} v#{ver} [#{ext["status"]}]#{units.empty? ? "" : " (#{units})"}"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
desc "unpublish ID", "Remove one of your published extensions from the marketplace"
|
|
184
|
+
def unpublish(id)
|
|
185
|
+
brand = Clacky::BrandConfig.load
|
|
186
|
+
result = brand.delete_extension!(id)
|
|
187
|
+
|
|
188
|
+
if result[:success]
|
|
189
|
+
puts "Unpublished #{id}."
|
|
190
|
+
else
|
|
191
|
+
warn "Error: #{result[:error]}"
|
|
192
|
+
exit 1
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
desc "search [QUERY]", "Search the public extension marketplace"
|
|
197
|
+
method_option :sort, type: :string, default: "newest",
|
|
198
|
+
desc: "Sort order: newest, updated, downloads"
|
|
199
|
+
def search(query = nil)
|
|
200
|
+
brand = Clacky::BrandConfig.load
|
|
201
|
+
result = brand.search_extensions!(query: query, sort: options[:sort])
|
|
202
|
+
|
|
203
|
+
unless result[:success]
|
|
204
|
+
warn "Error: #{result[:error]}"
|
|
205
|
+
exit 1
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
extensions = result[:extensions]
|
|
209
|
+
if extensions.empty?
|
|
210
|
+
puts query ? "No extensions found for #{query.inspect}." : "No extensions available yet."
|
|
211
|
+
return
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
extensions.each do |ext|
|
|
215
|
+
emoji = ext["emoji"] || "🧩"
|
|
216
|
+
ver = ext["version"]
|
|
217
|
+
units = (ext["units"] || {}).map { |k, v| "#{v} #{k}" }.join(", ")
|
|
218
|
+
name = ext["name_zh"] || ext["name"]
|
|
219
|
+
puts "#{emoji} #{name}#{ver ? " v#{ver}" : ""}#{units.empty? ? "" : " (#{units})"}"
|
|
220
|
+
desc = ext["description_zh"] || ext["description"]
|
|
221
|
+
puts " #{desc}" if desc && !desc.to_s.strip.empty?
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -17,11 +17,15 @@ module Clacky
|
|
|
17
17
|
# Public entry called from HttpServer#_dispatch_rest.
|
|
18
18
|
# Returns true to indicate the request was handled (200/4xx/5xx).
|
|
19
19
|
def handle(req, res, http_server:)
|
|
20
|
-
|
|
21
|
-
return not_found(res, "extension id missing") unless
|
|
20
|
+
mount_id, sub_path = parse_path(req.path)
|
|
21
|
+
return not_found(res, "extension mount id missing") unless mount_id
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
# Lazily (re)load the handler on each request so edits to handler.rb take
|
|
24
|
+
# effect with no restart — cheap for a local app with a handful of exts.
|
|
25
|
+
Clacky::ApiExtensionLoader.ensure_fresh(mount_id)
|
|
26
|
+
|
|
27
|
+
klass = Clacky::ApiExtension.registry[mount_id]
|
|
28
|
+
return not_found(res, "extension '#{mount_id}' not found") unless klass
|
|
25
29
|
|
|
26
30
|
method = req.request_method.to_s.downcase.to_sym
|
|
27
31
|
route, params = find_route(klass, method, sub_path)
|
|
@@ -38,10 +42,10 @@ module Clacky
|
|
|
38
42
|
# Tells HttpServer whether a given /api/ext/... path can skip access-key
|
|
39
43
|
# auth, so the host can keep its single-source-of-truth auth logic.
|
|
40
44
|
def public_path?(path, method)
|
|
41
|
-
|
|
42
|
-
return false unless
|
|
45
|
+
mount_id, sub_path = parse_path(path)
|
|
46
|
+
return false unless mount_id
|
|
43
47
|
|
|
44
|
-
klass = Clacky::ApiExtension.registry[
|
|
48
|
+
klass = Clacky::ApiExtension.registry[mount_id]
|
|
45
49
|
return false unless klass
|
|
46
50
|
return false if klass.public_paths.empty?
|
|
47
51
|
|
|
@@ -51,16 +55,25 @@ module Clacky
|
|
|
51
55
|
klass.public_paths.include?(route.pattern)
|
|
52
56
|
end
|
|
53
57
|
|
|
58
|
+
# Local-app convenience: see ApiExtensionLoader.ensure_fresh — that is
|
|
59
|
+
# the single source of truth for per-request hot reload.
|
|
60
|
+
|
|
61
|
+
# /api/ext/<ext_id>/<sub-path> → [ext_id, "/<sub-path>"]
|
|
54
62
|
private def parse_path(path)
|
|
55
63
|
return [nil, nil] unless path.to_s.start_with?(MOUNT_PREFIX)
|
|
56
64
|
|
|
57
65
|
tail = path[MOUNT_PREFIX.length..]
|
|
58
66
|
slash = tail.index("/")
|
|
59
67
|
if slash
|
|
60
|
-
|
|
68
|
+
ext_id = tail[0...slash]
|
|
69
|
+
sub = tail[slash..]
|
|
61
70
|
else
|
|
62
|
-
|
|
71
|
+
ext_id = tail
|
|
72
|
+
sub = "/"
|
|
63
73
|
end
|
|
74
|
+
return [nil, nil] if ext_id.empty?
|
|
75
|
+
|
|
76
|
+
[ext_id, sub]
|
|
64
77
|
end
|
|
65
78
|
|
|
66
79
|
private def find_route(klass, method, sub_path)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
# Loads Ruby hook files contributed by ext.yml containers
|
|
5
|
+
# (`contributes.hooks: [{ event, file }]`). Each file is required once at
|
|
6
|
+
# process start; it registers callbacks via `Clacky::ExtensionHookRegistry.add`.
|
|
7
|
+
# Every agent then copies those callbacks onto its own `HookManager` during
|
|
8
|
+
# init, so each agent gets its own isolated hook chain.
|
|
9
|
+
module ExtensionHookLoader
|
|
10
|
+
Result = Struct.new(:registered, :skipped, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
def self.load_all
|
|
13
|
+
result = Result.new(registered: [], skipped: [])
|
|
14
|
+
units = Array(Clacky::ExtensionLoader.last_result&.hooks)
|
|
15
|
+
units.each do |unit|
|
|
16
|
+
event = unit.spec["event"].to_sym
|
|
17
|
+
unless Clacky::HookManager::HOOK_EVENTS.include?(event)
|
|
18
|
+
msg = "unknown event: #{event}"
|
|
19
|
+
result.skipped << [unit.id, msg]
|
|
20
|
+
Clacky::Logger.warn("[ExtensionHookLoader] #{unit.ext_id}/#{unit.id}: #{msg}")
|
|
21
|
+
next
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Clacky::ExtensionHookRegistry.current_event = event
|
|
25
|
+
require unit.spec["file_abs"]
|
|
26
|
+
result.registered << [unit.ext_id, unit.id, event]
|
|
27
|
+
rescue StandardError, ScriptError => e
|
|
28
|
+
result.skipped << [unit.id, e.message]
|
|
29
|
+
Clacky::Logger.warn("[ExtensionHookLoader] #{unit.ext_id}/#{unit.id}: #{e.message}")
|
|
30
|
+
ensure
|
|
31
|
+
Clacky::ExtensionHookRegistry.current_event = nil
|
|
32
|
+
end
|
|
33
|
+
@last_result = result
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.last_result
|
|
38
|
+
@last_result || load_all
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Process-wide registry for ext-contributed hook callbacks. Callbacks are
|
|
43
|
+
# registered once at file load time; each new agent copies them onto its own
|
|
44
|
+
# HookManager during init via `apply_to`.
|
|
45
|
+
module ExtensionHookRegistry
|
|
46
|
+
@callbacks = Hash.new { |h, k| h[k] = [] }
|
|
47
|
+
@current_event = nil
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
attr_accessor :current_event
|
|
51
|
+
|
|
52
|
+
# Register a callback. `event` falls back to the loader-set context so
|
|
53
|
+
# ext hook files can call `add { ... }` without repeating the event name
|
|
54
|
+
# already declared in ext.yml.
|
|
55
|
+
def add(event = nil, &block)
|
|
56
|
+
ev = (event || @current_event)
|
|
57
|
+
raise ArgumentError, "ExtensionHookRegistry.add called outside a hook file" unless ev
|
|
58
|
+
|
|
59
|
+
@callbacks[ev.to_sym] << block
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def callbacks
|
|
63
|
+
@callbacks
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply_to(hook_manager)
|
|
67
|
+
@callbacks.each do |event, blocks|
|
|
68
|
+
blocks.each { |b| hook_manager.add(event, &b) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def clear!
|
|
73
|
+
@callbacks.clear
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|