walheim 0.1.2

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.
data/bin/whctl ADDED
@@ -0,0 +1,703 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'optparse'
6
+ require_relative '../lib/walheim'
7
+
8
+ # Generate help text for a resource's operations
9
+ def generate_resource_help(handler_info)
10
+ handler_class = handler_info[:handler]
11
+ kind_name = handler_info[:name]
12
+
13
+ # Skip if resource doesn't have operation_info (e.g., Namespaces)
14
+ return [] unless handler_class.respond_to?(:operation_info)
15
+
16
+ # Get operation metadata from the resource class
17
+ operation_info = handler_class.operation_info
18
+
19
+ help_lines = []
20
+
21
+ operation_info.each do |operation, metadata|
22
+ # Check if this resource actually supports the operation
23
+ next unless Walheim::HandlerRegistry.supports_operation?(kind_name, operation)
24
+
25
+ # Add each usage line
26
+ metadata[:usage].each do |usage_line|
27
+ help_lines << " #{usage_line.ljust(70)} #{metadata[:description]}"
28
+ end
29
+ end
30
+
31
+ help_lines
32
+ end
33
+
34
+ # Generate help for global commands
35
+ def generate_global_help
36
+ [
37
+ " create namespace {name} [--username {user}] [--hostname {host}]".ljust(72) + "Create a new namespace",
38
+ " apply -f {manifest.yaml}".ljust(72) + "Apply resource from file (use -f - for stdin)",
39
+ "",
40
+ "Context commands:",
41
+ " context new {name} --data-dir {path}".ljust(72) + "Create a new context",
42
+ " context list".ljust(72) + "List all contexts",
43
+ " context use {name}".ljust(72) + "Switch to a context",
44
+ " context current".ljust(72) + "Show current context",
45
+ " context delete {name}".ljust(72) + "Delete a context"
46
+ ]
47
+ end
48
+
49
+ # Handle help before parsing
50
+ if ARGV.empty? || ARGV.include?('help') || ARGV.include?('--help') || ARGV.include?('-h')
51
+ puts 'Usage: whctl [global flags] <command> [arguments]'
52
+ puts ''
53
+ puts 'Global flags:'
54
+ puts ' --context CONTEXT Override active context for this command'
55
+ puts ' --whconfig PATH Use alternate config file (default: ~/.walheim/config)'
56
+ puts ' -d, --data-dir DIR Data directory containing namespaces (deprecated: use contexts)'
57
+ puts ''
58
+ puts 'Available commands:'
59
+ puts ''
60
+
61
+ # Global commands
62
+ generate_global_help.each { |line| puts line }
63
+ puts ''
64
+
65
+ # Resource commands (dynamically generated)
66
+ Walheim::HandlerRegistry.all_visible.each do |handler_info|
67
+ kind_name = handler_info[:name]
68
+ kind_info = handler_info[:info]
69
+
70
+ # Show supported variations
71
+ variations = [kind_info[:singular]]
72
+ variations += kind_info[:aliases] if kind_info[:aliases]
73
+ variations_text = variations.empty? ? "" : " (aliases: #{variations.join(', ')})"
74
+
75
+ puts "#{kind_name.capitalize}#{variations_text}:"
76
+ generate_resource_help(handler_info).each { |line| puts line }
77
+ puts ''
78
+ end
79
+
80
+ exit 0
81
+ end
82
+
83
+ # Parse global flags first
84
+ options = {}
85
+ global_parser = OptionParser.new do |opts|
86
+ opts.on('--context CONTEXT', 'Override active context') { |v| options[:context] = v }
87
+ opts.on('--whconfig PATH', 'Alternate config file path') { |v| options[:whconfig] = v }
88
+ opts.on('-d DIR', '--data-dir DIR', 'Data directory (deprecated)') { |v| options[:data_dir] = v }
89
+ end
90
+
91
+ # Parse only global flags, leaving the rest for later
92
+ begin
93
+ global_parser.order!(ARGV)
94
+ rescue OptionParser::InvalidOption => e
95
+ warn "Error: #{e.message}"
96
+ exit 1
97
+ end
98
+
99
+ # Now get the command (first remaining arg)
100
+ command = ARGV[0]
101
+
102
+ # Parse command-specific flags
103
+ command_parser = OptionParser.new do |opts|
104
+ opts.on('-n NAMESPACE', '--namespace NAMESPACE', 'Specify namespace') { |v| options[:namespace] = v }
105
+ opts.on('-A', '--all', 'All namespaces') { options[:all_namespaces] = true }
106
+ opts.on('-f FILE', '--file FILE', 'Manifest file path') { |v| options[:file] = v }
107
+ opts.on('-d DIR', '--data-dir DIR', 'Data directory containing namespaces') { |v| options[:data_dir] = v }
108
+ opts.on('--hostname HOSTNAME', 'Hostname for namespace') { |v| options[:hostname] = v }
109
+ opts.on('--username USERNAME', 'Username for namespace') { |v| options[:username] = v }
110
+
111
+ # Logs-specific options
112
+ opts.on('--follow', 'Follow log output') { options[:follow] = true }
113
+ opts.on('--tail N', Integer, 'Number of lines to show from end of logs') { |v| options[:tail] = v }
114
+ opts.on('--timestamps', 'Show timestamps') { options[:timestamps] = true }
115
+ end
116
+
117
+ # Parse command flags
118
+ begin
119
+ command_parser.parse!(ARGV)
120
+ rescue OptionParser::InvalidOption => e
121
+ warn "Error: #{e.message}"
122
+ exit 1
123
+ end
124
+
125
+ # Get positional arguments after all flags are removed
126
+ kind = ARGV[1]
127
+ name = ARGV[2]
128
+
129
+ namespace = options[:namespace]
130
+ all_namespaces = options[:all_namespaces]
131
+
132
+ # Resolve data directory from context or --data-dir flag
133
+ data_dir = nil
134
+
135
+ # Skip context resolution for context commands (they manage config themselves)
136
+ unless command == 'context'
137
+ # Try to load config and resolve data directory from context
138
+ begin
139
+ config = Walheim::Config.new(config_path: options[:whconfig])
140
+
141
+ # Determine which context to use
142
+ context_name = options[:context] || config.current_context
143
+
144
+ if context_name
145
+ # Get data directory from the specified or active context
146
+ data_dir = config.data_dir(context_name)
147
+ elsif options[:data_dir]
148
+ # Fall back to --data-dir if provided (deprecated path)
149
+ data_dir = options[:data_dir]
150
+ warn "Warning: --data-dir is deprecated. Use 'whctl context new' to create a context."
151
+ else
152
+ # No context and no --data-dir
153
+ warn 'Error: No Walheim configuration found.'
154
+ warn ''
155
+ warn 'Create your first context:'
156
+ warn ' whctl context new <name> --data-dir <path>'
157
+ warn ''
158
+ warn 'Example:'
159
+ warn ' whctl context new homelab --data-dir ~/my-homelab'
160
+ exit 1
161
+ end
162
+ rescue Walheim::Config::ConfigError, Walheim::Config::ValidationError => e
163
+ # Config file doesn't exist or is invalid
164
+ if options[:data_dir]
165
+ # Fall back to --data-dir if provided
166
+ data_dir = options[:data_dir]
167
+ warn "Warning: --data-dir is deprecated. Use 'whctl context new' to create a context."
168
+ else
169
+ warn 'Error: No Walheim configuration found.'
170
+ warn ''
171
+ warn 'Create your first context:'
172
+ warn ' whctl context new <name> --data-dir <path>'
173
+ exit 1
174
+ end
175
+ end
176
+ end
177
+
178
+ # Default to current directory if still not set (shouldn't happen but safety)
179
+ data_dir ||= Dir.pwd
180
+
181
+ # For handlers that need it - namespaced resources use namespaces_dir
182
+ # Cluster resources use data_dir directly
183
+ namespaces_dir = File.join(data_dir, 'namespaces')
184
+
185
+ # Helper method to read YAML from file or stdin
186
+ def read_yaml_input(file_path)
187
+ if file_path == '-'
188
+ # Read from stdin
189
+ YAML.load(STDIN.read)
190
+ else
191
+ # Read from file
192
+ YAML.load_file(file_path)
193
+ end
194
+ end
195
+
196
+ # Helper method to print resources table
197
+ def print_resources_table(result, all_namespaces, kind_name)
198
+ require 'terminal-table'
199
+
200
+ if result.is_a?(Hash)
201
+ # Single resource - print YAML
202
+ puts YAML.dump(result[:manifest])
203
+ return
204
+ end
205
+
206
+ if result.empty?
207
+ puts all_namespaces ? "No #{kind_name} found" : "No #{kind_name} found in namespace"
208
+ return
209
+ end
210
+
211
+ # Extract summary field names from first result
212
+ summary_fields = result.first[:summary].keys
213
+
214
+ # Build header row
215
+ if all_namespaces
216
+ headers = ['NAMESPACE', 'NAME'] + summary_fields.map(&:to_s).map(&:upcase)
217
+ else
218
+ headers = ['NAME'] + summary_fields.map(&:to_s).map(&:upcase)
219
+ end
220
+
221
+ # Build data rows
222
+ rows = result.map do |resource|
223
+ row = if all_namespaces
224
+ [resource[:namespace], resource[:name]]
225
+ else
226
+ [resource[:name]]
227
+ end
228
+
229
+ # Add summary field values
230
+ summary_values = summary_fields.map { |field| resource[:summary][field] || 'N/A' }
231
+ row + summary_values
232
+ end
233
+
234
+ all_rows = [headers] + rows
235
+
236
+ table = Terminal::Table.new do |t|
237
+ t.rows = all_rows
238
+ t.style = {
239
+ border_x: '', border_y: '', border_i: '',
240
+ padding_left: 0, padding_right: 3,
241
+ border_top: false, border_bottom: false,
242
+ all_separators: false
243
+ }
244
+ end
245
+
246
+ puts table
247
+ end
248
+
249
+ def print_cluster_resources_table(result, kind_name)
250
+ require 'terminal-table'
251
+
252
+ if result.is_a?(Hash)
253
+ # Single resource - print YAML
254
+ puts YAML.dump(result[:manifest])
255
+ return
256
+ end
257
+
258
+ if result.empty?
259
+ puts "No #{kind_name} found"
260
+ return
261
+ end
262
+
263
+ # Extract summary field names from first result
264
+ summary_fields = result.first[:summary].keys
265
+
266
+ # Build header row (no NAMESPACE column for cluster resources)
267
+ headers = ['NAME'] + summary_fields.map(&:to_s).map(&:upcase)
268
+
269
+ # Build data rows
270
+ rows = result.map do |resource|
271
+ row = [resource[:name]]
272
+
273
+ # Add summary field values
274
+ summary_values = summary_fields.map { |field| resource[:summary][field] || 'N/A' }
275
+ row + summary_values
276
+ end
277
+
278
+ all_rows = [headers] + rows
279
+
280
+ table = Terminal::Table.new do |t|
281
+ t.rows = all_rows
282
+ t.style = {
283
+ border_x: '', border_y: '', border_i: '',
284
+ padding_left: 0, padding_right: 3,
285
+ border_top: false, border_bottom: false,
286
+ all_separators: false
287
+ }
288
+ end
289
+
290
+ puts table
291
+ end
292
+
293
+ # Route commands
294
+ case command
295
+ when 'get'
296
+ handler_info = Walheim::HandlerRegistry.get(kind)
297
+
298
+ unless handler_info
299
+ warn "Error: unknown kind '#{kind}'"
300
+ warn ''
301
+ warn 'Available kinds:'
302
+ Walheim::HandlerRegistry.all_visible.each do |h|
303
+ warn " #{h[:name]}"
304
+ end
305
+ exit 1
306
+ end
307
+
308
+ handler = handler_info[:handler].new(data_dir: data_dir)
309
+
310
+ # Special handling for cluster resources (no namespace argument)
311
+ if handler.is_a?(Walheim::ClusterResource)
312
+ result = handler.get
313
+ print_cluster_resources_table(result, handler_info[:name])
314
+ else
315
+ # All other resources support namespace filtering
316
+ unless all_namespaces || namespace
317
+ warn 'Error: either -n {namespace} or --all/-A flag is required'
318
+ warn "Usage: whctl get #{handler_info[:name]} -n {namespace}"
319
+ warn "Usage: whctl get #{handler_info[:name]} --all"
320
+ exit 1
321
+ end
322
+
323
+ # Call handler and get data
324
+ result = if all_namespaces
325
+ handler.get(namespace: nil, name: nil)
326
+ else
327
+ handler.get(namespace: namespace, name: name)
328
+ end
329
+
330
+ # Format and print output
331
+ print_resources_table(result, all_namespaces, handler_info[:name])
332
+ end
333
+ when 'apply'
334
+ # If -f is provided, extract namespace and name from manifest
335
+ if options[:file]
336
+ manifest_data = read_yaml_input(options[:file])
337
+ kind = manifest_data['kind']&.downcase + 's' # App -> apps
338
+ namespace = manifest_data['metadata']['namespace']
339
+ name = manifest_data['metadata']['name']
340
+
341
+ unless kind && namespace && name
342
+ warn 'Error: Manifest must contain kind, metadata.namespace, and metadata.name'
343
+ exit 1
344
+ end
345
+ end
346
+
347
+ handler_info = Walheim::HandlerRegistry.get(kind)
348
+
349
+ unless handler_info
350
+ warn "Error: unknown kind '#{kind}'"
351
+ exit 1
352
+ end
353
+
354
+ # Check if this resource supports apply operation
355
+ unless Walheim::HandlerRegistry.supports_operation?(kind, :apply)
356
+ warn "Error: apply not supported for kind '#{kind}'"
357
+ exit 1
358
+ end
359
+
360
+ # Validate required arguments
361
+ unless namespace && name
362
+ warn 'Error: -n and name are required (or use -f)'
363
+ warn "Usage: whctl apply #{handler_info[:name]} {name} -n {namespace}"
364
+ warn "Or: whctl apply -f {manifest.yaml}"
365
+ exit 1
366
+ end
367
+
368
+ # Execute operation
369
+ handler = handler_info[:handler].new(data_dir: data_dir)
370
+ handler.apply(namespace: namespace, name: name, manifest_source: options[:file])
371
+ when 'delete'
372
+ handler_info = Walheim::HandlerRegistry.get(kind)
373
+
374
+ unless handler_info
375
+ warn "Error: unknown kind '#{kind}'"
376
+ exit 1
377
+ end
378
+
379
+ # Check if this resource supports delete operation
380
+ unless Walheim::HandlerRegistry.supports_operation?(kind, :delete)
381
+ warn "Error: delete not supported for kind '#{kind}'"
382
+ exit 1
383
+ end
384
+
385
+ # Validate required arguments
386
+ unless namespace && name
387
+ warn 'Error: -n and name are required'
388
+ warn "Usage: whctl delete #{handler_info[:name]} {name} -n {namespace}"
389
+ exit 1
390
+ end
391
+
392
+ # Execute operation
393
+ handler = handler_info[:handler].new(data_dir: data_dir)
394
+ handler.delete(namespace: namespace, name: name)
395
+ when 'create'
396
+ # Only namespaces support create
397
+ if %w[namespace namespaces ns].include?(kind)
398
+ unless name
399
+ warn 'Error: namespace name is required'
400
+ warn 'Usage: whctl create namespace {name} [--username {user}] [--hostname {host}]'
401
+ exit 1
402
+ end
403
+
404
+ # Use namespace name as fallback for hostname
405
+ hostname = options[:hostname] || name
406
+
407
+ # Create namespace directory
408
+ namespace_path = File.join(namespaces_dir, name)
409
+ if Dir.exist?(namespace_path)
410
+ warn "Error: namespace '#{name}' already exists at #{namespace_path}"
411
+ exit 1
412
+ end
413
+
414
+ # Create directory structure
415
+ Dir.mkdir(namespace_path)
416
+ Dir.mkdir(File.join(namespace_path, 'apps'))
417
+ Dir.mkdir(File.join(namespace_path, 'secrets'))
418
+ Dir.mkdir(File.join(namespace_path, 'configmaps'))
419
+
420
+ # Create .namespace.yaml with optional username
421
+ config_path = File.join(namespace_path, '.namespace.yaml')
422
+ config_content = "hostname: #{hostname}\n"
423
+ config_content = "username: #{options[:username]}\n#{config_content}" if options[:username]
424
+ File.write(config_path, config_content)
425
+
426
+ puts "Created namespace '#{name}' at #{namespace_path}"
427
+ puts " Username: #{options[:username] || '(from SSH config)'}"
428
+ puts " Hostname: #{hostname}"
429
+ else
430
+ warn "Error: create not supported for kind '#{kind}'"
431
+ warn 'Usage: whctl create namespace {name} [--username {user}] [--hostname {host}]'
432
+ exit 1
433
+ end
434
+ when 'start', 'pause', 'stop'
435
+ # Resource-specific operations
436
+ handler_info = Walheim::HandlerRegistry.get(kind)
437
+
438
+ unless handler_info
439
+ warn "Error: unknown kind '#{kind}'"
440
+ exit 1
441
+ end
442
+
443
+ # Check if this resource supports the operation
444
+ unless Walheim::HandlerRegistry.supports_operation?(kind, command.to_sym)
445
+ warn "Error: #{command} not supported for #{kind}"
446
+ exit 1
447
+ end
448
+
449
+ # Validate required arguments
450
+ unless namespace && name
451
+ warn 'Error: -n and name are required'
452
+ warn "Usage: whctl #{command} #{handler_info[:name]} {name} -n {namespace}"
453
+ exit 1
454
+ end
455
+
456
+ # Execute operation
457
+ handler = handler_info[:handler].new(data_dir: data_dir)
458
+ handler.send(command.to_sym, namespace: namespace, name: name)
459
+ when 'logs'
460
+ # Resource-specific operation
461
+ handler_info = Walheim::HandlerRegistry.get(kind)
462
+
463
+ unless handler_info
464
+ warn "Error: unknown kind '#{kind}'"
465
+ exit 1
466
+ end
467
+
468
+ # Check if this resource supports logs operation
469
+ unless Walheim::HandlerRegistry.supports_operation?(kind, :logs)
470
+ warn "Error: logs not supported for #{kind}"
471
+ exit 1
472
+ end
473
+
474
+ # Validate required arguments
475
+ unless namespace && name
476
+ warn 'Error: -n and name are required'
477
+ warn "Usage: whctl logs #{handler_info[:name]} {name} -n {namespace} [--follow] [--tail N] [--timestamps]"
478
+ exit 1
479
+ end
480
+
481
+ # Build log options
482
+ log_options = {}
483
+ log_options[:follow] = true if options[:follow]
484
+ log_options[:tail] = options[:tail] if options[:tail]
485
+ log_options[:timestamps] = true if options[:timestamps]
486
+
487
+ # Execute operation
488
+ handler = handler_info[:handler].new(data_dir: data_dir)
489
+ handler.logs(namespace: namespace, name: name, **log_options)
490
+ when 'import'
491
+ # Only apps support import
492
+ unless %w[apps app].include?(kind)
493
+ warn "Error: import only supported for apps"
494
+ warn 'Usage: whctl import app {name} -n {namespace} -f {docker-compose.yml}'
495
+ exit 1
496
+ end
497
+
498
+ # Require -f flag
499
+ unless options[:file]
500
+ warn 'Error: -f flag is required for import'
501
+ warn 'Usage: whctl import app {name} -n {namespace} -f {docker-compose.yml}'
502
+ warn 'Or: whctl import app {name} -n {namespace} -f - (read from stdin)'
503
+ exit 1
504
+ end
505
+
506
+ # Validate required arguments
507
+ unless namespace && name
508
+ warn 'Error: -n and name are required'
509
+ warn 'Usage: whctl import app {name} -n {namespace} -f {docker-compose.yml}'
510
+ exit 1
511
+ end
512
+
513
+ # Read docker-compose manifest
514
+ compose_manifest = read_yaml_input(options[:file])
515
+
516
+ # Execute import
517
+ handler_info = Walheim::HandlerRegistry.get('apps')
518
+ handler = handler_info[:handler].new(data_dir: data_dir)
519
+ handler.import(namespace: namespace, name: name, compose_manifest: compose_manifest)
520
+ when 'context'
521
+ # Context management commands
522
+ subcommand = kind # The kind position is the subcommand for context
523
+ context_name = name # The name position is the context name
524
+
525
+ case subcommand
526
+ when 'new'
527
+ # whctl context new {name} --data-dir {path}
528
+ unless context_name
529
+ warn 'Error: context name is required'
530
+ warn 'Usage: whctl context new {name} --data-dir {path}'
531
+ exit 1
532
+ end
533
+
534
+ unless options[:data_dir]
535
+ warn 'Error: --data-dir flag is required'
536
+ warn 'Usage: whctl context new {name} --data-dir {path}'
537
+ exit 1
538
+ end
539
+
540
+ # Validate data_dir exists or can be created
541
+ expanded_data_dir = File.expand_path(options[:data_dir])
542
+ unless Dir.exist?(expanded_data_dir)
543
+ warn "Error: data directory '#{expanded_data_dir}' does not exist"
544
+ warn ''
545
+ warn 'Please create the directory first or provide a valid path'
546
+ exit 1
547
+ end
548
+
549
+ # Check for namespaces subdirectory
550
+ namespaces_path = File.join(expanded_data_dir, 'namespaces')
551
+ unless Dir.exist?(namespaces_path)
552
+ warn "Warning: data directory does not contain 'namespaces' subdirectory"
553
+ warn "Expected path: #{namespaces_path}"
554
+ warn ''
555
+ warn 'This directory should contain your homelab configuration.'
556
+ warn 'Creating namespaces directory...'
557
+ Dir.mkdir(namespaces_path)
558
+ end
559
+
560
+ # Load or create config
561
+ config = Walheim::Config.new(config_path: options[:whconfig])
562
+
563
+ begin
564
+ config.add_context(context_name, expanded_data_dir, activate: true)
565
+ config.save_config
566
+ puts "Created context '#{context_name}'"
567
+ puts " Data directory: #{expanded_data_dir}"
568
+ puts " Status: Active (automatically activated)"
569
+ rescue Walheim::Config::ValidationError => e
570
+ warn "Error: #{e.message}"
571
+ exit 1
572
+ rescue Walheim::Config::ConfigError => e
573
+ warn "Error: #{e.message}"
574
+ exit 1
575
+ end
576
+
577
+ when 'list'
578
+ # whctl context list
579
+ config = Walheim::Config.new(config_path: options[:whconfig])
580
+
581
+ unless Walheim::Config.exists?(config_path: options[:whconfig])
582
+ warn 'No Walheim configuration found.'
583
+ warn ''
584
+ warn 'Create your first context:'
585
+ warn ' whctl context new <name> --data-dir <path>'
586
+ exit 1
587
+ end
588
+
589
+ contexts = config.list_contexts
590
+ if contexts.empty?
591
+ puts 'No contexts configured.'
592
+ exit 0
593
+ end
594
+
595
+ require 'terminal-table'
596
+
597
+ rows = contexts.map do |ctx|
598
+ active_marker = ctx['active'] ? '*' : ''
599
+ [active_marker, ctx['name'], ctx['dataDir']]
600
+ end
601
+
602
+ table = Terminal::Table.new do |t|
603
+ t.headings = ['CURRENT', 'NAME', 'DATA DIRECTORY']
604
+ t.rows = rows
605
+ t.style = {
606
+ border_x: '', border_y: '', border_i: '',
607
+ padding_left: 0, padding_right: 3,
608
+ border_top: false, border_bottom: false,
609
+ all_separators: false
610
+ }
611
+ end
612
+
613
+ puts table
614
+
615
+ when 'use'
616
+ # whctl context use {name}
617
+ unless context_name
618
+ warn 'Error: context name is required'
619
+ warn 'Usage: whctl context use {name}'
620
+ exit 1
621
+ end
622
+
623
+ config = Walheim::Config.new(config_path: options[:whconfig])
624
+
625
+ begin
626
+ config.use_context(context_name)
627
+ config.save_config
628
+ puts "Switched to context '#{context_name}'"
629
+ rescue Walheim::Config::ConfigError => e
630
+ warn "Error: #{e.message}"
631
+ exit 1
632
+ end
633
+
634
+ when 'current'
635
+ # whctl context current
636
+ config = Walheim::Config.new(config_path: options[:whconfig])
637
+
638
+ unless Walheim::Config.exists?(config_path: options[:whconfig])
639
+ warn 'No Walheim configuration found.'
640
+ warn ''
641
+ warn 'Create your first context:'
642
+ warn ' whctl context new <name> --data-dir <path>'
643
+ exit 1
644
+ end
645
+
646
+ if config.current_context
647
+ puts "Current context: #{config.current_context}"
648
+ puts "Data directory: #{config.data_dir}"
649
+ else
650
+ puts 'No active context selected.'
651
+ puts ''
652
+ puts 'Available contexts:'
653
+ config.list_contexts.each do |ctx|
654
+ puts " - #{ctx['name']}"
655
+ end
656
+ puts ''
657
+ puts 'Select a context:'
658
+ puts ' whctl context use <context-name>'
659
+ end
660
+
661
+ when 'delete'
662
+ # whctl context delete {name}
663
+ unless context_name
664
+ warn 'Error: context name is required'
665
+ warn 'Usage: whctl context delete {name}'
666
+ exit 1
667
+ end
668
+
669
+ config = Walheim::Config.new(config_path: options[:whconfig])
670
+
671
+ begin
672
+ config.delete_context(context_name)
673
+ config.save_config
674
+ puts "Deleted context '#{context_name}'"
675
+
676
+ # Show warning if this was the active context
677
+ unless config.current_context
678
+ puts ''
679
+ puts 'Note: This was your active context.'
680
+ puts 'Select a new context with:'
681
+ puts ' whctl context use <context-name>'
682
+ end
683
+ rescue Walheim::Config::ConfigError => e
684
+ warn "Error: #{e.message}"
685
+ exit 1
686
+ end
687
+
688
+ else
689
+ warn "Error: unknown context subcommand '#{subcommand}'"
690
+ warn ''
691
+ warn 'Available context commands:'
692
+ warn ' whctl context new {name} --data-dir {path}'
693
+ warn ' whctl context list'
694
+ warn ' whctl context use {name}'
695
+ warn ' whctl context current'
696
+ warn ' whctl context delete {name}'
697
+ exit 1
698
+ end
699
+ else
700
+ warn "Error: unknown command '#{command}'"
701
+ warn 'Usage: whctl <command> [arguments]'
702
+ exit 1
703
+ end