kward 0.79.0 → 0.80.1

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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -0
  3. data/CHANGELOG.md +65 -1
  4. data/Gemfile.lock +2 -2
  5. data/README.md +31 -38
  6. data/doc/agent-tools.md +9 -10
  7. data/doc/api.md +3 -3
  8. data/doc/authentication.md +79 -110
  9. data/doc/code-search.md +1 -1
  10. data/doc/composer.md +1 -1
  11. data/doc/configuration.md +69 -26
  12. data/doc/context-budgeting.md +6 -6
  13. data/doc/context-tools.md +2 -2
  14. data/doc/editor.md +7 -7
  15. data/doc/extensibility.md +7 -29
  16. data/doc/files.md +6 -6
  17. data/doc/getting-started.md +2 -2
  18. data/doc/git.md +8 -2
  19. data/doc/lifecycle-hooks.md +5 -4
  20. data/doc/local-models.md +2 -2
  21. data/doc/mcp.md +5 -5
  22. data/doc/permissions.md +13 -4
  23. data/doc/plugins.md +39 -4
  24. data/doc/prompt-templates.md +105 -0
  25. data/doc/providers.md +140 -0
  26. data/doc/releasing.md +10 -2
  27. data/doc/rpc.md +54 -31
  28. data/doc/sandboxing.md +22 -0
  29. data/doc/security.md +22 -7
  30. data/doc/session-management.md +2 -4
  31. data/doc/skills.md +2 -0
  32. data/doc/tabs.md +37 -0
  33. data/doc/telegram.md +138 -0
  34. data/doc/transports.md +209 -0
  35. data/doc/usage.md +11 -3
  36. data/doc/web-search.md +3 -3
  37. data/doc/workspace-tools.md +2 -2
  38. data/examples/plugins/stardate_footer.rb +10 -0
  39. data/examples/plugins/telegram/plugin.rb +39 -0
  40. data/examples/plugins/telegram/telegram_api.rb +136 -0
  41. data/examples/plugins/telegram/telegram_transport.rb +304 -0
  42. data/lib/kward/agent.rb +11 -6
  43. data/lib/kward/ansi.rb +1 -0
  44. data/lib/kward/auth/api_key_store.rb +99 -0
  45. data/lib/kward/cli/auth_commands.rb +72 -13
  46. data/lib/kward/cli/commands.rb +7 -0
  47. data/lib/kward/cli/git.rb +31 -8
  48. data/lib/kward/cli/memory_commands.rb +2 -2
  49. data/lib/kward/cli/plugins.rb +1 -0
  50. data/lib/kward/cli/prompt_interface.rb +44 -1
  51. data/lib/kward/cli/rendering.rb +6 -2
  52. data/lib/kward/cli/runtime_helpers.rb +5 -2
  53. data/lib/kward/cli/sessions.rb +1 -1
  54. data/lib/kward/cli/settings.rb +97 -33
  55. data/lib/kward/cli/slash_commands.rb +1 -1
  56. data/lib/kward/cli/tabs.rb +103 -37
  57. data/lib/kward/cli/transports.rb +67 -0
  58. data/lib/kward/cli/worktrees.rb +360 -0
  59. data/lib/kward/cli.rb +22 -0
  60. data/lib/kward/compactor.rb +7 -2
  61. data/lib/kward/config_files.rb +51 -12
  62. data/lib/kward/conversation.rb +13 -7
  63. data/lib/kward/git_worktree_manager.rb +291 -0
  64. data/lib/kward/hooks/audit_log.rb +5 -2
  65. data/lib/kward/model/azure_openai_config.rb +54 -0
  66. data/lib/kward/model/catalog.rb +113 -0
  67. data/lib/kward/model/client.rb +225 -20
  68. data/lib/kward/model/model_info.rb +49 -0
  69. data/lib/kward/model/payloads.rb +63 -4
  70. data/lib/kward/model/provider_catalog.rb +114 -0
  71. data/lib/kward/model/sources.rb +70 -0
  72. data/lib/kward/model/stream_parser.rb +83 -13
  73. data/lib/kward/permissions/policy.rb +18 -4
  74. data/lib/kward/plugin_chat_runtime.rb +374 -0
  75. data/lib/kward/plugin_registry.rb +99 -19
  76. data/lib/kward/private_file.rb +9 -3
  77. data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
  78. data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
  79. data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
  80. data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
  81. data/lib/kward/prompt_interface/editor/controller.rb +7 -7
  82. data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
  83. data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
  84. data/lib/kward/prompt_interface/editor/state.rb +3 -9
  85. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +219 -19
  86. data/lib/kward/prompt_interface/file_overlay.rb +2 -2
  87. data/lib/kward/prompt_interface/git_prompt.rb +7 -3
  88. data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
  89. data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
  90. data/lib/kward/prompt_interface/interactive/state.rb +2 -2
  91. data/lib/kward/prompt_interface/project_browser.rb +25 -6
  92. data/lib/kward/prompt_interface/runtime_state.rb +22 -6
  93. data/lib/kward/prompt_interface/screen.rb +3 -0
  94. data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
  95. data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
  96. data/lib/kward/prompt_interface.rb +53 -12
  97. data/lib/kward/prompts/commands.rb +1 -1
  98. data/lib/kward/prompts/templates.rb +11 -6
  99. data/lib/kward/prompts.rb +6 -5
  100. data/lib/kward/rpc/auth_manager.rb +112 -152
  101. data/lib/kward/rpc/config_manager.rb +46 -6
  102. data/lib/kward/rpc/plugin_chat_manager.rb +55 -196
  103. data/lib/kward/rpc/prompt_bridge.rb +8 -4
  104. data/lib/kward/rpc/redactor.rb +1 -1
  105. data/lib/kward/rpc/server.rb +60 -10
  106. data/lib/kward/rpc/session_manager.rb +109 -30
  107. data/lib/kward/session_store.rb +84 -24
  108. data/lib/kward/skills/registry.rb +16 -11
  109. data/lib/kward/tab_driver.rb +6 -3
  110. data/lib/kward/telemetry/logger.rb +5 -2
  111. data/lib/kward/tools/git_commit.rb +39 -0
  112. data/lib/kward/tools/registry.rb +7 -2
  113. data/lib/kward/tools/tool_call.rb +1 -0
  114. data/lib/kward/transport/gateway.rb +253 -0
  115. data/lib/kward/transport/host.rb +343 -0
  116. data/lib/kward/transport/manager.rb +179 -0
  117. data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
  118. data/lib/kward/transport/runtime.rb +41 -0
  119. data/lib/kward/transport/store.rb +101 -0
  120. data/lib/kward/transport.rb +204 -0
  121. data/lib/kward/version.rb +1 -1
  122. data/lib/kward/workspace_factory.rb +19 -2
  123. data/templates/default/fulldoc/html/js/kward.js +2 -0
  124. data/templates/default/kward_navigation.rb +7 -2
  125. data/templates/default/layout/html/setup.rb +4 -0
  126. metadata +26 -1
