samagotchi 0.122.0 → 0.124.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 +58 -1
- data/bin/chi +13 -2
- data/docs/architecture.md +36 -7
- data/docs/cli.md +48 -3
- data/docs/configuration.md +31 -6
- data/docs/desktop.md +51 -3
- data/docs/guardrails.md +112 -15
- data/docs/internals/engine.md +11 -4
- data/docs/internals/participants.md +67 -17
- data/docs/internals/prompt-caching.md +19 -7
- data/docs/internals/web-push.md +7 -3
- data/docs/sessions.md +7 -3
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bridge.rb +4 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +7 -1
- data/lib/samagotchi/delivery.rb +7 -0
- data/lib/samagotchi/desktop/macos/App.swift +9 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +129 -17
- data/lib/samagotchi/desktop/macos/Images.swift +4 -3
- data/lib/samagotchi/desktop/macos/Panel.swift +315 -80
- data/lib/samagotchi/desktop/macos/Sessions.swift +272 -0
- data/lib/samagotchi/engine.rb +200 -29
- data/lib/samagotchi/gem_update.rb +41 -4
- data/lib/samagotchi/guardrail_wiring.rb +45 -12
- data/lib/samagotchi/guardrails/approval.rb +42 -4
- data/lib/samagotchi/guardrails/gate.rb +1 -1
- data/lib/samagotchi/guardrails/pair_boundary.rb +172 -0
- data/lib/samagotchi/guardrails/pair_command.rb +365 -0
- data/lib/samagotchi/guardrails/review_paths.rb +41 -7
- data/lib/samagotchi/guardrails/shell_lex.rb +7 -5
- data/lib/samagotchi/guardrails/shell_paths.rb +10 -1
- data/lib/samagotchi/guardrails/verdict.rb +7 -0
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/guardrails_command.rb +6 -1
- data/lib/samagotchi/kernel_loop.rb +20 -0
- data/lib/samagotchi/llm/chat_loop.rb +4 -0
- data/lib/samagotchi/participant_mode.rb +77 -3
- data/lib/samagotchi/participants.rb +7 -5
- data/lib/samagotchi/review_apply.rb +10 -2
- data/lib/samagotchi/review_start.rb +38 -10
- data/lib/samagotchi/run_options.rb +43 -2
- data/lib/samagotchi/session_access/local.rb +5 -2
- data/lib/samagotchi/session_manager.rb +11 -0
- data/lib/samagotchi/session_participants.rb +18 -3
- data/lib/samagotchi/share_command.rb +2 -2
- data/lib/samagotchi/steer.rb +7 -1
- data/lib/samagotchi/system_one/client.rb +10 -2
- data/lib/samagotchi/system_prompt.rb +20 -4
- data/lib/samagotchi/terminal_ui/question_prompt.rb +17 -1
- data/lib/samagotchi/tool_runner.rb +10 -1
- data/lib/samagotchi/toolkit_binding.rb +34 -0
- data/lib/samagotchi/toolkit_report_command.rb +6 -4
- data/lib/samagotchi/toolkit_suggest.rb +181 -15
- data/lib/samagotchi/tools/builtins.rb +14 -1
- data/lib/samagotchi/update_command.rb +47 -19
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +204 -9
- data/lib/samagotchi/web/grants.rb +17 -5
- data/lib/samagotchi/web/message_display.rb +6 -1
- data/lib/samagotchi/web/participant_review.rb +3 -1
- data/lib/samagotchi/web/participant_view.rb +12 -4
- data/lib/samagotchi/web/participants_admin.rb +10 -6
- data/lib/samagotchi/web/public/app.js +73 -10
- data/lib/samagotchi/web/public/data.js +17 -0
- data/lib/samagotchi/web/public/delivery_line.js +8 -6
- data/lib/samagotchi/web/public/index.html +21 -2
- data/lib/samagotchi/web/public/participant.html +1 -1
- data/lib/samagotchi/web/public/participant.js +10 -12
- data/lib/samagotchi/web/public/participant_model.js +20 -8
- data/lib/samagotchi/web/public/participant_names.js +29 -8
- data/lib/samagotchi/web/public/participants_panel.js +17 -3
- data/lib/samagotchi/web/public/question_card.js +12 -2
- data/lib/samagotchi/web/public/question_cards.js +7 -0
- data/lib/samagotchi/web/public/stream_handlers.js +6 -2
- data/lib/samagotchi/web/public/toasts.js +13 -5
- data/lib/samagotchi/web/public/tools_overview.js +42 -0
- data/lib/samagotchi/web/public/turn_events.js +5 -4
- data/lib/samagotchi/web/public/turn_html.js +2 -2
- data/lib/samagotchi/web/public/turn_view.js +6 -4
- data/lib/samagotchi/web/public/update_notice.js +33 -4
- data/lib/samagotchi/web/public/update_sheet.js +158 -0
- data/lib/samagotchi/web/push/notifier.rb +28 -1
- data/lib/samagotchi/web/server.rb +121 -8
- data/lib/samagotchi/web/session_access_routes.rb +2 -1
- data/lib/samagotchi/web/session_hub.rb +52 -3
- data/lib/samagotchi/web/tunnel/tailscale.rb +8 -2
- data/lib/samagotchi/web/update_check.rb +186 -0
- data/lib/samagotchi/worker.rb +44 -6
- metadata +7 -1
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
|
+
require_relative "cancellation_controller"
|
|
6
|
+
require_relative "log"
|
|
5
7
|
require_relative "message_time"
|
|
6
8
|
require_relative "steer"
|
|
7
9
|
require_relative "system_one/question"
|
|
10
|
+
require_relative "toolkit"
|
|
8
11
|
|
|
9
12
|
module Samagotchi
|
|
10
|
-
# The first-prompt toolkit classifier and its append-only
|
|
11
|
-
# The Engine prepares a Snapshot on its turn thread; #
|
|
12
|
-
# copy, so the question cannot change the running session or tool
|
|
13
|
+
# The first-prompt toolkit classifier and its append-only record.
|
|
14
|
+
# The Engine prepares a Snapshot on its turn thread; #judge only sees that
|
|
15
|
+
# copy, so the question cannot change the running session or tool
|
|
16
|
+
# registry. On mode's turn applies the answer itself (Pending#wait,
|
|
17
|
+
# ToolkitBinding#suggest).
|
|
13
18
|
class ToolkitSuggest
|
|
14
19
|
FILE = "toolkit_suggestions.jsonl"
|
|
15
20
|
QUESTION_ID = "toolkit"
|
|
@@ -24,6 +29,10 @@ module Samagotchi
|
|
|
24
29
|
# built-ins registered when the ask started.
|
|
25
30
|
Snapshot = Data.define(:session_id, :turn_number, :default, :candidates, :core_tools, :state)
|
|
26
31
|
Suggestion = Data.define(:kit, :raw, :confidence, :decided)
|
|
32
|
+
# One ask's outcome: its Suggestion, what on mode would do with it
|
|
33
|
+
# (suggested, default, defer, below_threshold, invalid, no_judgment)
|
|
34
|
+
# and its log line's fields.
|
|
35
|
+
Judged = Data.define(:suggestion, :would, :fields)
|
|
27
36
|
|
|
28
37
|
# The candidates with classifier descriptions, excluding the configured
|
|
29
38
|
# default, the participants' review boundary, the defer sentinel, and a
|
|
@@ -56,8 +65,8 @@ module Samagotchi
|
|
|
56
65
|
[question, { project: project.to_s, prompt: prompt.to_s[0, PROMPT_MAX] }]
|
|
57
66
|
end
|
|
58
67
|
|
|
59
|
-
# The single turn eligibility predicate shared by audit and
|
|
60
|
-
#
|
|
68
|
+
# The single turn eligibility predicate shared by audit and on mode.
|
|
69
|
+
# A later prompt is eligible only inside an undecided defer window.
|
|
61
70
|
# +candidate_count+ and +last_suggestion+ may be callables: they are
|
|
62
71
|
# read only once the session's own clauses pass (the kits read memory
|
|
63
72
|
# files; the suggestion log is read whole), the log only on the defer
|
|
@@ -70,7 +79,7 @@ module Samagotchi
|
|
|
70
79
|
return false if value(candidate_count).to_i < 2
|
|
71
80
|
|
|
72
81
|
messages = Array(session.messages)
|
|
73
|
-
return true unless
|
|
82
|
+
return true unless model_reply?(messages)
|
|
74
83
|
return false unless field(value(last_suggestion), :raw) == "undecided"
|
|
75
84
|
|
|
76
85
|
current_turn_number = turn_number(messages)
|
|
@@ -108,15 +117,27 @@ module Samagotchi
|
|
|
108
117
|
end
|
|
109
118
|
end
|
|
110
119
|
|
|
111
|
-
# Runs one already-prepared ask
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
# Runs one already-prepared ask and appends its line (audit's thread).
|
|
121
|
+
# @return [Suggestion]
|
|
122
|
+
def self.ask(client:, log:, snapshot:, threshold:, cancel_controller: nil)
|
|
123
|
+
judged = judge(client: client, snapshot: snapshot, threshold: threshold, cancel_controller: cancel_controller)
|
|
124
|
+
record(log, judged)
|
|
125
|
+
judged.suggestion
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Runs one already-prepared ask: its raw pick, what on mode would do
|
|
129
|
+
# with it, and the log line's fields (every offered kit's #rules, the
|
|
130
|
+
# default's too, so the report can tell a narrower kit that would also
|
|
131
|
+
# have covered the turn). A missing judgment (Client#ask's nil) is a
|
|
132
|
+
# Judged too, would "no_judgment".
|
|
133
|
+
# @raise [LLM::RequestCancelled] the turn stopped while it was asked
|
|
134
|
+
# @return [Judged]
|
|
135
|
+
def self.judge(client:, snapshot:, threshold:, cancel_controller: nil)
|
|
116
136
|
question, = question(default: Toolkit.new(name: snapshot.default.name),
|
|
117
137
|
kits: snapshot.candidates.to_h { |candidate| [candidate.name, kit_for(candidate)] },
|
|
118
138
|
project: snapshot.state.fetch(:project), prompt: snapshot.state.fetch(:prompt))
|
|
119
|
-
answer = client.ask(state: snapshot.state, questions: { QUESTION_ID => question }
|
|
139
|
+
answer = client.ask(state: snapshot.state, questions: { QUESTION_ID => question },
|
|
140
|
+
cancel_controller: cancel_controller)&.fetch(QUESTION_ID, nil)
|
|
120
141
|
raw = answer&.choice
|
|
121
142
|
candidate_name = raw.to_s.delete_prefix("kit:")
|
|
122
143
|
candidate = snapshot.candidates.find { |item| item.name == candidate_name } if raw.to_s.start_with?("kit:")
|
|
@@ -140,7 +161,7 @@ module Samagotchi
|
|
|
140
161
|
else
|
|
141
162
|
"suggested"
|
|
142
163
|
end
|
|
143
|
-
|
|
164
|
+
fields = {
|
|
144
165
|
session_id: snapshot.session_id,
|
|
145
166
|
turn_number: snapshot.turn_number,
|
|
146
167
|
default_kit: snapshot.default.name,
|
|
@@ -151,8 +172,153 @@ module Samagotchi
|
|
|
151
172
|
would: would,
|
|
152
173
|
core_tools: snapshot.core_tools,
|
|
153
174
|
kits: [snapshot.default, *snapshot.candidates].to_h { |kit| [kit.name, kit.rules] }
|
|
154
|
-
}
|
|
155
|
-
Suggestion.new(kit: selected, raw: raw, confidence: confidence, decided: !!decided)
|
|
175
|
+
}
|
|
176
|
+
Judged.new(suggestion: Suggestion.new(kit: selected, raw: raw, confidence: confidence, decided: !!decided),
|
|
177
|
+
would: would, fields: fields)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# One line in the suggestion log. +did+ (on mode only) is what the
|
|
181
|
+
# turn did with it: suggested (the session switched), default, defer,
|
|
182
|
+
# chosen (the user picked a toolkit first), timeout or canceled (the
|
|
183
|
+
# turn stopped waiting; the line is written when the ask ends).
|
|
184
|
+
def self.record(log, judged, did: nil)
|
|
185
|
+
log.entry("suggestion", did.nil? ? judged.fields : judged.fields.merge(did: did))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Whether the session has a model reply: a later prompt's ask is the
|
|
189
|
+
# defer path, and on mode's switch applies "from this turn".
|
|
190
|
+
def self.model_reply?(messages)
|
|
191
|
+
Array(messages).any? { |message| field(message, :role).to_s == "model" }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# An ask's hand-over between its thread and the turn. Audit mode never
|
|
195
|
+
# waits (+joined+ false): the thread records its line itself. On mode's
|
|
196
|
+
# turn waits (#wait) until the answer, a Stop or the deadline (System
|
|
197
|
+
# 1's timeout from the ask's start), and records what it did with the
|
|
198
|
+
# answer (#record); a turn that stopped waiting leaves the line to the
|
|
199
|
+
# thread, which writes it with why (timeout, canceled) when the ask
|
|
200
|
+
# ends.
|
|
201
|
+
#
|
|
202
|
+
# The request watches the ask's own controller (#cancel_controller),
|
|
203
|
+
# cut only by a Stop the turn sees before it settled (#wait, #abandon
|
|
204
|
+
# :canceled): a Stop later in a turn that went on after a timeout
|
|
205
|
+
# leaves the late ask (and its timeout line) alone.
|
|
206
|
+
class Pending
|
|
207
|
+
POLL_SECONDS = 0.05
|
|
208
|
+
|
|
209
|
+
# @return [CancellationController] what the ask's request watches
|
|
210
|
+
attr_reader :cancel_controller
|
|
211
|
+
|
|
212
|
+
def initialize(log:, timeout_seconds:, joined:, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
213
|
+
@log = log
|
|
214
|
+
@clock = clock
|
|
215
|
+
@deadline = clock.call + timeout_seconds.to_f
|
|
216
|
+
@joined = joined
|
|
217
|
+
@cancel_controller = CancellationController.new
|
|
218
|
+
@mutex = Mutex.new
|
|
219
|
+
@done = ConditionVariable.new
|
|
220
|
+
@finished = false
|
|
221
|
+
@recorded = false
|
|
222
|
+
@settled = false
|
|
223
|
+
@judged = nil
|
|
224
|
+
@gave_up = nil
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# On the ask's thread, always (an ensure): its Judged, nil when it
|
|
228
|
+
# was cut (a Stop) or raised.
|
|
229
|
+
def finish(judged)
|
|
230
|
+
@mutex.synchronize do
|
|
231
|
+
@finished = true
|
|
232
|
+
@judged = judged
|
|
233
|
+
if !@joined
|
|
234
|
+
write(nil)
|
|
235
|
+
elsif @gave_up
|
|
236
|
+
write(@gave_up)
|
|
237
|
+
end
|
|
238
|
+
@done.broadcast
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# On the turn thread: [:answered, Judged or nil] once the ask ended,
|
|
243
|
+
# [:canceled, nil] when +controller+ is cancelled (first: a Stop wins
|
|
244
|
+
# over an answer already in), [:timeout, nil] at the deadline. It
|
|
245
|
+
# waits in short slices, so a Stop is seen at once.
|
|
246
|
+
def wait(controller)
|
|
247
|
+
@mutex.synchronize do
|
|
248
|
+
loop do
|
|
249
|
+
return give_up(:canceled) if controller&.cancelled?
|
|
250
|
+
return [:answered, @judged] if @finished
|
|
251
|
+
|
|
252
|
+
remaining = @deadline - @clock.call
|
|
253
|
+
return give_up(:timeout) unless remaining.positive?
|
|
254
|
+
|
|
255
|
+
@done.wait(@mutex, [remaining, POLL_SECONDS].min)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# The answered ask's line, with what the turn did.
|
|
261
|
+
def record(did)
|
|
262
|
+
@mutex.synchronize do
|
|
263
|
+
@settled = true
|
|
264
|
+
write(did.to_s)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# The turn ended before it settled the ask (a Stop before or after
|
|
269
|
+
# the wait, a failure, an Interrupt): its line says +reason+, now or
|
|
270
|
+
# when the ask ends. Nothing once the turn settled it (#record, a
|
|
271
|
+
# timeout or Stop in #wait), so prepare_turn calls it from an ensure.
|
|
272
|
+
def abandon(reason)
|
|
273
|
+
@mutex.synchronize do
|
|
274
|
+
next if @settled
|
|
275
|
+
|
|
276
|
+
if @finished
|
|
277
|
+
@settled = true
|
|
278
|
+
write(reason.to_s)
|
|
279
|
+
else
|
|
280
|
+
give_up(reason.to_sym)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def give_up(reason)
|
|
288
|
+
@settled = true
|
|
289
|
+
@gave_up = reason.to_s
|
|
290
|
+
@cancel_controller.cancel!(:manual) if reason == :canceled
|
|
291
|
+
# An answer already in (a Stop wins over it): its line now.
|
|
292
|
+
write(@gave_up) if @finished
|
|
293
|
+
[reason, nil]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Once: an ask that ended with nothing (cut, raised) has no line.
|
|
297
|
+
def write(did)
|
|
298
|
+
return if @recorded || @judged.nil?
|
|
299
|
+
|
|
300
|
+
@recorded = true
|
|
301
|
+
ToolkitSuggest.record(@log, @judged, did: did)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# The warm-up's stand-in prompt: the question (instruction + criteria)
|
|
306
|
+
# is the real one, so System 1's server has that prefix cached when
|
|
307
|
+
# the first real prompt is asked.
|
|
308
|
+
WARMUP_PROMPT = "hello"
|
|
309
|
+
|
|
310
|
+
# One ask with the real question and a stand-in prompt, its answer
|
|
311
|
+
# dropped: on mode's warm-up as an idle worker starts (Engine
|
|
312
|
+
# #warm_toolkit_suggestion!). Straight to the backend, so a failure is
|
|
313
|
+
# one info line, never Client#ask's warn, and nothing is logged as a
|
|
314
|
+
# suggestion.
|
|
315
|
+
def self.warm(client:, default:, kits:, project:)
|
|
316
|
+
question, state = question(default: default, kits: kits, project: project, prompt: WARMUP_PROMPT)
|
|
317
|
+
client.backend.ask(state: state, questions: { QUESTION_ID => question })
|
|
318
|
+
nil
|
|
319
|
+
rescue StandardError => e
|
|
320
|
+
Log.info(:toolkit, "toolkit_suggest_warmup_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
321
|
+
nil
|
|
156
322
|
end
|
|
157
323
|
|
|
158
324
|
# A resolved kit (Toolkit::Resolved) as frozen, JSON-ready rules, not
|
|
@@ -25,6 +25,7 @@ require_relative "delegate_result"
|
|
|
25
25
|
require_relative "ask_user_question"
|
|
26
26
|
require_relative "forget_outputs"
|
|
27
27
|
require_relative "../guardrails/parent_approvals"
|
|
28
|
+
require_relative "../guardrails/pair_boundary"
|
|
28
29
|
require_relative "../model_profile"
|
|
29
30
|
|
|
30
31
|
module Samagotchi
|
|
@@ -89,7 +90,7 @@ module Samagotchi
|
|
|
89
90
|
end,
|
|
90
91
|
Execute::NAME => lambda do |call, kctx|
|
|
91
92
|
Execute.call(call[:content], cwd: call[:cwd], cancelled: cancelled_proc(kctx), stop_reason: stop_reason_proc(kctx),
|
|
92
|
-
|
|
93
|
+
**participant_execute(kctx, parent_env(kctx)))
|
|
93
94
|
end,
|
|
94
95
|
RegisterReminder::NAME => lambda do |call, kctx|
|
|
95
96
|
RegisterReminder.call(call[:content], reminder_store: kctx.reminder_store, description: call[:description],
|
|
@@ -143,6 +144,18 @@ module Samagotchi
|
|
|
143
144
|
SESSION_REASONING_HISTORY_ENV => history }.merge(cli_model_unset(env)).merge(xdg_absolute(env))
|
|
144
145
|
end
|
|
145
146
|
|
|
147
|
+
# execute's env: and on_timeout: for the running turn. A participant's
|
|
148
|
+
# turn runs without the variables whose names hold a secret
|
|
149
|
+
# (Guardrails::PairBoundary.scrubbed_env), and a command still running
|
|
150
|
+
# at the limit is killed, never handed over as a background task that
|
|
151
|
+
# would outlive their turn; the owner's turns as configured.
|
|
152
|
+
def participant_execute(kctx, env)
|
|
153
|
+
mode = kctx.respond_to?(:participant_mode) ? kctx.participant_mode : nil
|
|
154
|
+
return { env: env } unless mode
|
|
155
|
+
|
|
156
|
+
{ env: env.merge(Guardrails::PairBoundary.scrubbed_env), on_timeout: :kill }
|
|
157
|
+
end
|
|
158
|
+
|
|
146
159
|
# The XDG dirs set in +env+, made absolute (nothing for an unset one).
|
|
147
160
|
def xdg_absolute(env = ENV)
|
|
148
161
|
Paths.absolute_xdg(env: env)
|
|
@@ -12,6 +12,7 @@ require_relative "memory_bundle/system_bundle"
|
|
|
12
12
|
require_relative "session"
|
|
13
13
|
require_relative "version"
|
|
14
14
|
require_relative "web/lan"
|
|
15
|
+
require "json"
|
|
15
16
|
|
|
16
17
|
module Samagotchi
|
|
17
18
|
# `chi update`: brings an installed chi up to date in one go: the system
|
|
@@ -24,13 +25,15 @@ module Samagotchi
|
|
|
24
25
|
include CLI::Command
|
|
25
26
|
|
|
26
27
|
USAGE = <<~TEXT
|
|
27
|
-
Usage: chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]
|
|
28
|
+
Usage: chi update [--dry-run] [--to VERSION] [--json] [--no-gem] [--no-bundles] [--no-desktop]
|
|
28
29
|
Updates this chi's install: the gem (from rubygems.org), then the
|
|
29
30
|
system bundle, the bundles chi ships that you installed (chi bundle
|
|
30
31
|
list), and the desktop helper (macOS; rebuilt only when its sources
|
|
31
32
|
changed). Edited memory files are kept and reported; nothing is
|
|
32
33
|
installed that isn't already, and old gem versions stay.
|
|
33
34
|
--dry-run show what would change; change nothing
|
|
35
|
+
--to VERSION install exactly this gem version (must be newer than this chi)
|
|
36
|
+
--json print update rows as one JSON document
|
|
34
37
|
--no-gem don't install a newer gem (config update.gem: false)
|
|
35
38
|
--no-bundles leave the shipped bundles alone (config update.bundles: false)
|
|
36
39
|
--no-desktop leave the desktop helper alone (config update.desktop: false)
|
|
@@ -44,6 +47,8 @@ module Samagotchi
|
|
|
44
47
|
FLAGS = CLI::Flags.new(args: false) do |f|
|
|
45
48
|
f.switch "-h", "--help", "help"
|
|
46
49
|
f.switch "--dry-run"
|
|
50
|
+
f.value "--to"
|
|
51
|
+
f.switch "--json"
|
|
47
52
|
f.switch "--no-gem"
|
|
48
53
|
f.switch "--no-bundles"
|
|
49
54
|
f.switch "--no-desktop"
|
|
@@ -54,11 +59,11 @@ module Samagotchi
|
|
|
54
59
|
end
|
|
55
60
|
# The switches the newer gem's update gets again (hand_over).
|
|
56
61
|
HANDED_OVER = { "--no-gem" => :no_gem, "--no-bundles" => :no_bundles, "--no-desktop" => :no_desktop,
|
|
57
|
-
"--no-register" => :no_register }.freeze
|
|
62
|
+
"--no-register" => :no_register, "--to" => :to, "--json" => :json }.freeze
|
|
58
63
|
|
|
59
64
|
# A table row. info rows (live workers, chi web) say what runs; they
|
|
60
65
|
# never fail.
|
|
61
|
-
Row = Struct.new(:component, :from, :to, :status, :failed, keyword_init: true)
|
|
66
|
+
Row = Struct.new(:component, :from, :to, :status, :failed, :kept, keyword_init: true)
|
|
62
67
|
|
|
63
68
|
# What a row says after a change, and in a dry run.
|
|
64
69
|
VERBS = { update: ["updated", "would update"], install: ["installed", "would install"],
|
|
@@ -95,6 +100,13 @@ module Samagotchi
|
|
|
95
100
|
@stdout.puts(USAGE)
|
|
96
101
|
return 0
|
|
97
102
|
end
|
|
103
|
+
return usage_error("invalid version #{options[:to]}") if options[:to] && !Gem::Version.correct?(options[:to])
|
|
104
|
+
|
|
105
|
+
if options[:to] && !options[:gem_from] && Gem::Version.new(options[:to]) <= Gem::Version.new(VERSION)
|
|
106
|
+
@stderr.puts("chi update: #{options[:to]} is not newer than the running chi (#{VERSION})")
|
|
107
|
+
return 1
|
|
108
|
+
end
|
|
109
|
+
|
|
98
110
|
unless @gem_spec
|
|
99
111
|
@stderr.puts("chi update: running from a checkout (#{InstalledGem::ROOT}): chi update updates an installed chi; " \
|
|
100
112
|
"git pull, or chi bundle upgrade NAME for one bundle")
|
|
@@ -104,9 +116,13 @@ module Samagotchi
|
|
|
104
116
|
@dry_run = options[:dry_run]
|
|
105
117
|
gem = gem_row(options)
|
|
106
118
|
rows = [gem, system_bundle_row, *bundle_rows(options), desktop_row(options), *live_rows].compact
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
if options[:json]
|
|
120
|
+
@stdout.puts(JSON.generate(rows.map { |row| row.to_h.merge(failed: !!row.failed, kept: row.kept || []) }))
|
|
121
|
+
else
|
|
122
|
+
print_table(rows)
|
|
123
|
+
footers(options).each { |line| @stdout.puts(line) }
|
|
124
|
+
@stdout.puts(summary(rows))
|
|
125
|
+
end
|
|
110
126
|
rows.any?(&:failed) ? 1 : 0
|
|
111
127
|
end
|
|
112
128
|
|
|
@@ -133,22 +149,32 @@ module Samagotchi
|
|
|
133
149
|
row.to = VERSION
|
|
134
150
|
return done(row, :update)
|
|
135
151
|
end
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return skipped(row, "under Bundler: bundle update samagotchi") if @bundler
|
|
152
|
+
why = off(options, "gem") || ("under Bundler: bundle update samagotchi" if @bundler)
|
|
153
|
+
# --to asks for that exact version: not installing it is a failure, not a skip.
|
|
154
|
+
return options[:to] ? failed(row, "skipped: #{why}") : skipped(row, why) if why
|
|
140
155
|
|
|
141
156
|
latest = begin
|
|
142
|
-
|
|
157
|
+
if options[:to]
|
|
158
|
+
if ENV[GemUpdate::LOCAL_ENV].to_s.empty?
|
|
159
|
+
GemUpdate::Latest.new(version: options[:to], source: "samagotchi")
|
|
160
|
+
else
|
|
161
|
+
local = @gem_update.latest
|
|
162
|
+
return failed(row, "local gem file is #{local.version}, not #{options[:to]}") unless local.version == options[:to]
|
|
163
|
+
|
|
164
|
+
local
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
@gem_update.latest
|
|
168
|
+
end
|
|
143
169
|
rescue GemUpdate::Error => e
|
|
144
170
|
return row.tap { |r| r.status = "couldn't check (#{e.message})" }
|
|
145
171
|
end
|
|
146
|
-
return up_to_date(row) unless @gem_update.newer?(latest)
|
|
172
|
+
return up_to_date(row) unless options[:to] || @gem_update.newer?(latest)
|
|
147
173
|
|
|
148
174
|
row.to = latest.version
|
|
149
175
|
return done(row, :update) if @dry_run
|
|
150
176
|
|
|
151
|
-
@stdout.puts("installing samagotchi #{latest.version}…")
|
|
177
|
+
(options[:json] ? @stderr : @stdout).puts("installing samagotchi #{latest.version}…")
|
|
152
178
|
output, ok = @gem_update.install(latest)
|
|
153
179
|
return failed(row, output.lines.map(&:strip).reject(&:empty?).last.to_s) unless ok
|
|
154
180
|
|
|
@@ -161,7 +187,8 @@ module Samagotchi
|
|
|
161
187
|
wrapper = InstalledGem.wrapper(@gem_spec)
|
|
162
188
|
return done(row, :update, "run chi update again to sync with it") unless wrapper
|
|
163
189
|
|
|
164
|
-
passed =
|
|
190
|
+
passed = options[:to] ? ["--to", options[:to]] : []
|
|
191
|
+
passed += HANDED_OVER.except("--to").select { |_, key| options[key] }.keys
|
|
165
192
|
@stdout.flush
|
|
166
193
|
@exec.call([RbConfig.ruby, wrapper, "update", "--no-gem", "--gem-from", VERSION, *passed])
|
|
167
194
|
done(row, :update) # only when exec is a spec's stand-in
|
|
@@ -180,7 +207,7 @@ module Samagotchi
|
|
|
180
207
|
row = Row.new(component: "system bundle", from: result.from, to: result.to)
|
|
181
208
|
case result.status
|
|
182
209
|
when :installed then done(row, :install)
|
|
183
|
-
when :updated then done(row, :update, kept(result.kept, MemoryBundle::SystemBundle::BUNDLE_NAME))
|
|
210
|
+
when :updated then done(row, :update, kept(row, result.kept, MemoryBundle::SystemBundle::BUNDLE_NAME))
|
|
184
211
|
when :restored then done(row, :restore).tap { |r| r.to = nil }
|
|
185
212
|
when :up_to_date then up_to_date(row)
|
|
186
213
|
when :newer_installed then skipped(row, "newer than this chi's #{result.to}: left").tap { |r| r.to = nil }
|
|
@@ -203,7 +230,7 @@ module Samagotchi
|
|
|
203
230
|
row = Row.new(component: update.name, from: update.from, to: update.to)
|
|
204
231
|
case update.status
|
|
205
232
|
when :would_update, :updated
|
|
206
|
-
notes = [adds(update.adds), kept(update.kept, update.name), replaced(update.replaced), *update.kept_own].compact
|
|
233
|
+
notes = [adds(update.adds), kept(row, update.kept, update.name), replaced(update.replaced), *update.kept_own].compact
|
|
207
234
|
done(row, :update, notes.empty? ? nil : notes.join("; "))
|
|
208
235
|
when :up_to_date then up_to_date(row)
|
|
209
236
|
when :skipped then skipped(row, update.note)
|
|
@@ -325,11 +352,12 @@ module Samagotchi
|
|
|
325
352
|
members.nil? || members.empty? ? nil : "+ #{members.join(", ")}"
|
|
326
353
|
end
|
|
327
354
|
|
|
328
|
-
def kept(files, bundle)
|
|
329
|
-
|
|
355
|
+
def kept(row, files, bundle)
|
|
356
|
+
row.kept = files.dup
|
|
357
|
+
return nil if row.kept.empty?
|
|
330
358
|
|
|
331
359
|
verb = @dry_run ? "would keep" : "kept"
|
|
332
|
-
"#{verb} your edits in #{
|
|
360
|
+
"#{verb} your edits in #{row.kept.join(", ")}: chi bundle diff #{bundle} #{row.kept.first}"
|
|
333
361
|
end
|
|
334
362
|
|
|
335
363
|
def replaced(files)
|