samagotchi 0.53.0 → 0.54.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 +32 -1
- data/README.md +1 -1
- data/docs/architecture.md +94 -54
- data/docs/configuration.md +1 -2
- data/docs/context.md +104 -9
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +73 -0
- data/docs/internals/context-telemetry.md +8 -5
- data/docs/internals/engine.md +16 -7
- data/docs/internals/tool-guardrails.md +8 -6
- data/docs/plugins.md +2 -1
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
- data/lib/samagotchi/bundle_command.rb +31 -36
- data/lib/samagotchi/bundle_needs.rb +4 -5
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +6 -4
- data/lib/samagotchi/config.rb +0 -1
- data/lib/samagotchi/context_command.rb +1 -1
- data/lib/samagotchi/context_providers.rb +11 -14
- data/lib/samagotchi/context_status.rb +10 -18
- data/lib/samagotchi/engine.rb +117 -134
- data/lib/samagotchi/extension_load.rb +102 -0
- data/lib/samagotchi/guardrail_wiring.rb +16 -16
- data/lib/samagotchi/kernel_loop.rb +5 -3
- data/lib/samagotchi/llm/chat_loop.rb +5 -4
- data/lib/samagotchi/memory_bundle/asset_installer.rb +302 -0
- data/lib/samagotchi/memory_bundle/builder.rb +170 -178
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +90 -0
- data/lib/samagotchi/memory_bundle/installer.rb +252 -488
- data/lib/samagotchi/memory_bundle/listing.rb +6 -6
- data/lib/samagotchi/memory_bundle/manifest.rb +4 -2
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +27 -0
- data/lib/samagotchi/memory_bundle/profile.rb +21 -21
- data/lib/samagotchi/memory_bundle/provenance.rb +38 -40
- data/lib/samagotchi/memory_bundle/shipped_update.rb +24 -26
- data/lib/samagotchi/memory_bundle/status.rb +22 -24
- data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -5
- data/lib/samagotchi/memory_bundle/uninstaller.rb +8 -10
- data/lib/samagotchi/plugin/loader.rb +8 -8
- data/lib/samagotchi/self_report.rb +2 -2
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session_continue.rb +275 -0
- data/lib/samagotchi/session_manager.rb +9 -253
- data/lib/samagotchi/session_metrics.rb +3 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui.rb +12 -2
- data/lib/samagotchi/tool_activity.rb +84 -14
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -0
- data/lib/samagotchi/web/public/app.js +6 -6
- data/lib/samagotchi/web/public/ctx.js +18 -4
- data/lib/samagotchi/web/public/index.html +4 -2
- data/lib/samagotchi/web/public/question_card.js +9 -1
- data/lib/samagotchi/web/public/question_cards.js +5 -3
- data/lib/samagotchi/web/public/timing.js +8 -2
- data/lib/samagotchi/web/public/turn_html.js +10 -6
- metadata +7 -1
|
@@ -43,14 +43,14 @@ module Samagotchi
|
|
|
43
43
|
# @return [Array<Installed>] by name; upgrade is the Shipped when it is newer
|
|
44
44
|
def installed(shipped: self.shipped)
|
|
45
45
|
by_name = shipped.to_h { |s| [s.name, s] }
|
|
46
|
-
Provenance.each_installed.map do |name,
|
|
47
|
-
next Installed.new(name: name, error: "manifest.json unreadable") if
|
|
46
|
+
Provenance.each_installed.map do |name, bundle|
|
|
47
|
+
next Installed.new(name: name, error: "manifest.json unreadable") if bundle.error?
|
|
48
48
|
|
|
49
49
|
ship = by_name[name]
|
|
50
|
-
Installed.new(name: name, version:
|
|
51
|
-
files:
|
|
52
|
-
upgrade: ship && newer?(ship.version,
|
|
53
|
-
includes:
|
|
50
|
+
Installed.new(name: name, version: bundle.version, scope: bundle.scope,
|
|
51
|
+
files: bundle.files.size, installed_at: bundle.installed_at,
|
|
52
|
+
upgrade: ship && newer?(ship.version, bundle.version) ? ship : nil,
|
|
53
|
+
includes: bundle.includes)
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
@@ -99,9 +99,11 @@ module Samagotchi
|
|
|
99
99
|
raw.start_with?("sha256:") ? raw[7..] : raw
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
# Returns sha256 hex for a hook basename (e.g. "guardrails.rb")
|
|
102
|
+
# Returns sha256 hex for a hook basename (e.g. "guardrails.rb"), nil
|
|
103
|
+
# when it declares none (nothing to verify).
|
|
103
104
|
def checksum_for_hook(basename)
|
|
104
|
-
@hooks[basename]&.hex
|
|
105
|
+
hex = @hooks[basename]&.hex
|
|
106
|
+
hex unless hex.nil? || hex.empty?
|
|
105
107
|
end
|
|
106
108
|
|
|
107
109
|
# @return [String, nil] the plugin's sha256 hex, "sha256:" stripped
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module MemoryBundle
|
|
5
|
+
# The plugin an installed bundle's record (manifest.json) names: its
|
|
6
|
+
# file under plugin/ and the sha256 recorded at install ("" when none
|
|
7
|
+
# is; either "sha256:<hex>" or a bare hex, Provenance.recorded_sha
|
|
8
|
+
# reads both).
|
|
9
|
+
PluginRef = Data.define(:file, :sha256) do
|
|
10
|
+
def initialize(file:, sha256: "")
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param raw [Hash, PluginRef, nil] the record's plugin: mapping
|
|
15
|
+
# (symbol or string keys)
|
|
16
|
+
# @return [PluginRef, nil] nil when there is none or it names no file
|
|
17
|
+
def self.parse(raw)
|
|
18
|
+
return raw if raw.is_a?(PluginRef)
|
|
19
|
+
return nil unless raw.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
h = raw.transform_keys(&:to_s)
|
|
22
|
+
file = h["file"].to_s
|
|
23
|
+
file.empty? ? nil : new(file: file, sha256: h["sha256"].to_s)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -46,32 +46,32 @@ module Samagotchi
|
|
|
46
46
|
false
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# Whether the installed bundle +name+ (its
|
|
50
|
-
# meta: it records includes, or it came
|
|
51
|
-
# a meta.
|
|
52
|
-
def installed_meta?(name,
|
|
53
|
-
return false unless
|
|
54
|
-
return true if
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
# Whether the installed bundle +name+ (its record +bundle+, an
|
|
50
|
+
# InstalledBundle or nil) is a meta: it records includes, or it came
|
|
51
|
+
# from chi and chi ships it as a meta.
|
|
52
|
+
def installed_meta?(name, bundle, shipped_dir: SourceNormalizer::SHIPPED_DIR)
|
|
53
|
+
return false unless bundle
|
|
54
|
+
return true if bundle.profile?
|
|
55
|
+
|
|
56
|
+
bundle.source.to_s.match?(SourceNormalizer::SHIPPED_SOURCE) && shipped_meta?(File.join(shipped_dir, name), shipped_dir: shipped_dir)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
# The members the meta has recorded: none when it isn't installed,
|
|
60
60
|
# all of +manifest+'s (the shipped meta, nil when chi ships none) when
|
|
61
61
|
# its provenance has no includes: an unknown record is taken as
|
|
62
62
|
# everything.
|
|
63
|
-
def recorded(
|
|
64
|
-
return [] unless
|
|
65
|
-
return manifest&.includes || [] unless
|
|
63
|
+
def recorded(bundle, manifest)
|
|
64
|
+
return [] unless bundle
|
|
65
|
+
return manifest&.includes || [] unless bundle.profile?
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
bundle.includes
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def installed_names = Provenance.each_installed.map { |name, _| name }
|
|
71
71
|
|
|
72
72
|
# The one rule.
|
|
73
|
-
def to_install(manifest,
|
|
74
|
-
manifest.includes - recorded(
|
|
73
|
+
def to_install(manifest, bundle = Provenance.new(name: manifest.name).record, installed: installed_names)
|
|
74
|
+
manifest.includes - recorded(bundle, manifest) - installed
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
# Installs the shipped meta at +dir+: its to_install members from the
|
|
@@ -80,13 +80,13 @@ module Samagotchi
|
|
|
80
80
|
# @return [InstallResult]
|
|
81
81
|
def install(dir, shipped_dir: SourceNormalizer::SHIPPED_DIR, chi_version: Samagotchi::VERSION, dry_run: false, force: false)
|
|
82
82
|
manifest = Manifest.read(dir: dir)
|
|
83
|
-
|
|
84
|
-
was = recorded(
|
|
83
|
+
bundle = Provenance.new(name: manifest.name).record
|
|
84
|
+
was = recorded(bundle, manifest)
|
|
85
85
|
installed = installed_names
|
|
86
86
|
result = InstallResult.new(name: manifest.name, version: manifest.version, installed: [],
|
|
87
87
|
already: (manifest.includes - was) & installed, skipped: {}, failed: {})
|
|
88
88
|
|
|
89
|
-
to_install(manifest,
|
|
89
|
+
to_install(manifest, bundle, installed: installed).each do |member|
|
|
90
90
|
install_member(member, shipped_dir, chi_version, dry_run, force, result)
|
|
91
91
|
end
|
|
92
92
|
return result if dry_run
|
|
@@ -94,7 +94,7 @@ module Samagotchi
|
|
|
94
94
|
now = was | result.installed | result.already
|
|
95
95
|
includes = (manifest.includes & now) + (now - manifest.includes)
|
|
96
96
|
# Nothing new: the record stays as it is (installed_at too).
|
|
97
|
-
return result if
|
|
97
|
+
return result if bundle && bundle.version == manifest.version && bundle.source == dir && bundle.includes == includes
|
|
98
98
|
|
|
99
99
|
Provenance.new(name: manifest.name).write(
|
|
100
100
|
files: {}, scope: "system", version: manifest.version, source_path: dir,
|
|
@@ -123,12 +123,12 @@ module Samagotchi
|
|
|
123
123
|
# @return [UninstallResult]
|
|
124
124
|
# @raise [Uninstaller::UninstallError] when +name+ isn't installed
|
|
125
125
|
def uninstall(name, shipped_dir: SourceNormalizer::SHIPPED_DIR, force: false)
|
|
126
|
-
|
|
127
|
-
raise Uninstaller::UninstallError, "Bundle '#{name}' is not installed" unless
|
|
126
|
+
bundle = Provenance.new(name: name).record
|
|
127
|
+
raise Uninstaller::UninstallError, "Bundle '#{name}' is not installed" unless bundle
|
|
128
128
|
|
|
129
129
|
shipped = File.join(shipped_dir, name)
|
|
130
130
|
manifest = File.file?(File.join(shipped, "manifest.yml")) ? Manifest.read(dir: shipped) : nil
|
|
131
|
-
members = recorded(
|
|
131
|
+
members = recorded(bundle, manifest)
|
|
132
132
|
result = UninstallResult.new(name: name, removed: [], blocked: {}, gone: false, trash: {}, warnings: [])
|
|
133
133
|
(members & installed_names).each do |member|
|
|
134
134
|
uninstaller = Uninstaller.new(name: member, force: force)
|
|
@@ -7,6 +7,7 @@ require "fileutils"
|
|
|
7
7
|
require_relative "../atomic_file"
|
|
8
8
|
require_relative "../memory_paths"
|
|
9
9
|
require_relative "bundle_hook"
|
|
10
|
+
require_relative "installed_bundle"
|
|
10
11
|
|
|
11
12
|
module Samagotchi
|
|
12
13
|
module MemoryBundle
|
|
@@ -47,9 +48,10 @@ module Samagotchi
|
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# The plugin file as installed, or nil when the bundle has none.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
file
|
|
51
|
+
# @param bundle [InstalledBundle, nil] its record (read when not given)
|
|
52
|
+
def plugin_path(bundle = record)
|
|
53
|
+
file = bundle&.plugin&.file
|
|
54
|
+
file ? File.join(plugin_dir, file) : nil
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
# The plugin's base snapshot (bases/plugin/<file>), for bundle diff.
|
|
@@ -57,14 +59,14 @@ module Samagotchi
|
|
|
57
59
|
File.join(@bundle_dir, "bases", "plugin", file.to_s)
|
|
58
60
|
end
|
|
59
61
|
|
|
60
|
-
# Yields [name,
|
|
61
|
-
# bundles_dir holding a manifest.json, dot dirs aside), by name.
|
|
62
|
-
# is
|
|
63
|
-
# or isn't an object; the rest still come.
|
|
62
|
+
# Yields [name, bundle] for each installed bundle (a dir under
|
|
63
|
+
# bundles_dir holding a manifest.json, dot dirs aside), by name. bundle
|
|
64
|
+
# is its InstalledBundle, one with error set when manifest.json doesn't
|
|
65
|
+
# parse or isn't an object; the rest still come.
|
|
64
66
|
#
|
|
65
67
|
# With holding: (:hooks, :guardrails or :plugin) only the bundles
|
|
66
68
|
# whose manifest has a non-empty mapping under that key come, and a
|
|
67
|
-
# manifest that doesn't parse comes
|
|
69
|
+
# manifest that doesn't parse comes (with error) only when its bundle
|
|
68
70
|
# has that dir (hooks/, guardrails/, plugin/): what it would load
|
|
69
71
|
# can't be checked. One that isn't an object is skipped there.
|
|
70
72
|
# @param dir [String] the bundles dir (bundles_dir by default)
|
|
@@ -79,15 +81,14 @@ module Samagotchi
|
|
|
79
81
|
begin
|
|
80
82
|
data = JSON.parse(File.read(mjson), symbolize_names: true)
|
|
81
83
|
rescue JSON::ParserError, SystemCallError => e
|
|
82
|
-
|
|
84
|
+
if holding.nil? || Dir.exist?(File.join(dir, name, holding.to_s))
|
|
85
|
+
yield name, InstalledBundle.unreadable(name, "manifest.json is unreadable: #{e.message}")
|
|
86
|
+
end
|
|
83
87
|
next
|
|
84
88
|
end
|
|
85
|
-
if holding
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
data = { error: "manifest.json is not an object" }
|
|
89
|
-
end
|
|
90
|
-
yield name, data
|
|
89
|
+
next if holding && !(data.is_a?(Hash) && data[holding].is_a?(Hash) && !data[holding].empty?)
|
|
90
|
+
|
|
91
|
+
yield name, InstalledBundle.parse(name, data)
|
|
91
92
|
end
|
|
92
93
|
end
|
|
93
94
|
|
|
@@ -96,10 +97,9 @@ module Samagotchi
|
|
|
96
97
|
# A project-scoped record doesn't say which project: one from another
|
|
97
98
|
# repo counts too, which errs on keeping the file.
|
|
98
99
|
def self.claimants(file_key, scope:, except: nil, dir: bundles_dir)
|
|
99
|
-
each_installed(dir: dir).filter_map do |name,
|
|
100
|
-
next if name == except ||
|
|
101
|
-
next unless
|
|
102
|
-
next unless data[:files].is_a?(Hash) && data[:files].keys.map(&:to_s).include?(file_key.to_s)
|
|
100
|
+
each_installed(dir: dir).filter_map do |name, bundle|
|
|
101
|
+
next if name == except || bundle.error?
|
|
102
|
+
next unless bundle.effective_scope == scope.to_s && bundle.owns?(file_key)
|
|
103
103
|
|
|
104
104
|
name
|
|
105
105
|
end
|
|
@@ -146,11 +146,10 @@ module Samagotchi
|
|
|
146
146
|
bases_dir = File.join(@bundle_dir, "bases")
|
|
147
147
|
FileUtils.mkdir_p(bases_dir)
|
|
148
148
|
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
merged_entries = existing && existing[:files] ? existing[:files].transform_keys(&:to_s) : {}
|
|
149
|
+
# The existing record: its file entries merge with these, and its
|
|
150
|
+
# files and hooks not in this install are stale.
|
|
151
|
+
existing = record
|
|
152
|
+
merged_entries = existing ? existing.files.dup : {}
|
|
154
153
|
|
|
155
154
|
files.each do |file_key, file_path|
|
|
156
155
|
content = File.read(file_path)
|
|
@@ -163,10 +162,9 @@ module Samagotchi
|
|
|
163
162
|
end
|
|
164
163
|
|
|
165
164
|
# Prune stale base snapshots for files no longer in the bundle.
|
|
166
|
-
if existing
|
|
167
|
-
existing
|
|
168
|
-
old_key_str
|
|
169
|
-
next if files.key?(old_key_str) || files.key?(old_key)
|
|
165
|
+
if existing
|
|
166
|
+
existing.files.each_key do |old_key_str|
|
|
167
|
+
next if files.key?(old_key_str) || files.key?(old_key_str.to_sym)
|
|
170
168
|
|
|
171
169
|
old_base = File.join(bases_dir, old_key_str)
|
|
172
170
|
FileUtils.rm_f(old_base)
|
|
@@ -176,8 +174,6 @@ module Samagotchi
|
|
|
176
174
|
|
|
177
175
|
# Hooks provenance: persist metadata and base snapshots for hooks
|
|
178
176
|
hooks_map = {}
|
|
179
|
-
# existing hooks for pruning
|
|
180
|
-
existing_hooks = existing && existing[:hooks] ? existing[:hooks] : {}
|
|
181
177
|
# Incoming hooks: basename => BundleHook (or its metadata hash)
|
|
182
178
|
normalized_hooks = {}
|
|
183
179
|
if hooks.is_a?(Hash)
|
|
@@ -189,14 +185,8 @@ module Samagotchi
|
|
|
189
185
|
end
|
|
190
186
|
end
|
|
191
187
|
# Prune stale hook bases
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
old_key_str = old_key.to_s
|
|
195
|
-
unless normalized_hooks.key?(old_key_str) || normalized_hooks.key?(old_key_str.to_sym)
|
|
196
|
-
old_base = File.join(bases_dir, old_key_str)
|
|
197
|
-
FileUtils.rm_f(old_base)
|
|
198
|
-
end
|
|
199
|
-
end
|
|
188
|
+
existing&.hooks&.each_key do |old_key_str|
|
|
189
|
+
FileUtils.rm_f(File.join(bases_dir, old_key_str)) unless normalized_hooks.key?(old_key_str)
|
|
200
190
|
end
|
|
201
191
|
# Save base snapshots for hooks where a file path was provided, and
|
|
202
192
|
# record the sha256 of the installed file itself: the load-time check
|
|
@@ -276,7 +266,15 @@ module Samagotchi
|
|
|
276
266
|
write_manifest(raw)
|
|
277
267
|
end
|
|
278
268
|
|
|
279
|
-
#
|
|
269
|
+
# The installed bundle's record, nil when it isn't installed.
|
|
270
|
+
# @return [InstalledBundle, nil]
|
|
271
|
+
# @raise [JSON::ParserError] manifest.json doesn't parse
|
|
272
|
+
def record
|
|
273
|
+
InstalledBundle.read(@name, File.join(@bundle_dir, "manifest.json"))
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# manifest.json as it is on disk, symbol keys (nil if not installed).
|
|
277
|
+
# Readers take #record.
|
|
280
278
|
def read
|
|
281
279
|
manifest_path = File.join(@bundle_dir, "manifest.json")
|
|
282
280
|
return nil unless File.exist?(manifest_path)
|
|
@@ -299,7 +297,7 @@ module Samagotchi
|
|
|
299
297
|
|
|
300
298
|
# Checks whether the bundle is installed.
|
|
301
299
|
def installed?
|
|
302
|
-
|
|
300
|
+
!record.nil?
|
|
303
301
|
end
|
|
304
302
|
end
|
|
305
303
|
end
|
|
@@ -45,8 +45,8 @@ module Samagotchi
|
|
|
45
45
|
# @return [Array<Row>] by name
|
|
46
46
|
def plan(shipped_dir: SourceNormalizer::SHIPPED_DIR, chi_version: Samagotchi::VERSION)
|
|
47
47
|
shipped = Listing.shipped(dir: shipped_dir).to_h { |s| [s.name, s] }
|
|
48
|
-
installed.map do |name,
|
|
49
|
-
plan_row(name,
|
|
48
|
+
installed.map do |name, bundle|
|
|
49
|
+
plan_row(name, bundle, shipped[name], shipped_dir, chi_version)
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -88,24 +88,24 @@ module Samagotchi
|
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
# [name,
|
|
91
|
+
# [name, InstalledBundle] for each installed bundle but the system one, by name.
|
|
92
92
|
def installed
|
|
93
93
|
Provenance.each_installed.reject { |name, _| name == SystemBundle::BUNDLE_NAME }
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
def plan_row(name,
|
|
96
|
+
def plan_row(name, bundle, ship, shipped_dir, chi_version)
|
|
97
97
|
row = Row.new(name: name, kept: [], replaced: [])
|
|
98
|
-
return skip(row, "manifest.json unreadable") if
|
|
98
|
+
return skip(row, "manifest.json unreadable") if bundle.error?
|
|
99
99
|
|
|
100
|
-
row.from =
|
|
101
|
-
row.scope =
|
|
102
|
-
return skip(row, "not from chi") unless ship &&
|
|
100
|
+
row.from = bundle.version.to_s
|
|
101
|
+
row.scope = bundle.effective_scope
|
|
102
|
+
return skip(row, "not from chi") unless ship && bundle.source.to_s.match?(SourceNormalizer::SHIPPED_SOURCE)
|
|
103
103
|
|
|
104
104
|
row.to = ship.version.to_s
|
|
105
105
|
return skip(row, "newer than shipped: left") if Listing.newer?(row.from, row.to)
|
|
106
106
|
|
|
107
107
|
row.source_dir = File.join(shipped_dir, ship.source)
|
|
108
|
-
return profile_row(row,
|
|
108
|
+
return profile_row(row, bundle, shipped_dir, chi_version) if Profile.shipped_meta?(row.source_dir, shipped_dir: shipped_dir)
|
|
109
109
|
return row.tap { |r| r.status = :up_to_date; r.to = nil } unless Listing.newer?(row.to, row.from)
|
|
110
110
|
|
|
111
111
|
manifest = Manifest.read(dir: row.source_dir)
|
|
@@ -115,20 +115,20 @@ module Samagotchi
|
|
|
115
115
|
return skip(row, "project scope: chi bundle upgrade #{ship.source} in the project") if row.scope == "project"
|
|
116
116
|
return skip(row, "unknown scope #{row.scope}: upgrade chi or reinstall") unless MemoryPaths.scope_dir(row.scope)
|
|
117
117
|
|
|
118
|
-
row.kept = conflicts(row,
|
|
119
|
-
row.replaced = edited_executables(name,
|
|
118
|
+
row.kept = conflicts(row, bundle)
|
|
119
|
+
row.replaced = edited_executables(name, bundle)
|
|
120
120
|
row.status = :would_update
|
|
121
121
|
row
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
-
def profile_row(row,
|
|
124
|
+
def profile_row(row, bundle, shipped_dir, chi_version)
|
|
125
125
|
manifest = Manifest.read(dir: row.source_dir)
|
|
126
126
|
newer = Listing.newer?(row.to, row.from)
|
|
127
127
|
if newer && Manifest.requires_chi_failure(manifest.requires_chi, chi_version)
|
|
128
128
|
return skip(row, "needs chi #{manifest.requires_chi}", keep_to: true)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
row.adds = Profile.to_install(manifest,
|
|
131
|
+
row.adds = Profile.to_install(manifest, bundle).reject do |member|
|
|
132
132
|
Manifest.requires_chi_failure(Manifest.read(dir: File.join(shipped_dir, member)).requires_chi, chi_version)
|
|
133
133
|
rescue Manifest::ValidationError, Psych::Exception
|
|
134
134
|
false # apply reports it failing
|
|
@@ -147,12 +147,10 @@ module Samagotchi
|
|
|
147
147
|
|
|
148
148
|
# The bundle's .md files whose local edits conflict with the shipped
|
|
149
149
|
# version (a same-name file it didn't install is skipped, not merged).
|
|
150
|
-
def conflicts(row,
|
|
150
|
+
def conflicts(row, bundle)
|
|
151
151
|
provenance = Provenance.new(name: row.name)
|
|
152
152
|
target_dir = MemoryPaths.scope_dir!(row.scope)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
owned = data[:files].keys.map(&:to_s)
|
|
153
|
+
owned = bundle.files.keys
|
|
156
154
|
(Dir.glob(File.join(row.source_dir, "*.md")).map { |f| File.basename(f) }.sort & owned).select do |key|
|
|
157
155
|
Merger.classify(base_path: provenance.base_path(key), current_path: File.join(target_dir, key),
|
|
158
156
|
incoming_path: File.join(row.source_dir, key)) == :conflict
|
|
@@ -161,22 +159,22 @@ module Samagotchi
|
|
|
161
159
|
|
|
162
160
|
# Installed hooks, rules and the plugin whose sha differs from the
|
|
163
161
|
# recorded one (they didn't load), as hooks/F, guardrails/F, plugin/F.
|
|
164
|
-
def edited_executables(name,
|
|
162
|
+
def edited_executables(name, bundle)
|
|
165
163
|
provenance = Provenance.new(name: name)
|
|
166
164
|
edited = []
|
|
167
|
-
|
|
168
|
-
edited << "hooks/#{file}" if edited?(File.join(provenance.hooks_dir, file
|
|
165
|
+
bundle.hooks.each do |file, hook|
|
|
166
|
+
edited << "hooks/#{file}" if edited?(File.join(provenance.hooks_dir, file), hook.sha256)
|
|
169
167
|
end
|
|
170
|
-
|
|
171
|
-
edited << "guardrails/#{file}" if edited?(File.join(provenance.guardrails_dir, file
|
|
168
|
+
bundle.guardrails.each do |file, sha|
|
|
169
|
+
edited << "guardrails/#{file}" if edited?(File.join(provenance.guardrails_dir, file), sha)
|
|
172
170
|
end
|
|
173
|
-
plugin = provenance.plugin_path(
|
|
174
|
-
edited << "plugin/#{File.basename(plugin)}" if plugin && edited?(plugin,
|
|
171
|
+
plugin = provenance.plugin_path(bundle)
|
|
172
|
+
edited << "plugin/#{File.basename(plugin)}" if plugin && edited?(plugin, bundle.plugin.sha256)
|
|
175
173
|
edited
|
|
176
174
|
end
|
|
177
175
|
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
# Whether the file differs from its recorded sha (none recorded: no).
|
|
177
|
+
def edited?(path, recorded)
|
|
180
178
|
return false if Provenance.recorded_sha(recorded).empty? || !File.file?(path)
|
|
181
179
|
|
|
182
180
|
!Provenance.sha_matches?(path, recorded)
|
|
@@ -17,30 +17,28 @@ module Samagotchi
|
|
|
17
17
|
# unchecked (no disk reads, nothing counted as missing).
|
|
18
18
|
def self.bundle_status(name, scope_override: nil)
|
|
19
19
|
provenance = Provenance.new(name: name)
|
|
20
|
-
|
|
21
|
-
return nil unless
|
|
20
|
+
bundle = provenance.record
|
|
21
|
+
return nil unless bundle
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
scope = raw_scope.nil? || raw_scope.strip.empty? ? "system" : raw_scope
|
|
23
|
+
scope = (scope_override ? bundle.with(scope: scope_override) : bundle).effective_scope
|
|
25
24
|
target_dir = MemoryPaths.scope_dir(scope)
|
|
26
|
-
files =
|
|
25
|
+
files = bundle.files
|
|
27
26
|
details = {}
|
|
28
|
-
files.each do |
|
|
29
|
-
file_key_str = file_key.to_s
|
|
27
|
+
files.each do |file_key_str, meta|
|
|
30
28
|
next details[file_key_str] = unchecked_file(meta) unless target_dir
|
|
31
29
|
|
|
32
30
|
target_path = File.join(target_dir, file_key_str)
|
|
33
31
|
base_path = provenance.base_path(file_key_str)
|
|
34
|
-
stored_checksum = meta[:checksum]
|
|
32
|
+
stored_checksum = meta[:checksum]
|
|
35
33
|
current_checksum = File.exist?(target_path) ? Digest::SHA256.hexdigest(File.read(target_path)) : nil
|
|
36
34
|
base_checksum = File.exist?(base_path) ? Digest::SHA256.hexdigest(File.read(base_path)) : nil
|
|
37
35
|
modified = base_checksum && current_checksum && base_checksum != current_checksum
|
|
38
36
|
missing = !File.exist?(target_path)
|
|
39
37
|
# A model overlay has no index line (Installer#note_overlay).
|
|
40
|
-
overlay = ModelOverlay.bundle_overlay?(file_key_str, bundle_files: files.keys
|
|
38
|
+
overlay = ModelOverlay.bundle_overlay?(file_key_str, bundle_files: files.keys, target_dir: target_dir)
|
|
41
39
|
index_present = overlay ? nil : index_has_entry?(scope, file_key_str)
|
|
42
40
|
details[file_key_str] = {
|
|
43
|
-
conflict: meta
|
|
41
|
+
conflict: meta[:conflict] == true,
|
|
44
42
|
stored_checksum: stored_checksum,
|
|
45
43
|
current_checksum: current_checksum,
|
|
46
44
|
base_checksum: base_checksum,
|
|
@@ -52,23 +50,23 @@ module Samagotchi
|
|
|
52
50
|
base_path: base_path
|
|
53
51
|
}
|
|
54
52
|
end
|
|
55
|
-
{ provenance:
|
|
56
|
-
target_dir: target_dir, files: details, plugin: plugin_status(provenance,
|
|
57
|
-
hooks_requires_failure: hooks_requires_failure(
|
|
53
|
+
{ provenance: bundle, scope: scope, scope_error: target_dir ? nil : "unknown scope: #{scope}",
|
|
54
|
+
target_dir: target_dir, files: details, plugin: plugin_status(provenance, bundle),
|
|
55
|
+
hooks_requires_failure: hooks_requires_failure(bundle), needs: needs_status(bundle) }
|
|
58
56
|
end
|
|
59
57
|
|
|
60
58
|
# Why none of the bundle's hooks load (this chi doesn't meet its
|
|
61
59
|
# requires_chi, Hooks::BundleLoader), or nil: it has no hooks or meets it.
|
|
62
|
-
def self.hooks_requires_failure(
|
|
63
|
-
return nil if
|
|
60
|
+
def self.hooks_requires_failure(bundle)
|
|
61
|
+
return nil if bundle.hooks.empty?
|
|
64
62
|
|
|
65
|
-
Manifest.requires_chi_failure(
|
|
63
|
+
Manifest.requires_chi_failure(bundle.requires_chi, Samagotchi::VERSION)
|
|
66
64
|
end
|
|
67
65
|
|
|
68
66
|
# The stored needs, each with found: from this process's PATH (the
|
|
69
67
|
# shell's; a worker's PATH can differ, see docs/memory.md).
|
|
70
|
-
def self.needs_status(
|
|
71
|
-
Manifest.parse_needs(
|
|
68
|
+
def self.needs_status(bundle, path: ENV["PATH"])
|
|
69
|
+
Manifest.parse_needs(bundle.needs).map do |need|
|
|
72
70
|
need.merge(found: BundleNeeds.found?(need[:command], path: path))
|
|
73
71
|
end
|
|
74
72
|
rescue Manifest::ValidationError
|
|
@@ -86,20 +84,20 @@ module Samagotchi
|
|
|
86
84
|
# The installed plugin: {file:, path:, state:, requires_chi:,
|
|
87
85
|
# requires_failure:}; state is "ok", "modified" (its sha256 differs
|
|
88
86
|
# from the installed one: it won't load) or "missing". nil without one.
|
|
89
|
-
def self.plugin_status(provenance,
|
|
90
|
-
path = provenance.plugin_path(
|
|
87
|
+
def self.plugin_status(provenance, bundle)
|
|
88
|
+
path = provenance.plugin_path(bundle)
|
|
91
89
|
return nil unless path
|
|
92
90
|
|
|
93
91
|
state = if !File.file?(path) then "missing"
|
|
94
|
-
elsif Provenance.sha_matches?(path,
|
|
92
|
+
elsif Provenance.sha_matches?(path, bundle.plugin.sha256) then "ok"
|
|
95
93
|
else "modified"
|
|
96
94
|
end
|
|
97
|
-
{ file: File.basename(path), path: path, state: state, requires_chi:
|
|
98
|
-
requires_failure: Manifest.requires_chi_failure(
|
|
95
|
+
{ file: File.basename(path), path: path, state: state, requires_chi: bundle.requires_chi,
|
|
96
|
+
requires_failure: Manifest.requires_chi_failure(bundle.requires_chi, Samagotchi::VERSION) }
|
|
99
97
|
end
|
|
100
98
|
|
|
101
99
|
def self.unchecked_file(meta)
|
|
102
|
-
{ conflict: meta
|
|
100
|
+
{ conflict: meta[:conflict] == true, stored_checksum: meta[:checksum],
|
|
103
101
|
unchecked: true, modified: false, missing: false, index_present: nil, overlay: false }
|
|
104
102
|
end
|
|
105
103
|
private_class_method :unchecked_file
|
|
@@ -72,10 +72,10 @@ module Samagotchi
|
|
|
72
72
|
to = gem_manifest.version.to_s
|
|
73
73
|
|
|
74
74
|
with_lock do
|
|
75
|
-
|
|
76
|
-
from =
|
|
75
|
+
bundle = Provenance.new(name: BUNDLE_NAME).record
|
|
76
|
+
from = bundle&.version.to_s
|
|
77
77
|
|
|
78
|
-
if
|
|
78
|
+
if bundle.nil?
|
|
79
79
|
dry_run ? result(:installed, to: to) : install_fresh(to)
|
|
80
80
|
elsif installed_newer?(from, to)
|
|
81
81
|
# Running an older checkout/gem: never downgrade the user's system memories
|
|
@@ -174,8 +174,7 @@ module Samagotchi
|
|
|
174
174
|
# here and changed in the shipped bundle (a dry run's kept).
|
|
175
175
|
def conflicting(gem_manifest)
|
|
176
176
|
provenance = Provenance.new(name: BUNDLE_NAME)
|
|
177
|
-
owned =
|
|
178
|
-
owned = owned.is_a?(Hash) ? owned.keys.map(&:to_s) : []
|
|
177
|
+
owned = provenance.record&.files&.keys || []
|
|
179
178
|
(gem_manifest.files.keys.map(&:to_s).sort & owned).select do |key|
|
|
180
179
|
Merger.classify(base_path: provenance.base_path(key), current_path: File.join(MemoryPaths.system_dir, key),
|
|
181
180
|
incoming_path: File.join(GEM_BUNDLE_DIR, key)) == :conflict
|
|
@@ -33,17 +33,15 @@ module Samagotchi
|
|
|
33
33
|
|
|
34
34
|
def run
|
|
35
35
|
provenance = Provenance.new(name: @name)
|
|
36
|
-
|
|
37
|
-
raise UninstallError, "Bundle '#{@name}' is not installed" unless
|
|
36
|
+
bundle = provenance.record
|
|
37
|
+
raise UninstallError, "Bundle '#{@name}' is not installed" unless bundle
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
raw = cli_scope || data[:scope]&.to_s
|
|
42
|
-
target_scope = raw.nil? || raw.strip.empty? ? "system" : raw
|
|
39
|
+
# The scope from the flag, else the record's
|
|
40
|
+
target_scope = @scope.to_s.strip.empty? ? bundle.effective_scope : @scope
|
|
43
41
|
target_dir = MemoryPaths.scope_dir(target_scope) or raise UninstallError, "invalid scope: #{target_scope}"
|
|
44
42
|
|
|
45
|
-
files =
|
|
46
|
-
hooks =
|
|
43
|
+
files = bundle.files
|
|
44
|
+
hooks = bundle.hooks
|
|
47
45
|
if files.empty? && hooks.empty?
|
|
48
46
|
# No file list, just remove provenance
|
|
49
47
|
FileUtils.rm_rf(provenance.bundle_dir)
|
|
@@ -99,7 +97,7 @@ module Samagotchi
|
|
|
99
97
|
# ── Hooks removal (bundle-owned) ──────────────────────────
|
|
100
98
|
# Hooks are stored under <bundle_dir>/hooks/ and are not part of the index.
|
|
101
99
|
# Explicit removal for intent; the final rm_rf sweeps it anyway.
|
|
102
|
-
if hooks.
|
|
100
|
+
if hooks.any?
|
|
103
101
|
hooks_dir = provenance.hooks_dir
|
|
104
102
|
hooks.each do |hook_key, _meta|
|
|
105
103
|
hook_key_str = hook_key.to_s
|
|
@@ -117,7 +115,7 @@ module Samagotchi
|
|
|
117
115
|
FileUtils.rm_rf(provenance.hooks_dir)
|
|
118
116
|
end
|
|
119
117
|
|
|
120
|
-
plugin_path = provenance.plugin_path(
|
|
118
|
+
plugin_path = provenance.plugin_path(bundle)
|
|
121
119
|
@removed_files << "plugin/#{File.basename(plugin_path)}" if plugin_path && File.exist?(plugin_path)
|
|
122
120
|
FileUtils.rm_rf(provenance.bundle_dir)
|
|
123
121
|
true
|
|
@@ -44,8 +44,8 @@ module Samagotchi
|
|
|
44
44
|
# @return [Array<String>] the bundles whose plugin loaded
|
|
45
45
|
def load_installed(registries, failures: nil, settings: {})
|
|
46
46
|
loaded = []
|
|
47
|
-
MemoryBundle::Provenance.each_installed(holding: :plugin) do |bundle_name,
|
|
48
|
-
ok = load_bundle(bundle_name,
|
|
47
|
+
MemoryBundle::Provenance.each_installed(holding: :plugin) do |bundle_name, bundle|
|
|
48
|
+
ok = load_bundle(bundle_name, bundle, registries, failures: failures, settings: settings[bundle_name.to_s] || {})
|
|
49
49
|
loaded << bundle_name if ok
|
|
50
50
|
end
|
|
51
51
|
loaded
|
|
@@ -56,10 +56,10 @@ module Samagotchi
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# @return [Boolean] whether the plugin loaded
|
|
59
|
-
def load_bundle(bundle_name,
|
|
60
|
-
file = MemoryBundle::Provenance.new(name: bundle_name).plugin_path(
|
|
59
|
+
def load_bundle(bundle_name, bundle, registries, failures: nil, settings: {})
|
|
60
|
+
file = MemoryBundle::Provenance.new(name: bundle_name).plugin_path(bundle) unless bundle.error?
|
|
61
61
|
basename = file ? File.basename(file) : "plugin"
|
|
62
|
-
reason =
|
|
62
|
+
reason = bundle.error || unloadable_reason(file, bundle)
|
|
63
63
|
return failed(bundle_name, basename, reason, failures) if reason
|
|
64
64
|
|
|
65
65
|
api = nil
|
|
@@ -82,12 +82,12 @@ module Samagotchi
|
|
|
82
82
|
def label(bundle_name, basename) = "#{basename} (bundle #{bundle_name})"
|
|
83
83
|
|
|
84
84
|
# Why the installed file can't be loaded, or nil.
|
|
85
|
-
def unloadable_reason(file,
|
|
86
|
-
failure = MemoryBundle::Manifest.requires_chi_failure(
|
|
85
|
+
def unloadable_reason(file, bundle)
|
|
86
|
+
failure = MemoryBundle::Manifest.requires_chi_failure(bundle.requires_chi, Samagotchi::VERSION)
|
|
87
87
|
return failure if failure
|
|
88
88
|
return "the file is missing" unless file && File.file?(file)
|
|
89
89
|
|
|
90
|
-
Hooks::BundleLoader.sha_mismatch(file,
|
|
90
|
+
Hooks::BundleLoader.sha_mismatch(file, bundle.plugin.sha256, required: true)
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# module_eval the file into a fresh module in the bundle's namespace
|
|
@@ -406,8 +406,8 @@ module Samagotchi
|
|
|
406
406
|
# "samagotchi-system 0.1.5 (shipped 0.1.5), other 1.0.0"
|
|
407
407
|
def bundles_summary
|
|
408
408
|
shipped = MemoryBundle::Manifest.read(dir: MemoryBundle::SystemBundle::GEM_BUNDLE_DIR).version rescue nil
|
|
409
|
-
installed = MemoryBundle::Provenance.each_installed.map do |name,
|
|
410
|
-
version =
|
|
409
|
+
installed = MemoryBundle::Provenance.each_installed.map do |name, bundle|
|
|
410
|
+
version = bundle.version || "?"
|
|
411
411
|
label = "#{name} #{version}"
|
|
412
412
|
label += " (shipped #{shipped})" if name == MemoryBundle::SystemBundle::BUNDLE_NAME
|
|
413
413
|
label
|