@@ -8,6 +8,12 @@ module Kward
8
8
  class CLI
9
9
  # TUI session tab coordination and asynchronous turn execution.
10
10
  module Tabs
11
+ WORKTREE_AGENT_CONTEXT = <<~PROMPT.strip.freeze
12
+ This is an active Git worktree tab. The workspace is sandboxed separately from the host repository metadata.
13
+ When the user explicitly asks you to commit, use the advertised `git_commit` tool. Do not use `run_shell_command` for `git add` or `git commit`, because shared Git metadata is intentionally protected there.
14
+ `git_commit` requires a commit message and accepts optional workspace-relative paths; omit paths to include all current changes in this worktree. Do not create a commit unless the user requested one.
15
+ PROMPT
16
+
11
17
  TabRuntime = Struct.new(
12
18
  :session,
13
19
  :agent,
@@ -91,6 +97,7 @@ module Kward
91
97
  @active_tab_index = 0
92
98
  @tab_store = session_store ? TabStore.new(config_dir: session_store.config_dir, cwd: session_store.cwd) : nil
93
99
  @tab_live_view = nil
100
+ @tab_live_view_token = nil
94
101
  @restored_tabs = false
95
102
  restored = restore_tabs(session_store) if session_store && agent.nil?
96
103
  return restored if restored
@@ -120,11 +127,14 @@ module Kward
120
127
  descriptors = Array(data["tabs"])
121
128
  return nil if descriptors.empty?
122
129
 
123
- descriptors.each_with_index do |_descriptor, index|
124
- tab = build_tab_from_descriptor(data.fetch("tabs")[index], session_store, label: restored_tab_label(data, index))
130
+ descriptors.each_with_index do |descriptor, index|
131
+ tab = build_tab_from_descriptor(descriptor, session_store, label: restored_tab_label(data, index))
125
132
  @tabs << tab if tab
126
- rescue StandardError
127
- next
133
+ rescue StandardError => e
134
+ descriptor = descriptor.transform_keys(&:to_s) if descriptor.respond_to?(:transform_keys)
135
+ if descriptor.is_a?(Hash) && descriptor.key?("worktree")
136
+ @tabs << unavailable_worktree_tab(descriptor, e, label: restored_tab_label(data, index))
137
+ end
128
138
  end
129
139
  return nil if @tabs.empty?
130
140
 
@@ -136,13 +146,21 @@ module Kward
136
146
  def build_tab_from_descriptor(descriptor, session_store, label: nil)
137
147
  descriptor = descriptor.transform_keys(&:to_s)
138
148
  if descriptor["kind"] == "session"
139
- session, conversation = restore_tab_session(session_store, descriptor.fetch("session_path"))
140
- agent = build_tab_agent(conversation, session)
141
- return build_tab(session, agent, driver: SessionTabDriver.new(session: session, agent: agent), label: label)
149
+ worktree = if descriptor.key?("worktree")
150
+ binding = GitWorktreeBinding.from_descriptor(descriptor["worktree"])
151
+ raise GitWorktreeManager::Error, "Invalid worktree binding." unless binding
152
+
153
+ binding
154
+ end
155
+ validate_worktree_binding!(worktree)
156
+ root = worktree_root_for(worktree, fallback: session_store.cwd)
157
+ session, conversation = restore_tab_session(session_store, descriptor.fetch("session_path"), workspace_root: root, strict: worktree&.active? == true)
158
+ agent = build_tab_agent(conversation, session, worktree: worktree)
159
+ return build_tab(session, agent, driver: SessionTabDriver.new(session: session, agent: agent, worktree: worktree), label: label)
142
160
  end
143
161
 
144
162
  tab_type = plugin_registry.tab_type_for_id(descriptor["plugin_tab_type"])
145
- driver = if tab_type
163
+ driver = if tab_type&.local
146
164
  host = PluginTabHost.new(client: @client, workspace_root: session_store.cwd)
147
165
  tab_type.handler.call(host, descriptor)
148
166
  else
@@ -154,7 +172,7 @@ module Kward
154
172
 
155
173
  def open_plugin_tab(name, session_store)
156
174
  tab_type = plugin_registry.tab_type_for(name)
157
- return runtime_output("Plugin tab #{name.inspect} is not available.") unless tab_type
175
+ return runtime_output("Plugin tab #{name.inspect} is not available.") unless tab_type&.local
158
176
 
159
177
  if tab_type.singleton == :global && (existing = @tabs.find { |tab| tab.driver.descriptor["plugin_tab_type"] == tab_type.id })
160
178
  return switch_tab(@tabs.index(existing))
@@ -174,30 +192,37 @@ module Kward
174
192
  runtime_output("Could not open plugin tab #{name.inspect}: #{e.message}")
175
193
  end
176
194
 
177
- def restore_tab_session(session_store, path)
195
+ def restore_tab_session(session_store, path, workspace_root: session_store.cwd, strict: false)
178
196
  if File.file?(path)
179
- session, conversation = session_store.load(path, workspace: configured_workspace(root: session_store.cwd), provider: current_model_provider, model: current_model_id, reasoning_effort: current_reasoning_effort)
197
+ session, conversation = session_store.load(path, workspace: configured_workspace(root: workspace_root, strict: strict), provider: current_model_provider, model: current_model_id, reasoning_effort: current_reasoning_effort)
180
198
  return [track_session(session), conversation]
181
199
  end
182
200
 
183
201
  session = track_session(session_store.create(provider: current_model_provider, model: current_model_id, reasoning_effort: current_reasoning_effort))
184
- conversation = new_conversation(workspace_root: session_store.cwd)
202
+ conversation = new_conversation(workspace_root: workspace_root)
185
203
  session.attach(conversation)
186
204
  [session, conversation]
187
205
  end
188
206
 
189
- def build_tab_agent(conversation, _session)
207
+ def build_tab_agent(conversation, _session, worktree: nil)
190
208
  conversation.plugin_registry ||= plugin_registry if conversation.respond_to?(:plugin_registry)
191
- workspace = configured_workspace(root: conversation.workspace_root)
209
+ strict = worktree&.active? == true
210
+ update_worktree_agent_context(conversation, strict)
211
+ workspace = configured_workspace(root: conversation.workspace_root, strict: strict)
192
212
  prompt = TabQuestionPrompt.new(self)
193
- hook_manager = lifecycle_hook_manager(conversation)
194
- hook_context = lifecycle_hook_context(conversation)
213
+ hook_manager = strict ? nil : lifecycle_hook_manager(conversation)
214
+ hook_context = strict ? nil : lifecycle_hook_context(conversation)
215
+ git_committer = if strict
216
+ ->(message:, paths:) { git_commit_for_agent(workspace.root.to_s, message: message, paths: paths) }
217
+ end
195
218
  tool_registry = ToolRegistry.new(
196
219
  workspace: workspace,
197
220
  prompt: prompt,
198
221
  tool_approval: tab_tool_approval_callback(prompt),
199
222
  hook_manager: hook_manager,
200
- hook_context: hook_context
223
+ hook_context: hook_context,
224
+ mcp_clients: strict ? [] : nil,
225
+ git_committer: git_committer
201
226
  )
202
227
  @footer_conversation = conversation
203
228
  @footer_tool_registry = tool_registry
@@ -205,6 +230,7 @@ module Kward
205
230
  client: @client,
206
231
  tool_registry: tool_registry,
207
232
  conversation: conversation,
233
+ warning_sink: ConfigFiles.warning_sink,
208
234
  hook_manager: hook_manager,
209
235
  hook_context: hook_context
210
236
  )
