samagotchi 0.149.0 → 0.151.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 +69 -1
- data/README.md +4 -2
- data/docs/architecture.md +4 -4
- data/docs/bundle-authoring.md +1 -1
- data/docs/cli.md +59 -24
- data/docs/configuration.md +6 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +96 -0
- data/docs/internals/bridge.md +53 -9
- data/docs/internals/bundles.md +43 -14
- data/docs/internals/objectives.md +7 -2
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +48 -10
- data/docs/plugins.md +49 -3
- data/docs/sessions.md +5 -4
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/block_renderers.rb +115 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +224 -82
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/try/trace.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 +19 -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 +23 -3
- data/lib/samagotchi/memory_bundle/builder.rb +5 -3
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +70 -6
- data/lib/samagotchi/memory_bundle/installed_copy.rb +1 -0
- data/lib/samagotchi/memory_bundle/installer.rb +162 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/manifest.rb +23 -2
- 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 +182 -22
- 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/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/terminal_ui.rb +4 -4
- 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_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -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/app.rb +22 -12
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
- data/lib/samagotchi/web/markdown_renderer.rb +39 -6
- data/lib/samagotchi/web/message_display.rb +3 -3
- data/lib/samagotchi/web/public/app.js +7 -3
- data/lib/samagotchi/web/public/block_frame.js +202 -0
- data/lib/samagotchi/web/public/block_host.js +502 -0
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/block_renderers.css +20 -0
- data/lib/samagotchi/web/public/block_source.js +37 -0
- data/lib/samagotchi/web/public/bubbles.js +11 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +10 -5
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- 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/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- 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/session_show.rb +1 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +17 -1
|
@@ -9,6 +9,7 @@ require_relative "desktop"
|
|
|
9
9
|
require_relative "gem_update"
|
|
10
10
|
require_relative "installed_gem"
|
|
11
11
|
require_relative "live_versions"
|
|
12
|
+
require_relative "memory_bundle/file_standing"
|
|
12
13
|
require_relative "memory_bundle/shipped_update"
|
|
13
14
|
require_relative "memory_bundle/system_bundle"
|
|
14
15
|
require_relative "session"
|
|
@@ -20,8 +21,8 @@ require "json"
|
|
|
20
21
|
module Samagotchi
|
|
21
22
|
# `chi update`: brings an installed chi up to date in one go: the system
|
|
22
23
|
# bundle, the shipped bundles the user installed, and the macOS helper.
|
|
23
|
-
# Unedited things update; edited ones
|
|
24
|
-
# reported (hooks, plugin, rules, which didn't load once edited). Live
|
|
24
|
+
# Unedited things update; edited ones merge or stay behind (.md) or are replaced
|
|
25
|
+
# and reported (hooks, plugin, rules, which didn't load once edited). Live
|
|
25
26
|
# workers and a running chi web are reported, never restarted. The bundles
|
|
26
27
|
# chi didn't ship are only checked, under the chi that will run them
|
|
27
28
|
# (BundleCommand::InstalledCheck): what it finds is rows that fail nothing.
|
|
@@ -34,10 +35,15 @@ module Samagotchi
|
|
|
34
35
|
Updates this chi's install: the gem (from rubygems.org), then the
|
|
35
36
|
system bundle, the bundles chi ships that you installed (chi bundle
|
|
36
37
|
list), and the desktop helper (macOS; rebuilt only when its sources
|
|
37
|
-
changed).
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
a
|
|
38
|
+
changed). A memory file you edited takes the update in a text merge
|
|
39
|
+
(the old file goes to the bundle trash), or stays behind as it is when
|
|
40
|
+
no merge is possible, and the table says which and why; each run
|
|
41
|
+
retries a file left behind, though one an older chi marked, with a base
|
|
42
|
+
no installed chi shipped, merges only if the bundle takes out or rewords
|
|
43
|
+
no line it still holds. Nothing is
|
|
44
|
+
installed that isn't already, and old gem versions stay. The bundles chi doesn't ship are
|
|
45
|
+
never changed: the new chi checks them and adds a row per warning or
|
|
46
|
+
error (these don't fail the update).
|
|
41
47
|
--dry-run show what would change; change nothing (with a newer
|
|
42
48
|
gem to install, bundles chi doesn't ship aren't checked:
|
|
43
49
|
that chi would)
|
|
@@ -76,8 +82,11 @@ module Samagotchi
|
|
|
76
82
|
|
|
77
83
|
# A table row. info rows (live workers, chi web) say what runs; they
|
|
78
84
|
# never fail. level ("warn", "error"; else nil) marks a finding on a bundle
|
|
79
|
-
# chi didn't ship: it reports, changes nothing and fails nothing.
|
|
80
|
-
|
|
85
|
+
# chi didn't ship: it reports, changes nothing and fails nothing. kept: the
|
|
86
|
+
# memory files that stay behind; merged: the ones that took the update;
|
|
87
|
+
# trash_copy: where their old copies went, in full (the table's short: --json
|
|
88
|
+
# has the path); earlier: the files of the earlier merges the row names.
|
|
89
|
+
Row = Struct.new(:component, :from, :to, :status, :failed, :kept, :merged, :earlier, :trash_copy, :level, keyword_init: true)
|
|
81
90
|
|
|
82
91
|
# What a row says after a change, and in a dry run.
|
|
83
92
|
VERBS = { update: ["updated", "would update"], install: ["installed", "would install"],
|
|
@@ -95,6 +104,7 @@ module Samagotchi
|
|
|
95
104
|
gem_update: nil, exec: nil, bundler: nil, home: ChiHome, env: ENV)
|
|
96
105
|
@home = home
|
|
97
106
|
@in_home = env.delete(IN_HOME_ENV) == "1"
|
|
107
|
+
@unattended = env[ChildEnv::UNATTENDED_UPDATE] == "1"
|
|
98
108
|
# Built after the home is known: its install env goes into the home.
|
|
99
109
|
@gem_update_given = gem_update
|
|
100
110
|
@exec = exec || ->(argv) { Kernel.exec(*argv) }
|
|
@@ -147,7 +157,7 @@ module Samagotchi
|
|
|
147
157
|
gem = gem_row(options)
|
|
148
158
|
rows = [gem, system_bundle_row, *bundle_rows(options), *external_rows(options), desktop_row(options), *live_rows].compact
|
|
149
159
|
if options[:json]
|
|
150
|
-
@stdout.puts(JSON.generate(rows.map { |row| row.to_h.merge(failed: !!row.failed, kept: row.kept || []) }))
|
|
160
|
+
@stdout.puts(JSON.generate(rows.map { |row| row.to_h.merge(failed: !!row.failed, kept: row.kept || [], merged: row.merged || [], earlier: row.earlier || []) }))
|
|
151
161
|
else
|
|
152
162
|
print_table(rows)
|
|
153
163
|
footers(options, rows).each { |line| @stdout.puts(line) }
|
|
@@ -279,17 +289,24 @@ module Samagotchi
|
|
|
279
289
|
end
|
|
280
290
|
|
|
281
291
|
def system_bundle_row
|
|
282
|
-
|
|
292
|
+
system = MemoryBundle::SystemBundle
|
|
293
|
+
earlier = system.merge_notes
|
|
294
|
+
result = system.sync(dry_run: @dry_run)
|
|
295
|
+
# These rows have no place for the notes, which wait for one that has.
|
|
296
|
+
earlier = [] if %i[installed skipped failed].include?(result.status)
|
|
283
297
|
row = Row.new(component: "system bundle", from: result.from, to: result.to)
|
|
298
|
+
notes = file_notes(row, system::BUNDLE_NAME, result.to, result.merged, result.behind, result.trash_copy, earlier: earlier)
|
|
284
299
|
case result.status
|
|
285
300
|
when :installed then done(row, :install)
|
|
286
|
-
when :updated then done(row, :update,
|
|
287
|
-
when :restored then done(row, :restore).tap { |r| r.to = nil }
|
|
288
|
-
when :up_to_date then
|
|
289
|
-
when :newer_installed then skipped(row, "newer than this chi's #{result.to}: left").tap { |r| r.to = nil }
|
|
301
|
+
when :updated then done(row, :update, notes.empty? ? nil : notes.join("; "))
|
|
302
|
+
when :restored then done(row, :restore, notes.empty? ? nil : notes.join("; ")).tap { |r| r.to = nil }
|
|
303
|
+
when :merged, :up_to_date then same_version(row, notes + result.warnings, changed: result.merged.any?)
|
|
304
|
+
when :newer_installed then skipped(row, ["newer than this chi's #{result.to}: left", *notes].join("; ")).tap { |r| r.to = nil }
|
|
290
305
|
when :skipped then skipped(row, result.error)
|
|
291
306
|
else failed(row, result.error)
|
|
292
307
|
end
|
|
308
|
+
system.clear_merge_notes(earlier, files: result.merged) if read_by_someone?
|
|
309
|
+
row
|
|
293
310
|
end
|
|
294
311
|
|
|
295
312
|
def bundle_rows(options)
|
|
@@ -320,14 +337,50 @@ module Samagotchi
|
|
|
320
337
|
|
|
321
338
|
def bundle_row(update)
|
|
322
339
|
row = Row.new(component: update.name, from: update.from, to: update.to)
|
|
340
|
+
earlier = update.status == :failed ? [] : Array(update.earlier)
|
|
341
|
+
notes = file_notes(row, update.name, update.to || update.from, update.merged, update.behind, update.trash_copy, earlier: earlier)
|
|
323
342
|
case update.status
|
|
324
343
|
when :would_update, :updated
|
|
325
|
-
notes = [adds(update.adds),
|
|
344
|
+
notes = [adds(update.adds), *notes, replaced(update.replaced), *update.kept_own].compact
|
|
326
345
|
done(row, :update, notes.empty? ? nil : notes.join("; "))
|
|
327
|
-
when :up_to_date then
|
|
328
|
-
when :skipped then skipped(row, update.note)
|
|
329
|
-
else failed(row, update.note)
|
|
346
|
+
when :would_merge, :merged, :up_to_date then same_version(row, [*notes, update.note].compact, changed: Array(update.merged).any?)
|
|
347
|
+
when :skipped then skipped(row, [update.note, *notes].join("; "))
|
|
348
|
+
else failed(row, [update.note, *notes].compact.join("; "))
|
|
330
349
|
end
|
|
350
|
+
# A failed row keeps them: the run didn't finish, and the next says what stands.
|
|
351
|
+
MemoryBundle::Provenance.new(name: update.name).clear_merge_notes(earlier, files: row.merged) if read_by_someone? && update.status != :failed
|
|
352
|
+
row
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# What one bundle's memory files did, in FileStanding's words; the row keeps
|
|
356
|
+
# the file names and the trash copy's full line for --json. merged is what
|
|
357
|
+
# this run merged, earlier the files of the notes it shows.
|
|
358
|
+
# @return [Array<String>] the row's clauses
|
|
359
|
+
def file_notes(row, bundle, version, merged, behind, trash_copy, earlier: [])
|
|
360
|
+
merged = Array(merged)
|
|
361
|
+
behind = Array(behind)
|
|
362
|
+
row.merged = merged
|
|
363
|
+
row.earlier = earlier.map(&:file)
|
|
364
|
+
row.kept = behind.map(&:file)
|
|
365
|
+
@unsure_behind ||= behind.any?(&:unsure)
|
|
366
|
+
row.trash_copy = trash_copy
|
|
367
|
+
notes = MemoryBundle::FileStanding.clauses(bundle, version, merged: merged, behind: behind, earlier: earlier, dry_run: @dry_run)
|
|
368
|
+
trash_copy || earlier.any?(&:trash) ? [*notes, MemoryBundle::FileStanding::IN_THE_TRASH] : notes
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Whether a row printed now ends the notes for what it printed, this run's
|
|
372
|
+
# merges too. A dry run changes nothing, and an automatic update's rows may be
|
|
373
|
+
# read by no one (ChildEnv::UNATTENDED_UPDATE).
|
|
374
|
+
def read_by_someone? = !@dry_run && !@unattended
|
|
375
|
+
|
|
376
|
+
# A row at the same version: up to date only when nothing merged and nothing
|
|
377
|
+
# stays behind. No to, and no verb of its own: the clauses say it all.
|
|
378
|
+
def same_version(row, notes, changed:)
|
|
379
|
+
return up_to_date(row, notes.join("; ")) unless changed || row.kept.any?
|
|
380
|
+
|
|
381
|
+
row.to = nil
|
|
382
|
+
row.status = notes.join("; ")
|
|
383
|
+
row
|
|
331
384
|
end
|
|
332
385
|
|
|
333
386
|
def desktop_row(options)
|
|
@@ -420,6 +473,9 @@ module Samagotchi
|
|
|
420
473
|
return "(dry run: nothing was changed)" if @dry_run
|
|
421
474
|
return "some parts failed (see above)" if rows.any?(&:failed)
|
|
422
475
|
|
|
476
|
+
behind = rows.sum { |row| Array(row.kept).size }
|
|
477
|
+
return "#{MemoryBundle::FileStanding.stay_behind(behind, unsure: @unsure_behind)} (see above)" if behind.positive?
|
|
478
|
+
|
|
423
479
|
# A finding on a bundle chi didn't ship repeats until its bundle changes; it isn't a change.
|
|
424
480
|
changes = rows.reject(&:level)
|
|
425
481
|
changes.all? { |r| r.status.start_with?("up to date", "skipped", "not installed", "couldn't check") } ? "everything is up to date" : "done"
|
|
@@ -434,9 +490,9 @@ module Samagotchi
|
|
|
434
490
|
row
|
|
435
491
|
end
|
|
436
492
|
|
|
437
|
-
def up_to_date(row)
|
|
493
|
+
def up_to_date(row, note = nil)
|
|
438
494
|
row.to = nil
|
|
439
|
-
row.status = "up to date"
|
|
495
|
+
row.status = note.to_s.empty? ? "up to date" : "up to date (#{note})"
|
|
440
496
|
row
|
|
441
497
|
end
|
|
442
498
|
|
|
@@ -456,14 +512,6 @@ module Samagotchi
|
|
|
456
512
|
members.nil? || members.empty? ? nil : "+ #{members.join(", ")}"
|
|
457
513
|
end
|
|
458
514
|
|
|
459
|
-
def kept(row, files, bundle)
|
|
460
|
-
row.kept = files.dup
|
|
461
|
-
return nil if row.kept.empty?
|
|
462
|
-
|
|
463
|
-
verb = @dry_run ? "would keep" : "kept"
|
|
464
|
-
"#{verb} your edits in #{row.kept.join(", ")}: chi bundle diff #{bundle} #{row.kept.first}"
|
|
465
|
-
end
|
|
466
|
-
|
|
467
515
|
def replaced(files)
|
|
468
516
|
return nil if files.empty?
|
|
469
517
|
|
|
@@ -43,7 +43,10 @@ module Samagotchi
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def message(version, shipped_dir:, helper:)
|
|
46
|
-
|
|
46
|
+
# A retry that would merge is an update too; one that only records a reason isn't.
|
|
47
|
+
bundles = MemoryBundle::ShippedUpdate.plan(shipped_dir: shipped_dir).count do |r|
|
|
48
|
+
r.status == :would_update || (r.status == :would_merge && r.merged.any?)
|
|
49
|
+
end
|
|
47
50
|
parts = []
|
|
48
51
|
parts << (bundles == 1 ? "1 bundle" : "#{bundles} bundles") if bundles.positive?
|
|
49
52
|
parts << "the desktop helper" if helper&.stale?
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -74,6 +74,7 @@ require_relative "session_create"
|
|
|
74
74
|
require_relative "session_control"
|
|
75
75
|
require_relative "verify_fresh_routes"
|
|
76
76
|
require_relative "push_routes"
|
|
77
|
+
require_relative "block_renderer_routes"
|
|
77
78
|
require_relative "update_check"
|
|
78
79
|
require_relative "update_ops"
|
|
79
80
|
require_relative "proxy_gate"
|
|
@@ -107,6 +108,7 @@ module Samagotchi
|
|
|
107
108
|
include SessionControl
|
|
108
109
|
include VerifyFreshRoutes
|
|
109
110
|
include PushRoutes
|
|
111
|
+
include BlockRendererRoutes
|
|
110
112
|
include ProxyGate
|
|
111
113
|
|
|
112
114
|
DEFAULT_HOST = BridgeClient::HOST
|
|
@@ -222,8 +224,11 @@ module Samagotchi
|
|
|
222
224
|
private
|
|
223
225
|
|
|
224
226
|
# No page of chi web's may be framed by another (clickjacking): every
|
|
225
|
-
# HTML answer says so.
|
|
226
|
-
|
|
227
|
+
# HTML answer says so. frame-src 'none': a page's own frames can't navigate
|
|
228
|
+
# anywhere, which a block renderer's frame's own CSP doesn't stop (a srcdoc
|
|
229
|
+
# frame still loads under it; docs/internals/block-renderers.md). The page's
|
|
230
|
+
# full CSP is a later change.
|
|
231
|
+
FRAME_HEADERS = { "X-Frame-Options" => "DENY", "Content-Security-Policy" => "frame-ancestors 'none'; frame-src 'none'" }.freeze
|
|
227
232
|
FRAME_DESTS = %w[iframe frame embed object].freeze
|
|
228
233
|
|
|
229
234
|
def unframed(response)
|
|
@@ -315,6 +320,8 @@ module Samagotchi
|
|
|
315
320
|
["DELETE", %r{\A/api/push/subscriptions/([^/]+)\z}, :handle_push_remove, :owner],
|
|
316
321
|
["POST", %r{\A/api/push/test\z}, :handle_push_test, :owner],
|
|
317
322
|
["POST", %r{\A/api/push/presence\z}, :handle_push_presence, :owner],
|
|
323
|
+
["GET", %r{\A/api/block_renderers\z}, :handle_block_renderers, :owner],
|
|
324
|
+
["GET", %r{\A/api/block_renderers/([^/]+)/([^/]+)\z}, :handle_block_renderer_file, :owner],
|
|
318
325
|
["GET", %r{\A/api/sessions/([^/]+)\z}, :handle_show, :participant, :session],
|
|
319
326
|
["DELETE", %r{\A/api/sessions/([^/]+)\z}, :handle_delete, :owner, :session]
|
|
320
327
|
].freeze
|
|
@@ -1800,21 +1807,24 @@ module Samagotchi
|
|
|
1800
1807
|
rel = rel.split("/").reject { |p| p == ".." || p.empty? }.join("/")
|
|
1801
1808
|
full = File.join(@public_dir, rel)
|
|
1802
1809
|
if File.file?(full) && full.start_with?(@public_dir)
|
|
1803
|
-
|
|
1804
|
-
# no-cache + a content ETag: the browser revalidates every asset
|
|
1805
|
-
# (the ES-module imports too) and gets a cheap 304 while it is
|
|
1806
|
-
# unchanged, so a gem upgrade + chi web restart takes effect on the
|
|
1807
|
-
# next load instead of after max-age runs out.
|
|
1808
|
-
etag = %("#{Digest::SHA256.hexdigest(body)[0, 32]}")
|
|
1809
|
-
headers = { "Cache-Control" => "no-cache", "ETag" => etag }
|
|
1810
|
-
return [304, headers, []] if etag_match?(req.get_header("HTTP_IF_NONE_MATCH"), etag)
|
|
1811
|
-
|
|
1812
|
-
[200, headers.merge("Content-Type" => mime_type(full), "Content-Length" => body.bytesize.to_s), [body]]
|
|
1810
|
+
revalidated_body(req, File.binread(full), mime_type(full))
|
|
1813
1811
|
else
|
|
1814
1812
|
not_found(path: req.path_info)
|
|
1815
1813
|
end
|
|
1816
1814
|
end
|
|
1817
1815
|
|
|
1816
|
+
# no-cache + a content ETag: the browser revalidates every asset
|
|
1817
|
+
# (the ES-module imports too) and gets a cheap 304 while it is
|
|
1818
|
+
# unchanged, so a gem upgrade + chi web restart takes effect on the
|
|
1819
|
+
# next load instead of after max-age runs out.
|
|
1820
|
+
def revalidated_body(req, body, content_type)
|
|
1821
|
+
etag = %("#{Digest::SHA256.hexdigest(body)[0, 32]}")
|
|
1822
|
+
headers = { "Cache-Control" => "no-cache", "ETag" => etag }
|
|
1823
|
+
return [304, headers, []] if etag_match?(req.get_header("HTTP_IF_NONE_MATCH"), etag)
|
|
1824
|
+
|
|
1825
|
+
[200, headers.merge("Content-Type" => content_type, "Content-Length" => body.bytesize.to_s), [body]]
|
|
1826
|
+
end
|
|
1827
|
+
|
|
1818
1828
|
def etag_match?(if_none_match, etag)
|
|
1819
1829
|
return false if if_none_match.nil?
|
|
1820
1830
|
|
|
@@ -197,7 +197,7 @@ module Samagotchi
|
|
|
197
197
|
end
|
|
198
198
|
|
|
199
199
|
# A newer chi this didn't install, settled: a run of its own from here.
|
|
200
|
-
# The terminal's table already told what `chi update`
|
|
200
|
+
# The terminal's table already told what `chi update` merged or left behind.
|
|
201
201
|
def adopt_installed
|
|
202
202
|
return unless @restart == "idle" && @seen
|
|
203
203
|
|
|
@@ -215,10 +215,10 @@ module Samagotchi
|
|
|
215
215
|
return Log.info(:web, "auto_update_deferred", version: version, why: result) if result.is_a?(Symbol)
|
|
216
216
|
|
|
217
217
|
rows = result.rows
|
|
218
|
-
record = AutoUpdateRecord.new(version: version, from: VERSION, at: @clock.call.iso8601, kept:
|
|
219
|
-
phase: "installed")
|
|
218
|
+
record = AutoUpdateRecord.new(version: version, from: VERSION, at: @clock.call.iso8601, kept: files_in(rows, "kept"),
|
|
219
|
+
merged: files_in(rows, "merged"), phase: "installed")
|
|
220
220
|
if result.done?
|
|
221
|
-
Log.info(:web, "auto_update_installed", version: version, kept: record.kept.size)
|
|
221
|
+
Log.info(:web, "auto_update_installed", version: version, kept: record.kept.size, merged: record.merged.size)
|
|
222
222
|
installed(record)
|
|
223
223
|
else
|
|
224
224
|
failed(record, rows)
|
|
@@ -232,7 +232,7 @@ module Samagotchi
|
|
|
232
232
|
return carry_on(save(record)) unless @restart == "never"
|
|
233
233
|
|
|
234
234
|
save(record.with(phase: "done", pushed: true))
|
|
235
|
-
@hub.publish_notice(kind: "installed", version: record.version, kept: record.kept.size)
|
|
235
|
+
@hub.publish_notice(kind: "installed", version: record.version, kept: record.kept.size, merged: record.merged.size)
|
|
236
236
|
end
|
|
237
237
|
|
|
238
238
|
# The next step of a run on record.
|
|
@@ -328,7 +328,7 @@ module Samagotchi
|
|
|
328
328
|
record = restart_workers(record) || record
|
|
329
329
|
Log.info(:web, "auto_update_done", version: record.version, restarted: record.restarted, left: record.left)
|
|
330
330
|
save(record.with(phase: record.left_to_try?(@clock.call, RETRY_FOR) ? RETRYING : "done", pushed: true))
|
|
331
|
-
@hub.publish_notice(kind: "updated", version: record.version, kept: record.kept.size,
|
|
331
|
+
@hub.publish_notice(kind: "updated", version: record.version, kept: record.kept.size, merged: record.merged.size,
|
|
332
332
|
restarted: record.restarted, left: record.left)
|
|
333
333
|
end
|
|
334
334
|
|
|
@@ -366,8 +366,8 @@ module Samagotchi
|
|
|
366
366
|
installed: gem_installed)
|
|
367
367
|
end
|
|
368
368
|
|
|
369
|
-
def
|
|
370
|
-
rows.flat_map { |row| Array(row[
|
|
369
|
+
def files_in(rows, field)
|
|
370
|
+
rows.flat_map { |row| Array(row[field]).map(&:to_s) }.uniq
|
|
371
371
|
end
|
|
372
372
|
end
|
|
373
373
|
end
|
|
@@ -13,8 +13,10 @@ module Samagotchi
|
|
|
13
13
|
# +phase+: installed, workers, web (AutoUpdate::OPEN_PHASES), retrying,
|
|
14
14
|
# done or failed. +restarted+ and +left+ count the stale workers (`workers` in
|
|
15
15
|
# the file); +at+ is when the install ended, an ISO 8601 string.
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# +kept+ and +merged+ name the edited memory files that stayed behind and
|
|
17
|
+
# that took the update (chi update --json's).
|
|
18
|
+
AutoUpdateRecord = Data.define(:version, :from, :at, :kept, :merged, :phase, :restarted, :left, :detail, :pushed) do
|
|
19
|
+
def initialize(version:, phase:, from: nil, at: nil, kept: [], merged: [], restarted: 0, left: 0, detail: nil, pushed: false)
|
|
18
20
|
super
|
|
19
21
|
end
|
|
20
22
|
|
|
@@ -30,14 +32,14 @@ module Samagotchi
|
|
|
30
32
|
|
|
31
33
|
workers = hash["workers"].is_a?(Hash) ? hash["workers"] : {}
|
|
32
34
|
new(version: version, phase: phase, from: hash["from"], at: hash["at"],
|
|
33
|
-
kept: Array(hash["kept"]).grep(String), restarted: workers["restarted"].to_i, left: workers["left"].to_i,
|
|
35
|
+
kept: Array(hash["kept"]).grep(String), merged: Array(hash["merged"]).grep(String), restarted: workers["restarted"].to_i, left: workers["left"].to_i,
|
|
34
36
|
detail: hash["detail"], pushed: hash["pushed"] == true)
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
# What goes in the file, for .from_h to read back.
|
|
38
40
|
# @return [Hash{String => Object}]
|
|
39
41
|
def to_store
|
|
40
|
-
{ "version" => version, "from" => from, "at" => at, "kept" => kept, "phase" => phase,
|
|
42
|
+
{ "version" => version, "from" => from, "at" => at, "kept" => kept, "merged" => merged, "phase" => phase,
|
|
41
43
|
"workers" => { "restarted" => restarted, "left" => left }, "detail" => detail,
|
|
42
44
|
"pushed" => pushed ? true : nil }.compact
|
|
43
45
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../block_renderers"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
module Web
|
|
7
|
+
# What the owner's page asks to draw a fenced block (App::ROUTES, :owner
|
|
8
|
+
# rows; a participant's page shows the source). Included in App: its helpers
|
|
9
|
+
# answer. docs/internals/block-renderers.md.
|
|
10
|
+
module BlockRendererRoutes
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# {renderers: [{lang, bundle, files}]}
|
|
14
|
+
def handle_block_renderers(_req)
|
|
15
|
+
json_response(200, { renderers: BlockRenderers.for_installed.map(&:to_h) })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# One of a renderer's files. The captures are the URL's and unchecked:
|
|
19
|
+
# BlockRenderers.script_path never makes a path of one that isn't a
|
|
20
|
+
# renderer's. A file changed since the install gets an error, so the page
|
|
21
|
+
# keeps the block as source.
|
|
22
|
+
def handle_block_renderer_file(req, bundle, file)
|
|
23
|
+
path = BlockRenderers.script_path(bundle, file)
|
|
24
|
+
return not_found(path: req.path_info) unless path
|
|
25
|
+
|
|
26
|
+
revalidated_body(req, File.binread(path), "text/javascript; charset=utf-8")
|
|
27
|
+
rescue BlockRenderers::Invalid => e
|
|
28
|
+
error_response(409, "renderer_changed", e.message)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "nokogiri"
|
|
4
4
|
require "uri"
|
|
5
|
+
require_relative "../block_renderers"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
7
8
|
module Web
|
|
@@ -17,6 +18,12 @@ module Samagotchi
|
|
|
17
18
|
ALLOWED_ELEMENTS = %w[a blockquote br code del em h1 h2 h3 h4 h5 h6 hr li ol p pre s strong span table tbody td th thead tr ul].freeze
|
|
18
19
|
DROP_ELEMENTS = %w[iframe object script style svg].freeze
|
|
19
20
|
ALLOWED_PROTOCOLS = %w[http https mailto].freeze
|
|
21
|
+
# Both Commonmarker calls take these: the parse that reads the fences has
|
|
22
|
+
# to see the blocks #render's own parse does.
|
|
23
|
+
OPTIONS = {
|
|
24
|
+
render: { escape: true }, # escape raw HTML from source; highlighter output is still emitted
|
|
25
|
+
extension: { header_ids: nil }
|
|
26
|
+
}.freeze
|
|
20
27
|
|
|
21
28
|
def initialize(enabled: false)
|
|
22
29
|
@enabled = enabled
|
|
@@ -46,10 +53,7 @@ module Samagotchi
|
|
|
46
53
|
def render(markdown)
|
|
47
54
|
return nil unless available?
|
|
48
55
|
|
|
49
|
-
html = Commonmarker.to_html(markdown.to_s, options: {
|
|
50
|
-
render: { escape: true }, # escape raw HTML from source; highlighter output is still emitted
|
|
51
|
-
extension: { header_ids: nil }
|
|
52
|
-
}, plugins: {
|
|
56
|
+
html = Commonmarker.to_html(markdown.to_s, options: OPTIONS, plugins: {
|
|
53
57
|
# An empty theme makes syntect emit scope classes (`keyword`, `string`, ...)
|
|
54
58
|
# instead of inline colours from one fixed theme, so the page's light and
|
|
55
59
|
# dark palettes colour code blocks (the --code-* tokens in index.html).
|
|
@@ -57,7 +61,7 @@ module Samagotchi
|
|
|
57
61
|
})
|
|
58
62
|
return nil unless html
|
|
59
63
|
|
|
60
|
-
sanitize(html)
|
|
64
|
+
sanitize(html, fence_languages(markdown.to_s))
|
|
61
65
|
end
|
|
62
66
|
|
|
63
67
|
private
|
|
@@ -77,7 +81,35 @@ module Samagotchi
|
|
|
77
81
|
@commonmarker_installed = Gem::Specification.find_all_by_name("commonmarker").any?
|
|
78
82
|
end
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
# Every code block's language, in document order (nil for an indented block
|
|
85
|
+
# or a bare fence): the highlighter drops it from the HTML, the parse tree
|
|
86
|
+
# has it. [] for a text with no fence: its blocks have no language.
|
|
87
|
+
def fence_languages(markdown)
|
|
88
|
+
return [] unless markdown.include?("```") || markdown.include?("~~~")
|
|
89
|
+
|
|
90
|
+
languages = []
|
|
91
|
+
Commonmarker.parse(markdown, options: OPTIONS).walk do |node|
|
|
92
|
+
languages << language_of(node.fence_info) if node.type == :code_block
|
|
93
|
+
end
|
|
94
|
+
languages
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def language_of(info)
|
|
98
|
+
word = info.to_s.split.first.to_s.downcase
|
|
99
|
+
word if word.match?(BlockRenderers::LANG)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# After the walk, which drops every attribute it doesn't know. Blocks and
|
|
103
|
+
# languages pair by position; a count that differs says the two parses
|
|
104
|
+
# disagree, and then no block gets one (it shows as source).
|
|
105
|
+
def mark_languages(fragment, languages)
|
|
106
|
+
blocks = fragment.css("pre")
|
|
107
|
+
return unless blocks.size == languages.size
|
|
108
|
+
|
|
109
|
+
blocks.zip(languages) { |pre, language| pre["data-lang"] = language if language }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def sanitize(html, languages)
|
|
81
113
|
fragment = Nokogiri::HTML5.fragment(html)
|
|
82
114
|
fragment.traverse do |node|
|
|
83
115
|
next unless node.element?
|
|
@@ -90,6 +122,7 @@ module Samagotchi
|
|
|
90
122
|
sanitize_attributes(node)
|
|
91
123
|
end
|
|
92
124
|
end
|
|
125
|
+
mark_languages(fragment, languages)
|
|
93
126
|
fragment.to_html
|
|
94
127
|
end
|
|
95
128
|
|
|
@@ -59,7 +59,7 @@ module Samagotchi
|
|
|
59
59
|
return nil unless saved
|
|
60
60
|
|
|
61
61
|
messages = Array(session.messages)
|
|
62
|
-
since = messages.drop(saved[:covered].to_i).count { |m| Samagotchi::Steer.
|
|
62
|
+
since = messages.drop(saved[:covered].to_i).count { |m| Samagotchi::Steer.turn_opening?(m) }
|
|
63
63
|
{ text: saved[:text], turns_since: since, created_at: saved[:created_at] }.compact
|
|
64
64
|
rescue StandardError
|
|
65
65
|
nil
|
|
@@ -288,8 +288,8 @@ module Samagotchi
|
|
|
288
288
|
end
|
|
289
289
|
|
|
290
290
|
# As JSON has them (string keys and values), like a snapshot's.
|
|
291
|
-
def saved_cards(id)
|
|
292
|
-
JSON.parse(JSON.generate(Samagotchi::Bridge::CardStore.saved(Session.session_dir(id, state_dir: default_state_dir))))
|
|
291
|
+
def saved_cards(id, messages: nil)
|
|
292
|
+
JSON.parse(JSON.generate(Samagotchi::Bridge::CardStore.saved(Session.session_dir(id, state_dir: default_state_dir), messages: messages)))
|
|
293
293
|
rescue StandardError
|
|
294
294
|
[]
|
|
295
295
|
end
|
|
@@ -71,6 +71,7 @@ import { createAnnotateBar } from "./annotate_bar.js";
|
|
|
71
71
|
import { VERIFY_PRESET, VERIFY_UNAVAILABLE, verifyWorkerUnavailable, withVerify } from "./verify_preset.js";
|
|
72
72
|
import { OWNER_FINAL, verifyTurnOf } from "./verify_target.js";
|
|
73
73
|
import { actionLine, annotationActions, composerWithAction } from "./annotate_actions.js";
|
|
74
|
+
import { createBlockHost } from "./block_host.js";
|
|
74
75
|
import { copyText, installCopy } from "./copy.js";
|
|
75
76
|
import { installAnswerFold, sweepAnswerFolds } from "./answer_fold.js";
|
|
76
77
|
import { installDetailsMotion } from "./details_motion.js";
|
|
@@ -1188,8 +1189,9 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1188
1189
|
restoreCommandBubbles(historyEl, kept);
|
|
1189
1190
|
// A failed turn's prompt went back to the user: shown after the
|
|
1190
1191
|
// conversation, marked failed, as it was live (prompt_restored).
|
|
1191
|
-
|
|
1192
|
-
|
|
1192
|
+
const failedPrompt = data.failed_turn ? makeUserBubble(data.failed_turn.prompt, { state: "failed" }) : null;
|
|
1193
|
+
if (failedPrompt) {
|
|
1194
|
+
failedPrompt.dataset.failedTurn = "1";
|
|
1193
1195
|
addEndBubble(failedTurnText(data.failed_turn));
|
|
1194
1196
|
}
|
|
1195
1197
|
seedSeenFromDom();
|
|
@@ -1203,7 +1205,8 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1203
1205
|
// A live worker's last cards and between-turns notices, where they
|
|
1204
1206
|
// arrived; the running turn's go into its step once it is drawn.
|
|
1205
1207
|
const snapshotCards = splitSnapshotCards(data.cards, pageCapabilities);
|
|
1206
|
-
|
|
1208
|
+
// Above a failed turn's prompt: it came after them, and it is no turn.
|
|
1209
|
+
placeSnapshotCards(snapshotCards.placed, { before: failedPrompt });
|
|
1207
1210
|
// The recap saved with the session (also a stopped one's), else a live
|
|
1208
1211
|
// worker's from before saved_recap existed. After the cards: the last
|
|
1209
1212
|
// turn's check-in notice goes to the end, and the recap closes the page.
|
|
@@ -2117,6 +2120,7 @@ historyEl.addEventListener("click", (e) => {
|
|
|
2117
2120
|
});
|
|
2118
2121
|
turnDom.scrollEl.addEventListener("scroll", annotateBar.schedule, true);
|
|
2119
2122
|
turnRoots().forEach((root) => installCopy(root));
|
|
2123
|
+
createBlockHost({ roots: turnRoots(), sessionId: () => store.selected });
|
|
2120
2124
|
// A steps block, a thinking block and the recap open and close by hand
|
|
2121
2125
|
// with their height animated (details_motion.js), in the history and on
|
|
2122
2126
|
// the stage.
|