samagotchi 0.156.0 → 0.158.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 +70 -1
- data/README.md +1 -1
- data/bin/chi +4 -1
- data/docs/architecture.md +18 -12
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +4 -1
- data/docs/cli.md +29 -24
- data/docs/configuration.md +43 -9
- data/docs/context.md +5 -3
- data/docs/guardrails.md +12 -7
- data/docs/hooks.md +53 -83
- data/docs/internals/bridge.md +84 -7
- data/docs/internals/bundles.md +69 -46
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +23 -7
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +51 -10
- data/docs/plugins.md +48 -11
- data/docs/releasing.md +6 -5
- data/docs/sessions.md +13 -13
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +56 -5
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +5 -7
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
- data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/bundle_command/check.rb +41 -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/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +20 -45
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- 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 +7 -7
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_sources.rb +9 -6
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +30 -9
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +26 -44
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
- data/lib/samagotchi/memory_bundle/builder.rb +12 -35
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
- data/lib/samagotchi/memory_bundle/installer.rb +212 -178
- data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
- data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
- data/lib/samagotchi/memory_bundle/status.rb +26 -30
- data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
- data/lib/samagotchi/memory_bundle/trash.rb +28 -18
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +42 -10
- data/lib/samagotchi/plugin/loader.rb +75 -15
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- 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/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +26 -0
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +349 -441
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +134 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -4
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/delegate.rb +60 -51
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +66 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/index.html +6 -1
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +22 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -34,6 +34,20 @@ module Samagotchi
|
|
|
34
34
|
class Installer
|
|
35
35
|
class InstallError < StandardError; end
|
|
36
36
|
|
|
37
|
+
# What the run did to one file (a memory, or a rule, plugin or script:
|
|
38
|
+
# AssetInstaller writes them too, and removes an older chi's hooks), as
|
|
39
|
+
# #results holds it. word is
|
|
40
|
+
# the report bucket (#summary): a real run's (:installed, :updated,
|
|
41
|
+
# :merged, :kept, :kept_pruned, :kept_shared, :kept_own (a file left as
|
|
42
|
+
# the user's), :up_to_date, :conflict, :removed) or a dry run's
|
|
43
|
+
# (:would_install, :would_update, :would_merge, :would_skip, :would_remove,
|
|
44
|
+
# and an upgrade's :fast_forward and :keep). state and action: the
|
|
45
|
+
# memory's FileState and FileActions::Action, nil where none was read.
|
|
46
|
+
# note: why, in words (a conflict's Behind#state, a kept file's reason).
|
|
47
|
+
FileResult = Data.define(:word, :state, :action, :note) do
|
|
48
|
+
def initialize(word:, state: nil, action: nil, note: nil) = super
|
|
49
|
+
end
|
|
50
|
+
|
|
37
51
|
attr_reader :results, :warnings, :placeholder_warnings, :conflicts
|
|
38
52
|
|
|
39
53
|
# Each file this run left behind (FileStanding::Behind).
|
|
@@ -65,7 +79,7 @@ module Samagotchi
|
|
|
65
79
|
@conflicts = {}
|
|
66
80
|
@behind = []
|
|
67
81
|
@replaced = []
|
|
68
|
-
@
|
|
82
|
+
@actions = {}
|
|
69
83
|
@overlays = []
|
|
70
84
|
@trash = Trash.new(name)
|
|
71
85
|
end
|
|
@@ -82,7 +96,7 @@ module Samagotchi
|
|
|
82
96
|
def trash_copy_line = @trash.copy_line
|
|
83
97
|
|
|
84
98
|
# The files this run merged, the bundle's changes joined to the user's.
|
|
85
|
-
def merged_files =
|
|
99
|
+
def merged_files = with_word(:merged)
|
|
86
100
|
|
|
87
101
|
# The normalized source dir. An owned one (a git/zip/tar extract) is
|
|
88
102
|
# removed after #run, or kept until #cleanup_source! when an upgrade
|
|
@@ -140,33 +154,35 @@ module Samagotchi
|
|
|
140
154
|
def summary
|
|
141
155
|
lines = []
|
|
142
156
|
lines << "Source changes: #{@source_change.join(" → ")}" if @source_change
|
|
143
|
-
installed =
|
|
144
|
-
updated =
|
|
157
|
+
installed = with_word(:installed)
|
|
158
|
+
updated = with_word(:updated)
|
|
145
159
|
merged = merged_files
|
|
146
|
-
would_merge =
|
|
147
|
-
kept =
|
|
160
|
+
would_merge = with_word(:would_merge)
|
|
161
|
+
kept = with_word(:kept, :keep, :kept_pruned)
|
|
148
162
|
# A file whose bytes are already the bundle's was not left out: it is
|
|
149
163
|
# up to date. Skipped keeps the files really left out -- the user's own
|
|
150
|
-
# memory, and a dry run's would_skip
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
would_remove = @results.select { |_, r| r[:status].to_s == "would_remove" }.keys
|
|
164
|
+
# memory, and a dry run's would_skip.
|
|
165
|
+
up_to_date = with_word(:up_to_date)
|
|
166
|
+
skipped = with_word(:would_skip, :kept_own)
|
|
167
|
+
would_install = with_word(:would_install)
|
|
168
|
+
would_update = with_word(:would_update)
|
|
169
|
+
fast_forward = with_word(:fast_forward)
|
|
170
|
+
# An older chi's class-file hooks this run removes: a line of their
|
|
171
|
+
# own, with why (AssetInstaller#retire_hooks).
|
|
172
|
+
retired, removed = with_word(:removed).partition { |key| @results[key].note == AssetInstaller::RETIRED }
|
|
173
|
+
would_retire, would_remove = with_word(:would_remove).partition { |key| @results[key].note == AssetInstaller::RETIRED }
|
|
161
174
|
|
|
162
175
|
lines << "Installed: #{installed.join(", ")}" unless installed.empty?
|
|
163
176
|
lines << "Updated: #{updated.join(", ")}" unless updated.empty?
|
|
164
177
|
lines << "Would install: #{would_install.join(", ")}" unless would_install.empty?
|
|
178
|
+
lines << "Would update: #{would_update.join(", ")}" unless would_update.empty?
|
|
165
179
|
lines << "Fast-forward: #{fast_forward.join(", ")}" unless fast_forward.empty?
|
|
166
180
|
lines << FileStanding.merged_line(merged, @manifest&.version) unless merged.empty?
|
|
167
181
|
lines << FileStanding.merged_line(would_merge, @manifest&.version, dry_run: true) unless would_merge.empty?
|
|
168
182
|
lines << "Removed (no longer in the bundle): #{removed.join(", ")}#{" (moved to #{trash_dir})" if trash_dir}" unless removed.empty?
|
|
169
183
|
lines << "Would remove (no longer in the bundle): #{would_remove.join(", ")}" unless would_remove.empty?
|
|
184
|
+
lines << "Removed (#{AssetInstaller::RETIRED}): #{retired.join(", ")}" unless retired.empty?
|
|
185
|
+
lines << "Would remove (#{AssetInstaller::RETIRED}): #{would_retire.join(", ")}" unless would_retire.empty?
|
|
170
186
|
lines << "Kept (local edits preserved): #{kept.join(", ")}" unless kept.empty?
|
|
171
187
|
lines << "Up to date: #{up_to_date.join(", ")}" unless up_to_date.empty?
|
|
172
188
|
lines << "Skipped: #{skipped.join(", ")}" unless skipped.empty?
|
|
@@ -198,16 +214,19 @@ module Samagotchi
|
|
|
198
214
|
|
|
199
215
|
private
|
|
200
216
|
|
|
217
|
+
# The files whose result has one of +words+, in the order they ran.
|
|
218
|
+
def with_word(*words) = @results.select { |_, result| words.include?(result.word) }.keys
|
|
219
|
+
|
|
201
220
|
# A plain install over a bundle that is already installed from the same
|
|
202
|
-
# source: it rewrote the bundle's
|
|
221
|
+
# source: it rewrote the bundle's rules, plugin, scripts and its
|
|
203
222
|
# record, and left a memory the user wrote as it was. So the line says
|
|
204
223
|
# what changed, and points at the upgrade for the memories it kept.
|
|
205
224
|
def reinstall_line
|
|
206
|
-
line = "#{@name} was already installed from #{@source}: its
|
|
225
|
+
line = "#{@name} was already installed from #{@source}: its rules, plugin and scripts " \
|
|
207
226
|
"now match it and its record was rewritten"
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
if @results.any? { |_, r| r
|
|
227
|
+
# :kept_own is the only way a memory is left out of a plain install
|
|
228
|
+
# (its KEPT_OWN or HELD row): its bytes differ from the bundle's.
|
|
229
|
+
if @results.any? { |_, r| r.word == :kept_own }
|
|
211
230
|
line << "; your own copies of its memories were kept (chi bundle upgrade #{@name} " \
|
|
212
231
|
"merges the bundle's changes into them)"
|
|
213
232
|
end
|
|
@@ -242,10 +261,10 @@ module Samagotchi
|
|
|
242
261
|
# #ref; a path resolved); one it can't place (an older record's
|
|
243
262
|
# relative path) never refuses.
|
|
244
263
|
# - a name chi ships comes from a shipped dir only, unless --force
|
|
245
|
-
# (install and upgrade): the plugin and
|
|
264
|
+
# (install and upgrade): the plugin and rules it replaces are chi's;
|
|
246
265
|
# - a plain install over a bundle from another source is refused,
|
|
247
266
|
# --force too (that overwrites files, it doesn't switch sources):
|
|
248
|
-
# it would delete the plugin and
|
|
267
|
+
# it would delete the plugin and rules and rewrite the record;
|
|
249
268
|
# - an upgrade may switch sources (a URL and a local copy of it are
|
|
250
269
|
# one bundle, an archive's file name has its version), and
|
|
251
270
|
# #source_change says so (the summary's first line).
|
|
@@ -337,19 +356,32 @@ module Samagotchi
|
|
|
337
356
|
@bundle_dir = @source_dir = prepared.dir
|
|
338
357
|
@source_owned = prepared.owned
|
|
339
358
|
@source_commit = prepared.commit
|
|
359
|
+
refuse_hooks
|
|
340
360
|
@manifest = Manifest.read(dir: @bundle_dir)
|
|
341
361
|
clash = @manifest.session_settings_clash
|
|
342
362
|
raise InstallError, clash if clash
|
|
343
363
|
rescue SourceNormalizer::UnknownSourceError => e
|
|
344
364
|
raise InstallError, "Source normalization failed: #{e.message}"
|
|
365
|
+
rescue Manifest::HooksRetired => e
|
|
366
|
+
# A rule, not a strictness option: refused strict or not, a dry run too.
|
|
367
|
+
raise InstallError, e.message
|
|
345
368
|
rescue Manifest::ValidationError => e
|
|
346
369
|
raise InstallError, e.message if @strict
|
|
347
370
|
|
|
348
371
|
@warnings << "No manifest found — proceeding without strict manifest validation"
|
|
349
372
|
end
|
|
350
373
|
|
|
374
|
+
# A source that ships class-file hooks (hooks/*.rb, listed or not) is
|
|
375
|
+
# refused before its manifest is read; its manifest's hooks: is
|
|
376
|
+
# Manifest.read's refusal (Manifest::HooksRetired either way).
|
|
377
|
+
# @raise [Manifest::HooksRetired]
|
|
378
|
+
def refuse_hooks
|
|
379
|
+
files = Manifest::HooksRetired.files_in(@bundle_dir)
|
|
380
|
+
raise Manifest::HooksRetired.for_files(files) unless files.empty?
|
|
381
|
+
end
|
|
382
|
+
|
|
351
383
|
# A bundle whose requires_chi this chi doesn't meet is not installed at
|
|
352
|
-
# all: --force is what installs it, and its plugin
|
|
384
|
+
# all: --force is what installs it, and its plugin and rules then
|
|
353
385
|
# warn that they won't load (AssetInstaller). The words come from
|
|
354
386
|
# Manifest.requires_chi_failure, so the requirement is said one way.
|
|
355
387
|
# A dry run is refused the same way: nothing there would load either.
|
|
@@ -374,47 +406,38 @@ module Samagotchi
|
|
|
374
406
|
|
|
375
407
|
# Copies the bundle's memories (MemoryFiles: the files: list, else
|
|
376
408
|
# every *.md but README and the like) into the target dir and refreshes
|
|
377
|
-
# their index lines
|
|
378
|
-
#
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
#
|
|
409
|
+
# their index lines. Each file's FileState and its command's row
|
|
410
|
+
# (FileActions: INSTALL, FORCE, UPGRADE) say what happens: a dry run
|
|
411
|
+
# only reads the row (preview_memory), a real run does it
|
|
412
|
+
# (write_memory). Sets @bundle_md (every file the bundle ships) and
|
|
413
|
+
# @owned: the bundle owns (records, upgrades, removes) only the files it
|
|
414
|
+
# wrote, these from an earlier install and the ones written now. A
|
|
415
|
+
# same-name file that was already there is the user's: skipped and never
|
|
416
|
+
# recorded (the not-owned rows).
|
|
383
417
|
def install_memories
|
|
384
|
-
owned_before = @existing ? @existing.files.keys : []
|
|
385
418
|
@bundle_md = []
|
|
386
419
|
@owned = []
|
|
387
420
|
warn_unshipped_files
|
|
388
421
|
warn_symlinked_files
|
|
389
422
|
memory_files.each do |file_key|
|
|
390
|
-
file_path = File.join(@bundle_dir, file_key)
|
|
391
423
|
@bundle_md << file_key
|
|
392
424
|
note_overlay(file_key, @target_dir)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
# Only --force gets here with a file in place: a copy keeps it.
|
|
410
|
-
@replaced << file_key if File.exist?(target_path)
|
|
411
|
-
@trash.copy(target_path) if File.exist?(target_path) && !FileUtils.identical?(file_path, target_path)
|
|
412
|
-
FileUtils.cp(file_path, target_path)
|
|
413
|
-
@owned << file_key
|
|
414
|
-
@outcomes[file_key] = Merger::INSTALL
|
|
415
|
-
@results[file_key] = { status: "installed" }
|
|
416
|
-
update_target_index(@target_scope, target_path, file_key)
|
|
417
|
-
end
|
|
425
|
+
state = FileState.of(file_key, record: @existing, provenance: @provenance, target_dir: @target_dir,
|
|
426
|
+
source_dir: @bundle_dir)
|
|
427
|
+
action = FileActions.for(memory_command, state)
|
|
428
|
+
next preview_memory(file_key, state, action) if @dry_run
|
|
429
|
+
|
|
430
|
+
@owned << file_key unless action.record == :not_owned
|
|
431
|
+
write_memory(file_key, state, action)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# The table a memory's row is read from: --force (install or upgrade:
|
|
436
|
+
# one code path), an upgrade over a record, else a plain install.
|
|
437
|
+
def memory_command
|
|
438
|
+
if @force then :force
|
|
439
|
+
elsif @upgrade && @existing then :upgrade
|
|
440
|
+
else :install
|
|
418
441
|
end
|
|
419
442
|
end
|
|
420
443
|
|
|
@@ -434,42 +457,64 @@ module Samagotchi
|
|
|
434
457
|
MemoryFiles.symlinks(@bundle_dir, @manifest).each { |name| @warnings << "#{name} is a symlink (not installed)" }
|
|
435
458
|
end
|
|
436
459
|
|
|
437
|
-
#
|
|
438
|
-
#
|
|
439
|
-
#
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
460
|
+
# One memory's row, done: the bundle's text written (a copy of other
|
|
461
|
+
# bytes in the trash first), the text merge's result written, the file
|
|
462
|
+
# kept or left behind (a conflict, recorded in @conflicts for the agent
|
|
463
|
+
# step), or skipped as the user's; its index line refreshed. A :decide
|
|
464
|
+
# cell is the upgrade's (Merger::Upgrade#decision: the text merge or the
|
|
465
|
+
# legacy guard, against the base the last install recorded).
|
|
466
|
+
def write_memory(file_key, state, action)
|
|
467
|
+
file_path = state.paths[:incoming]
|
|
468
|
+
target_path = state.paths[:current]
|
|
469
|
+
decision = upgrade_view.decision(file_key, state) if action.do == :decide
|
|
470
|
+
action = decision.action if decision
|
|
471
|
+
@actions[file_key] = action
|
|
472
|
+
@replaced << file_key if @force && state.digests.disk
|
|
473
|
+
result = FileResult.new(word: action.word, state: state, action: action)
|
|
474
|
+
case action.do
|
|
475
|
+
when :write_incoming
|
|
476
|
+
save_first(file_key, file_path, target_path) if action.save_first
|
|
444
477
|
FileUtils.cp(file_path, target_path)
|
|
445
|
-
@results[file_key] =
|
|
446
|
-
when :
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
478
|
+
@results[file_key] = action.word == :updated ? result.with(note: "auto-merged (not edited)") : result
|
|
479
|
+
when :merge
|
|
480
|
+
if Merger.write_merged(target_path, decision.outcome, @trash)
|
|
481
|
+
@results[file_key] = result.with(note: "the bundle's changes joined to local edits")
|
|
482
|
+
else
|
|
483
|
+
@actions[file_key] = FileActions::BEHIND
|
|
484
|
+
note_conflict(file_key, Merger::CONFLICT.with(merge: TextMerge.failed(TextMerge::RACED)), state)
|
|
485
|
+
end
|
|
450
486
|
when :keep
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if Merger.write_merged(target_path, outcome, @trash)
|
|
456
|
-
@results[file_key] = { status: "merged", reason: "the bundle's changes joined to local edits" }
|
|
487
|
+
if action.behind?
|
|
488
|
+
note_conflict(file_key, (decision || upgrade_view.decision(file_key, state)).outcome, state)
|
|
489
|
+
elsif action.word == :kept
|
|
490
|
+
@results[file_key] = result.with(note: decision.nil? || decision.outcome == Merger::KEEP ? "bundle unchanged, local edits preserved" : "local edits already hold the bundle's changes")
|
|
457
491
|
else
|
|
458
|
-
|
|
492
|
+
@results[file_key] = result
|
|
459
493
|
end
|
|
460
|
-
when :
|
|
461
|
-
|
|
462
|
-
|
|
494
|
+
when :skip
|
|
495
|
+
@results[file_key] = result.with(note: "already exists")
|
|
496
|
+
@warnings << "Skipped #{file_key} (already exists; #{FileStanding::FORCE_TAKES})"
|
|
497
|
+
when :refuse then refuse_unreadable(file_key, target_path)
|
|
498
|
+
else raise ArgumentError, "no install step for #{action.do.inspect} (#{file_key})" # the base would move with nothing written
|
|
463
499
|
end
|
|
464
|
-
update_target_index(@target_scope, target_path, file_key)
|
|
500
|
+
update_target_index(@target_scope, target_path, file_key, owned: action.record != :not_owned)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# A file chi can't read: whether it was edited can't be told, so the
|
|
504
|
+
# run stops (the read raises; Trash.failure_message says nothing was
|
|
505
|
+
# lost).
|
|
506
|
+
def refuse_unreadable(file_key, target_path)
|
|
507
|
+
FileDigest.hex(target_path)
|
|
508
|
+
raise InstallError, "#{file_key} can't be read"
|
|
465
509
|
end
|
|
466
510
|
|
|
467
511
|
# The file stays as it is, with the old base and the mark, in FileStanding's
|
|
468
512
|
# words.
|
|
469
|
-
def note_conflict(file_key, outcome)
|
|
513
|
+
def note_conflict(file_key, outcome, state = nil)
|
|
470
514
|
behind = upgrade_view.behind(file_key, outcome, @manifest&.version)
|
|
471
515
|
@behind << behind
|
|
472
|
-
@results[file_key] =
|
|
516
|
+
@results[file_key] = FileResult.new(word: :conflict, state: state, action: state && FileActions::BEHIND,
|
|
517
|
+
note: behind.state(dry_run: @dry_run))
|
|
473
518
|
@conflicts[file_key] = upgrade_view.paths(file_key)
|
|
474
519
|
@warnings << "#{behind.line(dry_run: @dry_run)} (#{FileStanding::FORCE_TAKES})"
|
|
475
520
|
end
|
|
@@ -488,58 +533,57 @@ module Samagotchi
|
|
|
488
533
|
@upgrade_view ||= Merger::Upgrade.new(provenance: @provenance, target_dir: @target_dir, source_dir: @bundle_dir)
|
|
489
534
|
end
|
|
490
535
|
|
|
491
|
-
# A
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
#
|
|
495
|
-
|
|
496
|
-
|
|
536
|
+
# A save_first cell: the file's other bytes go to the trash before the
|
|
537
|
+
# bundle's text replaces them. Under an upgrade that is a marked file
|
|
538
|
+
# that equals its base, and the update goes ahead: the base is known to
|
|
539
|
+
# be real (this chi's mark, or ShippedHistory), or no line of the file
|
|
540
|
+
# goes (Merger::Upgrade's legacy guard stops an older chi's mark
|
|
541
|
+
# otherwise), and it says so.
|
|
542
|
+
def save_first(file_key, file_path, target_path)
|
|
543
|
+
return unless File.exist?(target_path) && !FileUtils.identical?(file_path, target_path)
|
|
497
544
|
|
|
498
545
|
@trash.copy(target_path)
|
|
546
|
+
return if @force
|
|
547
|
+
|
|
499
548
|
@warnings << "#{file_key} was marked as kept but matched its recorded base, so it took the update; your copy is in the trash"
|
|
500
549
|
end
|
|
501
550
|
|
|
502
|
-
# A dry run's
|
|
503
|
-
#
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
@results[file_key] = { status: "would_install" }
|
|
507
|
-
return
|
|
508
|
-
end
|
|
551
|
+
# A dry run's word for an action's (the real run's): what would happen,
|
|
552
|
+
# nothing written. A conflict is reported as the real run would.
|
|
553
|
+
DRY_WORDS = { installed: :would_install, updated: :fast_forward, kept: :keep, merged: :would_merge,
|
|
554
|
+
kept_own: :would_skip, up_to_date: :up_to_date }.freeze
|
|
509
555
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
556
|
+
# A dry run's verdict for one file: its row read, not done (a :decide
|
|
557
|
+
# cell's text merge runs on temp copies). --force would install every
|
|
558
|
+
# file, unread.
|
|
559
|
+
def preview_memory(file_key, state, action)
|
|
560
|
+
return @results[file_key] = FileResult.new(word: :would_install, state: state, action: action) if @force
|
|
561
|
+
return refuse_unreadable(file_key, state.paths[:current]) if action.do == :refuse
|
|
513
562
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
end
|
|
563
|
+
decision = upgrade_view.decision(file_key, state) if action.do == :decide || action.behind?
|
|
564
|
+
action = decision.action if decision
|
|
565
|
+
return note_conflict(file_key, decision.outcome, state) if action.behind?
|
|
566
|
+
|
|
567
|
+
@results[file_key] = FileResult.new(word: DRY_WORDS.fetch(action.word), state: state, action: action)
|
|
520
568
|
end
|
|
521
569
|
|
|
522
|
-
# Strict mode: a memory
|
|
523
|
-
#
|
|
524
|
-
#
|
|
525
|
-
#
|
|
570
|
+
# Strict mode: a memory that takes the bundle's text (Action#takes_source?:
|
|
571
|
+
# a new or missing file, --force, an upgrade's or a re-install's
|
|
572
|
+
# fast-forward, a merge into a local edit) warns when the bundle's file
|
|
573
|
+
# differs from its manifest's sha256. One kept or skipped isn't checked
|
|
574
|
+
# (it takes nothing from the bundle). What is checked is the source
|
|
575
|
+
# file, the bytes the write or merge takes, so a dry run checks what it
|
|
576
|
+
# would bring in, never the user's bytes on disk. A dev install follows
|
|
577
|
+
# its own tree; chi bundle check flags its stale shas instead.
|
|
526
578
|
def verify_memory_checksums
|
|
527
|
-
|
|
528
|
-
next if @conflicts.key?(file_key)
|
|
529
|
-
|
|
530
|
-
status = @results[file_key] ? @results[file_key][:status].to_s : nil
|
|
531
|
-
next if %w[kept keep kept_pruned fast_forward updated skipped would_skip merged would_merge].include?(status)
|
|
579
|
+
return if @dev
|
|
532
580
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
expected = @manifest.checksum_for(file_key)
|
|
537
|
-
next unless expected
|
|
581
|
+
@bundle_md.each do |file_key|
|
|
582
|
+
result = @results[file_key]
|
|
583
|
+
next unless result&.action&.takes_source?
|
|
538
584
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
@warnings << "Checksum mismatch for #{file_key}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
542
|
-
end
|
|
585
|
+
warning = FileDigest.mismatch(file_key, result.state.paths[:incoming], @manifest.checksum_for(file_key))
|
|
586
|
+
@warnings << warning if warning
|
|
543
587
|
end
|
|
544
588
|
end
|
|
545
589
|
|
|
@@ -556,7 +600,7 @@ module Samagotchi
|
|
|
556
600
|
|
|
557
601
|
# The install's record (Provenance#write), only with a manifest and
|
|
558
602
|
# not on a dry run. An upgrade with conflicts writes it too: the new
|
|
559
|
-
# version, the plugin's
|
|
603
|
+
# version, the plugin's new sha, and the old base for each
|
|
560
604
|
# conflicted file, marked conflict (chi bundle status shows it).
|
|
561
605
|
# Skipping it left the replaced plugin failing its sha check.
|
|
562
606
|
# @param installed [AssetInstaller::Installed]
|
|
@@ -566,13 +610,11 @@ module Samagotchi
|
|
|
566
610
|
scope: @target_scope,
|
|
567
611
|
version: @manifest.version,
|
|
568
612
|
source_path: @source,
|
|
569
|
-
# AssetInstaller added the discovered hooks/*.rb to manifest.hooks.
|
|
570
|
-
hooks: @manifest.hooks,
|
|
571
613
|
trust_level: @manifest.trust_level,
|
|
572
614
|
source_commit: @source_commit,
|
|
573
|
-
hooks_files: installed.hook_files,
|
|
574
615
|
guardrails_files: installed.guardrail_files,
|
|
575
616
|
plugin_file: installed.plugin_file,
|
|
617
|
+
plugin_required: @manifest.plugin&.dig(:required) == true,
|
|
576
618
|
requires_chi: @manifest.requires_chi,
|
|
577
619
|
needs: @manifest.needs,
|
|
578
620
|
marks: @behind + held_conflict_marks,
|
|
@@ -600,7 +642,7 @@ module Samagotchi
|
|
|
600
642
|
def held_conflict_marks
|
|
601
643
|
return [] if @upgrade || @existing.nil?
|
|
602
644
|
|
|
603
|
-
kept = @owned.select { |key| @
|
|
645
|
+
kept = @owned.select { |key| @actions[key].record == :hold_mark }
|
|
604
646
|
kept.map { |key| held_mark(key) }
|
|
605
647
|
end
|
|
606
648
|
|
|
@@ -624,26 +666,26 @@ module Samagotchi
|
|
|
624
666
|
@owned.each do |file_key|
|
|
625
667
|
next unless File.exist?(File.join(@target_dir, file_key))
|
|
626
668
|
|
|
627
|
-
files[file_key] = next_base(file_key, @
|
|
669
|
+
files[file_key] = next_base(file_key, @actions.fetch(file_key))
|
|
628
670
|
end
|
|
629
671
|
@existing.files.each_key do |old_key_str|
|
|
630
672
|
next if @bundle_md.include?(old_key_str)
|
|
631
|
-
next unless @results[old_key_str]
|
|
673
|
+
next unless @results[old_key_str]&.word == :kept_pruned
|
|
632
674
|
|
|
633
675
|
files[old_key_str] = recorded_base(@provenance.base_path(old_key_str))
|
|
634
676
|
end
|
|
635
677
|
files
|
|
636
678
|
end
|
|
637
679
|
|
|
638
|
-
# The file an
|
|
639
|
-
# the user's file must not become one, or the next upgrade
|
|
640
|
-
# over it.
|
|
641
|
-
def next_base(file_key,
|
|
642
|
-
case
|
|
680
|
+
# The file an action's next_base names. nil for a kept file with no
|
|
681
|
+
# base: the user's file must not become one, or the next upgrade
|
|
682
|
+
# fast-forwards over it.
|
|
683
|
+
def next_base(file_key, action)
|
|
684
|
+
case action.next_base
|
|
643
685
|
when :recorded then recorded_base(@provenance.base_path(file_key))
|
|
644
686
|
when :on_disk then File.join(@target_dir, file_key)
|
|
645
687
|
when :incoming then File.join(@bundle_dir, file_key)
|
|
646
|
-
else raise ArgumentError, "unknown base #{
|
|
688
|
+
else raise ArgumentError, "unknown base #{action.next_base.inspect} for #{file_key}" # nil would drop the base silently
|
|
647
689
|
end
|
|
648
690
|
end
|
|
649
691
|
|
|
@@ -656,11 +698,9 @@ module Samagotchi
|
|
|
656
698
|
# sees the edit (not a fast-forward over it).
|
|
657
699
|
def install_provenance_files
|
|
658
700
|
files = @owned.each_with_object({}) do |file_key, map|
|
|
659
|
-
|
|
660
|
-
next unless File.exist?(target_path)
|
|
701
|
+
next unless File.exist?(File.join(@target_dir, file_key))
|
|
661
702
|
|
|
662
|
-
|
|
663
|
-
map[file_key] = kept ? recorded_base(@provenance.base_path(file_key)) : target_path
|
|
703
|
+
map[file_key] = next_base(file_key, @actions.fetch(file_key))
|
|
664
704
|
end
|
|
665
705
|
files.merge(dropped_owned_files)
|
|
666
706
|
end
|
|
@@ -682,58 +722,52 @@ module Samagotchi
|
|
|
682
722
|
end
|
|
683
723
|
end
|
|
684
724
|
|
|
685
|
-
# A file
|
|
686
|
-
#
|
|
687
|
-
#
|
|
688
|
-
#
|
|
689
|
-
|
|
690
|
-
if FileUtils.identical?(file_path, target_path)
|
|
691
|
-
@results[file_key] = { status: "skipped", reason: "already up to date" }
|
|
692
|
-
elsif @dry_run
|
|
693
|
-
@results[file_key] = { status: "would_skip" }
|
|
694
|
-
return
|
|
695
|
-
else
|
|
696
|
-
@results[file_key] = { status: "skipped", reason: "already exists" }
|
|
697
|
-
@warnings << "Skipped #{file_key} (already exists; #{FileStanding::FORCE_TAKES})"
|
|
698
|
-
end
|
|
699
|
-
update_target_index(scope, target_path, file_key, owned: owned) unless @dry_run
|
|
700
|
-
end
|
|
701
|
-
|
|
702
|
-
# A file the previous version installed that this one doesn't ship is
|
|
703
|
-
# moved to the trash (with its index line removed) when it still
|
|
704
|
-
# matches what was installed, or with --force; one the user edited is
|
|
705
|
-
# kept with a note.
|
|
725
|
+
# A file the previous version installed that this one doesn't ship:
|
|
726
|
+
# its FileState's dropped kind and the UPGRADE (or FORCE) row. An
|
|
727
|
+
# unedited one is moved to the trash (with its index line removed), as
|
|
728
|
+
# is any with --force; an edited one is kept with a note, and one
|
|
729
|
+
# another bundle lists too stays.
|
|
706
730
|
def prune_dropped_files(previous_files, bundle_files, target_dir, scope, provenance)
|
|
707
|
-
previous_files.
|
|
731
|
+
previous_files.each_key do |key|
|
|
708
732
|
next if bundle_files.include?(key)
|
|
709
733
|
|
|
710
734
|
target = File.join(target_dir, key)
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
735
|
+
state = FileState.of(key, record: @existing, provenance: provenance, target_dir: target_dir, source_dir: @bundle_dir,
|
|
736
|
+
shipped: false, claimants: ->(k) { Provenance.claimants(k, scope: scope, except: @name) })
|
|
737
|
+
action = if state.kind == :unreadable
|
|
738
|
+
unreadable_dropped(state, target)
|
|
739
|
+
else
|
|
740
|
+
FileActions.for(@force ? :force : :upgrade, state)
|
|
741
|
+
end
|
|
742
|
+
result = FileResult.new(word: action.word, state: state, action: action)
|
|
743
|
+
case action.word
|
|
744
|
+
when :kept_shared
|
|
745
|
+
others = state.shared_with.join(", ")
|
|
746
|
+
@results[key] = result.with(note: "bundle #{others} has it too")
|
|
747
|
+
@warnings << "Kept #{key}: no longer in the bundle, but bundle #{others} has it too"
|
|
748
|
+
when :removed
|
|
749
|
+
@results[key] = result.with(word: @dry_run ? :would_remove : :removed, note: "no longer in the bundle")
|
|
719
750
|
next if @dry_run
|
|
720
751
|
|
|
721
752
|
@trash.move(target)
|
|
722
753
|
remove_target_index(scope, key)
|
|
723
|
-
|
|
724
|
-
@results[key] =
|
|
754
|
+
when :kept_pruned
|
|
755
|
+
@results[key] = result.with(note: "local edits preserved")
|
|
725
756
|
@warnings << "Kept #{key}: no longer in the bundle but edited locally (#{FileStanding::FORCE_MOVES})"
|
|
726
757
|
end
|
|
727
758
|
end
|
|
728
759
|
end
|
|
729
760
|
|
|
730
|
-
#
|
|
731
|
-
#
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
761
|
+
# A dropped file chi can't read, in the order the rows are asked: kept
|
|
762
|
+
# when another bundle lists it, moved unread with --force; otherwise
|
|
763
|
+
# whether it was edited can't be told, and the read raises (the run
|
|
764
|
+
# stops, nothing lost).
|
|
765
|
+
def unreadable_dropped(state, target)
|
|
766
|
+
return FileActions::KEPT_SHARED if state.shared_with.any?
|
|
767
|
+
return FileActions::REMOVE if @force
|
|
735
768
|
|
|
736
|
-
|
|
769
|
+
FileDigest.hex(target)
|
|
770
|
+
raise InstallError, "#{state.key} can't be read"
|
|
737
771
|
end
|
|
738
772
|
|
|
739
773
|
# One warning per manifest key this chi's parser never reads (docs/cli.md's
|