@@ -212,6 +238,14 @@ module Kward
212
238
  agent
213
239
  end
214
240
 
241
+ def update_worktree_agent_context(conversation, strict)
242
+ desired = strict ? WORKTREE_AGENT_CONTEXT : nil
243
+ return if conversation.execution_profile_context == desired
244
+ return unless strict || conversation.execution_profile_context == WORKTREE_AGENT_CONTEXT
245
+
246
+ conversation.update_execution_profile_context!(desired)
247
+ end
248
+
215
249
  def tab_tool_approval_callback(prompt)
216
250
  return nil unless ConfigFiles.permission_policy(safely_read_config.to_h).enabled?
217
251
 
@@ -372,8 +406,9 @@ module Kward
372
406
  return agent unless tab
373
407
 
374
408
  tab.session = @active_session
375
- tab.agent = build_tab_agent(agent.conversation, tab.session)
376
- tab.driver = SessionTabDriver.new(session: tab.session, agent: tab.agent)
409
+ worktree = reconcile_worktree_binding(tab, agent.conversation)
410
+ tab.agent = build_tab_agent(agent.conversation, tab.session, worktree: worktree)
411
+ tab.driver = SessionTabDriver.new(session: tab.session, agent: tab.agent, worktree: worktree)
377
412
  assign_tab_question_prompt(tab.agent, tab)
