samagotchi 0.2.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 +7 -0
- data/CHANGELOG.md +43 -0
- data/LICENSE +21 -0
- data/README.md +126 -0
- data/bin/chi +1140 -0
- data/docs/architecture.md +299 -0
- data/docs/cli.md +490 -0
- data/docs/configuration.md +494 -0
- data/docs/desktop.md +97 -0
- data/docs/guardrails.md +218 -0
- data/docs/hooks.md +309 -0
- data/docs/internals/background-tasks.md +26 -0
- data/docs/internals/context-telemetry.md +36 -0
- data/docs/internals/gemma4-contract.md +23 -0
- data/docs/internals/tool-guardrails.md +45 -0
- data/docs/memory.md +85 -0
- data/docs/plugins.md +819 -0
- data/docs/releasing.md +135 -0
- data/docs/sessions.md +155 -0
- data/lib/samagotchi/bridge/bounded_queue.rb +70 -0
- data/lib/samagotchi/bridge/card_store.rb +126 -0
- data/lib/samagotchi/bridge/event_id.rb +25 -0
- data/lib/samagotchi/bridge/ring_buffer.rb +63 -0
- data/lib/samagotchi/bridge/sse_writer.rb +248 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +189 -0
- data/lib/samagotchi/bridge.rb +993 -0
- data/lib/samagotchi/bridge_client/event_stream.rb +158 -0
- data/lib/samagotchi/bridge_client/sse_parser.rb +51 -0
- data/lib/samagotchi/bridge_client.rb +330 -0
- data/lib/samagotchi/bundle_needs.rb +97 -0
- data/lib/samagotchi/bundles/btw/manifest.yml +10 -0
- data/lib/samagotchi/bundles/btw/plugin.rb +100 -0
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +82 -0
- data/lib/samagotchi/bundles/guardrails/guardrails.md +14 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +8 -0
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +210 -0
- data/lib/samagotchi/bundles/known-names/known_names.md +3 -0
- data/lib/samagotchi/bundles/known-names/manifest.yml +14 -0
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +10 -0
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +158 -0
- data/lib/samagotchi/bundles/mcp/manifest.yml +11 -0
- data/lib/samagotchi/bundles/mcp/plugin.rb +631 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +149 -0
- data/lib/samagotchi/bundles/system/delegated.md +10 -0
- data/lib/samagotchi/bundles/system/identity.md +7 -0
- data/lib/samagotchi/bundles/system/manifest.yml +11 -0
- data/lib/samagotchi/bundles/system/memory_guide.md +107 -0
- data/lib/samagotchi/bundles/system/self_map.md +55 -0
- data/lib/samagotchi/cancellation_controller.rb +78 -0
- data/lib/samagotchi/client.rb +429 -0
- data/lib/samagotchi/commands/registry.rb +112 -0
- data/lib/samagotchi/config.rb +910 -0
- data/lib/samagotchi/context_note.rb +77 -0
- data/lib/samagotchi/context_quote.rb +21 -0
- data/lib/samagotchi/context_usage.rb +66 -0
- data/lib/samagotchi/context_window.rb +76 -0
- data/lib/samagotchi/debug_log.rb +110 -0
- data/lib/samagotchi/desktop/macos/App.swift +102 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +201 -0
- data/lib/samagotchi/desktop/macos/Hotkey.swift +42 -0
- data/lib/samagotchi/desktop/macos/Info.plist.erb +42 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +383 -0
- data/lib/samagotchi/desktop/macos.rb +255 -0
- data/lib/samagotchi/desktop.rb +21 -0
- data/lib/samagotchi/desktop_command.rb +143 -0
- data/lib/samagotchi/engine.rb +2807 -0
- data/lib/samagotchi/guardrails/approval.rb +125 -0
- data/lib/samagotchi/guardrails/approvals.rb +177 -0
- data/lib/samagotchi/guardrails/context.rb +71 -0
- data/lib/samagotchi/guardrails/gate.rb +125 -0
- data/lib/samagotchi/guardrails/load_failures.rb +46 -0
- data/lib/samagotchi/guardrails/protected_paths.rb +77 -0
- data/lib/samagotchi/guardrails/rules.rb +199 -0
- data/lib/samagotchi/guardrails/targets.rb +119 -0
- data/lib/samagotchi/guardrails/verdict.rb +134 -0
- data/lib/samagotchi/guardrails.rb +18 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +158 -0
- data/lib/samagotchi/hooks/loader.rb +162 -0
- data/lib/samagotchi/hooks/registry.rb +261 -0
- data/lib/samagotchi/hooks.rb +30 -0
- data/lib/samagotchi/host_registry.rb +315 -0
- data/lib/samagotchi/idle_client.rb +147 -0
- data/lib/samagotchi/idle_recap.rb +549 -0
- data/lib/samagotchi/idle_reminders.rb +101 -0
- data/lib/samagotchi/idle_scheduler.rb +76 -0
- data/lib/samagotchi/image_store.rb +393 -0
- data/lib/samagotchi/installed_gem.rb +38 -0
- data/lib/samagotchi/kernel_loop.rb +1017 -0
- data/lib/samagotchi/launch_mode.rb +34 -0
- data/lib/samagotchi/llm/backend.rb +28 -0
- data/lib/samagotchi/llm/chat_loop.rb +450 -0
- data/lib/samagotchi/llm/errors.rb +329 -0
- data/lib/samagotchi/llm/http.rb +412 -0
- data/lib/samagotchi/llm/model_result.rb +72 -0
- data/lib/samagotchi/llm/native_backend.rb +50 -0
- data/lib/samagotchi/llm/native_tool_normalizer.rb +277 -0
- data/lib/samagotchi/llm/openai_chat.rb +403 -0
- data/lib/samagotchi/llm/usage.rb +79 -0
- data/lib/samagotchi/log.rb +200 -0
- data/lib/samagotchi/log_line.rb +127 -0
- data/lib/samagotchi/log_path.rb +31 -0
- data/lib/samagotchi/log_subscriber.rb +163 -0
- data/lib/samagotchi/memory_bundle/builder.rb +364 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +123 -0
- data/lib/samagotchi/memory_bundle/installer.rb +528 -0
- data/lib/samagotchi/memory_bundle/listing.rb +72 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +225 -0
- data/lib/samagotchi/memory_bundle/merger.rb +52 -0
- data/lib/samagotchi/memory_bundle/placeholder.rb +37 -0
- data/lib/samagotchi/memory_bundle/provenance.rb +257 -0
- data/lib/samagotchi/memory_bundle/source.rb +153 -0
- data/lib/samagotchi/memory_bundle/status.rb +107 -0
- data/lib/samagotchi/memory_bundle/system_bundle.rb +161 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +128 -0
- data/lib/samagotchi/memory_bundle.rb +17 -0
- data/lib/samagotchi/memory_paths.rb +101 -0
- data/lib/samagotchi/model_overlay.rb +53 -0
- data/lib/samagotchi/model_profile.rb +309 -0
- data/lib/samagotchi/muted_memories.rb +66 -0
- data/lib/samagotchi/note_command.rb +163 -0
- data/lib/samagotchi/output_formatter.rb +100 -0
- data/lib/samagotchi/owner_lock.rb +110 -0
- data/lib/samagotchi/pending_input_queue.rb +48 -0
- data/lib/samagotchi/plugin/api.rb +362 -0
- data/lib/samagotchi/plugin/context.rb +193 -0
- data/lib/samagotchi/plugin/loader.rb +126 -0
- data/lib/samagotchi/plugin/service.rb +117 -0
- data/lib/samagotchi/plugin/sessions.rb +150 -0
- data/lib/samagotchi/plugin/side_question.rb +60 -0
- data/lib/samagotchi/plugin/tool_result.rb +24 -0
- data/lib/samagotchi/project_scope.rb +25 -0
- data/lib/samagotchi/prompt.rb +119 -0
- data/lib/samagotchi/prompt_literal_guard.rb +70 -0
- data/lib/samagotchi/recap_store.rb +92 -0
- data/lib/samagotchi/reminder_store.rb +165 -0
- data/lib/samagotchi/self_report.rb +195 -0
- data/lib/samagotchi/send_command.rb +170 -0
- data/lib/samagotchi/served_model.rb +32 -0
- data/lib/samagotchi/session.rb +508 -0
- data/lib/samagotchi/session_commands.rb +527 -0
- data/lib/samagotchi/session_delete_command.rb +105 -0
- data/lib/samagotchi/session_manager.rb +1049 -0
- data/lib/samagotchi/session_metrics.rb +466 -0
- data/lib/samagotchi/session_observer.rb +117 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +118 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1037 -0
- data/lib/samagotchi/terminal_ui/attached_view.rb +264 -0
- data/lib/samagotchi/terminal_ui/event_renderer.rb +192 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +291 -0
- data/lib/samagotchi/terminal_ui/image_input.rb +36 -0
- data/lib/samagotchi/terminal_ui/input_support.rb +324 -0
- data/lib/samagotchi/terminal_ui/legacy_surface.rb +111 -0
- data/lib/samagotchi/terminal_ui/line_reader.rb +113 -0
- data/lib/samagotchi/terminal_ui/live_region.rb +36 -0
- data/lib/samagotchi/terminal_ui/plain_surface.rb +51 -0
- data/lib/samagotchi/terminal_ui/question_prompt.rb +153 -0
- data/lib/samagotchi/terminal_ui/question_slot.rb +131 -0
- data/lib/samagotchi/terminal_ui/reline_seam.rb +216 -0
- data/lib/samagotchi/terminal_ui/repl_input.rb +138 -0
- data/lib/samagotchi/terminal_ui/screen.rb +316 -0
- data/lib/samagotchi/terminal_ui/surface.rb +47 -0
- data/lib/samagotchi/terminal_ui/thinking_line.rb +101 -0
- data/lib/samagotchi/terminal_ui.rb +1992 -0
- data/lib/samagotchi/thinking_ticker.rb +110 -0
- data/lib/samagotchi/thought_stream_splitter.rb +149 -0
- data/lib/samagotchi/token_usage.rb +88 -0
- data/lib/samagotchi/tool_activity.rb +216 -0
- data/lib/samagotchi/tool_call_parser.rb +637 -0
- data/lib/samagotchi/tool_declarations.rb +561 -0
- data/lib/samagotchi/tool_runner.rb +211 -0
- data/lib/samagotchi/tools/args.rb +259 -0
- data/lib/samagotchi/tools/ask_user_question.rb +152 -0
- data/lib/samagotchi/tools/builtins.rb +122 -0
- data/lib/samagotchi/tools/cancel_reminder.rb +21 -0
- data/lib/samagotchi/tools/delegate.rb +167 -0
- data/lib/samagotchi/tools/delegate_result.rb +53 -0
- data/lib/samagotchi/tools/delegate_wait.rb +153 -0
- data/lib/samagotchi/tools/edit.rb +155 -0
- data/lib/samagotchi/tools/execute.rb +214 -0
- data/lib/samagotchi/tools/list_reminders.rb +20 -0
- data/lib/samagotchi/tools/list_sessions.rb +74 -0
- data/lib/samagotchi/tools/memory.rb +256 -0
- data/lib/samagotchi/tools/output_guardrails.rb +93 -0
- data/lib/samagotchi/tools/peers.rb +18 -0
- data/lib/samagotchi/tools/read.rb +182 -0
- data/lib/samagotchi/tools/register_reminder.rb +53 -0
- data/lib/samagotchi/tools/registry.rb +60 -0
- data/lib/samagotchi/tools/send_note.rb +49 -0
- data/lib/samagotchi/tools/task_create.rb +29 -0
- data/lib/samagotchi/tools/task_get.rb +39 -0
- data/lib/samagotchi/tools/task_list.rb +43 -0
- data/lib/samagotchi/tools/task_runtime.rb +311 -0
- data/lib/samagotchi/tools/task_stop.rb +29 -0
- data/lib/samagotchi/tools/task_wait.rb +104 -0
- data/lib/samagotchi/tools/tool_path.rb +18 -0
- data/lib/samagotchi/tools/web_fetch.rb +163 -0
- data/lib/samagotchi/tools/write.rb +26 -0
- data/lib/samagotchi/turn_flow.rb +242 -0
- data/lib/samagotchi/turn_note.rb +76 -0
- data/lib/samagotchi/turn_tally.rb +101 -0
- data/lib/samagotchi/version.rb +7 -0
- data/lib/samagotchi/vision_context.rb +132 -0
- data/lib/samagotchi/vision_support.rb +109 -0
- data/lib/samagotchi/web/app.rb +1349 -0
- data/lib/samagotchi/web/markdown_renderer.rb +107 -0
- data/lib/samagotchi/web/message_parts.rb +169 -0
- data/lib/samagotchi/web/public/activity.js +100 -0
- data/lib/samagotchi/web/public/annotations.js +67 -0
- data/lib/samagotchi/web/public/app.js +2382 -0
- data/lib/samagotchi/web/public/card.js +74 -0
- data/lib/samagotchi/web/public/chat_view.js +360 -0
- data/lib/samagotchi/web/public/chunk_router.js +25 -0
- data/lib/samagotchi/web/public/command_complete.js +39 -0
- data/lib/samagotchi/web/public/composer_size.js +19 -0
- data/lib/samagotchi/web/public/copy.js +142 -0
- data/lib/samagotchi/web/public/ctx.js +35 -0
- data/lib/samagotchi/web/public/data.js +256 -0
- data/lib/samagotchi/web/public/format.js +232 -0
- data/lib/samagotchi/web/public/hold.js +78 -0
- data/lib/samagotchi/web/public/images.js +77 -0
- data/lib/samagotchi/web/public/index.html +568 -0
- data/lib/samagotchi/web/public/init_row.js +60 -0
- data/lib/samagotchi/web/public/model_pick.js +23 -0
- data/lib/samagotchi/web/public/question_card.js +100 -0
- data/lib/samagotchi/web/public/route.js +17 -0
- data/lib/samagotchi/web/public/scope.js +36 -0
- data/lib/samagotchi/web/public/scroll.js +24 -0
- data/lib/samagotchi/web/public/sentences.js +88 -0
- data/lib/samagotchi/web/public/sessions_list.js +60 -0
- data/lib/samagotchi/web/public/strip.js +25 -0
- data/lib/samagotchi/web/public/tally.js +37 -0
- data/lib/samagotchi/web/public/thinking_ticker.js +79 -0
- data/lib/samagotchi/web/public/timing.js +185 -0
- data/lib/samagotchi/web/public/turn_events.js +209 -0
- data/lib/samagotchi/web/public/turn_model.js +204 -0
- data/lib/samagotchi/web/public/turn_view.js +587 -0
- data/lib/samagotchi/web/server.rb +183 -0
- data/lib/samagotchi/web/session_hub.rb +329 -0
- data/lib/samagotchi/web/session_summary.rb +85 -0
- data/lib/samagotchi/worker.rb +635 -0
- data/lib/samagotchi/worker_idle_exit.rb +87 -0
- data/lib/samagotchi.rb +12 -0
- metadata +374 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "rbconfig"
|
|
8
|
+
require_relative "../version"
|
|
9
|
+
require_relative "../self_report"
|
|
10
|
+
require_relative "../installed_gem"
|
|
11
|
+
|
|
12
|
+
module Samagotchi
|
|
13
|
+
module Desktop
|
|
14
|
+
# The macOS helper, "Chi Helper.app": a Service ("Send to chi") and a
|
|
15
|
+
# hotkey that open a panel sending text to live sessions via `chi note`.
|
|
16
|
+
#
|
|
17
|
+
# Built on this machine with plain swiftc and an ad-hoc signature (no
|
|
18
|
+
# Xcode project, no notarization) into ~/Applications. Every outside
|
|
19
|
+
# command goes through the runner, and the dirs are injectable, so specs
|
|
20
|
+
# (and smokes with register: false) never touch the real system.
|
|
21
|
+
class MacOS
|
|
22
|
+
Error = Class.new(StandardError)
|
|
23
|
+
|
|
24
|
+
APP_NAME = "Chi Helper"
|
|
25
|
+
BUNDLE_ID = "dev.samagotchi.chi-helper"
|
|
26
|
+
EXECUTABLE = "ChiHelper"
|
|
27
|
+
SOURCES_DIR = File.expand_path("macos", __dir__)
|
|
28
|
+
PBS = "/System/Library/CoreServices/pbs"
|
|
29
|
+
LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
|
|
30
|
+
# Built next to the target without an .app suffix, so Launch Services
|
|
31
|
+
# never registers a half-built app.
|
|
32
|
+
BUILD_NAME = ".chi-helper-build"
|
|
33
|
+
OLD_NAME = ".chi-helper-old"
|
|
34
|
+
# Apps started by launchd get a bare env (no shell rc files), so the
|
|
35
|
+
# launch file carries what chi needs to find its gems and state. Only
|
|
36
|
+
# these, and only when set: no tokens, no runtime knobs.
|
|
37
|
+
ENV_ALLOWLIST = %w[XDG_CONFIG_HOME XDG_STATE_HOME GEM_HOME GEM_PATH RUBYLIB].freeze
|
|
38
|
+
# Without it Ruby reads stdin as US-ASCII and non-ASCII notes break.
|
|
39
|
+
LANG = "en_US.UTF-8"
|
|
40
|
+
|
|
41
|
+
# Runs a command without a shell; [output, success].
|
|
42
|
+
class Runner
|
|
43
|
+
def run(argv)
|
|
44
|
+
output, status = Open3.capture2e(*argv)
|
|
45
|
+
[output, status.success?]
|
|
46
|
+
rescue SystemCallError => e
|
|
47
|
+
[e.message, false]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
attr_reader :app_dir, :support_dir
|
|
52
|
+
|
|
53
|
+
def initialize(app_dir: nil, support_dir: nil, env: ENV, runner: Runner.new,
|
|
54
|
+
source_dir: SelfReport::SOURCE_DIR, ruby: RbConfig.ruby, version: VERSION,
|
|
55
|
+
arch: nil, sources_dir: SOURCES_DIR, register: true, chi_path: nil)
|
|
56
|
+
home = env["HOME"] || Dir.home
|
|
57
|
+
@app_dir = app_dir || File.join(home, "Applications")
|
|
58
|
+
@support_dir = support_dir || File.join(home, "Library", "Application Support", APP_NAME)
|
|
59
|
+
@env = env
|
|
60
|
+
@runner = runner
|
|
61
|
+
@source_dir = source_dir
|
|
62
|
+
@chi_path = chi_path || InstalledGem.wrapper(InstalledGem.spec(source_dir)) || File.join(source_dir, "bin", "chi")
|
|
63
|
+
@ruby = ruby
|
|
64
|
+
@version = version
|
|
65
|
+
@arch = arch
|
|
66
|
+
@sources_dir = sources_dir
|
|
67
|
+
@register = register
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def app_path = File.join(@app_dir, "#{APP_NAME}.app")
|
|
71
|
+
def launch_path = File.join(@support_dir, "launch.json")
|
|
72
|
+
# Written by the app at launch: its hotkey and whether macOS took it.
|
|
73
|
+
def hotkey_path = File.join(@support_dir, "hotkey.json")
|
|
74
|
+
def executable_path = File.join(app_path, "Contents", "MacOS", EXECUTABLE)
|
|
75
|
+
def installed? = File.directory?(app_path)
|
|
76
|
+
|
|
77
|
+
# What the helper runs: absolute ruby + chi (no shims, which need the
|
|
78
|
+
# shell's PATH), and the allowlisted env. chi is an installed gem's
|
|
79
|
+
# RubyGems wrapper (it activates the gem and outlives upgrades and
|
|
80
|
+
# `gem cleanup`), else this checkout's bin/chi.
|
|
81
|
+
def launch_config
|
|
82
|
+
env = { "LANG" => LANG }
|
|
83
|
+
ENV_ALLOWLIST.each { |name| env[name] = @env[name] unless @env[name].to_s.empty? }
|
|
84
|
+
{ "version" => @version, "argv" => [@ruby, @chi_path], "env" => env }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def warnings
|
|
88
|
+
return [] unless File.file?(File.join(@source_dir, ".git"))
|
|
89
|
+
|
|
90
|
+
["chi runs from a linked git worktree (#{@source_dir}); the helper stops working once it is removed. " \
|
|
91
|
+
"Install from the main checkout or a gem install."]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def info_plist
|
|
95
|
+
ERB.new(File.read(File.join(SOURCES_DIR, "Info.plist.erb")))
|
|
96
|
+
.result_with_hash(bundle_id: BUNDLE_ID, app_name: APP_NAME, executable: EXECUTABLE, version: @version)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @yield [String] progress lines
|
|
100
|
+
# @param login [Boolean] also start the app at login
|
|
101
|
+
def install(force: false, login: false, &progress)
|
|
102
|
+
raise Error, "already installed (#{app_version}); use chi desktop upgrade" if installed? && !force
|
|
103
|
+
|
|
104
|
+
toolchain!
|
|
105
|
+
quit if installed?
|
|
106
|
+
progress&.call("building #{APP_NAME} #{@version} (can take a few minutes)…")
|
|
107
|
+
built = build_app
|
|
108
|
+
swap_in(built)
|
|
109
|
+
write_launch_file
|
|
110
|
+
register_app
|
|
111
|
+
self.login = true if login
|
|
112
|
+
progress&.call("installed #{app_path}")
|
|
113
|
+
app_path
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Keeps the login item as it was.
|
|
117
|
+
def upgrade(&progress)
|
|
118
|
+
was_login = installed? && login == "enabled"
|
|
119
|
+
install(force: true, login: was_login, &progress)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# SMAppService status as the app reports it ("enabled",
|
|
123
|
+
# "notRegistered", "requiresApproval", "notFound"); nil if unknown.
|
|
124
|
+
# Only the app itself can register it, run directly with --login.
|
|
125
|
+
def login
|
|
126
|
+
return nil unless @register && installed?
|
|
127
|
+
|
|
128
|
+
output, ok = @runner.run([executable_path, "--login", "status"])
|
|
129
|
+
ok ? output.strip : nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def login=(on)
|
|
133
|
+
return unless @register && installed?
|
|
134
|
+
|
|
135
|
+
output, ok = @runner.run([executable_path, "--login", on ? "on" : "off"])
|
|
136
|
+
raise Error, "could not turn #{on ? "on" : "off"} the login item: #{output.strip}" if on && !ok
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [Boolean] false when there was nothing to remove
|
|
140
|
+
def uninstall
|
|
141
|
+
return false unless installed? || File.exist?(@support_dir)
|
|
142
|
+
|
|
143
|
+
self.login = false
|
|
144
|
+
quit
|
|
145
|
+
FileUtils.rm_rf(app_path)
|
|
146
|
+
FileUtils.rm_rf(@support_dir)
|
|
147
|
+
if @register
|
|
148
|
+
@runner.run(["defaults", "delete", BUNDLE_ID])
|
|
149
|
+
@runner.run([PBS, "-update"])
|
|
150
|
+
end
|
|
151
|
+
true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def status
|
|
155
|
+
result = { installed: installed?, app_path: app_path, chi_version: @version }
|
|
156
|
+
return result unless result[:installed]
|
|
157
|
+
|
|
158
|
+
launch = (JSON.parse(File.read(launch_path)) rescue nil)
|
|
159
|
+
argv = launch.is_a?(Hash) ? Array(launch["argv"]) : []
|
|
160
|
+
env = launch.is_a?(Hash) && launch["env"].is_a?(Hash) ? launch["env"] : {}
|
|
161
|
+
dump, = @runner.run([PBS, "-dump"])
|
|
162
|
+
_, running = @runner.run(["pgrep", "-x", EXECUTABLE])
|
|
163
|
+
hotkey = (JSON.parse(File.read(hotkey_path)) rescue nil)
|
|
164
|
+
result.merge(
|
|
165
|
+
app_version: app_version,
|
|
166
|
+
login: login,
|
|
167
|
+
hotkey: hotkey.is_a?(Hash) ? hotkey : nil,
|
|
168
|
+
launch_argv: argv,
|
|
169
|
+
launch_ok: !argv.empty? && argv.all? { |path| File.exist?(path) },
|
|
170
|
+
baked_dirs: env.select { |name, _| name.start_with?("XDG_") },
|
|
171
|
+
service: dump.include?(BUNDLE_ID),
|
|
172
|
+
running: running
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def app_version
|
|
177
|
+
plist = File.read(File.join(app_path, "Contents", "Info.plist"))
|
|
178
|
+
plist[%r{<key>CFBundleShortVersionString</key>\s*<string>([^<]*)</string>}, 1]
|
|
179
|
+
rescue SystemCallError
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# rename(2) can't replace a non-empty dir: old aside, new in, old gone;
|
|
184
|
+
# the old copy comes back if the second rename fails.
|
|
185
|
+
def swap_in(build)
|
|
186
|
+
old = File.join(@app_dir, OLD_NAME)
|
|
187
|
+
FileUtils.rm_rf(old)
|
|
188
|
+
had_old = installed?
|
|
189
|
+
File.rename(app_path, old) if had_old
|
|
190
|
+
begin
|
|
191
|
+
File.rename(build, app_path)
|
|
192
|
+
rescue StandardError
|
|
193
|
+
File.rename(old, app_path) if had_old
|
|
194
|
+
raise
|
|
195
|
+
end
|
|
196
|
+
FileUtils.rm_rf(old)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def toolchain!
|
|
202
|
+
_, ok = @runner.run(%w[xcrun --find swiftc])
|
|
203
|
+
return if ok
|
|
204
|
+
|
|
205
|
+
raise Error, "chi desktop needs the Command Line Tools (swiftc): xcode-select --install"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def build_app
|
|
209
|
+
build = File.join(@app_dir, BUILD_NAME)
|
|
210
|
+
FileUtils.rm_rf(build)
|
|
211
|
+
macos_dir = File.join(build, "Contents", "MacOS")
|
|
212
|
+
FileUtils.mkdir_p(macos_dir)
|
|
213
|
+
File.write(File.join(build, "Contents", "Info.plist"), info_plist)
|
|
214
|
+
sources = Dir[File.join(@sources_dir, "*.swift")].sort
|
|
215
|
+
# Through xcrun: swiftc run by its bare path finds no SDK.
|
|
216
|
+
compile = ["xcrun", "--sdk", "macosx", "swiftc", "-O", "-swift-version", "5", "-target", "#{arch}-apple-macos13", "-parse-as-library",
|
|
217
|
+
*sources, "-o", File.join(macos_dir, EXECUTABLE)]
|
|
218
|
+
run!(compile, "swiftc failed")
|
|
219
|
+
run!(["codesign", "--force", "--sign", "-", build], "codesign failed")
|
|
220
|
+
build
|
|
221
|
+
rescue StandardError
|
|
222
|
+
FileUtils.rm_rf(build) if build
|
|
223
|
+
raise
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def run!(argv, what)
|
|
227
|
+
output, ok = @runner.run(argv)
|
|
228
|
+
raise Error, "#{what}:\n#{output}" unless ok
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def arch
|
|
232
|
+
@arch ||= RbConfig::CONFIG["host_cpu"] == "arm64" ? "arm64" : `uname -m`.strip
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def write_launch_file
|
|
236
|
+
FileUtils.mkdir_p(@support_dir)
|
|
237
|
+
tmp = "#{launch_path}.tmp"
|
|
238
|
+
File.write(tmp, JSON.pretty_generate(launch_config) + "\n")
|
|
239
|
+
File.rename(tmp, launch_path)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def register_app
|
|
243
|
+
return unless @register
|
|
244
|
+
|
|
245
|
+
@runner.run([PBS, "-update"])
|
|
246
|
+
@runner.run([LSREGISTER, "-f", app_path])
|
|
247
|
+
@runner.run(["open", "-g", app_path])
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def quit
|
|
251
|
+
@runner.run(["pkill", "-x", EXECUTABLE]) if @register
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# `chi desktop`: a small native helper that sends selected text to live
|
|
7
|
+
# sessions as a context note. It talks to chi only through the CLI
|
|
8
|
+
# (`chi sessions list --live --scope=all --format json`, `chi note`), so it could ship
|
|
9
|
+
# on its own later. macOS only for now; a Linux variant would be another
|
|
10
|
+
# class next to MacOS.
|
|
11
|
+
module Desktop
|
|
12
|
+
autoload :MacOS, "samagotchi/desktop/macos"
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# @return [Boolean] whether this machine has a desktop helper variant
|
|
17
|
+
def supported?(host_os = RbConfig::CONFIG["host_os"])
|
|
18
|
+
host_os.to_s.include?("darwin")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "desktop"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# `chi desktop install|upgrade|uninstall|status`: the native helper that
|
|
7
|
+
# sends selected text to live sessions as a context note (macOS for now).
|
|
8
|
+
class DesktopCommand
|
|
9
|
+
USAGE = <<~TEXT
|
|
10
|
+
Usage: chi desktop <install|upgrade|uninstall|status>
|
|
11
|
+
install [--force] [--login]
|
|
12
|
+
build "Chi Helper" into ~/Applications (needs the Command Line Tools)
|
|
13
|
+
and start it: Services > "Send to chi", or ⌃⌥⌘N with the clipboard,
|
|
14
|
+
sends text to a live session as a context note. --login: start it
|
|
15
|
+
at login too (else it runs until you log out)
|
|
16
|
+
upgrade rebuild it for this chi and restart it
|
|
17
|
+
uninstall remove it, its settings and its launch file
|
|
18
|
+
status installed version, how it runs chi, Service and process state
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
SUBCOMMANDS = %w[install upgrade uninstall status].freeze
|
|
22
|
+
|
|
23
|
+
# @param platform [#call] register: → a Desktop::MacOS-like object
|
|
24
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr, platform: nil, supported: Desktop.supported?)
|
|
25
|
+
@argv = argv.dup
|
|
26
|
+
@stdout = stdout
|
|
27
|
+
@stderr = stderr
|
|
28
|
+
@platform = platform || ->(register:) { Desktop::MacOS.new(register: register) }
|
|
29
|
+
@supported = supported
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Integer] 0 ok, 1 failed, 2 usage
|
|
33
|
+
def run
|
|
34
|
+
sub = @argv.shift
|
|
35
|
+
if sub.nil? || %w[-h --help help].include?(sub)
|
|
36
|
+
@stdout.puts(USAGE)
|
|
37
|
+
return 0
|
|
38
|
+
end
|
|
39
|
+
return usage_error("unknown subcommand #{sub}") unless SUBCOMMANDS.include?(sub)
|
|
40
|
+
|
|
41
|
+
unless @supported
|
|
42
|
+
@stderr.puts("chi desktop supports macOS only for now")
|
|
43
|
+
return 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
options = parse(sub) or return 2
|
|
47
|
+
# --no-register (hidden): no pbs/lsregister/open/pkill, for installs
|
|
48
|
+
# into temp dirs (HOME=…) that must not touch the real Services.
|
|
49
|
+
helper = @platform.call(register: !options[:no_register])
|
|
50
|
+
send(sub.to_sym, helper, options)
|
|
51
|
+
rescue Desktop::MacOS::Error => e
|
|
52
|
+
@stderr.puts("chi desktop: #{e.message}")
|
|
53
|
+
1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# @return [Hash, nil] nil after a usage error
|
|
59
|
+
def parse(sub)
|
|
60
|
+
flags = { "--no-register" => :no_register }
|
|
61
|
+
flags.merge!("--force" => :force, "--login" => :login) if sub == "install"
|
|
62
|
+
@argv.each_with_object({}) do |arg, options|
|
|
63
|
+
unless flags.key?(arg)
|
|
64
|
+
usage_error("unknown option #{arg}")
|
|
65
|
+
return nil
|
|
66
|
+
end
|
|
67
|
+
options[flags[arg]] = true
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The warnings come after a done install: a refused or failed one
|
|
72
|
+
# prints only its error.
|
|
73
|
+
def install(helper, options)
|
|
74
|
+
helper.install(force: options[:force] || false, login: options[:login] || false) { |line| @stdout.puts(line) }
|
|
75
|
+
print_warnings(helper)
|
|
76
|
+
0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def upgrade(helper, _options)
|
|
80
|
+
helper.upgrade { |line| @stdout.puts(line) }
|
|
81
|
+
print_warnings(helper)
|
|
82
|
+
0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def uninstall(helper, _options)
|
|
86
|
+
@stdout.puts(helper.uninstall ? "removed Chi Helper" : "Chi Helper is not installed")
|
|
87
|
+
0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def status(helper, _options)
|
|
91
|
+
s = helper.status
|
|
92
|
+
unless s[:installed]
|
|
93
|
+
@stdout.puts(format_rows([["Chi Helper", "not installed (chi desktop install)"], ["app", s[:app_path]]]))
|
|
94
|
+
return 0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
version = if s[:app_version] == s[:chi_version]
|
|
98
|
+
"#{s[:app_version]} (matches chi)"
|
|
99
|
+
else
|
|
100
|
+
"#{s[:app_version] || "?"} (chi is #{s[:chi_version]}: chi desktop upgrade)"
|
|
101
|
+
end
|
|
102
|
+
launch = s[:launch_argv].empty? ? "(none)" : s[:launch_argv].join(" ")
|
|
103
|
+
launch += s[:launch_ok] ? " (ok)" : " (missing: chi desktop upgrade)"
|
|
104
|
+
dirs = s[:baked_dirs].empty? ? "(defaults)" : s[:baked_dirs].map { |name, value| "#{name}=#{value}" }.join(" ")
|
|
105
|
+
service = s[:service] ? "registered" : "not registered (try chi desktop upgrade, or log out and back in)"
|
|
106
|
+
running = s[:running] ? "running" : "not running (open -g \"#{s[:app_path]}\")"
|
|
107
|
+
@stdout.puts(format_rows([["Chi Helper", version], ["app", s[:app_path]], ["runs chi", launch],
|
|
108
|
+
["state dirs", dirs], ["service", service], ["hotkey", hotkey(s[:hotkey])],
|
|
109
|
+
["process", running], ["at login", login(s[:login])]]))
|
|
110
|
+
0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def hotkey(state)
|
|
114
|
+
return "unknown (the app writes it when it starts)" unless state
|
|
115
|
+
|
|
116
|
+
state["registered"] ? "#{state["keys"]} (clipboard)" : "#{state["keys"]} taken by another app"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def login(state)
|
|
120
|
+
case state
|
|
121
|
+
when "enabled" then "on"
|
|
122
|
+
when "requiresApproval" then "waiting for approval in System Settings > General > Login Items"
|
|
123
|
+
when nil then "unknown"
|
|
124
|
+
else "off (chi desktop install --force --login)"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def format_rows(rows)
|
|
129
|
+
width = rows.map { |label, _| label.length }.max
|
|
130
|
+
rows.map { |label, value| "#{label.ljust(width)} #{value}" }.join("\n")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def print_warnings(helper)
|
|
134
|
+
helper.warnings.each { |warning| @stderr.puts("warning: #{warning}") }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def usage_error(message)
|
|
138
|
+
@stderr.puts("chi desktop: #{message}")
|
|
139
|
+
@stderr.puts(USAGE)
|
|
140
|
+
2
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|