Module: SmartBox::CLI

Defined in:
lib/smart_box/cli.rb

Class Method Summary collapse

Class Method Details

.cmd_apply(argv) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/smart_box/cli.rb', line 243

def self.cmd_apply(argv)
  opts = parse_apply(argv)

  unless opts[:id]
    $stderr.puts "Error: --id is required"
    exit 1
  end

  box = SmartBox::Box.load(source: opts[:source] || ".", id: opts[:id])

  if opts[:dry_run]
    result = box.apply(dry_run: true)
    puts "Dry run:"
    puts "  patch size: #{result[:patch_size]} bytes"
    puts "  Would apply #{result[:patch_size] > 0 ? 'changes' : 'no changes'} to #{box.source_path}"
    return
  end

  # Check if source is clean before applying
  unless opts[:force] || box.source_clean?
    $stderr.puts "Error: Source project has uncommitted changes."
    $stderr.puts "Use --force to apply anyway, or --dry-run to preview."
    exit 1
  end

  result = box.apply(force: opts[:force])
  puts "Patch applied:"
  puts result
end

.cmd_checkpoint(argv) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/smart_box/cli.rb', line 148

def self.cmd_checkpoint(argv)
  opts = parse_checkpoint(argv)
  id   = opts[:id]
  name = opts[:name]

  unless id
    $stderr.puts "Error: --id is required"
    exit 1
  end

  unless name
    $stderr.puts "Error: --name is required"
    exit 1
  end

  box  = SmartBox::Box.load(source: opts[:source] || ".", id: id)
  cp   = box.checkpoint(name)

  puts "Checkpoint created:"
  puts "  id: #{cp[:id]}"
  puts "  name: #{cp[:name]}"
  puts "  commit: #{cp[:commit][0..7]}"
end

.cmd_checkpoints(argv) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/smart_box/cli.rb', line 172

def self.cmd_checkpoints(argv)
  opts = parse_box_opts(argv, require_id: true)
  box  = SmartBox::Box.load(source: opts[:source] || ".", id: opts[:id])
  cps  = box.checkpoints

  if cps.empty?
    puts "No checkpoints."
    return
  end

  cps.each do |cp|
    puts "#{cp['id']}  #{cp['name']}"
  end
end

.cmd_create(argv) ⇒ Object

— Command implementations —



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/smart_box/cli.rb', line 51

def self.cmd_create(argv)
  opts = parse_create(argv)
  source = opts[:source] || "."
  id     = opts[:id]
  mode   = opts[:mode] || "copy"
  name   = opts[:name]

  unless id
    $stderr.puts "Error: --id is required"
    exit 1
  end

  unless %w[copy git-worktree].include?(mode)
    $stderr.puts "Error: --mode must be 'copy' or 'git-worktree'"
    exit 1
  end

  box = SmartBox::Box.create(source: source, id: id, mode: mode, name: name)

  puts "Box created:"
  puts "  id: #{box.id}"
  puts "  mode: #{box.mode}"
  puts "  workspace: #{box.workspace_path}"
  puts "  checkpoint: cp-001 initial"
end

.cmd_diff(argv) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
# File 'lib/smart_box/cli.rb', line 210

def self.cmd_diff(argv)
  opts = parse_diff(argv)
  box  = SmartBox::Box.load(source: opts[:source] || ".", id: opts[:id])

  unless box..id
    $stderr.puts "Error: --id is required"
    exit 1
  end

  puts box.diff(from: opts[:from], to: opts[:to])
end

.cmd_discard(argv) ⇒ Object



273
274
275
276
277
278
279
280
281
# File 'lib/smart_box/cli.rb', line 273

def self.cmd_discard(argv)
  opts = parse_box_opts(argv, require_id: true)
  box  = SmartBox::Box.load(source: opts[:source] || ".", id: opts[:id])
  result = box.discard

  puts "Box discarded:"
  puts "  id: #{result[:id]}"
  puts "  status: #{result[:status]}"
end

.cmd_export_patch(argv) ⇒ Object



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/smart_box/cli.rb', line 222

def self.cmd_export_patch(argv)
  opts = parse_export_patch(argv)

  unless opts[:id]
    $stderr.puts "Error: --id is required"
    exit 1
  end

  unless opts[:output]
    $stderr.puts "Error: --output is required"
    exit 1
  end

  box = SmartBox::Box.load(source: opts[:source] || ".", id: opts[:id])
  result = box.export_patch(output: opts[:output], from: opts[:from], to: opts[:to])

  puts "Patch exported:"
  puts "  #{result[:output]}"
  puts "  #{result[:size]} bytes"