378
413
  tab.diff = @session_diff || (tab.session&.path ? SessionDiff.from_session_file(tab.session.path) : SessionDiff.new)
379
414
  tab.snapshot = nil
@@ -389,6 +424,7 @@ module Kward
389
424
  tab.shell = nil
390
425
  tab.stream_state = new_tab_stream_state(tab.driver)
391
426
  tab.markdown_chunks.clear
427
+ update_prompt_workspace_root(current_workspace_root)
392
428
  update_prompt_tabs
393
429
  persist_tabs
394
430
  tab.agent
@@ -399,13 +435,23 @@ module Kward
399
435
  return unless tab
400
436
 
401
437
  if @prompt.respond_to?(:tab_view_snapshot)
402
- tab.snapshot = @prompt.tab_view_snapshot
438
+ tab.snapshot = tab_view_snapshot_for(tab)
403
439
  elsif @prompt.respond_to?(:composer_snapshot)
404
440
  tab.snapshot = @prompt.composer_snapshot
405
441
  end
406
442
  tab.diff = @session_diff
407
443
  end
408
444
 
445
+ def tab_view_snapshot_for(tab)
446
+ snapshot_method = @prompt.method(:tab_view_snapshot)
447
+ supports_transcript_option = snapshot_method.parameters.any? do |type, name|
448
+ type == :keyrest || ([:key, :keyreq].include?(type) && name == :include_transcript)
449
+ end
450
+ return snapshot_method.call unless supports_transcript_option
451
+
452
+ snapshot_method.call(include_transcript: tab.running? || !tab.shell.nil?)
453
+ end
454
+
409
455
  def activate_tab(index, render: true)
