zillabyte-cli 0.0.9

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.
Files changed (53) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +20 -0
  4. data/README.md +29 -0
  5. data/bin/zillabyte +18 -0
  6. data/lib/zillabyte/api/base.rb +11 -0
  7. data/lib/zillabyte/api/data.rb +126 -0
  8. data/lib/zillabyte/api/flows.rb +239 -0
  9. data/lib/zillabyte/api/locks.rb +4 -0
  10. data/lib/zillabyte/api/logs.rb +32 -0
  11. data/lib/zillabyte/api/metrics.rb +48 -0
  12. data/lib/zillabyte/api/queries.rb +58 -0
  13. data/lib/zillabyte/api/semantic_tags.rb +24 -0
  14. data/lib/zillabyte/api/settings.rb +8 -0
  15. data/lib/zillabyte/api/sources.rb +28 -0
  16. data/lib/zillabyte/api/zillalogs.rb +66 -0
  17. data/lib/zillabyte/api.rb +170 -0
  18. data/lib/zillabyte/auth.rb +155 -0
  19. data/lib/zillabyte/cli/auth.rb +33 -0
  20. data/lib/zillabyte/cli/base.rb +161 -0
  21. data/lib/zillabyte/cli/config.rb +63 -0
  22. data/lib/zillabyte/cli/counters.rb +61 -0
  23. data/lib/zillabyte/cli/flows.rb +702 -0
  24. data/lib/zillabyte/cli/help.rb +137 -0
  25. data/lib/zillabyte/cli/helpers/data_schema_builder.rb +3 -0
  26. data/lib/zillabyte/cli/host.rb +21 -0
  27. data/lib/zillabyte/cli/logs.rb +118 -0
  28. data/lib/zillabyte/cli/query.rb +172 -0
  29. data/lib/zillabyte/cli/relations.rb +326 -0
  30. data/lib/zillabyte/cli/sources.rb +37 -0
  31. data/lib/zillabyte/cli/templates/js/simple_function.js +33 -0
  32. data/lib/zillabyte/cli/templates/js/zillabyte.conf.yaml +2 -0
  33. data/lib/zillabyte/cli/templates/python/requirements.txt +7 -0
  34. data/lib/zillabyte/cli/templates/python/simple_function.py +27 -0
  35. data/lib/zillabyte/cli/templates/python/zillabyte.conf.yaml +4 -0
  36. data/lib/zillabyte/cli/templates/ruby/Gemfile +3 -0
  37. data/lib/zillabyte/cli/templates/ruby/simple_function.rb +36 -0
  38. data/lib/zillabyte/cli/templates/ruby/zillabyte.conf.yaml +2 -0
  39. data/lib/zillabyte/cli/version.rb +11 -0
  40. data/lib/zillabyte/cli/zillalogs.rb +86 -0
  41. data/lib/zillabyte/cli.rb +37 -0
  42. data/lib/zillabyte/command.rb +254 -0
  43. data/lib/zillabyte/common/progress.rb +17 -0
  44. data/lib/zillabyte/common/tar.rb +102 -0
  45. data/lib/zillabyte/common.rb +13 -0
  46. data/lib/zillabyte/helpers.rb +49 -0
  47. data/lib/zillabyte/queries.rb +39 -0
  48. data/lib/zillabyte-cli/version.rb +5 -0
  49. data/lib/zillabyte-cli.rb +5 -0
  50. data/zillabyte-cli.gemspec +42 -0
  51. data/zillabyte_emails.csv +1 -0
  52. data/zillaconf.json +3 -0
  53. metadata +278 -0
