samagotchi 0.150.0 → 0.152.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 +50 -1
- data/docs/architecture.md +2 -2
- data/docs/bundle-authoring.md +5 -5
- data/docs/cli.md +32 -7
- data/docs/configuration.md +1 -1
- data/docs/guardrails.md +4 -0
- data/docs/internals/block-renderers.md +98 -0
- data/docs/internals/bridge.md +53 -17
- data/docs/internals/bundles.md +49 -14
- data/docs/internals/objectives.md +6 -4
- data/docs/internals/web-frontend.md +2 -2
- data/docs/memory.md +7 -2
- data/docs/plugins.md +91 -14
- data/docs/releasing.md +7 -2
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/block_renderers.rb +116 -0
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +219 -122
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/check.rb +43 -3
- data/lib/samagotchi/bundle_command/installed_check.rb +6 -2
- data/lib/samagotchi/bundle_command/try/sandbox.rb +11 -3
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +88 -10
- data/lib/samagotchi/bundle_needs.rb +4 -6
- data/lib/samagotchi/bundle_settings_check.rb +14 -13
- data/lib/samagotchi/bundles/agents-md/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +3 -3
- data/lib/samagotchi/child_env.rb +15 -0
- data/lib/samagotchi/context_providers.rb +42 -10
- data/lib/samagotchi/engine.rb +47 -6
- data/lib/samagotchi/extension_load.rb +4 -3
- data/lib/samagotchi/guardrail_wiring.rb +28 -19
- data/lib/samagotchi/guardrails/approval.rb +2 -2
- data/lib/samagotchi/guardrails/rules.rb +1 -0
- data/lib/samagotchi/guardrails/verdict.rb +20 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +5 -7
- data/lib/samagotchi/memory_bundle/asset_installer.rb +18 -9
- data/lib/samagotchi/memory_bundle/builder.rb +39 -31
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +3 -2
- data/lib/samagotchi/memory_bundle/file_digest.rb +31 -0
- data/lib/samagotchi/memory_bundle/file_standing.rb +2 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +55 -43
- data/lib/samagotchi/memory_bundle/installed_copy.rb +38 -14
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +224 -0
- data/lib/samagotchi/memory_bundle/installer.rb +91 -16
- data/lib/samagotchi/memory_bundle/manifest.rb +166 -5
- data/lib/samagotchi/memory_bundle/manifest_standing.rb +171 -0
- data/lib/samagotchi/memory_bundle/merger.rb +7 -8
- data/lib/samagotchi/memory_bundle/profile.rb +3 -2
- data/lib/samagotchi/memory_bundle/provenance.rb +220 -19
- data/lib/samagotchi/memory_bundle/shas.rb +4 -3
- data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -24
- data/lib/samagotchi/memory_bundle/status.rb +11 -10
- data/lib/samagotchi/memory_bundle/system_bundle.rb +13 -2
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_config.rb +5 -4
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/system_one/record.rb +10 -2
- data/lib/samagotchi/terminal_ui.rb +4 -4
- data/lib/samagotchi/tools/delegate.rb +20 -2
- data/lib/samagotchi/turn_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -3
- data/lib/samagotchi/update_command.rb +13 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +24 -13
- data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
- data/lib/samagotchi/web/markdown_renderer.rb +39 -6
- data/lib/samagotchi/web/message_display.rb +3 -3
- data/lib/samagotchi/web/participant_view.rb +1 -1
- data/lib/samagotchi/web/public/app.js +2 -0
- data/lib/samagotchi/web/public/block_frame.js +202 -0
- data/lib/samagotchi/web/public/block_host.js +502 -0
- data/lib/samagotchi/web/public/block_renderers.css +20 -0
- data/lib/samagotchi/web/public/block_source.js +37 -0
- data/lib/samagotchi/web/public/bubbles.js +9 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +2 -0
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- data/lib/samagotchi/web/public/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/session_access_routes.rb +2 -1
- data/lib/samagotchi/web/session_hub.rb +6 -0
- data/lib/samagotchi/web/session_show.rb +1 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +13 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require_relative "../atomic_file"
|
|
5
|
+
require_relative "file_digest"
|
|
5
6
|
require_relative "installed_bundle"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
@@ -154,14 +155,12 @@ module Samagotchi
|
|
|
154
155
|
|
|
155
156
|
# No base (legacy bundle without provenance or first file) -> treat as modified
|
|
156
157
|
unless base_exists
|
|
157
|
-
|
|
158
|
-
current_digest = Digest::SHA256.hexdigest(File.read(current_path))
|
|
159
|
-
return current_digest == incoming_digest ? NOOP : CONFLICT
|
|
158
|
+
return FileDigest.hex(current_path) == FileDigest.hex(incoming_path) ? NOOP : CONFLICT
|
|
160
159
|
end
|
|
161
160
|
|
|
162
|
-
base_digest =
|
|
163
|
-
current_digest =
|
|
164
|
-
incoming_digest =
|
|
161
|
+
base_digest = FileDigest.hex(base_path)
|
|
162
|
+
current_digest = FileDigest.hex(current_path)
|
|
163
|
+
incoming_digest = FileDigest.hex(incoming_path)
|
|
165
164
|
|
|
166
165
|
if current_digest == incoming_digest
|
|
167
166
|
NOOP
|
|
@@ -242,13 +241,13 @@ module Samagotchi
|
|
|
242
241
|
|
|
243
242
|
# The file's digest, nil when there is none.
|
|
244
243
|
def self.digest(path)
|
|
245
|
-
|
|
244
|
+
FileDigest.hex(path)
|
|
246
245
|
end
|
|
247
246
|
|
|
248
247
|
def self.current_modified?(base_path, current_path)
|
|
249
248
|
return true unless base_path && File.exist?(base_path) && File.exist?(current_path)
|
|
250
249
|
|
|
251
|
-
|
|
250
|
+
FileDigest.hex(base_path) != FileDigest.hex(current_path)
|
|
252
251
|
end
|
|
253
252
|
end
|
|
254
253
|
end
|
|
@@ -114,7 +114,8 @@ module Samagotchi
|
|
|
114
114
|
|
|
115
115
|
Provenance.new(name: manifest.name).write(
|
|
116
116
|
files: {}, scope: "system", version: manifest.version, source_path: dir,
|
|
117
|
-
trust_level: manifest.trust_level, requires_chi: manifest.requires_chi, includes: includes
|
|
117
|
+
trust_level: manifest.trust_level, requires_chi: manifest.requires_chi, includes: includes,
|
|
118
|
+
manifest_from: dir
|
|
118
119
|
)
|
|
119
120
|
result
|
|
120
121
|
end
|
|
@@ -127,7 +128,7 @@ module Samagotchi
|
|
|
127
128
|
# A dry run runs the Installer's too, which writes nothing, so it
|
|
128
129
|
# names the files the real run would keep as the user's own.
|
|
129
130
|
installer = Installer.new(source: member_dir, name: member, scope: "system", force: force, strict: true,
|
|
130
|
-
shipped_dir: shipped_dir, dry_run: dry_run)
|
|
131
|
+
shipped_dir: shipped_dir, dry_run: dry_run, chi_version: chi_version)
|
|
131
132
|
installer.run
|
|
132
133
|
result.installed << member
|
|
133
134
|
kept = installer.results.select { |_, entry| kept_own?(entry) }.keys
|
|
@@ -4,10 +4,14 @@ require "json"
|
|
|
4
4
|
require "digest"
|
|
5
5
|
require "date"
|
|
6
6
|
require "fileutils"
|
|
7
|
+
require "tmpdir"
|
|
7
8
|
require_relative "../atomic_file"
|
|
8
9
|
require_relative "../memory_paths"
|
|
10
|
+
require_relative "../version"
|
|
9
11
|
require_relative "bundle_hook"
|
|
12
|
+
require_relative "file_digest"
|
|
10
13
|
require_relative "installed_bundle"
|
|
14
|
+
require_relative "manifest"
|
|
11
15
|
|
|
12
16
|
module Samagotchi
|
|
13
17
|
module MemoryBundle
|
|
@@ -15,6 +19,9 @@ module Samagotchi
|
|
|
15
19
|
# Each installed bundle gets a directory with:
|
|
16
20
|
# manifest.json — metadata about the install (merged on reinstall)
|
|
17
21
|
# bases/ — snapshots of every file as installed (for 3-way merge later)
|
|
22
|
+
# manifest.yml — the bundle's own manifest as installed (#write's
|
|
23
|
+
# manifest_from:), its sha in the record; read live
|
|
24
|
+
# through InstalledManifest
|
|
18
25
|
class Provenance
|
|
19
26
|
def self.bundles_dir
|
|
20
27
|
MemoryPaths.bundles_dir
|
|
@@ -59,6 +66,11 @@ module Samagotchi
|
|
|
59
66
|
File.join(@bundle_dir, "scripts")
|
|
60
67
|
end
|
|
61
68
|
|
|
69
|
+
# The bundle's manifest.yml as installed (InstalledBundle#kept_manifest_path).
|
|
70
|
+
def kept_manifest_path
|
|
71
|
+
File.join(@bundle_dir, "manifest.yml")
|
|
72
|
+
end
|
|
73
|
+
|
|
62
74
|
# The plugin file as installed, or nil when the bundle has none.
|
|
63
75
|
# @param bundle [InstalledBundle, nil] its record (read when not given)
|
|
64
76
|
def plugin_path(bundle = record)
|
|
@@ -92,7 +104,7 @@ module Samagotchi
|
|
|
92
104
|
|
|
93
105
|
begin
|
|
94
106
|
data = JSON.parse(File.read(mjson), symbolize_names: true)
|
|
95
|
-
rescue JSON::ParserError, SystemCallError => e
|
|
107
|
+
rescue JSON::ParserError, EncodingError, SystemCallError => e # a key that isn't UTF-8 can't be a symbol
|
|
96
108
|
if holding.nil? || Dir.exist?(File.join(dir, name, holding.to_s))
|
|
97
109
|
yield name, InstalledBundle.unreadable(name, "manifest.json is unreadable: #{e.message}")
|
|
98
110
|
end
|
|
@@ -100,7 +112,7 @@ module Samagotchi
|
|
|
100
112
|
end
|
|
101
113
|
next if holding && !(data.is_a?(Hash) && data[holding].is_a?(Hash) && !data[holding].empty?)
|
|
102
114
|
|
|
103
|
-
yield name, InstalledBundle.parse(name, data)
|
|
115
|
+
yield name, InstalledBundle.parse(name, data, File.join(dir, name))
|
|
104
116
|
end
|
|
105
117
|
end
|
|
106
118
|
|
|
@@ -124,8 +136,11 @@ module Samagotchi
|
|
|
124
136
|
value.to_s.delete_prefix("sha256:")
|
|
125
137
|
end
|
|
126
138
|
|
|
139
|
+
# The digest of a file, nil when it isn't there (FileDigest). Every
|
|
140
|
+
# caller checks existence first; #sha_matches? reads a missing file as
|
|
141
|
+
# not the recorded one.
|
|
127
142
|
def self.file_sha(path)
|
|
128
|
-
|
|
143
|
+
FileDigest.hex(path)
|
|
129
144
|
end
|
|
130
145
|
|
|
131
146
|
# What a dev install (`chi bundle install --dev`) records instead of
|
|
@@ -144,6 +159,24 @@ module Samagotchi
|
|
|
144
159
|
unchecked?(recorded) || file_sha(path) == recorded_sha(recorded)
|
|
145
160
|
end
|
|
146
161
|
|
|
162
|
+
# The record's mirror of the manifest keys (InstalledBundle::MIRROR_KEYS)
|
|
163
|
+
# as #write records them from its kwargs: what an older chi reads, and
|
|
164
|
+
# what #adopt_manifest holds a source's manifest against.
|
|
165
|
+
def self.mirror_fields(trust_level:, requires_chi:, needs:, context_providers:, block_renderers:, session_settings:,
|
|
166
|
+
session_setting_types:, settings:, setting_types:)
|
|
167
|
+
fields = {}
|
|
168
|
+
fields["trust_level"] = trust_level.to_s if trust_level && !trust_level.to_s.empty?
|
|
169
|
+
fields["requires_chi"] = requires_chi.to_s if requires_chi && !requires_chi.to_s.empty?
|
|
170
|
+
fields["needs"] = needs.map { |n| n.transform_keys(&:to_s).compact } if needs.is_a?(Array) && !needs.empty?
|
|
171
|
+
fields["context_providers"] = context_providers.map(&:to_h) unless context_providers.empty?
|
|
172
|
+
fields["block_renderers"] = block_renderers.map(&:to_h) unless block_renderers.empty?
|
|
173
|
+
fields["session_settings"] = Array(session_settings).map(&:to_s) unless Array(session_settings).empty?
|
|
174
|
+
fields["session_setting_types"] = session_setting_types.to_h { |k, v| [k.to_s, v.to_s] } unless session_setting_types.to_h.empty?
|
|
175
|
+
fields["settings"] = settings.map(&:to_s) unless settings.nil?
|
|
176
|
+
fields["setting_types"] = setting_types.to_h { |k, v| [k.to_s, v.to_s] } unless setting_types.to_h.empty?
|
|
177
|
+
fields
|
|
178
|
+
end
|
|
179
|
+
|
|
147
180
|
# Writes provenance for an installation: merges into existing manifest.json
|
|
148
181
|
# and saves base snapshots. Stale base snapshots (files removed from bundle)
|
|
149
182
|
# are pruned.
|
|
@@ -172,12 +205,21 @@ module Samagotchi
|
|
|
172
205
|
# @param scripts_files [Hash] file name => installed script; the
|
|
173
206
|
# sha256 of each is recorded (ContextFetch checks it before a run)
|
|
174
207
|
# @param context_providers [Array<ContextProviders::Provider>]
|
|
208
|
+
# @param block_renderers [Array<BlockRenderers::Renderer>]
|
|
175
209
|
# @param dev [Boolean] a dev install: its files are links to the
|
|
176
210
|
# source, recorded as DEV_SHA (no sha256 check) with "dev": true
|
|
211
|
+
# @param manifest_from [String, nil] the source dir: its manifest.yml is
|
|
212
|
+
# kept at #kept_manifest_path (a copy, a link for a dev install),
|
|
213
|
+
# recorded as manifest_sha (DEV_SHA for dev), with installed_by: this
|
|
214
|
+
# chi's VERSION. nil leaves the kept file, and the record's
|
|
215
|
+
# manifest_sha and installed_by, as they are. The keys a record mirrors
|
|
216
|
+
# (requires_chi … setting_types) are still written from the kwargs:
|
|
217
|
+
# an older chi sharing this memories dir reads only them
|
|
218
|
+
# (docs/internals/bundles.md); a key a later manifest gains is never added
|
|
177
219
|
def write(files:, scope:, version:, source_path:, hooks: {}, trust_level: nil, source_commit: nil, hooks_files: {},
|
|
178
220
|
guardrails_files: {}, plugin_file: nil, requires_chi: nil, needs: nil, marks: [], incoming_dir: nil, includes: nil,
|
|
179
|
-
scripts_files: {}, context_providers: [], file_descriptions: {}, session_settings: [], session_setting_types: {}, settings: nil, setting_types: {},
|
|
180
|
-
dev: false)
|
|
221
|
+
scripts_files: {}, context_providers: [], block_renderers: [], file_descriptions: {}, session_settings: [], session_setting_types: {}, settings: nil, setting_types: {},
|
|
222
|
+
dev: false, manifest_from: nil)
|
|
181
223
|
FileUtils.mkdir_p(@bundle_dir)
|
|
182
224
|
bases_dir = File.join(@bundle_dir, "bases")
|
|
183
225
|
FileUtils.mkdir_p(bases_dir)
|
|
@@ -264,35 +306,32 @@ module Samagotchi
|
|
|
264
306
|
"files" => merged_entries,
|
|
265
307
|
"hooks" => hooks_map
|
|
266
308
|
}
|
|
267
|
-
manifest_data["trust_level"] = trust_level.to_s if trust_level && !trust_level.to_s.empty?
|
|
268
309
|
manifest_data["dev"] = true if dev
|
|
269
310
|
if guardrails_files.is_a?(Hash) && !guardrails_files.empty?
|
|
270
311
|
manifest_data["guardrails"] = guardrails_files.sort.to_h do |basename, path|
|
|
271
|
-
[basename.to_s, { "sha256" => dev ? DEV_SHA :
|
|
312
|
+
[basename.to_s, { "sha256" => dev ? DEV_SHA : FileDigest.sha256(path.to_s) }]
|
|
272
313
|
end
|
|
273
314
|
end
|
|
274
315
|
manifest_data["source_commit"] = source_commit.to_s if source_commit && !source_commit.to_s.empty?
|
|
275
316
|
FileUtils.rm_rf(File.join(bases_dir, "plugin"))
|
|
276
317
|
if plugin_file
|
|
277
|
-
content = File.binread(plugin_file.to_s)
|
|
318
|
+
content = File.binread(plugin_file.to_s) # the base snapshot's bytes
|
|
278
319
|
manifest_data["plugin"] = { "file" => File.basename(plugin_file.to_s),
|
|
279
|
-
"sha256" => dev ? DEV_SHA :
|
|
320
|
+
"sha256" => dev ? DEV_SHA : FileDigest.sha256(plugin_file.to_s) }
|
|
280
321
|
FileUtils.mkdir_p(File.join(bases_dir, "plugin"))
|
|
281
322
|
File.binwrite(plugin_base_path(File.basename(plugin_file.to_s)), content)
|
|
282
323
|
end
|
|
283
|
-
manifest_data
|
|
284
|
-
|
|
324
|
+
manifest_data.merge!(self.class.mirror_fields(trust_level: trust_level, requires_chi: requires_chi, needs: needs,
|
|
325
|
+
context_providers: context_providers, block_renderers: block_renderers,
|
|
326
|
+
session_settings: session_settings, session_setting_types: session_setting_types,
|
|
327
|
+
settings: settings, setting_types: setting_types))
|
|
285
328
|
manifest_data["includes"] = includes.map(&:to_s) if includes.is_a?(Array)
|
|
286
329
|
unless scripts_files.empty?
|
|
287
330
|
manifest_data["scripts"] = scripts_files.sort.to_h do |file, path|
|
|
288
|
-
[file.to_s, { "sha256" => dev ? DEV_SHA :
|
|
331
|
+
[file.to_s, { "sha256" => dev ? DEV_SHA : FileDigest.sha256(path.to_s) }]
|
|
289
332
|
end
|
|
290
333
|
end
|
|
291
|
-
manifest_data
|
|
292
|
-
manifest_data["session_settings"] = Array(session_settings).map(&:to_s) unless Array(session_settings).empty?
|
|
293
|
-
manifest_data["session_setting_types"] = session_setting_types.to_h { |k, v| [k.to_s, v.to_s] } unless session_setting_types.to_h.empty?
|
|
294
|
-
manifest_data["settings"] = settings.map(&:to_s) unless settings.nil?
|
|
295
|
-
manifest_data["setting_types"] = setting_types.to_h { |k, v| [k.to_s, v.to_s] } unless setting_types.to_h.empty?
|
|
334
|
+
manifest_data.merge!(kept_manifest_fields(existing, manifest_from, dev))
|
|
296
335
|
|
|
297
336
|
write_manifest(manifest_data)
|
|
298
337
|
marked.each_key { |key| save_incoming(key, incoming_dir) }
|
|
@@ -402,6 +441,39 @@ module Samagotchi
|
|
|
402
441
|
nil
|
|
403
442
|
end
|
|
404
443
|
|
|
444
|
+
# Keeps +dir+/manifest.yml for an installed bundle whose record has no
|
|
445
|
+
# usable kept manifest (InstalledBundle#manifest_unkept?: an older chi
|
|
446
|
+
# wrote it, or the file is gone), when +dir+ is the source of what's
|
|
447
|
+
# installed: its manifest names this bundle at the recorded version and
|
|
448
|
+
# says what the record keeps (#manifest_difference), and every sha the
|
|
449
|
+
# record has for a memory, plugin, hook, script or rule is the source
|
|
450
|
+
# file's. Then the file is copied to #kept_manifest_path and the
|
|
451
|
+
# record gains its manifest_sha, and nothing else changes (installed_at,
|
|
452
|
+
# the mirror, installed_by). A dev record is never adopted: its next
|
|
453
|
+
# install --dev links the file. Read-only with dry_run.
|
|
454
|
+
# @return [String, nil] why it wasn't (wouldn't be) kept, nil when it was
|
|
455
|
+
def adopt_manifest(dir, dry_run: false)
|
|
456
|
+
bundle = record
|
|
457
|
+
return "#{@name} isn't installed" unless bundle
|
|
458
|
+
return "#{@name} is a dev install" if bundle.dev
|
|
459
|
+
return "#{@name} keeps its manifest already" unless bundle.manifest_unkept?
|
|
460
|
+
|
|
461
|
+
source = File.join(dir.to_s, "manifest.yml")
|
|
462
|
+
return "#{dir} has no manifest.yml" unless File.file?(source)
|
|
463
|
+
|
|
464
|
+
# One read: the bytes checked are the bytes kept.
|
|
465
|
+
bytes = File.binread(source)
|
|
466
|
+
why = adoption_mismatch(bundle, dir.to_s, bytes)
|
|
467
|
+
return why if why || dry_run
|
|
468
|
+
|
|
469
|
+
AtomicFile.write(kept_manifest_path, bytes)
|
|
470
|
+
raw = JSON.parse(File.read(File.join(@bundle_dir, "manifest.json")))
|
|
471
|
+
write_manifest(raw.merge("manifest_sha" => "sha256:#{Digest::SHA256.hexdigest(bytes)}"))
|
|
472
|
+
nil
|
|
473
|
+
rescue JSON::ParserError, SystemCallError => e
|
|
474
|
+
"#{@name}'s record can't take it: #{e.message}"
|
|
475
|
+
end
|
|
476
|
+
|
|
405
477
|
# The installed bundle's record, nil when it isn't installed.
|
|
406
478
|
# @return [InstalledBundle, nil]
|
|
407
479
|
# @raise [JSON::ParserError] manifest.json doesn't parse
|
|
@@ -426,6 +498,125 @@ module Samagotchi
|
|
|
426
498
|
end
|
|
427
499
|
private :write_manifest
|
|
428
500
|
|
|
501
|
+
# The record's manifest_sha and installed_by: a fresh kept copy's, or
|
|
502
|
+
# (no manifest_from) the existing record's, the kept file left alone.
|
|
503
|
+
def kept_manifest_fields(existing, manifest_from, dev)
|
|
504
|
+
if manifest_from.nil?
|
|
505
|
+
return { "manifest_sha" => existing&.manifest_sha, "installed_by" => existing&.installed_by }.compact
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
sha = keep_manifest(File.join(manifest_from.to_s, "manifest.yml"), dev)
|
|
509
|
+
sha ? { "manifest_sha" => sha, "installed_by" => Samagotchi::VERSION } : {}
|
|
510
|
+
end
|
|
511
|
+
private :kept_manifest_fields
|
|
512
|
+
|
|
513
|
+
# +source+ to #kept_manifest_path: a link to its realpath for a dev
|
|
514
|
+
# install (DEV_SHA), else a copy written aside and renamed (a link left
|
|
515
|
+
# by a dev install is replaced, never written through), its sha.
|
|
516
|
+
# A source with no manifest.yml leaves none kept: nil.
|
|
517
|
+
def keep_manifest(source, dev)
|
|
518
|
+
dest = kept_manifest_path
|
|
519
|
+
unless File.file?(source)
|
|
520
|
+
FileUtils.rm_f(dest)
|
|
521
|
+
return nil
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
if dev
|
|
525
|
+
FileUtils.rm_f(dest)
|
|
526
|
+
File.symlink(File.realpath(source), dest)
|
|
527
|
+
return DEV_SHA
|
|
528
|
+
end
|
|
529
|
+
AtomicFile.write(dest, File.binread(source))
|
|
530
|
+
FileDigest.sha256(dest)
|
|
531
|
+
end
|
|
532
|
+
private :keep_manifest
|
|
533
|
+
|
|
534
|
+
# Why +dir+ isn't the source of what +bundle+ installed (#adopt_manifest), nil when it is.
|
|
535
|
+
def adoption_mismatch(bundle, dir, bytes)
|
|
536
|
+
manifest = begin
|
|
537
|
+
parsed(bytes)
|
|
538
|
+
rescue Manifest::ValidationError, Psych::Exception, EncodingError => e
|
|
539
|
+
return "#{dir}'s manifest.yml doesn't read: #{e.message}"
|
|
540
|
+
end
|
|
541
|
+
found = "#{manifest.name} #{manifest.version}"
|
|
542
|
+
return "#{dir} has #{found}, not #{@name} #{bundle.version}" unless manifest.name == @name
|
|
543
|
+
return "#{dir} has #{found}, not #{bundle.version}" unless manifest.version == bundle.version.to_s
|
|
544
|
+
|
|
545
|
+
key = manifest_difference(bundle, manifest)
|
|
546
|
+
return "#{dir}'s manifest.yml isn't the installed one (#{key})" if key
|
|
547
|
+
|
|
548
|
+
installed_shas(bundle, manifest).each do |rel, sha|
|
|
549
|
+
next if self.class.recorded_sha(sha).empty?
|
|
550
|
+
return "#{dir}'s #{rel} isn't the installed one" unless self.class.file_sha(File.join(dir, rel)) == self.class.recorded_sha(sha)
|
|
551
|
+
end
|
|
552
|
+
nil
|
|
553
|
+
end
|
|
554
|
+
private :adoption_mismatch
|
|
555
|
+
|
|
556
|
+
# +bytes+ as a manifest.yml, read where nothing else can change it.
|
|
557
|
+
def parsed(bytes)
|
|
558
|
+
Dir.mktmpdir("chi-adopt-") do |tmp|
|
|
559
|
+
path = File.join(tmp, "manifest.yml")
|
|
560
|
+
File.binwrite(path, bytes)
|
|
561
|
+
Manifest.new(path: path)
|
|
562
|
+
rescue Manifest::ValidationError => e
|
|
563
|
+
raise Manifest::ValidationError, e.message.gsub(tmp, "the source")
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
private :parsed
|
|
567
|
+
|
|
568
|
+
# The SINCE entry of a mirrored key whose shape a newer chi reads.
|
|
569
|
+
MIRROR_SINCE = { "session_setting_types" => "session_settings.types", "setting_types" => "settings.types" }.freeze
|
|
570
|
+
|
|
571
|
+
# The first manifest key in which +manifest+ says other than what the
|
|
572
|
+
# record keeps, nil when none: each mirrored key as #write records it (one
|
|
573
|
+
# the record lacks is fine only when an older chi couldn't have known it:
|
|
574
|
+
# Manifest::SINCE has it), then each recorded hook's events, on_error and
|
|
575
|
+
# priority.
|
|
576
|
+
def manifest_difference(bundle, manifest)
|
|
577
|
+
recorded = plain(bundle.mirror)
|
|
578
|
+
source = plain(self.class.mirror_fields(
|
|
579
|
+
trust_level: manifest.trust_level, requires_chi: manifest.requires_chi, needs: manifest.needs,
|
|
580
|
+
context_providers: manifest.context_providers, block_renderers: manifest.block_renderers,
|
|
581
|
+
session_settings: manifest.session_settings, session_setting_types: manifest.session_setting_types,
|
|
582
|
+
settings: manifest.settings, setting_types: manifest.setting_types
|
|
583
|
+
))
|
|
584
|
+
differing = InstalledBundle::MIRROR_KEYS.find do |key|
|
|
585
|
+
next false if recorded[key] == source[key]
|
|
586
|
+
|
|
587
|
+
!(recorded[key].nil? && Manifest::SINCE.key?(MIRROR_SINCE.fetch(key, key)))
|
|
588
|
+
end
|
|
589
|
+
return differing if differing
|
|
590
|
+
|
|
591
|
+
same_hooks = bundle.hooks.all? do |file, hook|
|
|
592
|
+
theirs = manifest.hooks.fetch(file, BundleHook.new)
|
|
593
|
+
[hook.events, hook.on_error, hook.priority] == [theirs.events, theirs.on_error, theirs.priority]
|
|
594
|
+
end
|
|
595
|
+
same_hooks ? nil : "hooks"
|
|
596
|
+
end
|
|
597
|
+
private :manifest_difference
|
|
598
|
+
|
|
599
|
+
def plain(value) = JSON.parse(JSON.generate(value))
|
|
600
|
+
private :plain
|
|
601
|
+
|
|
602
|
+
# Each file the record has a sha for, by its path in the bundle's source
|
|
603
|
+
# => that sha: the plugin, hooks, scripts, rules, and each memory whose
|
|
604
|
+
# entry has a checksum and no conflict mark (its base is the bundle's text).
|
|
605
|
+
def installed_shas(bundle, manifest)
|
|
606
|
+
shas = bundle.files.each_with_object({}) do |(key, entry), acc|
|
|
607
|
+
acc[key] = entry[:checksum].to_s unless entry[:conflict] || entry[:checksum].to_s.empty?
|
|
608
|
+
end
|
|
609
|
+
if bundle.plugin
|
|
610
|
+
file = manifest.plugin && manifest.plugin[:file].to_s
|
|
611
|
+
shas[file && File.basename(file) == bundle.plugin.file ? file : bundle.plugin.file] = bundle.plugin.sha256
|
|
612
|
+
end
|
|
613
|
+
bundle.hooks.each { |file, hook| shas["hooks/#{file}"] = hook.sha256 }
|
|
614
|
+
bundle.scripts.each { |file, sha| shas["scripts/#{file}"] = sha }
|
|
615
|
+
bundle.guardrails.each { |file, sha| shas["guardrails/#{file}"] = sha }
|
|
616
|
+
shas
|
|
617
|
+
end
|
|
618
|
+
private :installed_shas
|
|
619
|
+
|
|
429
620
|
def mark_fields(mark)
|
|
430
621
|
{ conflict: true, why: mark.why, missed: mark.missed.empty? ? nil : mark.missed,
|
|
431
622
|
missed_places: mark.places.zero? ? nil : mark.places }.compact
|
|
@@ -433,7 +624,7 @@ module Samagotchi
|
|
|
433
624
|
private :mark_fields
|
|
434
625
|
|
|
435
626
|
def incoming_sha(path)
|
|
436
|
-
|
|
627
|
+
FileDigest.hex(path)
|
|
437
628
|
rescue SystemCallError
|
|
438
629
|
nil
|
|
439
630
|
end
|
|
@@ -457,11 +648,21 @@ module Samagotchi
|
|
|
457
648
|
private :marked_in
|
|
458
649
|
|
|
459
650
|
# Each file that settled loses its saved text; the folder goes with the last.
|
|
651
|
+
# A file key can nest (a manifest's `files:` may name a path), so every file
|
|
652
|
+
# under incoming/ at any depth is looked at, and a dir the prune empties goes
|
|
653
|
+
# with it, deepest first.
|
|
460
654
|
def prune_incoming(keep)
|
|
461
655
|
dir = File.join(@bundle_dir, "incoming")
|
|
462
656
|
return unless Dir.exist?(dir)
|
|
463
657
|
|
|
464
|
-
|
|
658
|
+
keep = keep.map(&:to_s)
|
|
659
|
+
entries = Dir.glob("**/*", base: dir)
|
|
660
|
+
entries.each do |rel|
|
|
661
|
+
FileUtils.rm_f(File.join(dir, rel)) if File.file?(File.join(dir, rel)) && !keep.include?(rel)
|
|
662
|
+
end
|
|
663
|
+
entries.select { |rel| File.directory?(File.join(dir, rel)) }
|
|
664
|
+
.sort_by { |rel| -rel.count("/") } # deepest first, so a parent is checked after its children
|
|
665
|
+
.each { |rel| Dir.rmdir(File.join(dir, rel)) if Dir.empty?(File.join(dir, rel)) }
|
|
465
666
|
Dir.rmdir(dir) if Dir.empty?(dir)
|
|
466
667
|
rescue SystemCallError
|
|
467
668
|
nil
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "digest"
|
|
4
3
|
require "yaml"
|
|
4
|
+
require_relative "file_digest"
|
|
5
5
|
|
|
6
6
|
module Samagotchi
|
|
7
7
|
module MemoryBundle
|
|
@@ -100,9 +100,10 @@ module Samagotchi
|
|
|
100
100
|
|
|
101
101
|
def digest(dir, file)
|
|
102
102
|
path = File.join(dir, file)
|
|
103
|
-
|
|
103
|
+
hex = FileDigest.hex(path)
|
|
104
|
+
raise Error, "#{File.basename(dir)}/manifest.yml names #{file}, which isn't there" if hex.nil?
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
hex
|
|
106
107
|
end
|
|
107
108
|
private_class_method :digest
|
|
108
109
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "installer"
|
|
4
4
|
require_relative "listing"
|
|
5
5
|
require_relative "manifest"
|
|
6
|
+
require_relative "manifest_standing"
|
|
6
7
|
require_relative "merger"
|
|
7
8
|
require_relative "profile"
|
|
8
9
|
require_relative "provenance"
|
|
@@ -51,8 +52,11 @@ module Samagotchi
|
|
|
51
52
|
# (Installer#trash_copy_line).
|
|
52
53
|
# earlier: the merge notes on its record when the plan read it
|
|
53
54
|
# (FileStanding::MergeNote): this run's own merges aren't among them.
|
|
55
|
+
# manifest: what a row at the shipped version says of its manifest.yml
|
|
56
|
+
# (ManifestStanding.keep's line: would keep or kept it, or why not and
|
|
57
|
+
# the command), nil when nothing.
|
|
54
58
|
Row = Struct.new(:name, :from, :to, :status, :note, :merged, :behind, :replaced, :source_dir, :scope, :adds, :kept_own,
|
|
55
|
-
:trash_copy, :earlier, keyword_init: true) do
|
|
59
|
+
:trash_copy, :earlier, :manifest, keyword_init: true) do
|
|
56
60
|
def kept = Array(behind).map(&:file)
|
|
57
61
|
end
|
|
58
62
|
|
|
@@ -67,6 +71,7 @@ module Samagotchi
|
|
|
67
71
|
# A row that retried nothing still has its files that stay behind.
|
|
68
72
|
row.behind = bundle.behind_files if row.status == :skipped && !bundle.error?
|
|
69
73
|
row.earlier = bundle.merge_notes
|
|
74
|
+
row.manifest = ManifestStanding.keep(bundle, source_dir: row.source_dir, dry_run: true).line if same_version?(row)
|
|
70
75
|
row
|
|
71
76
|
end
|
|
72
77
|
end
|
|
@@ -89,32 +94,55 @@ module Samagotchi
|
|
|
89
94
|
|
|
90
95
|
# Runs the Installer for each :would_update row, Remerge for each
|
|
91
96
|
# :would_merge row; the others pass through. A failing bundle doesn't
|
|
92
|
-
# stop the rest.
|
|
97
|
+
# stop the rest. A bundle at the shipped version whose record an older
|
|
98
|
+
# chi wrote keeps the shipped manifest.yml (#keep_manifest).
|
|
93
99
|
# @return [Array<Row>]
|
|
94
100
|
def apply(rows)
|
|
95
|
-
rows.map
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
r.status = :failed
|
|
112
|
-
r.note = e.message.lines.first.to_s.strip
|
|
113
|
-
r.merged = installer&.merged_files || []
|
|
114
|
-
r.behind = installer&.behind || []
|
|
115
|
-
r.trash_copy = installer&.trash_copy_line
|
|
116
|
-
end
|
|
101
|
+
rows.map { |row| keep_manifest(apply_row(row)) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def apply_row(row)
|
|
105
|
+
return apply_remerge(row) if row.status == :would_merge
|
|
106
|
+
return row unless row.status == :would_update
|
|
107
|
+
return apply_profile(row) if row.adds
|
|
108
|
+
|
|
109
|
+
installer = Installer.new(source: row.source_dir, name: row.name, scope: row.scope, strict: true, upgrade: true,
|
|
110
|
+
shipped_dir: File.dirname(row.source_dir))
|
|
111
|
+
installer.run
|
|
112
|
+
row.dup.tap do |r|
|
|
113
|
+
r.status = :updated
|
|
114
|
+
r.merged = installer.merged_files
|
|
115
|
+
r.behind = installer.behind
|
|
116
|
+
r.trash_copy = installer.trash_copy_line
|
|
117
117
|
end
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
row.dup.tap do |r|
|
|
120
|
+
r.status = :failed
|
|
121
|
+
r.note = e.message.lines.first.to_s.strip
|
|
122
|
+
r.merged = installer&.merged_files || []
|
|
123
|
+
r.behind = installer&.behind || []
|
|
124
|
+
r.trash_copy = installer&.trash_copy_line
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The statuses of a row at the shipped version, in the plan and after
|
|
129
|
+
# apply: its record can keep the shipped dir's manifest.yml.
|
|
130
|
+
SAME_VERSION = %i[up_to_date would_merge merged].freeze
|
|
131
|
+
|
|
132
|
+
def same_version?(row) = !row.source_dir.nil? && SAME_VERSION.include?(row.status)
|
|
133
|
+
|
|
134
|
+
# ManifestStanding.keep from the shipped dir for a same-version row: a
|
|
135
|
+
# record an older chi wrote keeps the shipped manifest.yml, and the row
|
|
136
|
+
# says so (or why not). A bumped bundle gets the file from its Installer.
|
|
137
|
+
# @return [Row] the row
|
|
138
|
+
def keep_manifest(row)
|
|
139
|
+
return row unless same_version?(row)
|
|
140
|
+
|
|
141
|
+
bundle = Provenance.new(name: row.name).record
|
|
142
|
+
row.manifest = bundle && ManifestStanding.keep(bundle, source_dir: row.source_dir).line
|
|
143
|
+
row
|
|
144
|
+
rescue JSON::ParserError, SystemCallError
|
|
145
|
+
row
|
|
118
146
|
end
|
|
119
147
|
|
|
120
148
|
def apply_remerge(row)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "file_digest"
|
|
4
4
|
require_relative "provenance"
|
|
5
5
|
require_relative "manifest"
|
|
6
6
|
require_relative "../version"
|
|
@@ -77,7 +77,7 @@ module Samagotchi
|
|
|
77
77
|
|
|
78
78
|
# The file's digest, nil when there is none, :unreadable when it can't be read.
|
|
79
79
|
def self.checksum(path)
|
|
80
|
-
|
|
80
|
+
FileDigest.hex(path)
|
|
81
81
|
rescue SystemCallError
|
|
82
82
|
:unreadable
|
|
83
83
|
end
|
|
@@ -86,19 +86,19 @@ module Samagotchi
|
|
|
86
86
|
# Why none of the bundle's hooks load (this chi doesn't meet its
|
|
87
87
|
# requires_chi, Hooks::BundleLoader), or nil: it has no hooks or meets it.
|
|
88
88
|
def self.hooks_requires_failure(bundle)
|
|
89
|
-
|
|
89
|
+
manifest = bundle.manifest
|
|
90
|
+
return nil if manifest.hooks.empty?
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
manifest.requires_chi_failure
|
|
92
93
|
end
|
|
93
94
|
|
|
94
95
|
# The stored needs, each with found: from this process's PATH (the
|
|
95
|
-
# shell's; a worker's PATH can differ, see docs/memory.md).
|
|
96
|
+
# shell's; a worker's PATH can differ, see docs/memory.md). A needs:
|
|
97
|
+
# that doesn't parse is none.
|
|
96
98
|
def self.needs_status(bundle, path: ENV["PATH"])
|
|
97
|
-
|
|
99
|
+
bundle.manifest.needs.map do |need|
|
|
98
100
|
need.merge(found: BundleNeeds.found?(need[:command], path: path))
|
|
99
101
|
end
|
|
100
|
-
rescue Manifest::ValidationError
|
|
101
|
-
[]
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
# "needs gh (reads PRs) [ok]" / "needs gh [not found]: brew install gh"
|
|
@@ -120,8 +120,9 @@ module Samagotchi
|
|
|
120
120
|
elsif Provenance.sha_matches?(path, bundle.plugin.sha256) then "ok"
|
|
121
121
|
else "modified"
|
|
122
122
|
end
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
manifest = bundle.manifest
|
|
124
|
+
{ file: File.basename(path), path: path, state: state, requires_chi: manifest.requires_chi,
|
|
125
|
+
requires_failure: manifest.requires_chi_failure }
|
|
125
126
|
end
|
|
126
127
|
|
|
127
128
|
def self.unchecked_file(meta)
|
|
@@ -117,7 +117,9 @@ module Samagotchi
|
|
|
117
117
|
dry_run ? planned_update(gem_manifest, from, to) : upgrade_existing(from, to)
|
|
118
118
|
else
|
|
119
119
|
# Already at desired version — still verify files present (e.g. user deleted one)
|
|
120
|
-
# and retry the files marked conflict.
|
|
120
|
+
# and retry the files marked conflict. A record an older chi wrote keeps
|
|
121
|
+
# the shipped manifest.yml (once: then it has it).
|
|
122
|
+
keep_manifest(bundle) unless dry_run
|
|
121
123
|
verified = verify_files_present(gem_manifest, from, dry_run)
|
|
122
124
|
next remerge(bundle, verified, dry_run) if verified.status == :up_to_date
|
|
123
125
|
|
|
@@ -247,11 +249,20 @@ module Samagotchi
|
|
|
247
249
|
result(:updated, from: from, to: to, merged: forecast.merged, behind: forecast.behind)
|
|
248
250
|
end
|
|
249
251
|
|
|
252
|
+
# Provenance#adopt_manifest from the shipped dir, for a record without a
|
|
253
|
+
# usable kept manifest. A refusal (a checkout whose bundle changed at the
|
|
254
|
+
# same version) leaves the record reading its mirror, as before.
|
|
255
|
+
def keep_manifest(bundle)
|
|
256
|
+
return unless bundle.manifest_unkept?
|
|
257
|
+
|
|
258
|
+
Provenance.new(name: BUNDLE_NAME).adopt_manifest(GEM_BUNDLE_DIR)
|
|
259
|
+
end
|
|
260
|
+
|
|
250
261
|
def upgrade_installer
|
|
251
262
|
Installer.new(source: GEM_BUNDLE_DIR, name: BUNDLE_NAME, scope: SCOPE, force: false, strict: true, upgrade: true,
|
|
252
263
|
shipped_dir: File.dirname(GEM_BUNDLE_DIR))
|
|
253
264
|
end
|
|
254
|
-
private_class_method :with_lock, :note_newer_installed, :install_fresh, :upgrade_existing, :verify_files_present, :restore_file, :remerge,
|
|
265
|
+
private_class_method :with_lock, :note_newer_installed, :install_fresh, :keep_manifest, :upgrade_existing, :verify_files_present, :restore_file, :remerge,
|
|
255
266
|
:upgrade_installer, :result, :planned_update
|
|
256
267
|
end
|
|
257
268
|
end
|
|
@@ -113,7 +113,7 @@ module Samagotchi
|
|
|
113
113
|
|
|
114
114
|
# Why the installed file can't be loaded, or nil.
|
|
115
115
|
def unloadable_reason(file, bundle)
|
|
116
|
-
failure =
|
|
116
|
+
failure = bundle.manifest.requires_chi_failure
|
|
117
117
|
return failure if failure
|
|
118
118
|
return "the file is missing" unless file && File.file?(file)
|
|
119
119
|
|