410
456
  tab = @tabs[index]
411
457
  return nil unless tab
@@ -424,6 +470,8 @@ module Kward
424
470
  end
425
471
  tab.unread = false
426
472
  restore_tab_composer_snapshot(tab.snapshot)
473
+ update_prompt_workspace_root(current_workspace_root)
474
+ @prompt.refresh_composer_status if @prompt.respond_to?(:refresh_composer_status)
427
475
  update_plugin_tab_slash_commands(tab)
428
476
  update_prompt_tabs
429
477
  render_tab(tab) if render
@@ -640,12 +688,12 @@ module Kward
640
688
 
641
689
  def refresh_active_tab
642
690
  tab = active_tab
643
- return unless tab
691
+ return unless tab&.thread && !tab.thread.alive? && tab.running?
644
692
 
645
- if tab.thread && !tab.thread.alive? && tab.running?
646
- tab.thread.join
647
- tab.status = "ready" if tab.status == "running"
648
- end
693
+ tab.thread.join
694
+ return unless tab.status == "running"
695
+
696
+ tab.status = "ready"
649
697
  update_prompt_tabs
650
698
  end
651
699
 
@@ -653,23 +701,31 @@ module Kward
653
701
  return unless prompt_interface?
654
702
 
655
703
  stop_tab_live_view
656
- @tab_live_view_stop = false
657
- @tab_live_view = Thread.new { run_tab_live_view(tab) }
704
+ token = Object.new
705
+ @tab_live_view_token = token
706
+ @tab_live_view = Thread.new { run_tab_live_view(tab, token) }
658
707
  @tab_live_view.report_on_exception = false
659
708
  end
660
709
 
661
710
  def stop_tab_live_view
662
- @tab_live_view_stop = true
663
- @tab_live_view&.join(0.2)
664
- @tab_live_view = nil
711
+ thread = @tab_live_view
712
+ @tab_live_view_token = nil
713
+ thread&.join(0.2)
714
+ @tab_live_view = nil if @tab_live_view.equal?(thread)
665
715
  end
666
716
 
667
- def run_tab_live_view(tab)
717
+ def run_tab_live_view(tab, token)
668
718
  renderer = tab_live_renderer(tab)
669
- until @tab_live_view_stop
719
+ while @tab_live_view_token.equal?(token)
670
720
  events = tab.event_history[tab.seen_events..] || []
671
- events.each { |event| renderer.call(event, tab.driver) }
672
- tab.seen_events += events.length
721
+ events.each do |event|
722
+ break unless @tab_live_view_token.equal?(token)
723
+
724
+ renderer.call(event, tab.driver)
725
+ tab.seen_events += 1
726
+ end
727
+ break unless @tab_live_view_token.equal?(token)
728
+
673
729
  if tab.idle?
674
730
  renderer.call(:flush, tab.driver)
675
731
  break
@@ -677,7 +733,8 @@ module Kward
677
733
  sleep 0.05
678
734
  end
679
735
  ensure
680
- @tab_live_view_stop = false if @tab_live_view == Thread.current
736
+ @tab_live_view_token = nil if @tab_live_view_token.equal?(token)
737
+ @tab_live_view = nil if @tab_live_view == Thread.current
681
738
  end
682
739
 
683
740
  def tab_live_renderer(tab)
@@ -726,6 +783,8 @@ module Kward
726
783
  def tab_labels
727
784
  @tabs.each_with_index.map do |tab, index|
728
785
  label = tab.label.to_s.empty? ? default_tab_label(index) : tab.label.to_s