@@ -0,0 +1,702 @@
1
+ require "zillabyte/cli/base"
2
+ require "zillabyte/cli/config"
3
+ require "zillabyte/common"
4
+ require "pty"
5
+ require 'indentation'
6
+ require 'open3'
7
+ require 'securerandom'
8
+
9
+ # manage custom flows
10
+ #
11
+ class Zillabyte::Command::Flows < Zillabyte::Command::Base
12
+
13
+
14
+ # flows
15
+ #
16
+ #
17
+ def index
18
+ self.list
19
+ end
20
+
21
+
22
+ # flows
23
+ #
24
+ # list custom flows
25
+ #
26
+ def list
27
+
28
+ headings = ["id", "name", "state"]
29
+ rows = api.flow.list.map do |row|
30
+ if headings.size == 0
31
+ headings = row.keys
32
+ headings.delete("rel_dir")
33
+ end
34
+ v = row.values_at *headings
35
+ v
36
+ end
37
+ display "flows:\n" + Terminal::Table.new(:headings => headings, :rows => rows).to_s
38
+ display "Total number of flows: "+rows.length.to_s
39
+
40
+
41
+ end
42
+
43
+ alias_command "list", "flows:list"
44
+
45
+
46
+ def status
47
+ headings = ["name", "implementable", "implemented"]
48
+ rows = api.flows.list.map do |row|
49
+ if headings.size == 0
50
+ headings = row.keys
51
+ headings.delete("rel_dir")
52
+ end
53
+ v = row.values_at *headings
54
+ v
55
+ end
56
+ display "flow status:\n" + Terminal::Table.new(:headings => headings, :rows => rows).to_s
57
+ display "Total number of flows in queue: "+rows.length.to_s
58
+
59
+ end
60
+
61
+ alias_command "status", "flows:status"
62
+
63
+ # def status
64
+ # headings = ["name", "matches", "emits", "implementable?", "implemented", ]
65
+ # end
66
+
67
+ # flows:push [DIR]
68
+ #
69
+ # uploads a flow
70
+ #
71
+ # --config CONFIG_FILE # use the given config file
72
+ #
73
+ #Examples:
74
+ #
75
+ # $ zillabyte flows:push .
76
+ #
77
+ def push
78
+
79
+ dir = options[:directory] || shift_argument || Dir.pwd
80
+ res = api.flows.push_directory dir, progress, options
81
+
82
+ if res['error']
83
+ display "error: #{res['error_message']}"
84
+ else
85
+ display "flow ##{res['id']} #{res['action']}"
86
+ end
87
+
88
+ end
89
+ alias_command "exec:push", "flows:push"
90
+ alias_command "push", "flows:push"
91
+
92
+
93
+
94
+ # flows:pull ID DIR
95
+ #
96
+ # pulls a flow source to a directory. The target directory must be empty
97
+ #
98
+ # --force # pulls even if the directory exists
99
+ #
100
+ #Examples:
101
+ #
102
+ # $ zillabyte flows:pull .
103
+ #
104
+ def pull
105
+
106
+ id = options[:id] || shift_argument
107
+ dir = options[:directory] || shift_argument
108
+
109
+ error "no id given" if id.nil?
110
+ error "no directory given" if dir.nil?
111
+
112
+ # Create if not exists..
113
+ if File.exists?(dir)
114
+ if Dir.entries(dir).size != 2 and options[:force].nil?
115
+ error "target directory not empty. use --force to override"
116
+ end
117
+ else
118
+ FileUtils.mkdir_p(dir)
119
+ end
120
+
121
+ res = api.flows.pull_to_directory id, dir, progress
122
+
123
+ if res['error']
124
+ display "error: #{res['error_message']}"
125
+ else
126
+ display "flow ##{res['id']} pulled to #{dir}"
127
+ end
128
+
129
+ end
130
+
131
+ alias_command "pull_to", "flows:pull"
132
+ alias_command "pull", "flows:pull"
133
+
134
+
135
+
136
+
137
+ # flows:prep [DIR]
138
+ #
139
+ # prepares a flow for execution
140
+ #
141
+ def prep
142
+
143
+ dir = options[:directory] || shift_argument || Dir.pwd
144
+ meta = Zillabyte::CLI::Config.get_config_info(dir)
145
+
146
+ case meta["language"]
147
+ when "ruby"
148
+
149
+ # Execute in the bundler context
150
+ full_script = File.join(dir, meta["script"])
151
+ cmd = "cd \"#{meta['home_dir']}\"; unset BUNDLE_GEMFILE; bundle install"
152
+ exec(cmd)
153
+
154
+ when "python"
155
+ vDir = "#{meta['home_dir']}/vEnv"
156
+ lock_file = meta['home_dir']+"/zillabyte_thread_lock_file"
157
+ if File.exists?(lock_file)
158
+ sleep(1) while File.exists?(lock_file)
159
+ else
160
+ begin
161
+ cmd = "touch #{lock_file}; virtualenv --clear --system-site-packages #{vDir}; PYTHONPATH=~/zb1/multilang/python/Zillabyte #{vDir}/bin/pip install -r #{meta['home_dir']}/requirements.txt"
162
+ system cmd, :out => :out
163
+ ensure
164
+ File.delete(lock_file)
165
+ end
166
+ end
167
+
168
+ when "js"
169
+ end
170
+
171
+ end
172
+ alias_command "prep", "flows:prep"
173
+
174
+
175
+
176
+
177
+ # flows:init [LANG] [DIR]
178
+ #
179
+ # initializes a new executable in DIR
180
+ # defaults to a ruby executable for the current directory
181
+ #
182
+ #Examples:
183
+ #
184
+ # $ zillabyte flows:init python contact_extractor
185
+ #
186
+ def init
187
+
188
+
189
+ lang = options[:lang] || shift_argument || "ruby"
190
+ dir = options[:dir] || shift_argument || Dir.pwd
191
+ languages = ["ruby","python", "js"]
192
+
193
+ error "Unsupported language #{lang}. We only support #{languages.join(', ')}." if not languages.include? lang
194
+
195
+ display "initializing empty #{lang} flow in #{dir}"
196
+ FileUtils.cp_r( File.expand_path("../templates/#{lang}", __FILE__) + "/." , dir )
197
+
198
+
199
+ end
200
+ alias_command "exec:init", "flows:init"
201
+
202
+
203
+
204
+
205
+ # flows:test [TEST_DATASET_ID]
206
+ #
207
+ # tests a local flow with sample data
208
+ #
209
+ # --config CONFIG_FILE # use the given config file
210
+ # --output OUTPUT_FILE # writes sink output to a file
211
+ # --wait MAX # max time to spend on each operation (default 10 seconds)
212
+ # --batches BATCHES # number of batches to emit (default 1)
213
+ #
214
+ def test
215
+
216
+ output = options[:output]
217
+ max_seconds = (options[:wait] || "30").to_i
218
+ batches = (options[:batches] || "1").to_i
219
+
220
+ def read_message(read_stream, color)
221
+ msg = nil
222
+ read_stream.each do |line|
223
+ line.strip!
224
+ if(line == "end")
225
+ return msg
226
+ end
227
+ begin
228
+ hash = JSON.parse(line)
229
+ if(hash["command"] == "done")
230
+ msg = "done"
231
+ else
232
+ msg = hash.to_json
233
+ end
234
+ rescue
235
+ next
236
+ end
237
+ end
238
+ msg
239
+ end
240
+
241
+ def write_message(write_stream, msg)
242
+ write_stream.write msg.strip + "\n"
243
+ write_stream.write "end\n"
244
+ write_stream.flush
245
+ end
246
+
247
+ def handshake(write_stream, read_stream, node, color)
248
+ begin
249
+ write_message write_stream, "{\"pidDir\": \"/tmp\"}\n"
250
+ read_message read_stream, color # Read to "end\n"
251
+ rescue Exception => e
252
+ puts "Error handshaking node: #{node}"
253
+ raise e
254
+ end
255
+ end
256
+
257
+
258
+ # INIT
259
+ test_data = options[:test_data] || shift_argument
260
+ dir = options[:dir] || Dir.pwd
261
+
262
+ meta = Zillabyte::API::Flows.get_rich_meta_info_from_script(dir, self, {:test => true})
263
+ if meta.nil?
264
+ error "this is not a valid zillabyte flow directory"
265
+ exit
266
+ end
267
+
268
+ # Show the user what we know about their flow...
269
+ display "inferring your flow details..."
270
+ colors = {}
271
+ describe_flow(meta, colors)
272
+
273
+
274
+ # Extract the flow's information..
275
+ nodes = meta["nodes"]
276
+ write_to_next_each = []
277
+ write_queue = []
278
+ stream_messages = {}
279
+ default_stream = "_default"
280
+
281
+ split_branches = false
282
+
283
+ # Iterate all nodes sequentially and invoke them in separate processes...
284
+ nodes.each do |node|
285
+
286
+ # Init
287
+ type = node["type"]
288
+ name = node["name"]
289
+ color = colors[name] || :default
290
+
291
+ op_display = lambda do |msg, override_color = nil|
292
+ display "#{name} - #{msg}".colorize(override_color || color)
293
+ end
294
+
295
+ # A Spout?
296
+ if type == "spout"
297
+
298
+ # A spout from relation?
299
+ if node['matches'] or node["relation"]
300
+ matches = node['matches'] || (node["relation"]["query"])
301
+ op_display.call "Grabbing remote data"
302
+ res = api.query.agnostic(matches)["rows"]
303
+ if(res.nil? or res.length == 0)
304
+ raise NameError, "Could not find data that matches your 'matches' clause"
305
+ end
306
+ res.each do |tuple|
307
+ values = {}
308
+ meta = {}
309
+ tuple.each do |k, v|
310
+ if(k == "id")
311
+ next
312
+ elsif(k == "confidence" or k == "since" or k == "source")
313
+ meta[k] = v
314
+ else
315
+ values[k] = v
316
+ end
317
+ end
318
+ read_msg = {"tuple" => values, "meta" => meta}.to_json
319
+ op_display.call "emit tuple: #{values} #{meta}"
320
+ stream_messages[default_stream] ||= []
321
+ stream_messages[default_stream] << read_msg
322
+ end
323
+
324
+ # Done processing...
325
+ next
326
+
327
+ else
328
+
329
+ # A regular spout..
330
+ stream_messages[default_stream] ||= []
331
+ batches.times do |i|
332
+ stream_messages[default_stream] << "{\"command\": \"next\"}\n"
333
+ end
334
+
335
+ end
336
+
337
+ end
338
+
339
+
340
+
341
+ # A Sink?
342
+ if type == "sink"
343
+
344
+ if split_branches || stream_messages.size > 1
345
+ sink_stream = node["consumes"]
346
+ messages = stream_messages[sink_stream] || []
347
+ else
348
+ messages = stream_messages.values.first || []
349
+ end
350
+
351
+ table = Terminal::Table.new :title => name
352
+ csv_str = CSV.generate do |csv|
353
+ header_written = false;
354
+ messages.each do |msg|
355
+ obj = JSON.parse(msg)
356
+ if obj['tuple']
357
+ if header_written == false
358
+ keys = [obj['tuple'].keys, obj['meta'].keys].flatten
359
+ csv << keys
360
+ table << keys
361
+ table << :separator
362
+ header_written = true
363
+ end
364
+ vals = [obj['tuple'].values, obj['meta'].values].flatten
365
+ csv << vals
366
+ table << vals
367
+ end
368
+ end
369
+ end
370
+
371
+ display table.to_s.colorize(color)
372
+
373
+ if output
374
+ filename = "#{output}.csv"
375
+ f = File.open(filename, "w")
376
+ f.write(csv_str)
377
+ f.close()
378
+ op_display.call "output written to #{filename}"
379
+ end
380
+
381
+
382
+
383
+ next
384
+ end
385
+
386
+
387
+ cmd = command("--execute_live --name #{name}")
388
+ begin
389
+
390
+ # Start the operation...
391
+ op_display.call "beginning #{type} #{name}"
392
+ Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thread|
393
+ begin
394
+
395
+ # Init
396
+ handshake stdin, stdout, node, color
397
+ write_queue = []
398
+ read_queue = []
399
+
400
+ # Get the incoming stream
401
+ if !split_branches && stream_messages.size == 1 # i.e. the number of streams we're dealing with right now
402
+ # Assume default stream
403
+ stream_name = stream_messages.keys.first
404
+ write_queue = stream_messages.values.first.clone
405
+ stream_messages.delete(stream_name)
406
+ else
407
+ # Multiple streams...
408
+ split_branches = true;
409
+ if node['consumes'].nil?
410
+ error "The node #{name} must declare which stream it 'consumes'"
411
+ end
412
+ stream_name = node["consumes"]
413
+ write_queue = stream_messages[stream_name].clone()
414
+ stream_messages.delete(stream_name)
415
+ end
416
+
417
+ # Start writing the messages...
418
+ stuff_to_read = false
419
+ writing_thread = Thread.start do
420
+ until(write_queue.empty?)
421
+
422
+ # Make sure we're not reading anything...
423
+ while(stuff_to_read) # TODO: semaphores
424
+ sleep 0.5 # spin wait
425
+ end
426
+
427
+ # Get next mesage
428
+ write_msg = write_queue.shift
429
+
430
+ # Make it human-understable
431
+ write_json = JSON.parse(write_msg)
432
+ if write_json['tuple']
433
+ op_display.call "receiving: #{write_json['tuple']}"
434
+ elsif write_json['command'] == 'next'
435
+ op_display.call "starting next spout batch"
436
+ else
437
+ puts write_json
438
+ end
439
+
440
+ # Actually send it to the process
441
+ begin
442
+ write_message stdin, write_msg
443
+ stuff_to_read = true
444
+ sleep 0.1
445
+ rescue Exception => e
446
+ puts "Error running #{cmd}: #{e}"
447
+ raise e
448
+ end
449
+ end
450
+ end
451
+
452
+ # Start reading messages...
453
+ reading_thread = Thread.start do
454
+ while(true)
455
+
456
+ # Get next message
457
+ read_msg = read_message(stdout, color)
458
+ if read_msg == "done" || read_msg.nil?
459
+ stuff_to_read = false
460
+ if write_queue.empty?
461
+ break # exit while loop
462
+ else
463
+ sleep 0.5 # spin wait
464
+ next
465
+ end
466
+ end
467
+ stuff_to_read = true
468
+
469
+ # Process message
470
+ obj = JSON.parse(read_msg)
471
+ if obj['tuple']
472
+
473
+ # Conver to a incoming tuple for the next operation
474
+ next_msg = {
475
+ :tuple => obj['tuple'],
476
+ :meta => obj['meta']
477
+ }
478
+ emit_stream = obj['stream'] || default_stream
479
+ stream_messages[emit_stream] ||= []
480
+ stream_messages[emit_stream] << next_msg.to_json
481
+ op_display.call "emitted: #{obj['tuple']} to #{emit_stream}"
482
+ elsif obj['command'] == 'log'
483
+ op_display.call "log: #{obj['msg']}"
484
+ else
485
+ error "unknown message: #{read_msg}"
486
+ end
487
+
488
+ end
489
+ end
490
+
491
+
492
+ # stderr thread
493
+ stderr_thread = Thread.start do
494
+ stderr.each do |line|
495
+ op_display.call("stderr: #{line}", :red)
496
+ end
497
+ end
498
+
499
+ begin
500
+ killed = Timeout.timeout(max_seconds) do
501
+ reading_thread.join()
502
+ writing_thread.join()
503
+ stderr_thread.kill()
504
+ op_display.call "completed #{type} #{name}"
505
+ end
506
+ rescue Timeout::Error
507
+ op_display.call "max time reached. preempting #{type} #{name}. set --wait to increase", :red
508
+ reading_thread.kill() if reading_thread.alive?
509
+ writing_thread.kill() if writing_thread.alive?
510
+ stderr_thread.kill() if stderr_thread.alive?
511
+ end
512
+
513
+ rescue Errno::EIO
514
+ puts "Errno:EIO error, but this probably just means " +
515
+ "that the process has finished giving output"
516
+ end
517
+ end
518
+ rescue PTY::ChildExited
519
+ puts "The child process exited!"
520
+ end
521
+ end
522
+
523
+ end
524
+ alias_command "test", "flows:test"
525
+
526
+
527
+
528
+
529
+
530
+ # flows:status ID
531
+ #
532
+ # gets the flows status
533
+ #
534
+ # --config CONFIG_FILE # use the given config file
535
+ #
536
+ def status
537
+
538
+ id = options[:id] || shift_argument
539
+
540
+ display api.flows.get(id)
541
+
542
+ end
543
+
544
+
545
+
546
+
547
+
548
+
549
+ # flows:kill ID
550
+ #
551
+ # kills the given flow
552
+ #
553
+ # --config CONFIG_FILE # use the given config file
554
+ #
555
+ def kill
556
+
557
+ id = options[:id] || shift_argument
558
+ api.flows.kill(id)
559
+ display "flow #{id} killed"
560
+
561
+ end
562
+
563
+
564
+
565
+
566
+
567
+
568
+ # flows:live_run [DIR]
569
+ #
570
+ # runs a local flow with live data
571
+ #
572
+ # --config CONFIG_FILE # use the given config file
573
+ #
574
+ def live_run
575
+
576
+ name = options[:name] || shift_argument
577
+ thread_id = options[:thread] || shift_argument || ""
578
+ dir = options[:directory] || shift_argument || Dir.pwd
579
+ meta = Zillabyte::CLI::Config.get_config_info(dir)
580
+
581
+ if meta.nil?
582
+ throw "could not find meta information for: #{dir}"
583
+ end
584
+
585
+ if(thread_id == "")
586
+ exec(command("--execute_live --name #{name.to_s}"))
587
+ else
588
+ exec(command("--execute_live --name #{name.to_s} --pipe #{thread_id}"))
589
+ end
590
+ end
591
+ alias_command "live_run", "flows:live_run"
592
+
593
+
594
+ # flows:info [DIR]
595
+ #
596
+ # outputs the info for the flow in the dir.
597
+ #
598
+ # --pretty # Pretty prints the info output
599
+ #
600
+ def info
601
+ info_file = SecureRandom.uuid
602
+ cmd = command("--info --file #{info_file}")
603
+ flow_info = Zillabyte::Command::Flows.get_info(cmd, info_file)
604
+
605
+ if options[:pretty]
606
+ puts JSON.pretty_generate(JSON.parse(flow_info))
607
+ else
608
+ puts flow_info
609
+ end
610
+ exit
611
+ end
612
+ alias_command "info", "flows:info"
613
+
614
+ def self.get_info(cmd, info_file, options = {})
615
+ response = `#{cmd}`
616
+ if($?.exitstatus == 1)
617
+ File.delete("#{info_file}") if File.exists?(info_file)
618
+ puts "error: #{response}" if options[:test]
619
+ Process.exit 1
620
+ end
621
+
622
+ flow_info = {}
623
+ File.open("#{info_file}", "r+").each do |line|
624
+ line = JSON.parse(line)
625
+ if(line["type"])
626
+ flow_info["nodes"] << line
627
+ else
628
+ flow_info = line
629
+ flow_info["nodes"] = []
630
+ end
631
+ end
632
+ File.delete("#{info_file}")
633
+
634
+ flow_info = flow_info.to_json
635
+ if(File.exists?("info_to_java.in"))
636
+ java_pipe = open("info_to_java.in","w+")
637
+ java_pipe.puts(flow_info+"\n")
638
+ java_pipe.flush
639
+ java_pipe.close()
640
+ end
641
+
642
+ flow_info
643
+ end
644
+
645
+ private
646
+
647
+
648
+ def command(arg="--execute_live", ignore_stderr = false)
649
+
650
+ dir = options[:directory] || shift_argument || Dir.pwd
651
+ meta = Zillabyte::CLI::Config.get_config_info(dir, progress=nil, options)
652
+ #meta = Zillabyte::API::Functions.get_rich_meta_info_from_script(dir, self)
653
+ error "could not extract meta information. missing zillabyte.conf.yml?" if meta.nil?
654
+ error meta["error_message"] if meta['status'] == "error"
655
+ full_script = File.join(dir, meta["script"])
656
+ stderr_opt = "2> /dev/null" if ignore_stderr
657
+
658
+ case meta["language"]
659
+ when "ruby"
660
+ # Execute in the bundler context
661
+ cmd = "cd \"#{dir}\"; unset BUNDLE_GEMFILE; ZILLABYTE_HARNESS=1 bundle exec ruby \"#{full_script}\" #{arg} #{stderr_opt}"
662
+
663
+ when "python"#{
664
+ if(File.directory?("#{dir}/vEnv"))
665
+ cmd = "cd \"#{dir}\"; PYTHONPATH=~/zb1/multilang/python/Zillabyte #{dir}/vEnv/bin/python \"#{full_script}\" #{arg} #{stderr_opt}"
666
+ else
667
+ cmd = "cd \"#{dir}\"; PYTHONPATH=~/zb1/multilang/python/Zillabyte python \"#{full_script}\" #{arg} #{stderr_opt}"
668
+ end
669
+
670
+ when "js"
671
+ # cmd = "#{Zillabyte::API::CASPERJS_BIN} #{Zillabyte::API::API_CLIENT_JS} \"#{full_script}\" #{arg}"
672
+ cmd = "cd \"#{dir}\"; NODE_PATH=~/zb1/multilang/js #{Zillabyte::API::NODEJS_BIN} \"#{full_script}\" #{arg} #{stderr_opt}"
673
+
674
+ else
675
+ error "no language specified"
676
+ end
677
+
678
+ return cmd
679
+
680
+ end
681
+
682
+
683
+ def describe_flow(meta, colors = {})
684
+ @colors ||= [:green, :yellow, :magenta, :cyan, :light_black, :light_green, :light_yellow, :light_blue, :light_magenta, :light_cyan]
685
+ rjust = 20
686
+ display "#{'flow name'.rjust(rjust)}: #{meta['name']}"
687
+ display "#{'flow language'.rjust(rjust)}: #{meta['language']}"
688
+
689
+ meta['nodes'].each_with_index do |node, index|
690
+ colors[node['name']] ||= @colors.shift
691
+ color = colors[node['name']]
692
+ display (("="*rjust + " operation ##{index}").colorize(color))
693
+ display "#{"name".rjust(rjust)}: #{node['name'].to_s.colorize(color)}"
694
+ display "#{"type".rjust(rjust)}: #{node['type'].to_s.colorize(color)}"
695
+ display "#{"matches".rjust(rjust)}: #{JSON.pretty_generate(node['matches']).indent(rjust+2).lstrip.colorize(color)}" if node['matches']
696
+ display "#{"emits".rjust(rjust)}: #{JSON.pretty_generate(node['emits']).indent(rjust+2).lstrip.colorize(color)}" if node['emits']
697
+ end
698
+
699
+ end
700
+
701
+
702
+ end