samagotchi 0.156.0 → 0.157.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 +44 -1
- data/bin/chi +1 -0
- data/docs/architecture.md +12 -7
- data/docs/bundles.md +3 -1
- data/docs/cli.md +9 -8
- data/docs/configuration.md +36 -4
- data/docs/guardrails.md +9 -5
- data/docs/hooks.md +11 -5
- data/docs/internals/bridge.md +83 -6
- data/docs/internals/bundles.md +28 -18
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +16 -2
- 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 +13 -2
- data/docs/plugins.md +35 -2
- data/docs/sessions.md +9 -9
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +53 -2
- 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 +10 -4
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
- data/lib/samagotchi/bridge/turn_drawings.rb +265 -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 +18 -0
- 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 +5 -2
- 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/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.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_sources.rb +5 -4
- 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 +29 -3
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +6 -3
- data/lib/samagotchi/guardrails_command.rb +1 -1
- 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 +71 -76
- data/lib/samagotchi/memory_bundle/builder.rb +2 -1
- 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_copy.rb +2 -1
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
- data/lib/samagotchi/memory_bundle/installer.rb +185 -169
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- 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 +15 -1
- data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
- data/lib/samagotchi/memory_bundle/status.rb +14 -20
- data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
- data/lib/samagotchi/memory_bundle/trash.rb +23 -8
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- 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 +41 -6
- data/lib/samagotchi/plugin/loader.rb +33 -10
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +27 -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/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 +321 -439
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +133 -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_cut_after.rb +7 -14
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/install.rb +2 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/tools/delegate.rb +48 -43
- 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 +31 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- 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/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 +20 -4
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -34,6 +34,19 @@ 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 hook, rule, plugin or
|
|
38
|
+
# script: AssetInstaller writes them too), as #results holds it. word is
|
|
39
|
+
# the report bucket (#summary): a real run's (:installed, :updated,
|
|
40
|
+
# :merged, :kept, :kept_pruned, :kept_shared, :kept_own (a file left as
|
|
41
|
+
# the user's), :up_to_date, :conflict, :removed) or a dry run's
|
|
42
|
+
# (:would_install, :would_update, :would_merge, :would_skip, :would_remove,
|
|
43
|
+
# and an upgrade's :fast_forward and :keep). state and action: the
|
|
44
|
+
# memory's FileState and FileActions::Action, nil where none was read.
|
|
45
|
+
# note: why, in words (a conflict's Behind#state, a kept file's reason).
|
|
46
|
+
FileResult = Data.define(:word, :state, :action, :note) do
|
|
47
|
+
def initialize(word:, state: nil, action: nil, note: nil) = super
|
|
48
|
+
end
|
|
49
|
+
|
|
37
50
|
attr_reader :results, :warnings, :placeholder_warnings, :conflicts
|
|
38
51
|
|
|
39
52
|
# Each file this run left behind (FileStanding::Behind).
|
|
@@ -65,7 +78,7 @@ module Samagotchi
|
|
|
65
78
|
@conflicts = {}
|
|
66
79
|
@behind = []
|
|
67
80
|
@replaced = []
|
|
68
|
-
@
|
|
81
|
+
@actions = {}
|
|
69
82
|
@overlays = []
|
|
70
83
|
@trash = Trash.new(name)
|
|
71
84
|
end
|
|
@@ -82,7 +95,7 @@ module Samagotchi
|
|
|
82
95
|
def trash_copy_line = @trash.copy_line
|
|
83
96
|
|
|
84
97
|
# The files this run merged, the bundle's changes joined to the user's.
|
|
85
|
-
def merged_files =
|
|
98
|
+
def merged_files = with_word(:merged)
|
|
86
99
|
|
|
87
100
|
# The normalized source dir. An owned one (a git/zip/tar extract) is
|
|
88
101
|
# removed after #run, or kept until #cleanup_source! when an upgrade
|
|
@@ -140,28 +153,26 @@ module Samagotchi
|
|
|
140
153
|
def summary
|
|
141
154
|
lines = []
|
|
142
155
|
lines << "Source changes: #{@source_change.join(" → ")}" if @source_change
|
|
143
|
-
installed =
|
|
144
|
-
updated =
|
|
156
|
+
installed = with_word(:installed)
|
|
157
|
+
updated = with_word(:updated)
|
|
145
158
|
merged = merged_files
|
|
146
|
-
would_merge =
|
|
147
|
-
kept =
|
|
159
|
+
would_merge = with_word(:would_merge)
|
|
160
|
+
kept = with_word(:kept, :keep, :kept_pruned)
|
|
148
161
|
# A file whose bytes are already the bundle's was not left out: it is
|
|
149
162
|
# 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
|
-
fast_forward = @results.select { |_, r| r[:status].to_s == "fast_forward" }.keys
|
|
159
|
-
removed = @results.select { |_, r| r[:status].to_s == "removed" }.keys
|
|
160
|
-
would_remove = @results.select { |_, r| r[:status].to_s == "would_remove" }.keys
|
|
163
|
+
# memory, and a dry run's would_skip.
|
|
164
|
+
up_to_date = with_word(:up_to_date)
|
|
165
|
+
skipped = with_word(:would_skip, :kept_own)
|
|
166
|
+
would_install = with_word(:would_install)
|
|
167
|
+
would_update = with_word(:would_update)
|
|
168
|
+
fast_forward = with_word(:fast_forward)
|
|
169
|
+
removed = with_word(:removed)
|
|
170
|
+
would_remove = with_word(:would_remove)
|
|
161
171
|
|
|
162
172
|
lines << "Installed: #{installed.join(", ")}" unless installed.empty?
|
|
163
173
|
lines << "Updated: #{updated.join(", ")}" unless updated.empty?
|
|
164
174
|
lines << "Would install: #{would_install.join(", ")}" unless would_install.empty?
|
|
175
|
+
lines << "Would update: #{would_update.join(", ")}" unless would_update.empty?
|
|
165
176
|
lines << "Fast-forward: #{fast_forward.join(", ")}" unless fast_forward.empty?
|
|
166
177
|
lines << FileStanding.merged_line(merged, @manifest&.version) unless merged.empty?
|
|
167
178
|
lines << FileStanding.merged_line(would_merge, @manifest&.version, dry_run: true) unless would_merge.empty?
|
|
@@ -198,6 +209,9 @@ module Samagotchi
|
|
|
198
209
|
|
|
199
210
|
private
|
|
200
211
|
|
|
212
|
+
# The files whose result has one of +words+, in the order they ran.
|
|
213
|
+
def with_word(*words) = @results.select { |_, result| words.include?(result.word) }.keys
|
|
214
|
+
|
|
201
215
|
# A plain install over a bundle that is already installed from the same
|
|
202
216
|
# source: it rewrote the bundle's hooks, rules, plugin, scripts and its
|
|
203
217
|
# record, and left a memory the user wrote as it was. So the line says
|
|
@@ -205,9 +219,9 @@ module Samagotchi
|
|
|
205
219
|
def reinstall_line
|
|
206
220
|
line = "#{@name} was already installed from #{@source}: its hooks, rules, plugin and scripts " \
|
|
207
221
|
"now match it and its record was rewritten"
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
if @results.any? { |_, r| r
|
|
222
|
+
# :kept_own is the only way a memory is left out of a plain install
|
|
223
|
+
# (its KEPT_OWN or HELD row): its bytes differ from the bundle's.
|
|
224
|
+
if @results.any? { |_, r| r.word == :kept_own }
|
|
211
225
|
line << "; your own copies of its memories were kept (chi bundle upgrade #{@name} " \
|
|
212
226
|
"merges the bundle's changes into them)"
|
|
213
227
|
end
|
|
@@ -374,47 +388,38 @@ module Samagotchi
|
|
|
374
388
|
|
|
375
389
|
# Copies the bundle's memories (MemoryFiles: the files: list, else
|
|
376
390
|
# every *.md but README and the like) into the target dir and refreshes
|
|
377
|
-
# their index lines
|
|
378
|
-
#
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
#
|
|
391
|
+
# their index lines. Each file's FileState and its command's row
|
|
392
|
+
# (FileActions: INSTALL, FORCE, UPGRADE) say what happens: a dry run
|
|
393
|
+
# only reads the row (preview_memory), a real run does it
|
|
394
|
+
# (write_memory). Sets @bundle_md (every file the bundle ships) and
|
|
395
|
+
# @owned: the bundle owns (records, upgrades, removes) only the files it
|
|
396
|
+
# wrote, these from an earlier install and the ones written now. A
|
|
397
|
+
# same-name file that was already there is the user's: skipped and never
|
|
398
|
+
# recorded (the not-owned rows).
|
|
383
399
|
def install_memories
|
|
384
|
-
owned_before = @existing ? @existing.files.keys : []
|
|
385
400
|
@bundle_md = []
|
|
386
401
|
@owned = []
|
|
387
402
|
warn_unshipped_files
|
|
388
403
|
warn_symlinked_files
|
|
389
404
|
memory_files.each do |file_key|
|
|
390
|
-
file_path = File.join(@bundle_dir, file_key)
|
|
391
405
|
@bundle_md << file_key
|
|
392
406
|
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
|
|
407
|
+
state = FileState.of(file_key, record: @existing, provenance: @provenance, target_dir: @target_dir,
|
|
408
|
+
source_dir: @bundle_dir)
|
|
409
|
+
action = FileActions.for(memory_command, state)
|
|
410
|
+
next preview_memory(file_key, state, action) if @dry_run
|
|
411
|
+
|
|
412
|
+
@owned << file_key unless action.record == :not_owned
|
|
413
|
+
write_memory(file_key, state, action)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# The table a memory's row is read from: --force (install or upgrade:
|
|
418
|
+
# one code path), an upgrade over a record, else a plain install.
|
|
419
|
+
def memory_command
|
|
420
|
+
if @force then :force
|
|
421
|
+
elsif @upgrade && @existing then :upgrade
|
|
422
|
+
else :install
|
|
418
423
|
end
|
|
419
424
|
end
|
|
420
425
|
|
|
@@ -434,42 +439,64 @@ module Samagotchi
|
|
|
434
439
|
MemoryFiles.symlinks(@bundle_dir, @manifest).each { |name| @warnings << "#{name} is a symlink (not installed)" }
|
|
435
440
|
end
|
|
436
441
|
|
|
437
|
-
#
|
|
438
|
-
#
|
|
439
|
-
#
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
442
|
+
# One memory's row, done: the bundle's text written (a copy of other
|
|
443
|
+
# bytes in the trash first), the text merge's result written, the file
|
|
444
|
+
# kept or left behind (a conflict, recorded in @conflicts for the agent
|
|
445
|
+
# step), or skipped as the user's; its index line refreshed. A :decide
|
|
446
|
+
# cell is the upgrade's (Merger::Upgrade#decision: the text merge or the
|
|
447
|
+
# legacy guard, against the base the last install recorded).
|
|
448
|
+
def write_memory(file_key, state, action)
|
|
449
|
+
file_path = state.paths[:incoming]
|
|
450
|
+
target_path = state.paths[:current]
|
|
451
|
+
decision = upgrade_view.decision(file_key, state) if action.do == :decide
|
|
452
|
+
action = decision.action if decision
|
|
453
|
+
@actions[file_key] = action
|
|
454
|
+
@replaced << file_key if @force && state.digests.disk
|
|
455
|
+
result = FileResult.new(word: action.word, state: state, action: action)
|
|
456
|
+
case action.do
|
|
457
|
+
when :write_incoming
|
|
458
|
+
save_first(file_key, file_path, target_path) if action.save_first
|
|
448
459
|
FileUtils.cp(file_path, target_path)
|
|
449
|
-
@results[file_key] =
|
|
460
|
+
@results[file_key] = action.word == :updated ? result.with(note: "auto-merged (not edited)") : result
|
|
461
|
+
when :merge
|
|
462
|
+
if Merger.write_merged(target_path, decision.outcome, @trash)
|
|
463
|
+
@results[file_key] = result.with(note: "the bundle's changes joined to local edits")
|
|
464
|
+
else
|
|
465
|
+
@actions[file_key] = FileActions::BEHIND
|
|
466
|
+
note_conflict(file_key, Merger::CONFLICT.with(merge: TextMerge.failed(TextMerge::RACED)), state)
|
|
467
|
+
end
|
|
450
468
|
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" }
|
|
469
|
+
if action.behind?
|
|
470
|
+
note_conflict(file_key, (decision || upgrade_view.decision(file_key, state)).outcome, state)
|
|
471
|
+
elsif action.word == :kept
|
|
472
|
+
@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
473
|
else
|
|
458
|
-
|
|
474
|
+
@results[file_key] = result
|
|
459
475
|
end
|
|
460
|
-
when :
|
|
461
|
-
|
|
462
|
-
|
|
476
|
+
when :skip
|
|
477
|
+
@results[file_key] = result.with(note: "already exists")
|
|
478
|
+
@warnings << "Skipped #{file_key} (already exists; #{FileStanding::FORCE_TAKES})"
|
|
479
|
+
when :refuse then refuse_unreadable(file_key, target_path)
|
|
480
|
+
else raise ArgumentError, "no install step for #{action.do.inspect} (#{file_key})" # the base would move with nothing written
|
|
463
481
|
end
|
|
464
|
-
update_target_index(@target_scope, target_path, file_key)
|
|
482
|
+
update_target_index(@target_scope, target_path, file_key, owned: action.record != :not_owned)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# A file chi can't read: whether it was edited can't be told, so the
|
|
486
|
+
# run stops (the read raises; Trash.failure_message says nothing was
|
|
487
|
+
# lost).
|
|
488
|
+
def refuse_unreadable(file_key, target_path)
|
|
489
|
+
FileDigest.hex(target_path)
|
|
490
|
+
raise InstallError, "#{file_key} can't be read"
|
|
465
491
|
end
|
|
466
492
|
|
|
467
493
|
# The file stays as it is, with the old base and the mark, in FileStanding's
|
|
468
494
|
# words.
|
|
469
|
-
def note_conflict(file_key, outcome)
|
|
495
|
+
def note_conflict(file_key, outcome, state = nil)
|
|
470
496
|
behind = upgrade_view.behind(file_key, outcome, @manifest&.version)
|
|
471
497
|
@behind << behind
|
|
472
|
-
@results[file_key] =
|
|
498
|
+
@results[file_key] = FileResult.new(word: :conflict, state: state, action: state && FileActions::BEHIND,
|
|
499
|
+
note: behind.state(dry_run: @dry_run))
|
|
473
500
|
@conflicts[file_key] = upgrade_view.paths(file_key)
|
|
474
501
|
@warnings << "#{behind.line(dry_run: @dry_run)} (#{FileStanding::FORCE_TAKES})"
|
|
475
502
|
end
|
|
@@ -488,58 +515,54 @@ module Samagotchi
|
|
|
488
515
|
@upgrade_view ||= Merger::Upgrade.new(provenance: @provenance, target_dir: @target_dir, source_dir: @bundle_dir)
|
|
489
516
|
end
|
|
490
517
|
|
|
491
|
-
# A
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
#
|
|
495
|
-
|
|
496
|
-
|
|
518
|
+
# A save_first cell: the file's other bytes go to the trash before the
|
|
519
|
+
# bundle's text replaces them. Under an upgrade that is a marked file
|
|
520
|
+
# that equals its base, and the update goes ahead: the base is known to
|
|
521
|
+
# be real (this chi's mark, or ShippedHistory), or no line of the file
|
|
522
|
+
# goes (Merger::Upgrade's legacy guard stops an older chi's mark
|
|
523
|
+
# otherwise), and it says so.
|
|
524
|
+
def save_first(file_key, file_path, target_path)
|
|
525
|
+
return unless File.exist?(target_path) && !FileUtils.identical?(file_path, target_path)
|
|
497
526
|
|
|
498
527
|
@trash.copy(target_path)
|
|
528
|
+
return if @force
|
|
529
|
+
|
|
499
530
|
@warnings << "#{file_key} was marked as kept but matched its recorded base, so it took the update; your copy is in the trash"
|
|
500
531
|
end
|
|
501
532
|
|
|
502
|
-
# A dry run's
|
|
503
|
-
#
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
@results[file_key] = { status: "would_install" }
|
|
507
|
-
return
|
|
508
|
-
end
|
|
533
|
+
# A dry run's word for an action's (the real run's): what would happen,
|
|
534
|
+
# nothing written. A conflict is reported as the real run would.
|
|
535
|
+
DRY_WORDS = { installed: :would_install, updated: :fast_forward, kept: :keep, merged: :would_merge,
|
|
536
|
+
kept_own: :would_skip, up_to_date: :up_to_date }.freeze
|
|
509
537
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
538
|
+
# A dry run's verdict for one file: its row read, not done (a :decide
|
|
539
|
+
# cell's text merge runs on temp copies). --force would install every
|
|
540
|
+
# file, unread.
|
|
541
|
+
def preview_memory(file_key, state, action)
|
|
542
|
+
return @results[file_key] = FileResult.new(word: :would_install, state: state, action: action) if @force
|
|
543
|
+
return refuse_unreadable(file_key, state.paths[:current]) if action.do == :refuse
|
|
513
544
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
end
|
|
545
|
+
decision = upgrade_view.decision(file_key, state) if action.do == :decide || action.behind?
|
|
546
|
+
action = decision.action if decision
|
|
547
|
+
return note_conflict(file_key, decision.outcome, state) if action.behind?
|
|
548
|
+
|
|
549
|
+
@results[file_key] = FileResult.new(word: DRY_WORDS.fetch(action.word), state: state, action: action)
|
|
520
550
|
end
|
|
521
551
|
|
|
522
|
-
# Strict mode: a memory
|
|
523
|
-
#
|
|
524
|
-
#
|
|
525
|
-
#
|
|
552
|
+
# Strict mode: a memory that takes the bundle's text (Action#takes_source?:
|
|
553
|
+
# a new or missing file, --force, an upgrade's or a re-install's
|
|
554
|
+
# fast-forward, a merge into a local edit) warns when the bundle's file
|
|
555
|
+
# differs from its manifest's sha256. One kept or skipped isn't checked
|
|
556
|
+
# (it takes nothing from the bundle). What is checked is the source
|
|
557
|
+
# file, the bytes the write or merge takes, so a dry run checks what it
|
|
558
|
+
# would bring in, never the user's bytes on disk.
|
|
526
559
|
def verify_memory_checksums
|
|
527
560
|
@bundle_md.each do |file_key|
|
|
528
|
-
|
|
561
|
+
result = @results[file_key]
|
|
562
|
+
next unless result&.action&.takes_source?
|
|
529
563
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
target_path = File.join(@target_dir, file_key)
|
|
534
|
-
next unless File.exist?(target_path)
|
|
535
|
-
|
|
536
|
-
expected = @manifest.checksum_for(file_key)
|
|
537
|
-
next unless expected
|
|
538
|
-
|
|
539
|
-
actual = FileDigest.hex(target_path)
|
|
540
|
-
if actual != expected
|
|
541
|
-
@warnings << "Checksum mismatch for #{file_key}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
542
|
-
end
|
|
564
|
+
warning = FileDigest.mismatch(file_key, result.state.paths[:incoming], @manifest.checksum_for(file_key))
|
|
565
|
+
@warnings << warning if warning
|
|
543
566
|
end
|
|
544
567
|
end
|
|
545
568
|
|
|
@@ -573,6 +596,7 @@ module Samagotchi
|
|
|
573
596
|
hooks_files: installed.hook_files,
|
|
574
597
|
guardrails_files: installed.guardrail_files,
|
|
575
598
|
plugin_file: installed.plugin_file,
|
|
599
|
+
plugin_required: @manifest.plugin&.dig(:required) == true,
|
|
576
600
|
requires_chi: @manifest.requires_chi,
|
|
577
601
|
needs: @manifest.needs,
|
|
578
602
|
marks: @behind + held_conflict_marks,
|
|
@@ -600,7 +624,7 @@ module Samagotchi
|
|
|
600
624
|
def held_conflict_marks
|
|
601
625
|
return [] if @upgrade || @existing.nil?
|
|
602
626
|
|
|
603
|
-
kept = @owned.select { |key| @
|
|
627
|
+
kept = @owned.select { |key| @actions[key].record == :hold_mark }
|
|
604
628
|
kept.map { |key| held_mark(key) }
|
|
605
629
|
end
|
|
606
630
|
|
|
@@ -624,26 +648,26 @@ module Samagotchi
|
|
|
624
648
|
@owned.each do |file_key|
|
|
625
649
|
next unless File.exist?(File.join(@target_dir, file_key))
|
|
626
650
|
|
|
627
|
-
files[file_key] = next_base(file_key, @
|
|
651
|
+
files[file_key] = next_base(file_key, @actions.fetch(file_key))
|
|
628
652
|
end
|
|
629
653
|
@existing.files.each_key do |old_key_str|
|
|
630
654
|
next if @bundle_md.include?(old_key_str)
|
|
631
|
-
next unless @results[old_key_str]
|
|
655
|
+
next unless @results[old_key_str]&.word == :kept_pruned
|
|
632
656
|
|
|
633
657
|
files[old_key_str] = recorded_base(@provenance.base_path(old_key_str))
|
|
634
658
|
end
|
|
635
659
|
files
|
|
636
660
|
end
|
|
637
661
|
|
|
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
|
|
662
|
+
# The file an action's next_base names. nil for a kept file with no
|
|
663
|
+
# base: the user's file must not become one, or the next upgrade
|
|
664
|
+
# fast-forwards over it.
|
|
665
|
+
def next_base(file_key, action)
|
|
666
|
+
case action.next_base
|
|
643
667
|
when :recorded then recorded_base(@provenance.base_path(file_key))
|
|
644
668
|
when :on_disk then File.join(@target_dir, file_key)
|
|
645
669
|
when :incoming then File.join(@bundle_dir, file_key)
|
|
646
|
-
else raise ArgumentError, "unknown base #{
|
|
670
|
+
else raise ArgumentError, "unknown base #{action.next_base.inspect} for #{file_key}" # nil would drop the base silently
|
|
647
671
|
end
|
|
648
672
|
end
|
|
649
673
|
|
|
@@ -656,11 +680,9 @@ module Samagotchi
|
|
|
656
680
|
# sees the edit (not a fast-forward over it).
|
|
657
681
|
def install_provenance_files
|
|
658
682
|
files = @owned.each_with_object({}) do |file_key, map|
|
|
659
|
-
|
|
660
|
-
next unless File.exist?(target_path)
|
|
683
|
+
next unless File.exist?(File.join(@target_dir, file_key))
|
|
661
684
|
|
|
662
|
-
|
|
663
|
-
map[file_key] = kept ? recorded_base(@provenance.base_path(file_key)) : target_path
|
|
685
|
+
map[file_key] = next_base(file_key, @actions.fetch(file_key))
|
|
664
686
|
end
|
|
665
687
|
files.merge(dropped_owned_files)
|
|
666
688
|
end
|
|
@@ -682,58 +704,52 @@ module Samagotchi
|
|
|
682
704
|
end
|
|
683
705
|
end
|
|
684
706
|
|
|
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.
|
|
707
|
+
# A file the previous version installed that this one doesn't ship:
|
|
708
|
+
# its FileState's dropped kind and the UPGRADE (or FORCE) row. An
|
|
709
|
+
# unedited one is moved to the trash (with its index line removed), as
|
|
710
|
+
# is any with --force; an edited one is kept with a note, and one
|
|
711
|
+
# another bundle lists too stays.
|
|
706
712
|
def prune_dropped_files(previous_files, bundle_files, target_dir, scope, provenance)
|
|
707
|
-
previous_files.
|
|
713
|
+
previous_files.each_key do |key|
|
|
708
714
|
next if bundle_files.include?(key)
|
|
709
715
|
|
|
710
716
|
target = File.join(target_dir, key)
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
717
|
+
state = FileState.of(key, record: @existing, provenance: provenance, target_dir: target_dir, source_dir: @bundle_dir,
|
|
718
|
+
shipped: false, claimants: ->(k) { Provenance.claimants(k, scope: scope, except: @name) })
|
|
719
|
+
action = if state.kind == :unreadable
|
|
720
|
+
unreadable_dropped(state, target)
|
|
721
|
+
else
|
|
722
|
+
FileActions.for(@force ? :force : :upgrade, state)
|
|
723
|
+
end
|
|
724
|
+
result = FileResult.new(word: action.word, state: state, action: action)
|
|
725
|
+
case action.word
|
|
726
|
+
when :kept_shared
|
|
727
|
+
others = state.shared_with.join(", ")
|
|
728
|
+
@results[key] = result.with(note: "bundle #{others} has it too")
|
|
729
|
+
@warnings << "Kept #{key}: no longer in the bundle, but bundle #{others} has it too"
|
|
730
|
+
when :removed
|
|
731
|
+
@results[key] = result.with(word: @dry_run ? :would_remove : :removed, note: "no longer in the bundle")
|
|
719
732
|
next if @dry_run
|
|
720
733
|
|
|
721
734
|
@trash.move(target)
|
|
722
735
|
remove_target_index(scope, key)
|
|
723
|
-
|
|
724
|
-
@results[key] =
|
|
736
|
+
when :kept_pruned
|
|
737
|
+
@results[key] = result.with(note: "local edits preserved")
|
|
725
738
|
@warnings << "Kept #{key}: no longer in the bundle but edited locally (#{FileStanding::FORCE_MOVES})"
|
|
726
739
|
end
|
|
727
740
|
end
|
|
728
741
|
end
|
|
729
742
|
|
|
730
|
-
#
|
|
731
|
-
#
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
743
|
+
# A dropped file chi can't read, in the order the rows are asked: kept
|
|
744
|
+
# when another bundle lists it, moved unread with --force; otherwise
|
|
745
|
+
# whether it was edited can't be told, and the read raises (the run
|
|
746
|
+
# stops, nothing lost).
|
|
747
|
+
def unreadable_dropped(state, target)
|
|
748
|
+
return FileActions::KEPT_SHARED if state.shared_with.any?
|
|
749
|
+
return FileActions::REMOVE if @force
|
|
735
750
|
|
|
736
|
-
|
|
751
|
+
FileDigest.hex(target)
|
|
752
|
+
raise InstallError, "#{state.key} can't be read"
|
|
737
753
|
end
|
|
738
754
|
|
|
739
755
|
# One warning per manifest key this chi's parser never reads (docs/cli.md's
|
|
@@ -26,6 +26,8 @@ module Samagotchi
|
|
|
26
26
|
# plugin: # optional — the bundle's plugin.rb (docs/plugins.md)
|
|
27
27
|
# file: plugin.rb
|
|
28
28
|
# sha256: sha256:0a1b...
|
|
29
|
+
# required: true # optional: a guardrail; when it fails to load
|
|
30
|
+
# # chi denies every tool call (docs/hooks.md)
|
|
29
31
|
# requires_chi: ">= 0.1.28" # optional — a Gem::Requirement for chi's VERSION
|
|
30
32
|
# requires_chi_next: true # optional, shipped bundles — ignored here:
|
|
31
33
|
# # `rake release:bump` raises requires_chi to
|
|
@@ -100,7 +102,8 @@ module Samagotchi
|
|
|
100
102
|
"settings.types" => "0.92.0",
|
|
101
103
|
"hooks.events" => "0.98.0",
|
|
102
104
|
"settings" => "0.143.0",
|
|
103
|
-
"block_renderers" => "0.151.0"
|
|
105
|
+
"block_renderers" => "0.151.0",
|
|
106
|
+
"plugin.required" => "0.157.0"
|
|
104
107
|
}.freeze
|
|
105
108
|
|
|
106
109
|
# files: name → "sha256:<hex>" (both forms); file_descriptions: name →
|
|
@@ -249,6 +252,7 @@ module Samagotchi
|
|
|
249
252
|
found["scripts"] = SINCE["scripts"] unless @scripts.empty?
|
|
250
253
|
found["context_providers"] = SINCE["context_providers"] unless @context_providers.empty?
|
|
251
254
|
found["block_renderers"] = SINCE["block_renderers"] unless @block_renderers.empty?
|
|
255
|
+
found["plugin.required"] = SINCE["plugin.required"] if @plugin&.dig(:required) && SINCE.key?("plugin.required")
|
|
252
256
|
found["session_settings"] = SINCE["session_settings"] unless @session_settings.empty?
|
|
253
257
|
found["settings"] = SINCE["settings"] unless @settings.nil?
|
|
254
258
|
found["files.description"] = SINCE["files.description"] if raw["files"].is_a?(Hash) && raw["files"].values.any?(Hash)
|
|
@@ -281,6 +285,7 @@ module Samagotchi
|
|
|
281
285
|
if plugin
|
|
282
286
|
sha = plugin[:sha256].to_s
|
|
283
287
|
manifest["plugin"] = { "file" => plugin[:file].to_s, "sha256" => sha.start_with?("sha256:") ? sha : "sha256:#{sha}" }
|
|
288
|
+
manifest["plugin"]["required"] = true if plugin[:required]
|
|
284
289
|
end
|
|
285
290
|
manifest["requires_chi"] = requires_chi.to_s if requires_chi && !requires_chi.to_s.empty?
|
|
286
291
|
needs = parse_needs(needs)
|
|
@@ -471,8 +476,10 @@ module Samagotchi
|
|
|
471
476
|
end
|
|
472
477
|
end
|
|
473
478
|
|
|
474
|
-
# plugin: {file:, sha256:} → {file:, sha256:} (sha "" when
|
|
475
|
-
#
|
|
479
|
+
# plugin: {file:, sha256:, required:} → {file:, sha256:} (sha "" when
|
|
480
|
+
# not given) with required: true when it says so, or nil; a file that
|
|
481
|
+
# isn't a plain .rb name, or a required: that isn't true or false, is a
|
|
482
|
+
# validation error.
|
|
476
483
|
def parse_plugin(raw)
|
|
477
484
|
return nil if raw.nil?
|
|
478
485
|
raise ValidationError, "plugin: must be a mapping with file: and sha256:" unless raw.is_a?(Hash)
|
|
@@ -484,7 +491,10 @@ module Samagotchi
|
|
|
484
491
|
|
|
485
492
|
sha = (raw["sha256"] || raw[:sha256]).to_s.strip
|
|
486
493
|
sha = "sha256:#{sha}" unless sha.empty? || sha.start_with?("sha256:")
|
|
487
|
-
|
|
494
|
+
required = raw.key?("required") ? raw["required"] : raw[:required]
|
|
495
|
+
raise ValidationError, "plugin required: must be true or false, not #{required.inspect}" unless [nil, true, false].include?(required)
|
|
496
|
+
|
|
497
|
+
required ? { file: file, sha256: sha, required: true } : { file: file, sha256: sha }
|
|
488
498
|
end
|
|
489
499
|
|
|
490
500
|
# scripts: {file => sha256} → the same, "sha256:" prefixed; {} when absent.
|