Class: GDKBox::CLI

Inherits:
Thor
  • Object
show all
Defined in:
lib/gdkbox/cli.rb

Overview

The gdkbox command-line interface.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.exit_on_failure?Boolean

Returns:

  • (Boolean)


10
11
12
# File 'lib/gdkbox/cli.rb', line 10

def self.exit_on_failure?
  true
end

Instance Method Details

#add_skill(box_name, *skill_tokens) ⇒ Object

Raises:



321
322
323
324
325
326
327
328
329
330
331
# File 'lib/gdkbox/cli.rb', line 321

def add_skill(box_name, *skill_tokens)
  box = load_box!(box_name)
  skill_tokens = prompt_for_skills if skill_tokens.empty?
  raise Error, "No skills selected." if skill_tokens.empty?

  skill_tokens.each do |token|
    skill = box.install_skill(token, force: options[:force])
    say "Installed skill '#{skill.name}' into box '#{box_name}'.", :green
  end
  say "\nDispatched agents in '#{box_name}' can now use these skills.", :cyan
end

#code(name) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/gdkbox/cli.rb', line 181

def code(name)
  box = load_box!(name)
  rewrite_ssh_config
  vscode = VSCode.new
  unless vscode.available?
    say "The `code` CLI was not found on PATH.", :yellow
    say "Open VS Code manually and connect to host: #{box.ssh_host_alias}"
    say "Or run: #{vscode.open_command(box).join(' ')}"
    return
  end
  say "Opening #{box.name} in VS Code (#{box.ssh_host_alias})...", :green
  vscode.open(box)
end

#completion(shell) ⇒ Object



343
344
345
# File 'lib/gdkbox/cli.rb', line 343

def completion(shell)
  puts Completion.new(self.class).script(shell)
end

#dispatch(name) ⇒ Object

Raises:



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/gdkbox/cli.rb', line 157

def dispatch(name)
  box = load_box!(name)
  task = options[:task]
  task = File.read(options[:task_file]) if options[:task_file]
  raise Error, "Provide a task with --task or --task-file." if task.nil? || task.strip.empty?

  result = box.run_agent(
    task: task,
    json: options[:json],
    yolo: options[:yolo],
    timeout: options[:timeout]
  )
  $stdout.print(result.stdout)
  $stderr.print(result.stderr) unless result.stderr.to_s.empty?
  exit(result.status)
end

#harnessesObject



205
206
207
208
209
210
# File 'lib/gdkbox/cli.rb', line 205

def harnesses
  Harness.all.each do |h|
    say format("%-10s ", h.id), :green, false
    say "#{h.summary} (key: $#{h.key_env})"
  end
end

#install_agent(name) ⇒ Object



196
197
198
199
200
201
# File 'lib/gdkbox/cli.rb', line 196

def install_agent(name)
  box = load_box!(name)
  say "Installing #{box.harness.summary} in '#{name}'...", :green
  box.install_agent!
  say "Done. SSH in and run `#{box.harness.bin}` to start an agent.", :green
end

#install_skill(name = nil) ⇒ Object

Raises:



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/gdkbox/cli.rb', line 278

def install_skill(name = nil)
  available = Skills.available
  raise Error, "This repo ships no skills." if available.empty?

  if name && !available.include?(name)
    raise Error, "No skill named '#{name}'. Available: #{available.join(', ')}."
  end

  dest = options[:project] ? Skills.project_dest : Skills::GLOBAL_DEST
  skills = Skills.new(dest: dest)

  (name ? [name] : available).each do |skill_name|
    target = skills.install(skill_name, force: options[:force])
    say "Installed skill '#{skill_name}' -> #{target}", :green
  end
  say "\nStart a new Claude Code session to pick up the skill.", :cyan
end

#lsObject



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/gdkbox/cli.rb', line 104

def ls
  boxes = Box.all(config: config).sort_by(&:name)
  docker = Docker.new

  if options[:json]
    summaries = boxes.map { |box| box.summary(state: docker.available? ? nil : "unknown") }
    puts JSON.generate(summaries)
    return
  end

  if boxes.empty?
    say "No GDK boxes yet. Create one with `gdkbox up <name>`."
    return
  end

  boxes.each do |box|
    status = docker.available? ? box.state : "unknown"
    say format("%-20s %-10s ssh:%-6s web:%-6s %s",
      box.name, status, box.ssh_port, box.web_port, box.web_url)
  end
