samagotchi 0.150.0 → 0.152.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 +50 -1
- data/docs/architecture.md +2 -2
- data/docs/bundle-authoring.md +5 -5
- data/docs/cli.md +32 -7
- data/docs/configuration.md +1 -1
- data/docs/guardrails.md +4 -0
- data/docs/internals/block-renderers.md +98 -0
- data/docs/internals/bridge.md +53 -17
- data/docs/internals/bundles.md +49 -14
- data/docs/internals/objectives.md +6 -4
- data/docs/internals/web-frontend.md +2 -2
- data/docs/memory.md +7 -2
- data/docs/plugins.md +91 -14
- data/docs/releasing.md +7 -2
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/block_renderers.rb +116 -0
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +219 -122
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/check.rb +43 -3
- data/lib/samagotchi/bundle_command/installed_check.rb +6 -2
- data/lib/samagotchi/bundle_command/try/sandbox.rb +11 -3
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +88 -10
- data/lib/samagotchi/bundle_needs.rb +4 -6
- data/lib/samagotchi/bundle_settings_check.rb +14 -13
- data/lib/samagotchi/bundles/agents-md/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +3 -3
- data/lib/samagotchi/child_env.rb +15 -0
- data/lib/samagotchi/context_providers.rb +42 -10
- data/lib/samagotchi/engine.rb +47 -6
- data/lib/samagotchi/extension_load.rb +4 -3
- data/lib/samagotchi/guardrail_wiring.rb +28 -19
- data/lib/samagotchi/guardrails/approval.rb +2 -2
- data/lib/samagotchi/guardrails/rules.rb +1 -0
- data/lib/samagotchi/guardrails/verdict.rb +20 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +5 -7
- data/lib/samagotchi/memory_bundle/asset_installer.rb +18 -9
- data/lib/samagotchi/memory_bundle/builder.rb +39 -31
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +3 -2
- data/lib/samagotchi/memory_bundle/file_digest.rb +31 -0
- data/lib/samagotchi/memory_bundle/file_standing.rb +2 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +55 -43
- data/lib/samagotchi/memory_bundle/installed_copy.rb +38 -14
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +224 -0
- data/lib/samagotchi/memory_bundle/installer.rb +91 -16
- data/lib/samagotchi/memory_bundle/manifest.rb +166 -5
- data/lib/samagotchi/memory_bundle/manifest_standing.rb +171 -0
- data/lib/samagotchi/memory_bundle/merger.rb +7 -8
- data/lib/samagotchi/memory_bundle/profile.rb +3 -2
- data/lib/samagotchi/memory_bundle/provenance.rb +220 -19
- data/lib/samagotchi/memory_bundle/shas.rb +4 -3
- data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -24
- data/lib/samagotchi/memory_bundle/status.rb +11 -10
- data/lib/samagotchi/memory_bundle/system_bundle.rb +13 -2
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_config.rb +5 -4
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/system_one/record.rb +10 -2
- data/lib/samagotchi/terminal_ui.rb +4 -4
- data/lib/samagotchi/tools/delegate.rb +20 -2
- data/lib/samagotchi/turn_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -3
- data/lib/samagotchi/update_command.rb +13 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +24 -13
- data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
- data/lib/samagotchi/web/markdown_renderer.rb +39 -6
- data/lib/samagotchi/web/message_display.rb +3 -3
- data/lib/samagotchi/web/participant_view.rb +1 -1
- data/lib/samagotchi/web/public/app.js +2 -0
- data/lib/samagotchi/web/public/block_frame.js +202 -0
- data/lib/samagotchi/web/public/block_host.js +502 -0
- data/lib/samagotchi/web/public/block_renderers.css +20 -0
- data/lib/samagotchi/web/public/block_source.js +37 -0
- data/lib/samagotchi/web/public/bubbles.js +9 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +2 -0
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- data/lib/samagotchi/web/public/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/session_access_routes.rb +2 -1
- data/lib/samagotchi/web/session_hub.rb +6 -0
- data/lib/samagotchi/web/session_show.rb +1 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +13 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
-
|
|
4
|
+
require_relative "file_digest"
|
|
5
5
|
require_relative "../memory_paths"
|
|
6
6
|
require_relative "provenance"
|
|
7
7
|
require_relative "manifest"
|
|
@@ -21,14 +21,16 @@ module Samagotchi
|
|
|
21
21
|
#
|
|
22
22
|
# Flow (#run):
|
|
23
23
|
# 1. Normalize source → directory, read manifest.yml (load_source)
|
|
24
|
-
# 2.
|
|
24
|
+
# 2. Refuse a bundle whose requires_chi this chi doesn't meet,
|
|
25
|
+
# unless force (refuse_too_new)
|
|
26
|
+
# 3. Copy .md files to the target scope dir and update index.md
|
|
25
27
|
# (install_memories: plain, dry run, or an upgrade's 3-way merge)
|
|
26
|
-
#
|
|
28
|
+
# 4. Hooks, guardrail rules, plugin, scripts into .bundles/<name>/
|
|
27
29
|
# (AssetInstaller)
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
30
|
+
# 5. An upgrade prunes the files the bundle dropped
|
|
31
|
+
# 6. Verify checksums (strict mode), warn about missing needs
|
|
32
|
+
# 7. Write provenance (.bundles/<name>/manifest.json, write_provenance)
|
|
33
|
+
# 8. Detect and report {{placeholders}}
|
|
32
34
|
class Installer
|
|
33
35
|
class InstallError < StandardError; end
|
|
34
36
|
|
|
@@ -42,8 +44,10 @@ module Samagotchi
|
|
|
42
44
|
# shipped_dir: the bundles this chi ships (a bundle of their names
|
|
43
45
|
# installs from a dir in it only, unless force); ShippedUpdate,
|
|
44
46
|
# Profile and SystemBundle pass the one they install from.
|
|
47
|
+
# chi_version: the chi this install is for (Profile and ShippedUpdate
|
|
48
|
+
# check their bundles against a version they are given); default this chi.
|
|
45
49
|
def initialize(source:, name:, scope: nil, force: false, strict: true, upgrade: false, dry_run: false, prepared: nil,
|
|
46
|
-
shipped_dir: SourceNormalizer::SHIPPED_DIR, dev: false)
|
|
50
|
+
shipped_dir: SourceNormalizer::SHIPPED_DIR, dev: false, chi_version: Samagotchi::VERSION)
|
|
47
51
|
@source = source
|
|
48
52
|
@shipped_dir = shipped_dir
|
|
49
53
|
@prepared = prepared
|
|
@@ -54,6 +58,7 @@ module Samagotchi
|
|
|
54
58
|
@upgrade = upgrade
|
|
55
59
|
@dry_run = dry_run
|
|
56
60
|
@dev = dev
|
|
61
|
+
@chi_version = chi_version
|
|
57
62
|
@results = {}
|
|
58
63
|
@warnings = []
|
|
59
64
|
@placeholder_warnings = []
|
|
@@ -99,12 +104,14 @@ module Samagotchi
|
|
|
99
104
|
|
|
100
105
|
def run
|
|
101
106
|
load_source
|
|
107
|
+
refuse_too_new
|
|
108
|
+
warn_unknown_keys(@manifest) if @manifest
|
|
102
109
|
@target_scope, @target_dir = resolve_target
|
|
103
110
|
@file_descriptions = @manifest&.file_descriptions || {}
|
|
104
111
|
@provenance = Provenance.new(name: @name)
|
|
105
112
|
@existing = @provenance.record
|
|
106
113
|
check_source
|
|
107
|
-
# A plain install over an installed bundle: summary
|
|
114
|
+
# A plain install over an installed bundle: its summary says so.
|
|
108
115
|
@reinstall = @existing && !@upgrade && !@force && !@dry_run
|
|
109
116
|
installed = replace_files
|
|
110
117
|
verify_memory_checksums if @manifest && @strict
|
|
@@ -138,7 +145,15 @@ module Samagotchi
|
|
|
138
145
|
merged = merged_files
|
|
139
146
|
would_merge = @results.select { |_, r| r[:status].to_s == "would_merge" }.keys
|
|
140
147
|
kept = @results.select { |_, r| %w[kept keep kept_pruned].include?(r[:status].to_s) }.keys
|
|
141
|
-
|
|
148
|
+
# A file whose bytes are already the bundle's was not left out: it is
|
|
149
|
+
# up to date. Skipped keeps the files really left out -- the user's own
|
|
150
|
+
# memory, and a dry run's would_skip (a dry run's `noop` carries no
|
|
151
|
+
# reason, so it stays here).
|
|
152
|
+
up_to_date = @results.select { |_, r| r[:reason] == "already up to date" }.keys
|
|
153
|
+
skipped = @results.select do |_, r|
|
|
154
|
+
status = r[:status].to_s
|
|
155
|
+
status == "would_skip" || status == "noop" || (status == "skipped" && r[:reason] != "already up to date")
|
|
156
|
+
end.keys
|
|
142
157
|
would_install = @results.select { |_, r| r[:status].to_s == "would_install" }.keys
|
|
143
158
|
fast_forward = @results.select { |_, r| r[:status].to_s == "fast_forward" }.keys
|
|
144
159
|
removed = @results.select { |_, r| r[:status].to_s == "removed" }.keys
|
|
@@ -153,17 +168,52 @@ module Samagotchi
|
|
|
153
168
|
lines << "Removed (no longer in the bundle): #{removed.join(", ")}#{" (moved to #{trash_dir})" if trash_dir}" unless removed.empty?
|
|
154
169
|
lines << "Would remove (no longer in the bundle): #{would_remove.join(", ")}" unless would_remove.empty?
|
|
155
170
|
lines << "Kept (local edits preserved): #{kept.join(", ")}" unless kept.empty?
|
|
171
|
+
lines << "Up to date: #{up_to_date.join(", ")}" unless up_to_date.empty?
|
|
156
172
|
lines << "Skipped: #{skipped.join(", ")}" unless skipped.empty?
|
|
157
173
|
lines << trash_copy_line if trash_copy_line
|
|
158
174
|
lines.concat(@warnings) unless @warnings.empty?
|
|
159
175
|
lines.concat(@placeholder_warnings) unless @placeholder_warnings.empty?
|
|
160
|
-
lines <<
|
|
176
|
+
lines << reinstall_line if @reinstall
|
|
161
177
|
|
|
162
178
|
lines.any? ? lines.join("\n") : "No files to install."
|
|
163
179
|
end
|
|
164
180
|
|
|
181
|
+
# The one words for a record whose source can't be read again (relative,
|
|
182
|
+
# empty or gone): what `chi bundle upgrade NAME` refuses with and
|
|
183
|
+
# ManifestStanding names. An upgrade from a copy switches the source.
|
|
184
|
+
def self.source_gone_line(name, source)
|
|
185
|
+
recorded = source.to_s.strip
|
|
186
|
+
owner = recorded.empty? ? "#{name} has no recorded source" : "#{name}'s recorded source #{recorded} is gone"
|
|
187
|
+
"#{owner}: chi bundle upgrade <path or URL of a copy>"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The one words for a dev install an upgrade or a plain install meets:
|
|
191
|
+
# it follows its source already. `Installer#refuse_dev_mismatch` raises
|
|
192
|
+
# with them, and `chi bundle upgrade NAME` (which never reaches the
|
|
193
|
+
# Installer for a dev record) prints the same line.
|
|
194
|
+
def self.dev_install_line(name, source)
|
|
195
|
+
"#{name} is a dev install of #{source}: it follows its source already " \
|
|
196
|
+
"(to leave dev mode: chi bundle uninstall #{name}, then chi bundle install without --dev)"
|
|
197
|
+
end
|
|
198
|
+
|
|
165
199
|
private
|
|
166
200
|
|
|
201
|
+
# A plain install over a bundle that is already installed from the same
|
|
202
|
+
# source: it rewrote the bundle's hooks, rules, plugin, scripts and its
|
|
203
|
+
# record, and left a memory the user wrote as it was. So the line says
|
|
204
|
+
# what changed, and points at the upgrade for the memories it kept.
|
|
205
|
+
def reinstall_line
|
|
206
|
+
line = "#{@name} was already installed from #{@source}: its hooks, rules, plugin and scripts " \
|
|
207
|
+
"now match it and its record was rewritten"
|
|
208
|
+
# `already exists` is the only reason a memory is left out of a plain
|
|
209
|
+
# install (skip_existing): its bytes differ from the bundle's.
|
|
210
|
+
if @results.any? { |_, r| r[:reason] == "already exists" }
|
|
211
|
+
line << "; your own copies of its memories were kept (chi bundle upgrade #{@name} " \
|
|
212
|
+
"merges the bundle's changes into them)"
|
|
213
|
+
end
|
|
214
|
+
line
|
|
215
|
+
end
|
|
216
|
+
|
|
167
217
|
# The steps that replace files: memories, the bundle's own folder, then
|
|
168
218
|
# the files it dropped. The record isn't written yet, so a failed run is
|
|
169
219
|
# retried as the same upgrade.
|
|
@@ -176,7 +226,7 @@ module Samagotchi
|
|
|
176
226
|
installed = AssetInstaller.new(
|
|
177
227
|
name: @name, source_dir: @bundle_dir, manifest: @manifest, provenance: @provenance, existing: @existing,
|
|
178
228
|
results: @results, warnings: @warnings, conflicts: @conflicts, trash: @trash,
|
|
179
|
-
force: @force, strict: @strict, upgrade: @upgrade, dry_run: @dry_run, dev: @dev
|
|
229
|
+
force: @force, strict: @strict, upgrade: @upgrade, dry_run: @dry_run, dev: @dev, chi_version: @chi_version
|
|
180
230
|
).run
|
|
181
231
|
|
|
182
232
|
# Files the previous version shipped and this one doesn't.
|
|
@@ -232,9 +282,9 @@ module Samagotchi
|
|
|
232
282
|
return if @existing.nil? || @existing.dev == @dev
|
|
233
283
|
|
|
234
284
|
if @existing.dev
|
|
235
|
-
raise InstallError,
|
|
236
|
-
"(to leave dev mode: chi bundle uninstall #{@name}, then chi bundle install without --dev)"
|
|
285
|
+
raise InstallError, self.class.dev_install_line(@name, @existing.source)
|
|
237
286
|
end
|
|
287
|
+
|
|
238
288
|
raise InstallError, "#{@name} is installed normally; `chi bundle uninstall #{@name}` first, then chi bundle install --dev"
|
|
239
289
|
end
|
|
240
290
|
|
|
@@ -298,6 +348,19 @@ module Samagotchi
|
|
|
298
348
|
@warnings << "No manifest found — proceeding without strict manifest validation"
|
|
299
349
|
end
|
|
300
350
|
|
|
351
|
+
# A bundle whose requires_chi this chi doesn't meet is not installed at
|
|
352
|
+
# all: --force is what installs it, and its plugin, hooks and rules then
|
|
353
|
+
# warn that they won't load (AssetInstaller). The words come from
|
|
354
|
+
# Manifest.requires_chi_failure, so the requirement is said one way.
|
|
355
|
+
# A dry run is refused the same way: nothing there would load either.
|
|
356
|
+
# @raise [InstallError]
|
|
357
|
+
def refuse_too_new
|
|
358
|
+
failure = Manifest.requires_chi_failure(@manifest&.requires_chi, @chi_version)
|
|
359
|
+
return if failure.nil? || @force
|
|
360
|
+
|
|
361
|
+
raise InstallError, "#{@name} can't be installed: #{failure} (chi update, or --force to install it anyway)"
|
|
362
|
+
end
|
|
363
|
+
|
|
301
364
|
# The scope the files go to (the CLI's wins over the manifest's) and
|
|
302
365
|
# its dir, created.
|
|
303
366
|
# @return [Array(String, String)] scope, dir
|
|
@@ -473,7 +536,7 @@ module Samagotchi
|
|
|
473
536
|
expected = @manifest.checksum_for(file_key)
|
|
474
537
|
next unless expected
|
|
475
538
|
|
|
476
|
-
actual =
|
|
539
|
+
actual = FileDigest.hex(target_path)
|
|
477
540
|
if actual != expected
|
|
478
541
|
@warnings << "Checksum mismatch for #{file_key}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
479
542
|
end
|
|
@@ -516,12 +579,14 @@ module Samagotchi
|
|
|
516
579
|
incoming_dir: @bundle_dir,
|
|
517
580
|
scripts_files: installed.script_files,
|
|
518
581
|
context_providers: @manifest.context_providers,
|
|
582
|
+
block_renderers: @manifest.block_renderers,
|
|
519
583
|
file_descriptions: @manifest.file_descriptions,
|
|
520
584
|
session_settings: @manifest.session_settings,
|
|
521
585
|
session_setting_types: @manifest.session_setting_types,
|
|
522
586
|
settings: @manifest.settings,
|
|
523
587
|
setting_types: @manifest.setting_types,
|
|
524
|
-
dev: @dev
|
|
588
|
+
dev: @dev,
|
|
589
|
+
manifest_from: @bundle_dir
|
|
525
590
|
)
|
|
526
591
|
# Replaced by --force: an earlier merge into it no longer says anything true.
|
|
527
592
|
@provenance.clear_merge_notes([], files: @replaced)
|
|
@@ -671,6 +736,16 @@ module Samagotchi
|
|
|
671
736
|
File.exist?(base_path) && Provenance.file_sha(target) == Provenance.file_sha(base_path)
|
|
672
737
|
end
|
|
673
738
|
|
|
739
|
+
# One warning per manifest key this chi's parser never reads (docs/cli.md's
|
|
740
|
+
# unknown-key): it is ignored, so the advice is chi's own. A dry run warns
|
|
741
|
+
# too: the key is in the manifest either way.
|
|
742
|
+
def warn_unknown_keys(manifest)
|
|
743
|
+
manifest.unknown_keys.each do |key|
|
|
744
|
+
@warnings << "#{Manifest.unknown_key_line(key)}; a newer chi reads it from the installed manifest " \
|
|
745
|
+
"(chi update, then chi bundle status #{manifest.name} says whether it needs chi bundle upgrade #{manifest.name})"
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
|
|
674
749
|
# Not fatal, like update_target_index: a warning says so.
|
|
675
750
|
def remove_target_index(scope, file_key)
|
|
676
751
|
IndexUpdater.remove_index(scope, file_key.delete_suffix(".md"))
|
|
@@ -4,9 +4,12 @@ require "yaml"
|
|
|
4
4
|
require "digest"
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require_relative "../context_providers"
|
|
7
|
+
require_relative "../version"
|
|
7
8
|
require_relative "bundle_hook"
|
|
8
9
|
|
|
9
10
|
module Samagotchi
|
|
11
|
+
autoload :BlockRenderers, File.expand_path("../block_renderers", __dir__)
|
|
12
|
+
|
|
10
13
|
module MemoryBundle
|
|
11
14
|
# Parses and validates a bundle manifest.yml.
|
|
12
15
|
#
|
|
@@ -41,6 +44,9 @@ module Samagotchi
|
|
|
41
44
|
# cmd: '{ruby} {bundle_dir}/scripts/pr_context.rb {url}'
|
|
42
45
|
# why: GitHub PR # optional
|
|
43
46
|
# every_seconds: 300 # optional
|
|
47
|
+
# block_renderers: # optional — a fence's language → the scripts/
|
|
48
|
+
# - lang: mermaid # files chi web's page loads to draw it
|
|
49
|
+
# load: [lib.js, draw.js] # (BlockRenderers); each one a scripts: key
|
|
44
50
|
# session_settings: [cap] # optional — the settings a session may set
|
|
45
51
|
# # (chi --set cost-cap.cap=3), read by the
|
|
46
52
|
# # plugin from ctx.settings at call time;
|
|
@@ -67,15 +73,51 @@ module Samagotchi
|
|
|
67
73
|
# A bundle name, as `chi bundle install <name>` takes a shipped one.
|
|
68
74
|
BUNDLE_NAME = /\A[A-Za-z0-9][A-Za-z0-9_-]*\z/
|
|
69
75
|
|
|
76
|
+
# Every top-level manifest key this chi reads (requires_chi_next is known
|
|
77
|
+
# but read only by the release tooling: docs/releasing.md). A key not
|
|
78
|
+
# here is one this chi ignores: #unknown_keys, which `chi bundle check`
|
|
79
|
+
# and the Installer say.
|
|
80
|
+
KEYS = %w[name version scope description files hooks trust_level plugin requires_chi requires_chi_next needs
|
|
81
|
+
scripts context_providers block_renderers includes session_settings settings].freeze
|
|
82
|
+
|
|
83
|
+
# The first chi release that reads each manifest key or shape (from git:
|
|
84
|
+
# the first tag whose manifest.rb reads it), for `chi bundle check`'s
|
|
85
|
+
# requires-chi-floor on the manifest itself: a bundle whose requires_chi
|
|
86
|
+
# admits a chi that never saw the key. A key the first tag (v0.2.0)
|
|
87
|
+
# already reads has no entry. `files.description` is a files: entry given
|
|
88
|
+
# as a mapping, `*.types` a session_settings:/settings: given as a mapping
|
|
89
|
+
# of keys to types, `hooks.events` a hook entry with an events: list. A key
|
|
90
|
+
# whose release is not out yet waits here as a comment naming it, the way
|
|
91
|
+
# Plugin::Api::SINCE does, until `release:bump` gives it its version
|
|
92
|
+
# (docs/releasing.md).
|
|
93
|
+
SINCE = {
|
|
94
|
+
"includes" => "0.8.0",
|
|
95
|
+
"scripts" => "0.32.0",
|
|
96
|
+
"context_providers" => "0.32.0",
|
|
97
|
+
"files.description" => "0.34.0",
|
|
98
|
+
"session_settings" => "0.84.0",
|
|
99
|
+
"session_settings.types" => "0.92.0",
|
|
100
|
+
"settings.types" => "0.92.0",
|
|
101
|
+
"hooks.events" => "0.98.0",
|
|
102
|
+
"settings" => "0.143.0",
|
|
103
|
+
"block_renderers" => "0.151.0"
|
|
104
|
+
}.freeze
|
|
105
|
+
|
|
70
106
|
# files: name → "sha256:<hex>" (both forms); file_descriptions: name →
|
|
71
107
|
# the description a mapping gave, for the files that have one; hooks:
|
|
72
108
|
# name → BundleHook.
|
|
73
109
|
attr_reader :name, :version, :scope, :description, :files, :file_descriptions, :hooks, :trust_level, :plugin, :requires_chi, :needs, :includes,
|
|
74
|
-
:scripts, :context_providers, :session_settings, :session_setting_types, :settings, :setting_types
|
|
110
|
+
:scripts, :context_providers, :block_renderers, :session_settings, :session_setting_types, :settings, :setting_types, :unknown_keys
|
|
75
111
|
|
|
76
112
|
def initialize(path:)
|
|
77
113
|
@path = path
|
|
78
114
|
raw = load_mapping(path)
|
|
115
|
+
# Every key of the manifest, for #floor_keys to look at the shapes a
|
|
116
|
+
# newer chi reads.
|
|
117
|
+
@raw = raw
|
|
118
|
+
# A key this chi's parser never reads is one it ignores, in the order
|
|
119
|
+
# the manifest lists it.
|
|
120
|
+
@unknown_keys = raw.keys - KEYS
|
|
79
121
|
@name = require_str(raw, "name")
|
|
80
122
|
# The name is a directory under .bundles/: never a path.
|
|
81
123
|
if @name.match?(%r{[/\\\0]}) || %w[. ..].include?(@name)
|
|
@@ -94,6 +136,7 @@ module Samagotchi
|
|
|
94
136
|
@needs = self.class.parse_needs(raw["needs"])
|
|
95
137
|
@scripts = parse_scripts(raw["scripts"])
|
|
96
138
|
@context_providers = parse_context_providers(raw["context_providers"])
|
|
139
|
+
@block_renderers = parse_block_renderers(raw["block_renderers"])
|
|
97
140
|
@includes = parse_includes(raw["includes"])
|
|
98
141
|
@session_settings = self.class.parse_session_settings(raw["session_settings"])
|
|
99
142
|
@session_setting_types = self.class.parse_session_setting_types(raw["session_settings"])
|
|
@@ -118,6 +161,28 @@ module Samagotchi
|
|
|
118
161
|
!@includes.empty?
|
|
119
162
|
end
|
|
120
163
|
|
|
164
|
+
# An installed bundle's kept manifest.yml read key by key
|
|
165
|
+
# (InstalledManifest): each key parsed as #initialize parses it, in its
|
|
166
|
+
# own rescue and in this order, so a key that doesn't parse under this
|
|
167
|
+
# chi (a newer chi tightened it, or an older one wrote it) costs only
|
|
168
|
+
# that key, which reads as empty with its message in the problems:
|
|
169
|
+
# name, version (never checked: identity is the record's), scope;
|
|
170
|
+
# files and their descriptions, scripts, plugin; hooks (one bad entry
|
|
171
|
+
# fails the whole key); needs, context_providers; block_renderers
|
|
172
|
+
# (against +fallback_scripts+ when scripts: failed); includes (after
|
|
173
|
+
# what its check reads); session_settings with its types, settings
|
|
174
|
+
# with theirs (one raw value each, so one problem); requires_chi,
|
|
175
|
+
# trust_level, description. Then unknown_keys.
|
|
176
|
+
# @param fallback_scripts [Array<String>] the script names block_renderers
|
|
177
|
+
# is checked against when scripts: doesn't parse (the record's)
|
|
178
|
+
# @return [Array(Manifest, Hash{String => String})] the manifest and
|
|
179
|
+
# key => message for each key that didn't parse
|
|
180
|
+
# @raise [ValidationError] the file isn't YAML or isn't a mapping
|
|
181
|
+
def self.tolerant(path:, fallback_scripts: [])
|
|
182
|
+
manifest = allocate
|
|
183
|
+
[manifest, manifest.send(:read_tolerantly, path, fallback_scripts)]
|
|
184
|
+
end
|
|
185
|
+
|
|
121
186
|
def self.read(dir:)
|
|
122
187
|
manifest_path = File.join(dir, "manifest.yml")
|
|
123
188
|
raise ValidationError, "manifest.yml not found in #{dir}" unless File.exist?(manifest_path)
|
|
@@ -126,12 +191,14 @@ module Samagotchi
|
|
|
126
191
|
end
|
|
127
192
|
|
|
128
193
|
# Returns the expected sha256 hex digest for a given file key
|
|
129
|
-
# (e.g. "identity.md"), with the "sha256:" prefix stripped
|
|
194
|
+
# (e.g. "identity.md"), with the "sha256:" prefix stripped, nil when
|
|
195
|
+
# the entry declares none (an empty value is nothing to verify).
|
|
130
196
|
def checksum_for(file_key)
|
|
131
197
|
raw = @files[file_key]
|
|
132
198
|
return nil unless raw
|
|
133
199
|
|
|
134
|
-
raw.start_with?("sha256:") ? raw[7..] : raw
|
|
200
|
+
hex = raw.start_with?("sha256:") ? raw[7..] : raw
|
|
201
|
+
hex unless hex.nil? || hex.empty?
|
|
135
202
|
end
|
|
136
203
|
|
|
137
204
|
# Returns sha256 hex for a hook basename (e.g. "guardrails.rb"), nil
|
|
@@ -156,14 +223,47 @@ module Samagotchi
|
|
|
156
223
|
return nil if Gem::Requirement.new(*requirement.to_s.split(",").map(&:strip)).satisfied_by?(Gem::Version.new(version.to_s))
|
|
157
224
|
|
|
158
225
|
"it requires chi #{requirement} (this is chi #{version})"
|
|
159
|
-
rescue ArgumentError => e
|
|
226
|
+
rescue ArgumentError, EncodingError => e
|
|
227
|
+
# Gem::Requirement::BadRequirementError is an ArgumentError; bytes that
|
|
228
|
+
# aren't UTF-8 (a record another chi wrote) raise an EncodingError out
|
|
229
|
+
# of Gem::Requirement. Either way this chi can't read the requirement,
|
|
230
|
+
# which is a failure: the bundle is refused, not loaded.
|
|
160
231
|
"its requires_chi #{requirement.inspect} is not a version requirement (#{e.message})"
|
|
161
232
|
end
|
|
162
233
|
|
|
234
|
+
# What an unknown manifest key means, said once (docs/cli.md): the
|
|
235
|
+
# Installer and `chi bundle check` add their own fix to it.
|
|
236
|
+
def self.unknown_key_line(key, version = Samagotchi::VERSION)
|
|
237
|
+
"this chi (#{version}) doesn't know the manifest key #{key}:, so it ignores it"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# The SINCE keys this manifest actually uses, each with the chi release
|
|
241
|
+
# that first read it: the plain keys, and the shapes only a newer chi
|
|
242
|
+
# reads — a files: entry given as a mapping, a hook entry with an events:
|
|
243
|
+
# list, a session_settings:/settings: given as a mapping of keys to types.
|
|
244
|
+
# @return [Hash{String => String}]
|
|
245
|
+
def floor_keys
|
|
246
|
+
raw = @raw
|
|
247
|
+
found = {}
|
|
248
|
+
found["includes"] = SINCE["includes"] unless @includes.empty?
|
|
249
|
+
found["scripts"] = SINCE["scripts"] unless @scripts.empty?
|
|
250
|
+
found["context_providers"] = SINCE["context_providers"] unless @context_providers.empty?
|
|
251
|
+
found["block_renderers"] = SINCE["block_renderers"] unless @block_renderers.empty?
|
|
252
|
+
found["session_settings"] = SINCE["session_settings"] unless @session_settings.empty?
|
|
253
|
+
found["settings"] = SINCE["settings"] unless @settings.nil?
|
|
254
|
+
found["files.description"] = SINCE["files.description"] if raw["files"].is_a?(Hash) && raw["files"].values.any?(Hash)
|
|
255
|
+
found["session_settings.types"] = SINCE["session_settings.types"] if raw["session_settings"].is_a?(Hash)
|
|
256
|
+
found["settings.types"] = SINCE["settings.types"] if raw["settings"].is_a?(Hash)
|
|
257
|
+
if raw["hooks"].is_a?(Hash) && raw["hooks"].values.any? { |entry| entry.is_a?(Hash) && entry.key?("events") }
|
|
258
|
+
found["hooks.events"] = SINCE["hooks.events"]
|
|
259
|
+
end
|
|
260
|
+
found
|
|
261
|
+
end
|
|
262
|
+
|
|
163
263
|
# Writes a fresh manifest.yml from computed checksums at `dir`.
|
|
164
264
|
# +file_descriptions+ (name → text) writes those files as mappings.
|
|
165
265
|
def self.write(dir:, name:, version:, files:, scope: nil, description: "", hooks: nil, trust_level: nil,
|
|
166
|
-
plugin: nil, requires_chi: nil, needs: nil, scripts: {}, context_providers: [], file_descriptions: {},
|
|
266
|
+
plugin: nil, requires_chi: nil, needs: nil, scripts: {}, context_providers: [], block_renderers: [], file_descriptions: {},
|
|
167
267
|
session_settings: [], session_setting_types: {}, settings: nil, setting_types: {})
|
|
168
268
|
FileUtils.mkdir_p(dir)
|
|
169
269
|
manifest = {
|
|
@@ -187,6 +287,7 @@ module Samagotchi
|
|
|
187
287
|
manifest["needs"] = needs.map { |n| n.transform_keys(&:to_s).compact } unless needs.empty?
|
|
188
288
|
manifest["scripts"] = scripts.transform_keys(&:to_s) unless scripts.empty?
|
|
189
289
|
manifest["context_providers"] = context_providers.map(&:to_h) unless context_providers.empty?
|
|
290
|
+
manifest["block_renderers"] = block_renderers.map(&:to_h) unless block_renderers.empty?
|
|
190
291
|
manifest["session_settings"] = session_settings_entry(session_settings, session_setting_types) unless Array(session_settings).empty?
|
|
191
292
|
manifest["settings"] = session_settings_entry(settings, setting_types) unless settings.nil?
|
|
192
293
|
File.write(File.join(dir, "manifest.yml"), YAML.dump(manifest))
|
|
@@ -270,6 +371,51 @@ module Samagotchi
|
|
|
270
371
|
|
|
271
372
|
private
|
|
272
373
|
|
|
374
|
+
# Manifest.tolerant's read: the order and the keys are its comment's.
|
|
375
|
+
def read_tolerantly(path, fallback_scripts)
|
|
376
|
+
@path = path
|
|
377
|
+
raw = @raw = load_mapping(path)
|
|
378
|
+
problems = {}
|
|
379
|
+
@name = raw["name"].to_s
|
|
380
|
+
@version = raw["version"].to_s
|
|
381
|
+
@scope = normalize_scope(raw["scope"])
|
|
382
|
+
@files, @file_descriptions = tolerate(problems, "files", [{}, {}]) do
|
|
383
|
+
[parse_files(raw["files"] || {}), parse_file_descriptions(raw["files"])]
|
|
384
|
+
end
|
|
385
|
+
@scripts = tolerate(problems, "scripts", {}) { parse_scripts(raw["scripts"]) }
|
|
386
|
+
@plugin = tolerate(problems, "plugin", nil) { parse_plugin(raw["plugin"]) }
|
|
387
|
+
@hooks = tolerate(problems, "hooks", {}) { parse_hooks(raw["hooks"] || {}) }
|
|
388
|
+
@needs = tolerate(problems, "needs", []) { self.class.parse_needs(raw["needs"]) }
|
|
389
|
+
@context_providers = tolerate(problems, "context_providers", []) { parse_context_providers(raw["context_providers"]) }
|
|
390
|
+
script_names = problems.key?("scripts") ? fallback_scripts.map(&:to_s) : @scripts.keys
|
|
391
|
+
@block_renderers = tolerate(problems, "block_renderers", []) { parse_block_renderers(raw["block_renderers"], script_names) }
|
|
392
|
+
@includes = tolerate(problems, "includes", []) { parse_includes(raw["includes"]) }
|
|
393
|
+
@session_settings, @session_setting_types = tolerate(problems, "session_settings", [[], {}]) do
|
|
394
|
+
[self.class.parse_session_settings(raw["session_settings"]), self.class.parse_session_setting_types(raw["session_settings"])]
|
|
395
|
+
end
|
|
396
|
+
@settings, @setting_types = tolerate(problems, "settings", [nil, {}]) do
|
|
397
|
+
next [nil, {}] if raw["settings"].nil?
|
|
398
|
+
|
|
399
|
+
[self.class.parse_session_settings(raw["settings"], key: "settings"),
|
|
400
|
+
self.class.parse_session_setting_types(raw["settings"], key: "settings")]
|
|
401
|
+
end
|
|
402
|
+
@requires_chi = tolerate(problems, "requires_chi", nil) { parse_requires_chi(raw["requires_chi"]) }
|
|
403
|
+
@trust_level = tolerate(problems, "trust_level", "experimental") { (raw["trust_level"] || "experimental").to_s }
|
|
404
|
+
@description = tolerate(problems, "description", "") { (raw["description"] || "").to_s }
|
|
405
|
+
@unknown_keys = raw.keys - KEYS
|
|
406
|
+
problems
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# The block's value, else +empty+ with the key's problem noted. Anything
|
|
410
|
+
# a parser raises counts: the file is a load-time input, and one key
|
|
411
|
+
# must never cost the bundle.
|
|
412
|
+
def tolerate(problems, key, empty)
|
|
413
|
+
yield
|
|
414
|
+
rescue StandardError => e
|
|
415
|
+
problems[key] = e.message
|
|
416
|
+
empty
|
|
417
|
+
end
|
|
418
|
+
|
|
273
419
|
# The manifest.yml's top-level mapping ({} for an empty file); YAML that
|
|
274
420
|
# doesn't parse, or isn't a mapping, raises ValidationError, as every
|
|
275
421
|
# other manifest problem does.
|
|
@@ -362,6 +508,21 @@ module Samagotchi
|
|
|
362
508
|
raise ValidationError, e.message
|
|
363
509
|
end
|
|
364
510
|
|
|
511
|
+
# @param script_names [Array<String>] the scripts: keys each load must be
|
|
512
|
+
# @return [Array<BlockRenderers::Renderer>] each load is a scripts: key
|
|
513
|
+
def parse_block_renderers(raw, script_names = @scripts.keys)
|
|
514
|
+
return [] if raw.nil?
|
|
515
|
+
|
|
516
|
+
renderers = BlockRenderers.parse_list(raw)
|
|
517
|
+
renderers.each do |renderer|
|
|
518
|
+
missing = renderer.load - script_names
|
|
519
|
+
raise ValidationError, "block_renderers: #{renderer.lang} loads #{missing.join(", ")}, not under scripts:" unless missing.empty?
|
|
520
|
+
end
|
|
521
|
+
renderers
|
|
522
|
+
rescue BlockRenderers::Invalid => e
|
|
523
|
+
raise ValidationError, e.message
|
|
524
|
+
end
|
|
525
|
+
|
|
365
526
|
# includes: → the names in order, duplicates dropped; [] when absent.
|
|
366
527
|
# A meta ships nothing of its own, so files, hooks or a plugin next
|
|
367
528
|
# to it is a ValidationError.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "installed_bundle"
|
|
4
|
+
require_relative "installer"
|
|
5
|
+
require_relative "manifest"
|
|
6
|
+
require_relative "provenance"
|
|
7
|
+
require_relative "source"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
module MemoryBundle
|
|
11
|
+
# The one owner of the words for an installed bundle's manifest.yml, as
|
|
12
|
+
# InstalledManifest read it (chi bundle status's Manifest: line, chi
|
|
13
|
+
# update's row), and of keeping it for a record an older chi wrote
|
|
14
|
+
# (.keep: Provenance#adopt_manifest from where it came from). The cases:
|
|
15
|
+
# :kept read from the kept file; a line only when chi +installed_by+
|
|
16
|
+
# predates a key it uses (Manifest::SINCE): newer than its installer
|
|
17
|
+
# :legacy no kept file, no manifest_sha: a chi before the kept manifest installed it
|
|
18
|
+
# :stale a kept file, no manifest_sha: such a chi rewrote the record since
|
|
19
|
+
# :missing a manifest_sha, no kept file
|
|
20
|
+
# :edited the kept file isn't the recorded one (or isn't a YAML mapping)
|
|
21
|
+
# The first three and :missing are "unkept": chi update keeps the manifest
|
|
22
|
+
# where it has the bundle's own at the recorded version, else the line
|
|
23
|
+
# names the command that does.
|
|
24
|
+
module ManifestStanding
|
|
25
|
+
UNKEPT = %i[legacy stale missing].freeze
|
|
26
|
+
|
|
27
|
+
# What .keep did: bundle is the record after it (re-read when kept),
|
|
28
|
+
# line what to say (nil: nothing), kept whether it kept (or, in a dry
|
|
29
|
+
# run, would keep) the manifest.
|
|
30
|
+
Outcome = Data.define(:bundle, :line, :kept)
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# @param bundle [InstalledBundle]
|
|
35
|
+
# @return [Symbol, nil] the case, nil for a record with no dir or that doesn't read
|
|
36
|
+
def state(bundle)
|
|
37
|
+
return nil if bundle.error? || bundle.dir.nil?
|
|
38
|
+
|
|
39
|
+
manifest = bundle.manifest
|
|
40
|
+
return :kept if manifest.origin == :kept
|
|
41
|
+
|
|
42
|
+
manifest.standing || :legacy
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# "[kept]" when chi reads the kept manifest.yml, else "[from the install record]".
|
|
46
|
+
def label(bundle) = state(bundle) == :kept ? "[kept]" : "[from the install record]"
|
|
47
|
+
|
|
48
|
+
# The case in words with what to run, nil for a clean kept one.
|
|
49
|
+
# @param refused [String, Symbol, nil] why .keep didn't keep it: :git (not
|
|
50
|
+
# cloned), :gone (no source left), or Provenance#adopt_manifest's why.
|
|
51
|
+
# nil: not tried, so a source chi update can read says that it will.
|
|
52
|
+
# @return [String, nil]
|
|
53
|
+
def line(bundle, refused: nil)
|
|
54
|
+
case (state = state(bundle))
|
|
55
|
+
when :kept then newer_line(bundle)
|
|
56
|
+
when :edited then "manifest.yml changed since it was installed, so chi reads the install record (#{reinstall(bundle)})"
|
|
57
|
+
when *UNKEPT then "#{unkept_words(state)}: #{unkept_fix(bundle, refused)}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# #line as chi bundle status says it: for a record chi update would keep
|
|
62
|
+
# from its folder, a dry Provenance#adopt_manifest first, so the line says
|
|
63
|
+
# whether it will or why not (an archive isn't extracted here: chi update
|
|
64
|
+
# tries it).
|
|
65
|
+
# @return [String, nil]
|
|
66
|
+
# A bundle chi ships is checked against this chi's shipped copy, which chi
|
|
67
|
+
# update reads (at another version, chi update upgrades it instead).
|
|
68
|
+
def status_line(bundle)
|
|
69
|
+
return line(bundle) unless bundle.manifest_unkept? && !bundle.dev && !bundle.profile?
|
|
70
|
+
|
|
71
|
+
dir = try_dir(bundle)
|
|
72
|
+
dir ? line(bundle, refused: Provenance.new(name: bundle.name).adopt_manifest(dir, dry_run: true)) : line(bundle)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The folder chi update keeps it from, nil when it wouldn't try one here.
|
|
76
|
+
def try_dir(bundle)
|
|
77
|
+
source = bundle.source.to_s.strip
|
|
78
|
+
unless SourceIdentity.of(source)&.shipped?
|
|
79
|
+
return source.start_with?("/") && File.directory?(source) ? source : nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
dir = File.join(SourceNormalizer::SHIPPED_DIR, File.basename(source.chomp("/")))
|
|
83
|
+
File.file?(File.join(dir, "manifest.yml")) && Manifest.read(dir: dir).version == bundle.version.to_s ? dir : nil
|
|
84
|
+
rescue Manifest::ValidationError, Psych::Exception
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Keeps the manifest of a record without a usable kept one (UNKEPT, not
|
|
89
|
+
# dev) from +source_dir+, else from the record's source: a dir as it is,
|
|
90
|
+
# a zip or tar extracted and removed after; a git URL is never cloned.
|
|
91
|
+
# A bundle with nothing to keep gets its #line.
|
|
92
|
+
# @return [Outcome]
|
|
93
|
+
def keep(bundle, source_dir: nil, dry_run: false)
|
|
94
|
+
return Outcome.new(bundle: bundle, line: line(bundle), kept: false) unless bundle.manifest_unkept? && !bundle.dev
|
|
95
|
+
|
|
96
|
+
refused = adopt(bundle, source_dir, dry_run)
|
|
97
|
+
return Outcome.new(bundle: bundle, line: line(bundle, refused: refused), kept: false) if refused
|
|
98
|
+
|
|
99
|
+
kept = dry_run ? bundle : Provenance.new(name: bundle.name).record
|
|
100
|
+
Outcome.new(bundle: kept, line: dry_run ? "would keep its manifest" : "kept its manifest", kept: true)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @return [String, Symbol, nil] why not (as #line's refused:), nil when kept
|
|
104
|
+
def adopt(bundle, source_dir, dry_run)
|
|
105
|
+
provenance = Provenance.new(name: bundle.name)
|
|
106
|
+
return provenance.adopt_manifest(source_dir, dry_run: dry_run) if source_dir
|
|
107
|
+
|
|
108
|
+
source = bundle.source.to_s.strip
|
|
109
|
+
return :git if SourceNormalizer.git_url?(source)
|
|
110
|
+
return :gone unless local?(source)
|
|
111
|
+
return provenance.adopt_manifest(source, dry_run: dry_run) if File.directory?(source)
|
|
112
|
+
|
|
113
|
+
prepared = PreparedSource.prepare(source)
|
|
114
|
+
begin
|
|
115
|
+
provenance.adopt_manifest(prepared.dir, dry_run: dry_run)
|
|
116
|
+
ensure
|
|
117
|
+
prepared.cleanup
|
|
118
|
+
end
|
|
119
|
+
rescue SourceNormalizer::UnknownSourceError => e
|
|
120
|
+
e.message
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def unkept_words(state)
|
|
124
|
+
case state
|
|
125
|
+
when :legacy then "installed by an older chi, which didn't keep its manifest"
|
|
126
|
+
when :stale then "an older chi rewrote its record after the install, so its manifest.yml may be an earlier version's"
|
|
127
|
+
else "its kept manifest.yml is gone, so chi reads the install record"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def unkept_fix(bundle, refused)
|
|
132
|
+
source = bundle.source.to_s.strip
|
|
133
|
+
return reinstall(bundle) if bundle.dev
|
|
134
|
+
return "chi bundle upgrade #{bundle.name} reads it again from #{source}" if refused == :git || SourceNormalizer.git_url?(source)
|
|
135
|
+
return Installer.source_gone_line(bundle.name, source) if gone?(source, refused)
|
|
136
|
+
|
|
137
|
+
shipped = SourceIdentity.of(source)&.shipped?
|
|
138
|
+
# chi update keeps a profile's only when chi ships it (ShippedUpdate).
|
|
139
|
+
return "chi bundle upgrade #{bundle.name} reads it again from #{source}" if bundle.profile? && !shipped
|
|
140
|
+
|
|
141
|
+
if refused
|
|
142
|
+
upgrade = shipped ? "chi bundle upgrade #{bundle.name}" : "chi bundle upgrade #{bundle.name} reads it again from #{source}"
|
|
143
|
+
return "#{upgrade} (#{refused})"
|
|
144
|
+
end
|
|
145
|
+
return "chi update keeps it" if shipped
|
|
146
|
+
|
|
147
|
+
File.file?(source) ? "chi update tries to keep it from #{source}" : "chi update keeps it from #{source}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def gone?(source, refused) = refused == :gone || (refused.nil? && !SourceIdentity.of(source)&.shipped? && !local?(source))
|
|
151
|
+
|
|
152
|
+
# A source chi can read again here: an absolute path that is there.
|
|
153
|
+
def local?(source) = source.start_with?("/") && File.exist?(source)
|
|
154
|
+
|
|
155
|
+
def reinstall(bundle) = bundle.dev ? "chi bundle install --dev #{bundle.source}" : "chi bundle upgrade #{bundle.name}"
|
|
156
|
+
|
|
157
|
+
# The kept keys whose floor is above the chi that installed it.
|
|
158
|
+
def newer_line(bundle)
|
|
159
|
+
by = bundle.installed_by.to_s
|
|
160
|
+
return nil if by.empty? || !Gem::Version.correct?(by)
|
|
161
|
+
|
|
162
|
+
newer = bundle.manifest.floor_keys.select { |_, floor| Gem::Version.new(floor) > Gem::Version.new(by) }
|
|
163
|
+
return nil if newer.empty?
|
|
164
|
+
|
|
165
|
+
keys = newer.map { |key, floor| "#{key}: (#{floor})" }.join(", ")
|
|
166
|
+
"installed by chi #{by}, which didn't act on #{keys}: #{reinstall(bundle)}"
|
|
167
|
+
end
|
|
168
|
+
private_class_method :try_dir, :adopt, :unkept_words, :unkept_fix, :gone?, :local?, :reinstall, :newer_line
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|