samagotchi 0.131.0 → 0.132.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 +47 -1
- data/bin/chi +3 -1
- data/docs/architecture.md +32 -1
- data/docs/cli.md +29 -8
- data/docs/configuration.md +62 -6
- data/docs/desktop.md +15 -6
- data/docs/guardrails.md +3 -3
- data/docs/internals/anthropic.md +3 -2
- data/docs/internals/engine.md +4 -4
- data/docs/internals/prompt-caching.md +26 -6
- data/docs/memory.md +16 -2
- data/docs/plugins.md +68 -1
- data/lib/samagotchi/bootstrap_command.rb +35 -6
- data/lib/samagotchi/bundles/model-notes/manifest.yml +4 -1
- data/lib/samagotchi/bundles/model-notes/model_notes_tone.md +3 -0
- data/lib/samagotchi/bundles/model-notes/notes/README.md +14 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cache_confirm.rb +18 -0
- data/lib/samagotchi/chi_home.rb +311 -0
- data/lib/samagotchi/desktop/macos.rb +75 -19
- data/lib/samagotchi/engine.rb +35 -4
- data/lib/samagotchi/engine_snapshots.rb +5 -0
- data/lib/samagotchi/gem_update.rb +19 -10
- data/lib/samagotchi/guardrail_wiring.rb +3 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +6 -3
- data/lib/samagotchi/guardrails/verdict.rb +1 -1
- data/lib/samagotchi/home_command.rb +149 -0
- data/lib/samagotchi/installed_versions.rb +5 -3
- data/lib/samagotchi/llm_context_command.rb +53 -7
- data/lib/samagotchi/llm_context_override.rb +3 -2
- data/lib/samagotchi/plugin/api.rb +45 -1
- data/lib/samagotchi/plugin/context.rb +34 -1
- data/lib/samagotchi/plugin/loader.rb +5 -2
- data/lib/samagotchi/plugin/prompt_sections.rb +124 -0
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_commands.rb +64 -17
- data/lib/samagotchi/session_manager.rb +36 -1
- data/lib/samagotchi/session_model_settings.rb +5 -2
- data/lib/samagotchi/system_prompt.rb +34 -3
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +3 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +3 -0
- data/lib/samagotchi/testing/bundle.rb +22 -3
- data/lib/samagotchi/testing/host.rb +22 -1
- data/lib/samagotchi/testing/recorder.rb +6 -1
- data/lib/samagotchi/testing/recording_api.rb +1 -1
- data/lib/samagotchi/update_command.rb +65 -7
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +11 -1
- data/lib/samagotchi/web/public/index.html +6 -2
- data/lib/samagotchi/web/public/llm_context_chip.js +4 -3
- data/lib/samagotchi/web/public/model_pick.js +26 -0
- data/lib/samagotchi/web/public/model_picker.js +3 -1
- data/lib/samagotchi/web/public/start_page.js +14 -2
- data/lib/samagotchi/web/server.rb +27 -8
- data/lib/samagotchi/web/update_check.rb +3 -2
- metadata +5 -1
|
@@ -9,6 +9,7 @@ require "rbconfig"
|
|
|
9
9
|
require_relative "../atomic_file"
|
|
10
10
|
require_relative "../version"
|
|
11
11
|
require_relative "../self_report"
|
|
12
|
+
require_relative "../chi_home"
|
|
12
13
|
require_relative "../installed_gem"
|
|
13
14
|
require_relative "../config"
|
|
14
15
|
require_relative "../host_keys"
|
|
@@ -41,9 +42,16 @@ module Samagotchi
|
|
|
41
42
|
BUILD_NAME = ".chi-helper-build"
|
|
42
43
|
OLD_NAME = ".chi-helper-old"
|
|
43
44
|
# Apps started by launchd get a bare env (no shell rc files), so the
|
|
44
|
-
# launch file carries what chi needs to find its
|
|
45
|
-
# Only these, and only when set: no tokens, no runtime knobs.
|
|
46
|
-
|
|
45
|
+
# launch file carries what chi needs to find its state and tools.
|
|
46
|
+
# Only these, and only when set: no tokens, no runtime knobs. No
|
|
47
|
+
# RUBYLIB, and GEM_HOME/GEM_PATH (GEM_ENV) only for a [ruby, chi]
|
|
48
|
+
# argv, whose wrapper may find its gem only through them (a chruby
|
|
49
|
+
# GEM_HOME): the chi home's boot sets its gem path itself, and they
|
|
50
|
+
# would reach every tool its sessions run (a project's `bundle exec`).
|
|
51
|
+
ENV_ALLOWLIST = %w[XDG_CONFIG_HOME XDG_STATE_HOME].freeze
|
|
52
|
+
GEM_ENV = %w[GEM_HOME GEM_PATH].freeze
|
|
53
|
+
# What older launch files carried and a refresh drops (#refresh_launch_file).
|
|
54
|
+
DROPPED_ENV = %w[GEM_HOME GEM_PATH RUBYLIB].freeze
|
|
47
55
|
# PATH is separately filtered to absolute dirs and frozen when written,
|
|
48
56
|
# so the helper's chi, workers and execute can find the shell's tools.
|
|
49
57
|
# Without it Ruby reads stdin as US-ASCII and non-ASCII notes break.
|
|
@@ -131,7 +139,7 @@ module Samagotchi
|
|
|
131
139
|
def initialize(app_dir: nil, support_dir: nil, env: ENV, runner: Runner.new,
|
|
132
140
|
source_dir: SelfReport::SOURCE_DIR, ruby: RbConfig.ruby, version: VERSION,
|
|
133
141
|
arch: nil, sources_dir: SOURCES_DIR, register: true, chi_path: nil, terminals: {},
|
|
134
|
-
broadcast_timeout: nil)
|
|
142
|
+
broadcast_timeout: nil, chi_home: ChiHome.current(gem_spec: InstalledGem.spec(source_dir)))
|
|
135
143
|
home = env["HOME"] || Dir.home
|
|
136
144
|
@app_dir = app_dir || File.join(home, "Applications")
|
|
137
145
|
@support_dir = support_dir || File.join(home, "Library", "Application Support", APP_NAME)
|
|
@@ -146,6 +154,7 @@ module Samagotchi
|
|
|
146
154
|
@register = register
|
|
147
155
|
@terminals = terminals
|
|
148
156
|
@broadcast_timeout = broadcast_timeout
|
|
157
|
+
@chi_home = chi_home
|
|
149
158
|
end
|
|
150
159
|
|
|
151
160
|
def app_path = File.join(@app_dir, "#{APP_NAME}.app")
|
|
@@ -155,10 +164,12 @@ module Samagotchi
|
|
|
155
164
|
def executable_path = File.join(app_path, "Contents", "MacOS", EXECUTABLE)
|
|
156
165
|
def installed? = File.directory?(app_path)
|
|
157
166
|
|
|
158
|
-
# What the helper runs:
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
167
|
+
# What the helper runs: the chi home's boot (ChiHome: its ruby, its gem
|
|
168
|
+
# path in the -e string, then "--" and the helper's args) when there
|
|
169
|
+
# is one; else absolute ruby + chi (no shims, which need the shell's
|
|
170
|
+
# PATH). chi is an installed gem's RubyGems wrapper (it activates the
|
|
171
|
+
# gem and outlives upgrades and `gem cleanup`), else this checkout's
|
|
172
|
+
# bin/chi. And the allowlisted env. A section per terminal
|
|
162
173
|
# switched on (TERMINALS order); the broadcast timeout when given.
|
|
163
174
|
# The XDG dirs go in absolute (Paths): launchd starts the app in /,
|
|
164
175
|
# where a relative one would mean /<dir>. PATH is filtered to absolute,
|
|
@@ -166,16 +177,22 @@ module Samagotchi
|
|
|
166
177
|
# it is frozen at bake time.
|
|
167
178
|
def launch_config
|
|
168
179
|
env = { "LANG" => LANG }
|
|
169
|
-
|
|
180
|
+
argv = launch_argv
|
|
181
|
+
names = home_boot?(argv) ? ENV_ALLOWLIST : ENV_ALLOWLIST + GEM_ENV
|
|
182
|
+
names.each { |name| env[name] = @env[name] unless @env[name].to_s.empty? }
|
|
170
183
|
path = self.class.baked_path(@env)
|
|
171
184
|
env["PATH"] = path if path
|
|
172
185
|
env.merge!(Paths.absolute_xdg(env: env, set: :key))
|
|
173
|
-
config = { "version" => @version, "argv" =>
|
|
186
|
+
config = { "version" => @version, "argv" => argv, "env" => env, "sources_sha" => sources_sha }
|
|
174
187
|
TERMINALS.each { |name| config[name] = @terminals[name] if @terminals[name] }
|
|
175
188
|
config["broadcast_timeout"] = @broadcast_timeout if @broadcast_timeout
|
|
176
189
|
config
|
|
177
190
|
end
|
|
178
191
|
|
|
192
|
+
def launch_argv
|
|
193
|
+
@chi_home ? @chi_home.boot_argv(run: ChiHome::CHI_BOOT) + ["--"] : [@ruby, @chi_path]
|
|
194
|
+
end
|
|
195
|
+
|
|
179
196
|
# A digest of what a build compiles: the Swift sources and the
|
|
180
197
|
# Info.plist template. The launch file records the one it was built
|
|
181
198
|
# from, so a new chi rebuilds only when these changed.
|
|
@@ -209,13 +226,11 @@ module Samagotchi
|
|
|
209
226
|
|
|
210
227
|
# Why chi update can't keep the launch file's ruby or chi: one of them
|
|
211
228
|
# is gone. nil when it runs, or has no usable argv (a refresh writes
|
|
212
|
-
# this chi's then).
|
|
229
|
+
# this chi's then). Only the absolute paths in it count (a boot
|
|
230
|
+
# string, "-e" and "--" are not paths), as ChiRunner.swift checks.
|
|
213
231
|
def launch_problem
|
|
214
232
|
argv = usable_argv(read_launch)
|
|
215
|
-
|
|
216
|
-
return nil if missing.empty?
|
|
217
|
-
|
|
218
|
-
"chi not found at #{missing.join(", ")}: run chi desktop install --force from the shell whose chi it should run"
|
|
233
|
+
argv && argv_problem(argv)
|
|
219
234
|
end
|
|
220
235
|
|
|
221
236
|
# The chi version the launch file names, nil without one.
|
|
@@ -232,7 +247,10 @@ module Samagotchi
|
|
|
232
247
|
# Rewrite the launch file for this chi and its settings, keeping
|
|
233
248
|
# its argv and the env it was installed with; a PATH only when it has
|
|
234
249
|
# none (changing them takes chi desktop install --force or upgrade). A
|
|
235
|
-
# newer version stays. The app reads it at each send.
|
|
250
|
+
# newer version stays. The app reads it at each send. The kept env
|
|
251
|
+
# loses what ENV_ALLOWLIST no longer takes (DROPPED_ENV): RUBYLIB
|
|
252
|
+
# always; GEM_HOME/GEM_PATH once the argv boots the chi home (an old
|
|
253
|
+
# [ruby, wrapper] argv may find its gem only through them).
|
|
236
254
|
def refresh_launch_file
|
|
237
255
|
config = launch_config
|
|
238
256
|
old = read_launch
|
|
@@ -240,7 +258,8 @@ module Samagotchi
|
|
|
240
258
|
config["version"] = old["version"] unless older_version?(old["version"])
|
|
241
259
|
old_env = old["env"]
|
|
242
260
|
if old_env.is_a?(Hash)
|
|
243
|
-
config["
|
|
261
|
+
dropped = home_boot?(config["argv"]) ? DROPPED_ENV : DROPPED_ENV - GEM_ENV
|
|
262
|
+
config["env"] = old_env.except(*dropped)
|
|
244
263
|
path = self.class.baked_path(@env)
|
|
245
264
|
config["env"]["PATH"] = path if config["env"]["PATH"].to_s.empty? && path
|
|
246
265
|
end
|
|
@@ -466,11 +485,41 @@ module Samagotchi
|
|
|
466
485
|
!env.is_a?(Hash) || env["PATH"].to_s.empty?
|
|
467
486
|
end
|
|
468
487
|
|
|
488
|
+
# A launch file whose argv runs (#argv_problem finds nothing).
|
|
469
489
|
def launch_ok?(launch)
|
|
470
|
-
argv =
|
|
471
|
-
!argv.
|
|
490
|
+
argv = usable_argv(launch)
|
|
491
|
+
!argv.nil? && argv_problem(argv).nil?
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Why a launch argv (strings) can't run chi, or nil: its ruby isn't an
|
|
495
|
+
# absolute path, an absolute path in it is gone, or it boots the chi
|
|
496
|
+
# home (home.json's ruby) and the home can't boot chi
|
|
497
|
+
# (ChiHome.problem).
|
|
498
|
+
def argv_problem(argv)
|
|
499
|
+
return "the launch file's ruby #{argv[0]} isn't an absolute path: run chi desktop install --force" unless argv[0].start_with?("/")
|
|
500
|
+
|
|
501
|
+
missing = launch_paths(argv).reject { |path| File.exist?(path) }
|
|
502
|
+
unless missing.empty?
|
|
503
|
+
return "chi not found at #{missing.join(", ")}: run chi desktop install --force from the shell whose chi it should run"
|
|
504
|
+
end
|
|
505
|
+
return nil unless home_boot?(argv)
|
|
506
|
+
|
|
507
|
+
home = begin
|
|
508
|
+
ChiHome.read
|
|
509
|
+
rescue ChiHome::Invalid
|
|
510
|
+
nil
|
|
511
|
+
end
|
|
512
|
+
why = home && ChiHome.real(home.ruby) == ChiHome.real(argv[0]) && ChiHome.problem(home)
|
|
513
|
+
why ? "the chi home it boots can't run chi (#{why}): chi home doctor" : nil
|
|
472
514
|
end
|
|
473
515
|
|
|
516
|
+
# The argv entries that are paths (absolute): a boot's "-e", its
|
|
517
|
+
# string and "--" are not.
|
|
518
|
+
def launch_paths(argv) = Array(argv).select { |arg| arg.is_a?(String) && arg.start_with?("/") }
|
|
519
|
+
|
|
520
|
+
# An argv in the chi home's boot form ([ruby, "-e", boot, "--"]).
|
|
521
|
+
def home_boot?(argv) = argv.is_a?(Array) && argv[1] == "-e"
|
|
522
|
+
|
|
474
523
|
def write_launch_file(config = launch_config, keep_launch: false)
|
|
475
524
|
if keep_launch
|
|
476
525
|
old = read_launch
|
|
@@ -478,6 +527,13 @@ module Samagotchi
|
|
|
478
527
|
old_env = old["env"]
|
|
479
528
|
old_path = old_env["PATH"] if old_env.is_a?(Hash)
|
|
480
529
|
config["env"]["PATH"] = old_path unless old_path.to_s.empty?
|
|
530
|
+
# A kept [ruby, wrapper] argv keeps the GEM_* its gem was found
|
|
531
|
+
# through; a kept home boot takes none.
|
|
532
|
+
if home_boot?(config["argv"])
|
|
533
|
+
config["env"] = config["env"].except(*GEM_ENV)
|
|
534
|
+
elsif old_env.is_a?(Hash)
|
|
535
|
+
config["env"].merge!(old_env.slice(*GEM_ENV))
|
|
536
|
+
end
|
|
481
537
|
end
|
|
482
538
|
FileUtils.mkdir_p(@support_dir)
|
|
483
539
|
AtomicFile.write(launch_path, JSON.pretty_generate(config) + "\n")
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -319,6 +319,9 @@ module Samagotchi
|
|
|
319
319
|
# The context sources the peer bundles declare as a peer agent's
|
|
320
320
|
# answers (chi.peer_source); the worker's ContextAbsorber asks it.
|
|
321
321
|
@peer_sources = PeerSources.new
|
|
322
|
+
# The system-prompt text plugins add (chi.prompt_section), run when
|
|
323
|
+
# the prompt is built (#build_system_prompt's sections:).
|
|
324
|
+
@prompt_sections = Plugin::PromptSections.new
|
|
322
325
|
load_plugins if load
|
|
323
326
|
# Registered from what the sources published while they loaded, and
|
|
324
327
|
# with no plugins too: a toolkit's left-out built-ins are reachable
|
|
@@ -527,7 +530,8 @@ module Samagotchi
|
|
|
527
530
|
thinking: -> { turn_thinking }, model: -> { model_identity },
|
|
528
531
|
model_notes: -> { model_notes }, memories: memories,
|
|
529
532
|
muted_memory_names: @muted_memory_names, appended_files: appended_prompt_files,
|
|
530
|
-
participants: -> { @participants_line }
|
|
533
|
+
participants: -> { @participants_line },
|
|
534
|
+
sections: -> { @prompt_sections ? @prompt_sections.render(@session&.id) : [] })
|
|
531
535
|
end
|
|
532
536
|
private :build_system_prompt
|
|
533
537
|
|
|
@@ -832,6 +836,13 @@ module Samagotchi
|
|
|
832
836
|
@host_registry.model_ref(name).ref
|
|
833
837
|
end
|
|
834
838
|
|
|
839
|
+
# The ref #switch_model! would switch +model_name+ to, checked the way it
|
|
840
|
+
# checks one (UnknownHost, MissingModel), without switching.
|
|
841
|
+
# @return [String]
|
|
842
|
+
def model_switch_ref(model_name)
|
|
843
|
+
model_ref_for(ModelProfile.check_host!(ModelProfile.required_model_name(model_name), hosts: @host_registry.entries))
|
|
844
|
+
end
|
|
845
|
+
|
|
835
846
|
# Writes the effective model to +session+: the resolved ref, and the
|
|
836
847
|
# name as typed when that differs (model_typed: an alias, for the
|
|
837
848
|
# models: lookup after a resume).
|
|
@@ -1304,7 +1315,7 @@ module Samagotchi
|
|
|
1304
1315
|
# the saved context's status: SessionModelSettings' (resolved on every
|
|
1305
1316
|
# read); these forward.
|
|
1306
1317
|
def saved_context_status(context, model: :current) = @model_settings.saved_context_status(context, model: model)
|
|
1307
|
-
def llm_context_explained = @model_settings.llm_context_explained
|
|
1318
|
+
def llm_context_explained(own: :session) = @model_settings.llm_context_explained(own: own)
|
|
1308
1319
|
def llm_context_summary = @model_settings.llm_context_summary
|
|
1309
1320
|
def llm_context_override = @model_settings.llm_context_override
|
|
1310
1321
|
def chat_model? = @model_settings.chat_model?
|
|
@@ -2827,7 +2838,9 @@ module Samagotchi
|
|
|
2827
2838
|
},
|
|
2828
2839
|
toolkits: @toolkit_binding.plugin_kits,
|
|
2829
2840
|
tool_index: @tool_index,
|
|
2830
|
-
peer_sources: @peer_sources
|
|
2841
|
+
peer_sources: @peer_sources,
|
|
2842
|
+
prompt_sections: @prompt_sections,
|
|
2843
|
+
prompt_changed: -> { prompt_changed! }
|
|
2831
2844
|
)
|
|
2832
2845
|
# What plugins show while they load (an MCP server that didn't
|
|
2833
2846
|
# start) waits for the first turn, beside the load warnings: no UI
|
|
@@ -3144,8 +3157,18 @@ module Samagotchi
|
|
|
3144
3157
|
# The tools changed (a plugin's chi.tools_changed!): the system prompts,
|
|
3145
3158
|
# which declare them, are built again on the next turn.
|
|
3146
3159
|
def tools_changed!
|
|
3160
|
+
prompt_changed!
|
|
3161
|
+
end
|
|
3162
|
+
|
|
3163
|
+
# A plugin's prompt section would give other text now
|
|
3164
|
+
# (chi.prompt_changed!): the system prompts are built again at their
|
|
3165
|
+
# next build (the next turn's start; a running turn keeps the prompt it
|
|
3166
|
+
# started with). One cache restart: the plugin's command asked first
|
|
3167
|
+
# (Plugin::Context#cache_confirm).
|
|
3168
|
+
def prompt_changed!
|
|
3147
3169
|
@prompt_builder&.reset!
|
|
3148
3170
|
end
|
|
3171
|
+
public :prompt_changed!
|
|
3149
3172
|
|
|
3150
3173
|
# What a Plugin::Context reads and calls: the session now, and the
|
|
3151
3174
|
# hook runtime's notify and ask_user.
|
|
@@ -3176,6 +3199,12 @@ module Samagotchi
|
|
|
3176
3199
|
},
|
|
3177
3200
|
model_name: -> { effective_model_ref },
|
|
3178
3201
|
model_key: -> { @model_binding.model_key },
|
|
3202
|
+
# The names a models: map looks the model up under; [] when it doesn't resolve.
|
|
3203
|
+
model_names: lambda do
|
|
3204
|
+
effective_model.names
|
|
3205
|
+
rescue StandardError
|
|
3206
|
+
[]
|
|
3207
|
+
end,
|
|
3179
3208
|
state_dir: -> { session_state_dir },
|
|
3180
3209
|
scratch: -> { @scratch },
|
|
3181
3210
|
setup: -> { SessionSetup.of(@session) },
|
|
@@ -3186,7 +3215,9 @@ module Samagotchi
|
|
|
3186
3215
|
frontend: -> { interface },
|
|
3187
3216
|
cost: -> { @metrics.cost },
|
|
3188
3217
|
usage: -> { @metrics.usage },
|
|
3189
|
-
turn_author: -> { turn_author }
|
|
3218
|
+
turn_author: -> { turn_author },
|
|
3219
|
+
# A plugin's prompt section is in the stable part: a full re-read.
|
|
3220
|
+
cache_confirm: ->(line, changed) { CacheConfirm.ask(line: line, cost: :full, changed: changed, engine: self) }
|
|
3190
3221
|
)
|
|
3191
3222
|
end
|
|
3192
3223
|
|
|
@@ -62,6 +62,8 @@ module Samagotchi
|
|
|
62
62
|
# price_host: the effective model's host, whose hosts.<name>.models
|
|
63
63
|
# prices the cost estimate names. kit_prompt: the next turn's kit
|
|
64
64
|
# prompt text ({kit:, tokens:}), only when its toolkit has one.
|
|
65
|
+
# prompt_sections: the plugins' sections the built prompt holds, only
|
|
66
|
+
# when it holds some.
|
|
65
67
|
# settings: the session's own settings ({"recap.enabled" => false}),
|
|
66
68
|
# only when it has some.
|
|
67
69
|
# @return [Hash] @metrics.snapshot, filled in
|
|
@@ -78,6 +80,9 @@ module Samagotchi
|
|
|
78
80
|
price_host: model.host_name)
|
|
79
81
|
kit_prompt = kit_prompt_stats
|
|
80
82
|
snapshot = snapshot.merge(kit_prompt: kit_prompt) if kit_prompt
|
|
83
|
+
# The plugins' prompt sections the built prompt holds ({bundle:, name:}).
|
|
84
|
+
sections = prompt_builder.prompt_sections.map(&:to_stats)
|
|
85
|
+
snapshot = snapshot.merge(prompt_sections: sections) unless sections.empty?
|
|
81
86
|
# Each participant's spend under their name (participants.json's).
|
|
82
87
|
if (spend = snapshot[:spend_by_author])
|
|
83
88
|
named = spend.to_h { |author, row| [author, row.merge(name: participants.name_for(author, session: session))] }
|
|
@@ -6,13 +6,15 @@ require "open3"
|
|
|
6
6
|
require "rbconfig"
|
|
7
7
|
require "rubygems/package"
|
|
8
8
|
require "time"
|
|
9
|
+
require_relative "chi_home"
|
|
9
10
|
require_relative "version"
|
|
10
11
|
require_relative "redacted_inspect"
|
|
11
12
|
|
|
12
13
|
module Samagotchi
|
|
13
14
|
# The gem part of `chi update`: the newest samagotchi on rubygems, and a
|
|
14
|
-
# `gem install` of it through
|
|
15
|
-
# interpreter's bindir, not a shim: the same under
|
|
15
|
+
# `gem install` of it through the chi home's gem command, else this
|
|
16
|
+
# Ruby's own (the real interpreter's bindir, not a shim: the same under
|
|
17
|
+
# mise, rbenv and asdf).
|
|
16
18
|
#
|
|
17
19
|
# SAMAGOTCHI_UPDATE_GEM_FILE=<path to a .gem> (hidden, for smokes) makes that
|
|
18
20
|
# file the newest version and installs it instead.
|
|
@@ -32,26 +34,31 @@ module Samagotchi
|
|
|
32
34
|
Latest = Struct.new(:version, :source, keyword_init: true)
|
|
33
35
|
Release = Data.define(:version, :created_at, :prerelease)
|
|
34
36
|
|
|
35
|
-
# Runs a command without a shell; [output, success].
|
|
37
|
+
# Runs a command without a shell; [output, success]. +env+ is merged
|
|
38
|
+
# into the inherited one (nil unsets a key).
|
|
36
39
|
class Runner
|
|
37
|
-
def run(argv)
|
|
38
|
-
output, status = Open3.capture2e(*argv)
|
|
40
|
+
def run(argv, env: {})
|
|
41
|
+
output, status = Open3.capture2e(env, *argv)
|
|
39
42
|
[output, status.success?]
|
|
40
43
|
rescue SystemCallError => e
|
|
41
44
|
[e.message, false]
|
|
42
45
|
end
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
#
|
|
46
|
-
# it too).
|
|
47
|
-
def self.gem_bin =
|
|
48
|
+
# The gem command an install runs: the chi home's (ChiHome.gem_bin),
|
|
49
|
+
# else this Ruby's own (chi bootstrap installs commonmarker with it too).
|
|
50
|
+
def self.gem_bin = ChiHome.gem_bin
|
|
48
51
|
|
|
49
52
|
# @param fetcher [#call, nil] → the latest.json body (specs)
|
|
50
|
-
|
|
53
|
+
# @param install_env [Hash] the install child's env on top of this
|
|
54
|
+
# process's (ChiHome.install_env: into the home's gems)
|
|
55
|
+
def initialize(env: ENV, fetcher: nil, runner: Runner.new, gem_bin: GemUpdate.gem_bin,
|
|
56
|
+
install_env: ChiHome.install_env)
|
|
51
57
|
@env = env
|
|
52
58
|
@fetcher = fetcher || method(:fetch)
|
|
53
59
|
@runner = runner
|
|
54
60
|
@gem_bin = gem_bin
|
|
61
|
+
@install_env = install_env
|
|
55
62
|
end
|
|
56
63
|
|
|
57
64
|
# @return [Latest]
|
|
@@ -104,7 +111,9 @@ module Samagotchi
|
|
|
104
111
|
|
|
105
112
|
# @return [Array(String, Boolean)] gem's output, success
|
|
106
113
|
def install(latest)
|
|
107
|
-
@runner.run(install_argv(latest))
|
|
114
|
+
return @runner.run(install_argv(latest)) if @install_env.empty?
|
|
115
|
+
|
|
116
|
+
@runner.run(install_argv(latest), env: @install_env)
|
|
108
117
|
end
|
|
109
118
|
|
|
110
119
|
private
|
|
@@ -265,12 +265,14 @@ module Samagotchi
|
|
|
265
265
|
@protected ||= begin
|
|
266
266
|
require_relative "memory_bundle/provenance"
|
|
267
267
|
require_relative "context_sources"
|
|
268
|
+
require_relative "chi_home"
|
|
268
269
|
Guardrails::ProtectedPaths.new(
|
|
269
270
|
store_dir: File.dirname(@approvals.path),
|
|
270
271
|
bundles_dir: MemoryBundle::Provenance.bundles_dir,
|
|
271
272
|
config_path: Samagotchi::ConfigFile.global_path,
|
|
272
273
|
hooks_dir: Hooks::Loader.hooks_dir,
|
|
273
|
-
context_dir: ContextSources.root
|
|
274
|
+
context_dir: ContextSources.root,
|
|
275
|
+
home_path: ChiHome.path
|
|
274
276
|
)
|
|
275
277
|
end
|
|
276
278
|
end
|
|
@@ -14,7 +14,8 @@ module Samagotchi
|
|
|
14
14
|
# hooks, rules, manifests) and attached context (a source's
|
|
15
15
|
# command runs later, outside the gate; chi context writes
|
|
16
16
|
# it); nothing legitimate writes there through the file tools.
|
|
17
|
-
# ask (once / session): config.yml
|
|
17
|
+
# ask (once / session): config.yml, home.json (the chi home: the
|
|
18
|
+
# Ruby every chi launch boots) and the plain hooks dir. The
|
|
18
19
|
# system bundle's config protocol edits config.yml with
|
|
19
20
|
# write/edit, so this keeps the user in the loop without
|
|
20
21
|
# breaking it.
|
|
@@ -29,7 +30,8 @@ module Samagotchi
|
|
|
29
30
|
# @param config_path [String] config.yml
|
|
30
31
|
# @param hooks_dir [String] the plain hooks dir
|
|
31
32
|
# @param context_dir [String, nil] <state dir>/context (ContextSources.root)
|
|
32
|
-
|
|
33
|
+
# @param home_path [String, nil] home.json (ChiHome.path)
|
|
34
|
+
def initialize(store_dir:, bundles_dir:, config_path:, hooks_dir:, context_dir: nil, home_path: nil)
|
|
33
35
|
@deny = [
|
|
34
36
|
["guardrail-store", store_dir, "the approval store belongs to the user"],
|
|
35
37
|
["installed-bundles", bundles_dir, "installed bundles (hooks, rules, manifests) are changed by chi bundle install"],
|
|
@@ -37,8 +39,9 @@ module Samagotchi
|
|
|
37
39
|
]
|
|
38
40
|
@ask = [
|
|
39
41
|
["chi-config", config_path, "changes chi's config.yml"],
|
|
42
|
+
["chi-home", home_path, "changes the Ruby every chi launch boots"],
|
|
40
43
|
["chi-hooks", hooks_dir, "changes chi's hooks"]
|
|
41
|
-
]
|
|
44
|
+
].select { |_, root, _| root }
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
# Vote on +verdict+ (its targets).
|
|
@@ -23,7 +23,7 @@ module Samagotchi
|
|
|
23
23
|
# ParentPolicy's user_only); System 1 may, only if the user lists the
|
|
24
24
|
# rule by name in guardrails.system_one.rules, and never for the
|
|
25
25
|
# chi-answer/chi-broadcast ones (SystemOne::SelfProtect::NEVER_AUTO).
|
|
26
|
-
PROTECTED_RULES = %w[chi-config chi-hooks shell-touches-chi chi-context-cmd chi-broadcast prompt-memory-write
|
|
26
|
+
PROTECTED_RULES = %w[chi-config chi-home chi-hooks shell-touches-chi chi-context-cmd chi-broadcast prompt-memory-write
|
|
27
27
|
model-overlay-write chi-answer-piped chi-answer-http].freeze
|
|
28
28
|
SCOPES = %w[once session repo rule].freeze
|
|
29
29
|
DO_NOT_RETRY = "Do not retry it or reach the same result another way; ask the user how to proceed."
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require_relative "chi_home"
|
|
5
|
+
require_relative "cli/command"
|
|
6
|
+
require_relative "cli/exit"
|
|
7
|
+
require_relative "cli/flags"
|
|
8
|
+
require_relative "installed_gem"
|
|
9
|
+
require_relative "paths"
|
|
10
|
+
require_relative "version"
|
|
11
|
+
|
|
12
|
+
module Samagotchi
|
|
13
|
+
# `chi home`: the chi home (ChiHome), the Ruby every chi launch boots.
|
|
14
|
+
# Shows it, sets it (`chi home set [--ruby PATH]`), and checks it
|
|
15
|
+
# (`chi home doctor`: for now whether its Ruby is there and has chi).
|
|
16
|
+
class HomeCommand
|
|
17
|
+
include CLI::Command
|
|
18
|
+
|
|
19
|
+
USAGE = <<~TEXT
|
|
20
|
+
Usage: chi home [set [--ruby PATH] | doctor]
|
|
21
|
+
(none) show the chi home: its Ruby, the chi it runs, its gem home and path, mise
|
|
22
|
+
set record this chi's Ruby and gem path as the home (a chruby shell's
|
|
23
|
+
GEM_HOME/GEM_PATH are part of it)
|
|
24
|
+
set --ruby PATH record that Ruby (its own gem dirs) as the home; it must have samagotchi
|
|
25
|
+
(gem install samagotchi with it first)
|
|
26
|
+
doctor check the home: its Ruby is there and has samagotchi
|
|
27
|
+
Every chi launch (workers, chi web's restart and Update, the macOS helper's next
|
|
28
|
+
chi desktop install or upgrade) boots the home's Ruby with the home's gems. Saved in
|
|
29
|
+
#{Paths.tilde(ChiHome.path)}.
|
|
30
|
+
TEXT
|
|
31
|
+
|
|
32
|
+
SUBCOMMANDS = %w[set doctor].freeze
|
|
33
|
+
FLAGS = {
|
|
34
|
+
nil => CLI::Flags.new(help: HELP_WORDS, args: false),
|
|
35
|
+
"set" => CLI::Flags.new(help: HELP_WORDS, args: false) { |f| f.value "--ruby" },
|
|
36
|
+
"doctor" => CLI::Flags.new(help: HELP_WORDS, args: false)
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# @param path [String] home.json
|
|
40
|
+
# @param probe [#call] (ruby) → ChiHome::Record or nil (ChiHome.probe)
|
|
41
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr, path: ChiHome.path, gem_spec: InstalledGem.spec,
|
|
42
|
+
env: ENV, probe: nil)
|
|
43
|
+
@argv = argv.dup
|
|
44
|
+
@stdout = stdout
|
|
45
|
+
@stderr = stderr
|
|
46
|
+
@path = path
|
|
47
|
+
@gem_spec = gem_spec
|
|
48
|
+
@env = env
|
|
49
|
+
@probe = probe || ->(ruby) { ChiHome.probe(ruby, env: env) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Integer] 0, 1 (no usable home, a refusal), 2 usage
|
|
53
|
+
def run
|
|
54
|
+
@sub = SUBCOMMANDS.include?(@argv.first) ? @argv.first : nil
|
|
55
|
+
parsed = parse_flags(FLAGS.fetch(@sub), @sub ? @argv[1..] : @argv)
|
|
56
|
+
return parsed if parsed.is_a?(Integer)
|
|
57
|
+
|
|
58
|
+
case @sub
|
|
59
|
+
when "set" then run_set(parsed.options)
|
|
60
|
+
when "doctor" then run_doctor
|
|
61
|
+
else run_show
|
|
62
|
+
end
|
|
63
|
+
rescue ChiHome::Invalid => e
|
|
64
|
+
error_line("#{command_name}: #{e.message} (chi home set rewrites it)")
|
|
65
|
+
CLI::Exit::FAILED
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def command_name = @sub ? "chi home #{@sub}" : "chi home"
|
|
71
|
+
def usage_on_error = nil
|
|
72
|
+
|
|
73
|
+
def run_show
|
|
74
|
+
home = ChiHome.read(path: @path)
|
|
75
|
+
unless home
|
|
76
|
+
@stdout.puts("no chi home: chi launches on the Ruby that starts it (chi home set records this one, #{running_ruby})")
|
|
77
|
+
return CLI::Exit::OK
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
rows(home).each { |label, value| @stdout.puts("#{label.ljust(9)} #{value}") }
|
|
81
|
+
problem = ChiHome.problem(home)
|
|
82
|
+
@stdout.puts("problem #{problem} (chi home doctor)") if problem
|
|
83
|
+
problem ? CLI::Exit::FAILED : CLI::Exit::OK
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def rows(home)
|
|
87
|
+
list = [["ruby", home.ruby], ["chi", home.newest_version&.to_s || "none"], ["gem home", home.gem_home],
|
|
88
|
+
["gem path", home.gem_path], ["mise", home.mise || "none"],
|
|
89
|
+
["set", "#{home.set_at} by chi #{home.set_by_version}"]]
|
|
90
|
+
list << ["running", "chi #{VERSION} on #{running_ruby}"] unless running_home?(home)
|
|
91
|
+
list
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def running_home?(home) = @gem_spec && home.running_here? && home.newest_version.to_s == VERSION
|
|
95
|
+
|
|
96
|
+
def running_ruby = "#{RbConfig.ruby}#{" (a checkout)" unless @gem_spec}"
|
|
97
|
+
|
|
98
|
+
def run_set(options)
|
|
99
|
+
home = options[:ruby] ? probed(options[:ruby]) : ChiHome.from_running(env: @env)
|
|
100
|
+
return CLI::Exit::FAILED unless home
|
|
101
|
+
|
|
102
|
+
if (problem = ChiHome.problem(home))
|
|
103
|
+
error_line("#{command_name}: #{problem}: gem install samagotchi with #{home.ruby} first")
|
|
104
|
+
return CLI::Exit::FAILED
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
ChiHome.write!(home, path: @path)
|
|
108
|
+
@stdout.puts("chi home set to #{home.ruby} (chi #{home.newest_version}); new workers and launches boot it")
|
|
109
|
+
CLI::Exit::OK
|
|
110
|
+
rescue SystemCallError => e
|
|
111
|
+
error_line("#{command_name}: couldn't write #{Paths.tilde(@path)}: #{e.message}")
|
|
112
|
+
CLI::Exit::FAILED
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The home a --ruby names: this one's (with this shell's gem path) when
|
|
116
|
+
# it is the running Ruby, else asked of it.
|
|
117
|
+
def probed(ruby)
|
|
118
|
+
path = File.expand_path(ruby)
|
|
119
|
+
unless File.file?(path) && File.executable?(path)
|
|
120
|
+
error_line("#{command_name}: no Ruby at #{ruby}")
|
|
121
|
+
return nil
|
|
122
|
+
end
|
|
123
|
+
return ChiHome.from_running(env: @env) if ChiHome.real(path) == ChiHome.real(RbConfig.ruby)
|
|
124
|
+
|
|
125
|
+
home = @probe.call(path)
|
|
126
|
+
error_line("#{command_name}: #{ruby} didn't answer as a Ruby") unless home
|
|
127
|
+
home
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# P2c adds rows (running chi vs the home, the helper's argv, sessions,
|
|
131
|
+
# the project env); for now the home's own.
|
|
132
|
+
def run_doctor
|
|
133
|
+
home = ChiHome.read(path: @path)
|
|
134
|
+
unless home
|
|
135
|
+
@stdout.puts("ok no chi home: each launch uses the Ruby that starts it (chi home set to record one)")
|
|
136
|
+
return CLI::Exit::OK
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
if (problem = ChiHome.problem(home))
|
|
140
|
+
@stdout.puts("fail #{problem}: after a Ruby upgrade, gem install samagotchi with the new Ruby, then " \
|
|
141
|
+
"chi home set from it (chi home set --ruby PATH); until then launches use the Ruby that starts them")
|
|
142
|
+
return CLI::Exit::FAILED
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@stdout.puts("ok chi home #{home.ruby} runs chi #{home.newest_version}")
|
|
146
|
+
CLI::Exit::OK
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "chi_home"
|
|
3
4
|
require_relative "installed_gem"
|
|
4
5
|
require_relative "version"
|
|
5
6
|
require_relative "redacted_inspect"
|
|
@@ -8,8 +9,9 @@ module Samagotchi
|
|
|
8
9
|
# The newest chi on this machine, which a long-lived process (chi web, an
|
|
9
10
|
# attaching TUI) compares with its own VERSION to say "chi X is installed;
|
|
10
11
|
# restart". Run from an installed gem: the highest samagotchi-<v>.gemspec
|
|
11
|
-
# in RubyGems' specification dirs
|
|
12
|
-
#
|
|
12
|
+
# in RubyGems' specification dirs (the chi home's when there is one,
|
|
13
|
+
# ChiHome), matching the `>= 0.a` a new worker activates (prereleases
|
|
14
|
+
# count). Run from a source checkout: the VERSION
|
|
13
15
|
# in lib/samagotchi/version.rb on disk now (a `git pull` moves it).
|
|
14
16
|
#
|
|
15
17
|
# Cheap enough to poll: each dir is listed again only when its mtime
|
|
@@ -30,7 +32,7 @@ module Samagotchi
|
|
|
30
32
|
# runs from (nil: a source checkout)
|
|
31
33
|
# @param dirs [#call] → the specification dirs to list
|
|
32
34
|
# @param version_file [String] a checkout's version.rb
|
|
33
|
-
def initialize(env: ENV, gem_spec: InstalledGem.spec, dirs: -> {
|
|
35
|
+
def initialize(env: ENV, gem_spec: InstalledGem.spec, dirs: -> { ChiHome.spec_dirs(gem_spec: gem_spec) },
|
|
34
36
|
version_file: File.join(InstalledGem::ROOT, "lib", "samagotchi", "version.rb"))
|
|
35
37
|
@env = env
|
|
36
38
|
@gem_spec = gem_spec
|