samagotchi 0.157.0 → 0.159.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 +47 -1
- data/README.md +1 -1
- data/bin/chi +5 -1
- data/docs/architecture.md +14 -12
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +2 -1
- data/docs/cli.md +76 -17
- data/docs/configuration.md +16 -5
- data/docs/context.md +12 -4
- data/docs/guardrails.md +11 -8
- data/docs/hooks.md +47 -80
- data/docs/internals/bridge.md +2 -1
- data/docs/internals/bundles.md +51 -36
- data/docs/internals/engine.md +7 -5
- data/docs/memory.md +43 -9
- data/docs/plugins.md +13 -9
- data/docs/releasing.md +6 -5
- data/docs/scheduling.md +228 -0
- data/docs/sessions.md +30 -6
- data/docs/testing.md +4 -4
- data/lib/samagotchi/bridge/card_places.rb +5 -13
- data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
- data/lib/samagotchi/bridge/turn_drawings.rb +7 -1
- data/lib/samagotchi/bridge.rb +26 -2
- data/lib/samagotchi/bundle_command/check.rb +23 -10
- data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
- data/lib/samagotchi/bundle_command/try/run.rb +4 -1
- data/lib/samagotchi/bundle_command.rb +53 -44
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +6 -6
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/client_id.rb +13 -3
- data/lib/samagotchi/config.rb +12 -0
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_move.rb +74 -0
- data/lib/samagotchi/context_sources.rb +4 -2
- data/lib/samagotchi/engine.rb +1 -6
- data/lib/samagotchi/extension_load.rb +20 -41
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/pair_boundary.rb +1 -1
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/memory_bundle/asset_installer.rb +112 -211
- data/lib/samagotchi/memory_bundle/asset_state.rb +81 -0
- data/lib/samagotchi/memory_bundle/builder.rb +10 -34
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/file_actions.rb +51 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +11 -18
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +2 -16
- data/lib/samagotchi/memory_bundle/installer.rb +32 -14
- data/lib/samagotchi/memory_bundle/manifest.rb +37 -54
- data/lib/samagotchi/memory_bundle/provenance.rb +11 -51
- data/lib/samagotchi/memory_bundle/remerge.rb +7 -3
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +41 -41
- data/lib/samagotchi/memory_bundle/status.rb +30 -9
- data/lib/samagotchi/memory_bundle/system_bundle.rb +16 -5
- data/lib/samagotchi/memory_bundle/trash.rb +7 -12
- data/lib/samagotchi/memory_bundle/uninstaller.rb +10 -4
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/plugin/api.rb +1 -4
- data/lib/samagotchi/plugin/loader.rb +42 -5
- data/lib/samagotchi/project_command.rb +167 -0
- data/lib/samagotchi/project_init.rb +69 -0
- data/lib/samagotchi/project_scope.rb +6 -4
- data/lib/samagotchi/prompt_files.rb +10 -7
- data/lib/samagotchi/recap_place.rb +3 -4
- data/lib/samagotchi/run_options.rb +2 -0
- data/lib/samagotchi/schedule_command.rb +252 -0
- data/lib/samagotchi/schedules.rb +948 -0
- data/lib/samagotchi/session.rb +44 -18
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +20 -0
- data/lib/samagotchi/session_field.rb +5 -4
- data/lib/samagotchi/session_manager/spawn.rb +25 -11
- data/lib/samagotchi/session_manager/worker_command.rb +13 -13
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/formatting.rb +2 -0
- data/lib/samagotchi/terminal_ui/join_replay.rb +6 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -5
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/tool_declarations.rb +44 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/builtin_calls.rb +2 -0
- data/lib/samagotchi/tools/builtins.rb +17 -0
- data/lib/samagotchi/tools/cancel_schedule.rb +43 -0
- data/lib/samagotchi/tools/delegate.rb +12 -8
- data/lib/samagotchi/tools/list_schedules.rb +43 -0
- data/lib/samagotchi/tools/schedule_task.rb +39 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +35 -0
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/chains.js +32 -8
- data/lib/samagotchi/web/public/index.html +10 -2
- data/lib/samagotchi/web/public/info_bar.js +4 -3
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- data/lib/samagotchi/web/public/turn_events.js +4 -0
- data/lib/samagotchi/web/session_hub.rb +20 -0
- data/lib/samagotchi/worker.rb +24 -6
- data/lib/samagotchi/worker_boot.rb +3 -4
- data/lib/samagotchi/worker_inbound.rb +23 -0
- data/lib/samagotchi/worker_link.rb +5 -0
- data/lib/samagotchi/worker_wakes.rb +9 -1
- metadata +10 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
|
@@ -5,7 +5,6 @@ require "digest"
|
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require_relative "../context_providers"
|
|
7
7
|
require_relative "../version"
|
|
8
|
-
require_relative "bundle_hook"
|
|
9
8
|
|
|
10
9
|
module Samagotchi
|
|
11
10
|
autoload :BlockRenderers, File.expand_path("../block_renderers", __dir__)
|
|
@@ -66,6 +65,26 @@ module Samagotchi
|
|
|
66
65
|
class Manifest
|
|
67
66
|
class ValidationError < StandardError; end
|
|
68
67
|
|
|
68
|
+
# Class-file bundle hooks (hooks/*.rb, the manifest's hooks:) are
|
|
69
|
+
# retired: chi installs and loads none. WORDS is the one wording of
|
|
70
|
+
# it, wherever chi meets one (the Engine's first-turn line, install,
|
|
71
|
+
# chi bundle check and status, chi update's row). A strict read of a
|
|
72
|
+
# manifest with a hooks: entry raises it (.for_key's message); a
|
|
73
|
+
# tolerant read (a kept manifest.yml) ignores the key.
|
|
74
|
+
class HooksRetired < ValidationError
|
|
75
|
+
WORDS = "class-file bundle hooks are retired: chi loads no hooks/*.rb; a bundle hooks events from its " \
|
|
76
|
+
"plugin.rb with chi.on, see docs/plugins.md"
|
|
77
|
+
|
|
78
|
+
# The manifest's hooks: key.
|
|
79
|
+
def self.for_key = new("hooks: #{WORDS}")
|
|
80
|
+
|
|
81
|
+
# A source's hooks/*.rb (+files+, relative), with or without the key.
|
|
82
|
+
def self.for_files(files) = new("#{files.join(", ")}: #{WORDS}")
|
|
83
|
+
|
|
84
|
+
# The source's hooks/*.rb, relative and sorted ([] for none).
|
|
85
|
+
def self.files_in(dir) = Dir.glob(File.join(dir, "hooks", "*.rb")).map { |f| "hooks/#{File.basename(f)}" }.sort
|
|
86
|
+
end
|
|
87
|
+
|
|
69
88
|
# A plugin file is a plain .rb name in the bundle's top directory.
|
|
70
89
|
PLUGIN_FILE = /\A[A-Za-z0-9_][A-Za-z0-9_.-]*\.rb\z/
|
|
71
90
|
# A script is a plain file name in the bundle's scripts/.
|
|
@@ -88,7 +107,7 @@ module Samagotchi
|
|
|
88
107
|
# admits a chi that never saw the key. A key the first tag (v0.2.0)
|
|
89
108
|
# already reads has no entry. `files.description` is a files: entry given
|
|
90
109
|
# as a mapping, `*.types` a session_settings:/settings: given as a mapping
|
|
91
|
-
# of keys to types
|
|
110
|
+
# of keys to types. A key
|
|
92
111
|
# whose release is not out yet waits here as a comment naming it, the way
|
|
93
112
|
# Plugin::Api::SINCE does, until `release:bump` gives it its version
|
|
94
113
|
# (docs/releasing.md).
|
|
@@ -100,16 +119,14 @@ module Samagotchi
|
|
|
100
119
|
"session_settings" => "0.84.0",
|
|
101
120
|
"session_settings.types" => "0.92.0",
|
|
102
121
|
"settings.types" => "0.92.0",
|
|
103
|
-
"hooks.events" => "0.98.0",
|
|
104
122
|
"settings" => "0.143.0",
|
|
105
123
|
"block_renderers" => "0.151.0",
|
|
106
124
|
"plugin.required" => "0.157.0"
|
|
107
125
|
}.freeze
|
|
108
126
|
|
|
109
127
|
# files: name → "sha256:<hex>" (both forms); file_descriptions: name →
|
|
110
|
-
# the description a mapping gave, for the files that have one
|
|
111
|
-
|
|
112
|
-
attr_reader :name, :version, :scope, :description, :files, :file_descriptions, :hooks, :trust_level, :plugin, :requires_chi, :needs, :includes,
|
|
128
|
+
# the description a mapping gave, for the files that have one.
|
|
129
|
+
attr_reader :name, :version, :scope, :description, :files, :file_descriptions, :trust_level, :plugin, :requires_chi, :needs, :includes,
|
|
113
130
|
:scripts, :context_providers, :block_renderers, :session_settings, :session_setting_types, :settings, :setting_types, :unknown_keys
|
|
114
131
|
|
|
115
132
|
def initialize(path:)
|
|
@@ -131,7 +148,8 @@ module Samagotchi
|
|
|
131
148
|
@scope = normalize_scope(raw["scope"])
|
|
132
149
|
@description = (raw["description"] || "").to_s
|
|
133
150
|
@files = parse_files(raw["files"] || {})
|
|
134
|
-
|
|
151
|
+
raise HooksRetired.for_key if self.class.lists_hooks?(raw["hooks"])
|
|
152
|
+
|
|
135
153
|
@file_descriptions = parse_file_descriptions(raw["files"])
|
|
136
154
|
@trust_level = (raw["trust_level"] || "experimental").to_s
|
|
137
155
|
@plugin = parse_plugin(raw["plugin"])
|
|
@@ -170,8 +188,8 @@ module Samagotchi
|
|
|
170
188
|
# chi (a newer chi tightened it, or an older one wrote it) costs only
|
|
171
189
|
# that key, which reads as empty with its message in the problems:
|
|
172
190
|
# name, version (never checked: identity is the record's), scope;
|
|
173
|
-
# files and their descriptions, scripts, plugin
|
|
174
|
-
#
|
|
191
|
+
# files and their descriptions, scripts, plugin (hooks: is ignored,
|
|
192
|
+
# HooksRetired); needs, context_providers; block_renderers
|
|
175
193
|
# (against +fallback_scripts+ when scripts: failed); includes (after
|
|
176
194
|
# what its check reads); session_settings with its types, settings
|
|
177
195
|
# with theirs (one raw value each, so one problem); requires_chi,
|
|
@@ -186,6 +204,10 @@ module Samagotchi
|
|
|
186
204
|
[manifest, manifest.send(:read_tolerantly, path, fallback_scripts)]
|
|
187
205
|
end
|
|
188
206
|
|
|
207
|
+
# Whether a manifest's hooks: value lists anything (none, {} and []
|
|
208
|
+
# don't: a manifest may keep an empty key).
|
|
209
|
+
def self.lists_hooks?(raw) = !(raw.nil? || ((raw.is_a?(Hash) || raw.is_a?(Array)) && raw.empty?))
|
|
210
|
+
|
|
189
211
|
def self.read(dir:)
|
|
190
212
|
manifest_path = File.join(dir, "manifest.yml")
|
|
191
213
|
raise ValidationError, "manifest.yml not found in #{dir}" unless File.exist?(manifest_path)
|
|
@@ -204,13 +226,6 @@ module Samagotchi
|
|
|
204
226
|
hex unless hex.nil? || hex.empty?
|
|
205
227
|
end
|
|
206
228
|
|
|
207
|
-
# Returns sha256 hex for a hook basename (e.g. "guardrails.rb"), nil
|
|
208
|
-
# when it declares none (nothing to verify).
|
|
209
|
-
def checksum_for_hook(basename)
|
|
210
|
-
hex = @hooks[basename]&.hex
|
|
211
|
-
hex unless hex.nil? || hex.empty?
|
|
212
|
-
end
|
|
213
|
-
|
|
214
229
|
# @return [String, nil] the plugin's sha256 hex, "sha256:" stripped
|
|
215
230
|
def checksum_for_plugin
|
|
216
231
|
sha = @plugin && @plugin[:sha256].to_s
|
|
@@ -242,8 +257,8 @@ module Samagotchi
|
|
|
242
257
|
|
|
243
258
|
# The SINCE keys this manifest actually uses, each with the chi release
|
|
244
259
|
# that first read it: the plain keys, and the shapes only a newer chi
|
|
245
|
-
# reads — a files: entry given as a mapping, a
|
|
246
|
-
#
|
|
260
|
+
# reads — a files: entry given as a mapping, a session_settings:/settings:
|
|
261
|
+
# given as a mapping of keys to types.
|
|
247
262
|
# @return [Hash{String => String}]
|
|
248
263
|
def floor_keys
|
|
249
264
|
raw = @raw
|
|
@@ -258,15 +273,12 @@ module Samagotchi
|
|
|
258
273
|
found["files.description"] = SINCE["files.description"] if raw["files"].is_a?(Hash) && raw["files"].values.any?(Hash)
|
|
259
274
|
found["session_settings.types"] = SINCE["session_settings.types"] if raw["session_settings"].is_a?(Hash)
|
|
260
275
|
found["settings.types"] = SINCE["settings.types"] if raw["settings"].is_a?(Hash)
|
|
261
|
-
if raw["hooks"].is_a?(Hash) && raw["hooks"].values.any? { |entry| entry.is_a?(Hash) && entry.key?("events") }
|
|
262
|
-
found["hooks.events"] = SINCE["hooks.events"]
|
|
263
|
-
end
|
|
264
276
|
found
|
|
265
277
|
end
|
|
266
278
|
|
|
267
279
|
# Writes a fresh manifest.yml from computed checksums at `dir`.
|
|
268
280
|
# +file_descriptions+ (name → text) writes those files as mappings.
|
|
269
|
-
def self.write(dir:, name:, version:, files:, scope: nil, description: "",
|
|
281
|
+
def self.write(dir:, name:, version:, files:, scope: nil, description: "", trust_level: nil,
|
|
270
282
|
plugin: nil, requires_chi: nil, needs: nil, scripts: {}, context_providers: [], block_renderers: [], file_descriptions: {},
|
|
271
283
|
session_settings: [], session_setting_types: {}, settings: nil, setting_types: {})
|
|
272
284
|
FileUtils.mkdir_p(dir)
|
|
@@ -280,7 +292,6 @@ module Samagotchi
|
|
|
280
292
|
text = file_descriptions[key].to_s.strip
|
|
281
293
|
[key, text.empty? ? sha : { "sha256" => sha, "description" => text }]
|
|
282
294
|
end
|
|
283
|
-
manifest["hooks"] = hooks.to_h { |k, v| [k.to_s, BundleHook.parse(v).to_record] } if hooks && !hooks.empty?
|
|
284
295
|
manifest["trust_level"] = trust_level.to_s if trust_level && !trust_level.to_s.empty?
|
|
285
296
|
if plugin
|
|
286
297
|
sha = plugin[:sha256].to_s
|
|
@@ -389,7 +400,7 @@ module Samagotchi
|
|
|
389
400
|
end
|
|
390
401
|
@scripts = tolerate(problems, "scripts", {}) { parse_scripts(raw["scripts"]) }
|
|
391
402
|
@plugin = tolerate(problems, "plugin", nil) { parse_plugin(raw["plugin"]) }
|
|
392
|
-
|
|
403
|
+
# An old kept manifest's hooks: is no problem: it isn't read (HooksRetired).
|
|
393
404
|
@needs = tolerate(problems, "needs", []) { self.class.parse_needs(raw["needs"]) }
|
|
394
405
|
@context_providers = tolerate(problems, "context_providers", []) { parse_context_providers(raw["context_providers"]) }
|
|
395
406
|
script_names = problems.key?("scripts") ? fallback_scripts.map(&:to_s) : @scripts.keys
|
|
@@ -546,8 +557,8 @@ module Samagotchi
|
|
|
546
557
|
|
|
547
558
|
name
|
|
548
559
|
end.uniq
|
|
549
|
-
if !names.empty? && (!@files.empty? ||
|
|
550
|
-
raise ValidationError, "a bundle with includes: holds only its includes (no files,
|
|
560
|
+
if !names.empty? && (!@files.empty? || @plugin || !@scripts.empty?)
|
|
561
|
+
raise ValidationError, "a bundle with includes: holds only its includes (no files, plugin or scripts)"
|
|
551
562
|
end
|
|
552
563
|
|
|
553
564
|
names
|
|
@@ -559,34 +570,6 @@ module Samagotchi
|
|
|
559
570
|
text = raw.to_s.strip
|
|
560
571
|
text.empty? ? nil : text
|
|
561
572
|
end
|
|
562
|
-
|
|
563
|
-
def parse_hooks(raw)
|
|
564
|
-
return {} unless raw.is_a?(Hash)
|
|
565
|
-
|
|
566
|
-
raw.each_with_object({}) do |(k, v), acc|
|
|
567
|
-
next unless k.is_a?(String) && !k.empty?
|
|
568
|
-
next unless v.is_a?(Hash)
|
|
569
|
-
|
|
570
|
-
check_hook_events(k, v)
|
|
571
|
-
acc[k] = BundleHook.parse(v)
|
|
572
|
-
end
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
HOOK_EVENT_NAME = /\A[a-z][a-z0-9_]*\z/
|
|
576
|
-
|
|
577
|
-
# A hook's events: list holds event names (one instance registered
|
|
578
|
-
# for each); it and event: are one or the other.
|
|
579
|
-
def check_hook_events(file, entry)
|
|
580
|
-
return unless entry.key?("events")
|
|
581
|
-
|
|
582
|
-
raw = entry["events"]
|
|
583
|
-
raise ValidationError, "hooks: #{file}: give event: or events:, not both" if entry.key?("event")
|
|
584
|
-
raise ValidationError, "hooks: #{file}: events: must be a list of event names" unless raw.is_a?(Array) && !raw.empty?
|
|
585
|
-
|
|
586
|
-
raw.each do |item|
|
|
587
|
-
raise ValidationError, "hooks: #{file}: events: #{item.inspect} is not an event name" unless item.is_a?(String) && item.strip.match?(HOOK_EVENT_NAME)
|
|
588
|
-
end
|
|
589
|
-
end
|
|
590
573
|
end
|
|
591
574
|
end
|
|
592
575
|
end
|
|
@@ -8,7 +8,6 @@ require "tmpdir"
|
|
|
8
8
|
require_relative "../atomic_file"
|
|
9
9
|
require_relative "../memory_paths"
|
|
10
10
|
require_relative "../version"
|
|
11
|
-
require_relative "bundle_hook"
|
|
12
11
|
require_relative "file_digest"
|
|
13
12
|
require_relative "installed_bundle"
|
|
14
13
|
require_relative "manifest"
|
|
@@ -195,7 +194,6 @@ module Samagotchi
|
|
|
195
194
|
# the record. nil keeps an entry with no base (a kept edit that has
|
|
196
195
|
# none: the user's file must never become it), with its recorded
|
|
197
196
|
# checksum.
|
|
198
|
-
# @param hooks_files [Hash] basename => path for hook file snapshots (optional)
|
|
199
197
|
# @param guardrails_files [Hash] basename => installed rule file; the
|
|
200
198
|
# sha256 of each is recorded (the Engine checks it at load)
|
|
201
199
|
# @param plugin_file [String, nil] the installed plugin file; its name
|
|
@@ -229,7 +227,9 @@ module Samagotchi
|
|
|
229
227
|
# (requires_chi … setting_types) are still written from the kwargs:
|
|
230
228
|
# an older chi sharing this memories dir reads only them
|
|
231
229
|
# (docs/internals/bundles.md); a key a later manifest gains is never added
|
|
232
|
-
|
|
230
|
+
# The record has no hooks ("hooks" => {}: class-file hooks are retired)
|
|
231
|
+
# and an older record's hook bases go.
|
|
232
|
+
def write(files:, scope:, version:, source_path:, trust_level: nil, source_commit: nil,
|
|
233
233
|
guardrails_files: {}, plugin_file: nil, plugin_required: false, requires_chi: nil, needs: nil, marks: [], incoming_dir: nil, includes: nil,
|
|
234
234
|
scripts_files: {}, context_providers: [], block_renderers: [], file_descriptions: {}, session_settings: [], session_setting_types: {}, settings: nil, setting_types: {},
|
|
235
235
|
dev: false, manifest_from: nil)
|
|
@@ -275,40 +275,8 @@ module Samagotchi
|
|
|
275
275
|
end
|
|
276
276
|
end
|
|
277
277
|
|
|
278
|
-
#
|
|
279
|
-
|
|
280
|
-
# Incoming hooks: basename => BundleHook (or its metadata hash)
|
|
281
|
-
normalized_hooks = {}
|
|
282
|
-
if hooks.is_a?(Hash)
|
|
283
|
-
hooks.each do |k, v|
|
|
284
|
-
next unless k.is_a?(String) && !k.empty?
|
|
285
|
-
next unless v.is_a?(Hash) || v.is_a?(BundleHook)
|
|
286
|
-
|
|
287
|
-
normalized_hooks[k] = BundleHook.parse(v)
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
# Prune stale hook bases
|
|
291
|
-
existing&.hooks&.each_key do |old_key_str|
|
|
292
|
-
FileUtils.rm_f(File.join(bases_dir, old_key_str)) unless normalized_hooks.key?(old_key_str)
|
|
293
|
-
end
|
|
294
|
-
# Save base snapshots for hooks where a file path was provided, and
|
|
295
|
-
# record the sha256 of the installed file itself: the load-time check
|
|
296
|
-
# compares against it, and a declared sha can be wrong (installing
|
|
297
|
-
# only warns about that).
|
|
298
|
-
hooks_files = {} unless hooks_files.is_a?(Hash)
|
|
299
|
-
normalized_hooks.each do |k, hook|
|
|
300
|
-
src = hooks_files[k] || hooks_files[k.to_sym]
|
|
301
|
-
hook = hook.with(sha256: BundleHook.of_file(src.to_s).sha256) if src && File.exist?(src.to_s)
|
|
302
|
-
hook = hook.with(sha256: DEV_SHA) if dev
|
|
303
|
-
hooks_map[k] = hook.to_record
|
|
304
|
-
next unless src && File.exist?(src.to_s)
|
|
305
|
-
|
|
306
|
-
begin
|
|
307
|
-
File.write(File.join(bases_dir, k), File.read(src.to_s))
|
|
308
|
-
rescue StandardError
|
|
309
|
-
nil
|
|
310
|
-
end
|
|
311
|
-
end
|
|
278
|
+
# An older chi's hooks: their bases go with them (AssetInstaller#retire_hooks).
|
|
279
|
+
existing&.hooks&.each_key { |old_key_str| FileUtils.rm_f(File.join(bases_dir, old_key_str)) }
|
|
312
280
|
|
|
313
281
|
manifest_data = {
|
|
314
282
|
"name" => @name,
|
|
@@ -317,7 +285,7 @@ module Samagotchi
|
|
|
317
285
|
"source" => source_path.to_s,
|
|
318
286
|
"installed_at" => DateTime.now.iso8601,
|
|
319
287
|
"files" => merged_entries,
|
|
320
|
-
"hooks" =>
|
|
288
|
+
"hooks" => {}
|
|
321
289
|
}
|
|
322
290
|
manifest_data["dev"] = true if dev
|
|
323
291
|
if guardrails_files.is_a?(Hash) && !guardrails_files.empty?
|
|
@@ -460,7 +428,7 @@ module Samagotchi
|
|
|
460
428
|
# wrote it, or the file is gone), when +dir+ is the source of what's
|
|
461
429
|
# installed: its manifest names this bundle at the recorded version and
|
|
462
430
|
# says what the record keeps (#manifest_difference), and every sha the
|
|
463
|
-
# record has for a memory, plugin,
|
|
431
|
+
# record has for a memory, plugin, script or rule is the source
|
|
464
432
|
# file's. Then the file is copied to #kept_manifest_path and the
|
|
465
433
|
# record gains its manifest_sha, and nothing else changes (installed_at,
|
|
466
434
|
# the mirror, installed_by). A dev record is never adopted: its next
|
|
@@ -585,8 +553,8 @@ module Samagotchi
|
|
|
585
553
|
# The first manifest key in which +manifest+ says other than what the
|
|
586
554
|
# record keeps, nil when none: each mirrored key as #write records it (one
|
|
587
555
|
# the record lacks is fine only when an older chi couldn't have known it:
|
|
588
|
-
# Manifest::SINCE has it)
|
|
589
|
-
#
|
|
556
|
+
# Manifest::SINCE has it). An older record's hooks don't count: they
|
|
557
|
+
# no longer load, and the source can't list any (Manifest::HooksRetired).
|
|
590
558
|
def manifest_difference(bundle, manifest)
|
|
591
559
|
recorded = plain(bundle.mirror)
|
|
592
560
|
source = plain(self.class.mirror_fields(
|
|
@@ -595,18 +563,11 @@ module Samagotchi
|
|
|
595
563
|
session_settings: manifest.session_settings, session_setting_types: manifest.session_setting_types,
|
|
596
564
|
settings: manifest.settings, setting_types: manifest.setting_types
|
|
597
565
|
))
|
|
598
|
-
|
|
566
|
+
InstalledBundle::MIRROR_KEYS.find do |key|
|
|
599
567
|
next false if recorded[key] == source[key]
|
|
600
568
|
|
|
601
569
|
!(recorded[key].nil? && Manifest::SINCE.key?(MIRROR_SINCE.fetch(key, key)))
|
|
602
570
|
end
|
|
603
|
-
return differing if differing
|
|
604
|
-
|
|
605
|
-
same_hooks = bundle.hooks.all? do |file, hook|
|
|
606
|
-
theirs = manifest.hooks.fetch(file, BundleHook.new)
|
|
607
|
-
[hook.events, hook.on_error, hook.priority] == [theirs.events, theirs.on_error, theirs.priority]
|
|
608
|
-
end
|
|
609
|
-
same_hooks ? nil : "hooks"
|
|
610
571
|
end
|
|
611
572
|
private :manifest_difference
|
|
612
573
|
|
|
@@ -614,7 +575,7 @@ module Samagotchi
|
|
|
614
575
|
private :plain
|
|
615
576
|
|
|
616
577
|
# Each file the record has a sha for, by its path in the bundle's source
|
|
617
|
-
# => that sha: the plugin,
|
|
578
|
+
# => that sha: the plugin, scripts, rules, and each memory whose
|
|
618
579
|
# entry has a checksum and no conflict mark (its base is the bundle's text).
|
|
619
580
|
def installed_shas(bundle, manifest)
|
|
620
581
|
shas = bundle.files.each_with_object({}) do |(key, entry), acc|
|
|
@@ -624,7 +585,6 @@ module Samagotchi
|
|
|
624
585
|
file = manifest.plugin && manifest.plugin[:file].to_s
|
|
625
586
|
shas[file && File.basename(file) == bundle.plugin.file ? file : bundle.plugin.file] = bundle.plugin.sha256
|
|
626
587
|
end
|
|
627
|
-
bundle.hooks.each { |file, hook| shas["hooks/#{file}"] = hook.sha256 }
|
|
628
588
|
bundle.scripts.each { |file, sha| shas["scripts/#{file}"] = sha }
|
|
629
589
|
bundle.guardrails.each { |file, sha| shas["guardrails/#{file}"] = sha }
|
|
630
590
|
shas
|
|
@@ -4,6 +4,7 @@ require_relative "index_updater"
|
|
|
4
4
|
require_relative "manifest"
|
|
5
5
|
require_relative "memory_files"
|
|
6
6
|
require_relative "file_actions"
|
|
7
|
+
require_relative "file_digest"
|
|
7
8
|
require_relative "merger"
|
|
8
9
|
require_relative "provenance"
|
|
9
10
|
require_relative "trash"
|
|
@@ -77,7 +78,8 @@ module Samagotchi
|
|
|
77
78
|
@behind = []
|
|
78
79
|
@warnings = []
|
|
79
80
|
@trash = Trash.new(@name)
|
|
80
|
-
|
|
81
|
+
@manifest = Manifest.read(dir: @source_dir)
|
|
82
|
+
shipped = MemoryFiles.select(@source_dir, @manifest)
|
|
81
83
|
marked.each { |key, entry| retry_file(key, entry, shipped) }
|
|
82
84
|
settle unless dry_run
|
|
83
85
|
Result.new(merged: @merged, settled: @settled, stamped: @stamped.map(&:file), behind: @behind, warnings: @warnings, trash_copy: @trash.copy_line)
|
|
@@ -111,7 +113,7 @@ module Samagotchi
|
|
|
111
113
|
def retry_row(key, entry, state, action)
|
|
112
114
|
decision = view.decision(key, state) if action.do == :decide || action.behind?
|
|
113
115
|
action = FileActions.decided(decision.outcome, :remerge) if action.do == :decide
|
|
114
|
-
if action.do == :merge then merge_file(key, entry, decision.outcome)
|
|
116
|
+
if action.do == :merge then merge_file(key, entry, state, decision.outcome)
|
|
115
117
|
elsif action.behind?
|
|
116
118
|
waits = decision.outcome.status == :fast_forward
|
|
117
119
|
mark(key, entry, waits ? behind_for(key, FileStanding::WAITS_FOR_UPGRADE) : view.behind(key, decision.outcome, @record.version))
|
|
@@ -141,7 +143,9 @@ module Samagotchi
|
|
|
141
143
|
@stamped << behind unless entry[:incoming_sha] && @record.behind_entry(key, entry) == behind && File.exist?(@provenance.incoming_path(key))
|
|
142
144
|
end
|
|
143
145
|
|
|
144
|
-
def merge_file(key, entry, outcome)
|
|
146
|
+
def merge_file(key, entry, state, outcome)
|
|
147
|
+
warning = FileDigest.mismatch(key, state.paths[:incoming], @manifest.checksum_for(key))
|
|
148
|
+
@warnings << warning if warning
|
|
145
149
|
return @merged << key if @dry_run
|
|
146
150
|
return @merged << key if Merger.write_merged(view.paths(key)[:current], outcome, @trash)
|
|
147
151
|
|
|
@@ -14,7 +14,7 @@ module Samagotchi
|
|
|
14
14
|
|
|
15
15
|
# A sha line that doesn't match its file: +old+ is the hex the line
|
|
16
16
|
# holds ("" for a placeholder `sha256: sha256:`), nil when the entry
|
|
17
|
-
# has no sha line at all (a plugin
|
|
17
|
+
# has no sha line at all (a plugin: or scripts: entry the
|
|
18
18
|
# installer then takes unchecked; .refresh can't write one).
|
|
19
19
|
Stale = Data.define(:file, :old, :new)
|
|
20
20
|
|
|
@@ -30,7 +30,6 @@ module Samagotchi
|
|
|
30
30
|
# names, the layout otherwise untouched. The shapes:
|
|
31
31
|
# files: a.md: sha256:<hex> (dir/a.md)
|
|
32
32
|
# a.md:\n sha256: sha256:<hex> (dir/a.md, with description:)
|
|
33
|
-
# hooks: h.rb:\n sha256: sha256:<hex> (dir/hooks/h.rb)
|
|
34
33
|
# plugin: file: p.rb\n sha256: sha256:<hex> (dir/p.rb)
|
|
35
34
|
# scripts: s.rb: sha256:<hex> (dir/scripts/s.rb)
|
|
36
35
|
# Returns [text, [[relative file, old sha, new sha], ...]] for the lines
|
|
@@ -43,8 +42,9 @@ module Samagotchi
|
|
|
43
42
|
[text, changes]
|
|
44
43
|
end
|
|
45
44
|
|
|
46
|
-
# The stale sha lines of the bundle at +dir+, then the plugin
|
|
47
|
-
#
|
|
45
|
+
# The stale sha lines of the bundle at +dir+, then the plugin: and
|
|
46
|
+
# scripts: entries with no sha line, as [Stale]. (A hooks: key, which
|
|
47
|
+
# chi refuses, has no sha lines here.)
|
|
48
48
|
def stale(dir, text = File.read(File.join(dir, "manifest.yml")))
|
|
49
49
|
_, changes, unlined = scan(dir, text)
|
|
50
50
|
changes.map { |file, old, new| Stale.new(file, old, new) } +
|
|
@@ -86,11 +86,10 @@ module Samagotchi
|
|
|
86
86
|
# take a placeholder (`chi bundle check`) parse this text.
|
|
87
87
|
def parseable(text) = text.gsub(/^(\s*(?:["']?[^"':\s][^"':]*["']?: )?)sha256:(?=[ \t]*(?:#[^\r\n]*)?\r?$)/, "\\1sha256:0")
|
|
88
88
|
|
|
89
|
-
# The code files the manifest names (plugin,
|
|
89
|
+
# The code files the manifest names (plugin, scripts), as paths
|
|
90
90
|
# relative to the bundle dir.
|
|
91
91
|
def coded(parsed, plugin_file)
|
|
92
92
|
(plugin_file.is_a?(String) ? [plugin_file] : []) +
|
|
93
|
-
keys(parsed["hooks"]).map { |h| File.join("hooks", h) } +
|
|
94
93
|
keys(parsed["scripts"]).map { |s| File.join("scripts", s) }
|
|
95
94
|
end
|
|
96
95
|
private_class_method :coded
|
|
@@ -114,8 +113,6 @@ module Samagotchi
|
|
|
114
113
|
when "files"
|
|
115
114
|
m = line.match(/\A( sha256: )sha256:\h*#{TAIL}/) and return sub && [sub.delete("\"'"), m[1]]
|
|
116
115
|
m = line.match(/\A( ["']?([^"':]+)["']?: )sha256:\h*#{TAIL}/) and [m[2], m[1]]
|
|
117
|
-
when "hooks"
|
|
118
|
-
m = line.match(/\A( sha256: )sha256:\h*#{TAIL}/) and sub and [File.join("hooks", sub.delete("\"'")), m[1]]
|
|
119
116
|
when "plugin"
|
|
120
117
|
m = line.match(/\A( sha256: )sha256:\h*#{TAIL}/) and plugin_file and [plugin_file, m[1]]
|
|
121
118
|
when "scripts"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "asset_installer"
|
|
4
|
+
require_relative "asset_state"
|
|
4
5
|
require_relative "installer"
|
|
5
6
|
require_relative "listing"
|
|
6
7
|
require_relative "manifest"
|
|
@@ -27,11 +28,12 @@ module Samagotchi
|
|
|
27
28
|
# bundle that isn't installed and never downgrades.
|
|
28
29
|
#
|
|
29
30
|
# .md files take the Installer's 3-way merge: a clean text merge replaces
|
|
30
|
-
# the file (merged), a real conflict keeps the user's (behind).
|
|
31
|
-
# the plugin and scripts are replaced
|
|
32
|
-
#
|
|
33
|
-
# the
|
|
34
|
-
# replaced
|
|
31
|
+
# the file (merged), a real conflict keeps the user's (behind). Rules,
|
|
32
|
+
# the plugin and scripts are replaced, an older chi's hooks removed; an
|
|
33
|
+
# edited one (AssetState#edited?, Trash.edit?'s rule: its bytes neither
|
|
34
|
+
# the recorded sha, none recorded counting as edited, nor the incoming
|
|
35
|
+
# file's) is copied to the trash first and listed in replaced
|
|
36
|
+
# (#trash_bound).
|
|
35
37
|
#
|
|
36
38
|
# A profile (a shipped meta bundle) is updated through Profile: its row
|
|
37
39
|
# is :would_update when the shipped version is newer or it has members
|
|
@@ -119,10 +121,7 @@ module Samagotchi
|
|
|
119
121
|
r.merged = installer.merged_files
|
|
120
122
|
r.behind = installer.behind
|
|
121
123
|
r.trash_copy = installer.trash_copy_line
|
|
122
|
-
|
|
123
|
-
# what the row's merged and behind already do.
|
|
124
|
-
mismatches = installer.warnings.grep(/\AChecksum mismatch/)
|
|
125
|
-
r.note = mismatches.join("; ") unless mismatches.empty?
|
|
124
|
+
r.note = mismatch_note(installer)
|
|
126
125
|
end
|
|
127
126
|
rescue StandardError => e
|
|
128
127
|
row.dup.tap do |r|
|
|
@@ -202,6 +201,10 @@ module Samagotchi
|
|
|
202
201
|
if Manifest.requires_chi_failure(manifest.requires_chi, chi_version)
|
|
203
202
|
return skip(row, "needs chi #{manifest.requires_chi}", keep_to: true)
|
|
204
203
|
end
|
|
204
|
+
|
|
205
|
+
# The upgrade would refuse it (Installer#refuse_hooks).
|
|
206
|
+
hooks = Manifest::HooksRetired.files_in(row.source_dir)
|
|
207
|
+
return skip(row, Manifest::HooksRetired.for_files(hooks).message, keep_to: true) unless hooks.empty?
|
|
205
208
|
return skip(row, "project scope: chi bundle upgrade #{ship.source} in the project") if row.scope == "project"
|
|
206
209
|
return skip(row, "unknown scope #{row.scope}: upgrade chi or reinstall") unless MemoryPaths.scope_dir(row.scope)
|
|
207
210
|
|
|
@@ -210,6 +213,7 @@ module Samagotchi
|
|
|
210
213
|
row.behind = forecast.behind
|
|
211
214
|
row.replaced = trash_bound(name, bundle, manifest, row.source_dir)
|
|
212
215
|
row.status = :would_update
|
|
216
|
+
row.note = planned_mismatch_note(row)
|
|
213
217
|
row
|
|
214
218
|
rescue SystemCallError => e
|
|
215
219
|
# A file it can't read: the upgrade would fail on it, so this is that
|
|
@@ -242,6 +246,20 @@ module Samagotchi
|
|
|
242
246
|
|
|
243
247
|
def note_of(remerged) = remerged.warnings.empty? ? nil : remerged.warnings.join("; ")
|
|
244
248
|
|
|
249
|
+
def mismatch_note(installer)
|
|
250
|
+
mismatches = installer.warnings.grep(/\AChecksum mismatch/)
|
|
251
|
+
mismatches.join("; ") unless mismatches.empty?
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def planned_mismatch_note(row)
|
|
255
|
+
installer = Installer.new(source: row.source_dir, name: row.name, scope: row.scope, strict: true, upgrade: true, dry_run: true,
|
|
256
|
+
shipped_dir: File.dirname(row.source_dir))
|
|
257
|
+
installer.run
|
|
258
|
+
mismatch_note(installer)
|
|
259
|
+
rescue Installer::InstallError
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
|
|
245
263
|
def profile_row(row, bundle, shipped_dir, chi_version)
|
|
246
264
|
manifest = Manifest.read(dir: row.source_dir)
|
|
247
265
|
newer = Listing.newer?(row.to, row.from)
|
|
@@ -277,49 +295,31 @@ module Samagotchi
|
|
|
277
295
|
end
|
|
278
296
|
|
|
279
297
|
# What the upgrade from +source_dir+ copies to the trash, as
|
|
280
|
-
# hooks/F, guardrails/F, plugin/F, scripts/F
|
|
281
|
-
#
|
|
282
|
-
# its place
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
298
|
+
# hooks/F, guardrails/F, plugin/F, scripts/F. Rules, the plugin and
|
|
299
|
+
# scripts: each file of the folders AssetInstaller replaces, against the
|
|
300
|
+
# one it puts in its place, that is AssetState#edited? (the upgrade's
|
|
301
|
+
# save_first: the same files, the same rule). hooks/: an older chi's,
|
|
302
|
+
# which the upgrade removes whole (AssetInstaller#retire_hooks; no
|
|
303
|
+
# incoming): Trash.edits over it.
|
|
286
304
|
def trash_bound(name, bundle, manifest, source_dir)
|
|
287
305
|
provenance = Provenance.new(name: name)
|
|
288
306
|
rules = Dir.glob(File.join(source_dir, "guardrails", "*.yml")).sort
|
|
289
307
|
.reject { |file| AssetInstaller.linked?(file, source_dir) }
|
|
290
308
|
plugin = manifest.plugin ? { manifest.plugin[:file] => File.join(source_dir, manifest.plugin[:file]) } : {}
|
|
291
309
|
scripts = manifest.scripts.keys.to_h { |file| [file, File.join(source_dir, "scripts", file)] }
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
# #trash_bound's hooks: AssetInstaller#install_hooks' set (incoming: the
|
|
299
|
-
# source's file), then #prune_dropped_hooks' (incoming: none). A stray
|
|
300
|
-
# file in hooks/ is in neither: the upgrade leaves it.
|
|
301
|
-
def trash_bound_hooks(provenance, bundle, manifest, source_dir)
|
|
302
|
-
src = ->(file) { File.join(source_dir, "hooks", file) }
|
|
303
|
-
listed = manifest.hooks.keys.select { |file| file.is_a?(String) && !file.empty? }
|
|
304
|
-
listed = Dir.glob(src.call("*.rb")).map { |file| File.basename(file) } if manifest.hooks.empty?
|
|
305
|
-
shipped = listed.reject do |file|
|
|
306
|
-
(File.symlink?(src.call(file)) || File.exist?(src.call(file))) && AssetInstaller.linked?(src.call(file), source_dir)
|
|
307
|
-
end
|
|
308
|
-
incoming = shipped.select { |file| File.exist?(src.call(file)) }.to_h { |file| [file, src.call(file)] }
|
|
309
|
-
incoming.merge!((bundle.hooks.keys - shipped).to_h { |file| [file, nil] })
|
|
310
|
-
incoming.filter_map do |file, from|
|
|
311
|
-
"hooks/#{file}" if Trash.edit?(File.join(provenance.hooks_dir, file), recorded: bundle.hooks[file]&.sha256, incoming: from)
|
|
312
|
-
end
|
|
310
|
+
assets = AssetState.in(provenance.guardrails_dir, kind_of_file: :rule, recorded: bundle.guardrails,
|
|
311
|
+
incoming: rules.to_h { |file| [File.basename(file), file] }) +
|
|
312
|
+
AssetState.in(provenance.plugin_dir, kind_of_file: :plugin, recorded: bundle.plugin_shas, incoming: plugin) +
|
|
313
|
+
AssetState.in(provenance.scripts_dir, kind_of_file: :script, recorded: bundle.scripts, incoming: scripts)
|
|
314
|
+
Trash.edits(provenance.hooks_dir, "hooks", bundle.hooks.transform_values(&:sha256)) +
|
|
315
|
+
assets.select(&:edited?).map(&:label)
|
|
313
316
|
end
|
|
314
317
|
|
|
315
|
-
# Installed
|
|
316
|
-
#
|
|
318
|
+
# Installed rules and the plugin whose sha differs from the recorded
|
|
319
|
+
# one (they didn't load), as guardrails/F, plugin/F.
|
|
317
320
|
def edited_executables(name, bundle)
|
|
318
321
|
provenance = Provenance.new(name: name)
|
|
319
322
|
edited = []
|
|
320
|
-
bundle.hooks.each do |file, hook|
|
|
321
|
-
edited << "hooks/#{file}" if edited?(File.join(provenance.hooks_dir, file), hook.sha256)
|
|
322
|
-
end
|
|
323
323
|
bundle.guardrails.each do |file, sha|
|
|
324
324
|
edited << "guardrails/#{file}" if edited?(File.join(provenance.guardrails_dir, file), sha)
|
|
325
325
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "file_digest"
|
|
4
4
|
require_relative "provenance"
|
|
5
5
|
require_relative "file_state"
|
|
6
|
+
require_relative "asset_state"
|
|
6
7
|
require_relative "manifest"
|
|
7
8
|
require_relative "../version"
|
|
8
9
|
require_relative "index_updater"
|
|
@@ -16,14 +17,17 @@ module Samagotchi
|
|
|
16
17
|
# A bundle as this chi reads it now (what chi bundle status prints):
|
|
17
18
|
# its stored record, the scope it resolves to (scope_error when it
|
|
18
19
|
# can't), the folder its files belong in, what each file looks like on
|
|
19
|
-
# disk, its installed plugin,
|
|
20
|
+
# disk, its installed plugin, the class-file hooks its record still
|
|
21
|
+
# has (retired_hooks: they don't load), and its needs
|
|
20
22
|
# with what this process's PATH found. The +files+ entries and +plugin+
|
|
21
23
|
# stay Hashes: their fields differ per case (an unchecked file has no
|
|
22
24
|
# paths at all). An entry's behind is its FileStanding::Behind, nil for a
|
|
23
25
|
# file that isn't; merge_note its FileStanding::MergeNote, nil without one;
|
|
24
26
|
# unreadable: the file is there, but this chi can't read it.
|
|
27
|
+
# rules and scripts: each recorded one by name, "ok", "modified" or
|
|
28
|
+
# "missing" (asset_states).
|
|
25
29
|
Report = Data.define(:provenance, :scope, :scope_error, :target_dir, :files, :plugin,
|
|
26
|
-
:
|
|
30
|
+
:rules, :scripts, :retired_hooks, :needs)
|
|
27
31
|
|
|
28
32
|
# A scope this chi can't resolve (a newer chi's, a hand edit) isn't
|
|
29
33
|
# guessed: scope_error says so, target_dir is nil and each file is
|
|
@@ -69,7 +73,9 @@ module Samagotchi
|
|
|
69
73
|
end
|
|
70
74
|
Report.new(provenance: bundle, scope: scope, scope_error: target_dir ? nil : "unknown scope: #{scope}",
|
|
71
75
|
target_dir: target_dir, files: details, plugin: plugin_status(provenance, bundle),
|
|
72
|
-
|
|
76
|
+
rules: asset_states(provenance.guardrails_dir, :rule, bundle.guardrails),
|
|
77
|
+
scripts: asset_states(provenance.scripts_dir, :script, bundle.scripts),
|
|
78
|
+
retired_hooks: retired_hooks(provenance, bundle), needs: needs_status(bundle))
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
# A FileState digest as the Report has it: nil for none, or one that
|
|
@@ -77,13 +83,28 @@ module Samagotchi
|
|
|
77
83
|
def self.digest_or_nil(digest) = digest == :unreadable ? nil : digest
|
|
78
84
|
private_class_method :digest_or_nil
|
|
79
85
|
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
# The class-file hooks the record still has (a 0.157 install's; chi
|
|
87
|
+
# loads none, Manifest::HooksRetired), each "ok" or "missing" in
|
|
88
|
+
# hooks/, by name; {} for none.
|
|
89
|
+
# @return [Hash{String => String}]
|
|
90
|
+
def self.retired_hooks(provenance, bundle)
|
|
91
|
+
bundle.hooks.keys.sort.to_h do |file|
|
|
92
|
+
[file, File.exist?(File.join(provenance.hooks_dir, file)) ? "ok" : "missing"]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
85
95
|
|
|
86
|
-
|
|
96
|
+
# Each recorded rule or script: "missing" (not there), "modified" (its
|
|
97
|
+
# AssetState is edited: the next install replaces it, a copy kept in
|
|
98
|
+
# the trash; an edited rule doesn't load) or "ok"; {} for none.
|
|
99
|
+
# @return [Hash{String => String}]
|
|
100
|
+
def self.asset_states(dir, kind_of_file, recorded)
|
|
101
|
+
recorded.keys.sort.to_h do |name|
|
|
102
|
+
asset = AssetState.of(File.join(dir, name), kind_of_file: kind_of_file, recorded: recorded[name], name: name)
|
|
103
|
+
[name, if asset.state == :missing then "missing"
|
|
104
|
+
elsif asset.edited? then "modified"
|
|
105
|
+
else "ok"
|
|
106
|
+
end]
|
|
107
|
+
end
|
|
87
108
|
end
|
|
88
109
|
|
|
89
110
|
# The stored needs, each with found: from this process's PATH (the
|