end

.cmd_list(argv) ⇒ Object



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

def self.cmd_list(argv)
  opts = parse_source(argv)
  source = opts[:source] || "."

  boxes = SmartBox::Box.list(source: source)

  if boxes.empty?
    puts "No boxes found."
    return
  end

  puts "ID          MODE          STATUS    UPDATED_AT"
  boxes.each do |b|
    updated = b["updated_at"]
    updated = updated[0..15] if updated.is_a?(String) && updated.length > 16
    printf "%-10s  %-12s  %-8s  %s\n",
      b["id"], b["mode"], b["status"], updated
  end
end

.cmd_rollback(argv) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/smart_box/cli.rb', line 187

def self.cmd_rollback(argv)
  opts        = parse_rollback(argv)
  id          = opts[:id]
  checkpoint  = opts[:checkpoint]

  unless id
    $stderr.puts "Error: --id is required"
    exit 1
  end

  unless checkpoint
    $stderr.puts "Error: --checkpoint is required"
    exit 1
  end

  box = SmartBox::Box.load(source: opts[:source] || ".", id: id)
  box.rollback(checkpoint)

  puts "Rolled back:"
  puts "  box: #{id}"
  puts "  checkpoint: #{checkpoint}"
end

.cmd_run(argv) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/smart_box/cli.rb', line 124

def self.cmd_run(argv)
  argv.shift  # remove "run" subcommand name
  opts = parse_run(argv)
  id = opts[:id]

  unless id
    $stderr.puts "Error: --id is required"
    exit 1
  end

  command = argv.join(" ")
  if command.empty?
    $stderr.puts "Error: command is required (use -- before command args)"
    exit 1
  end

  box = SmartBox::Box.load(source: opts[:source] || ".", id: id)
  result = box.run(command, allow_dangerous: opts[:allow_dangerous])

  $stdout.write(result.stdout)
  $stderr.write(result.stderr) unless result.stderr.empty?
  exit result.exit_code
end

.cmd_status(argv) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/smart_box/cli.rb', line 97

def self.cmd_status(argv)
  opts = parse_box_opts(argv, require_id: true)
  box  = SmartBox::Box.load(source: opts[:source] || ".", id: opts[:id])
  s    = box.status_summary

  puts "Box: #{s['id']}"
  puts "Mode: #{s['mode']}"
  puts "Status: #{s['status']}"
  puts "Workspace: #{s['workspace']}"
  puts

  if s["changed_files"]&.any?
    puts "Changed files:"
    s["changed_files"].each { |f| puts "  #{f}" }
  else
    puts "No changed files."
  end
  puts

  if s["checkpoints"]&.any?
    puts "Checkpoints:"
    s["checkpoints"].each do |cp|
      cp.each { |id, name| puts "  #{id} #{name}" }
    end
  end
end

.parse_apply(argv) ⇒ Object



371
372
373
374
375
376
377
378
379
380
# File 'lib/smart_box/cli.rb', line 371

def self.parse_apply(argv)
  opts = {}
  OptionParser.new do |p|
    p.on("--source PATH") { |v| opts[:source] = v }
    p.on("--id ID")       { |v| opts[:id] = v }
    p.on("--dry-run")     { |v| opts[:dry_run] = true }
    p.on("--force")       { |v| opts[:force] = true }
  end.parse!(argv)
  opts
end

.parse_box_opts(argv, require_id: false) ⇒ Object



304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/smart_box/cli.rb', line 304

def self.parse_box_opts(argv, require_id: false)
  opts = {}
  OptionParser.new do |p|
    p.on("--source PATH") { |v| opts[:source] = v }
    p.on("--id ID")       { |v| opts[:id] = v }
  end.parse!(argv)

  if require_id && !opts[:id]
    $stderr.puts "Error: --id is required"
    exit 1
  end
  opts
end

.parse_checkpoint(argv) ⇒ Object



328
329
330
331
332
333
334
335
336
# File 'lib/smart_box/cli.rb', line 328

def self.parse_checkpoint(argv)
  opts = {}
  OptionParser.new do |p|
    p.on("--source PATH") { |v| opts[:source] = v }
    p.on("--id ID")       { |v| opts[:id] = v }
    p.on("--name NAME")   { |v| opts[:name] = v }
  end.parse!(argv)
  opts
end

.parse_create(argv) ⇒ Object

— Option parsers —



285
286
287
288
289
290
291
292
293
294
# File 'lib/smart_box/cli.rb', line 285

