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
@@ -0,0 +1,501 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# TuskLang Configuration SDK for Ruby
|
4
|
+
# Handles TOML-like TSK format with full FUJSEN (function serialization) support
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
require 'zlib'
|
8
|
+
require 'base64'
|
9
|
+
require 'securerandom'
|
10
|
+
require 'net/http'
|
11
|
+
require 'uri'
|
12
|
+
require 'date'
|
13
|
+
|
14
|
+
module TuskLang
|
15
|
+
# Main TSK class for parsing, generating, and executing TSK files
|
16
|
+
class TSK
|
17
|
+
attr_reader :data, :comments, :metadata
|
18
|
+
|
19
|
+
def initialize(data = {})
|
20
|
+
@data = data
|
21
|
+
@fujsen_cache = {}
|
22
|
+
@comments = {}
|
23
|
+
@metadata = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Create TSK instance from string content
|
27
|
+
def self.from_string(content)
|
28
|
+
parser = TSKParser.new
|
29
|
+
data, comments = parser.parse_with_comments(content)
|
30
|
+
tsk = new(data)
|
31
|
+
tsk.instance_variable_set(:@comments, comments)
|
32
|
+
tsk
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create TSK instance from file
|
36
|
+
def self.from_file(filepath)
|
37
|
+
content = File.read(filepath)
|
38
|
+
from_string(content)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get a section by name
|
42
|
+
def get_section(name)
|
43
|
+
@data[name]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get a value from a section
|
47
|
+
def get_value(section, key)
|
48
|
+
section_data = get_section(section)
|
49
|
+
section_data&.dig(key)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Set a section with values
|
53
|
+
def set_section(name, values)
|
54
|
+
@data[name] = values
|
55
|
+
end
|
56
|
+
|
57
|
+
# Set a value in a section
|
58
|
+
def set_value(section, key, value)
|
59
|
+
@data[section] ||= {}
|
60
|
+
@data[section][key] = value
|
61
|
+
end
|
62
|
+
|
63
|
+
# Convert TSK to string representation
|
64
|
+
def to_s
|
65
|
+
TSKParser.stringify(@data)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Convert TSK to hash
|
69
|
+
def to_hash
|
70
|
+
@data.dup
|
71
|
+
end
|
72
|
+
|
73
|
+
# Execute a FUJSEN function from a section
|
74
|
+
def execute_fujsen(section, key = 'fujsen', *args)
|
75
|
+
section_data = get_section(section)
|
76
|
+
raise ArgumentError, "Section '#{section}' not found" unless section_data
|
77
|
+
raise ArgumentError, "FUJSEN function '#{key}' not found in section '#{section}'" unless section_data.key?(key)
|
78
|
+
|
79
|
+
code = section_data[key].to_s
|
80
|
+
raise ArgumentError, "FUJSEN code is empty for '#{section}.#{key}'" if code.empty?
|
81
|
+
|
82
|
+
cache_key = "#{section}.#{key}"
|
83
|
+
unless @fujsen_cache.key?(cache_key)
|
84
|
+
@fujsen_cache[cache_key] = compile_fujsen(code)
|
85
|
+
end
|
86
|
+
|
87
|
+
@fujsen_cache[cache_key].call(*args)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Execute FUJSEN with context injection
|
91
|
+
def execute_fujsen_with_context(section, key, context, *args)
|
92
|
+
section_data = get_section(section)
|
93
|
+
raise ArgumentError, "Section '#{section}' not found" unless section_data
|
94
|
+
raise ArgumentError, "FUJSEN function '#{key}' not found in section '#{section}'" unless section_data.key?(key)
|
95
|
+
|
96
|
+
code = section_data[key].to_s
|
97
|
+
raise ArgumentError, "FUJSEN code is empty for '#{section}.#{key}'" if code.empty?
|
98
|
+
|
99
|
+
# Inject context into the function
|
100
|
+
context_code = inject_context_into_code(code, context)
|
101
|
+
|
102
|
+
cache_key = "#{section}.#{key}.#{context_hash(context)}"
|
103
|
+
unless @fujsen_cache.key?(cache_key)
|
104
|
+
@fujsen_cache[cache_key] = compile_fujsen(context_code)
|
105
|
+
end
|
106
|
+
|
107
|
+
@fujsen_cache[cache_key].call(*args)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Compile JavaScript FUJSEN code to Ruby proc
|
111
|
+
private def compile_fujsen(code)
|
112
|
+
# Convert JavaScript to Ruby-like code
|
113
|
+
ruby_code = convert_javascript_to_ruby(code)
|
114
|
+
|
115
|
+
# Create a safe execution environment
|
116
|
+
binding = create_safe_binding
|
117
|
+
|
118
|
+
# Compile and return proc
|
119
|
+
eval("proc { |*args| #{ruby_code} }", binding)
|
120
|
+
rescue => e
|
121
|
+
raise "FUJSEN compilation failed: #{e.message}"
|
122
|
+
end
|
123
|
+
|
124
|
+
# Convert JavaScript code to Ruby-like code
|
125
|
+
private def convert_javascript_to_ruby(js_code)
|
126
|
+
# Basic JavaScript to Ruby conversion
|
127
|
+
ruby_code = js_code
|
128
|
+
.gsub(/function\s+(\w+)\s*\(([^)]*)\)\s*\{/, 'lambda { |\2|')
|
129
|
+
.gsub(/=>/, '->')
|
130
|
+
.gsub(/throw new Error/, 'raise')
|
131
|
+
.gsub(/Date\.now\(\)/, 'Time.now.to_i * 1000')
|
132
|
+
.gsub(/new Date\(\)\.toISOString\(\)/, 'Time.now.iso8601')
|
133
|
+
.gsub(/console\.log/, 'puts')
|
134
|
+
.gsub(/\.length/, '.length')
|
135
|
+
.gsub(/\.forEach\(/, '.each { |')
|
136
|
+
.gsub(/\.map\(/, '.map { |')
|
137
|
+
.gsub(/\.filter\(/, '.select { |')
|
138
|
+
.gsub(/\.push\(/, '.push(')
|
139
|
+
.gsub(/Math\.max/, '[].max')
|
140
|
+
.gsub(/Math\.min/, '[].min')
|
141
|
+
.gsub(/Array\.isArray/, '->(obj) { obj.is_a?(Array) }')
|
142
|
+
.gsub(/typeof\s+(\w+)\s*===/, '\1.is_a?')
|
143
|
+
.gsub(/null/, 'nil')
|
144
|
+
.gsub(/true/, 'true')
|
145
|
+
.gsub(/false/, 'false')
|
146
|
+
|
147
|
+
# Handle function declarations
|
148
|
+
if ruby_code.match?(/lambda \{ \|/)
|
149
|
+
ruby_code = ruby_code.gsub(/\}\s*$/, '}')
|
150
|
+
else
|
151
|
+
# Convert arrow functions
|
152
|
+
ruby_code = ruby_code.gsub(/\(([^)]*)\)\s*=>\s*\{?([^}]*)\}?/, 'lambda { |\1| \2 }')
|
153
|
+
end
|
154
|
+
|
155
|
+
ruby_code
|
156
|
+
end
|
157
|
+
|
158
|
+
# Inject context variables into FUJSEN code
|
159
|
+
private def inject_context_into_code(code, context)
|
160
|
+
context_vars = context.map { |k, v| "#{k} = #{serialize_value(v)}" }.join("\n")
|
161
|
+
"#{context_vars}\n#{code}"
|
162
|
+
end
|
163
|
+
|
164
|
+
# Serialize value for Ruby context
|
165
|
+
private def serialize_value(value)
|
166
|
+
case value
|
167
|
+
when nil then 'nil'
|
168
|
+
when String then "\"#{value.gsub('"', '\\"')}\""
|
169
|
+
when Numeric then value.to_s
|
170
|
+
when true then 'true'
|
171
|
+
when false then 'false'
|
172
|
+
when Hash then "{#{value.map { |k, v| "\"#{k}\" => #{serialize_value(v)}" }.join(', ')}}"
|
173
|
+
when Array then "[#{value.map { |v| serialize_value(v) }.join(', ')}]"
|
174
|
+
else "\"#{value}\""
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Get hash for context caching
|
179
|
+
private def context_hash(context)
|
180
|
+
context_str = context.sort.map { |k, v| "#{k}=#{v}" }.join('|')
|
181
|
+
Base64.strict_encode64(context_str)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Get all FUJSEN functions from a section
|
185
|
+
def get_fujsen_map(section)
|
186
|
+
section_data = get_section(section)
|
187
|
+
return {} unless section_data
|
188
|
+
|
189
|
+
fujsen_map = {}
|
190
|
+
section_data.each do |key, value|
|
191
|
+
next unless value.is_a?(String) && !value.empty?
|
192
|
+
|
193
|
+
begin
|
194
|
+
compiled_function = compile_fujsen(value)
|
195
|
+
fujsen_map[key] = compiled_function
|
196
|
+
rescue
|
197
|
+
# Skip non-FUJSEN strings
|
198
|
+
end
|
199
|
+
end
|
200
|
+
fujsen_map
|
201
|
+
end
|
202
|
+
|
203
|
+
# Set a FUJSEN function in a section
|
204
|
+
def set_fujsen(section, key, function)
|
205
|
+
code = "# Function: #{function.class}\n# Parameters: #{function.parameters.map(&:last).join(', ')}"
|
206
|
+
set_value(section, key, code)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Execute FUJSEN @ operators
|
210
|
+
def execute_operators(value, context = {})
|
211
|
+
if value.is_a?(String) && value.include?('@')
|
212
|
+
execute_operator(value, context)
|
213
|
+
else
|
214
|
+
value
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Execute a single @ operator
|
219
|
+
private def execute_operator(expression, context)
|
220
|
+
operator_match = expression.match(/@(\w+)\s*\(([^)]*)\)/)
|
221
|
+
return expression unless operator_match
|
222
|
+
|
223
|
+
operator_name = operator_match[1].downcase
|
224
|
+
operator_args = operator_match[2]
|
225
|
+
|
226
|
+
case operator_name
|
227
|
+
when 'query' then execute_query(operator_args, context)
|
228
|
+
when 'cache' then execute_cache(operator_args, context)
|
229
|
+
when 'metrics' then execute_metrics(operator_args, context)
|
230
|
+
when 'if' then execute_if(operator_args, context)
|
231
|
+
when 'date' then execute_date(operator_args, context)
|
232
|
+
when 'env' then execute_env(operator_args, context)
|
233
|
+
when 'optimize' then execute_optimize(operator_args, context)
|
234
|
+
when 'learn' then execute_learn(operator_args, context)
|
235
|
+
when 'feature' then execute_feature(operator_args, context)
|
236
|
+
when 'json' then execute_json(operator_args, context)
|
237
|
+
when 'request' then execute_request(operator_args, context)
|
238
|
+
when 'file' then execute_file(operator_args, context)
|
239
|
+
when 'flex' then execute_flex(operator_args, context)
|
240
|
+
else expression
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# @ Operator implementations
|
245
|
+
private def execute_query(expression, context)
|
246
|
+
# Mock database query - in real implementation, connect to actual database
|
247
|
+
{
|
248
|
+
'results' => [],
|
249
|
+
'count' => 0,
|
250
|
+
'query' => expression
|
251
|
+
}
|
252
|
+
end
|
253
|
+
|
254
|
+
private def execute_cache(expression, context)
|
255
|
+
# Mock cache implementation
|
256
|
+
parts = expression.split(',')
|
257
|
+
ttl = parts.length > 0 ? parse_ttl(parts[0].strip) : 300.0
|
258
|
+
key = parts.length > 1 ? parts[1].strip.gsub(/^"|"$/, '') : 'default'
|
259
|
+
|
260
|
+
{
|
261
|
+
'cached' => true,
|
262
|
+
'ttl' => ttl,
|
263
|
+
'key' => key,
|
264
|
+
'value' => context['cache_value'] || 'default'
|
265
|
+
}
|
266
|
+
end
|
267
|
+
|
268
|
+
private def execute_metrics(expression, context)
|
269
|
+
parts = expression.split(',')
|
270
|
+
metric_name = parts[0].strip.gsub(/^"|"$/, '')
|
271
|
+
value = parts.length > 1 ? parts[1].strip.to_f : 0.0
|
272
|
+
|
273
|
+
{
|
274
|
+
'metric' => metric_name,
|
275
|
+
'value' => value,
|
276
|
+
'timestamp' => Time.now.to_i * 1000
|
277
|
+
}
|
278
|
+
end
|
279
|
+
|
280
|
+
private def execute_if(expression, context)
|
281
|
+
parts = expression.split(',')
|
282
|
+
return false if parts.length < 3
|
283
|
+
|
284
|
+
condition = parts[0].strip
|
285
|
+
true_value = parts[1].strip.gsub(/^"|"$/, '')
|
286
|
+
false_value = parts[2].strip.gsub(/^"|"$/, '')
|
287
|
+
|
288
|
+
# Simple condition evaluation
|
289
|
+
result = evaluate_condition(condition, context)
|
290
|
+
result ? true_value : false_value
|
291
|
+
end
|
292
|
+
|
293
|
+
private def execute_date(expression, context)
|
294
|
+
format = expression.empty? ? '%Y-%m-%d %H:%M:%S' : expression.strip.gsub(/^"|"$/, '')
|
295
|
+
Time.now.strftime(format)
|
296
|
+
end
|
297
|
+
|
298
|
+
private def execute_env(expression, context)
|
299
|
+
var_name = expression.strip.gsub(/^"|"$/, '')
|
300
|
+
ENV[var_name]
|
301
|
+
end
|
302
|
+
|
303
|
+
private def execute_optimize(expression, context)
|
304
|
+
{
|
305
|
+
'optimized' => true,
|
306
|
+
'parameter' => expression.strip.gsub(/^"|"$/, ''),
|
307
|
+
'value' => context['optimize_value'] || 'default'
|
308
|
+
}
|
309
|
+
end
|
310
|
+
|
311
|
+
private def execute_learn(expression, context)
|
312
|
+
{
|
313
|
+
'learned' => true,
|
314
|
+
'parameter' => expression.strip.gsub(/^"|"$/, ''),
|
315
|
+
'value' => context['learn_value'] || 'default'
|
316
|
+
}
|
317
|
+
end
|
318
|
+
|
319
|
+
private def execute_feature(expression, context)
|
320
|
+
feature_name = expression.strip.gsub(/^"|"$/, '')
|
321
|
+
# Mock feature detection
|
322
|
+
case feature_name
|
323
|
+
when 'redis' then true
|
324
|
+
when 'postgresql' then true
|
325
|
+
when 'sqlite' then true
|
326
|
+
when 'azure' then true
|
327
|
+
when 'unity' then true
|
328
|
+
when 'rails' then true
|
329
|
+
when 'jekyll' then true
|
330
|
+
else false
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
private def execute_json(expression, context)
|
335
|
+
context.to_json
|
336
|
+
rescue
|
337
|
+
'{}'
|
338
|
+
end
|
339
|
+
|
340
|
+
private def execute_request(expression, context)
|
341
|
+
# Mock HTTP request - in real implementation, use Net::HTTP
|
342
|
+
{
|
343
|
+
'status' => 200,
|
344
|
+
'data' => {},
|
345
|
+
'url' => expression.strip.gsub(/^"|"$/, '')
|
346
|
+
}
|
347
|
+
end
|
348
|
+
|
349
|
+
private def execute_file(expression, context)
|
350
|
+
filepath = expression.strip.gsub(/^"|"$/, '')
|
351
|
+
File.read(filepath)
|
352
|
+
rescue
|
353
|
+
nil
|
354
|
+
end
|
355
|
+
|
356
|
+
private def execute_flex(expression, context)
|
357
|
+
# FlexChain integration
|
358
|
+
parts = expression.split(',')
|
359
|
+
operation = parts[0].strip.gsub(/^"|"$/, '')
|
360
|
+
|
361
|
+
case operation
|
362
|
+
when 'balance' then get_flex_balance(parts.length > 1 ? parts[1].strip.gsub(/^"|"$/, '') : '')
|
363
|
+
when 'transfer' then execute_flex_transfer(parts[1..-1])
|
364
|
+
when 'stake' then execute_flex_stake(parts[1..-1])
|
365
|
+
when 'unstake' then execute_flex_unstake(parts[1..-1])
|
366
|
+
when 'reward' then get_flex_reward(parts.length > 1 ? parts[1].strip.gsub(/^"|"$/, '') : '')
|
367
|
+
when 'status' then get_flex_status
|
368
|
+
when 'delegate' then execute_flex_delegate(parts[1..-1])
|
369
|
+
when 'vote' then execute_flex_vote(parts[1..-1])
|
370
|
+
else { 'error' => "Unknown Flex operation: #{operation}" }
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# FlexChain operation implementations
|
375
|
+
private def get_flex_balance(address)
|
376
|
+
{
|
377
|
+
'address' => address,
|
378
|
+
'balance' => 1000.0,
|
379
|
+
'currency' => 'FLEX'
|
380
|
+
}
|
381
|
+
end
|
382
|
+
|
383
|
+
private def execute_flex_transfer(args)
|
384
|
+
{
|
385
|
+
'success' => true,
|
386
|
+
'transaction_id' => "tx_#{Time.now.to_i * 1000}",
|
387
|
+
'amount' => args.length > 0 ? args[0].to_f : 0.0,
|
388
|
+
'to' => args.length > 1 ? args[1].strip.gsub(/^"|"$/, '') : '',
|
389
|
+
'from' => args.length > 2 ? args[2].strip.gsub(/^"|"$/, '') : ''
|
390
|
+
}
|
391
|
+
end
|
392
|
+
|
393
|
+
private def execute_flex_stake(args)
|
394
|
+
{
|
395
|
+
'success' => true,
|
396
|
+
'staked_amount' => args.length > 0 ? args[0].to_f : 0.0,
|
397
|
+
'validator' => args.length > 1 ? args[1].strip.gsub(/^"|"$/, '') : ''
|
398
|
+
}
|
399
|
+
end
|
400
|
+
|
401
|
+
private def execute_flex_unstake(args)
|
402
|
+
{
|
403
|
+
'success' => true,
|
404
|
+
'unstaked_amount' => args.length > 0 ? args[0].to_f : 0.0,
|
405
|
+
'validator' => args.length > 1 ? args[1].strip.gsub(/^"|"$/, '') : ''
|
406
|
+
}
|
407
|
+
end
|
408
|
+
|
409
|
+
private def get_flex_reward(address)
|
410
|
+
{
|
411
|
+
'address' => address,
|
412
|
+
'reward' => 50.0,
|
413
|
+
'currency' => 'FLEX'
|
414
|
+
}
|
415
|
+
end
|
416
|
+
|
417
|
+
private def get_flex_status
|
418
|
+
{
|
419
|
+
'network' => 'FlexChain',
|
420
|
+
'status' => 'active',
|
421
|
+
'block_height' => 12345,
|
422
|
+
'validators' => 10,
|
423
|
+
'total_staked' => 1000000.0
|
424
|
+
}
|
425
|
+
end
|
426
|
+
|
427
|
+
private def execute_flex_delegate(args)
|
428
|
+
{
|
429
|
+
'success' => true,
|
430
|
+
'delegated_amount' => args.length > 0 ? args[0].to_f : 0.0,
|
431
|
+
'validator' => args.length > 1 ? args[1].strip.gsub(/^"|"$/, '') : '',
|
432
|
+
'delegator' => args.length > 2 ? args[2].strip.gsub(/^"|"$/, '') : ''
|
433
|
+
}
|
434
|
+
end
|
435
|
+
|
436
|
+
private def execute_flex_vote(args)
|
437
|
+
{
|
438
|
+
'success' => true,
|
439
|
+
'proposal' => args.length > 0 ? args[0].strip.gsub(/^"|"$/, '') : '',
|
440
|
+
'vote' => args.length > 1 ? args[1].strip.gsub(/^"|"$/, '') : '',
|
441
|
+
'voter' => args.length > 2 ? args[2].strip.gsub(/^"|"$/, '') : ''
|
442
|
+
}
|
443
|
+
end
|
444
|
+
|
445
|
+
# Helper methods
|
446
|
+
private def parse_ttl(ttl)
|
447
|
+
return ttl.to_f if ttl.match?(/^\d+$/)
|
448
|
+
|
449
|
+
match = ttl.match(/^(\d+)([smhd])$/)
|
450
|
+
return 300.0 unless match
|
451
|
+
|
452
|
+
value = match[1].to_f
|
453
|
+
unit = match[2]
|
454
|
+
|
455
|
+
case unit
|
456
|
+
when 's' then value
|
457
|
+
when 'm' then value * 60
|
458
|
+
when 'h' then value * 3600
|
459
|
+
when 'd' then value * 86400
|
460
|
+
else 300.0
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
private def evaluate_condition(condition, context)
|
465
|
+
if condition.include?('==')
|
466
|
+
parts = condition.split('==')
|
467
|
+
left = parts[0].strip.gsub(/^"|"$/, '')
|
468
|
+
right = parts[1].strip.gsub(/^"|"$/, '')
|
469
|
+
(context[left] || '').to_s == right
|
470
|
+
elsif condition.include?('!=')
|
471
|
+
parts = condition.split('!=')
|
472
|
+
left = parts[0].strip.gsub(/^"|"$/, '')
|
473
|
+
right = parts[1].strip.gsub(/^"|"$/, '')
|
474
|
+
(context[left] || '').to_s != right
|
475
|
+
else
|
476
|
+
false
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# Create a safe binding for FUJSEN execution
|
481
|
+
private def create_safe_binding
|
482
|
+
binding
|
483
|
+
end
|
484
|
+
|
485
|
+
# Store data with Shell format
|
486
|
+
def store_with_shell(data, metadata = nil)
|
487
|
+
shell_data = ShellStorage.create_shell_data(data, SecureRandom.uuid, metadata)
|
488
|
+
ShellStorage.pack(shell_data)
|
489
|
+
end
|
490
|
+
|
491
|
+
# Retrieve data from Shell format
|
492
|
+
def retrieve_from_shell(shell_data)
|
493
|
+
ShellStorage.unpack(shell_data)
|
494
|
+
end
|
495
|
+
|
496
|
+
# Detect type of Shell data
|
497
|
+
def detect_type(shell_data)
|
498
|
+
ShellStorage.detect_type(shell_data)
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|