samagotchi 0.159.0 → 0.161.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 +125 -1
- data/docs/architecture.md +12 -9
- data/docs/cli.md +7 -5
- data/docs/context.md +10 -1
- data/docs/guardrails.md +33 -5
- data/docs/internals/attached-context.md +4 -2
- data/docs/internals/bridge.md +28 -6
- data/docs/internals/bundles.md +24 -5
- data/docs/internals/engine.md +7 -1
- data/docs/internals/objectives.md +8 -2
- data/docs/internals/remote-sessions.md +2 -1
- data/docs/memory.md +43 -13
- data/docs/plugins.md +13 -4
- data/docs/releasing.md +7 -4
- data/docs/scheduling.md +53 -8
- data/docs/sessions.md +17 -12
- data/docs/testing.md +9 -1
- data/lib/samagotchi/bridge/card_store.rb +31 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +11 -1
- data/lib/samagotchi/bridge/sse_writer.rb +8 -5
- data/lib/samagotchi/bridge.rb +103 -44
- data/lib/samagotchi/bridge_client.rb +9 -6
- data/lib/samagotchi/broadcast/scope_card.rb +2 -1
- data/lib/samagotchi/broadcast_command.rb +24 -8
- data/lib/samagotchi/bundle_command.rb +57 -6
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +2 -2
- data/lib/samagotchi/child_reports.rb +29 -23
- data/lib/samagotchi/client_id.rb +6 -0
- data/lib/samagotchi/command_intake.rb +56 -0
- data/lib/samagotchi/compat.rb +32 -0
- data/lib/samagotchi/context_absorber.rb +6 -3
- data/lib/samagotchi/context_candidate_choice.rb +8 -5
- data/lib/samagotchi/context_candidates.rb +11 -8
- data/lib/samagotchi/context_candidates_log.rb +10 -1
- data/lib/samagotchi/context_command.rb +10 -4
- data/lib/samagotchi/context_move.rb +6 -1
- data/lib/samagotchi/context_poller.rb +5 -2
- data/lib/samagotchi/context_sources.rb +6 -4
- data/lib/samagotchi/engine/events.rb +25 -2
- data/lib/samagotchi/engine/guardrails.rb +3 -4
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/steering.rb +2 -2
- data/lib/samagotchi/engine.rb +1 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/events.rb +71 -0
- data/lib/samagotchi/guardrails/approvals.rb +27 -3
- data/lib/samagotchi/guardrails/child_boundary.rb +37 -4
- data/lib/samagotchi/guardrails/protected_paths.rb +8 -5
- data/lib/samagotchi/guardrails/targets.rb +25 -6
- data/lib/samagotchi/hook_routes.rb +0 -1
- data/lib/samagotchi/live_versions.rb +7 -2
- data/lib/samagotchi/memory_bundle/asset_installer.rb +86 -18
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +7 -27
- data/lib/samagotchi/memory_bundle/file_actions.rb +3 -4
- data/lib/samagotchi/memory_bundle/file_state.rb +11 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +7 -4
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installer.rb +93 -23
- data/lib/samagotchi/memory_bundle/manifest.rb +6 -0
- data/lib/samagotchi/memory_bundle/provenance.rb +158 -6
- data/lib/samagotchi/memory_bundle/shipped_update.rb +7 -4
- data/lib/samagotchi/memory_bundle/status.rb +3 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +29 -13
- data/lib/samagotchi/memory_orphans.rb +54 -13
- data/lib/samagotchi/memory_paths.rb +29 -6
- data/lib/samagotchi/memory_usage.rb +23 -12
- data/lib/samagotchi/note_command.rb +9 -1
- data/lib/samagotchi/note_delivery.rb +34 -24
- data/lib/samagotchi/note_move.rb +205 -0
- data/lib/samagotchi/objectives_command.rb +3 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/plugin/attached_context.rb +3 -1
- data/lib/samagotchi/plugin/sessions.rb +4 -1
- data/lib/samagotchi/project_command.rb +58 -20
- data/lib/samagotchi/project_id.rb +102 -0
- data/lib/samagotchi/project_id_clash.rb +62 -0
- data/lib/samagotchi/project_init.rb +214 -10
- data/lib/samagotchi/project_scope.rb +11 -2
- data/lib/samagotchi/review_start.rb +5 -3
- data/lib/samagotchi/schedule_command.rb +12 -0
- data/lib/samagotchi/schedules.rb +374 -76
- data/lib/samagotchi/send_command.rb +2 -15
- data/lib/samagotchi/session.rb +59 -7
- data/lib/samagotchi/session_access/local.rb +53 -22
- data/lib/samagotchi/session_access/remote.rb +13 -5
- data/lib/samagotchi/session_access/web_bridge_client.rb +2 -2
- data/lib/samagotchi/session_access.rb +20 -4
- data/lib/samagotchi/session_commands.rb +7 -3
- data/lib/samagotchi/session_continue.rb +162 -24
- data/lib/samagotchi/session_delete_command.rb +3 -1
- data/lib/samagotchi/session_inbox.rb +41 -1
- data/lib/samagotchi/session_inbox_delivery.rb +254 -0
- data/lib/samagotchi/session_manager/delivery.rb +66 -12
- data/lib/samagotchi/session_manager/emptiness.rb +7 -2
- data/lib/samagotchi/session_manager/lifecycle.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +4 -1
- data/lib/samagotchi/session_manager/removal.rb +130 -26
- data/lib/samagotchi/session_manager/spawn.rb +167 -100
- data/lib/samagotchi/session_manager/worker_command.rb +7 -19
- data/lib/samagotchi/session_manager.rb +1 -0
- data/lib/samagotchi/session_retention.rb +53 -23
- data/lib/samagotchi/session_setup.rb +27 -7
- data/lib/samagotchi/starting_marker.rb +93 -24
- data/lib/samagotchi/state_format.rb +129 -0
- data/lib/samagotchi/system_one/runs.rb +6 -1
- data/lib/samagotchi/system_one/runs_store.rb +18 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +7 -4
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +5 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +4 -2
- data/lib/samagotchi/terminal_ui.rb +2 -3
- data/lib/samagotchi/tools/context_read.rb +7 -2
- data/lib/samagotchi/tools/delegate.rb +19 -6
- data/lib/samagotchi/tools/schedule_task.rb +5 -3
- data/lib/samagotchi/tools/send_note.rb +10 -4
- data/lib/samagotchi/turn_stages/context_judge.rb +7 -7
- data/lib/samagotchi/turn_state.rb +108 -4
- data/lib/samagotchi/update_command.rb +9 -4
- data/lib/samagotchi/verify_fresh.rb +27 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -5
- data/lib/samagotchi/web/public/app.js +2 -7
- data/lib/samagotchi/web/public/chains.js +4 -4
- data/lib/samagotchi/web/public/data.js +18 -2
- data/lib/samagotchi/web/public/info_bar_model.js +1 -1
- data/lib/samagotchi/web/public/session_fields.js +3 -2
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/update_notice.js +2 -2
- data/lib/samagotchi/web/session_access_routes.rb +15 -11
- data/lib/samagotchi/web/session_control.rb +9 -3
- data/lib/samagotchi/web/session_create.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +50 -5
- data/lib/samagotchi/web/session_summary.rb +5 -2
- data/lib/samagotchi/worker.rb +61 -11
- data/lib/samagotchi/worker_boot.rb +22 -11
- data/lib/samagotchi/worker_inbound.rb +62 -10
- data/lib/samagotchi/worker_link.rb +4 -1
- data/lib/samagotchi/worker_presence.rb +106 -0
- data/lib/samagotchi/worker_sidecar.rb +3 -4
- metadata +9 -1
|
@@ -20,7 +20,7 @@ module Samagotchi
|
|
|
20
20
|
# Installs a bundle into a scoped memories directory.
|
|
21
21
|
#
|
|
22
22
|
# Flow (#run):
|
|
23
|
-
# 1. Normalize source → directory, read manifest.yml (load_source)
|
|
23
|
+
# 1. Normalize source → directory, refuse retired hooks, read manifest.yml (load_source)
|
|
24
24
|
# 2. Refuse a bundle whose requires_chi this chi doesn't meet,
|
|
25
25
|
# unless force (refuse_too_new)
|
|
26
26
|
# 3. Copy .md files to the target scope dir and update index.md
|
|
@@ -61,7 +61,8 @@ module Samagotchi
|
|
|
61
61
|
# chi_version: the chi this install is for (Profile and ShippedUpdate
|
|
62
62
|
# check their bundles against a version they are given); default this chi.
|
|
63
63
|
def initialize(source:, name:, scope: nil, force: false, strict: true, upgrade: false, dry_run: false, prepared: nil,
|
|
64
|
-
shipped_dir: SourceNormalizer::SHIPPED_DIR, dev: false, chi_version: Samagotchi::VERSION
|
|
64
|
+
shipped_dir: SourceNormalizer::SHIPPED_DIR, dev: false, chi_version: Samagotchi::VERSION,
|
|
65
|
+
refuse_mismatch: false)
|
|
65
66
|
@source = source
|
|
66
67
|
@shipped_dir = shipped_dir
|
|
67
68
|
@prepared = prepared
|
|
@@ -73,6 +74,10 @@ module Samagotchi
|
|
|
73
74
|
@dry_run = dry_run
|
|
74
75
|
@dev = dev
|
|
75
76
|
@chi_version = chi_version
|
|
77
|
+
# `chi bundle install/upgrade` refuses a file whose bytes disagree with
|
|
78
|
+
# its manifest sha before writing anything; the callers that only warn
|
|
79
|
+
# (ShippedUpdate, SystemBundle, Profile) leave it unset.
|
|
80
|
+
@refuse_mismatch = refuse_mismatch
|
|
76
81
|
@results = {}
|
|
77
82
|
@warnings = []
|
|
78
83
|
@placeholder_warnings = []
|
|
@@ -119,10 +124,11 @@ module Samagotchi
|
|
|
119
124
|
def run
|
|
120
125
|
load_source
|
|
121
126
|
refuse_too_new
|
|
127
|
+
@provenance = Provenance.new(name: @name)
|
|
128
|
+
refuse_newer_record_format
|
|
122
129
|
warn_unknown_keys(@manifest) if @manifest
|
|
123
130
|
@target_scope, @target_dir = resolve_target
|
|
124
131
|
@file_descriptions = @manifest&.file_descriptions || {}
|
|
125
|
-
@provenance = Provenance.new(name: @name)
|
|
126
132
|
@existing = @provenance.record
|
|
127
133
|
check_source
|
|
128
134
|
# A plain install over an installed bundle: its summary says so.
|
|
@@ -212,6 +218,18 @@ module Samagotchi
|
|
|
212
218
|
"(to leave dev mode: chi bundle uninstall #{name}, then chi bundle install without --dev)"
|
|
213
219
|
end
|
|
214
220
|
|
|
221
|
+
# The line a refused install names one file with: the checksum warning and
|
|
222
|
+
# the way past it. Only `chi bundle install/upgrade` refuses a mismatch
|
|
223
|
+
# (--force skips the early check and keeps the post-write warning); the
|
|
224
|
+
# callers that only warn (chi update, SystemBundle, a profile) never reach
|
|
225
|
+
# this.
|
|
226
|
+
# @param label [String] "a.md", "plugin plugin.rb", "script s.sh"
|
|
227
|
+
# @param upgrade [Boolean] which command the hint names
|
|
228
|
+
def self.mismatch_refusal(label, path, expected, upgrade:)
|
|
229
|
+
warning = FileDigest.mismatch(label, path, expected)
|
|
230
|
+
warning && "#{warning}; chi bundle #{upgrade ? "upgrade" : "install"} --force installs it anyway"
|
|
231
|
+
end
|
|
232
|
+
|
|
215
233
|
private
|
|
216
234
|
|
|
217
235
|
# The files whose result has one of +words+, in the order they ran.
|
|
@@ -233,28 +251,71 @@ module Samagotchi
|
|
|
233
251
|
line
|
|
234
252
|
end
|
|
235
253
|
|
|
236
|
-
# The steps that replace files
|
|
237
|
-
# the files it dropped. The record isn't written yet, so a failed run is
|
|
254
|
+
# The steps that replace files, after the check of the files the manifest
|
|
255
|
+
# names: memories, the bundle's own folder, then the files it dropped. The record isn't written yet, so a failed run is
|
|
238
256
|
# retried as the same upgrade.
|
|
239
257
|
# @return [AssetInstaller::Installed]
|
|
240
258
|
# @raise [InstallError] a file system error (a file it can't read, a
|
|
241
259
|
# .trash it can't write)
|
|
242
260
|
def replace_files
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
installed = AssetInstaller.new(
|
|
261
|
+
assets = AssetInstaller.new(
|
|
246
262
|
name: @name, source_dir: @bundle_dir, manifest: @manifest, provenance: @provenance, existing: @existing,
|
|
247
263
|
results: @results, warnings: @warnings, conflicts: @conflicts, trash: @trash,
|
|
248
|
-
force: @force, strict: @strict, upgrade: @upgrade, dry_run: @dry_run, dev: @dev, chi_version: @chi_version
|
|
249
|
-
|
|
264
|
+
force: @force, strict: @strict, upgrade: @upgrade, dry_run: @dry_run, dev: @dev, chi_version: @chi_version,
|
|
265
|
+
refuse_mismatch: @refuse_mismatch
|
|
266
|
+
)
|
|
267
|
+
# Before the first write: a plugin or script the bundle lacks refuses the run with disk and record as they were.
|
|
268
|
+
assets.check_named_files
|
|
269
|
+
# And, for the commands that refuse it (--force aside), a file whose
|
|
270
|
+
# bytes disagree with the sha its manifest declares.
|
|
271
|
+
refuse_memory_mismatches
|
|
272
|
+
assets.check_named_checksums
|
|
273
|
+
write_pending
|
|
274
|
+
install_memories
|
|
275
|
+
installed = assets.run
|
|
250
276
|
|
|
251
277
|
# Files the previous version shipped and this one doesn't.
|
|
252
|
-
|
|
278
|
+
previous_files = @provenance.files_with_pending(@existing, target_dir: @target_dir)
|
|
279
|
+
prune_dropped_files(previous_files, @bundle_md, @target_dir, @target_scope, @provenance) if @upgrade && @existing
|
|
253
280
|
installed
|
|
254
281
|
rescue SystemCallError => e
|
|
255
282
|
raise InstallError, Trash.failure_message(e)
|
|
256
283
|
end
|
|
257
284
|
|
|
285
|
+
# The intent journal (Provenance#write_pending): the text of each memory
|
|
286
|
+
# this run will change, noted before the first write, so a run that
|
|
287
|
+
# stops before its record leaves files a retry still knows as the
|
|
288
|
+
# bundle's.
|
|
289
|
+
def write_pending
|
|
290
|
+
return if @dry_run || @manifest.nil?
|
|
291
|
+
|
|
292
|
+
incoming = memory_files.to_h { |key| [key, FileDigest.hex(File.join(@bundle_dir, key))] }
|
|
293
|
+
incoming.reject! do |key, sha|
|
|
294
|
+
sha.nil? || sha == FileDigest.hex(File.join(@target_dir, key))
|
|
295
|
+
rescue SystemCallError
|
|
296
|
+
true
|
|
297
|
+
end
|
|
298
|
+
@provenance.write_pending(version: @manifest.version, scope: @target_scope, target_dir: @target_dir, files: incoming) unless incoming.empty?
|
|
299
|
+
rescue SystemCallError => e
|
|
300
|
+
raise InstallError, Trash.failure_message(e)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Before the first write, the bundle's own bytes against the sha its
|
|
304
|
+
# manifest declares for each memory: `chi bundle install/upgrade` refuses
|
|
305
|
+
# a mismatch (--force skips the check, and #verify_memory_checksums keeps
|
|
306
|
+
# the post-write warning for the callers that only warn). A dev install
|
|
307
|
+
# follows its own tree; a non-strict run has no shas to trust.
|
|
308
|
+
# @raise [InstallError] one line per mismatched file
|
|
309
|
+
def refuse_memory_mismatches
|
|
310
|
+
return unless @refuse_mismatch && @strict && !@dev && !@force && @manifest
|
|
311
|
+
|
|
312
|
+
lines = memory_files.filter_map do |file_key|
|
|
313
|
+
self.class.mismatch_refusal(file_key, File.join(@bundle_dir, file_key),
|
|
314
|
+
@manifest.checksum_for(file_key), upgrade: @upgrade)
|
|
315
|
+
end
|
|
316
|
+
raise InstallError, lines.join("\n") unless lines.empty?
|
|
317
|
+
end
|
|
318
|
+
|
|
258
319
|
# Who may install under this name (a manifest can take any name: a
|
|
259
320
|
# repo calling itself guardrails). Sources compare by SourceIdentity
|
|
260
321
|
# (a shipped dir by name, in any gem or checkout; a git URL without
|
|
@@ -356,7 +417,7 @@ module Samagotchi
|
|
|
356
417
|
@bundle_dir = @source_dir = prepared.dir
|
|
357
418
|
@source_owned = prepared.owned
|
|
358
419
|
@source_commit = prepared.commit
|
|
359
|
-
|
|
420
|
+
Manifest::HooksRetired.refuse_source_files(@bundle_dir)
|
|
360
421
|
@manifest = Manifest.read(dir: @bundle_dir)
|
|
361
422
|
clash = @manifest.session_settings_clash
|
|
362
423
|
raise InstallError, clash if clash
|
|
@@ -371,15 +432,6 @@ module Samagotchi
|
|
|
371
432
|
@warnings << "No manifest found — proceeding without strict manifest validation"
|
|
372
433
|
end
|
|
373
434
|
|
|
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
|
-
|
|
383
435
|
# A bundle whose requires_chi this chi doesn't meet is not installed at
|
|
384
436
|
# all: --force is what installs it, and its plugin and rules then
|
|
385
437
|
# warn that they won't load (AssetInstaller). The words come from
|
|
@@ -393,6 +445,12 @@ module Samagotchi
|
|
|
393
445
|
raise InstallError, "#{@name} can't be installed: #{failure} (chi update, or --force to install it anyway)"
|
|
394
446
|
end
|
|
395
447
|
|
|
448
|
+
def refuse_newer_record_format
|
|
449
|
+
@provenance.ensure_supported_record_format!
|
|
450
|
+
rescue StateFormat::InvalidDocument => e
|
|
451
|
+
raise InstallError, e.message
|
|
452
|
+
end
|
|
453
|
+
|
|
396
454
|
# The scope the files go to (the CLI's wins over the manifest's) and
|
|
397
455
|
# its dir, created.
|
|
398
456
|
# @return [Array(String, String)] scope, dir
|
|
@@ -557,6 +615,7 @@ module Samagotchi
|
|
|
557
615
|
# cell's text merge runs on temp copies). --force would install every
|
|
558
616
|
# file, unread.
|
|
559
617
|
def preview_memory(file_key, state, action)
|
|
618
|
+
preview_trash_copy(file_key, state, action)
|
|
560
619
|
return @results[file_key] = FileResult.new(word: :would_install, state: state, action: action) if @force
|
|
561
620
|
return refuse_unreadable(file_key, state.paths[:current]) if action.do == :refuse
|
|
562
621
|
|
|
@@ -564,9 +623,20 @@ module Samagotchi
|
|
|
564
623
|
action = decision.action if decision
|
|
565
624
|
return note_conflict(file_key, decision.outcome, state) if action.behind?
|
|
566
625
|
|
|
626
|
+
preview_trash_copy(file_key, state, action)
|
|
567
627
|
@results[file_key] = FileResult.new(word: DRY_WORDS.fetch(action.word), state: state, action: action)
|
|
568
628
|
end
|
|
569
629
|
|
|
630
|
+
def preview_trash_copy(file_key, state, action)
|
|
631
|
+
return unless action.save_first
|
|
632
|
+
|
|
633
|
+
current = state.paths[:current]
|
|
634
|
+
incoming = state.paths[:incoming]
|
|
635
|
+
return unless File.file?(current) && !FileUtils.identical?(incoming, current)
|
|
636
|
+
|
|
637
|
+
@warnings << "Would copy to the trash first: #{file_key}"
|
|
638
|
+
end
|
|
639
|
+
|
|
570
640
|
# Strict mode: a memory that takes the bundle's text (Action#takes_source?:
|
|
571
641
|
# a new or missing file, --force, an upgrade's or a re-install's
|
|
572
642
|
# fast-forward, a merge into a local edit) warns when the bundle's file
|
|
@@ -782,8 +852,8 @@ module Samagotchi
|
|
|
782
852
|
|
|
783
853
|
# Not fatal, like update_target_index: a warning says so.
|
|
784
854
|
def remove_target_index(scope, file_key)
|
|
785
|
-
IndexUpdater.remove_index(scope, file_key.delete_suffix(".md"))
|
|
786
|
-
IndexUpdater.remove_index(scope, file_key) # legacy "name.md" line
|
|
855
|
+
IndexUpdater.remove_index(scope, file_key.delete_suffix(".md"), target_dir: @target_dir)
|
|
856
|
+
IndexUpdater.remove_index(scope, file_key, target_dir: @target_dir) # legacy "name.md" line
|
|
787
857
|
rescue StandardError => e
|
|
788
858
|
@warnings << "index.md: line for #{file_key.delete_suffix(".md")} not removed (#{e.message})"
|
|
789
859
|
end
|
|
@@ -83,6 +83,12 @@ module Samagotchi
|
|
|
83
83
|
|
|
84
84
|
# The source's hooks/*.rb, relative and sorted ([] for none).
|
|
85
85
|
def self.files_in(dir) = Dir.glob(File.join(dir, "hooks", "*.rb")).map { |f| "hooks/#{File.basename(f)}" }.sort
|
|
86
|
+
|
|
87
|
+
# Refuse a source's class-file hooks before parsing its manifest.
|
|
88
|
+
def self.refuse_source_files(dir)
|
|
89
|
+
files = files_in(dir)
|
|
90
|
+
raise for_files(files) unless files.empty?
|
|
91
|
+
end
|
|
86
92
|
end
|
|
87
93
|
|
|
88
94
|
# A plugin file is a plain .rb name in the bundle's top directory.
|
|
@@ -7,6 +7,7 @@ require "fileutils"
|
|
|
7
7
|
require "tmpdir"
|
|
8
8
|
require_relative "../atomic_file"
|
|
9
9
|
require_relative "../memory_paths"
|
|
10
|
+
require_relative "../state_format"
|
|
10
11
|
require_relative "../version"
|
|
11
12
|
require_relative "file_digest"
|
|
12
13
|
require_relative "installed_bundle"
|
|
@@ -32,6 +33,38 @@ module Samagotchi
|
|
|
32
33
|
MARK_FIELDS = %w[conflict incoming_sha legacy_base why missed missed_places].freeze
|
|
33
34
|
# What a merge no report has shown adds to a file's entry (#note_merged).
|
|
34
35
|
MERGE_NOTE_FIELDS = %w[merged_version merged_at merged_trash].freeze
|
|
36
|
+
RECORD_FORMAT_VERSION = 1
|
|
37
|
+
RECORD_FORMAT = StateFormat.new(
|
|
38
|
+
version: RECORD_FORMAT_VERSION,
|
|
39
|
+
members: [
|
|
40
|
+
StateFormat.required(:name),
|
|
41
|
+
StateFormat.required(:version),
|
|
42
|
+
StateFormat.required(:scope),
|
|
43
|
+
StateFormat.required(:source),
|
|
44
|
+
StateFormat.required(:installed_at),
|
|
45
|
+
StateFormat.required(:files),
|
|
46
|
+
StateFormat.required(:hooks),
|
|
47
|
+
StateFormat.added(:record_format, in_version: 1, default: 1),
|
|
48
|
+
StateFormat.added(:dev, in_version: 1, default: false),
|
|
49
|
+
StateFormat.added(:guardrails, in_version: 1, default: {}),
|
|
50
|
+
StateFormat.added(:source_commit, in_version: 1, default: nil),
|
|
51
|
+
StateFormat.added(:plugin, in_version: 1, default: nil),
|
|
52
|
+
StateFormat.added(:includes, in_version: 1, default: nil),
|
|
53
|
+
StateFormat.added(:scripts, in_version: 1, default: {}),
|
|
54
|
+
StateFormat.added(:manifest_sha, in_version: 1, default: nil),
|
|
55
|
+
StateFormat.added(:installed_by, in_version: 1, default: nil),
|
|
56
|
+
StateFormat.added(:requires_chi, in_version: 1, default: nil),
|
|
57
|
+
StateFormat.added(:trust_level, in_version: 1, default: nil),
|
|
58
|
+
StateFormat.added(:needs, in_version: 1, default: nil),
|
|
59
|
+
StateFormat.added(:context_providers, in_version: 1, default: nil),
|
|
60
|
+
StateFormat.added(:block_renderers, in_version: 1, default: nil),
|
|
61
|
+
StateFormat.added(:session_settings, in_version: 1, default: nil),
|
|
62
|
+
StateFormat.added(:session_setting_types, in_version: 1, default: nil),
|
|
63
|
+
StateFormat.added(:settings, in_version: 1, default: nil),
|
|
64
|
+
StateFormat.added(:setting_types, in_version: 1, default: nil),
|
|
65
|
+
StateFormat.extra(:extra, in_version: 1)
|
|
66
|
+
]
|
|
67
|
+
)
|
|
35
68
|
|
|
36
69
|
def initialize(name:)
|
|
37
70
|
@name = name
|
|
@@ -65,6 +98,55 @@ module Samagotchi
|
|
|
65
98
|
File.join(@bundle_dir, "scripts")
|
|
66
99
|
end
|
|
67
100
|
|
|
101
|
+
# The intent journal of a run that has not written its record yet
|
|
102
|
+
# (#write_pending): its scope and target dir, plus the files and sha256s
|
|
103
|
+
# of the texts a run meant to put there. #write removes it.
|
|
104
|
+
def pending_path
|
|
105
|
+
File.join(@bundle_dir, "pending.json")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Notes, before an install's first write, the texts it is about to put
|
|
109
|
+
# in the memories dir, atomically. A journal an earlier interrupted run
|
|
110
|
+
# left keeps its shas (that run's files are still on disk).
|
|
111
|
+
# @param files [Hash{String => String}] memory => the incoming sha256
|
|
112
|
+
def write_pending(version:, scope:, target_dir:, files:)
|
|
113
|
+
shas = pending_files(target_dir: target_dir)
|
|
114
|
+
files.each { |key, sha| shas[key] = (shas.fetch(key, []) + [sha]).uniq }
|
|
115
|
+
FileUtils.mkdir_p(@bundle_dir)
|
|
116
|
+
AtomicFile.write(pending_path, JSON.pretty_generate(
|
|
117
|
+
"version" => version.to_s, "scope" => scope.to_s, "target_dir" => File.expand_path(target_dir), "files" => shas
|
|
118
|
+
))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The shas the journal names for +file_key+ (none without a journal, or
|
|
122
|
+
# with one that doesn't read or belongs to another target).
|
|
123
|
+
def pending_shas(file_key, target_dir:)
|
|
124
|
+
pending_files(target_dir: target_dir).fetch(file_key.to_s, [])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def pending_scope
|
|
128
|
+
pending_metadata&.fetch("scope")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def pending_target_dir
|
|
132
|
+
pending_metadata&.fetch("target_dir")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The record's files plus interrupted files whose current bytes match a
|
|
136
|
+
# journaled sha. Files whose bytes no longer match are not claimed.
|
|
137
|
+
def files_with_pending(record, target_dir:)
|
|
138
|
+
files = record&.files&.dup || {}
|
|
139
|
+
pending_files(target_dir: target_dir).each do |key, shas|
|
|
140
|
+
next if files.key?(key)
|
|
141
|
+
|
|
142
|
+
disk_sha = FileDigest.hex(File.join(target_dir, key))
|
|
143
|
+
files[key] = {} if shas.include?(disk_sha)
|
|
144
|
+
rescue SystemCallError
|
|
145
|
+
next
|
|
146
|
+
end
|
|
147
|
+
files
|
|
148
|
+
end
|
|
149
|
+
|
|
68
150
|
# The bundle's manifest.yml as installed (InstalledBundle#kept_manifest_path).
|
|
69
151
|
def kept_manifest_path
|
|
70
152
|
File.join(@bundle_dir, "manifest.yml")
|
|
@@ -233,6 +315,7 @@ module Samagotchi
|
|
|
233
315
|
guardrails_files: {}, plugin_file: nil, plugin_required: false, requires_chi: nil, needs: nil, marks: [], incoming_dir: nil, includes: nil,
|
|
234
316
|
scripts_files: {}, context_providers: [], block_renderers: [], file_descriptions: {}, session_settings: [], session_setting_types: {}, settings: nil, setting_types: {},
|
|
235
317
|
dev: false, manifest_from: nil)
|
|
318
|
+
record_extra = format_record(read)&.extra || {}
|
|
236
319
|
FileUtils.mkdir_p(@bundle_dir)
|
|
237
320
|
bases_dir = File.join(@bundle_dir, "bases")
|
|
238
321
|
FileUtils.mkdir_p(bases_dir)
|
|
@@ -280,6 +363,7 @@ module Samagotchi
|
|
|
280
363
|
|
|
281
364
|
manifest_data = {
|
|
282
365
|
"name" => @name,
|
|
366
|
+
"record_format" => RECORD_FORMAT_VERSION,
|
|
283
367
|
"version" => version.to_s,
|
|
284
368
|
"scope" => scope.to_s,
|
|
285
369
|
"source" => source_path.to_s,
|
|
@@ -314,8 +398,10 @@ module Samagotchi
|
|
|
314
398
|
end
|
|
315
399
|
end
|
|
316
400
|
manifest_data.merge!(kept_manifest_fields(existing, manifest_from, dev))
|
|
401
|
+
manifest_data = record_extra.merge(manifest_data)
|
|
317
402
|
|
|
318
403
|
write_manifest(manifest_data)
|
|
404
|
+
FileUtils.rm_f(pending_path)
|
|
319
405
|
marked.each_key { |key| save_incoming(key, incoming_dir) }
|
|
320
406
|
prune_incoming(marked.keys)
|
|
321
407
|
end
|
|
@@ -330,7 +416,7 @@ module Samagotchi
|
|
|
330
416
|
def resolve_conflicts(conflicts)
|
|
331
417
|
return [] unless installed?
|
|
332
418
|
|
|
333
|
-
raw =
|
|
419
|
+
raw = writable_record
|
|
334
420
|
raw["files"] ||= {}
|
|
335
421
|
settled = conflicts.filter_map do |file_key, info|
|
|
336
422
|
key = file_key.to_s
|
|
@@ -349,7 +435,7 @@ module Samagotchi
|
|
|
349
435
|
# A missing file was put back with the bundle's bytes: +path+ is its base,
|
|
350
436
|
# its entry is made or rebuilt (no mark), and the rest of the manifest stays.
|
|
351
437
|
def record_restored(file_key, path, description: nil)
|
|
352
|
-
raw =
|
|
438
|
+
raw = writable_record
|
|
353
439
|
content = File.binread(path)
|
|
354
440
|
File.binwrite(base_path(file_key), content)
|
|
355
441
|
entry = { "checksum" => Digest::SHA256.hexdigest(content) }
|
|
@@ -369,7 +455,7 @@ module Samagotchi
|
|
|
369
455
|
def refresh_marks(marks, incoming_dir)
|
|
370
456
|
return unless installed?
|
|
371
457
|
|
|
372
|
-
raw =
|
|
458
|
+
raw = writable_record
|
|
373
459
|
marks.each do |mark|
|
|
374
460
|
entry = raw.dig("files", mark.file)
|
|
375
461
|
next unless entry&.fetch("conflict", nil)
|
|
@@ -393,7 +479,7 @@ module Samagotchi
|
|
|
393
479
|
def note_merged(files, version:, trash:, at: Time.now)
|
|
394
480
|
return unless installed?
|
|
395
481
|
|
|
396
|
-
raw =
|
|
482
|
+
raw = writable_record
|
|
397
483
|
files.each do |key|
|
|
398
484
|
raw.dig("files", key)&.merge!({ "merged_version" => version.to_s, "merged_at" => at.iso8601, "merged_trash" => trash }.compact)
|
|
399
485
|
end
|
|
@@ -409,7 +495,7 @@ module Samagotchi
|
|
|
409
495
|
def clear_merge_notes(notes, files: [])
|
|
410
496
|
return if notes.empty? && files.empty?
|
|
411
497
|
|
|
412
|
-
raw =
|
|
498
|
+
raw = writable_record
|
|
413
499
|
stale = notes.select do |note|
|
|
414
500
|
entry = raw.dig("files", note.file)
|
|
415
501
|
entry && entry["merged_version"] == note.version && entry["merged_at"] == note.at
|
|
@@ -419,6 +505,8 @@ module Samagotchi
|
|
|
419
505
|
MERGE_NOTE_FIELDS.each { |field| entry.delete(field) } if entry
|
|
420
506
|
end
|
|
421
507
|
write_manifest(raw)
|
|
508
|
+
rescue StateFormat::InvalidDocument => e
|
|
509
|
+
e.message
|
|
422
510
|
rescue JSON::ParserError, SystemCallError
|
|
423
511
|
nil
|
|
424
512
|
end
|
|
@@ -437,7 +525,10 @@ module Samagotchi
|
|
|
437
525
|
def adopt_manifest(dir, dry_run: false)
|
|
438
526
|
bundle = record
|
|
439
527
|
return "#{@name} isn't installed" unless bundle
|
|
528
|
+
|
|
440
529
|
return "#{@name} is a dev install" if bundle.dev
|
|
530
|
+
|
|
531
|
+
raw = writable_record
|
|
441
532
|
return "#{@name} keeps its manifest already" unless bundle.manifest_unkept?
|
|
442
533
|
|
|
443
534
|
source = File.join(dir.to_s, "manifest.yml")
|
|
@@ -449,9 +540,10 @@ module Samagotchi
|
|
|
449
540
|
return why if why || dry_run
|
|
450
541
|
|
|
451
542
|
AtomicFile.write(kept_manifest_path, bytes)
|
|
452
|
-
raw = JSON.parse(File.read(File.join(@bundle_dir, "manifest.json")))
|
|
453
543
|
write_manifest(raw.merge("manifest_sha" => "sha256:#{Digest::SHA256.hexdigest(bytes)}"))
|
|
454
544
|
nil
|
|
545
|
+
rescue StateFormat::InvalidDocument => e
|
|
546
|
+
e.message
|
|
455
547
|
rescue JSON::ParserError, SystemCallError => e
|
|
456
548
|
"#{@name}'s record can't take it: #{e.message}"
|
|
457
549
|
end
|
|
@@ -463,6 +555,32 @@ module Samagotchi
|
|
|
463
555
|
InstalledBundle.read(@name, File.join(@bundle_dir, "manifest.json"))
|
|
464
556
|
end
|
|
465
557
|
|
|
558
|
+
def ensure_supported_record_format!
|
|
559
|
+
format_record(read)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# The journal's memory => shas, {} when it doesn't belong to this target
|
|
563
|
+
# or doesn't read.
|
|
564
|
+
def pending_files(target_dir:)
|
|
565
|
+
return {} if target_dir.to_s.empty?
|
|
566
|
+
|
|
567
|
+
metadata = pending_metadata
|
|
568
|
+
return {} unless metadata && File.expand_path(target_dir.to_s) == metadata["target_dir"]
|
|
569
|
+
|
|
570
|
+
files = metadata["files"]
|
|
571
|
+
files.is_a?(Hash) ? files.to_h { |key, shas| [key, Array(shas).grep(String)] } : {}
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def pending_metadata
|
|
575
|
+
data = JSON.parse(File.read(pending_path))
|
|
576
|
+
return unless data.is_a?(Hash) && data["scope"].is_a?(String) && data["target_dir"].is_a?(String)
|
|
577
|
+
|
|
578
|
+
data
|
|
579
|
+
rescue JSON::ParserError, EncodingError, SystemCallError
|
|
580
|
+
nil
|
|
581
|
+
end
|
|
582
|
+
private :pending_files, :pending_metadata
|
|
583
|
+
|
|
466
584
|
# manifest.json as it is on disk, symbol keys (nil if not installed).
|
|
467
585
|
# Readers take #record.
|
|
468
586
|
def read
|
|
@@ -472,6 +590,40 @@ module Samagotchi
|
|
|
472
590
|
JSON.parse(File.read(manifest_path), symbolize_names: true)
|
|
473
591
|
end
|
|
474
592
|
|
|
593
|
+
def writable_record
|
|
594
|
+
raw = JSON.parse(File.read(File.join(@bundle_dir, "manifest.json")))
|
|
595
|
+
format_record(raw)
|
|
596
|
+
raw
|
|
597
|
+
end
|
|
598
|
+
private :writable_record
|
|
599
|
+
|
|
600
|
+
def format_record(data)
|
|
601
|
+
return unless data
|
|
602
|
+
raise StateFormat::InvalidDocument, "manifest.json must contain a JSON object" unless data.is_a?(Hash)
|
|
603
|
+
|
|
604
|
+
data = data.transform_keys(&:to_s)
|
|
605
|
+
version = data["record_format"]
|
|
606
|
+
unless version.nil? || (version.is_a?(Integer) && version >= 0)
|
|
607
|
+
raise StateFormat::InvalidDocument, "manifest.json has an invalid record_format"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
document = RECORD_FORMAT.read({ "version" => version, "records" => [data] }, collection: "records")
|
|
611
|
+
if document.version > RECORD_FORMAT_VERSION
|
|
612
|
+
raise StateFormat::InvalidDocument,
|
|
613
|
+
"manifest.json record_format #{document.version} is newer than supported #{RECORD_FORMAT_VERSION}; update chi before writing it"
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
record = document.entries.first
|
|
617
|
+
unless record&.readable?
|
|
618
|
+
raise StateFormat::InvalidDocument,
|
|
619
|
+
"manifest.json is missing required record fields; repair with chi bundle uninstall #{@name} --force, " \
|
|
620
|
+
"then chi bundle install <source> --force"
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
record
|
|
624
|
+
end
|
|
625
|
+
private :format_record
|
|
626
|
+
|
|
475
627
|
# Write aside and rename, so a reader in another process (a parallel
|
|
476
628
|
# chi start) never parses a truncated manifest.json.
|
|
477
629
|
def write_manifest(manifest_data)
|
|
@@ -52,7 +52,8 @@ module Samagotchi
|
|
|
52
52
|
# mismatch warnings. merged and replaced are file
|
|
53
53
|
# names, behind FileStanding::Behind; under plan they are what apply would
|
|
54
54
|
# merge, replace or leave behind.
|
|
55
|
-
#
|
|
55
|
+
# added_files is the new version's memory files the bundle didn't own
|
|
56
|
+
# before; adds is nil but for a profile: the members it installs.
|
|
56
57
|
# kept_own is nil but for a profile that installed a member: its
|
|
57
58
|
# Profile.kept_own_line lines.
|
|
58
59
|
# trash_copy is nil but under apply when it saved edited files
|
|
@@ -62,8 +63,8 @@ module Samagotchi
|
|
|
62
63
|
# manifest: what a row at the shipped version says of its manifest.yml
|
|
63
64
|
# (ManifestStanding.keep's line: would keep or kept it, or why not and
|
|
64
65
|
# the command), nil when nothing.
|
|
65
|
-
Row = Struct.new(:name, :from, :to, :status, :note, :merged, :behind, :replaced, :source_dir, :scope, :
|
|
66
|
-
:trash_copy, :earlier, :manifest, keyword_init: true) do
|
|
66
|
+
Row = Struct.new(:name, :from, :to, :status, :note, :merged, :behind, :replaced, :source_dir, :scope, :added_files, :adds,
|
|
67
|
+
:kept_own, :trash_copy, :earlier, :manifest, keyword_init: true) do
|
|
67
68
|
def kept = Array(behind).map(&:file)
|
|
68
69
|
end
|
|
69
70
|
|
|
@@ -208,6 +209,7 @@ module Samagotchi
|
|
|
208
209
|
return skip(row, "project scope: chi bundle upgrade #{ship.source} in the project") if row.scope == "project"
|
|
209
210
|
return skip(row, "unknown scope #{row.scope}: upgrade chi or reinstall") unless MemoryPaths.scope_dir(row.scope)
|
|
210
211
|
|
|
212
|
+
row.added_files = (manifest.files.keys - bundle.files.keys).sort
|
|
211
213
|
forecast = forecast(row, bundle)
|
|
212
214
|
row.merged = forecast.merged
|
|
213
215
|
row.behind = forecast.behind
|
|
@@ -290,8 +292,9 @@ module Samagotchi
|
|
|
290
292
|
# Merger::Upgrade#forecast.
|
|
291
293
|
def forecast(row, bundle)
|
|
292
294
|
shipped = Dir.glob(File.join(row.source_dir, "*.md")).map { |f| File.basename(f) }.sort
|
|
295
|
+
files = Provenance.new(name: row.name).files_with_pending(bundle, target_dir: MemoryPaths.scope_dir!(row.scope))
|
|
293
296
|
Merger::Upgrade.new(provenance: Provenance.new(name: row.name), target_dir: MemoryPaths.scope_dir!(row.scope),
|
|
294
|
-
source_dir: row.source_dir).forecast(shipped &
|
|
297
|
+
source_dir: row.source_dir).forecast(shipped & files.keys, version: row.to)
|
|
295
298
|
end
|
|
296
299
|
|
|
297
300
|
# What the upgrade from +source_dir+ copies to the trash, as
|
|
@@ -39,7 +39,7 @@ module Samagotchi
|
|
|
39
39
|
|
|
40
40
|
scope = (scope_override ? bundle.with(scope: scope_override) : bundle).effective_scope
|
|
41
41
|
target_dir = MemoryPaths.scope_dir(scope)
|
|
42
|
-
files = bundle
|
|
42
|
+
files = provenance.files_with_pending(bundle, target_dir: target_dir)
|
|
43
43
|
details = {}
|
|
44
44
|
behind = bundle.behind_files(target_dir).to_h { |file| [file.file, file] }
|
|
45
45
|
notes = bundle.merge_notes.to_h { |note| [note.file, note] }
|
|
@@ -84,12 +84,12 @@ module Samagotchi
|
|
|
84
84
|
private_class_method :digest_or_nil
|
|
85
85
|
|
|
86
86
|
# The class-file hooks the record still has (a 0.157 install's; chi
|
|
87
|
-
# loads none, Manifest::HooksRetired), each "
|
|
87
|
+
# loads none, Manifest::HooksRetired), each "present" or "missing" in
|
|
88
88
|
# hooks/, by name; {} for none.
|
|
89
89
|
# @return [Hash{String => String}]
|
|
90
90
|
def self.retired_hooks(provenance, bundle)
|
|
91
91
|
bundle.hooks.keys.sort.to_h do |file|
|
|
92
|
-
[file, File.exist?(File.join(provenance.hooks_dir, file)) ? "
|
|
92
|
+
[file, File.exist?(File.join(provenance.hooks_dir, file)) ? "present" : "missing"]
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
|
@@ -84,6 +84,8 @@ module Samagotchi
|
|
|
84
84
|
# Those notes have been shown (Provenance#clear_merge_notes), under the lock.
|
|
85
85
|
def clear_merge_notes(notes, files: [])
|
|
86
86
|
with_lock { Provenance.new(name: BUNDLE_NAME).clear_merge_notes(notes, files: files) }
|
|
87
|
+
rescue StateFormat::InvalidDocument => e
|
|
88
|
+
e.message
|
|
87
89
|
rescue StandardError
|
|
88
90
|
nil
|
|
89
91
|
end
|
|
@@ -44,18 +44,30 @@ module Samagotchi
|
|
|
44
44
|
def run
|
|
45
45
|
provenance = Provenance.new(name: @name)
|
|
46
46
|
bundle = provenance.record
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
if bundle
|
|
48
|
+
target_scope = @scope.to_s.strip.empty? ? bundle.effective_scope : @scope
|
|
49
|
+
target_dir = MemoryPaths.scope_dir(target_scope) or raise UninstallError, "invalid scope: #{target_scope}"
|
|
50
|
+
else
|
|
51
|
+
target_scope = @scope.to_s.strip.empty? ? provenance.pending_scope : @scope
|
|
52
|
+
pending_target = provenance.pending_target_dir
|
|
53
|
+
raise UninstallError.not_installed(@name) unless target_scope && pending_target
|
|
54
|
+
|
|
55
|
+
target_dir = MemoryPaths.scope_dir(target_scope) or raise UninstallError, "invalid scope: #{target_scope}"
|
|
56
|
+
if @scope.to_s.strip.empty?
|
|
57
|
+
target_dir = pending_target
|
|
58
|
+
elsif File.expand_path(target_dir) != pending_target
|
|
59
|
+
raise UninstallError.not_installed(@name)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
52
62
|
|
|
53
|
-
files = bundle
|
|
54
|
-
hooks = bundle
|
|
63
|
+
files = provenance.files_with_pending(bundle, target_dir: target_dir)
|
|
64
|
+
hooks = bundle&.hooks || {}
|
|
55
65
|
if files.empty? && hooks.empty?
|
|
56
66
|
# No file list, just remove provenance (rules, a plugin and scripts
|
|
57
67
|
# live there too)
|
|
58
68
|
save_edits(provenance, bundle)
|
|
69
|
+
plugin_path = provenance.plugin_path(bundle)
|
|
70
|
+
@removed_files << "plugin/#{File.basename(plugin_path)}" if plugin_path && File.exist?(plugin_path)
|
|
59
71
|
FileUtils.rm_rf(provenance.bundle_dir)
|
|
60
72
|
return true
|
|
61
73
|
end
|
|
@@ -96,7 +108,7 @@ module Samagotchi
|
|
|
96
108
|
|
|
97
109
|
@warnings << "Kept #{file_key_str}: bundle #{others.join(", ")} has it too"
|
|
98
110
|
unless ModelOverlay.bundle_overlay?(file_key_str, bundle_files: files.keys.map(&:to_s), target_dir: target_dir)
|
|
99
|
-
retag(target_scope, target_path, file_key_str, others.first)
|
|
111
|
+
retag(target_scope, target_path, file_key_str, others.first, target_dir: target_dir)
|
|
100
112
|
end
|
|
101
113
|
next
|
|
102
114
|
end
|
|
@@ -106,8 +118,8 @@ module Samagotchi
|
|
|
106
118
|
end
|
|
107
119
|
# Remove index line (not fatal: a warning says so)
|
|
108
120
|
begin
|
|
109
|
-
IndexUpdater.remove_index(target_scope, file_key_str.delete_suffix(".md"))
|
|
110
|
-
IndexUpdater.remove_index(target_scope, file_key_str) # legacy "name.md" line
|
|
121
|
+
IndexUpdater.remove_index(target_scope, file_key_str.delete_suffix(".md"), target_dir: target_dir)
|
|
122
|
+
IndexUpdater.remove_index(target_scope, file_key_str, target_dir: target_dir) # legacy "name.md" line
|
|
111
123
|
rescue StandardError => e
|
|
112
124
|
@warnings << "index.md: line for #{file_key_str.delete_suffix(".md")} not removed (#{e.message})"
|
|
113
125
|
end
|
|
@@ -149,6 +161,8 @@ module Samagotchi
|
|
|
149
161
|
# scripts by their source-less AssetState and ASSET_UNINSTALL's
|
|
150
162
|
# save_first; an older chi's hooks/ by Trash#copy_edits.
|
|
151
163
|
def save_edits(provenance, bundle)
|
|
164
|
+
return unless bundle
|
|
165
|
+
|
|
152
166
|
@trash.copy_edits(provenance.hooks_dir, "hooks", bundle.hooks.transform_values(&:sha256))
|
|
153
167
|
{ rule: [provenance.guardrails_dir, bundle.guardrails], plugin: [provenance.plugin_dir, bundle.plugin_shas],
|
|
154
168
|
script: [provenance.scripts_dir, bundle.scripts] }.each do |kind_of_file, (dir, recorded)|
|
|
@@ -159,9 +173,11 @@ module Samagotchi
|
|
|
159
173
|
end
|
|
160
174
|
|
|
161
175
|
# A kept shared file's index line names the bundle that still has it
|
|
162
|
-
# (a model overlay has none: the caller skips it).
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
# (a model overlay has none: the caller skips it). +target_dir+ is the
|
|
177
|
+
# folder the file lives in, which for an install interrupted before its
|
|
178
|
+
# record is the journal's target dir, not the current folder's.
|
|
179
|
+
def retag(scope, path, file_key, bundle, target_dir:)
|
|
180
|
+
IndexUpdater.update_index(scope, file_key.delete_suffix(".md"), File.size(path), source: bundle, target_dir: target_dir)
|
|
165
181
|
rescue StandardError => e
|
|
166
182
|
@warnings << "index.md: line for #{file_key.delete_suffix(".md")} not updated (#{e.message})"
|
|
167
183
|
end
|