tusktsk 2.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE +14 -0
- data/README.md +759 -0
- data/cli/main.rb +1488 -0
- data/exe/tsk +10 -0
- data/lib/peanut_config.rb +621 -0
- data/lib/tusk/license.rb +303 -0
- data/lib/tusk/protection.rb +180 -0
- data/lib/tusk_lang/shell_storage.rb +104 -0
- data/lib/tusk_lang/tsk.rb +501 -0
- data/lib/tusk_lang/tsk_parser.rb +234 -0
- data/lib/tusk_lang/tsk_parser_enhanced.rb +563 -0
- data/lib/tusk_lang/version.rb +5 -0
- data/lib/tusk_lang.rb +14 -0
- metadata +249 -0
data/exe/tsk
ADDED
@@ -0,0 +1,621 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'json'
|
5
|
+
require 'pathname'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
##
|
9
|
+
# PeanutConfig - Hierarchical configuration with binary compilation
|
10
|
+
# Part of TuskLang Ruby SDK
|
11
|
+
#
|
12
|
+
# Features:
|
13
|
+
# - CSS-like inheritance with directory hierarchy
|
14
|
+
# - Binary compilation for 85% performance boost
|
15
|
+
# - Auto-compilation on change
|
16
|
+
# - Ruby Marshal serialization for speed
|
17
|
+
# - Thread-safe caching
|
18
|
+
# - File watching with automatic cache invalidation
|
19
|
+
#
|
20
|
+
# @example Basic usage
|
21
|
+
# config = PeanutConfig.new
|
22
|
+
# settings = config.load('/path/to/project')
|
23
|
+
# host = config.get('server.host', 'localhost')
|
24
|
+
#
|
25
|
+
# @example Custom options
|
26
|
+
# config = PeanutConfig.new(auto_compile: false, watch: false)
|
27
|
+
#
|
28
|
+
class PeanutConfig
|
29
|
+
# Binary format constants
|
30
|
+
MAGIC = 'PNUT'
|
31
|
+
VERSION = 1
|
32
|
+
HEADER_SIZE = 16
|
33
|
+
CHECKSUM_SIZE = 8
|
34
|
+
|
35
|
+
# Configuration file types
|
36
|
+
CONFIG_TYPES = {
|
37
|
+
binary: 'pnt',
|
38
|
+
tsk: 'tsk',
|
39
|
+
text: 'peanuts'
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
##
|
43
|
+
# Represents a configuration file in the hierarchy
|
44
|
+
#
|
45
|
+
ConfigFile = Struct.new(:path, :type, :mtime, keyword_init: true) do
|
46
|
+
def to_s
|
47
|
+
"#{path} (#{type}, #{mtime})"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_accessor :auto_compile, :watch
|
52
|
+
|
53
|
+
##
|
54
|
+
# Initialize a new PeanutConfig instance
|
55
|
+
#
|
56
|
+
# @param auto_compile [Boolean] automatically compile text configs to binary
|
57
|
+
# @param watch [Boolean] watch config files for changes
|
58
|
+
#
|
59
|
+
def initialize(auto_compile: true, watch: true)
|
60
|
+
@cache = {}
|
61
|
+
@cache_mutex = Mutex.new
|
62
|
+
@watchers = {}
|
63
|
+
@auto_compile = auto_compile
|
64
|
+
@watch = watch
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Find configuration files in directory hierarchy
|
69
|
+
#
|
70
|
+
# @param start_dir [String, Pathname] starting directory
|
71
|
+
# @return [Array<ConfigFile>] configuration files from root to current
|
72
|
+
#
|
73
|
+
def find_config_hierarchy(start_dir)
|
74
|
+
configs = []
|
75
|
+
current_dir = Pathname.new(start_dir).expand_path
|
76
|
+
|
77
|
+
# Walk up directory tree
|
78
|
+
until current_dir.root?
|
79
|
+
# Check for config files in priority order
|
80
|
+
binary_path = current_dir / 'peanu.pnt'
|
81
|
+
tsk_path = current_dir / 'peanu.tsk'
|
82
|
+
text_path = current_dir / 'peanu.peanuts'
|
83
|
+
|
84
|
+
if binary_path.exist?
|
85
|
+
configs << ConfigFile.new(
|
86
|
+
path: binary_path.to_s,
|
87
|
+
type: :binary,
|
88
|
+
mtime: binary_path.mtime
|
89
|
+
)
|
90
|
+
elsif tsk_path.exist?
|
91
|
+
configs << ConfigFile.new(
|
92
|
+
path: tsk_path.to_s,
|
93
|
+
type: :tsk,
|
94
|
+
mtime: tsk_path.mtime
|
95
|
+
)
|
96
|
+
elsif text_path.exist?
|
97
|
+
configs << ConfigFile.new(
|
98
|
+
path: text_path.to_s,
|
99
|
+
type: :text,
|
100
|
+
mtime: text_path.mtime
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
current_dir = current_dir.parent
|
105
|
+
end
|
106
|
+
|
107
|
+
# Check for global peanut.tsk
|
108
|
+
global_config = Pathname.new('peanut.tsk')
|
109
|
+
if global_config.exist?
|
110
|
+
configs.unshift(ConfigFile.new(
|
111
|
+
path: global_config.to_s,
|
112
|
+
type: :tsk,
|
113
|
+
mtime: global_config.mtime
|
114
|
+
))
|
115
|
+
end
|
116
|
+
|
117
|
+
# Reverse to get root->current order
|
118
|
+
configs.reverse
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Parse text-based peanut configuration
|
123
|
+
#
|
124
|
+
# @param content [String] configuration file content
|
125
|
+
# @return [Hash] parsed configuration
|
126
|
+
#
|
127
|
+
def parse_text_config(content)
|
128
|
+
config = {}
|
129
|
+
current_section = config
|
130
|
+
current_section_name = nil
|
131
|
+
|
132
|
+
content.each_line do |line|
|
133
|
+
line = line.strip
|
134
|
+
|
135
|
+
# Skip comments and empty lines
|
136
|
+
next if line.empty? || line.start_with?('#')
|
137
|
+
|
138
|
+
# Section header
|
139
|
+
if line.match?(/^\[.*\]$/)
|
140
|
+
section_name = line[1..-2]
|
141
|
+
current_section = {}
|
142
|
+
config[section_name] = current_section
|
143
|
+
current_section_name = section_name
|
144
|
+
next
|
145
|
+
end
|
146
|
+
|
147
|
+
# Key-value pair
|
148
|
+
if (match = line.match(/^([^:]+):\s*(.*)$/))
|
149
|
+
key = match[1].strip
|
150
|
+
value = match[2].strip
|
151
|
+
current_section[key] = parse_value(value)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
config
|
156
|
+
end
|
157
|
+
|
158
|
+
##
|
159
|
+
# Parse a value with type inference
|
160
|
+
#
|
161
|
+
# @param value [String] string value to parse
|
162
|
+
# @return [Object] parsed value (String, Integer, Float, Boolean, Array, nil)
|
163
|
+
#
|
164
|
+
def parse_value(value)
|
165
|
+
# Remove quotes
|
166
|
+
if (value.start_with?('"') && value.end_with?('"')) ||
|
167
|
+
(value.start_with?("'") && value.end_with?("'"))
|
168
|
+
return value[1..-2]
|
169
|
+
end
|
170
|
+
|
171
|
+
# Boolean
|
172
|
+
case value.downcase
|
173
|
+
when 'true'
|
174
|
+
return true
|
175
|
+
when 'false'
|
176
|
+
return false
|
177
|
+
when 'null'
|
178
|
+
return nil
|
179
|
+
end
|
180
|
+
|
181
|
+
# Number
|
182
|
+
if value.match?(/^\d+$/)
|
183
|
+
return Integer(value)
|
184
|
+
elsif value.match?(/^\d*\.\d+$/)
|
185
|
+
return Float(value)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Array (simple comma-separated)
|
189
|
+
if value.include?(',')
|
190
|
+
return value.split(',').map { |v| parse_value(v.strip) }
|
191
|
+
end
|
192
|
+
|
193
|
+
value
|
194
|
+
end
|
195
|
+
|
196
|
+
##
|
197
|
+
# Compile configuration to binary format
|
198
|
+
#
|
199
|
+
# @param config [Hash] configuration to compile
|
200
|
+
# @param output_path [String, Pathname] output file path
|
201
|
+
#
|
202
|
+
def compile_to_binary(config, output_path)
|
203
|
+
output_path = Pathname.new(output_path)
|
204
|
+
|
205
|
+
File.open(output_path, 'wb') do |file|
|
206
|
+
# Write header
|
207
|
+
file.write(MAGIC)
|
208
|
+
file.write([VERSION].pack('L<'))
|
209
|
+
file.write([Time.now.to_i].pack('Q<'))
|
210
|
+
|
211
|
+
# Serialize config with Marshal (Ruby's native binary format)
|
212
|
+
config_data = Marshal.dump(config)
|
213
|
+
|
214
|
+
# Create checksum
|
215
|
+
checksum = Digest::SHA256.digest(config_data)[0, CHECKSUM_SIZE]
|
216
|
+
file.write(checksum)
|
217
|
+
|
218
|
+
# Write config data
|
219
|
+
file.write(config_data)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Also create intermediate .shell format (JSON)
|
223
|
+
shell_path = output_path.sub_ext('.shell')
|
224
|
+
compile_to_shell(config, shell_path)
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# Compile to intermediate shell format (70% faster than text)
|
229
|
+
#
|
230
|
+
# @param config [Hash] configuration to compile
|
231
|
+
# @param output_path [String, Pathname] output file path
|
232
|
+
#
|
233
|
+
def compile_to_shell(config, output_path)
|
234
|
+
shell_data = {
|
235
|
+
version: VERSION,
|
236
|
+
timestamp: Time.now.to_i,
|
237
|
+
data: config
|
238
|
+
}
|
239
|
+
|
240
|
+
File.write(output_path, JSON.pretty_generate(shell_data))
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# Load binary configuration
|
245
|
+
#
|
246
|
+
# @param file_path [String, Pathname] path to binary file
|
247
|
+
# @return [Hash] loaded configuration
|
248
|
+
# @raise [StandardError] if file is invalid or corrupted
|
249
|
+
#
|
250
|
+
def load_binary(file_path)
|
251
|
+
data = File.binread(file_path)
|
252
|
+
|
253
|
+
raise 'Binary file too short' if data.bytesize < HEADER_SIZE + CHECKSUM_SIZE
|
254
|
+
|
255
|
+
# Verify magic number
|
256
|
+
magic = data[0, 4]
|
257
|
+
raise 'Invalid peanut binary file' unless magic == MAGIC
|
258
|
+
|
259
|
+
# Check version
|
260
|
+
version = data[4, 4].unpack1('L<')
|
261
|
+
raise "Unsupported binary version: #{version}" if version > VERSION
|
262
|
+
|
263
|
+
# Verify checksum
|
264
|
+
stored_checksum = data[HEADER_SIZE, CHECKSUM_SIZE]
|
265
|
+
config_data = data[HEADER_SIZE + CHECKSUM_SIZE..-1]
|
266
|
+
|
267
|
+
calculated_checksum = Digest::SHA256.digest(config_data)[0, CHECKSUM_SIZE]
|
268
|
+
unless stored_checksum == calculated_checksum
|
269
|
+
raise 'Binary file corrupted (checksum mismatch)'
|
270
|
+
end
|
271
|
+
|
272
|
+
# Deserialize configuration
|
273
|
+
Marshal.load(config_data)
|
274
|
+
rescue ArgumentError, TypeError => e
|
275
|
+
raise "Failed to deserialize binary config: #{e.message}"
|
276
|
+
end
|
277
|
+
|
278
|
+
##
|
279
|
+
# Deep merge configurations (CSS-like cascading)
|
280
|
+
#
|
281
|
+
# @param target [Hash] target configuration
|
282
|
+
# @param source [Hash] source configuration
|
283
|
+
# @return [Hash] merged configuration
|
284
|
+
#
|
285
|
+
def deep_merge(target, source)
|
286
|
+
result = target.dup
|
287
|
+
|
288
|
+
source.each do |key, value|
|
289
|
+
if result[key].is_a?(Hash) && value.is_a?(Hash)
|
290
|
+
# Merge nested hashes
|
291
|
+
result[key] = deep_merge(result[key], value)
|
292
|
+
else
|
293
|
+
# Override with source value
|
294
|
+
result[key] = value
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
result
|
299
|
+
end
|
300
|
+
|
301
|
+
##
|
302
|
+
# Load configuration with inheritance
|
303
|
+
#
|
304
|
+
# @param directory [String, Pathname] directory to load from (default: current)
|
305
|
+
# @return [Hash] merged configuration
|
306
|
+
#
|
307
|
+
def load(directory = '.')
|
308
|
+
abs_dir = Pathname.new(directory).expand_path.to_s
|
309
|
+
|
310
|
+
# Check cache
|
311
|
+
@cache_mutex.synchronize do
|
312
|
+
return @cache[abs_dir].dup if @cache.key?(abs_dir)
|
313
|
+
end
|
314
|
+
|
315
|
+
hierarchy = find_config_hierarchy(directory)
|
316
|
+
merged_config = {}
|
317
|
+
|
318
|
+
# Load and merge configs from root to current
|
319
|
+
hierarchy.each do |config_file|
|
320
|
+
begin
|
321
|
+
config = case config_file.type
|
322
|
+
when :binary
|
323
|
+
load_binary(config_file.path)
|
324
|
+
when :tsk, :text
|
325
|
+
content = File.read(config_file.path)
|
326
|
+
parse_text_config(content)
|
327
|
+
else
|
328
|
+
{}
|
329
|
+
end
|
330
|
+
|
331
|
+
# Merge with CSS-like cascading
|
332
|
+
merged_config = deep_merge(merged_config, config)
|
333
|
+
|
334
|
+
# Set up file watching
|
335
|
+
watch_config(config_file.path, abs_dir) if @watch
|
336
|
+
rescue => e
|
337
|
+
warn "Error loading config file #{config_file.path}: #{e.message}"
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# Cache the result
|
342
|
+
@cache_mutex.synchronize do
|
343
|
+
@cache[abs_dir] = merged_config.dup
|
344
|
+
end
|
345
|
+
|
346
|
+
# Auto-compile if enabled
|
347
|
+
auto_compile_configs(hierarchy) if @auto_compile
|
348
|
+
|
349
|
+
merged_config
|
350
|
+
end
|
351
|
+
|
352
|
+
##
|
353
|
+
# Get configuration value by dot-separated path
|
354
|
+
#
|
355
|
+
# @param key_path [String] dot-separated key path (e.g., 'server.host')
|
356
|
+
# @param default_value [Object] default value if key not found
|
357
|
+
# @param directory [String, Pathname] directory to load from
|
358
|
+
# @return [Object] configuration value or default
|
359
|
+
#
|
360
|
+
def get(key_path, default_value = nil, directory = '.')
|
361
|
+
config = load(directory)
|
362
|
+
keys = key_path.split('.')
|
363
|
+
current = config
|
364
|
+
|
365
|
+
keys.each do |key|
|
366
|
+
if current.is_a?(Hash) && current.key?(key)
|
367
|
+
current = current[key]
|
368
|
+
else
|
369
|
+
return default_value
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
current
|
374
|
+
rescue => e
|
375
|
+
warn "Error getting config value #{key_path}: #{e.message}"
|
376
|
+
default_value
|
377
|
+
end
|
378
|
+
|
379
|
+
##
|
380
|
+
# Clear configuration cache
|
381
|
+
#
|
382
|
+
def clear_cache
|
383
|
+
@cache_mutex.synchronize do
|
384
|
+
@cache.clear
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
##
|
389
|
+
# Benchmark performance comparison
|
390
|
+
#
|
391
|
+
# @return [Hash] benchmark results
|
392
|
+
#
|
393
|
+
def self.benchmark
|
394
|
+
config = new
|
395
|
+
test_content = <<~CONFIG
|
396
|
+
[server]
|
397
|
+
host: "localhost"
|
398
|
+
port: 8080
|
399
|
+
workers: 4
|
400
|
+
debug: true
|
401
|
+
|
402
|
+
[database]
|
403
|
+
driver: "postgresql"
|
404
|
+
host: "db.example.com"
|
405
|
+
port: 5432
|
406
|
+
pool_size: 10
|
407
|
+
|
408
|
+
[cache]
|
409
|
+
enabled: true
|
410
|
+
ttl: 3600
|
411
|
+
backend: "redis"
|
412
|
+
CONFIG
|
413
|
+
|
414
|
+
puts "🥜 Peanut Configuration Performance Test\n"
|
415
|
+
|
416
|
+
# Test text parsing
|
417
|
+
start_time = Time.now
|
418
|
+
1000.times { config.parse_text_config(test_content) }
|
419
|
+
text_time = Time.now - start_time
|
420
|
+
puts "Text parsing (1000 iterations): #{(text_time * 1000).round(2)}ms"
|
421
|
+
|
422
|
+
# Test binary loading
|
423
|
+
parsed = config.parse_text_config(test_content)
|
424
|
+
binary_data = Marshal.dump(parsed)
|
425
|
+
|
426
|
+
start_time = Time.now
|
427
|
+
1000.times { Marshal.load(binary_data) }
|
428
|
+
binary_time = Time.now - start_time
|
429
|
+
puts "Binary loading (1000 iterations): #{(binary_time * 1000).round(2)}ms"
|
430
|
+
|
431
|
+
improvement = ((text_time - binary_time) / text_time * 100).round
|
432
|
+
puts "\n✨ Binary format is #{improvement}% faster than text parsing!"
|
433
|
+
|
434
|
+
{
|
435
|
+
text_time: text_time,
|
436
|
+
binary_time: binary_time,
|
437
|
+
improvement: improvement
|
438
|
+
}
|
439
|
+
end
|
440
|
+
|
441
|
+
private
|
442
|
+
|
443
|
+
##
|
444
|
+
# Watch configuration file for changes
|
445
|
+
#
|
446
|
+
# @param file_path [String] path to watch
|
447
|
+
# @param directory [String] associated directory for cache invalidation
|
448
|
+
#
|
449
|
+
def watch_config(file_path, directory)
|
450
|
+
return if @watchers.key?(file_path)
|
451
|
+
|
452
|
+
# Simple file watching using mtime checking
|
453
|
+
# In production, you might want to use a gem like 'listen' for better performance
|
454
|
+
thread = Thread.new do
|
455
|
+
last_mtime = File.mtime(file_path)
|
456
|
+
|
457
|
+
loop do
|
458
|
+
sleep 1
|
459
|
+
begin
|
460
|
+
current_mtime = File.mtime(file_path)
|
461
|
+
if current_mtime > last_mtime
|
462
|
+
@cache_mutex.synchronize do
|
463
|
+
@cache.delete(directory)
|
464
|
+
end
|
465
|
+
puts "Configuration changed: #{file_path}"
|
466
|
+
last_mtime = current_mtime
|
467
|
+
end
|
468
|
+
rescue Errno::ENOENT
|
469
|
+
# File was deleted
|
470
|
+
@cache_mutex.synchronize do
|
471
|
+
@cache.delete(directory)
|
472
|
+
end
|
473
|
+
break
|
474
|
+
rescue => e
|
475
|
+
warn "Error watching file #{file_path}: #{e.message}"
|
476
|
+
break
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
thread.priority = -1 # Lower priority for file watching
|
482
|
+
@watchers[file_path] = thread
|
483
|
+
end
|
484
|
+
|
485
|
+
##
|
486
|
+
# Auto-compile text configs to binary
|
487
|
+
#
|
488
|
+
# @param hierarchy [Array<ConfigFile>] configuration file hierarchy
|
489
|
+
#
|
490
|
+
def auto_compile_configs(hierarchy)
|
491
|
+
hierarchy.each do |config_file|
|
492
|
+
next unless [:text, :tsk].include?(config_file.type)
|
493
|
+
|
494
|
+
binary_path = config_file.path.gsub(/\.(peanuts|tsk)$/, '.pnt')
|
495
|
+
|
496
|
+
# Check if binary is outdated
|
497
|
+
need_compile = !File.exist?(binary_path) ||
|
498
|
+
File.mtime(binary_path) < config_file.mtime
|
499
|
+
|
500
|
+
if need_compile
|
501
|
+
begin
|
502
|
+
content = File.read(config_file.path)
|
503
|
+
config = parse_text_config(content)
|
504
|
+
compile_to_binary(config, binary_path)
|
505
|
+
puts "Compiled #{File.basename(config_file.path)} to binary format"
|
506
|
+
rescue => e
|
507
|
+
warn "Failed to compile #{config_file.path}: #{e.message}"
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
##
|
515
|
+
# CLI functionality for standalone usage
|
516
|
+
#
|
517
|
+
class PeanutConfig::CLI
|
518
|
+
def self.run(args)
|
519
|
+
if args.empty?
|
520
|
+
show_usage
|
521
|
+
return
|
522
|
+
end
|
523
|
+
|
524
|
+
command = args[0]
|
525
|
+
config = PeanutConfig.new
|
526
|
+
|
527
|
+
case command
|
528
|
+
when 'compile'
|
529
|
+
if args.length < 2
|
530
|
+
puts 'Error: Please specify input file'
|
531
|
+
exit 1
|
532
|
+
end
|
533
|
+
compile_file(config, args[1])
|
534
|
+
|
535
|
+
when 'load'
|
536
|
+
directory = args[1] || '.'
|
537
|
+
load_config(config, directory)
|
538
|
+
|
539
|
+
when 'benchmark'
|
540
|
+
PeanutConfig.benchmark
|
541
|
+
|
542
|
+
when 'hierarchy'
|
543
|
+
directory = args[1] || '.'
|
544
|
+
show_hierarchy(config, directory)
|
545
|
+
|
546
|
+
else
|
547
|
+
puts "Unknown command: #{command}"
|
548
|
+
show_usage
|
549
|
+
exit 1
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
def self.show_usage
|
554
|
+
puts <<~USAGE
|
555
|
+
🥜 PeanutConfig - TuskLang Hierarchical Configuration
|
556
|
+
|
557
|
+
Commands:
|
558
|
+
compile <file> Compile .peanuts or .tsk to binary .pnt
|
559
|
+
load [dir] Load configuration hierarchy
|
560
|
+
hierarchy [dir] Show config file hierarchy
|
561
|
+
benchmark Run performance benchmark
|
562
|
+
|
563
|
+
Example:
|
564
|
+
ruby peanut_config.rb compile config.peanuts
|
565
|
+
ruby peanut_config.rb load /path/to/project
|
566
|
+
USAGE
|
567
|
+
end
|
568
|
+
|
569
|
+
def self.compile_file(config, input_file)
|
570
|
+
unless File.exist?(input_file)
|
571
|
+
puts "Error: File #{input_file} not found"
|
572
|
+
exit 1
|
573
|
+
end
|
574
|
+
|
575
|
+
content = File.read(input_file)
|
576
|
+
parsed = config.parse_text_config(content)
|
577
|
+
|
578
|
+
# Generate output filename
|
579
|
+
output_file = case File.extname(input_file)
|
580
|
+
when '.peanuts', '.tsk'
|
581
|
+
input_file.gsub(/\.(peanuts|tsk)$/, '.pnt')
|
582
|
+
else
|
583
|
+
input_file + '.pnt'
|
584
|
+
end
|
585
|
+
|
586
|
+
config.compile_to_binary(parsed, output_file)
|
587
|
+
puts "✅ Compiled to #{output_file}"
|
588
|
+
rescue => e
|
589
|
+
puts "Error: #{e.message}"
|
590
|
+
exit 1
|
591
|
+
end
|
592
|
+
|
593
|
+
def self.load_config(config, directory)
|
594
|
+
loaded = config.load(directory)
|
595
|
+
puts JSON.pretty_generate(loaded)
|
596
|
+
rescue => e
|
597
|
+
puts "Error: #{e.message}"
|
598
|
+
exit 1
|
599
|
+
end
|
600
|
+
|
601
|
+
def self.show_hierarchy(config, directory)
|
602
|
+
hierarchy = config.find_config_hierarchy(directory)
|
603
|
+
|
604
|
+
puts "Configuration hierarchy for #{directory}:\n\n"
|
605
|
+
hierarchy.each_with_index do |config_file, index|
|
606
|
+
puts "#{index + 1}. #{config_file.path} (#{config_file.type})"
|
607
|
+
puts " Modified: #{config_file.mtime}"
|
608
|
+
puts
|
609
|
+
end
|
610
|
+
|
611
|
+
puts 'No configuration files found' if hierarchy.empty?
|
612
|
+
rescue => e
|
613
|
+
puts "Error: #{e.message}"
|
614
|
+
exit 1
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
# Run CLI if this file is executed directly
|
619
|
+
if __FILE__ == $PROGRAM_NAME
|
620
|
+
PeanutConfig::CLI.run(ARGV)
|
621
|
+
end
|