Module: Clacky::ExtensionLoader
- Defined in:
- lib/clacky/extension/loader.rb
Overview
Discovers extension containers across three source layers and resolves them into a flat list of capability units (panels, api, skills, agents) for the rest of the system to mount.
A container is a directory holding an ext.yml manifest:
id: canvas-suite
name: Canvas Suite
version: "1.0.0"
author: Jane Doe # display credit (optional)
homepage: https://example.com # optional
license: MIT # optional, SPDX identifier
origin: self # self | marketplace | enterprise
contributes:
panels:
- id: canvas
view: panels/canvas/view.js
attach: [designer] # optional: panel author's default
# ("*" = all agents; omit = hidden
# unless an agent references it)
api: api/handler.rb # single backend for the whole ext
Source layers (ascending priority — same id, higher layer wins):
builtin gem default_extensions/<id>/
installed ~/.clacky/ext/installed/<id>/
local ~/.clacky/ext/local/<id>/
This class only discovers and resolves; actual mounting is done by the existing call sites:
- api unit → ApiExtensionLoader.load_one(...) (server boot / reload)
- panel units → http_server ext_script_block(...) (rendered per request)
- skill units → SkillLoader#load_extension_skills (reads last_result on load_all)
- agent units → AgentProfile + http_server agent_profile_data (lookup by id)
- channel units → channel.rb extension_adapter_loader (require + register at boot)
- patch units → PatchLoader.load_extension_patches (require at boot)
- hook units → ShellHookLoader.load_extension_hooks (register at boot)
Defined Under Namespace
Constant Summary collapse
- BUILTIN_DIR =
File.("../default_extensions", __dir__)
- INSTALLED_DIR =
File.("~/.clacky/ext/installed")
- LOCAL_DIR =
File.("~/.clacky/ext/local")
- DISABLED_FILE =
File.("~/.clacky/ext/disabled.json")
- MANIFEST =
"ext.yml"- LAYERS =
Layers in ascending priority; later entries override earlier ones by id.
%i[builtin installed local].freeze
- ORIGINS =
%w[self marketplace enterprise].freeze
Class Method Summary collapse
- .default_layers ⇒ Object
- .dir_for(layer) ⇒ Object
- .disable!(id) ⇒ Object
- .disabled?(id) ⇒ Boolean
-
.disabled_ids ⇒ Object
── Disabled state ──────────────────────────────────────────────── A disabled extension stays discoverable (still listed as installed) but contributes no units until re-enabled.
- .enable!(id) ⇒ Object
-
.invalidate_cache! ⇒ Object
Discard the mtime cache so the next
load_allrescans from disk. - .last_result ⇒ Object
-
.load_all(layers: default_layers, force: false) ⇒ Object
Scan all layers, resolve overrides, return a structured Result.
-
.uninstall!(id) ⇒ Object
Remove an installed extension by deleting its installed-layer dir.
Class Method Details
.default_layers ⇒ Object
129 130 131 |
# File 'lib/clacky/extension/loader.rb', line 129 def default_layers LAYERS.each_with_object({}) { |layer, h| h[layer] = dir_for(layer) } end |
.dir_for(layer) ⇒ Object
70 71 72 73 74 75 76 |
# File 'lib/clacky/extension/loader.rb', line 70 def dir_for(layer) case layer when :builtin then BUILTIN_DIR when :installed then INSTALLED_DIR when :local then LOCAL_DIR end end |
.disable!(id) ⇒ Object
161 162 163 164 165 |
# File 'lib/clacky/extension/loader.rb', line 161 def disable!(id) ids = disabled_ids ids << id.to_s write_disabled(ids) end |
.disabled?(id) ⇒ Boolean
157 158 159 |
# File 'lib/clacky/extension/loader.rb', line 157 def disabled?(id) disabled_ids.include?(id.to_s) end |
.disabled_ids ⇒ Object
── Disabled state ──────────────────────────────────────────────── A disabled extension stays discoverable (still listed as installed) but contributes no units until re-enabled. Persisted as a JSON array of ext ids at ~/.clacky/ext/disabled.json.
148 149 150 151 152 153 154 155 |
# File 'lib/clacky/extension/loader.rb', line 148 def disabled_ids return Set.new unless File.file?(DISABLED_FILE) data = JSON.parse(File.read(DISABLED_FILE)) data.is_a?(Array) ? data.map(&:to_s).to_set : Set.new rescue StandardError Set.new end |
.enable!(id) ⇒ Object
167 168 169 170 171 |
# File 'lib/clacky/extension/loader.rb', line 167 def enable!(id) ids = disabled_ids ids.delete(id.to_s) write_disabled(ids) end |
.invalidate_cache! ⇒ Object
Discard the mtime cache so the next load_all rescans from disk.
Used by CLI commands that mutate the ext tree (new / pack).
139 140 141 |
# File 'lib/clacky/extension/loader.rb', line 139 def invalidate_cache! @last_fingerprint = nil end |
.last_result ⇒ Object
133 134 135 |
# File 'lib/clacky/extension/loader.rb', line 133 def last_result @last_result || load_all end |
.load_all(layers: default_layers, force: false) ⇒ Object
Scan all layers, resolve overrides, return a structured Result.
layers maps layer name => root dir; defaults to the real three-layer
dirs. Tests inject temp dirs through it.
Cached by a fingerprint of every container's ext.yml mtime — a hot
path (per web request, per API call) hits the cache; the moment any
manifest changes, we invalidate and rescan. Pass force: true to
bypass the cache (used by CLI / tests).
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/clacky/extension/loader.rb', line 86 def load_all(layers: default_layers, force: false) fingerprint = [fingerprint_layers(layers), disabled_fingerprint] if !force && @last_result && @last_fingerprint == fingerprint return @last_result end by_id = {} # ext_id => resolved container (winning layer) overridden = [] # [ext_id, losing_layer, winning_layer] errors = [] layers.each do |layer, root| next unless root && Dir.exist?(root) Dir.children(root).sort.each do |ext_id| next if ext_id.start_with?("_", ".") dir = File.join(root, ext_id) next unless File.directory?(dir) manifest = File.join(dir, MANIFEST) next unless File.file?(manifest) container = read_container(ext_id, layer, dir, manifest, errors) next unless container if (prev = by_id[ext_id]) overridden << [ext_id, prev[:layer], layer] end by_id[ext_id] = container end end result = Result.new(panels: [], api: [], skills: [], agents: [], channels: [], patches: [], hooks: [], errors: errors, overridden: overridden, containers: by_id) disabled = disabled_ids by_id.each_value do |container| container[:disabled] = disabled.include?(container[:ext_id]) resolve_units(container, result) unless container[:disabled] end @last_result = result @last_fingerprint = fingerprint result end |
.uninstall!(id) ⇒ Object
Remove an installed extension by deleting its installed-layer dir. Only the installed layer is removable (builtin ships with the gem; local is the author's own working copy). Returns true on removal.
176 177 178 179 180 181 182 183 184 |
# File 'lib/clacky/extension/loader.rb', line 176 def uninstall!(id) dir = File.join(INSTALLED_DIR, id.to_s) return false unless File.directory?(dir) FileUtils.rm_rf(dir) enable!(id) # clear any stale disabled flag invalidate_cache! true end |