zillabyte-cli 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
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