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:



292
293
294
295
296
297
298
299
300
301
302
# File 'lib/gdkbox/cli.rb', line 292

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



152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/gdkbox/cli.rb', line 152

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



314
315
316
# File 'lib/gdkbox/cli.rb', line 314

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

#dispatch(name) ⇒ Object

Raises:



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

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



176
177
178
179
180
181
# File 'lib/gdkbox/cli.rb', line 176

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



167
168
169
170
171
172
# File 'lib/gdkbox/cli.rb', line 167

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:



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/gdkbox/cli.rb', line 249

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



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/gdkbox/cli.rb', line 75

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



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

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



194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/gdkbox/cli.rb', line 194

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



269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/gdkbox/cli.rb', line 269

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



146
147
148
149
# File 'lib/gdkbox/cli.rb', line 146

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

#start(name) ⇒ Object



209
210
211
212
213
214
# File 'lib/gdkbox/cli.rb', line 209

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

#status(name) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/gdkbox/cli.rb', line 99

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



217
218
219
220
221
# File 'lib/gdkbox/cli.rb', line 217

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

#versionObject



319
320
321
# File 'lib/gdkbox/cli.rb', line 319

def version
  say GDKBox::VERSION
end