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"- DATA_DIR =
User data lives OUTSIDE the package tree so uninstalling (which deletes the package dir) never takes the data with it, and reinstalling the same extension transparently reconnects to it.
File.("~/.clacky/ext-data")
- 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
-
.data_dir_for(id) ⇒ Object
Package-external directory holding an extension's persisted user data.
- .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, purge_data: false) ⇒ Object
Remove an installed extension by deleting its installed-layer dir.
Class Method Details
.data_dir_for(id) ⇒ Object
Package-external directory holding an extension's persisted user data.
84 85 86 |
# File 'lib/clacky/extension/loader.rb', line 84 def data_dir_for(id) File.join(DATA_DIR, id.to_s) end |
.default_layers ⇒ Object
139 140 141 |
# File 'lib/clacky/extension/loader.rb', line 139 def default_layers LAYERS.each_with_object({}) { |layer, h| h[layer] = dir_for(layer) } end |
.dir_for(layer) ⇒ Object
75 76 77 78 79 80 81 |
# File 'lib/clacky/extension/loader.rb', line 75 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
171 172 173 174 175 |
# File 'lib/clacky/extension/loader.rb', line 171 def disable!(id) ids = disabled_ids ids << id.to_s write_disabled(ids) end |
.disabled?(id) ⇒ Boolean
167 168 169 |
# File 'lib/clacky/extension/loader.rb', line 167 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.
158 159 160 161 162 163 164 165 |
# File 'lib/clacky/extension/loader.rb', line 158 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
177 178 179 180 181 |
# File 'lib/clacky/extension/loader.rb', line 177 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).
149 150 151 |
# File 'lib/clacky/extension/loader.rb', line 149 def invalidate_cache! @last_fingerprint = nil end |
.last_result ⇒ Object
143 144 145 |
# File 'lib/clacky/extension/loader.rb', line 143 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).
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 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/clacky/extension/loader.rb', line 96 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, purge_data: false) ⇒ 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). Package-external user data is
preserved by default so a reinstall reconnects to it; pass
purge_data: true to also delete ~/.clacky/ext-data/
189 190 191 192 193 194 195 196 197 198 |
# File 'lib/clacky/extension/loader.rb', line 189 def uninstall!(id, purge_data: false) dir = File.join(INSTALLED_DIR, id.to_s) return false unless File.directory?(dir) FileUtils.rm_rf(dir) FileUtils.rm_rf(data_dir_for(id)) if purge_data enable!(id) # clear any stale disabled flag invalidate_cache! true end |