samagotchi 0.158.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 +21 -1
- data/bin/chi +3 -1
- data/docs/architecture.md +7 -6
- data/docs/cli.md +56 -1
- data/docs/configuration.md +9 -0
- data/docs/context.md +7 -1
- data/docs/guardrails.md +3 -1
- data/docs/hooks.md +3 -0
- data/docs/internals/bridge.md +1 -0
- data/docs/internals/bundles.md +9 -7
- data/docs/memory.md +5 -1
- data/docs/scheduling.md +228 -0
- data/docs/sessions.md +25 -1
- data/lib/samagotchi/bridge.rb +26 -2
- data/lib/samagotchi/bundle_command.rb +38 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/client_id.rb +13 -3
- data/lib/samagotchi/config.rb +12 -0
- data/lib/samagotchi/context_move.rb +74 -0
- data/lib/samagotchi/guardrails/pair_boundary.rb +1 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +84 -103
- data/lib/samagotchi/memory_bundle/asset_state.rb +81 -0
- data/lib/samagotchi/memory_bundle/file_actions.rb +51 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +17 -12
- data/lib/samagotchi/memory_bundle/status.rb +20 -1
- data/lib/samagotchi/memory_bundle/uninstaller.rb +10 -4
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/schedule_command.rb +252 -0
- data/lib/samagotchi/schedules.rb +948 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +20 -0
- data/lib/samagotchi/session_manager/spawn.rb +25 -11
- data/lib/samagotchi/session_manager/worker_command.rb +13 -13
- data/lib/samagotchi/terminal_ui/formatting.rb +2 -0
- data/lib/samagotchi/tool_declarations.rb +43 -0
- 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/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/public/chains.js +32 -8
- data/lib/samagotchi/web/public/index.html +4 -1
- data/lib/samagotchi/web/public/info_bar.js +4 -3
- 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 +8 -1
|
@@ -1,6 +1,8 @@
|
|
|
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
7
|
require_relative "manifest"
|
|
6
8
|
require_relative "provenance"
|
|
@@ -20,8 +22,11 @@ module Samagotchi
|
|
|
20
22
|
# warnings, and #run returns what the record (Provenance#write) keeps.
|
|
21
23
|
# A dev install (+dev+, `chi bundle install --dev`) links each file to
|
|
22
24
|
# the source instead of copying it, and warns of no checksum mismatch.
|
|
23
|
-
#
|
|
24
|
-
#
|
|
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.
|
|
25
30
|
class AssetInstaller
|
|
26
31
|
# What was installed: the paths Provenance#write records (each
|
|
27
32
|
# empty / nil on a dry run).
|
|
@@ -32,6 +37,10 @@ module Samagotchi
|
|
|
32
37
|
# The retire's summary words (Installer#summary): why the hooks went.
|
|
33
38
|
RETIRED = "chi no longer loads class-file bundle hooks"
|
|
34
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
|
|
43
|
+
|
|
35
44
|
# @param source_dir [String] the normalized bundle source
|
|
36
45
|
# @param manifest [Manifest, nil] nil for a non-strict install without one
|
|
37
46
|
# @param existing [InstalledBundle, nil] the record before this install
|
|
@@ -65,9 +74,9 @@ module Samagotchi
|
|
|
65
74
|
def run
|
|
66
75
|
incoming_rules = Dir.glob(File.join(@bundle_dir, "guardrails", "*.yml")).sort
|
|
67
76
|
.reject { |src| linked_out?(src, "guardrails/#{File.basename(src)}") }
|
|
68
|
-
guardrail_files =
|
|
69
|
-
plugin_file =
|
|
70
|
-
script_files =
|
|
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)
|
|
71
80
|
# After the plugin and scripts, which raise for a file the manifest
|
|
72
81
|
# names and the bundle lacks: a refused install leaves hooks/ as it was.
|
|
73
82
|
retire_hooks
|
|
@@ -95,8 +104,8 @@ module Samagotchi
|
|
|
95
104
|
# load): every file in it goes, an edited one (or one the record
|
|
96
105
|
# doesn't know) copied to the trash first, a dev link never (it points
|
|
97
106
|
# at the source, which stays). Each is a :removed result (a dry run's
|
|
98
|
-
# :would_remove
|
|
99
|
-
#
|
|
107
|
+
# :would_remove) with the RETIRED note; an empty hooks/ dir goes
|
|
108
|
+
# without a word. The record keeps no hooks
|
|
100
109
|
# (Provenance#write).
|
|
101
110
|
def retire_hooks
|
|
102
111
|
dir = @provenance.hooks_dir
|
|
@@ -104,7 +113,7 @@ module Samagotchi
|
|
|
104
113
|
|
|
105
114
|
files = File.symlink?(dir) ? [] : Dir.glob("**/*", File::FNM_DOTMATCH, base: dir).sort.reject { |f| File.directory?(File.join(dir, f)) }
|
|
106
115
|
files.each do |file|
|
|
107
|
-
@results["hooks/#{file}"] = @dry_run ?
|
|
116
|
+
@results["hooks/#{file}"] = @dry_run ? retired.with(word: :would_remove) : retired
|
|
108
117
|
end
|
|
109
118
|
return if @dry_run
|
|
110
119
|
|
|
@@ -114,44 +123,15 @@ module Samagotchi
|
|
|
114
123
|
|
|
115
124
|
def retired = Installer::FileResult.new(word: :removed, note: RETIRED)
|
|
116
125
|
|
|
117
|
-
# The bundle's guardrails/*.yml
|
|
118
|
-
|
|
119
|
-
# installed are reported up to date, not "Installed".
|
|
120
|
-
# @return [Hash{String => String}] basename => installed path ({} on a dry run)
|
|
121
|
-
def install_guardrails(incoming_rules)
|
|
122
|
-
target = @provenance.guardrails_dir
|
|
123
|
-
results = incoming_rules.to_h { |src| [src, result_for(src, File.join(target, File.basename(src)), updated: false)] }
|
|
124
|
-
if @dry_run
|
|
125
|
-
results.each { |src, result| @results["guardrails/#{File.basename(src)}"] = planned(result) }
|
|
126
|
-
return {}
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
@trash.copy_edits(target, "guardrails", @existing&.guardrails, incoming_rules.to_h { |src| [File.basename(src), src] })
|
|
130
|
-
FileUtils.rm_rf(target)
|
|
131
|
-
return {} if incoming_rules.empty?
|
|
126
|
+
# The bundle's guardrails/*.yml, by basename.
|
|
127
|
+
def rules_incoming(incoming_rules) = incoming_rules.to_h { |src| [File.basename(src), src] }
|
|
132
128
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
@results["guardrails/#{File.basename(src)}"] = results[src]
|
|
138
|
-
[File.basename(src), dest]
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Copy the manifest's plugin file into the bundle's plugin/ dir,
|
|
143
|
-
# warning when its sha256 differs from the declared one or this chi
|
|
144
|
-
# doesn't meet requires_chi (the Engine then won't load it).
|
|
145
|
-
# @return [String, nil] the installed file (nil: none, or a dry run)
|
|
146
|
-
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
|
|
147
133
|
plugin = @manifest&.plugin
|
|
148
|
-
unless plugin
|
|
149
|
-
unless @dry_run
|
|
150
|
-
@trash.copy_edits(@provenance.plugin_dir, "plugin", @existing&.plugin_shas)
|
|
151
|
-
FileUtils.rm_rf(@provenance.plugin_dir)
|
|
152
|
-
end
|
|
153
|
-
return nil
|
|
154
|
-
end
|
|
134
|
+
return {} unless plugin
|
|
155
135
|
|
|
156
136
|
src = File.join(@bundle_dir, plugin[:file])
|
|
157
137
|
raise Installer::InstallError, "the manifest names plugin #{plugin[:file]}, which the bundle doesn't have" unless File.file?(src)
|
|
@@ -160,83 +140,84 @@ module Samagotchi
|
|
|
160
140
|
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, @chi_version))
|
|
161
141
|
@warnings << "Plugin #{plugin[:file]} won't load: #{failure}"
|
|
162
142
|
end
|
|
163
|
-
|
|
164
|
-
result = result_for(src, dest, updated: @upgrade && @provenance.installed?)
|
|
165
|
-
if @dry_run
|
|
166
|
-
@results[plugin[:file]] = planned(result)
|
|
167
|
-
return nil
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
@trash.copy_edits(@provenance.plugin_dir, "plugin", @existing&.plugin_shas, { plugin[:file] => src })
|
|
171
|
-
FileUtils.rm_rf(@provenance.plugin_dir)
|
|
172
|
-
FileUtils.mkdir_p(@provenance.plugin_dir)
|
|
173
|
-
place(src, dest)
|
|
174
|
-
@results[plugin[:file]] = result
|
|
175
|
-
warning = FileDigest.mismatch("plugin #{plugin[:file]}", dest, @manifest.checksum_for_plugin) if @strict && !@dev
|
|
176
|
-
@warnings << warning if warning
|
|
177
|
-
dest
|
|
143
|
+
{ plugin[:file] => src }
|
|
178
144
|
end
|
|
179
145
|
|
|
180
|
-
# The manifest's scripts
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
# @return [Hash{String => String}] file name => installed path
|
|
184
|
-
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
|
|
185
149
|
scripts = @manifest&.scripts || {}
|
|
186
|
-
scripts.
|
|
150
|
+
scripts.keys.to_h do |file|
|
|
187
151
|
src = File.join(@bundle_dir, "scripts", file)
|
|
188
152
|
raise Installer::InstallError, "the manifest names scripts/#{file}, which the bundle doesn't have" unless File.file?(src)
|
|
189
153
|
raise Installer::InstallError, "scripts/#{file} is a symlink (not installed)" if linked?(src)
|
|
190
154
|
|
|
191
155
|
@warnings << "scripts/#{file} is not executable; chmod +x #{src}" if @dev && !File.executable?(src) && !page_only?(file)
|
|
156
|
+
[file, src]
|
|
192
157
|
end
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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)]
|
|
196
179
|
end
|
|
197
180
|
if @dry_run
|
|
198
|
-
results.each { |
|
|
181
|
+
results.each { |name, result| @results[result_key(kind_of_file, name)] = result }
|
|
199
182
|
return {}
|
|
200
183
|
end
|
|
201
184
|
|
|
202
|
-
@trash.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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]
|
|
216
200
|
end
|
|
217
201
|
end
|
|
218
202
|
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
else :installed
|
|
227
|
-
end
|
|
228
|
-
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
|
|
229
210
|
end
|
|
230
211
|
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
when :
|
|
239
|
-
|
|
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:")
|
|
240
221
|
end
|
|
241
222
|
end
|
|
242
223
|
|
|
@@ -267,7 +248,7 @@ module Samagotchi
|
|
|
267
248
|
# GuardrailWiring#bundle_rules doesn't read the rules of a bundle whose
|
|
268
249
|
# requires_chi this chi doesn't meet, and a session that never loaded
|
|
269
250
|
# them records a required load failure (the gate refuses guarded calls
|
|
270
|
-
# until chi is updated): say so at install, as
|
|
251
|
+
# until chi is updated): say so at install, as #plugin_incoming does.
|
|
271
252
|
def warn_rules_requires_chi(rule_files)
|
|
272
253
|
return if @manifest.nil? || rule_files.empty?
|
|
273
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
|
|
@@ -22,6 +22,8 @@ module Samagotchi
|
|
|
22
22
|
# :keep left as it is (already the bundle's, or kept)
|
|
23
23
|
# :skip left as it is, the bundle doesn't take it
|
|
24
24
|
# :move_to_trash moved to the trash (a removal)
|
|
25
|
+
# :remove deleted (an asset: save_first copies an edit to the
|
|
26
|
+
# trash first)
|
|
25
27
|
# :refuse the command stops
|
|
26
28
|
# :restore the bundle's text put back (SystemBundle at its version)
|
|
27
29
|
# save_first: a copy of the old bytes goes to the trash before a write
|
|
@@ -157,6 +159,55 @@ module Samagotchi
|
|
|
157
159
|
|
|
158
160
|
TABLES = { install: INSTALL, force: FORCE, upgrade: UPGRADE, remerge: REMERGE, restore: RESTORE,
|
|
159
161
|
uninstall: UNINSTALL }.freeze
|
|
162
|
+
|
|
163
|
+
# A bundle's own folder (rules, the plugin, scripts: AssetState), which
|
|
164
|
+
# every install replaces whole: the source's files go in, the rest go,
|
|
165
|
+
# an edit (AssetState#edited?) copied to the trash first. A cell is keyed
|
|
166
|
+
# [state, kind_of_file] or [state, :any]. A file the source has is
|
|
167
|
+
# written even when it has its bytes already (and so checked against the
|
|
168
|
+
# manifest's sha, as any other).
|
|
169
|
+
ASSET_WRITE = Action.new(do: :write_incoming, word: :installed, next_base: :on_disk)
|
|
170
|
+
ASSET_REMOVE = Action.new(do: :remove, word: nil, next_base: :none, record: :drop)
|
|
171
|
+
ASSET_GONE = Action.new(do: :skip, word: nil, next_base: :none, record: :drop)
|
|
172
|
+
|
|
173
|
+
# A plain install, install --force, and an upgrade with no record yet.
|
|
174
|
+
ASSET_INSTALL = {
|
|
175
|
+
%i[new any] => ASSET_WRITE, %i[unchanged any] => ASSET_WRITE.with(word: :up_to_date),
|
|
176
|
+
%i[pristine any] => ASSET_WRITE, %i[edited any] => ASSET_WRITE.with(save_first: true),
|
|
177
|
+
%i[linked any] => ASSET_WRITE,
|
|
178
|
+
# The source dropped it: it goes with the folder, without a word.
|
|
179
|
+
%i[dropped_pristine any] => ASSET_REMOVE, %i[dropped_edited any] => ASSET_REMOVE.with(save_first: true),
|
|
180
|
+
%i[missing any] => ASSET_GONE
|
|
181
|
+
}.freeze
|
|
182
|
+
|
|
183
|
+
# An upgrade (--force too) over the record: a file it replaces is
|
|
184
|
+
# updated, except a rule (an upgrade lists a replaced rule as installed:
|
|
185
|
+
# the corpus's rule-installed).
|
|
186
|
+
ASSET_UPGRADE = ASSET_INSTALL.merge(
|
|
187
|
+
%i[pristine any] => ASSET_WRITE.with(word: :updated),
|
|
188
|
+
%i[edited any] => ASSET_WRITE.with(word: :updated, save_first: true),
|
|
189
|
+
%i[linked any] => ASSET_WRITE.with(word: :updated),
|
|
190
|
+
%i[pristine rule] => ASSET_WRITE, %i[edited rule] => ASSET_WRITE.with(save_first: true),
|
|
191
|
+
%i[linked rule] => ASSET_WRITE
|
|
192
|
+
).freeze
|
|
193
|
+
|
|
194
|
+
# Uninstall: the folder goes (no source, so every file there is a
|
|
195
|
+
# dropped_* or a link), an edit copied to the trash first, --force or not.
|
|
196
|
+
ASSET_UNINSTALL = {
|
|
197
|
+
%i[dropped_pristine any] => ASSET_REMOVE, %i[dropped_edited any] => ASSET_REMOVE.with(save_first: true),
|
|
198
|
+
%i[linked any] => ASSET_REMOVE, %i[missing any] => ASSET_GONE
|
|
199
|
+
}.freeze
|
|
200
|
+
|
|
201
|
+
ASSET_TABLES = { install: ASSET_INSTALL, upgrade: ASSET_UPGRADE, uninstall: ASSET_UNINSTALL }.freeze
|
|
202
|
+
|
|
203
|
+
# The command's action for an AssetState: [state, kind_of_file], then
|
|
204
|
+
# [state, :any].
|
|
205
|
+
# @param command [Symbol] an ASSET_TABLES key
|
|
206
|
+
# @return [Action, nil] nil: the command never meets the state
|
|
207
|
+
def self.for_asset(command, asset)
|
|
208
|
+
table = ASSET_TABLES.fetch(command)
|
|
209
|
+
table[[asset.state, asset.kind_of_file]] || table[[asset.state, :any]]
|
|
210
|
+
end
|
|
160
211
|
# The kinds whose row depends on another bundle listing the file.
|
|
161
212
|
SHARED_ASKED = %i[dropped_pristine dropped_edited pristine pristine_no_base edited edited_no_base].freeze
|
|
162
213
|
|
|
@@ -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"
|
|
@@ -28,10 +29,11 @@ module Samagotchi
|
|
|
28
29
|
#
|
|
29
30
|
# .md files take the Installer's 3-way merge: a clean text merge replaces
|
|
30
31
|
# the file (merged), a real conflict keeps the user's (behind). Rules,
|
|
31
|
-
# the plugin and scripts are replaced, an older chi's hooks removed; an
|
|
32
|
-
#
|
|
33
|
-
# the
|
|
34
|
-
# replaced
|
|
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
|
|
@@ -293,21 +295,24 @@ module Samagotchi
|
|
|
293
295
|
end
|
|
294
296
|
|
|
295
297
|
# What the upgrade from +source_dir+ copies to the trash, as
|
|
296
|
-
# hooks/F, guardrails/F, plugin/F, scripts/F
|
|
297
|
-
# folders AssetInstaller replaces
|
|
298
|
-
# it puts in its place
|
|
299
|
-
#
|
|
300
|
-
# upgrade removes whole (AssetInstaller#retire_hooks; no
|
|
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.
|
|
301
304
|
def trash_bound(name, bundle, manifest, source_dir)
|
|
302
305
|
provenance = Provenance.new(name: name)
|
|
303
306
|
rules = Dir.glob(File.join(source_dir, "guardrails", "*.yml")).sort
|
|
304
307
|
.reject { |file| AssetInstaller.linked?(file, source_dir) }
|
|
305
308
|
plugin = manifest.plugin ? { manifest.plugin[:file] => File.join(source_dir, manifest.plugin[:file]) } : {}
|
|
306
309
|
scripts = manifest.scripts.keys.to_h { |file| [file, File.join(source_dir, "scripts", file)] }
|
|
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)
|
|
307
314
|
Trash.edits(provenance.hooks_dir, "hooks", bundle.hooks.transform_values(&:sha256)) +
|
|
308
|
-
|
|
309
|
-
Trash.edits(provenance.plugin_dir, "plugin", bundle.plugin_shas, plugin) +
|
|
310
|
-
Trash.edits(provenance.scripts_dir, "scripts", bundle.scripts, scripts)
|
|
315
|
+
assets.select(&:edited?).map(&:label)
|
|
311
316
|
end
|
|
312
317
|
|
|
313
318
|
# Installed rules and the plugin whose sha differs from the recorded
|
|
@@ -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"
|
|
@@ -23,8 +24,10 @@ module Samagotchi
|
|
|
23
24
|
# paths at all). An entry's behind is its FileStanding::Behind, nil for a
|
|
24
25
|
# file that isn't; merge_note its FileStanding::MergeNote, nil without one;
|
|
25
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).
|
|
26
29
|
Report = Data.define(:provenance, :scope, :scope_error, :target_dir, :files, :plugin,
|
|
27
|
-
:retired_hooks, :needs)
|
|
30
|
+
:rules, :scripts, :retired_hooks, :needs)
|
|
28
31
|
|
|
29
32
|
# A scope this chi can't resolve (a newer chi's, a hand edit) isn't
|
|
30
33
|
# guessed: scope_error says so, target_dir is nil and each file is
|
|
@@ -70,6 +73,8 @@ module Samagotchi
|
|
|
70
73
|
end
|
|
71
74
|
Report.new(provenance: bundle, scope: scope, scope_error: target_dir ? nil : "unknown scope: #{scope}",
|
|
72
75
|
target_dir: target_dir, files: details, plugin: plugin_status(provenance, bundle),
|
|
76
|
+
rules: asset_states(provenance.guardrails_dir, :rule, bundle.guardrails),
|
|
77
|
+
scripts: asset_states(provenance.scripts_dir, :script, bundle.scripts),
|
|
73
78
|
retired_hooks: retired_hooks(provenance, bundle), needs: needs_status(bundle))
|
|
74
79
|
end
|
|
75
80
|
|
|
@@ -88,6 +93,20 @@ module Samagotchi
|
|
|
88
93
|
end
|
|
89
94
|
end
|
|
90
95
|
|
|
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
|
|
108
|
+
end
|
|
109
|
+
|
|
91
110
|
# The stored needs, each with found: from this process's PATH (the
|
|
92
111
|
# shell's; a worker's PATH can differ, see docs/memory.md). A needs:
|
|
93
112
|
# that doesn't parse is none.
|
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "digest"
|
|
5
5
|
require_relative "../memory_paths"
|
|
6
6
|
require_relative "provenance"
|
|
7
|
+
require_relative "asset_state"
|
|
7
8
|
require_relative "file_actions"
|
|
8
9
|
require_relative "index_updater"
|
|
9
10
|
require_relative "trash"
|
|
@@ -144,12 +145,17 @@ module Samagotchi
|
|
|
144
145
|
private
|
|
145
146
|
|
|
146
147
|
# Copies each edited file of the bundle's own folder to the trash: the
|
|
147
|
-
# rm_rf below takes them, --force or not.
|
|
148
|
+
# rm_rf below takes them, --force or not. Rules, the plugin and
|
|
149
|
+
# scripts by their source-less AssetState and ASSET_UNINSTALL's
|
|
150
|
+
# save_first; an older chi's hooks/ by Trash#copy_edits.
|
|
148
151
|
def save_edits(provenance, bundle)
|
|
149
152
|
@trash.copy_edits(provenance.hooks_dir, "hooks", bundle.hooks.transform_values(&:sha256))
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
{ rule: [provenance.guardrails_dir, bundle.guardrails], plugin: [provenance.plugin_dir, bundle.plugin_shas],
|
|
154
|
+
script: [provenance.scripts_dir, bundle.scripts] }.each do |kind_of_file, (dir, recorded)|
|
|
155
|
+
AssetState.in(dir, kind_of_file: kind_of_file, recorded: recorded).each do |asset|
|
|
156
|
+
@trash.copy(asset.path, as: asset.label) if FileActions.for_asset(:uninstall, asset)&.save_first
|
|
157
|
+
end
|
|
158
|
+
end
|
|
153
159
|
end
|
|
154
160
|
|
|
155
161
|
# A kept shared file's index line names the bundle that still has it
|
|
@@ -214,6 +214,7 @@ module Samagotchi
|
|
|
214
214
|
chi models [--format json] [TEXT] the models --model takes, from every host
|
|
215
215
|
chi home [set [--ruby PATH] | doctor] the Ruby every chi launch boots
|
|
216
216
|
chi project [init [DIR]] this folder's project; init makes a folder in no git repository one
|
|
217
|
+
chi schedule <add|list|show|tick|run-now|enable|disable|cancel> durable schedules that outlive every worker
|
|
217
218
|
chi remote <add|ls|rm> other machines' chi web, for chi --remote NAME
|
|
218
219
|
chi --remote NAME <send|answer|note|context|sessions|share|review> … those commands on that machine (or CHI_REMOTE=NAME)
|
|
219
220
|
chi --remote NAME --attach ID | --resume ID | --dir DIR [-p TEXT] a session on that machine, in this terminal
|