786
+ binding = worktree_binding_for(tab)
787
+ label = "#{label} · #{binding.branch}" if binding&.active?
729
788
  { name: label, color: tab_label_color(tab) }
730
789
  end
731
790
  end
@@ -752,7 +811,7 @@ module Kward
752
811
  action, value = argument.to_s.strip.split(/\s+/, 2)
753
812
  case action
754
813
  when nil, ""
755
- runtime_output("Usage: /tab 1-n | /tab move 1-n|left|right | /tab close | /tab new | /tab open <plugin-tab> | /tab name <label>")
814
+ runtime_output(tab_usage)
756
815
  nil
757
816
  when /^\d+$/
758
817
  switch_tab_number(action)
@@ -774,12 +833,19 @@ module Kward
774
833
  when "name", "rename"
775
834
  rename_active_tab(value)
776
835
  active_tab&.agent
836
+ when "worktree"
837
+ handle_worktree_command(value)
838
+ active_tab&.agent
777
839
  else
778
- runtime_output("Usage: /tab 1-n | /tab move 1-n|left|right | /tab close | /tab new | /tab open <plugin-tab> | /tab name <label>")
840
+ runtime_output(tab_usage)
779
841
  nil
780
842
  end
781
843
  end
782
844
 
845
+ def tab_usage
846
+ "/tab 1-n | /tab move 1-n|left|right | /tab close | /tab new | /tab open <plugin-tab> | /tab name <label> | /tab worktree [activate|detach|status|merge|merge abort|remove]"
847
+ end
848
+
783
849
  def switch_tab_number(number)
784
850
  index = number.to_i - 1
785
851
  return switch_tab(index) if @tabs && index.between?(0, @tabs.length - 1)
@@ -0,0 +1,67 @@
1
+ require_relative "../transport/runtime"
2
+
3
+ module Kward
4
+ class CLI
5
+ # Foreground transport lifecycle commands.
6
+ module Transports
7
+ private
8
+
9
+ def handle_transport_command(arguments)
10
+ command = arguments.first.to_s
11
+ case command
12
+ when "list"
13
+ raise ArgumentError, command_usage("transport") unless arguments.length == 1
14
+
15
+ with_transport_runtime { |runtime| print_transport_entries(runtime.manager.list) }
16
+ when "status"
17
+ raise ArgumentError, command_usage("transport") unless arguments.length <= 2
18
+
19
+ with_transport_runtime do |runtime|
20
+ entries = arguments.length == 2 ? [runtime.manager.status(arguments[1])] : runtime.manager.status
21
+ print_transport_entries(entries)
22
+ end
23
+ when "run"
24
+ raise ArgumentError, command_usage("transport") unless (2..3).cover?(arguments.length)
25
+
26
+ workspace_root = arguments[2] ? expanded_working_directory(arguments[2]) : @working_directory
27
+ run_transport(arguments[1], workspace_root: workspace_root)
28
+ else
29
+ raise ArgumentError, command_usage("transport")
30
+ end
31
+ end
32
+
33
+ def print_transport_entries(entries)
34
+ entries.each do |entry|
35
+ @prompt.say("#{entry[:id]}\t#{entry[:state] || "stopped"}")
36
+ end
37
+ end
38
+
39
+ def run_transport(name, workspace_root: nil)
40
+ runtime = Transport::Runtime.new(client: ensure_client!)
41
+ runtime.manager.start(name, workspace_root: workspace_root)
42
+ @prompt.say("Transport #{name} is running. Press Ctrl-C to stop.")
43
+ wait_for_transport_shutdown
44
+ ensure
45
+ runtime&.shutdown
46
+ end
47
+
48
+ def with_transport_runtime
49
+ runtime = Transport::Runtime.new(client: ensure_client!)
50
+ yield runtime
51
+ ensure
52
+ runtime&.shutdown
53
+ end
54
+
55
+ def wait_for_transport_shutdown
56
+ stopped = false
57
+ previous_handlers = {}
58
+ %w[INT TERM].each do |signal|
59
+ previous_handlers[signal] = Signal.trap(signal) { stopped = true }
60
+ end
61
+ sleep 0.1 until stopped
62
+ ensure
63
+ previous_handlers&.each { |signal, handler| Signal.trap(signal, handler) }
64
+ end
65
+ end
66
+ end
67
+ end