end

#rm(name) ⇒ Object



254
255
256
257
258
259
260
261
262
# File 'lib/gdkbox/cli.rb', line 254

def rm(name)
  box = load_box!(name)
  unless options[:force]
    return unless yes?("Remove box '#{name}' and its container? [y/N]")
  end
  box.destroy!
  rewrite_ssh_config
  say "Removed '#{name}'.", :green
end

#set_key(name) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/gdkbox/cli.rb', line 223

def set_key(name)
  box = load_box!(name)
  api_key = resolve_api_key(box.harness)
  unless api_key
    raise Error, "No API key. Pass --api-key, set $#{box.harness.key_env}, " \
      "or add `#{box.harness.config_key}:` to ~/.gdkbox/config.yml."
  end

  say "Seeding #{box.harness.key_env} into '#{name}'...", :green
  box.set_api_key!(api_key)
  say "Done. Unattended `gdkbox dispatch #{name}` is ready.", :green
end

#skillsObject



298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/gdkbox/cli.rb', line 298

def skills
  found = Skills.discover
  if found.empty?
    say "No skills found in this repo, ~/.claude/skills, or ./.claude/skills."
    return
  end
  found.each do |skill|
    say format("%-28s ", skill.name), :green, false
    say "[#{skill.origin}]", :cyan
    say "  #{skill.description}" if skill.description && !skill.description.empty?
  end
end

#ssh(name) ⇒ Object



175
176
177
178
# File 'lib/gdkbox/cli.rb', line 175

def ssh(name)
  box = load_box!(name)
  exec(*box.ssh_command)
end

#start(name) ⇒ Object



238
239
240
241
242
243
# File 'lib/gdkbox/cli.rb', line 238

def start(name)
  box = load_box!(name)
  say "Starting '#{name}'...", :green
  box.start!
  print_connection_details(box)
end

#status(name) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/gdkbox/cli.rb', line 128

def status(name)
  box = load_box!(name)
  docker = Docker.new

  if options[:json]
    puts JSON.generate(box.summary(state: docker.available? ? nil : "unknown"))
    return
  end

  say "Box:        #{box.name}"
  say "Container:  #{box.container_name}"
  say "State:      #{docker.available? ? box.state : 'unknown'}"
  print_connection_details(box)
end

#stop(name) ⇒ Object



246
247
248
249
250
# File 'lib/gdkbox/cli.rb', line 246

def stop(name)
  box = load_box!(name)
  say "Stopping '#{name}'...", :green
  box.stop!
end

#up(name) ⇒ Object

Raises:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/gdkbox/cli.rb', line 40

def up(name)
  ensure_docker!
  box = build_box(name)
  raise Error, "Box '#{name}' already exists. Use `gdkbox rm #{name}` first." if box.exists?
  harness = Harness[options[:harness] || config.default_harness]
  api_key = resolve_api_key(harness)

  say "Spinning up GDK box '#{name}' (#{harness.summary}; first run pulls a large image)...", :green unless options[:json]
  box.create!(
    image: options[:image],
    ssh_port: options[:ssh_port],
    web_port: options[:web_port],
    harness: harness.id,
    install_agent: options[:agent],
    api_key: api_key
  )
  rewrite_ssh_config
  seed_skills(box)

  if options[:json]
    puts JSON.generate(box.summary)
    return
  end

  say "\nBox '#{name}' is up.", :green
  print_connection_details(box)
  unless api_key
    say "\n  No #{harness.key_env} seeded. Before unattended dispatch, run:", :yellow
    say "    gdkbox set-key #{name}   (uses $#{harness.key_env})", :yellow
  end
end

#update_imageObject



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/gdkbox/cli.rb', line 83

def update_image
  ensure_docker!
  image = options[:image] || config.default_image
  docker = Docker.new

  say "Pulling #{image} ...", :green
  bar = ProgressBar.new
  docker.pull(image) do |progress|
    label = "#{progress.completed}/#{progress.total} layers"
    label += " ยท #{progress.human_speed}" if progress.human_speed
    bar.update(progress.fraction, label)
  end
  bar.finish

  say "Updated. New boxes will use this image; recreate existing boxes to adopt it.", :green
end

#versionObject



348
349
350
# File 'lib/gdkbox/cli.rb', line 348

def version
  say GDKBox::VERSION
end