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
|
@@ -52,7 +52,7 @@ module Samagotchi
|
|
|
52
52
|
RULE = "pair-boundary"
|
|
53
53
|
SOURCE = "core"
|
|
54
54
|
OWNER_VOICED = %w[delegate register_reminder cancel_reminder send_note forget_outputs task_create task_stop
|
|
55
|
-
find_tools objectives].freeze
|
|
55
|
+
find_tools objectives schedule_task list_schedules cancel_schedule].freeze
|
|
56
56
|
OWNER_VOICED_PREFIX = "delegate_"
|
|
57
57
|
# The background task tools that read the owner's tasks (a pair turn
|
|
58
58
|
# starts none: task_create is owner-voiced, and its execute is never
|
|
@@ -153,6 +153,17 @@ module Samagotchi
|
|
|
153
153
|
(low == low_dir || low.start_with?(File.join(low_dir, ""))) && folds_case?(dir)
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
# Whether +root+ is the home folder or a folder above it (symlinks
|
|
157
|
+
# resolved on both; false for a blank one). ReviewPaths (a participant
|
|
158
|
+
# turn's project) and MemoryPaths (where a chi project marker is
|
|
159
|
+
# ignored) ask this.
|
|
160
|
+
# @param root [String, nil] @param home [String, nil]
|
|
161
|
+
def home_root?(root, home)
|
|
162
|
+
return false if home.to_s.empty? || root.to_s.empty?
|
|
163
|
+
|
|
164
|
+
under?(real(home.to_s), real(root.to_s))
|
|
165
|
+
end
|
|
166
|
+
|
|
156
167
|
# Whether +path+ is in +dir+, not +dir+ itself (.under?'s terms).
|
|
157
168
|
def strictly_under?(path, dir, fold: true)
|
|
158
169
|
return true if path.start_with?(File.join(dir, ""))
|
|
@@ -59,11 +59,7 @@ module Samagotchi
|
|
|
59
59
|
# review turn reads nothing there (#refusal: HOME_ROOT); a pair turn
|
|
60
60
|
# asks the owner before every write (PairBoundary).
|
|
61
61
|
# @param root [String] @param home [String, nil]
|
|
62
|
-
def self.home_root?(root, home)
|
|
63
|
-
return false if home.to_s.empty? || root.to_s.empty?
|
|
64
|
-
|
|
65
|
-
PathResolve.under?(PathResolve.real(home.to_s), PathResolve.real(root.to_s))
|
|
66
|
-
end
|
|
62
|
+
def self.home_root?(root, home) = PathResolve.home_root?(root, home)
|
|
67
63
|
|
|
68
64
|
# @param root [String, nil] the session's project root (the work tree
|
|
69
65
|
# top of its folder); nil or blank refuses every path
|
|
@@ -21,7 +21,7 @@ module Samagotchi
|
|
|
21
21
|
# How much of a failed hook's error #plugin_hook_failed shows.
|
|
22
22
|
HOOK_FAILURE_CHARS = 300
|
|
23
23
|
# What the notice says the hook now does, by the policy Hooks.wrap gave it
|
|
24
|
-
# (a chi.on handler
|
|
24
|
+
# (a chi.on handler :log, or :deny for an on_error: :fail_closed one).
|
|
25
25
|
HOOK_FAILURE_EFFECTS = {
|
|
26
26
|
log: "it's skipped, and only the log has its next failures",
|
|
27
27
|
skip: "it's skipped, and its next failures are silent",
|
|
@@ -98,7 +98,7 @@ module Samagotchi
|
|
|
98
98
|
nil
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
# A
|
|
101
|
+
# A chi.on handler raised (Hooks.wrap; the log has
|
|
102
102
|
# every firing of a :log one). Its first failure on this Engine, so in
|
|
103
103
|
# this worker, tells the user, as a plugin that failed to load does; then
|
|
104
104
|
# only the log. One line per label — file, bundle and event — so a
|
data/lib/samagotchi/hooks.rb
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "hooks/registry"
|
|
4
4
|
require_relative "hooks/loader"
|
|
5
|
-
require_relative "hooks/bundle_loader"
|
|
6
5
|
require_relative "hooks/stream_watch"
|
|
7
6
|
|
|
8
7
|
module Samagotchi
|
|
9
|
-
# Thin wrapper that exposes the Hooks::Registry
|
|
10
|
-
#
|
|
8
|
+
# Thin wrapper that exposes the Hooks::Registry and Hooks::Loader
|
|
9
|
+
# (config.yml's hooks; a bundle's hook through its plugin, Plugin::Api#on).
|
|
11
10
|
#
|
|
12
11
|
# The Engine holds an instance of Hooks::Registry and provides:
|
|
13
12
|
# - #register_hook(name, &block) — register a turn-scoped hook
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require_relative "asset_state"
|
|
5
|
+
require_relative "file_actions"
|
|
4
6
|
require_relative "file_digest"
|
|
5
|
-
require_relative "bundle_hook"
|
|
6
7
|
require_relative "manifest"
|
|
7
8
|
require_relative "provenance"
|
|
8
9
|
require_relative "../version"
|
|
@@ -13,22 +14,32 @@ module Samagotchi
|
|
|
13
14
|
|
|
14
15
|
# The install of a bundle's parts that live in its own dir
|
|
15
16
|
# (<bundles>/<name>/, Provenance#bundle_dir) rather than in the
|
|
16
|
-
# memories dir:
|
|
17
|
-
#
|
|
17
|
+
# memories dir: guardrails/*.yml, the plugin file and scripts/*. Each
|
|
18
|
+
# replaces what an earlier version installed. An older chi's record
|
|
19
|
+
# with class-file hooks has them removed (#retire_hooks): chi no longer
|
|
20
|
+
# loads them, and the Installer refuses a source that has any. Installer
|
|
18
21
|
# runs it after the memories; it adds to the installer's results and
|
|
19
22
|
# warnings, and #run returns what the record (Provenance#write) keeps.
|
|
20
23
|
# A dev install (+dev+, `chi bundle install --dev`) links each file to
|
|
21
24
|
# the source instead of copying it, and warns of no checksum mismatch.
|
|
22
|
-
#
|
|
23
|
-
#
|
|
25
|
+
# Each folder is one loop over its files' AssetStates (#install_set):
|
|
26
|
+
# FileActions' asset tables say what each gets, the dry run reading the
|
|
27
|
+
# same row. An edited file it replaces or removes is copied to the trash
|
|
28
|
+
# first, under its subfolder (AssetState#edited?, Trash.edit?'s rule); a
|
|
29
|
+
# link never is.
|
|
24
30
|
class AssetInstaller
|
|
25
31
|
# What was installed: the paths Provenance#write records (each
|
|
26
32
|
# empty / nil on a dry run).
|
|
27
|
-
#
|
|
28
|
-
# hook_files, guardrail_files, script_files
|
|
29
|
-
# basename => installed path
|
|
33
|
+
# guardrail_files, script_files basename => installed path
|
|
30
34
|
# plugin_file the installed plugin file, nil without one
|
|
31
|
-
Installed = Data.define(:
|
|
35
|
+
Installed = Data.define(:guardrail_files, :plugin_file, :script_files)
|
|
36
|
+
|
|
37
|
+
# The retire's summary words (Installer#summary): why the hooks went.
|
|
38
|
+
RETIRED = "chi no longer loads class-file bundle hooks"
|
|
39
|
+
|
|
40
|
+
# A dry run's word for a real run's (Installer#summary): nothing would
|
|
41
|
+
# be written to a file already up to date.
|
|
42
|
+
DRY_WORDS = { installed: :would_install, updated: :would_update, up_to_date: :up_to_date }.freeze
|
|
32
43
|
|
|
33
44
|
# @param source_dir [String] the normalized bundle source
|
|
34
45
|
# @param manifest [Manifest, nil] nil for a non-strict install without one
|
|
@@ -61,18 +72,16 @@ module Samagotchi
|
|
|
61
72
|
# @raise [Installer::InstallError] the manifest names a plugin or
|
|
62
73
|
# script the bundle doesn't have
|
|
63
74
|
def run
|
|
64
|
-
install_hooks
|
|
65
|
-
prune_dropped_hooks if (@upgrade || (@dev && @existing&.dev)) && @existing && !@dry_run
|
|
66
|
-
verify_hook_checksums if @manifest && @strict && !@dev
|
|
67
75
|
incoming_rules = Dir.glob(File.join(@bundle_dir, "guardrails", "*.yml")).sort
|
|
68
76
|
.reject { |src| linked_out?(src, "guardrails/#{File.basename(src)}") }
|
|
69
|
-
guardrail_files =
|
|
70
|
-
plugin_file =
|
|
71
|
-
script_files =
|
|
72
|
-
|
|
77
|
+
guardrail_files = install_set(:rule, rules_incoming(incoming_rules))
|
|
78
|
+
plugin_file = install_set(:plugin, plugin_incoming).values.first
|
|
79
|
+
script_files = install_set(:script, scripts_incoming)
|
|
80
|
+
# After the plugin and scripts, which raise for a file the manifest
|
|
81
|
+
# names and the bundle lacks: a refused install leaves hooks/ as it was.
|
|
82
|
+
retire_hooks
|
|
73
83
|
warn_rules_requires_chi(incoming_rules)
|
|
74
|
-
Installed.new(
|
|
75
|
-
plugin_file: plugin_file, script_files: script_files)
|
|
84
|
+
Installed.new(guardrail_files: guardrail_files, plugin_file: plugin_file, script_files: script_files)
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
# Whether the bundle file +src+ reads through a symlink: it is one, or
|
|
@@ -91,138 +100,38 @@ module Samagotchi
|
|
|
91
100
|
|
|
92
101
|
private
|
|
93
102
|
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
end
|
|
109
|
-
hook_entries.keys.each do |basename|
|
|
110
|
-
next unless basename.is_a?(String) && !basename.empty?
|
|
111
|
-
|
|
112
|
-
src = File.join(@bundle_dir, "hooks", basename)
|
|
113
|
-
# A linked hook isn't the bundle's: not installed, counted or recorded.
|
|
114
|
-
next hook_entries.delete(basename) if (File.symlink?(src) || File.exist?(src)) && linked_out?(src, "hooks/#{basename}")
|
|
115
|
-
|
|
116
|
-
@bundle_hooks << basename
|
|
117
|
-
install_hook(basename, src) if File.exist?(src)
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# One listed hook; an upgrade overwrites a local edit with a warning.
|
|
122
|
-
def install_hook(basename, src)
|
|
123
|
-
dest = File.join(@provenance.hooks_dir, basename)
|
|
124
|
-
result = result_for(src, dest, updated: @upgrade && @existing)
|
|
125
|
-
return @results[basename] = planned(result) if @dry_run
|
|
126
|
-
|
|
127
|
-
prev_sha = @existing&.hooks&.dig(basename)&.sha256
|
|
128
|
-
if @upgrade && File.exist?(dest) && !@force && !Provenance.recorded_sha(prev_sha).empty? && !Provenance.sha_matches?(dest, prev_sha)
|
|
129
|
-
@warnings << "Hook #{basename} was locally modified; overwriting"
|
|
103
|
+
# The hooks/ an older chi installed (class-file hooks, which no longer
|
|
104
|
+
# load): every file in it goes, an edited one (or one the record
|
|
105
|
+
# doesn't know) copied to the trash first, a dev link never (it points
|
|
106
|
+
# at the source, which stays). Each is a :removed result (a dry run's
|
|
107
|
+
# :would_remove) with the RETIRED note; an empty hooks/ dir goes
|
|
108
|
+
# without a word. The record keeps no hooks
|
|
109
|
+
# (Provenance#write).
|
|
110
|
+
def retire_hooks
|
|
111
|
+
dir = @provenance.hooks_dir
|
|
112
|
+
return unless File.directory?(dir) || File.symlink?(dir)
|
|
113
|
+
|
|
114
|
+
files = File.symlink?(dir) ? [] : Dir.glob("**/*", File::FNM_DOTMATCH, base: dir).sort.reject { |f| File.directory?(File.join(dir, f)) }
|
|
115
|
+
files.each do |file|
|
|
116
|
+
@results["hooks/#{file}"] = @dry_run ? retired.with(word: :would_remove) : retired
|
|
130
117
|
end
|
|
118
|
+
return if @dry_run
|
|
131
119
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
place(src, dest)
|
|
135
|
-
@results[basename] = result
|
|
136
|
-
@hook_files[basename] = dest
|
|
120
|
+
@trash.copy_edits(dir, "hooks", @existing&.hooks&.transform_values(&:sha256))
|
|
121
|
+
FileUtils.rm_rf(dir)
|
|
137
122
|
end
|
|
138
123
|
|
|
139
|
-
|
|
140
|
-
def install_unlisted_hooks
|
|
141
|
-
Dir.glob(File.join(@bundle_dir, "hooks", "*.rb")).each do |src|
|
|
142
|
-
basename = File.basename(src)
|
|
143
|
-
next if linked_out?(src, "hooks/#{basename}")
|
|
144
|
-
|
|
145
|
-
@bundle_hooks << basename
|
|
146
|
-
if @dry_run
|
|
147
|
-
@results[basename] = Installer::FileResult.new(word: :would_install)
|
|
148
|
-
else
|
|
149
|
-
FileUtils.mkdir_p(@provenance.hooks_dir)
|
|
150
|
-
dest = File.join(@provenance.hooks_dir, basename)
|
|
151
|
-
@trash.copy_edit(dest, as: "hooks/#{basename}", recorded: @existing&.hooks&.dig(basename)&.sha256, incoming: src)
|
|
152
|
-
place(src, dest)
|
|
153
|
-
@results[basename] = Installer::FileResult.new(word: :installed)
|
|
154
|
-
@hook_files[basename] = dest
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
124
|
+
def retired = Installer::FileResult.new(word: :removed, note: RETIRED)
|
|
158
125
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
def prune_dropped_hooks
|
|
162
|
-
@existing.hooks.each_key do |old_key_str|
|
|
163
|
-
next if @bundle_hooks.include?(old_key_str)
|
|
164
|
-
|
|
165
|
-
old_dest = File.join(@provenance.hooks_dir, old_key_str)
|
|
166
|
-
if File.exist?(old_dest) && !@force && !File.symlink?(old_dest)
|
|
167
|
-
@warnings << "Bundle no longer includes hook #{old_key_str} but local file exists — removing"
|
|
168
|
-
end
|
|
169
|
-
@trash.copy_edit(old_dest, as: "hooks/#{old_key_str}", recorded: @existing.hooks[old_key_str]&.sha256)
|
|
170
|
-
FileUtils.rm_f(old_dest)
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Strict mode: a hook whose sha256 differs from the one its manifest
|
|
175
|
-
# declares warns (as verify_memory_checksums does). The bundle's file is
|
|
176
|
-
# checked, the one the install copies, so a dry run checks what it
|
|
177
|
-
# would install, not the old hook in place.
|
|
178
|
-
def verify_hook_checksums
|
|
179
|
-
@bundle_hooks.each do |basename|
|
|
180
|
-
next if @conflicts.key?(basename)
|
|
181
|
-
|
|
182
|
-
warning = FileDigest.mismatch("hook #{basename}", File.join(@bundle_dir, "hooks", basename),
|
|
183
|
-
@manifest.checksum_for_hook(basename))
|
|
184
|
-
@warnings << warning if warning
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# The bundle's guardrails/*.yml into <bundle_dir>/guardrails/: the
|
|
189
|
-
# set replaces what an earlier version installed. Same bytes as
|
|
190
|
-
# installed are reported up to date, not "Installed".
|
|
191
|
-
# @return [Hash{String => String}] basename => installed path ({} on a dry run)
|
|
192
|
-
def install_guardrails(incoming_rules)
|
|
193
|
-
target = @provenance.guardrails_dir
|
|
194
|
-
results = incoming_rules.to_h { |src| [src, result_for(src, File.join(target, File.basename(src)), updated: false)] }
|
|
195
|
-
if @dry_run
|
|
196
|
-
results.each { |src, result| @results["guardrails/#{File.basename(src)}"] = planned(result) }
|
|
197
|
-
return {}
|
|
198
|
-
end
|
|
126
|
+
# The bundle's guardrails/*.yml, by basename.
|
|
127
|
+
def rules_incoming(incoming_rules) = incoming_rules.to_h { |src| [File.basename(src), src] }
|
|
199
128
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
FileUtils.mkdir_p(target)
|
|
205
|
-
incoming_rules.to_h do |src|
|
|
206
|
-
dest = File.join(target, File.basename(src))
|
|
207
|
-
place(src, dest)
|
|
208
|
-
@results["guardrails/#{File.basename(src)}"] = results[src]
|
|
209
|
-
[File.basename(src), dest]
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Copy the manifest's plugin file into the bundle's plugin/ dir,
|
|
214
|
-
# warning when its sha256 differs from the declared one or this chi
|
|
215
|
-
# doesn't meet requires_chi (the Engine then won't load it).
|
|
216
|
-
# @return [String, nil] the installed file (nil: none, or a dry run)
|
|
217
|
-
def install_plugin
|
|
129
|
+
# The manifest's plugin file, warning when this chi doesn't meet
|
|
130
|
+
# requires_chi (the Engine then won't load it). {} without one.
|
|
131
|
+
# @return [Hash{String => String}] its file => the source file
|
|
132
|
+
def plugin_incoming
|
|
218
133
|
plugin = @manifest&.plugin
|
|
219
|
-
unless plugin
|
|
220
|
-
unless @dry_run
|
|
221
|
-
@trash.copy_edits(@provenance.plugin_dir, "plugin", @existing&.plugin_shas)
|
|
222
|
-
FileUtils.rm_rf(@provenance.plugin_dir)
|
|
223
|
-
end
|
|
224
|
-
return nil
|
|
225
|
-
end
|
|
134
|
+
return {} unless plugin
|
|
226
135
|
|
|
227
136
|
src = File.join(@bundle_dir, plugin[:file])
|
|
228
137
|
raise Installer::InstallError, "the manifest names plugin #{plugin[:file]}, which the bundle doesn't have" unless File.file?(src)
|
|
@@ -231,81 +140,84 @@ module Samagotchi
|
|
|
231
140
|
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, @chi_version))
|
|
232
141
|
@warnings << "Plugin #{plugin[:file]} won't load: #{failure}"
|
|
233
142
|
end
|
|
234
|
-
|
|
235
|
-
result = result_for(src, dest, updated: @upgrade && @provenance.installed?)
|
|
236
|
-
if @dry_run
|
|
237
|
-
@results[plugin[:file]] = planned(result)
|
|
238
|
-
return nil
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
@trash.copy_edits(@provenance.plugin_dir, "plugin", @existing&.plugin_shas, { plugin[:file] => src })
|
|
242
|
-
FileUtils.rm_rf(@provenance.plugin_dir)
|
|
243
|
-
FileUtils.mkdir_p(@provenance.plugin_dir)
|
|
244
|
-
place(src, dest)
|
|
245
|
-
@results[plugin[:file]] = result
|
|
246
|
-
warning = FileDigest.mismatch("plugin #{plugin[:file]}", dest, @manifest.checksum_for_plugin) if @strict && !@dev
|
|
247
|
-
@warnings << warning if warning
|
|
248
|
-
dest
|
|
143
|
+
{ plugin[:file] => src }
|
|
249
144
|
end
|
|
250
145
|
|
|
251
|
-
# The manifest's scripts
|
|
252
|
-
#
|
|
253
|
-
|
|
254
|
-
# @return [Hash{String => String}] file name => installed path
|
|
255
|
-
def install_scripts
|
|
146
|
+
# The manifest's scripts (a dev install warns of one it can't run).
|
|
147
|
+
# @return [Hash{String => String}] file name => the source file
|
|
148
|
+
def scripts_incoming
|
|
256
149
|
scripts = @manifest&.scripts || {}
|
|
257
|
-
scripts.
|
|
150
|
+
scripts.keys.to_h do |file|
|
|
258
151
|
src = File.join(@bundle_dir, "scripts", file)
|
|
259
152
|
raise Installer::InstallError, "the manifest names scripts/#{file}, which the bundle doesn't have" unless File.file?(src)
|
|
260
153
|
raise Installer::InstallError, "scripts/#{file} is a symlink (not installed)" if linked?(src)
|
|
261
154
|
|
|
262
155
|
@warnings << "scripts/#{file} is not executable; chmod +x #{src}" if @dev && !File.executable?(src) && !page_only?(file)
|
|
156
|
+
[file, src]
|
|
263
157
|
end
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# One folder of the bundle's own dir (guardrails/, plugin/, scripts/),
|
|
161
|
+
# replaced whole by +incoming+: each file's AssetState and its action
|
|
162
|
+
# (FileActions' ASSET_INSTALL, or ASSET_UPGRADE over an installed
|
|
163
|
+
# bundle), one result per incoming file (a dry run's DRY_WORDS of it,
|
|
164
|
+
# and nothing written), an edit copied to the trash first (save_first),
|
|
165
|
+
# then the folder replaced. A file the source dropped goes without a
|
|
166
|
+
# word. A plugin or script whose sha256 differs from the declared one
|
|
167
|
+
# warns (not a dev link: it is the source).
|
|
168
|
+
# @param incoming [Hash{String => String}] name => the source file, in
|
|
169
|
+
# the order the results list them
|
|
170
|
+
# @return [Hash{String => String}] name => installed path ({} on a dry run)
|
|
171
|
+
def install_set(kind_of_file, incoming)
|
|
172
|
+
dir, recorded = folder(kind_of_file)
|
|
173
|
+
command = @upgrade && @provenance.installed? ? :upgrade : :install
|
|
174
|
+
states = AssetState.in(dir, kind_of_file: kind_of_file, recorded: recorded, incoming: incoming)
|
|
175
|
+
.to_h { |asset| [asset.name, [asset, FileActions.for_asset(command, asset)]] }
|
|
176
|
+
results = incoming.keys.to_h do |name|
|
|
177
|
+
asset, action = states.fetch(name)
|
|
178
|
+
[name, Installer::FileResult.new(word: @dry_run ? DRY_WORDS.fetch(action.word) : action.word, state: asset, action: action)]
|
|
267
179
|
end
|
|
268
180
|
if @dry_run
|
|
269
|
-
results.each { |
|
|
181
|
+
results.each { |name, result| @results[result_key(kind_of_file, name)] = result }
|
|
270
182
|
return {}
|
|
271
183
|
end
|
|
272
184
|
|
|
273
|
-
@trash.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
185
|
+
states.each_value { |asset, action| @trash.copy(asset.path, as: asset.label) if action.save_first }
|
|
186
|
+
FileUtils.rm_rf(dir)
|
|
187
|
+
return {} if incoming.empty?
|
|
188
|
+
|
|
189
|
+
FileUtils.mkdir_p(dir)
|
|
190
|
+
incoming.to_h do |name, src|
|
|
191
|
+
dest = File.join(dir, name)
|
|
192
|
+
place(src, dest)
|
|
193
|
+
@results[result_key(kind_of_file, name)] = results[name]
|
|
194
|
+
FileUtils.chmod(0o755, dest) if kind_of_file == :script && !@dev
|
|
195
|
+
if (declared = declared_sha(kind_of_file, name)) && results[name].action.do == :write_incoming && @strict && !@dev
|
|
196
|
+
warning = FileDigest.mismatch("#{kind_of_file} #{name}", dest, declared)
|
|
197
|
+
@warnings << warning if warning
|
|
198
|
+
end
|
|
199
|
+
[name, dest]
|
|
287
200
|
end
|
|
288
201
|
end
|
|
289
202
|
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
else :installed
|
|
298
|
-
end
|
|
299
|
-
Installer::FileResult.new(word: word)
|
|
203
|
+
# The kind's folder and the record's shas for it.
|
|
204
|
+
def folder(kind_of_file)
|
|
205
|
+
case kind_of_file
|
|
206
|
+
when :rule then [@provenance.guardrails_dir, @existing&.guardrails]
|
|
207
|
+
when :plugin then [@provenance.plugin_dir, @existing&.plugin_shas]
|
|
208
|
+
else [@provenance.scripts_dir, @existing&.scripts]
|
|
209
|
+
end
|
|
300
210
|
end
|
|
301
211
|
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
def
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
212
|
+
# Its key in the Installer's results (the summary's name): a rule's
|
|
213
|
+
# guardrails/r.yml, the plugin's own file name, a script's scripts/s.sh.
|
|
214
|
+
def result_key(kind_of_file, name) = kind_of_file == :plugin ? name : "#{AssetState::FOLDERS.fetch(kind_of_file)}/#{name}"
|
|
215
|
+
|
|
216
|
+
# The manifest's sha256 for a plugin or script (hex), nil for a rule.
|
|
217
|
+
def declared_sha(kind_of_file, name)
|
|
218
|
+
case kind_of_file
|
|
219
|
+
when :plugin then @manifest.checksum_for_plugin
|
|
220
|
+
when :script then @manifest.scripts[name].to_s.delete_prefix("sha256:")
|
|
309
221
|
end
|
|
310
222
|
end
|
|
311
223
|
|
|
@@ -333,21 +245,10 @@ module Samagotchi
|
|
|
333
245
|
true
|
|
334
246
|
end
|
|
335
247
|
|
|
336
|
-
# The hook loader skips every hook of a bundle whose requires_chi this
|
|
337
|
-
# chi doesn't meet (Hooks::BundleLoader); say so at install, as
|
|
338
|
-
# install_plugin does for the plugin.
|
|
339
|
-
def warn_hooks_requires_chi
|
|
340
|
-
return if @manifest.nil? || @bundle_hooks.empty?
|
|
341
|
-
|
|
342
|
-
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, @chi_version))
|
|
343
|
-
@warnings << "Bundle #{@name}: its hooks won't load: #{failure}"
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
|
|
347
248
|
# GuardrailWiring#bundle_rules doesn't read the rules of a bundle whose
|
|
348
249
|
# requires_chi this chi doesn't meet, and a session that never loaded
|
|
349
250
|
# them records a required load failure (the gate refuses guarded calls
|
|
350
|
-
# until chi is updated): say so at install, as
|
|
251
|
+
# until chi is updated): say so at install, as #plugin_incoming does.
|
|
351
252
|
def warn_rules_requires_chi(rule_files)
|
|
352
253
|
return if @manifest.nil? || rule_files.empty?
|
|
353
254
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "trash"
|
|
5
|
+
|
|
6
|
+
module Samagotchi
|
|
7
|
+
module MemoryBundle
|
|
8
|
+
# One file of a bundle's own folder (a guardrail rule, the plugin, a
|
|
9
|
+
# script) as a command finds it: derived from the file on disk, the sha
|
|
10
|
+
# the record holds and the source file that would take its place, never
|
|
11
|
+
# stored. FileActions' ASSET_* tables say what each command does in each
|
|
12
|
+
# state; AssetInstaller, ShippedUpdate's plan, Uninstaller and Status read
|
|
13
|
+
# it. Whether it is edited is Trash.edit?'s answer (what the trash keeps a
|
|
14
|
+
# copy of), asked once in .of.
|
|
15
|
+
#
|
|
16
|
+
# name its path in its folder (r.yml, plugin.rb, s.sh)
|
|
17
|
+
# path where it is installed
|
|
18
|
+
# kind_of_file :rule, :plugin or :script
|
|
19
|
+
# state :new not there (or a broken link), the source has it
|
|
20
|
+
# :unchanged the source's bytes already (a dev link to them too)
|
|
21
|
+
# :pristine the recorded sha, the source has other bytes
|
|
22
|
+
# :edited neither the recorded sha (none recorded: unknown,
|
|
23
|
+
# so edited) nor the source's bytes
|
|
24
|
+
# :linked a link (a dev install's) to other bytes than the
|
|
25
|
+
# source's: it points at a source nothing here touches
|
|
26
|
+
# :dropped_pristine, :dropped_edited there, the source hasn't
|
|
27
|
+
# it (with no source read, as uninstall and status
|
|
28
|
+
# do, every file there is one of these)
|
|
29
|
+
# :missing recorded, not there, the source hasn't it
|
|
30
|
+
# recorded the record's sha256 for it (nil: not in the record)
|
|
31
|
+
# incoming the source file that replaces it (nil: none)
|
|
32
|
+
AssetState = Data.define(:name, :path, :kind_of_file, :state, :recorded, :incoming) do
|
|
33
|
+
# @param path [String] the installed file
|
|
34
|
+
# @param recorded [String, nil] its recorded sha256
|
|
35
|
+
# @param incoming [String, nil] the source file put in its place
|
|
36
|
+
def self.of(path, kind_of_file:, recorded: nil, incoming: nil, name: File.basename(path))
|
|
37
|
+
new(name: name, path: path, kind_of_file: kind_of_file, state: state_of(path, recorded, incoming),
|
|
38
|
+
recorded: recorded, incoming: incoming)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Every file of the folder +dir+ a command meets: each file in it (at
|
|
42
|
+
# any depth, as Trash#copy_edits reads it; a linked dir isn't followed),
|
|
43
|
+
# each incoming one and each recorded one, by name.
|
|
44
|
+
# @param recorded [Hash, nil] name => recorded sha256
|
|
45
|
+
# @param incoming [Hash] name => the source file
|
|
46
|
+
# @return [Array<AssetState>]
|
|
47
|
+
def self.in(dir, kind_of_file:, recorded: nil, incoming: {})
|
|
48
|
+
recorded ||= {}
|
|
49
|
+
on_disk = if File.symlink?(dir) || !File.directory?(dir) then []
|
|
50
|
+
else Dir.glob("**/*", File::FNM_DOTMATCH, base: dir).reject { |file| File.directory?(File.join(dir, file)) }
|
|
51
|
+
end
|
|
52
|
+
(on_disk | incoming.keys | recorded.keys).sort.map do |name|
|
|
53
|
+
of(File.join(dir, name), kind_of_file: kind_of_file, recorded: recorded[name], incoming: incoming[name], name: name)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.state_of(path, recorded, incoming)
|
|
58
|
+
if incoming && File.file?(path) && FileUtils.identical?(incoming, path) then :unchanged
|
|
59
|
+
elsif File.symlink?(path) && File.exist?(path) then :linked
|
|
60
|
+
elsif !File.file?(path) then incoming ? :new : :missing
|
|
61
|
+
elsif Trash.edit?(path, recorded: recorded, incoming: incoming) then incoming ? :edited : :dropped_edited
|
|
62
|
+
else incoming ? :pristine : :dropped_pristine
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
private_class_method :state_of
|
|
66
|
+
|
|
67
|
+
# Trash.edit?'s rule (asked in .of): an edit a replace or a removal
|
|
68
|
+
# would lose, so the trash keeps a copy first.
|
|
69
|
+
def edited? = %i[edited dropped_edited].include?(state)
|
|
70
|
+
|
|
71
|
+
# Where the trash keeps its copy, and the plan names it: guardrails/r.yml,
|
|
72
|
+
# plugin/plugin.rb, scripts/s.sh.
|
|
73
|
+
def label = "#{self.class::FOLDERS.fetch(kind_of_file)}/#{name}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class AssetState
|
|
77
|
+
# Each kind's folder in the bundle's own dir (and in the trash).
|
|
78
|
+
FOLDERS = { rule: "guardrails", plugin: "plugin", script: "scripts" }.freeze
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|