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.
Files changed (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -1
  3. data/README.md +1 -1
  4. data/bin/chi +4 -1
  5. data/docs/architecture.md +18 -12
  6. data/docs/bundle-authoring.md +5 -5
  7. data/docs/bundles.md +4 -1
  8. data/docs/cli.md +29 -24
  9. data/docs/configuration.md +43 -9
  10. data/docs/context.md +5 -3
  11. data/docs/guardrails.md +12 -7
  12. data/docs/hooks.md +53 -83
  13. data/docs/internals/bridge.md +84 -7
  14. data/docs/internals/bundles.md +69 -46
  15. data/docs/internals/context-telemetry.md +4 -3
  16. data/docs/internals/engine.md +23 -7
  17. data/docs/internals/participants.md +3 -2
  18. data/docs/internals/remote-sessions.md +5 -2
  19. data/docs/internals/web-frontend.md +3 -2
  20. data/docs/internals/working-set.md +1 -1
  21. data/docs/memory.md +51 -10
  22. data/docs/plugins.md +48 -11
  23. data/docs/releasing.md +6 -5
  24. data/docs/sessions.md +13 -13
  25. data/docs/sub-agent.md +17 -2
  26. data/docs/testing.md +56 -5
  27. data/lib/samagotchi/answer_command.rb +2 -1
  28. data/lib/samagotchi/archive_store.rb +4 -3
  29. data/lib/samagotchi/bootstrap_command.rb +3 -2
  30. data/lib/samagotchi/bridge/card_places.rb +5 -7
  31. data/lib/samagotchi/bridge/card_store.rb +3 -0
  32. data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
  33. data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
  34. data/lib/samagotchi/bridge.rb +31 -6
  35. data/lib/samagotchi/broadcast/recipients.rb +8 -8
  36. data/lib/samagotchi/broadcast/triage_model.rb +1 -1
  37. data/lib/samagotchi/bundle_command/check.rb +41 -10
  38. data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
  39. data/lib/samagotchi/bundle_command/try/run.rb +4 -1
  40. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  41. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  42. data/lib/samagotchi/bundle_command.rb +20 -45
  43. data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
  44. data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
  45. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
  46. data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
  47. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  48. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  49. data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
  50. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  51. data/lib/samagotchi/child_move.rb +6 -6
  52. data/lib/samagotchi/child_reports.rb +2 -2
  53. data/lib/samagotchi/children_status.rb +15 -21
  54. data/lib/samagotchi/cli/parent_wait.rb +8 -22
  55. data/lib/samagotchi/config.rb +3 -0
  56. data/lib/samagotchi/context_command.rb +7 -5
  57. data/lib/samagotchi/context_sources.rb +9 -6
  58. data/lib/samagotchi/context_window.rb +9 -12
  59. data/lib/samagotchi/effective_model.rb +57 -22
  60. data/lib/samagotchi/engine/plugins.rb +1 -1
  61. data/lib/samagotchi/engine/recap.rb +8 -4
  62. data/lib/samagotchi/engine.rb +30 -9
  63. data/lib/samagotchi/events.rb +3 -2
  64. data/lib/samagotchi/extension_load.rb +26 -44
  65. data/lib/samagotchi/guardrails/load_failures.rb +3 -2
  66. data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
  67. data/lib/samagotchi/guardrails/review_paths.rb +1 -5
  68. data/lib/samagotchi/guardrails_command.rb +1 -1
  69. data/lib/samagotchi/hook_routes.rb +2 -2
  70. data/lib/samagotchi/hooks.rb +2 -3
  71. data/lib/samagotchi/host_address.rb +52 -0
  72. data/lib/samagotchi/host_registry.rb +57 -45
  73. data/lib/samagotchi/kernel_loop.rb +7 -3
  74. data/lib/samagotchi/llm/chat_loop.rb +6 -3
  75. data/lib/samagotchi/llm_context_strategy.rb +27 -63
  76. data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
  77. data/lib/samagotchi/memory_bundle/builder.rb +12 -35
  78. data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
  79. data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
  80. data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
  81. data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
  82. data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
  83. data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
  84. data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
  85. data/lib/samagotchi/memory_bundle/installer.rb +212 -178
  86. data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
  87. data/lib/samagotchi/memory_bundle/merger.rb +69 -65
  88. data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
  89. data/lib/samagotchi/memory_bundle/profile.rb +5 -5
  90. data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
  91. data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
  92. data/lib/samagotchi/memory_bundle/shas.rb +5 -8
  93. data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
  94. data/lib/samagotchi/memory_bundle/status.rb +26 -30
  95. data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
  96. data/lib/samagotchi/memory_bundle/trash.rb +28 -18
  97. data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
  98. data/lib/samagotchi/memory_orphans.rb +4 -4
  99. data/lib/samagotchi/memory_paths.rb +102 -13
  100. data/lib/samagotchi/memory_usage.rb +1 -1
  101. data/lib/samagotchi/model_lookup.rb +75 -0
  102. data/lib/samagotchi/model_profile.rb +1 -0
  103. data/lib/samagotchi/models_command.rb +1 -1
  104. data/lib/samagotchi/note_command.rb +1 -1
  105. data/lib/samagotchi/participant_mode.rb +2 -1
  106. data/lib/samagotchi/plugin/api.rb +42 -10
  107. data/lib/samagotchi/plugin/loader.rb +75 -15
  108. data/lib/samagotchi/plugin/sessions.rb +1 -1
  109. data/lib/samagotchi/project_command.rb +167 -0
  110. data/lib/samagotchi/project_init.rb +69 -0
  111. data/lib/samagotchi/project_scope.rb +6 -4
  112. data/lib/samagotchi/prompt_files.rb +10 -7
  113. data/lib/samagotchi/reasoning_history.rb +14 -20
  114. data/lib/samagotchi/recap_place.rb +26 -0
  115. data/lib/samagotchi/reply_wait/liveness.rb +38 -0
  116. data/lib/samagotchi/reply_wait/poll.rb +44 -15
  117. data/lib/samagotchi/reply_wait/target.rb +113 -0
  118. data/lib/samagotchi/reply_wait.rb +22 -2
  119. data/lib/samagotchi/run_options.rb +1 -0
  120. data/lib/samagotchi/sampling_settings.rb +9 -16
  121. data/lib/samagotchi/self_report.rb +34 -35
  122. data/lib/samagotchi/send_command.rb +17 -23
  123. data/lib/samagotchi/session.rb +349 -441
  124. data/lib/samagotchi/session_access/contract.rb +1 -1
  125. data/lib/samagotchi/session_access/local.rb +5 -6
  126. data/lib/samagotchi/session_access/remote.rb +5 -3
  127. data/lib/samagotchi/session_field.rb +134 -0
  128. data/lib/samagotchi/session_inbox.rb +49 -1
  129. data/lib/samagotchi/session_manager/emptiness.rb +2 -1
  130. data/lib/samagotchi/session_manager/listing.rb +29 -100
  131. data/lib/samagotchi/session_manager.rb +2 -1
  132. data/lib/samagotchi/session_metrics.rb +7 -4
  133. data/lib/samagotchi/session_paths.rb +57 -0
  134. data/lib/samagotchi/session_retention.rb +2 -2
  135. data/lib/samagotchi/session_row.rb +200 -0
  136. data/lib/samagotchi/session_setup.rb +15 -12
  137. data/lib/samagotchi/sessions_command.rb +11 -129
  138. data/lib/samagotchi/steer.rb +10 -0
  139. data/lib/samagotchi/steer_cut_after.rb +7 -14
  140. data/lib/samagotchi/system_prompt.rb +20 -11
  141. data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
  142. data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
  143. data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
  144. data/lib/samagotchi/terminal_ui.rb +3 -1
  145. data/lib/samagotchi/testing/bundle.rb +5 -12
  146. data/lib/samagotchi/testing/install.rb +5 -4
  147. data/lib/samagotchi/testing/recorder.rb +1 -1
  148. data/lib/samagotchi/testing.rb +1 -1
  149. data/lib/samagotchi/thinking.rb +16 -21
  150. data/lib/samagotchi/tool_declarations.rb +1 -1
  151. data/lib/samagotchi/tool_response.rb +5 -3
  152. data/lib/samagotchi/tool_runner.rb +18 -1
  153. data/lib/samagotchi/toolkit_binding.rb +1 -4
  154. data/lib/samagotchi/toolkit_command.rb +1 -4
  155. data/lib/samagotchi/tools/delegate.rb +60 -51
  156. data/lib/samagotchi/tools/delegate_relay.rb +5 -5
  157. data/lib/samagotchi/tools/delegate_result.rb +1 -1
  158. data/lib/samagotchi/tools/delegate_wait.rb +41 -17
  159. data/lib/samagotchi/tools/list_sessions.rb +7 -7
  160. data/lib/samagotchi/update_command.rb +11 -7
  161. data/lib/samagotchi/verify_fresh.rb +7 -11
  162. data/lib/samagotchi/version.rb +1 -1
  163. data/lib/samagotchi/vision_support.rb +11 -14
  164. data/lib/samagotchi/web/app.rb +66 -21
  165. data/lib/samagotchi/web/message_display.rb +57 -7
  166. data/lib/samagotchi/web/participant_access.rb +1 -1
  167. data/lib/samagotchi/web/project_list.rb +3 -2
  168. data/lib/samagotchi/web/public/activity.js +16 -7
  169. data/lib/samagotchi/web/public/app.js +29 -8
  170. data/lib/samagotchi/web/public/bubbles.js +8 -3
  171. data/lib/samagotchi/web/public/cards_place.js +15 -0
  172. data/lib/samagotchi/web/public/commands.js +5 -1
  173. data/lib/samagotchi/web/public/data.js +4 -2
  174. data/lib/samagotchi/web/public/history_shape.js +220 -0
  175. data/lib/samagotchi/web/public/index.html +6 -1
  176. data/lib/samagotchi/web/public/project_picker.js +86 -4
  177. data/lib/samagotchi/web/public/recap_stamp.js +17 -0
  178. data/lib/samagotchi/web/public/reload_check.js +71 -0
  179. data/lib/samagotchi/web/public/stage_model.js +7 -5
  180. data/lib/samagotchi/web/public/stage_view.js +7 -3
  181. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  182. data/lib/samagotchi/web/public/timing.js +43 -2
  183. data/lib/samagotchi/web/public/turn_block.js +215 -30
  184. data/lib/samagotchi/web/public/turn_drawing.js +43 -0
  185. data/lib/samagotchi/web/public/turn_events.js +10 -1
  186. data/lib/samagotchi/web/public/turn_pieces.js +29 -0
  187. data/lib/samagotchi/web/reload_check_route.rb +95 -0
  188. data/lib/samagotchi/web/server.rb +4 -2
  189. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  190. data/lib/samagotchi/web/session_control.rb +1 -1
  191. data/lib/samagotchi/web/session_hub.rb +9 -17
  192. data/lib/samagotchi/web/session_show.rb +64 -2
  193. data/lib/samagotchi/web/session_summary.rb +36 -24
  194. data/lib/samagotchi/worker.rb +28 -5
  195. data/lib/samagotchi/worker_link.rb +3 -0
  196. metadata +22 -5
  197. data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
  198. data/lib/samagotchi/send_command/wait_target.rb +0 -85
@@ -8,7 +8,6 @@ require "tmpdir"
8
8
  require_relative "../atomic_file"
9
9
  require_relative "../memory_paths"
10
10
  require_relative "../version"
11
- require_relative "bundle_hook"
12
11
  require_relative "file_digest"
13
12
  require_relative "installed_bundle"
14
13
  require_relative "manifest"
@@ -159,6 +158,16 @@ module Samagotchi
159
158
  unchecked?(recorded) || file_sha(path) == recorded_sha(recorded)
160
159
  end
161
160
 
161
+ # Whether a memory on disk is what the install wrote: the entry's
162
+ # recorded checksum, else its base copy. Unknown counts as edited. The
163
+ # one rule a prune, an uninstall's refusal and chi bundle status read.
164
+ def self.unchanged?(path, entry, base_path)
165
+ recorded = entry[:checksum]
166
+ return sha_matches?(path, recorded) unless recorded_sha(recorded).empty?
167
+
168
+ File.exist?(base_path) && file_sha(path) == file_sha(base_path)
169
+ end
170
+
162
171
  # The record's mirror of the manifest keys (InstalledBundle::MIRROR_KEYS)
163
172
  # as #write records them from its kwargs: what an older chi reads, and
164
173
  # what #adopt_manifest holds a source's manifest against.
@@ -185,12 +194,14 @@ module Samagotchi
185
194
  # the record. nil keeps an entry with no base (a kept edit that has
186
195
  # none: the user's file must never become it), with its recorded
187
196
  # checksum.
188
- # @param hooks_files [Hash] basename => path for hook file snapshots (optional)
189
197
  # @param guardrails_files [Hash] basename => installed rule file; the
190
198
  # sha256 of each is recorded (the Engine checks it at load)
191
199
  # @param plugin_file [String, nil] the installed plugin file; its name
192
200
  # and sha256 are recorded (the Engine checks it at load) with a base
193
201
  # snapshot
202
+ # @param plugin_required [Boolean] the manifest's plugin: required: true
203
+ # (a guardrail plugin), recorded with it as "required": true, so the
204
+ # Engine knows it whatever becomes of the kept manifest.yml
194
205
  # @param requires_chi [String, nil] the manifest's requirement
195
206
  # @param needs [Array<Hash>] the manifest's needs ({command:, why:, hint:})
196
207
  # @param marks [Array<FileStanding::Behind>] files an upgrade left behind;
@@ -216,8 +227,10 @@ module Samagotchi
216
227
  # (requires_chi … setting_types) are still written from the kwargs:
217
228
  # an older chi sharing this memories dir reads only them
218
229
  # (docs/internals/bundles.md); a key a later manifest gains is never added
219
- def write(files:, scope:, version:, source_path:, hooks: {}, trust_level: nil, source_commit: nil, hooks_files: {},
220
- guardrails_files: {}, plugin_file: nil, requires_chi: nil, needs: nil, marks: [], incoming_dir: nil, includes: nil,
230
+ # The record has no hooks ("hooks" => {}: class-file hooks are retired)
231
+ # and an older record's hook bases go.
232
+ def write(files:, scope:, version:, source_path:, trust_level: nil, source_commit: nil,
233
+ guardrails_files: {}, plugin_file: nil, plugin_required: false, requires_chi: nil, needs: nil, marks: [], incoming_dir: nil, includes: nil,
221
234
  scripts_files: {}, context_providers: [], block_renderers: [], file_descriptions: {}, session_settings: [], session_setting_types: {}, settings: nil, setting_types: {},
222
235
  dev: false, manifest_from: nil)
223
236
  FileUtils.mkdir_p(@bundle_dir)
@@ -262,40 +275,8 @@ module Samagotchi
262
275
  end
263
276
  end
264
277
 
265
- # Hooks provenance: persist metadata and base snapshots for hooks
266
- hooks_map = {}
267
- # Incoming hooks: basename => BundleHook (or its metadata hash)
268
- normalized_hooks = {}
269
- if hooks.is_a?(Hash)
270
- hooks.each do |k, v|
271
- next unless k.is_a?(String) && !k.empty?
272
- next unless v.is_a?(Hash) || v.is_a?(BundleHook)
273
-
274
- normalized_hooks[k] = BundleHook.parse(v)
275
- end
276
- end
277
- # Prune stale hook bases
278
- existing&.hooks&.each_key do |old_key_str|
279
- FileUtils.rm_f(File.join(bases_dir, old_key_str)) unless normalized_hooks.key?(old_key_str)
280
- end
281
- # Save base snapshots for hooks where a file path was provided, and
282
- # record the sha256 of the installed file itself: the load-time check
283
- # compares against it, and a declared sha can be wrong (installing
284
- # only warns about that).
285
- hooks_files = {} unless hooks_files.is_a?(Hash)
286
- normalized_hooks.each do |k, hook|
287
- src = hooks_files[k] || hooks_files[k.to_sym]
288
- hook = hook.with(sha256: BundleHook.of_file(src.to_s).sha256) if src && File.exist?(src.to_s)
289
- hook = hook.with(sha256: DEV_SHA) if dev
290
- hooks_map[k] = hook.to_record
291
- next unless src && File.exist?(src.to_s)
292
-
293
- begin
294
- File.write(File.join(bases_dir, k), File.read(src.to_s))
295
- rescue StandardError
296
- nil
297
- end
298
- end
278
+ # An older chi's hooks: their bases go with them (AssetInstaller#retire_hooks).
279
+ existing&.hooks&.each_key { |old_key_str| FileUtils.rm_f(File.join(bases_dir, old_key_str)) }
299
280
 
300
281
  manifest_data = {
301
282
  "name" => @name,
@@ -304,7 +285,7 @@ module Samagotchi
304
285
  "source" => source_path.to_s,
305
286
  "installed_at" => DateTime.now.iso8601,
306
287
  "files" => merged_entries,
307
- "hooks" => hooks_map
288
+ "hooks" => {}
308
289
  }
309
290
  manifest_data["dev"] = true if dev
310
291
  if guardrails_files.is_a?(Hash) && !guardrails_files.empty?
@@ -318,6 +299,7 @@ module Samagotchi
318
299
  content = File.binread(plugin_file.to_s) # the base snapshot's bytes
319
300
  manifest_data["plugin"] = { "file" => File.basename(plugin_file.to_s),
320
301
  "sha256" => dev ? DEV_SHA : FileDigest.sha256(plugin_file.to_s) }
302
+ manifest_data["plugin"]["required"] = true if plugin_required
321
303
  FileUtils.mkdir_p(File.join(bases_dir, "plugin"))
322
304
  File.binwrite(plugin_base_path(File.basename(plugin_file.to_s)), content)
323
305
  end
@@ -446,7 +428,7 @@ module Samagotchi
446
428
  # wrote it, or the file is gone), when +dir+ is the source of what's
447
429
  # installed: its manifest names this bundle at the recorded version and
448
430
  # says what the record keeps (#manifest_difference), and every sha the
449
- # record has for a memory, plugin, hook, script or rule is the source
431
+ # record has for a memory, plugin, script or rule is the source
450
432
  # file's. Then the file is copied to #kept_manifest_path and the
451
433
  # record gains its manifest_sha, and nothing else changes (installed_at,
452
434
  # the mirror, installed_by). A dev record is never adopted: its next
@@ -571,8 +553,8 @@ module Samagotchi
571
553
  # The first manifest key in which +manifest+ says other than what the
572
554
  # record keeps, nil when none: each mirrored key as #write records it (one
573
555
  # the record lacks is fine only when an older chi couldn't have known it:
574
- # Manifest::SINCE has it), then each recorded hook's events, on_error and
575
- # priority.
556
+ # Manifest::SINCE has it). An older record's hooks don't count: they
557
+ # no longer load, and the source can't list any (Manifest::HooksRetired).
576
558
  def manifest_difference(bundle, manifest)
577
559
  recorded = plain(bundle.mirror)
578
560
  source = plain(self.class.mirror_fields(
@@ -581,18 +563,11 @@ module Samagotchi
581
563
  session_settings: manifest.session_settings, session_setting_types: manifest.session_setting_types,
582
564
  settings: manifest.settings, setting_types: manifest.setting_types
583
565
  ))
584
- differing = InstalledBundle::MIRROR_KEYS.find do |key|
566
+ InstalledBundle::MIRROR_KEYS.find do |key|
585
567
  next false if recorded[key] == source[key]
586
568
 
587
569
  !(recorded[key].nil? && Manifest::SINCE.key?(MIRROR_SINCE.fetch(key, key)))
588
570
  end
589
- return differing if differing
590
-
591
- same_hooks = bundle.hooks.all? do |file, hook|
592
- theirs = manifest.hooks.fetch(file, BundleHook.new)
593
- [hook.events, hook.on_error, hook.priority] == [theirs.events, theirs.on_error, theirs.priority]
594
- end
595
- same_hooks ? nil : "hooks"
596
571
  end
597
572
  private :manifest_difference
598
573
 
@@ -600,7 +575,7 @@ module Samagotchi
600
575
  private :plain
601
576
 
602
577
  # Each file the record has a sha for, by its path in the bundle's source
603
- # => that sha: the plugin, hooks, scripts, rules, and each memory whose
578
+ # => that sha: the plugin, scripts, rules, and each memory whose
604
579
  # entry has a checksum and no conflict mark (its base is the bundle's text).
605
580
  def installed_shas(bundle, manifest)
606
581
  shas = bundle.files.each_with_object({}) do |(key, entry), acc|
@@ -610,7 +585,6 @@ module Samagotchi
610
585
  file = manifest.plugin && manifest.plugin[:file].to_s
611
586
  shas[file && File.basename(file) == bundle.plugin.file ? file : bundle.plugin.file] = bundle.plugin.sha256
612
587
  end
613
- bundle.hooks.each { |file, hook| shas["hooks/#{file}"] = hook.sha256 }
614
588
  bundle.scripts.each { |file, sha| shas["scripts/#{file}"] = sha }
615
589
  bundle.guardrails.each { |file, sha| shas["guardrails/#{file}"] = sha }
616
590
  shas
@@ -3,6 +3,8 @@
3
3
  require_relative "index_updater"
4
4
  require_relative "manifest"
5
5
  require_relative "memory_files"
6
+ require_relative "file_actions"
7
+ require_relative "file_digest"
6
8
  require_relative "merger"
7
9
  require_relative "provenance"
8
10
  require_relative "trash"
@@ -17,7 +19,8 @@ module Samagotchi
17
19
  # scripts, and GuardrailWiring#rules stats per tool call, so a live session
18
20
  # could hit a transient deny.
19
21
  #
20
- # Each marked file, by what Merger::Upgrade says of it:
22
+ # Each marked file, by its FileState and REMERGE row (FileActions; a
23
+ # :decide cell by what Merger::Upgrade says of it):
21
24
  # - not shipped now: its mark, base and text stay, and it reads as behind
22
25
  # unless level or missing, as every report has it;
23
26
  # - the shipped text isn't the one the mark names (a checkout at the same
@@ -32,7 +35,7 @@ module Samagotchi
32
35
  # to miss, so the mark goes;
33
36
  # - conflict: behind, and the mark records why, only when that changed;
34
37
  # - fast_forward (one Merger::Upgrade#outcome lets go ahead): the next
35
- # upgrade's (save_marked_file), behind until then;
38
+ # upgrade's (its save_first), behind until then;
36
39
  # - install: a missing file, which isn't behind (FileStanding.behind?): the
37
40
  # next upgrade or a restore puts it back, and a warning says so;
38
41
  # - one it can't read or replace: left as it is, behind, saying why.
@@ -75,7 +78,8 @@ module Samagotchi
75
78
  @behind = []
76
79
  @warnings = []
77
80
  @trash = Trash.new(@name)
78
- shipped = MemoryFiles.select(@source_dir, Manifest.read(dir: @source_dir))
81
+ @manifest = Manifest.read(dir: @source_dir)
82
+ shipped = MemoryFiles.select(@source_dir, @manifest)
79
83
  marked.each { |key, entry| retry_file(key, entry, shipped) }
80
84
  settle unless dry_run
81
85
  Result.new(merged: @merged, settled: @settled, stamped: @stamped.map(&:file), behind: @behind, warnings: @warnings, trash_copy: @trash.copy_line)
@@ -90,24 +94,35 @@ module Samagotchi
90
94
  return
91
95
  end
92
96
 
93
- incoming_sha = Merger.digest(view.paths(key)[:incoming])
94
- if entry[:incoming_sha] && entry[:incoming_sha] != incoming_sha
97
+ state = view.state(key)
98
+ if state.source_moved?
95
99
  @behind << @record.behind_entry(key, entry) if behind
96
100
  return
97
101
  end
98
102
 
99
- outcome = view.outcome(key)
100
- case outcome.status
101
- when :merged then merge_file(key, entry, outcome)
102
- when :conflict then mark(key, entry, view.behind(key, outcome, @record.version))
103
- when :noop, :keep then @settled << key
104
- when :fast_forward then mark(key, entry, behind_for(key, FileStanding::WAITS_FOR_UPGRADE))
105
- when :install then @warnings << "#{key}: #{FileStanding::FILE_MISSING}"
106
- end
103
+ retry_row(key, entry, state, FileActions.for(:remerge, state))
107
104
  rescue SystemCallError => e
108
105
  stay(key, FileStanding.left_as_it_is(e.message))
109
106
  end
110
107
 
108
+ # The file's REMERGE row, done. A :decide cell, and a behind one, asks
109
+ # Merger::Upgrade#decision for the Outcome (the text merge, the legacy
110
+ # guard, or why no merge is possible); a fast-forward waits for the
111
+ # upgrade. A missing file is put back by the next upgrade or a restore;
112
+ # one it can't read raises here (left as it is, saying why).
113
+ def retry_row(key, entry, state, action)
114
+ decision = view.decision(key, state) if action.do == :decide || action.behind?
115
+ action = FileActions.decided(decision.outcome, :remerge) if action.do == :decide
116
+ if action.do == :merge then merge_file(key, entry, state, decision.outcome)
117
+ elsif action.behind?
118
+ waits = decision.outcome.status == :fast_forward
119
+ mark(key, entry, waits ? behind_for(key, FileStanding::WAITS_FOR_UPGRADE) : view.behind(key, decision.outcome, @record.version))
120
+ elsif action.word == :settled then @settled << key
121
+ elsif state.kind == :missing then @warnings << "#{key}: #{FileStanding::FILE_MISSING}"
122
+ elsif state.kind == :unreadable then FileDigest.hex(state.paths[:current])
123
+ end
124
+ end
125
+
111
126
  def behind_for(key, why) = FileStanding::Behind.new(file: key, version: @record.version, why: why)
112
127
 
113
128
  # The record says it too, so chi bundle status reads as the row does. There
@@ -128,7 +143,9 @@ module Samagotchi
128
143
  @stamped << behind unless entry[:incoming_sha] && @record.behind_entry(key, entry) == behind && File.exist?(@provenance.incoming_path(key))
129
144
  end
130
145
 
131
- def merge_file(key, entry, outcome)
146
+ def merge_file(key, entry, state, outcome)
147
+ warning = FileDigest.mismatch(key, state.paths[:incoming], @manifest.checksum_for(key))
148
+ @warnings << warning if warning
132
149
  return @merged << key if @dry_run
133
150
  return @merged << key if Merger.write_merged(view.paths(key)[:current], outcome, @trash)
134
151
 
@@ -14,7 +14,7 @@ module Samagotchi
14
14
 
15
15
  # A sha line that doesn't match its file: +old+ is the hex the line
16
16
  # holds ("" for a placeholder `sha256: sha256:`), nil when the entry
17
- # has no sha line at all (a plugin:, hooks: or scripts: entry the
17
+ # has no sha line at all (a plugin: or scripts: entry the
18
18
  # installer then takes unchecked; .refresh can't write one).
19
19
  Stale = Data.define(:file, :old, :new)
20
20
 
@@ -30,7 +30,6 @@ module Samagotchi
30
30
  # names, the layout otherwise untouched. The shapes:
31
31
  # files: a.md: sha256:<hex> (dir/a.md)
32
32
  # a.md:\n sha256: sha256:<hex> (dir/a.md, with description:)
33
- # hooks: h.rb:\n sha256: sha256:<hex> (dir/hooks/h.rb)
34
33
  # plugin: file: p.rb\n sha256: sha256:<hex> (dir/p.rb)
35
34
  # scripts: s.rb: sha256:<hex> (dir/scripts/s.rb)
36
35
  # Returns [text, [[relative file, old sha, new sha], ...]] for the lines
@@ -43,8 +42,9 @@ module Samagotchi
43
42
  [text, changes]
44
43
  end
45
44
 
46
- # The stale sha lines of the bundle at +dir+, then the plugin:, hooks:
47
- # and scripts: entries with no sha line, as [Stale].
45
+ # The stale sha lines of the bundle at +dir+, then the plugin: and
46
+ # scripts: entries with no sha line, as [Stale]. (A hooks: key, which
47
+ # chi refuses, has no sha lines here.)
48
48
  def stale(dir, text = File.read(File.join(dir, "manifest.yml")))
49
49
  _, changes, unlined = scan(dir, text)
50
50
  changes.map { |file, old, new| Stale.new(file, old, new) } +
@@ -86,11 +86,10 @@ module Samagotchi
86
86
  # take a placeholder (`chi bundle check`) parse this text.
87
87
  def parseable(text) = text.gsub(/^(\s*(?:["']?[^"':\s][^"':]*["']?: )?)sha256:(?=[ \t]*(?:#[^\r\n]*)?\r?$)/, "\\1sha256:0")
88
88
 
89
- # The code files the manifest names (plugin, hooks, scripts), as paths
89
+ # The code files the manifest names (plugin, scripts), as paths
90
90
  # relative to the bundle dir.
91
91
  def coded(parsed, plugin_file)
92
92
  (plugin_file.is_a?(String) ? [plugin_file] : []) +
93
- keys(parsed["hooks"]).map { |h| File.join("hooks", h) } +
94
93
  keys(parsed["scripts"]).map { |s| File.join("scripts", s) }
95
94
  end
96
95
  private_class_method :coded
@@ -114,8 +113,6 @@ module Samagotchi
114
113
  when "files"
115
114
  m = line.match(/\A( sha256: )sha256:\h*#{TAIL}/) and return sub && [sub.delete("\"'"), m[1]]
116
115
  m = line.match(/\A( ["']?([^"':]+)["']?: )sha256:\h*#{TAIL}/) and [m[2], m[1]]
117
- when "hooks"
118
- m = line.match(/\A( sha256: )sha256:\h*#{TAIL}/) and sub and [File.join("hooks", sub.delete("\"'")), m[1]]
119
116
  when "plugin"
120
117
  m = line.match(/\A( sha256: )sha256:\h*#{TAIL}/) and plugin_file and [plugin_file, m[1]]
121
118
  when "scripts"
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "asset_installer"
3
4
  require_relative "installer"
4
5
  require_relative "listing"
5
6
  require_relative "manifest"
@@ -11,6 +12,7 @@ require_relative "remerge"
11
12
  require_relative "source"
12
13
  require_relative "status"
13
14
  require_relative "system_bundle"
15
+ require_relative "trash"
14
16
  require_relative "../memory_paths"
15
17
  require_relative "../version"
16
18
 
@@ -25,9 +27,11 @@ module Samagotchi
25
27
  # bundle that isn't installed and never downgrades.
26
28
  #
27
29
  # .md files take the Installer's 3-way merge: a clean text merge replaces
28
- # the file (merged), a real conflict keeps the user's (behind). Hooks, rules and the plugin are replaced; an
29
- # edited one (which didn't load: its sha no longer matched) is listed
30
- # in replaced.
30
+ # 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 edited one (Trash.edit?: its
32
+ # bytes neither the recorded sha, none recorded counting as edited, nor
33
+ # the incoming file's) is copied to the trash first and listed in
34
+ # replaced (#trash_bound).
31
35
  #
32
36
  # A profile (a shipped meta bundle) is updated through Profile: its row
33
37
  # is :would_update when the shipped version is newer or it has members
@@ -41,8 +45,9 @@ module Samagotchi
41
45
  # a project-scope bundle: chi bundle upgrade in the project.
42
46
  module ShippedUpdate
43
47
  # status: :would_update, :would_merge (plan), :updated, :merged,
44
- # :up_to_date, :skipped, :failed. note says why for skipped/failed, and
45
- # what a retry at the same version warned of. merged and replaced are file
48
+ # :up_to_date, :skipped, :failed. note says why for skipped/failed,
49
+ # what a retry at the same version warned of, and an update's Checksum
50
+ # mismatch warnings. merged and replaced are file
46
51
  # names, behind FileStanding::Behind; under plan they are what apply would
47
52
  # merge, replace or leave behind.
48
53
  # adds is nil but for a profile: the members it installs.
@@ -114,6 +119,7 @@ module Samagotchi
114
119
  r.merged = installer.merged_files
115
120
  r.behind = installer.behind
116
121
  r.trash_copy = installer.trash_copy_line
122
+ r.note = mismatch_note(installer)
117
123
  end
118
124
  rescue StandardError => e
119
125
  row.dup.tap do |r|
@@ -193,14 +199,19 @@ module Samagotchi
193
199
  if Manifest.requires_chi_failure(manifest.requires_chi, chi_version)
194
200
  return skip(row, "needs chi #{manifest.requires_chi}", keep_to: true)
195
201
  end
202
+
203
+ # The upgrade would refuse it (Installer#refuse_hooks).
204
+ hooks = Manifest::HooksRetired.files_in(row.source_dir)
205
+ return skip(row, Manifest::HooksRetired.for_files(hooks).message, keep_to: true) unless hooks.empty?
196
206
  return skip(row, "project scope: chi bundle upgrade #{ship.source} in the project") if row.scope == "project"
197
207
  return skip(row, "unknown scope #{row.scope}: upgrade chi or reinstall") unless MemoryPaths.scope_dir(row.scope)
198
208
 
199
209
  forecast = forecast(row, bundle)
200
210
  row.merged = forecast.merged
201
211
  row.behind = forecast.behind
202
- row.replaced = edited_executables(name, bundle)
212
+ row.replaced = trash_bound(name, bundle, manifest, row.source_dir)
203
213
  row.status = :would_update
214
+ row.note = planned_mismatch_note(row)
204
215
  row
205
216
  rescue SystemCallError => e
206
217
  # A file it can't read: the upgrade would fail on it, so this is that
@@ -233,6 +244,20 @@ module Samagotchi
233
244
 
234
245
  def note_of(remerged) = remerged.warnings.empty? ? nil : remerged.warnings.join("; ")
235
246
 
247
+ def mismatch_note(installer)
248
+ mismatches = installer.warnings.grep(/\AChecksum mismatch/)
249
+ mismatches.join("; ") unless mismatches.empty?
250
+ end
251
+
252
+ def planned_mismatch_note(row)
253
+ installer = Installer.new(source: row.source_dir, name: row.name, scope: row.scope, strict: true, upgrade: true, dry_run: true,
254
+ shipped_dir: File.dirname(row.source_dir))
255
+ installer.run
256
+ mismatch_note(installer)
257
+ rescue Installer::InstallError
258
+ nil
259
+ end
260
+
236
261
  def profile_row(row, bundle, shipped_dir, chi_version)
237
262
  manifest = Manifest.read(dir: row.source_dir)
238
263
  newer = Listing.newer?(row.to, row.from)
@@ -267,14 +292,29 @@ module Samagotchi
267
292
  source_dir: row.source_dir).forecast(shipped & bundle.files.keys, version: row.to)
268
293
  end
269
294
 
270
- # Installed hooks, rules and the plugin whose sha differs from the
271
- # recorded one (they didn't load), as hooks/F, guardrails/F, plugin/F.
295
+ # What the upgrade from +source_dir+ copies to the trash, as
296
+ # hooks/F, guardrails/F, plugin/F, scripts/F: Trash.edits over the
297
+ # folders AssetInstaller replaces or removes, each file against the one
298
+ # it puts in its place. Rules, the plugin and scripts: their whole
299
+ # folder, which the upgrade replaces. hooks/: an older chi's, which the
300
+ # upgrade removes whole (AssetInstaller#retire_hooks; no incoming).
301
+ def trash_bound(name, bundle, manifest, source_dir)
302
+ provenance = Provenance.new(name: name)
303
+ rules = Dir.glob(File.join(source_dir, "guardrails", "*.yml")).sort
304
+ .reject { |file| AssetInstaller.linked?(file, source_dir) }
305
+ plugin = manifest.plugin ? { manifest.plugin[:file] => File.join(source_dir, manifest.plugin[:file]) } : {}
306
+ scripts = manifest.scripts.keys.to_h { |file| [file, File.join(source_dir, "scripts", file)] }
307
+ Trash.edits(provenance.hooks_dir, "hooks", bundle.hooks.transform_values(&:sha256)) +
308
+ Trash.edits(provenance.guardrails_dir, "guardrails", bundle.guardrails, rules.to_h { |file| [File.basename(file), file] }) +
309
+ Trash.edits(provenance.plugin_dir, "plugin", bundle.plugin_shas, plugin) +
310
+ Trash.edits(provenance.scripts_dir, "scripts", bundle.scripts, scripts)
311
+ end
312
+
313
+ # Installed rules and the plugin whose sha differs from the recorded
314
+ # one (they didn't load), as guardrails/F, plugin/F.
272
315
  def edited_executables(name, bundle)
273
316
  provenance = Provenance.new(name: name)
274
317
  edited = []
275
- bundle.hooks.each do |file, hook|
276
- edited << "hooks/#{file}" if edited?(File.join(provenance.hooks_dir, file), hook.sha256)
277
- end
278
318
  bundle.guardrails.each do |file, sha|
279
319
  edited << "guardrails/#{file}" if edited?(File.join(provenance.guardrails_dir, file), sha)
280
320
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "file_digest"
4
4
  require_relative "provenance"
5
+ require_relative "file_state"
5
6
  require_relative "manifest"
6
7
  require_relative "../version"
7
8
  require_relative "index_updater"
@@ -15,14 +16,15 @@ module Samagotchi
15
16
  # A bundle as this chi reads it now (what chi bundle status prints):
16
17
  # its stored record, the scope it resolves to (scope_error when it
17
18
  # can't), the folder its files belong in, what each file looks like on
18
- # disk, its installed plugin, why none of its hooks load, and its needs
19
+ # disk, its installed plugin, the class-file hooks its record still
20
+ # has (retired_hooks: they don't load), and its needs
19
21
  # with what this process's PATH found. The +files+ entries and +plugin+
20
22
  # stay Hashes: their fields differ per case (an unchecked file has no
21
23
  # paths at all). An entry's behind is its FileStanding::Behind, nil for a
22
24
  # file that isn't; merge_note its FileStanding::MergeNote, nil without one;
23
25
  # unreadable: the file is there, but this chi can't read it.
24
26
  Report = Data.define(:provenance, :scope, :scope_error, :target_dir, :files, :plugin,
25
- :hooks_requires_failure, :needs)
27
+ :retired_hooks, :needs)
26
28
 
27
29
  # A scope this chi can't resolve (a newer chi's, a hand edit) isn't
28
30
  # guessed: scope_error says so, target_dir is nil and each file is
@@ -41,16 +43,12 @@ module Samagotchi
41
43
  files.each do |file_key_str, meta|
42
44
  next details[file_key_str] = unchecked_file(meta) unless target_dir
43
45
 
44
- target_path = File.join(target_dir, file_key_str)
45
- base_path = provenance.base_path(file_key_str)
46
- stored_checksum = meta[:checksum]
47
- current_checksum = checksum(target_path)
48
- unreadable = current_checksum == :unreadable
49
- current_checksum = nil if unreadable
50
- base_checksum = checksum(base_path)
51
- base_checksum = nil if base_checksum == :unreadable
52
- modified = base_checksum && current_checksum && base_checksum != current_checksum
53
- missing = !File.exist?(target_path)
46
+ state = FileState.of(file_key_str, record: bundle, provenance: provenance, target_dir: target_dir, source_dir: nil)
47
+ target_path = state.paths[:current]
48
+ base_path = state.paths[:base]
49
+ unreadable = state.kind == :unreadable
50
+ missing = state.kind == :missing
51
+ modified = %i[edited edited_no_base].include?(state.kind)
54
52
  # A model overlay has no index line (Installer#note_overlay).
55
53
  overlay = ModelOverlay.bundle_overlay?(file_key_str, bundle_files: files.keys, target_dir: target_dir)
56
54
  index_present = overlay ? nil : index_has_entry?(scope, file_key_str)
@@ -59,9 +57,9 @@ module Samagotchi
59
57
  behind: behind[file_key_str],
60
58
  merge_note: notes[file_key_str],
61
59
  unreadable: unreadable,
62
- stored_checksum: stored_checksum,
63
- current_checksum: current_checksum,
64
- base_checksum: base_checksum,
60
+ stored_checksum: meta[:checksum],
61
+ current_checksum: digest_or_nil(state.digests.disk),
62
+ base_checksum: digest_or_nil(state.digests.base),
65
63
  modified: modified,
66
64
  missing: missing,
67
65
  index_present: index_present,
@@ -72,24 +70,22 @@ module Samagotchi
72
70
  end
73
71
  Report.new(provenance: bundle, scope: scope, scope_error: target_dir ? nil : "unknown scope: #{scope}",
74
72
  target_dir: target_dir, files: details, plugin: plugin_status(provenance, bundle),
75
- hooks_requires_failure: hooks_requires_failure(bundle), needs: needs_status(bundle))
73
+ retired_hooks: retired_hooks(provenance, bundle), needs: needs_status(bundle))
76
74
  end
77
75
 
78
- # The file's digest, nil when there is none, :unreadable when it can't be read.
79
- def self.checksum(path)
80
- FileDigest.hex(path)
81
- rescue SystemCallError
82
- :unreadable
83
- end
84
- private_class_method :checksum
85
-
86
- # Why none of the bundle's hooks load (this chi doesn't meet its
87
- # requires_chi, Hooks::BundleLoader), or nil: it has no hooks or meets it.
88
- def self.hooks_requires_failure(bundle)
89
- manifest = bundle.manifest
90
- return nil if manifest.hooks.empty?
76
+ # A FileState digest as the Report has it: nil for none, or one that
77
+ # can't be read (unreadable says which).
78
+ def self.digest_or_nil(digest) = digest == :unreadable ? nil : digest
79
+ private_class_method :digest_or_nil
91
80
 
92
- manifest.requires_chi_failure
81
+ # The class-file hooks the record still has (a 0.157 install's; chi
82
+ # loads none, Manifest::HooksRetired), each "ok" or "missing" in
83
+ # hooks/, by name; {} for none.
84
+ # @return [Hash{String => String}]
85
+ def self.retired_hooks(provenance, bundle)
86
+ bundle.hooks.keys.sort.to_h do |file|
87
+ [file, File.exist?(File.join(provenance.hooks_dir, file)) ? "ok" : "missing"]
88
+ end
93
89
  end
94
90
 
95
91
  # The stored needs, each with found: from this process's PATH (the
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "fileutils"
4
4
  require_relative "installer"
5
+ require_relative "file_digest"
5
6
  require_relative "provenance"
6
7
  require_relative "manifest"
7
8
  require_relative "index_updater"
@@ -204,29 +205,54 @@ module Samagotchi
204
205
  # A file from the bundle missing on disk (the user deleted it) is put
205
206
  # back, and nothing else: at the same version a checkout can carry other
206
207
  # text, so no other file is merged here (Remerge has its guard), and a live
207
- # session's guardrails, plugin and scripts stay where they are.
208
+ # session's guardrails, plugin and scripts stay where they are. Which
209
+ # missing file is put back is its RESTORE row (FileActions): one the
210
+ # record doesn't own too. A put-back file is checked against the
211
+ # manifest's files: sha, as an installed one is (a bad copy warns).
208
212
  def verify_files_present(gem_manifest, version, dry_run)
209
213
  target_dir = MemoryPaths.system_dir
210
214
  missing = MemoryFiles.select(GEM_BUNDLE_DIR, gem_manifest).reject { |key| File.exist?(File.join(target_dir, key)) }
215
+ missing = restored_rows(missing, target_dir) unless missing.empty?
211
216
  return result(:up_to_date, from: version, to: version) if missing.empty?
212
- return result(:restored, from: version, to: version) if dry_run
213
217
 
214
- warnings = missing.filter_map { |key| restore_file(key, gem_manifest.file_descriptions[key]) }
218
+ if dry_run
219
+ warnings = missing.filter_map { |key| restore_mismatch(key, gem_manifest) }
220
+ return result(:restored, from: version, to: version, warnings: warnings)
221
+ end
222
+
223
+ warnings = missing.flat_map { |key| restore_file(key, gem_manifest) }
215
224
  result(:restored, from: version, to: version, warnings: warnings)
216
225
  end
217
226
 
218
- # @return [String, nil] a warning: the file is in place, its index line isn't
219
- def restore_file(key, description)
227
+ # The ones among +keys+ whose FileState's RESTORE row restores.
228
+ def restored_rows(keys, target_dir)
229
+ provenance = Provenance.new(name: BUNDLE_NAME)
230
+ record = provenance.record
231
+ keys.select do |key|
232
+ state = FileState.of(key, record: record, provenance: provenance, target_dir: target_dir, source_dir: GEM_BUNDLE_DIR)
233
+ FileActions.for(:restore, state)&.do == :restore
234
+ end
235
+ end
236
+
237
+ # @return [Array<String>] warnings: its bytes don't match the manifest's
238
+ # sha, or the file is in place and its index line isn't
239
+ def restore_file(key, gem_manifest)
220
240
  source = File.join(GEM_BUNDLE_DIR, key)
221
241
  target = File.join(MemoryPaths.system_dir, key)
242
+ description = gem_manifest.file_descriptions[key]
222
243
  FileUtils.cp(source, target)
223
244
  Provenance.new(name: BUNDLE_NAME).record_restored(key, source, description: description)
245
+ warnings = [restore_mismatch(key, gem_manifest)].compact
224
246
  begin
225
247
  IndexUpdater.update_index(SCOPE, key.delete_suffix(".md"), File.size(target), description, source: BUNDLE_NAME)
226
- nil
227
248
  rescue StandardError => e
228
- "index.md: line for #{key.delete_suffix(".md")} not updated (#{e.message})"
249
+ warnings << "index.md: line for #{key.delete_suffix(".md")} not updated (#{e.message})"
229
250
  end
251
+ warnings
252
+ end
253
+
254
+ def restore_mismatch(key, gem_manifest)
255
+ FileDigest.mismatch(key, File.join(GEM_BUNDLE_DIR, key), gem_manifest.checksum_for(key))
230
256
  end
231
257
 
232
258
  # An up-to-date bundle's marked files, retried. No marks: nothing is read.
@@ -246,7 +272,10 @@ module Samagotchi
246
272
  owned = provenance.record&.files&.keys || []
247
273
  forecast = Merger::Upgrade.new(provenance: provenance, target_dir: MemoryPaths.system_dir, source_dir: GEM_BUNDLE_DIR)
248
274
  .forecast(gem_manifest.files.keys.map(&:to_s).sort & owned, version: to)
249
- result(:updated, from: from, to: to, merged: forecast.merged, behind: forecast.behind)
275
+ installer = upgrade_installer(dry_run: true)
276
+ installer.run
277
+ warnings = installer.warnings.grep(/\AChecksum mismatch/)
278
+ result(:updated, from: from, to: to, merged: forecast.merged, behind: forecast.behind, warnings: warnings)
250
279
  end
251
280
 
252
281
  # Provenance#adopt_manifest from the shipped dir, for a record without a
@@ -258,9 +287,9 @@ module Samagotchi
258
287
  Provenance.new(name: BUNDLE_NAME).adopt_manifest(GEM_BUNDLE_DIR)
259
288
  end
260
289
 
261
- def upgrade_installer
290
+ def upgrade_installer(dry_run: false)
262
291
  Installer.new(source: GEM_BUNDLE_DIR, name: BUNDLE_NAME, scope: SCOPE, force: false, strict: true, upgrade: true,
263
- shipped_dir: File.dirname(GEM_BUNDLE_DIR))
292
+ dry_run: dry_run, shipped_dir: File.dirname(GEM_BUNDLE_DIR))
264
293
  end
265
294
  private_class_method :with_lock, :note_newer_installed, :install_fresh, :keep_manifest, :upgrade_existing, :verify_files_present, :restore_file, :remerge,
266
295
  :upgrade_installer, :result, :planned_update