def self.parse_create(argv)
  opts = {}
  OptionParser.new do |p|
    p.on("--source PATH")   { |v| opts[:source] = v }
    p.on("--id ID")         { |v| opts[:id] = v }
    p.on("--mode MODE")     { |v| opts[:mode] = v }
    p.on("--name NAME")     { |v| opts[:name] = v }
  end.parse!(argv)
  opts
end

.parse_diff(argv) ⇒ Object



348
349
350
351
352
353
354
355
356
357
# File 'lib/smart_box/cli.rb', line 348

def self.parse_diff(argv)
  opts = {}
  OptionParser.new do |p|
    p.on("--source PATH") { |v| opts[:source] = v }
    p.on("--id ID")       { |v| opts[:id] = v }
    p.on("--from CP")     { |v| opts[:from] = v }
    p.on("--to CP")       { |v| opts[:to] = v }
  end.parse!(argv)
  opts
end

.parse_export_patch(argv) ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
# File 'lib/smart_box/cli.rb', line 359

def self.parse_export_patch(argv)
  opts = {}
  OptionParser.new do |p|
    p.on("--source PATH") { |v| opts[:source] = v }
    p.on("--id ID")       { |v| opts[:id] = v }
    p.on("--output FILE") { |v| opts[:output] = v }
    p.on("--from CP")     { |v| opts[:from] = v }
    p.on("--to CP")       { |v| opts[:to] = v }
  end.parse!(argv)
  opts
end

.parse_options(argv) ⇒ Object



382
383
384
# File 'lib/smart_box/cli.rb', line 382

def self.parse_options(argv)
  # Global options stripped before command dispatch — handled in `run`
end

.parse_rollback(argv) ⇒ Object



338
339
340
341
342
343
344
345
346
# File 'lib/smart_box/cli.rb', line 338

def self.parse_rollback(argv)
  opts = {}
  OptionParser.new do |p|
    p.on("--source PATH")     { |v| opts[:source] = v }
    p.on("--id ID")           { |v| opts[:id] = v }
    p.on("--checkpoint CP")   { |v| opts[:checkpoint] = v }
  end.parse!(argv)
  opts
end

.parse_run(argv) ⇒ Object



318
319
320
321
322
323
324
325
326
# File 'lib/smart_box/cli.rb', line 318

def self.parse_run(argv)
  opts = {}
  OptionParser.new do |p|
    p.on("--source PATH")           { |v| opts[:source] = v }
    p.on("--id ID")                 { |v| opts[:id] = v }
    p.on("--allow-dangerous")       { |v| opts[:allow_dangerous] = true }
  end.parse!(argv)
  opts
end

.parse_source(argv) ⇒ Object



296
297
298
299
300
301
302
# File 'lib/smart_box/cli.rb', line 296

def self.parse_source(argv)
  opts = {}
  OptionParser.new do |p|
    p.on("--source PATH") { |v| opts[:source] = v }
  end.parse!(argv)
  opts
end


386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/smart_box/cli.rb', line 386

def self.print_help
  puts <<~HELP
    smart_box #{SmartBox::VERSION}

    Usage:
      smart_box <command> [options]

    Commands:
      create            Create a new box
      list              List boxes
      status            Show box status
      run               Run a command in a box
      checkpoint        Create a checkpoint
      checkpoints       List checkpoints
      rollback          Rollback to a checkpoint
      diff              Show diff
      export-patch      Export patch file
      apply             Apply patch to source project
      discard           Discard a box

    Options:
      --version, -v     Show version
      --help, -h        Show this help
  HELP
end

.run(argv = ARGV) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/smart_box/cli.rb', line 8

def self.run(argv = ARGV)
  parse_options(argv)
  command = argv.first

  case command
  when "--version", "-v"
    puts "smart_box #{SmartBox::VERSION}"
  when "--help", "-h", nil
    print_help
  when "create"
    cmd_create(argv)
  when "list"
    cmd_list(argv)
  when "status"
    cmd_status(argv)
  when "run"
    cmd_run(argv)
  when "checkpoint"
    cmd_checkpoint(argv)
  when "checkpoints"
    cmd_checkpoints(argv)
  when "rollback"
    cmd_rollback(argv)
  when "diff"
    cmd_diff(argv)
  when "export-patch"
    cmd_export_patch(argv)
  when "apply"
    cmd_apply(argv)
  when "discard"
    cmd_discard(argv)
  else
    puts "Unknown command: #{command}"
    print_help
    exit 1
  end
rescue SmartBox::Error => e
  $stderr.puts "Error: #{e.message}"
  exit 2
end