samagotchi 0.149.0 → 0.150.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 +48 -1
- data/README.md +4 -2
- data/docs/architecture.md +2 -2
- data/docs/cli.md +59 -24
- data/docs/configuration.md +5 -5
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +8 -0
- data/docs/internals/bundles.md +42 -14
- data/docs/internals/objectives.md +7 -2
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +47 -9
- data/docs/plugins.md +7 -1
- data/docs/sessions.md +5 -4
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_store.rb +52 -7
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/engine.rb +1 -1
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +16 -2
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +62 -1
- data/lib/samagotchi/memory_bundle/installer.rb +161 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +178 -20
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/objectives_check.rb +37 -7
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_flow.rb +3 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/participant_view.rb +1 -1
- data/lib/samagotchi/web/public/app.js +5 -3
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/bubbles.js +4 -2
- data/lib/samagotchi/web/public/cards_place.js +10 -5
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/index.html +18 -14
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- metadata +8 -1
data/docs/memory.md
CHANGED
|
@@ -187,7 +187,7 @@ Working habits for this model:
|
|
|
187
187
|
`model_notes_<name>.md` you already have and never owns it, so your note
|
|
188
188
|
is what loads, the two never stack, and upgrades leave it alone.
|
|
189
189
|
`chi bundle install model-notes` says `Skipped model_notes_deepseek.md
|
|
190
|
-
(already exists;
|
|
190
|
+
(already exists; --force takes the bundle's version, your file copied to the trash first)`; through the `dev` profile
|
|
191
191
|
(`chi bundle install dev`, `chi bundle upgrade dev`, `chi update`) it is
|
|
192
192
|
`model-notes: kept your own model_notes_deepseek.md (not the bundle's; to
|
|
193
193
|
take the bundle's, remove yours with chi memory rm model_notes_deepseek
|
|
@@ -197,14 +197,14 @@ Working habits for this model:
|
|
|
197
197
|
(`chi memory rm model_notes_deepseek --scope system`, or `memory_write`
|
|
198
198
|
with `remove: true`, moves it to the trash) and run
|
|
199
199
|
`chi bundle install model-notes` again: the install writes the missing
|
|
200
|
-
file and owns it from then on.
|
|
201
|
-
|
|
200
|
+
file and owns it from then on. `--force` overwrites every file of the
|
|
201
|
+
bundle instead, each one that differs copied to the bundle trash first.
|
|
202
202
|
- **Other notes stack.** A note of another name for the same models
|
|
203
203
|
(`model_notes_my_deepseek`), or a project-scope `model_notes_deepseek`,
|
|
204
204
|
loads beside the bundle's (notes are told apart by their file, not their
|
|
205
205
|
name), and the total may pass the 3,000-character warning.
|
|
206
|
-
- **Your edits to an installed note**
|
|
207
|
-
|
|
206
|
+
- **Your edits to an installed note** survive an upgrade: it merges them
|
|
207
|
+
with the new version, or leaves your file as it is on a real conflict.
|
|
208
208
|
- Without a model note the system prompt is what it was.
|
|
209
209
|
|
|
210
210
|
|
|
@@ -487,7 +487,9 @@ for a file another bundle installed.
|
|
|
487
487
|
and the output names them and the dir:
|
|
488
488
|
`Moved to the trash: notes.md (…/.trash/team-notes-20261003-120000)`. So
|
|
489
489
|
does an upgrade's removal of a file the new version no longer ships. A
|
|
490
|
-
file you edited still needs `--force`.
|
|
490
|
+
file you edited still needs `--force`. A hook, rule, plugin or script you
|
|
491
|
+
edited isn't asked about: it is copied to the same dir (`Copied to the trash
|
|
492
|
+
first: scripts/x (…)`) and the bundle's own copy goes. `chi bundle trash` lists the trash
|
|
491
493
|
(oldest first, with file count, size, and age); `--empty` deletes all
|
|
492
494
|
folders, `--older-than DAYS` keeps only recent ones, and `--dry-run`
|
|
493
495
|
previews. Hooks, rules and the plugin live in the bundle's own dir and
|
|
@@ -509,12 +511,48 @@ for a file another bundle installed.
|
|
|
509
511
|
`.tgz` or `.tar` gives that archive, any other path a directory; default
|
|
510
512
|
`<name>.zip`).
|
|
511
513
|
- **An upgrade that meets your edits.** `chi bundle upgrade` updates a file
|
|
512
|
-
you didn't edit
|
|
514
|
+
you didn't edit. One you edited that the bundle also changed takes a text
|
|
515
|
+
merge (with `git`, which chi doesn't otherwise need): when it comes out clean
|
|
516
|
+
the file holds both, the old one is in the bundle trash, and the output says
|
|
517
|
+
`merged 0.8.0 into your X` (a dry run, `would merge 0.8.0 into your X`). Entries both sides added under a
|
|
518
|
+
`Changelog` heading are kept, yours first. A clean merge is text, not
|
|
519
|
+
meaning: two sentences can still disagree, so read what you care about (`chi
|
|
520
|
+
bundle trash` lists the old copy).
|
|
521
|
+
- **A real conflict** leaves your file untouched, never with markers in it, and
|
|
522
|
+
marks it. So does no `git`, a file with no recorded base, or a file marked by
|
|
523
|
+
an older chi (below). The output says `X stays behind 0.8.0:` with why and what
|
|
524
|
+
it missed, as `chi update`, `chi bundle status` and `chi bundle list` (`1 file
|
|
525
|
+
behind`) do from then on, until the file has the bundle's bytes; the bundle's text
|
|
526
|
+
waits in `.bundles/<name>/incoming/` until it settles. The upgrade then asks on a
|
|
513
527
|
terminal whether to launch a chi session that merges the two (`/exit`
|
|
514
528
|
when done). `--agent` launches it without asking, `--no-agent` never does;
|
|
515
|
-
with neither and no terminal
|
|
529
|
+
with neither and no terminal the file just stays behind. Only a file
|
|
530
|
+
the session changed, with no conflict markers left, counts as resolved: any
|
|
531
|
+
other stays a conflict (`Nothing changed in X: still behind.`). A file marked
|
|
532
|
+
this way is retried at the same version: the system bundle's at each chi start
|
|
533
|
+
and on `chi update`, another shipped bundle's on `chi update`, and any other
|
|
534
|
+
bundle's on `chi bundle upgrade`.
|
|
535
|
+
- **A file marked by an older chi** merges normally when its recorded base is a
|
|
536
|
+
version chi shipped, which chi checks against the samagotchi gems still
|
|
537
|
+
installed. Otherwise that base may be your own text, so the file merges only if
|
|
538
|
+
the bundle takes out or rewords no line it still holds. If it does, chi can't
|
|
539
|
+
tell whether the file holds edits of yours and says that; the file
|
|
540
|
+
stays behind until you resolve it by hand, with `--agent` or with `--force`, and
|
|
541
|
+
the retry can't fix that. A file left behind makes the upgrade
|
|
516
542
|
exit 2; `chi bundle diff NAME FILE` shows the base, and `--force` takes
|
|
517
|
-
the bundle's version
|
|
543
|
+
the bundle's version, copying a file that differs to the bundle trash first
|
|
544
|
+
(the output says `Copied to the trash first: …`).
|
|
545
|
+
- **Every merge is noted on the file's record.** The system bundle's upgrade and
|
|
546
|
+
retry run at each start, a worker's only output is its log, and no one may read
|
|
547
|
+
an automatic update. So `chi bundle status` shows the merge with the old copy's
|
|
548
|
+
folder while the note exists. A `chi update` row or `chi bundle upgrade` output
|
|
549
|
+
printed to you ends it for what it printed (an automatic update leaves it), and
|
|
550
|
+
`--force` replacing the file ends it too. No session is told at start.
|
|
551
|
+
- **`chi bundle status`** says per file what is `modified`, `missing` or
|
|
552
|
+
`unreadable`, and that a file stays behind and why. Its `issues=` counts files
|
|
553
|
+
behind, missing or unreadable (and a plugin or hooks that won't load), not edits. A
|
|
554
|
+
file an older chi marked, which no retry has looked at, says `may be behind`, and so
|
|
555
|
+
do the counts in `chi bundle list`, `chi self` and `chi update`'s last line.
|
|
518
556
|
- **A dev install** (`chi bundle install --dev PATH`) links the hooks, rules,
|
|
519
557
|
plugin and scripts to `PATH` but still copies the memories at install, so
|
|
520
558
|
an edit to one of them in `PATH` isn't seen until the next install.
|
data/docs/plugins.md
CHANGED
|
@@ -942,6 +942,10 @@ worker gone), then moves out of the block, after the turn's end line, so it
|
|
|
942
942
|
stays in sight; a reload puts it in the same place. An `:info` card of a
|
|
943
943
|
completed turn stays in its step.
|
|
944
944
|
|
|
945
|
+
Between turns, a card goes above the first turn prompt after it, live and
|
|
946
|
+
after a reload. A failed turn whose prompt went back to its sender isn't a
|
|
947
|
+
turn: its prompt line comes after the cards.
|
|
948
|
+
|
|
945
949
|
An [anytime command](#anytime-true)'s cards show as it shows them, after its
|
|
946
950
|
line, in every UI, whether a turn runs or not.
|
|
947
951
|
|
|
@@ -1380,7 +1384,9 @@ that calls it, or for a real external bundle that does. No method on the chance
|
|
|
1380
1384
|
5. `Samagotchi::Tools::TaskRuntime::TASKS_DIR` and `EXIT_CODE_FILENAME`.
|
|
1381
1385
|
6. `Samagotchi::AtomicFile.write`, `Samagotchi::Log.warn` /
|
|
1382
1386
|
`Samagotchi::Log.debug`, `Samagotchi::Config.get`,
|
|
1383
|
-
`Samagotchi::VERSION
|
|
1387
|
+
`Samagotchi::VERSION`, `Samagotchi::CancellationController` (`new`, `cancel!`: what `cancel:` takes) and
|
|
1388
|
+
`Samagotchi::GitEnv::UNSET` (a spawn env: a bundle's own git, or gh, asks about the folder it names, not the repo
|
|
1389
|
+
a hook's or `rebase --exec`'s environment names).
|
|
1384
1390
|
|
|
1385
1391
|
`Samagotchi::Testing::Boundary::ALLOWED` is the checked copy of this list,
|
|
1386
1392
|
and `Boundary::EXCEPTIONS` the per-bundle list of what each shipped bundle
|
data/docs/sessions.md
CHANGED
|
@@ -219,8 +219,9 @@ reword is refused when you changed that objective since. The model never changes
|
|
|
219
219
|
|
|
220
220
|
**Check this** (`/objectives check O2`, or the button in the web's list, for an objective with a move) asks for an independent read of
|
|
221
221
|
the latest move: one side request, on your request only, on the session's model or `objectives.check_model`. It sees the
|
|
222
|
-
claim, what people and notes said
|
|
223
|
-
settle
|
|
222
|
+
claim, what people and notes said, what tools returned and the agent's visible answers (never its thinking), and no file. The agent's
|
|
223
|
+
words settle what it said or asked, never that its work was done: "I ran the tests" with no run behind it isn't `confirmed`. What the record doesn't
|
|
224
|
+
settle comes back `unclear`. **That is up to 24,000 characters of raw tool output, messages and the agent's visible answers, which with a hosted `check_model` leaves the machine on one tap, even from a local session.** chi's redaction takes out known key shapes and `token=`-style values first, by pattern only, so another secret in them still goes. The verdict (confirmed, refuted, unclear) and its reasons come as a card and under the
|
|
224
225
|
objective. It is information only: it changes no status, approves nothing, and isn't in the conversation or the model's next request.
|
|
225
226
|
A second tap while one runs asks nothing. Its cost is in the session's side line. Mid-turn it runs beside the turn; on a one-slot
|
|
226
227
|
local server it answers after the running request. A participant can't reach it. See
|
|
@@ -471,8 +472,8 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
471
472
|
included, asks you first; a read-only mention (`find . -name mise.toml`) doesn't. `.envrc` and mise's local files
|
|
472
473
|
(`mise.local.toml`, `mise.<env>.local.toml`, dotted or not, and `config.local.toml` / `config.<env>.local.toml` in
|
|
473
474
|
`.config/mise/`, `mise/`, `.mise/`) are secrets to them, as `.env` is.
|
|
474
|
-
- **What tools still drop**: every `execute`, task, `!cmd`, context source and MCP server takes no `RUBYOPT`, `RUBYLIB`, `
|
|
475
|
-
`
|
|
475
|
+
- **What tools still drop**: every `execute`, task, `!cmd`, context source and MCP server takes no `RUBYOPT`, `RUBYLIB`, or what `bundle exec` sets
|
|
476
|
+
(`BUNDLE_GEMFILE`, `BUNDLE_LOCKFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION`, `BUNDLER_SETUP`: `BundlerEnv`) from the project's mise env: it runs without `RUBYLIB` and Bundler's
|
|
476
477
|
variables, and with the worker's own `RUBYOPT`, Bundler's part removed (`TaskRuntime`).
|
|
477
478
|
- **`chi` in a tool** stays the home's: with a chi home the tools' PATH starts with `$XDG_STATE_HOME/samagotchi/bin`,
|
|
478
479
|
a dir holding only a `chi` script that boots the home (rewritten at each worker start when the home changed; it
|
data/docs/testing.md
CHANGED
|
@@ -40,6 +40,11 @@ that way locally: `npx playwright run-server --port 3333`, then `PW_TEST_CONNECT
|
|
|
40
40
|
|
|
41
41
|
No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
|
|
42
42
|
clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
|
|
43
|
+
It also clears what tells git which repo to act on (`GIT_DIR`, `GIT_WORK_TREE`, the rest of
|
|
44
|
+
`git rev-parse --local-env-vars`: `GitEnv`), first thing in `spec_helper.rb` and as the bundle test kit's
|
|
45
|
+
adapters load, so a run from `git rebase --exec`, `bisect run` or a hook can't reach the real repo through a spec's git.
|
|
46
|
+
`npm run e2e` clears the same list as `spec/e2e/support/env.js` loads, asked of git. chi's own git calls on a folder chi
|
|
47
|
+
chose spawn with `GitEnv::UNSET`, in any run.
|
|
43
48
|
`SAMAGOTCHI_TAILSCALE_CLI` points at a file that does not exist (`spec_helper.rb`), so no spec reaches a real `tailscale`; a
|
|
44
49
|
`chi web --tunnel tailscale` spec writes a fake one and sets the variable.
|
|
45
50
|
|
|
@@ -18,6 +18,8 @@ module Samagotchi
|
|
|
18
18
|
# @param content [String] written as-is, byte for byte
|
|
19
19
|
# @param perm [Integer, nil] the new file's mode; nil = a fresh file's
|
|
20
20
|
# (0666 less the umask), like File.write
|
|
21
|
+
# @yield just before the rename, when given: a last check of what is being
|
|
22
|
+
# replaced; raising leaves +path+ as it was
|
|
21
23
|
# @return [String] +path+
|
|
22
24
|
def write(path, content, perm: nil)
|
|
23
25
|
tmp = "#{path}.#{Process.pid}.#{SecureRandom.hex(4)}.tmp"
|
|
@@ -25,6 +27,7 @@ module Samagotchi
|
|
|
25
27
|
io.write(content)
|
|
26
28
|
end
|
|
27
29
|
File.chmod(perm, tmp) if perm
|
|
30
|
+
yield if block_given?
|
|
28
31
|
File.rename(tmp, path)
|
|
29
32
|
path
|
|
30
33
|
ensure
|
|
@@ -477,11 +477,14 @@ module Samagotchi
|
|
|
477
477
|
|
|
478
478
|
def system_bundle_line(result, dry_run)
|
|
479
479
|
version = "v#{result.to || result.from}"
|
|
480
|
+
standing = MemoryBundle::FileStanding.clauses(MemoryBundle::SystemBundle::BUNDLE_NAME, nil, merged: Array(result.merged),
|
|
481
|
+
behind: Array(result.behind), dry_run: dry_run).join("; ")
|
|
480
482
|
text = case result.status
|
|
481
483
|
when :installed then dry_run ? "would install #{version}" : "#{version} installed"
|
|
482
|
-
when :updated then "#{result.from} → #{result.to} #{dry_run ? "would update" : "updated"}"
|
|
484
|
+
when :updated then "#{result.from} → #{result.to} #{dry_run ? "would update" : "updated"}#{" (#{standing})" unless standing.empty?}"
|
|
483
485
|
when :restored then "#{version} #{dry_run ? "would restore" : "restored"} missing files"
|
|
484
|
-
when :
|
|
486
|
+
when :merged then "#{version} #{standing}"
|
|
487
|
+
when :up_to_date then "#{version} #{standing.empty? ? "up to date" : standing}"
|
|
485
488
|
when :newer_installed then "v#{result.from} left as is (newer than this chi's v#{result.to})"
|
|
486
489
|
when :skipped then "skipped (#{result.error})"
|
|
487
490
|
else "failed (#{result.error})"
|
|
@@ -28,6 +28,10 @@ module Samagotchi
|
|
|
28
28
|
# isn't the turn's but comes while one runs (an anytime command's, such
|
|
29
29
|
# as /btw) goes after that turn: its prompt is already in the history.
|
|
30
30
|
# A failed turn leaves no prompt, so its cards stay before the next one.
|
|
31
|
+
# A !rollback (:turn_rolled_back) takes a kept turn's prompt out again,
|
|
32
|
+
# and its count with it. Of a turn that left nothing, its cards and hook
|
|
33
|
+
# notices stay as between-turns ones where it was, without their
|
|
34
|
+
# actions; its other rows (no step to sit in) go.
|
|
31
35
|
#
|
|
32
36
|
# Notices (hook notices, load warnings, "asking again" rows), ✂ rows
|
|
33
37
|
# and cards (with questions) are capped apart, +capacity+ each, so a
|
|
@@ -50,6 +54,15 @@ module Samagotchi
|
|
|
50
54
|
# to a turn no worker runs any more (check-in's Nudge would steer the
|
|
51
55
|
# new worker's turn). A card shown between turns (github-pr's offer,
|
|
52
56
|
# btw, coordinator) keeps its buttons.
|
|
57
|
+
#
|
|
58
|
+
# A turn its worker was killed in never ended here, yet the session keeps
|
|
59
|
+
# its prompt (TurnCheckpoint, written before :turn_started) and it ends
|
|
60
|
+
# canceled. So +killed_turn_stays+ (a worker's) saves that a turn runs, and
|
|
61
|
+
# a store started from such a file counts that turn. A turn that ended
|
|
62
|
+
# (a graceful stop's cancel too) saved that it no longer runs. Started
|
|
63
|
+
# from the same file again, the count is the same: nothing is written
|
|
64
|
+
# until the next event. A REPL keeps no running turn in the session: a kill
|
|
65
|
+
# takes the prompt along, so its store saves no such thing.
|
|
53
66
|
class CardStore
|
|
54
67
|
CAPACITY = 20
|
|
55
68
|
NOTICE_TYPES = %i[hook_notice guardrail_warning empty_answer_retry steer_cut].freeze
|
|
@@ -59,8 +72,8 @@ module Samagotchi
|
|
|
59
72
|
FILE = "cards.json"
|
|
60
73
|
# The events after which the file is written again: the ones that
|
|
61
74
|
# change an entry or the turn count.
|
|
62
|
-
SAVED_ON = (%i[card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
|
|
63
|
-
question_cancelled question_relay turn_failed] + Events::TURN_KEPT).freeze
|
|
75
|
+
SAVED_ON = (%i[turn_started card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
|
|
76
|
+
question_cancelled question_relay turn_failed turn_rolled_back] + Events::TURN_KEPT).freeze
|
|
64
77
|
|
|
65
78
|
# What a session's saved file lists, as a store started from it lists
|
|
66
79
|
# (no turn running): the web's cards for a session no worker runs.
|
|
@@ -72,9 +85,12 @@ module Samagotchi
|
|
|
72
85
|
|
|
73
86
|
# @param path [String, nil] the file the entries are saved in and
|
|
74
87
|
# started from; nil keeps them in memory only
|
|
75
|
-
|
|
88
|
+
# @param killed_turn_stays [Boolean] the session keeps a running turn's
|
|
89
|
+
# prompt on disk (a worker's TurnCheckpoint): save that a turn runs
|
|
90
|
+
def initialize(capacity: CAPACITY, path: nil, killed_turn_stays: false)
|
|
76
91
|
@capacity = capacity
|
|
77
92
|
@path = path
|
|
93
|
+
@killed_turn_stays = killed_turn_stays
|
|
78
94
|
@mutex = Mutex.new
|
|
79
95
|
@entries = []
|
|
80
96
|
@turns_done = 0
|
|
@@ -122,21 +138,26 @@ module Samagotchi
|
|
|
122
138
|
return if entries.empty? && !File.exist?(@path)
|
|
123
139
|
|
|
124
140
|
FileUtils.mkdir_p(File.dirname(@path))
|
|
125
|
-
|
|
141
|
+
saved = { "version" => 1, "turns" => @turns_done, "entries" => entries }
|
|
142
|
+
saved["running"] = true if @killed_turn_stays && @running
|
|
143
|
+
AtomicFile.write(@path, JSON.generate(saved))
|
|
126
144
|
rescue StandardError
|
|
127
145
|
nil # a card that can't be saved still shows live
|
|
128
146
|
end
|
|
129
147
|
|
|
130
148
|
# The entries an earlier worker saved (a missing or broken file:
|
|
131
149
|
# none), their types as symbols again; a card that came during that
|
|
132
|
-
# worker's last turn stays where it was, as after a failed turn
|
|
150
|
+
# worker's last turn stays where it was, as after a failed turn, or
|
|
151
|
+
# after that turn when the worker was killed in it. A file from before
|
|
152
|
+
# "running" has none: no turn counted.
|
|
133
153
|
def seed
|
|
134
154
|
return unless @path && File.file?(@path)
|
|
135
155
|
|
|
136
156
|
data = JSON.parse(File.read(@path), symbolize_names: true)
|
|
137
157
|
return unless data.is_a?(Hash) && data[:turns].is_a?(Integer) && data[:entries].is_a?(Array)
|
|
138
158
|
|
|
139
|
-
|
|
159
|
+
killed_in_turn = data[:running] == true
|
|
160
|
+
@turns_done = data[:turns] + (killed_in_turn ? 1 : 0)
|
|
140
161
|
@entries = data[:entries].filter_map do |entry|
|
|
141
162
|
next unless entry.is_a?(Hash) && entry[:type].is_a?(String) && entry[:turns].is_a?(Integer)
|
|
142
163
|
|
|
@@ -144,7 +165,7 @@ module Samagotchi
|
|
|
144
165
|
entry = entry.merge(actions: []) if entry[:type] == :card && entry[:in_turn] && entry.key?(:actions)
|
|
145
166
|
entry unless entry[:type] == :guardrail_warning || open_question?(entry)
|
|
146
167
|
end
|
|
147
|
-
settle_during(after_turn:
|
|
168
|
+
settle_during(after_turn: killed_in_turn)
|
|
148
169
|
rescue StandardError
|
|
149
170
|
@turns_done = 0
|
|
150
171
|
@entries = []
|
|
@@ -169,6 +190,8 @@ module Samagotchi
|
|
|
169
190
|
kept = Events.kept_turn?(event)
|
|
170
191
|
@turns_done += 1 if kept
|
|
171
192
|
settle_during(after_turn: kept)
|
|
193
|
+
unstep_turn_left_nothing unless kept
|
|
194
|
+
when :turn_rolled_back then take_turn_back
|
|
172
195
|
when :card then add_card(event)
|
|
173
196
|
when :hook_notice then add_notice(event)
|
|
174
197
|
# What failed to load (guardrails, plugins): before the first turn
|
|
@@ -210,6 +233,28 @@ module Samagotchi
|
|
|
210
233
|
end
|
|
211
234
|
end
|
|
212
235
|
|
|
236
|
+
# The last kept turn's prompt left the conversation. An entry from
|
|
237
|
+
# after it (a card shown before the !rollback) stays before the next.
|
|
238
|
+
def take_turn_back
|
|
239
|
+
@turns_done = [@turns_done - 1, 0].max
|
|
240
|
+
unstep_turn_left_nothing
|
|
241
|
+
@entries.each { |entry| entry[:turns] = [entry[:turns], @turns_done].min }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# The turn that would be the next one's place has no prompt in the
|
|
245
|
+
# history: what it showed isn't a row of a turn any more (a later
|
|
246
|
+
# turn would read it as its own, +current+).
|
|
247
|
+
def unstep_turn_left_nothing
|
|
248
|
+
@entries = @entries.filter_map do |entry|
|
|
249
|
+
next entry unless entry[:in_turn] && entry[:turns] == @turns_done
|
|
250
|
+
|
|
251
|
+
case entry[:type]
|
|
252
|
+
when :card then entry.merge(in_turn: false, **(entry.key?(:actions) ? { actions: [] } : {}))
|
|
253
|
+
when :hook_notice then entry.except(:iteration, :calls).merge(in_turn: false)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
213
258
|
def add_notice(event)
|
|
214
259
|
notice = event.slice(:type, :hook, :text, :level, :fallback_for)
|
|
215
260
|
if event[:between_turns] || !@running
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -136,7 +136,7 @@ module Samagotchi
|
|
|
136
136
|
@accumulator = TurnAccumulator.new
|
|
137
137
|
# Saved in the session's folder: a later worker, and the web without
|
|
138
138
|
# one, show the cards where they were.
|
|
139
|
-
@cards = CardStore.new(path: File.join(session_dir, CardStore::FILE))
|
|
139
|
+
@cards = CardStore.new(path: File.join(session_dir, CardStore::FILE), killed_turn_stays: true)
|
|
140
140
|
@pending_card = PendingCard.new(session_dir)
|
|
141
141
|
@heartbeat_interval = heartbeat_interval
|
|
142
142
|
@epoch = SecureRandom.hex(4)
|
|
@@ -15,17 +15,19 @@ module Samagotchi
|
|
|
15
15
|
install <source> [--scope system|project] [--force]
|
|
16
16
|
Install a memory bundle from a directory, zip, tar archive, or git URL.
|
|
17
17
|
Source is the path/URL to the bundle (dir/zip/tar.gz/git).
|
|
18
|
-
--force overwrites existing entries; default skips them.
|
|
18
|
+
--force overwrites existing entries (one that differs is copied to the trash first); default skips them.
|
|
19
19
|
|
|
20
20
|
upgrade <source> [--scope system|project] [--force] [--dry-run] [--agent|--no-agent]
|
|
21
|
-
Upgrade a bundle (3-way
|
|
21
|
+
Upgrade a bundle (3-way: a file you didn't edit updates; one you edited takes a text
|
|
22
|
+
merge, the old file in the bundle trash; a real conflict leaves it as it is).
|
|
22
23
|
--dry-run shows what would change. --agent launches interactive session on conflict.
|
|
23
24
|
|
|
24
25
|
uninstall <bundle> [--scope system|project] [--force]
|
|
25
|
-
Remove a bundle and its index entries (--force
|
|
26
|
+
Remove a bundle and its index entries (--force moves a locally edited file to the trash).
|
|
26
27
|
|
|
27
28
|
status [<bundle>]
|
|
28
|
-
Show provenance
|
|
29
|
+
Show provenance and each file's status for bundles. issues= counts what
|
|
30
|
+
needs you: a file that stays behind, is missing or can't be read, not one you edited.
|
|
29
31
|
|
|
30
32
|
diff <bundle> [file]
|
|
31
33
|
Show diffs between base/current/incoming for a bundle.
|
|
@@ -66,7 +68,7 @@ module Samagotchi
|
|
|
66
68
|
install <source> [--scope system|project] [--force]
|
|
67
69
|
Install a memory bundle from a directory, zip, tar archive, or git URL,
|
|
68
70
|
or a bundle shipped with chi by name (see: chi bundle list).
|
|
69
|
-
--force overwrites existing entries; default skips them.
|
|
71
|
+
--force overwrites existing entries (one that differs is copied to the trash first); default skips them.
|
|
70
72
|
install --dev PATH
|
|
71
73
|
Install the bundle in the local directory PATH as a link to it: its plugin, hooks, rules and scripts
|
|
72
74
|
are PATH's own files (an edit applies at the next worker or chi web restart, with no re-install) and
|
|
@@ -231,6 +233,9 @@ module Samagotchi
|
|
|
231
233
|
run_installer(installer, failed: "Upgrade") do |manifest|
|
|
232
234
|
print_hooks_and_plugin(manifest, plugin: !dry_run)
|
|
233
235
|
next dry_run_done if dry_run
|
|
236
|
+
|
|
237
|
+
# Its summary said so, to someone reading.
|
|
238
|
+
Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).clear_merge_notes([], files: installer.merged_files)
|
|
234
239
|
next upgrade_conflicts(installer, bundle_name, expanded_source, agent) if installer.conflicts.any? && !force
|
|
235
240
|
|
|
236
241
|
print_provenance(bundle_name, manifest)
|
|
@@ -321,42 +326,64 @@ module Samagotchi
|
|
|
321
326
|
# (scripts already read it so): not CLI::Exit::USAGE by meaning.
|
|
322
327
|
CONFLICTS_KEPT = 2
|
|
323
328
|
|
|
324
|
-
# @return [Integer] 0 resolved, CONFLICTS_KEPT kept
|
|
329
|
+
# @return [Integer] 0 all resolved, CONFLICTS_KEPT kept some edits
|
|
325
330
|
def upgrade_conflicts(installer, bundle_name, expanded_source, agent)
|
|
326
|
-
|
|
327
|
-
|
|
331
|
+
standing = Samagotchi::MemoryBundle::FileStanding
|
|
332
|
+
@stdout.puts "\n#{standing.stay_behind(installer.behind.size)}: #{installer.behind.map(&:file).join(", ")}"
|
|
328
333
|
launch = if agent.nil? && @stdin.tty?
|
|
329
|
-
@stdout.print "
|
|
334
|
+
@stdout.print "Launch an interactive agent to merge them? [y/N] "
|
|
330
335
|
ans = begin; @stdin.gets; rescue StandardError => _e; nil; end
|
|
331
336
|
ans && ans.strip.downcase.start_with?("y")
|
|
332
337
|
elsif agent.nil?
|
|
333
|
-
@stdout.puts "Non-interactive terminal:
|
|
338
|
+
@stdout.puts "Non-interactive terminal: the rest is upgraded. #{standing::FORCE_TAKES}; --agent in a TTY merges."
|
|
334
339
|
return CONFLICTS_KEPT
|
|
335
340
|
else
|
|
336
341
|
agent
|
|
337
342
|
end
|
|
338
343
|
unless launch
|
|
339
|
-
@stdout.puts "
|
|
344
|
+
@stdout.puts "The rest is upgraded. #{standing.diff_hint(bundle_name, "FILE")} shows the base; #{standing::FORCE_TAKES}."
|
|
340
345
|
return CONFLICTS_KEPT
|
|
341
346
|
end
|
|
342
347
|
|
|
343
348
|
prompt = build_conflict_prompt(bundle_name, installer.conflicts, expanded_source)
|
|
344
|
-
|
|
349
|
+
before = installer.conflicts.to_h { |key, info| [key, Samagotchi::MemoryBundle::Merger.digest(info[:current])] }
|
|
350
|
+
@stdout.puts "Launching an interactive session to merge them… (/exit when done)"
|
|
345
351
|
require "samagotchi/terminal_ui"
|
|
346
352
|
Samagotchi::TerminalUI.new(prompt: prompt).run
|
|
347
|
-
|
|
348
|
-
# their old base); the resolved files now start from the bundle's
|
|
349
|
-
# version.
|
|
350
|
-
Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).resolve_conflicts(installer.conflicts)
|
|
351
|
-
@stdout.puts "Provenance updated after interactive resolution."
|
|
352
|
-
@stdout.puts "Upgrade resolved interactively."
|
|
353
|
-
0
|
|
353
|
+
settle_conflicts(installer.conflicts, before, bundle_name)
|
|
354
354
|
ensure
|
|
355
355
|
# An owned source (git/zip/tar) was kept for the incoming files above;
|
|
356
356
|
# the agent step is done with them now.
|
|
357
357
|
installer.cleanup_source!
|
|
358
358
|
end
|
|
359
359
|
|
|
360
|
+
# After the agent step. The installer already recorded the upgrade
|
|
361
|
+
# (conflicted files kept their old base); a file the step changed, with
|
|
362
|
+
# no conflict markers left, now starts from the bundle's version. Any
|
|
363
|
+
# other stays a conflict: moving its base would turn a visible miss into
|
|
364
|
+
# a kept edit the next upgrade says nothing about.
|
|
365
|
+
# @param before [Hash{String => String, nil}] each file's digest before the step
|
|
366
|
+
# @return [Integer] 0 all resolved, CONFLICTS_KEPT some still behind
|
|
367
|
+
def settle_conflicts(conflicts, before, bundle_name)
|
|
368
|
+
merger = Samagotchi::MemoryBundle::Merger
|
|
369
|
+
untouched, changed = conflicts.partition { |key, info| merger.digest(info[:current]) == before[key] }
|
|
370
|
+
marked, clean = changed.partition { |_, info| merger.markers?(info[:current]) }
|
|
371
|
+
settled = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).resolve_conflicts(clean.to_h)
|
|
372
|
+
gone = clean.map(&:first) - settled
|
|
373
|
+
standing = Samagotchi::MemoryBundle::FileStanding
|
|
374
|
+
untouched.each { |key, _| @stdout.puts standing.still_behind(:untouched, key) }
|
|
375
|
+
marked.each { |key, _| @stdout.puts standing.still_behind(:markers, key) }
|
|
376
|
+
gone.each { |key| @stdout.puts standing.still_behind(:incoming_gone, key) }
|
|
377
|
+
if untouched.empty? && marked.empty? && gone.empty?
|
|
378
|
+
@stdout.puts "Provenance updated after interactive resolution."
|
|
379
|
+
@stdout.puts "Upgrade resolved interactively."
|
|
380
|
+
return 0
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
@stdout.puts "Resolved: #{settled.join(", ")}" unless settled.empty?
|
|
384
|
+
CONFLICTS_KEPT
|
|
385
|
+
end
|
|
386
|
+
|
|
360
387
|
def uninstall(rest)
|
|
361
388
|
parsed = parse_flags(UNINSTALL_FLAGS, rest, "uninstall", help: UNINSTALL_HELP)
|
|
362
389
|
return parsed if parsed.is_a?(Integer)
|
|
@@ -377,6 +404,7 @@ module Samagotchi
|
|
|
377
404
|
uninstaller.run
|
|
378
405
|
@stdout.puts "Uninstalled bundle '#{bundle_name}'"
|
|
379
406
|
puts_trashed(uninstaller.trashed_files, uninstaller.trash_dir)
|
|
407
|
+
@stdout.puts uninstaller.trash_copy_line if uninstaller.trash_copy_line
|
|
380
408
|
@stdout.puts "Removed: #{uninstaller.removed_files.join(", ")}" unless uninstaller.removed_files.empty?
|
|
381
409
|
hook_removed = uninstaller.removed_files.count { |f| f.start_with?("hooks/") }
|
|
382
410
|
@stdout.puts "Hooks removed: #{hook_removed}" if hook_removed > 0
|
|
@@ -402,6 +430,7 @@ module Samagotchi
|
|
|
402
430
|
result.skipped.each { |member, why| lines << "skipped #{member}: #{why}" }
|
|
403
431
|
result.failed.each { |member, why| lines << "failed #{member}: #{why}" }
|
|
404
432
|
lines.concat(result.kept_own_lines)
|
|
433
|
+
lines.concat(result.trash_copies)
|
|
405
434
|
lines << "nothing new to install" if lines.empty?
|
|
406
435
|
lines.each { |line| @stdout.puts " #{line}" }
|
|
407
436
|
@stdout.puts "(dry-run: no changes written)" if dry_run
|
|
@@ -453,16 +482,21 @@ module Samagotchi
|
|
|
453
482
|
else
|
|
454
483
|
@stdout.puts "Target: #{st.target_dir}"
|
|
455
484
|
end
|
|
485
|
+
standing = Samagotchi::MemoryBundle::FileStanding
|
|
456
486
|
st.files.each do |k, info|
|
|
457
487
|
next @stdout.puts(" #{k}: unchecked") if info[:unchecked]
|
|
458
488
|
|
|
459
489
|
mods = []
|
|
460
|
-
mods << "conflict (kept your edits over v#{st.provenance.version}: chi bundle diff #{name} #{k})" if info[:conflict]
|
|
461
490
|
mods << "modified" if info[:modified]
|
|
462
491
|
mods << "missing" if info[:missing]
|
|
492
|
+
mods << "unreadable" if info[:unreadable]
|
|
463
493
|
mods << "no-index" unless info[:index_present] || info[:overlay]
|
|
464
|
-
label = mods.
|
|
494
|
+
label = mods.join(",")
|
|
495
|
+
label = "ok" if mods.empty? && !info[:behind]
|
|
465
496
|
label += " (model overlay)" if info[:overlay]
|
|
497
|
+
behind = info[:behind] && "#{info[:behind].state} (#{standing.diff_hint(name, k)})"
|
|
498
|
+
label = [label, behind].reject { |part| part.nil? || part.empty? }.join("; ")
|
|
499
|
+
label += "; #{info[:merge_note].line}#{" (old copy in #{info[:merge_note].trash})" if info[:merge_note].trash}" if info[:merge_note]
|
|
466
500
|
@stdout.puts " #{k}: #{label}"
|
|
467
501
|
end
|
|
468
502
|
# Hooks
|
|
@@ -499,7 +533,7 @@ module Samagotchi
|
|
|
499
533
|
next @stdout.puts(" #{bname} (manifest.json unreadable)") if record.error?
|
|
500
534
|
|
|
501
535
|
st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
|
|
502
|
-
mods = st.files.values.count { |v| v[:
|
|
536
|
+
mods = st.files.values.count { |v| v[:behind] || v[:missing] || v[:unreadable] }
|
|
503
537
|
hooks_count = st.provenance.hooks.size
|
|
504
538
|
hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
|
|
505
539
|
plugin = st.plugin
|
|
@@ -543,77 +577,62 @@ module Samagotchi
|
|
|
543
577
|
rescue ArgumentError => e
|
|
544
578
|
@stderr.puts "Bundle '#{bname}': #{e.message}"; return 1
|
|
545
579
|
end
|
|
546
|
-
files = record.files
|
|
547
580
|
hooks = record.hooks
|
|
548
581
|
plugin_path = prov.plugin_path(record)
|
|
549
582
|
plugin_file = plugin_path && File.basename(plugin_path)
|
|
550
|
-
show_plugin = lambda do
|
|
551
|
-
@stdout.puts "=== plugin/#{plugin_file} ==="
|
|
552
|
-
@stdout.puts "--- base (provenance) ---"
|
|
553
|
-
base = prov.plugin_base_path(plugin_file)
|
|
554
|
-
@stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
|
|
555
|
-
@stdout.puts "--- current (on-disk) ---"
|
|
556
|
-
@stdout.puts File.exist?(plugin_path) ? File.read(plugin_path) : "(missing)"
|
|
557
|
-
@stdout.puts "--- metadata: sha256=#{record.plugin.sha256}#{" requires_chi=#{record.requires_chi}" if record.requires_chi}"
|
|
558
|
-
end
|
|
559
583
|
if file_arg
|
|
560
584
|
# Try file first, then hook
|
|
561
585
|
if record.owns?(file_arg)
|
|
562
|
-
|
|
563
|
-
cur = File.join(target_dir, file_arg)
|
|
564
|
-
@stdout.puts "=== #{file_arg} ==="
|
|
565
|
-
@stdout.puts "--- base (provenance) ---"
|
|
566
|
-
@stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
|
|
567
|
-
@stdout.puts "--- current (on-disk) ---"
|
|
568
|
-
@stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
|
|
586
|
+
diff_memory(prov, target_dir, file_arg)
|
|
569
587
|
elsif hooks.key?(file_arg)
|
|
570
|
-
|
|
571
|
-
cur = File.join(prov.hooks_dir, file_arg)
|
|
572
|
-
@stdout.puts "=== hooks/#{file_arg} ==="
|
|
573
|
-
@stdout.puts "--- base (provenance) ---"
|
|
574
|
-
@stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
|
|
575
|
-
@stdout.puts "--- current (on-disk) ---"
|
|
576
|
-
@stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
|
|
577
|
-
# Show hook metadata
|
|
578
|
-
@stdout.puts hook_metadata_line(hooks[file_arg])
|
|
588
|
+
diff_hook(prov, file_arg, hooks[file_arg])
|
|
579
589
|
elsif plugin_file && [plugin_file, "plugin/#{plugin_file}"].include?(file_arg)
|
|
580
|
-
|
|
590
|
+
diff_plugin(prov, record, plugin_path)
|
|
581
591
|
else
|
|
582
592
|
@stdout.puts "=== #{file_arg} ==="
|
|
583
593
|
@stdout.puts "(not found in bundle)"
|
|
584
594
|
end
|
|
585
595
|
@stdout.puts ""
|
|
586
596
|
else
|
|
587
|
-
files.each_key do |
|
|
588
|
-
|
|
589
|
-
cur = File.join(target_dir, k)
|
|
590
|
-
@stdout.puts "=== #{k} ==="
|
|
591
|
-
@stdout.puts "--- base (provenance) ---"
|
|
592
|
-
@stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
|
|
593
|
-
@stdout.puts "--- current (on-disk) ---"
|
|
594
|
-
@stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
|
|
597
|
+
record.files.each_key do |key|
|
|
598
|
+
diff_memory(prov, target_dir, key)
|
|
595
599
|
@stdout.puts ""
|
|
596
600
|
end
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
base = prov.base_path(k)
|
|
600
|
-
cur = File.join(prov.hooks_dir, k)
|
|
601
|
-
@stdout.puts "=== hooks/#{k} ==="
|
|
602
|
-
@stdout.puts "--- base (provenance) ---"
|
|
603
|
-
@stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
|
|
604
|
-
@stdout.puts "--- current (on-disk) ---"
|
|
605
|
-
@stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
|
|
606
|
-
@stdout.puts hook_metadata_line(hook)
|
|
601
|
+
hooks.each do |key, hook|
|
|
602
|
+
diff_hook(prov, key, hook)
|
|
607
603
|
@stdout.puts ""
|
|
608
604
|
end
|
|
609
605
|
if plugin_file
|
|
610
|
-
|
|
606
|
+
diff_plugin(prov, record, plugin_path)
|
|
611
607
|
@stdout.puts ""
|
|
612
608
|
end
|
|
613
609
|
end
|
|
614
610
|
0
|
|
615
611
|
end
|
|
616
612
|
|
|
613
|
+
def diff_memory(prov, target_dir, key)
|
|
614
|
+
print_base_and_current(key, prov.base_path(key), File.join(target_dir, key))
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def diff_hook(prov, key, hook)
|
|
618
|
+
print_base_and_current("hooks/#{key}", prov.base_path(key), File.join(prov.hooks_dir, key))
|
|
619
|
+
@stdout.puts hook_metadata_line(hook)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def diff_plugin(prov, record, plugin_path)
|
|
623
|
+
file = File.basename(plugin_path)
|
|
624
|
+
print_base_and_current("plugin/#{file}", prov.plugin_base_path(file), plugin_path)
|
|
625
|
+
@stdout.puts "--- metadata: sha256=#{record.plugin.sha256}#{" requires_chi=#{record.requires_chi}" if record.requires_chi}"
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def print_base_and_current(heading, base, current)
|
|
629
|
+
@stdout.puts "=== #{heading} ==="
|
|
630
|
+
@stdout.puts "--- base (provenance) ---"
|
|
631
|
+
@stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
|
|
632
|
+
@stdout.puts "--- current (on-disk) ---"
|
|
633
|
+
@stdout.puts File.exist?(current) ? File.read(current) : "(missing)"
|
|
634
|
+
end
|
|
635
|
+
|
|
617
636
|
def list
|
|
618
637
|
listing = Samagotchi::MemoryBundle::Listing
|
|
619
638
|
shipped = listing.shipped
|
|
@@ -634,6 +653,10 @@ module Samagotchi
|
|
|
634
653
|
line = " #{b.name.ljust(name_w)} #{"v#{b.version || "?"}".ljust(ver_w)} scope=#{b.scope || "?"} " \
|
|
635
654
|
"#{b.includes ? profile_members(b.includes, installed) : "files=#{b.files}"} installed=#{b.installed_at || "?"}"
|
|
636
655
|
line += " (shipped v#{b.upgrade.version}: chi bundle upgrade #{b.upgrade.source})" if b.upgrade
|
|
656
|
+
if b.behind&.any?
|
|
657
|
+
count = Samagotchi::MemoryBundle::FileStanding.behind_count(b.behind.size, unsure: b.behind.any?(&:unsure))
|
|
658
|
+
line += " (#{count}: chi bundle status #{b.name})"
|
|
659
|
+
end
|
|
637
660
|
@stdout.puts line
|
|
638
661
|
end
|
|
639
662
|
@stdout.puts " (or all at once: chi update)" if installed.any?(&:upgrade)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# Bundler's variables, in one place. Every chi spawn that must not carry a
|
|
5
|
+
# `bundle exec` around it takes its names from here.
|
|
6
|
+
module BundlerEnv
|
|
7
|
+
# All Bundler's own and its settings (BUNDLE_PATH, BUNDLE_WITHOUT, ...), for
|
|
8
|
+
# what must take none: a pattern's start (ChiShim, ProjectEnv::RESTORE_NEVER).
|
|
9
|
+
PREFIX = "BUNDLE"
|
|
10
|
+
# What `bundle exec` sets for a child (Bundler::SharedHelpers#set_bundle_variables;
|
|
11
|
+
# bundler_env_spec asks it). Without them, and without Bundler's RUBYOPT, a
|
|
12
|
+
# child ruby is free of the bundle; BUNDLER_SETUP on its own isn't: RubyGems
|
|
13
|
+
# requires what it names at boot, and a Gemfile it then finds writes its lock
|
|
14
|
+
# to BUNDLE_LOCKFILE, the bundle's. A project's own settings (BUNDLE_PATH and
|
|
15
|
+
# the like) aren't here: its `bundle` keeps them. Nor is BUNDLER_ORIG_*, which
|
|
16
|
+
# also reaches a tool: a project's `bundle exec` there reads it, for
|
|
17
|
+
# Bundler.original_env and with_unbundled_env, whose children get the PATH and
|
|
18
|
+
# GEM_* chi's own `bundle exec` started from.
|
|
19
|
+
VARS = %w[BUNDLE_BIN_PATH BUNDLE_GEMFILE BUNDLE_LOCKFILE BUNDLER_SETUP BUNDLER_VERSION].freeze
|
|
20
|
+
# With what else `bundle exec` steers a child ruby by: what a command that
|
|
21
|
+
# isn't the bundle's runs without (TaskRuntime, ProjectEnv, ChiHome.probe).
|
|
22
|
+
CHILD_RUBY = (%w[RUBYOPT RUBYLIB] + VARS).freeze
|
|
23
|
+
end
|
|
24
|
+
end
|