wisco 0.3.5 → 0.3.7
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 +4 -4
- data/lib/wisco/commands/exec.rb +152 -10
- data/lib/wisco/commands/fixtures.rb +57 -2
- data/lib/wisco/commands/init.rb +74 -31
- data/lib/wisco/commands/profile.rb +236 -0
- data/lib/wisco/config.rb +53 -3
- data/lib/wisco/exec_script.rb +160 -0
- data/lib/wisco/profile.rb +137 -0
- data/lib/wisco/version.rb +1 -1
- data/lib/wisco.rb +21 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 942295e665d5ecea09078c736425531bb9846bfe29db26223bf52034ff4221b0
|
|
4
|
+
data.tar.gz: c56480bf18d42ac7797dd2b46f9d50146788f1590e673f4056bbe0ce55580347
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 797b1ac153469a31d1edd1781510bac1ac7c05a7f7b2545de34a48d40540a79fcbffd189c851b85d830f9ca63424a5f9006f1a85915bdd9bd6da01f9fe2da392
|
|
7
|
+
data.tar.gz: 7133a41ae9da79285993f77ccd32e65ff49c3b3c0d203140c1f3c128bdfa8657c8cd882d435ab180b1bd51811103e3c51f16f8c49ee67150388e11a5062e1448
|
data/lib/wisco/commands/exec.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'workato/cli/exec_command'
|
|
|
4
4
|
require_relative '../config'
|
|
5
5
|
require_relative '../connector'
|
|
6
6
|
require_relative '../path_utils'
|
|
7
|
+
require_relative '../exec_script'
|
|
7
8
|
|
|
8
9
|
module Wisco
|
|
9
10
|
module Commands
|
|
@@ -54,7 +55,7 @@ module Wisco
|
|
|
54
55
|
next
|
|
55
56
|
end
|
|
56
57
|
|
|
57
|
-
input_files = resolve_input_files(input, fixtures_dir)
|
|
58
|
+
input_files = resolve_input_files(input, fixtures_dir, debug: debug)
|
|
58
59
|
|
|
59
60
|
if input_files.empty?
|
|
60
61
|
if %w[pick_lists methods].include?(section)
|
|
@@ -71,19 +72,32 @@ module Wisco
|
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
input_files.each do |input_file|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
if File.extname(input_file).downcase == '.rb'
|
|
76
|
+
execute_ruby_script(section, key, input_file, fixtures_dir, target_dir,
|
|
77
|
+
connector_full_path, connection, config,
|
|
78
|
+
pagination: pagination, verbose: verbose,
|
|
79
|
+
extended: extended, closure: closure, config_fields: config_fields,
|
|
80
|
+
continue: continue, extended_input_schema: extended_input_schema,
|
|
81
|
+
extended_output_schema: extended_output_schema, debug: debug)
|
|
82
|
+
else
|
|
83
|
+
execute_one(section, key, input_file, fixtures_dir,
|
|
84
|
+
connector_full_path, connection, pagination: pagination, verbose: verbose,
|
|
85
|
+
extended: extended, closure: closure, config_fields: config_fields,
|
|
86
|
+
continue: continue, extended_input_schema: extended_input_schema,
|
|
87
|
+
extended_output_schema: extended_output_schema, debug: debug)
|
|
88
|
+
end
|
|
79
89
|
end
|
|
80
90
|
end
|
|
81
91
|
end
|
|
82
92
|
|
|
83
93
|
# Resolve the list of input files to execute.
|
|
84
94
|
# If an explicit input filename/path is given, use that (relative to fixtures_dir).
|
|
85
|
-
# Otherwise glob execute_
|
|
86
|
-
|
|
95
|
+
# Otherwise glob execute_*.{json,rb} in fixtures_dir and exclude files still containing
|
|
96
|
+
# their respective sentinel.
|
|
97
|
+
#
|
|
98
|
+
# In debug mode, prints which candidate files were found and which were
|
|
99
|
+
# skipped (because they still contain a sentinel comment).
|
|
100
|
+
def resolve_input_files(input, fixtures_dir, debug: false)
|
|
87
101
|
if input
|
|
88
102
|
path = File.absolute_path?(input) ? input : File.join(fixtures_dir, input)
|
|
89
103
|
unless File.exist?(path)
|
|
@@ -92,13 +106,28 @@ module Wisco
|
|
|
92
106
|
end
|
|
93
107
|
[path]
|
|
94
108
|
else
|
|
95
|
-
Dir.glob(File.join(fixtures_dir, 'execute_
|
|
96
|
-
|
|
109
|
+
candidates = Dir.glob(File.join(fixtures_dir, 'execute_*.{json,rb}')).select { |f| File.file?(f) }
|
|
110
|
+
warn "[exec] candidate files: #{candidates.map { |f| File.basename(f) }}" if debug
|
|
111
|
+
candidates.reject do |f|
|
|
112
|
+
if file_has_sentinel?(f)
|
|
113
|
+
warn "[exec] skipped (sentinel): #{File.basename(f)}" if debug
|
|
114
|
+
true
|
|
115
|
+
else
|
|
116
|
+
false
|
|
117
|
+
end
|
|
97
118
|
end
|
|
98
119
|
end
|
|
99
120
|
end
|
|
100
121
|
|
|
122
|
+
# Returns true if the file should be skipped. Each input format has its own
|
|
123
|
+
# sentinel convention:
|
|
124
|
+
# .json — first line exactly equals Wisco::Commands::Fixtures::SENTINEL
|
|
125
|
+
# .rb — first non-blank line matches `# WISCO_SKIP`
|
|
101
126
|
def file_has_sentinel?(path)
|
|
127
|
+
if File.extname(path).downcase == '.rb'
|
|
128
|
+
return Wisco::ExecScript.sentinel?(path)
|
|
129
|
+
end
|
|
130
|
+
|
|
102
131
|
first_line = begin
|
|
103
132
|
File.open(path, &:readline).chomp
|
|
104
133
|
rescue StandardError
|
|
@@ -223,6 +252,7 @@ module Wisco
|
|
|
223
252
|
cmd = Workato::CLI::ExecCommand.new(path: exec_path, options: options)
|
|
224
253
|
cmd.call
|
|
225
254
|
rescue StandardError => e
|
|
255
|
+
FileUtils.rm_f(output_file)
|
|
226
256
|
File.write(error_file, "#{e.class}: #{e.message}\n\n#{e.backtrace.join("\n")}\n")
|
|
227
257
|
Wisco::TerminalOutput.emit_error("Error executing #{section}.#{key} with #{input_file ? File.basename(input_file) : 'no input'}: #{e.message}")
|
|
228
258
|
Wisco::TerminalOutput.emit_error(" Details written to: #{error_file}")
|
|
@@ -237,6 +267,118 @@ module Wisco
|
|
|
237
267
|
File.write(output_file, pretty + "\n")
|
|
238
268
|
puts " Written: #{output_file}"
|
|
239
269
|
end
|
|
270
|
+
|
|
271
|
+
# Handles `execute_*.rb` scripts: evaluates the script to produce dynamic
|
|
272
|
+
# input, writes it to <subdir>/input.json, runs the connector item via
|
|
273
|
+
# ExecCommand, and writes <subdir>/output.json or <subdir>/error.txt.
|
|
274
|
+
# The subdirectory is named after the script (without .rb), e.g.
|
|
275
|
+
# execute_input.rb -> execute_input/.
|
|
276
|
+
def execute_ruby_script(section, key, script_path, fixtures_dir, target_dir,
|
|
277
|
+
connector_full_path, connection, config,
|
|
278
|
+
pagination: true, verbose: true, debug: false,
|
|
279
|
+
extended: true, closure: nil, config_fields: nil, continue: nil,
|
|
280
|
+
extended_input_schema: nil, extended_output_schema: nil)
|
|
281
|
+
subdir = File.join(fixtures_dir, File.basename(script_path, '.rb'))
|
|
282
|
+
FileUtils.mkdir_p(subdir)
|
|
283
|
+
input_file = File.join(subdir, 'input.json')
|
|
284
|
+
output_file = File.join(subdir, 'output.json')
|
|
285
|
+
error_file = File.join(subdir, 'error.txt')
|
|
286
|
+
|
|
287
|
+
# Step 1: evaluate the script to obtain dynamic input
|
|
288
|
+
connection_name = config['connection'] || 'default'
|
|
289
|
+
|
|
290
|
+
begin
|
|
291
|
+
generated = Wisco::ExecScript.evaluate(
|
|
292
|
+
script_path: script_path,
|
|
293
|
+
connector_full_path: connector_full_path,
|
|
294
|
+
target_dir: target_dir,
|
|
295
|
+
connection_name: connection_name
|
|
296
|
+
)
|
|
297
|
+
rescue StandardError => e
|
|
298
|
+
FileUtils.rm_f(input_file)
|
|
299
|
+
FileUtils.rm_f(output_file)
|
|
300
|
+
File.write(error_file, "#{e.class}: #{e.message}\n\n#{Array(e.backtrace).join("\n")}\n")
|
|
301
|
+
Wisco::TerminalOutput.emit_error("Error evaluating #{File.basename(script_path)}: #{e.message}")
|
|
302
|
+
Wisco::TerminalOutput.emit_error(" Details written to: #{error_file}")
|
|
303
|
+
return
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
File.write(input_file, JSON.pretty_generate(generated) + "\n")
|
|
307
|
+
puts " Generated: #{input_file}"
|
|
308
|
+
|
|
309
|
+
# Step 2: build ExecCommand options (mirrors execute_one)
|
|
310
|
+
use_args = %w[pick_lists methods].include?(section)
|
|
311
|
+
exec_path = if use_args
|
|
312
|
+
"#{section}.#{key}"
|
|
313
|
+
elsif section == 'triggers'
|
|
314
|
+
pagination ? "#{section}.#{key}.poll" : "#{section}.#{key}.poll_page"
|
|
315
|
+
else
|
|
316
|
+
"#{section}.#{key}.execute"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
options = { connector: connector_full_path, output: output_file }
|
|
320
|
+
options[:connection] = connection if connection
|
|
321
|
+
options[:verbose] = verbose
|
|
322
|
+
if use_args
|
|
323
|
+
options[:args] = input_file
|
|
324
|
+
else
|
|
325
|
+
options[:input] = input_file
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
options[:closure] = resolve_option_path(closure, fixtures_dir) if closure
|
|
329
|
+
options[:config_fields] = resolve_option_path(config_fields, fixtures_dir) if config_fields
|
|
330
|
+
options[:continue] = resolve_option_path(continue, fixtures_dir) if continue
|
|
331
|
+
|
|
332
|
+
# extended schema files still live at the item's fixtures_dir (not the subdir)
|
|
333
|
+
unless use_args
|
|
334
|
+
eis = if extended_input_schema
|
|
335
|
+
resolve_option_path(extended_input_schema, fixtures_dir)
|
|
336
|
+
elsif extended
|
|
337
|
+
f = File.join(fixtures_dir, 'input_fields.json')
|
|
338
|
+
File.exist?(f) ? f : nil
|
|
339
|
+
end
|
|
340
|
+
options[:extended_input_schema] = eis if eis
|
|
341
|
+
|
|
342
|
+
eos = if extended_output_schema
|
|
343
|
+
resolve_option_path(extended_output_schema, fixtures_dir)
|
|
344
|
+
elsif extended
|
|
345
|
+
f = File.join(fixtures_dir, 'output_fields.json')
|
|
346
|
+
File.exist?(f) ? f : nil
|
|
347
|
+
end
|
|
348
|
+
options[:extended_output_schema] = eos if eos
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
if debug
|
|
352
|
+
warn "[exec.rb] script: #{script_path}"
|
|
353
|
+
warn "[exec.rb] subdir: #{subdir}"
|
|
354
|
+
warn "[exec.rb] path: #{exec_path}"
|
|
355
|
+
warn "[exec.rb] connector: #{connector_full_path}"
|
|
356
|
+
warn "[exec.rb] connection: #{connection.inspect}"
|
|
357
|
+
warn "[exec.rb] #{use_args ? 'args' : 'input'}: #{input_file}"
|
|
358
|
+
warn "[exec.rb] output: #{output_file}"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Step 3: invoke ExecCommand against the generated input
|
|
362
|
+
begin
|
|
363
|
+
cmd = Workato::CLI::ExecCommand.new(path: exec_path, options: options)
|
|
364
|
+
cmd.call
|
|
365
|
+
rescue StandardError => e
|
|
366
|
+
FileUtils.rm_f(input_file)
|
|
367
|
+
FileUtils.rm_f(output_file)
|
|
368
|
+
File.write(error_file, "#{e.class}: #{e.message}\n\n#{e.backtrace.join("\n")}\n")
|
|
369
|
+
Wisco::TerminalOutput.emit_error("Error executing #{section}.#{key} with #{File.basename(script_path)}: #{e.message}")
|
|
370
|
+
Wisco::TerminalOutput.emit_error(" Details written to: #{error_file}")
|
|
371
|
+
return
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
FileUtils.rm_f(error_file)
|
|
375
|
+
|
|
376
|
+
return unless File.exist?(output_file)
|
|
377
|
+
|
|
378
|
+
pretty = JSON.pretty_generate(JSON.parse(File.read(output_file)))
|
|
379
|
+
File.write(output_file, pretty + "\n")
|
|
380
|
+
puts " Written: #{output_file}"
|
|
381
|
+
end
|
|
240
382
|
end
|
|
241
383
|
end
|
|
242
384
|
end
|
|
@@ -8,11 +8,34 @@ require_relative '../path_utils'
|
|
|
8
8
|
module Wisco
|
|
9
9
|
module Commands
|
|
10
10
|
module Fixtures
|
|
11
|
-
SENTINEL
|
|
11
|
+
SENTINEL = '# Remove this comment before updating. Files that include this line will be overwritten.'
|
|
12
|
+
RB_SENTINEL = '# WISCO_SKIP'
|
|
13
|
+
|
|
14
|
+
RB_TEMPLATE = <<~RUBY
|
|
15
|
+
# WISCO_SKIP
|
|
16
|
+
# Remove the WISCO_SKIP line above once this script is ready to run.
|
|
17
|
+
#
|
|
18
|
+
# The last expression in this file becomes the input passed to the connector item.
|
|
19
|
+
# Helper methods available inside the script:
|
|
20
|
+
# call_method(:name, *args) — invoke a methods: entry on the connector
|
|
21
|
+
# call_pick_list(:name, *args) — invoke a pick_lists: entry on the connector
|
|
22
|
+
# connection — Hash of decrypted settings.yaml(.enc)
|
|
23
|
+
#
|
|
24
|
+
# Example:
|
|
25
|
+
# require 'securerandom'
|
|
26
|
+
# {
|
|
27
|
+
# order_number: "ORD-\#{SecureRandom.hex(4).upcase}",
|
|
28
|
+
# created_at: Time.now.iso8601
|
|
29
|
+
# }
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
# TODO: fill in fields
|
|
33
|
+
}
|
|
34
|
+
RUBY
|
|
12
35
|
|
|
13
36
|
module_function
|
|
14
37
|
|
|
15
|
-
def run(path_arg, target_dir, overwrite: false, debug: false)
|
|
38
|
+
def run(path_arg, target_dir, overwrite: false, ruby: false, debug: false)
|
|
16
39
|
target_dir = File.expand_path(target_dir)
|
|
17
40
|
config_path = Wisco.config_path(target_dir)
|
|
18
41
|
|
|
@@ -44,8 +67,10 @@ module Wisco
|
|
|
44
67
|
|
|
45
68
|
if section == 'pick_lists'
|
|
46
69
|
process_pick_list(key, connector, fixtures_dir, overwrite: overwrite)
|
|
70
|
+
generate_execute_input_rb(fixtures_dir, overwrite: overwrite) if ruby
|
|
47
71
|
elsif section == 'methods'
|
|
48
72
|
process_method(key, connector, fixtures_dir, overwrite: overwrite)
|
|
73
|
+
generate_execute_input_rb(fixtures_dir, overwrite: overwrite) if ruby
|
|
49
74
|
else
|
|
50
75
|
# ── config_fields pre-check ──────────────────────────────────────
|
|
51
76
|
item = connector[section.to_sym]&.[](key.to_sym)
|
|
@@ -76,6 +101,7 @@ module Wisco
|
|
|
76
101
|
)
|
|
77
102
|
|
|
78
103
|
generate_execute_input(input_fields_file, fixtures_dir, overwrite: overwrite, debug: debug)
|
|
104
|
+
generate_execute_input_rb(fixtures_dir, overwrite: overwrite) if ruby
|
|
79
105
|
|
|
80
106
|
# ── output_fields ────────────────────────────────────────────────
|
|
81
107
|
output_fields_file = File.join(fixtures_dir, 'output_fields.json')
|
|
@@ -133,6 +159,35 @@ module Wisco
|
|
|
133
159
|
puts " Written: #{output_file}"
|
|
134
160
|
end
|
|
135
161
|
|
|
162
|
+
# Writes an `execute_input.rb` template into `fixtures_dir`. The template
|
|
163
|
+
# is prefixed with `# WISCO_SKIP` so it is identifiable as unedited and
|
|
164
|
+
# will be skipped by `wisco exec` until the user removes that line.
|
|
165
|
+
#
|
|
166
|
+
# Overwrite rules mirror generate_execute_input:
|
|
167
|
+
# - File absent -> write
|
|
168
|
+
# - File present, # WISCO_SKIP on L1 -> overwrite (still a template)
|
|
169
|
+
# - File present, no sentinel -> skip (user-edited); force with --overwrite
|
|
170
|
+
def generate_execute_input_rb(fixtures_dir, overwrite: false)
|
|
171
|
+
output_file = File.join(fixtures_dir, 'execute_input.rb')
|
|
172
|
+
|
|
173
|
+
if File.exist?(output_file)
|
|
174
|
+
first_line = begin
|
|
175
|
+
File.open(output_file, &:readline).chomp
|
|
176
|
+
rescue StandardError
|
|
177
|
+
''
|
|
178
|
+
end
|
|
179
|
+
has_sentinel = first_line.strip == RB_SENTINEL
|
|
180
|
+
|
|
181
|
+
unless has_sentinel || overwrite
|
|
182
|
+
puts " Skipped (user-edited): #{output_file}"
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
File.write(output_file, RB_TEMPLATE)
|
|
188
|
+
puts " Written: #{output_file}"
|
|
189
|
+
end
|
|
190
|
+
|
|
136
191
|
# Recursively converts a Workato schema array into a template hash.
|
|
137
192
|
# Scalars become "<type_value_required|optional>" placeholder strings.
|
|
138
193
|
# Objects expand into a nested hash via their properties.
|
data/lib/wisco/commands/init.rb
CHANGED
|
@@ -2,22 +2,14 @@ require 'erb'
|
|
|
2
2
|
require 'fileutils'
|
|
3
3
|
require_relative '../config'
|
|
4
4
|
require_relative '../connector'
|
|
5
|
+
require_relative '../profile'
|
|
5
6
|
|
|
6
7
|
module Wisco
|
|
7
8
|
module Commands
|
|
8
9
|
module Init
|
|
9
|
-
HOSTNAMES = [
|
|
10
|
-
{ region: 'US Data Center', hostname: 'www.workato.com' },
|
|
11
|
-
{ region: 'EU Data Center', hostname: 'app.eu.workato.com' },
|
|
12
|
-
{ region: 'JP Data Center', hostname: 'app.jp.workato.com' },
|
|
13
|
-
{ region: 'SG Data Center', hostname: 'app.sg.workato.com' },
|
|
14
|
-
{ region: 'AU Data Center', hostname: 'app.au.workato.com' },
|
|
15
|
-
{ region: 'IL Data Center', hostname: 'app.il.workato.com' },
|
|
16
|
-
].freeze
|
|
17
|
-
|
|
18
10
|
module_function
|
|
19
11
|
|
|
20
|
-
def run(target_dir)
|
|
12
|
+
def run(target_dir, profile: nil)
|
|
21
13
|
target_dir = File.expand_path(target_dir)
|
|
22
14
|
|
|
23
15
|
unless Dir.exist?(target_dir)
|
|
@@ -46,7 +38,7 @@ module Wisco
|
|
|
46
38
|
config['connector']['path'] = target_dir
|
|
47
39
|
config['connector']['file'] = connector_file
|
|
48
40
|
|
|
49
|
-
prompt_hostname(config)
|
|
41
|
+
prompt_hostname(config, profile: profile)
|
|
50
42
|
|
|
51
43
|
Wisco::Config.save_config(config_path, config)
|
|
52
44
|
puts "Config written to #{config_path}"
|
|
@@ -56,32 +48,78 @@ module Wisco
|
|
|
56
48
|
deploy_github_workflow(target_dir, connector_file, config)
|
|
57
49
|
end
|
|
58
50
|
|
|
59
|
-
def prompt_hostname(config)
|
|
51
|
+
def prompt_hostname(config, profile: nil)
|
|
60
52
|
api_cfg = config['workato_developer_api'] ||= {}
|
|
61
|
-
current = api_cfg['hostname']
|
|
62
53
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
# --profile flag: skip all prompts and attach the named profile
|
|
55
|
+
if profile
|
|
56
|
+
unless Wisco::Profiles.exists?(profile)
|
|
57
|
+
Wisco::TerminalOutput.emit_error("Error: Profile \"#{profile}\" not found in #{Wisco::Profiles.profiles_path}.")
|
|
58
|
+
Wisco::TerminalOutput.emit_error(" Run 'wisco profile list' to see available profiles.")
|
|
59
|
+
exit 1
|
|
60
|
+
end
|
|
61
|
+
api_cfg.delete('hostname')
|
|
62
|
+
api_cfg.delete('api_token')
|
|
63
|
+
api_cfg['profile'] = profile
|
|
64
|
+
puts "Using profile: #{profile}"
|
|
65
|
+
return
|
|
67
66
|
end
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
# If profiles exist, offer them alongside "enter manually"
|
|
69
|
+
existing_profiles = Wisco::Profiles.all
|
|
70
|
+
unless existing_profiles.empty?
|
|
71
|
+
current_profile = api_cfg['profile'].to_s.strip
|
|
72
|
+
current_host = api_cfg['hostname'].to_s.strip
|
|
73
|
+
|
|
74
|
+
if !current_profile.empty?
|
|
75
|
+
puts "Current Workato profile: #{current_profile}"
|
|
76
|
+
print 'Keep this? (y/n): '
|
|
77
|
+
return if $stdin.gets.strip.downcase == 'y'
|
|
78
|
+
elsif !current_host.empty?
|
|
79
|
+
puts "Current Workato hostname: #{current_host} (inline credentials)"
|
|
80
|
+
print 'Keep this? (y/n): '
|
|
81
|
+
return if $stdin.gets.strip.downcase == 'y'
|
|
82
|
+
end
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
puts 'Workato API credentials'
|
|
85
|
+
name_width = existing_profiles.keys.map(&:length).max
|
|
86
|
+
existing_profiles.each_with_index do |(prof_name, p), i|
|
|
87
|
+
puts " #{i + 1}. #{prof_name.ljust(name_width)} (#{p['hostname']})"
|
|
88
|
+
end
|
|
89
|
+
manual_index = existing_profiles.size + 1
|
|
90
|
+
puts " #{manual_index}. Enter credentials manually"
|
|
91
|
+
loop do
|
|
92
|
+
print "Select an option (1-#{manual_index}): "
|
|
93
|
+
input = $stdin.gets.strip
|
|
94
|
+
index = input.to_i
|
|
95
|
+
if index >= 1 && index < manual_index
|
|
96
|
+
chosen = existing_profiles.keys[index - 1]
|
|
97
|
+
api_cfg.delete('hostname')
|
|
98
|
+
api_cfg.delete('api_token')
|
|
99
|
+
api_cfg['profile'] = chosen
|
|
100
|
+
puts "Using profile: #{chosen}"
|
|
101
|
+
return
|
|
102
|
+
elsif index == manual_index
|
|
103
|
+
break # fall through to manual hostname prompt below
|
|
104
|
+
else
|
|
105
|
+
warn "Invalid selection. Please enter a number between 1 and #{manual_index}."
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
# No profiles — check for existing inline hostname
|
|
110
|
+
current = api_cfg['hostname']
|
|
111
|
+
if current && !current.strip.empty?
|
|
112
|
+
puts "Current Workato hostname: #{current}"
|
|
113
|
+
print 'Keep this? (y/n): '
|
|
114
|
+
return if $stdin.gets.strip.downcase == 'y'
|
|
82
115
|
end
|
|
83
|
-
warn "Invalid selection. Please enter a number between 1 and #{HOSTNAMES.size}."
|
|
84
116
|
end
|
|
117
|
+
|
|
118
|
+
# Manual hostname selection
|
|
119
|
+
hostname = Wisco::Profiles.prompt_hostname_selection
|
|
120
|
+
api_cfg.delete('profile')
|
|
121
|
+
api_cfg['hostname'] = hostname
|
|
122
|
+
puts "Hostname set to: #{hostname}"
|
|
85
123
|
end
|
|
86
124
|
|
|
87
125
|
def update_gitignore(target_dir)
|
|
@@ -128,7 +166,12 @@ module Wisco
|
|
|
128
166
|
end
|
|
129
167
|
end
|
|
130
168
|
|
|
131
|
-
|
|
169
|
+
api_cfg = config['workato_developer_api'] || {}
|
|
170
|
+
hostname = if api_cfg['profile'].to_s.strip != ''
|
|
171
|
+
Wisco::Profiles.get(api_cfg['profile'])&.dig('hostname')
|
|
172
|
+
else
|
|
173
|
+
api_cfg['hostname']
|
|
174
|
+
end
|
|
132
175
|
workato_base_url = "https://#{hostname}"
|
|
133
176
|
connector_name = connector_file
|
|
134
177
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
require_relative '../profile'
|
|
2
|
+
require_relative '../config'
|
|
3
|
+
require_relative '../terminal_output'
|
|
4
|
+
|
|
5
|
+
module Wisco
|
|
6
|
+
module Commands
|
|
7
|
+
class Profile < Thor
|
|
8
|
+
def self.exit_on_failure?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc 'add [NAME]', 'Create a new connection profile'
|
|
13
|
+
long_desc <<~DESC
|
|
14
|
+
Prompts for hostname and API token and saves a named profile in ~/.wisco/profiles.yaml.
|
|
15
|
+
If NAME is omitted, a name is suggested based on the selected datacenter.
|
|
16
|
+
DESC
|
|
17
|
+
def add(name = nil)
|
|
18
|
+
hostname = Wisco::Profiles.prompt_hostname_selection
|
|
19
|
+
puts "Hostname: #{hostname}"
|
|
20
|
+
|
|
21
|
+
api_token = Wisco::Profiles.prompt_token('API token (input hidden): ')
|
|
22
|
+
|
|
23
|
+
if name.nil?
|
|
24
|
+
suggestion = Wisco::Profiles.suggest_name(hostname)
|
|
25
|
+
name = Wisco::Profiles.prompt_name(suggestion: suggestion)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if Wisco::Profiles.exists?(name)
|
|
29
|
+
Wisco::TerminalOutput.emit_error("Error: Profile \"#{name}\" already exists. Use 'wisco profile edit #{name}' to update it.")
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Wisco::Profiles.set(name, hostname, api_token)
|
|
34
|
+
puts "Profile \"#{name}\" saved to #{Wisco::Profiles.profiles_path}."
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc 'list', 'List all connection profiles'
|
|
38
|
+
def list
|
|
39
|
+
profiles = Wisco::Profiles.all
|
|
40
|
+
if profiles.empty?
|
|
41
|
+
puts "No profiles found. Run 'wisco profile add' to create one."
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
puts "Profiles (from #{Wisco::Profiles.profiles_path}):\n\n"
|
|
45
|
+
name_width = profiles.keys.map(&:length).max
|
|
46
|
+
profiles.each do |prof_name, p|
|
|
47
|
+
puts " #{prof_name.ljust(name_width)} #{p['hostname']}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc 'show NAME', 'Show details of a profile'
|
|
52
|
+
def show(name)
|
|
53
|
+
profile = Wisco::Profiles.get(name)
|
|
54
|
+
if profile.nil?
|
|
55
|
+
Wisco::TerminalOutput.emit_error("Error: Profile \"#{name}\" not found.")
|
|
56
|
+
Wisco::TerminalOutput.emit_error(" Run 'wisco profile list' to see available profiles.")
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
puts "Profile: #{name}"
|
|
60
|
+
puts " hostname: #{profile['hostname']}"
|
|
61
|
+
puts " api_token: #{Wisco::Profiles.mask_token(profile['api_token'])}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
desc 'edit NAME', 'Update the hostname and/or API token for a profile'
|
|
65
|
+
long_desc 'Re-prompts for hostname and API token. Press Enter to keep the current value.'
|
|
66
|
+
def edit(name)
|
|
67
|
+
profile = Wisco::Profiles.get(name)
|
|
68
|
+
if profile.nil?
|
|
69
|
+
Wisco::TerminalOutput.emit_error("Error: Profile \"#{name}\" not found.")
|
|
70
|
+
Wisco::TerminalOutput.emit_error(" Run 'wisco profile list' to see available profiles.")
|
|
71
|
+
exit 1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
puts "Editing profile \"#{name}\":"
|
|
75
|
+
puts " Current hostname: #{profile['hostname']}"
|
|
76
|
+
puts " Current api_token: #{Wisco::Profiles.mask_token(profile['api_token'])}"
|
|
77
|
+
puts
|
|
78
|
+
|
|
79
|
+
selected = Wisco::Profiles.prompt_hostname_selection(allow_skip: true)
|
|
80
|
+
hostname = selected || profile['hostname']
|
|
81
|
+
puts "Hostname: #{hostname}"
|
|
82
|
+
|
|
83
|
+
raw = Wisco::Profiles.prompt_token('New API token (input hidden, Enter to keep current): ')
|
|
84
|
+
api_token = raw.empty? ? profile['api_token'] : raw
|
|
85
|
+
|
|
86
|
+
Wisco::Profiles.set(name, hostname, api_token)
|
|
87
|
+
puts "Profile \"#{name}\" updated."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
desc 'remove NAME', 'Remove a connection profile'
|
|
91
|
+
def remove(name)
|
|
92
|
+
unless Wisco::Profiles.exists?(name)
|
|
93
|
+
Wisco::TerminalOutput.emit_error("Error: Profile \"#{name}\" not found.")
|
|
94
|
+
exit 1
|
|
95
|
+
end
|
|
96
|
+
print "Remove profile \"#{name}\"? (y/n): "
|
|
97
|
+
unless $stdin.gets.strip.downcase == 'y'
|
|
98
|
+
puts 'Aborted.'
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
Wisco::Profiles.delete(name)
|
|
102
|
+
puts "Profile \"#{name}\" removed."
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
desc 'use NAME', 'Attach a profile to the current connector project'
|
|
106
|
+
def use(name)
|
|
107
|
+
unless Wisco::Profiles.exists?(name)
|
|
108
|
+
Wisco::TerminalOutput.emit_error("Error: Profile \"#{name}\" not found in #{Wisco::Profiles.profiles_path}.")
|
|
109
|
+
Wisco::TerminalOutput.emit_error(" Run 'wisco profile list' to see available profiles.")
|
|
110
|
+
exit 1
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
config_path = Wisco.config_path(Dir.pwd)
|
|
114
|
+
unless File.exist?(config_path)
|
|
115
|
+
Wisco::TerminalOutput.emit_error("Error: No #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} found in #{Dir.pwd}.")
|
|
116
|
+
Wisco::TerminalOutput.emit_error(" Run 'wisco init' first.")
|
|
117
|
+
exit 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
config = Wisco::Config.load_config(config_path)
|
|
121
|
+
api_cfg = config['workato_developer_api'] ||= {}
|
|
122
|
+
has_inline = !api_cfg['hostname'].to_s.strip.empty? || !api_cfg['api_token'].to_s.strip.empty?
|
|
123
|
+
|
|
124
|
+
if has_inline
|
|
125
|
+
print "This project has inline credentials configured.\nReplace with a reference to profile \"#{name}\"? (y/n): "
|
|
126
|
+
unless $stdin.gets.strip.downcase == 'y'
|
|
127
|
+
puts 'Aborted.'
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
api_cfg.delete('hostname')
|
|
131
|
+
api_cfg.delete('api_token')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
api_cfg['profile'] = name
|
|
135
|
+
Wisco::Config.save_config(config_path, config)
|
|
136
|
+
puts "Project config updated to use profile \"#{name}\"."
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
desc 'extract [NAME]', "Save this project's inline credentials as a named profile"
|
|
140
|
+
long_desc <<~DESC
|
|
141
|
+
Reads the inline hostname and api_token from this project's .wisco/config.json,
|
|
142
|
+
saves them as a named profile in ~/.wisco/profiles.yaml, then updates config.json
|
|
143
|
+
to reference the profile instead of storing inline credentials.
|
|
144
|
+
|
|
145
|
+
If NAME is omitted, a name is suggested based on the project's configured hostname.
|
|
146
|
+
DESC
|
|
147
|
+
def extract(name = nil)
|
|
148
|
+
config_path = Wisco.config_path(Dir.pwd)
|
|
149
|
+
unless File.exist?(config_path)
|
|
150
|
+
Wisco::TerminalOutput.emit_error("Error: No #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} found in #{Dir.pwd}.")
|
|
151
|
+
Wisco::TerminalOutput.emit_error(" Run 'wisco init' first.")
|
|
152
|
+
exit 1
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
config = Wisco::Config.load_config(config_path)
|
|
156
|
+
api_cfg = config['workato_developer_api'] || {}
|
|
157
|
+
hostname = api_cfg['hostname'].to_s.strip
|
|
158
|
+
api_token = api_cfg['api_token'].to_s.strip
|
|
159
|
+
|
|
160
|
+
if hostname.empty? || api_token.empty?
|
|
161
|
+
Wisco::TerminalOutput.emit_error('Error: This project does not have inline credentials to extract.')
|
|
162
|
+
Wisco::TerminalOutput.emit_error(" Use 'wisco profile use <name>' to attach an existing profile.")
|
|
163
|
+
exit 1
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
duplicate = Wisco::Profiles.find_duplicate(hostname, api_token)
|
|
167
|
+
if duplicate
|
|
168
|
+
puts "A profile with these credentials already exists: #{duplicate} (#{hostname})"
|
|
169
|
+
print 'Reference existing profile instead of creating a new one? (y/n): '
|
|
170
|
+
if $stdin.gets.strip.downcase == 'y'
|
|
171
|
+
api_cfg.delete('hostname')
|
|
172
|
+
api_cfg.delete('api_token')
|
|
173
|
+
api_cfg['profile'] = duplicate
|
|
174
|
+
Wisco::Config.save_config(config_path, config)
|
|
175
|
+
puts "Project config updated to reference profile \"#{duplicate}\"."
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if name.nil?
|
|
181
|
+
suggestion = Wisco::Profiles.suggest_name(hostname)
|
|
182
|
+
name = Wisco::Profiles.prompt_name(suggestion: suggestion)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
if Wisco::Profiles.exists?(name)
|
|
186
|
+
Wisco::TerminalOutput.emit_error("Error: Profile \"#{name}\" already exists. Choose a different name or use 'wisco profile edit #{name}'.")
|
|
187
|
+
exit 1
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
Wisco::Profiles.set(name, hostname, api_token)
|
|
191
|
+
puts "Profile \"#{name}\" created in #{Wisco::Profiles.profiles_path}."
|
|
192
|
+
|
|
193
|
+
api_cfg.delete('hostname')
|
|
194
|
+
api_cfg.delete('api_token')
|
|
195
|
+
api_cfg['profile'] = name
|
|
196
|
+
Wisco::Config.save_config(config_path, config)
|
|
197
|
+
puts "Project config updated to reference profile \"#{name}\"."
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
desc 'current', 'Show which profile or credentials this project is using'
|
|
201
|
+
def current
|
|
202
|
+
config_path = Wisco.config_path(Dir.pwd)
|
|
203
|
+
unless File.exist?(config_path)
|
|
204
|
+
Wisco::TerminalOutput.emit_error("Error: No #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} found in #{Dir.pwd}.")
|
|
205
|
+
Wisco::TerminalOutput.emit_error(" Run 'wisco init' first.")
|
|
206
|
+
exit 1
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
config = Wisco::Config.load_config(config_path)
|
|
210
|
+
api_cfg = config['workato_developer_api'] || {}
|
|
211
|
+
prof_name = api_cfg['profile'].to_s.strip
|
|
212
|
+
hostname = api_cfg['hostname'].to_s.strip
|
|
213
|
+
api_token = api_cfg['api_token'].to_s.strip
|
|
214
|
+
|
|
215
|
+
if !prof_name.empty?
|
|
216
|
+
profile = Wisco::Profiles.get(prof_name)
|
|
217
|
+
if profile.nil?
|
|
218
|
+
Wisco::TerminalOutput.emit_error("This project references profile \"#{prof_name}\" which does not exist.")
|
|
219
|
+
Wisco::TerminalOutput.emit_error("Run 'wisco profile list' to see available profiles.")
|
|
220
|
+
exit 1
|
|
221
|
+
end
|
|
222
|
+
puts "This project uses profile: #{prof_name}"
|
|
223
|
+
puts " hostname: #{profile['hostname']}"
|
|
224
|
+
puts " api_token: #{Wisco::Profiles.mask_token(profile['api_token'])}"
|
|
225
|
+
elsif !hostname.empty?
|
|
226
|
+
puts 'This project uses inline credentials (no profile).'
|
|
227
|
+
puts " hostname: #{hostname}"
|
|
228
|
+
puts " api_token: #{Wisco::Profiles.mask_token(api_token)}"
|
|
229
|
+
else
|
|
230
|
+
puts 'This project has no Workato API credentials configured.'
|
|
231
|
+
puts "Run 'wisco init' or 'wisco profile use <name>' to configure."
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
data/lib/wisco/config.rb
CHANGED
|
@@ -2,10 +2,32 @@ module Wisco
|
|
|
2
2
|
module Config
|
|
3
3
|
module_function
|
|
4
4
|
|
|
5
|
-
# Ensures workato_developer_api hostname and api_token are
|
|
6
|
-
#
|
|
5
|
+
# Ensures workato_developer_api hostname and api_token are available in config.
|
|
6
|
+
# Resolves profile references, handles conflicts, and prompts for missing inline values.
|
|
7
|
+
# When a profile is used, credentials are injected into the in-memory config so all
|
|
8
|
+
# callers can continue to use config.dig('workato_developer_api', 'hostname') etc.
|
|
7
9
|
def ensure_api_config(config, config_path)
|
|
8
|
-
api_cfg
|
|
10
|
+
api_cfg = config['workato_developer_api'] ||= {}
|
|
11
|
+
has_profile = !api_cfg['profile'].to_s.strip.empty?
|
|
12
|
+
has_inline = !api_cfg['hostname'].to_s.strip.empty? && !api_cfg['api_token'].to_s.strip.empty?
|
|
13
|
+
|
|
14
|
+
if has_profile && has_inline
|
|
15
|
+
resolve_profile_conflict(api_cfg, config, config_path)
|
|
16
|
+
has_profile = !api_cfg['profile'].to_s.strip.empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if has_profile
|
|
20
|
+
profile_name = api_cfg['profile']
|
|
21
|
+
profile = Wisco::Profiles.get(profile_name)
|
|
22
|
+
if profile.nil?
|
|
23
|
+
Wisco::TerminalOutput.emit_error("Error: Profile \"#{profile_name}\" not found in #{Wisco::Profiles.profiles_path}.")
|
|
24
|
+
Wisco::TerminalOutput.emit_error(" Run 'wisco profile list' to see available profiles.")
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
api_cfg['hostname'] = profile['hostname']
|
|
28
|
+
api_cfg['api_token'] = profile['api_token']
|
|
29
|
+
return config
|
|
30
|
+
end
|
|
9
31
|
|
|
10
32
|
if api_cfg['hostname'].nil? || api_cfg['hostname'].strip.empty?
|
|
11
33
|
print 'Workato API hostname not configured. Enter hostname (e.g. app.au.workato.com): '
|
|
@@ -21,6 +43,34 @@ module Wisco
|
|
|
21
43
|
config
|
|
22
44
|
end
|
|
23
45
|
|
|
46
|
+
def resolve_profile_conflict(api_cfg, config, config_path)
|
|
47
|
+
Wisco::TerminalOutput.emit_warning('Warning: This project has both a profile reference and inline credentials.')
|
|
48
|
+
puts " Profile: #{api_cfg['profile']}"
|
|
49
|
+
puts " Hostname: #{api_cfg['hostname']}"
|
|
50
|
+
puts
|
|
51
|
+
puts 'Which should be kept?'
|
|
52
|
+
puts " 1. Profile reference (\"#{api_cfg['profile']}\")"
|
|
53
|
+
puts ' 2. Inline credentials'
|
|
54
|
+
loop do
|
|
55
|
+
print 'Enter 1 or 2: '
|
|
56
|
+
case $stdin.gets.strip
|
|
57
|
+
when '1'
|
|
58
|
+
api_cfg.delete('hostname')
|
|
59
|
+
api_cfg.delete('api_token')
|
|
60
|
+
save_config(config_path, config)
|
|
61
|
+
puts "Inline credentials removed. Using profile \"#{api_cfg['profile']}\"."
|
|
62
|
+
return
|
|
63
|
+
when '2'
|
|
64
|
+
api_cfg.delete('profile')
|
|
65
|
+
save_config(config_path, config)
|
|
66
|
+
puts 'Profile reference removed. Using inline credentials.'
|
|
67
|
+
return
|
|
68
|
+
else
|
|
69
|
+
warn 'Please enter 1 or 2.'
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
24
74
|
def load_config(path)
|
|
25
75
|
return {} unless File.exist?(path)
|
|
26
76
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
require 'workato/connector/sdk'
|
|
2
|
+
require_relative 'terminal_output'
|
|
3
|
+
|
|
4
|
+
module Wisco
|
|
5
|
+
# Evaluates an `execute_*.rb` script and returns the value of its last
|
|
6
|
+
# expression. The script is eval'd inside a binding that exposes helpers
|
|
7
|
+
# (`call_method`, `call_pick_list`, `connection`) so the script can build
|
|
8
|
+
# dynamic input that varies per run.
|
|
9
|
+
module ExecScript
|
|
10
|
+
SENTINEL_REGEX = /\A\s*#\s*WISCO_SKIP\b/.freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Returns true if the file's first non-blank line is a `# WISCO_SKIP`
|
|
15
|
+
# comment, in which case the script should be skipped by `wisco exec`.
|
|
16
|
+
def sentinel?(path)
|
|
17
|
+
File.foreach(path) do |line|
|
|
18
|
+
stripped = line.strip
|
|
19
|
+
next if stripped.empty?
|
|
20
|
+
return SENTINEL_REGEX.match?(line)
|
|
21
|
+
end
|
|
22
|
+
false
|
|
23
|
+
rescue StandardError
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Evaluates the script at `script_path` and returns the value of its last
|
|
28
|
+
# expression. Raises StandardError subclasses on:
|
|
29
|
+
# - syntax / runtime errors in the script
|
|
30
|
+
# - invalid return value (must be a Hash or Array)
|
|
31
|
+
#
|
|
32
|
+
# `connector_full_path` — absolute path to connector.rb (used to build
|
|
33
|
+
# the SDK Connector for helper invocations).
|
|
34
|
+
# `target_dir` — connector project root (used to find
|
|
35
|
+
# settings.yaml(.enc) + master.key).
|
|
36
|
+
# `connection_name` — connection key inside settings.yaml; defaults to 'default'.
|
|
37
|
+
def evaluate(script_path:, connector_full_path:, target_dir:, connection_name: 'default')
|
|
38
|
+
host = ScriptHost.new(connector_full_path, target_dir, connection_name)
|
|
39
|
+
source = File.read(script_path)
|
|
40
|
+
result = host.instance_eval(source, script_path, 1)
|
|
41
|
+
|
|
42
|
+
unless result.is_a?(Hash) || result.is_a?(Array)
|
|
43
|
+
raise InvalidReturn,
|
|
44
|
+
"Script must return a Hash (for actions/triggers) or Array/Hash (for methods/pick_lists). " \
|
|
45
|
+
"Got: #{result.class}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class InvalidReturn < StandardError; end
|
|
52
|
+
|
|
53
|
+
# Host object that the script is eval'd against. Exposes the helper API.
|
|
54
|
+
# Defined as a class (not a Module) so `instance_eval` gives the script
|
|
55
|
+
# access to standard top-level Ruby plus our helpers, without polluting
|
|
56
|
+
# Object globally.
|
|
57
|
+
class ScriptHost
|
|
58
|
+
def initialize(connector_full_path, target_dir, connection_name)
|
|
59
|
+
@connector_full_path = connector_full_path
|
|
60
|
+
@target_dir = target_dir
|
|
61
|
+
@connection_name = connection_name || 'default'
|
|
62
|
+
@sdk_connector = nil
|
|
63
|
+
@connection_loaded = false
|
|
64
|
+
@connection_cached = nil
|
|
65
|
+
@warned_settings = false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Hash of decrypted settings for the configured connection. Returns {}
|
|
69
|
+
# and emits a one-time warning if settings can't be loaded.
|
|
70
|
+
def connection
|
|
71
|
+
return @connection_cached if @connection_loaded
|
|
72
|
+
|
|
73
|
+
@connection_loaded = true
|
|
74
|
+
@connection_cached = load_connection
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Invoke a connector `methods:` entry. Args are forwarded to the lambda
|
|
78
|
+
# as-is. Returns the lambda's return value.
|
|
79
|
+
#
|
|
80
|
+
# Example:
|
|
81
|
+
# call_method(:format_timestamp, Time.now)
|
|
82
|
+
def call_method(name, *args)
|
|
83
|
+
proxy = sdk_connector.methods
|
|
84
|
+
unless proxy.respond_to?(name.to_sym)
|
|
85
|
+
raise InvalidReturn, "No methods entry named '#{name}' found in connector."
|
|
86
|
+
end
|
|
87
|
+
proxy.public_send(name.to_sym, *args)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Invoke a connector `pick_lists:` entry. `args` is a Hash of named
|
|
91
|
+
# arguments for dependent pick lists. The SDK supplies `connection` to
|
|
92
|
+
# the lambda automatically.
|
|
93
|
+
#
|
|
94
|
+
# Examples:
|
|
95
|
+
# call_pick_list(:active_customers)
|
|
96
|
+
# call_pick_list(:customer_orders, customer_id: 123)
|
|
97
|
+
def call_pick_list(name, **args)
|
|
98
|
+
proxy = sdk_connector.pick_lists
|
|
99
|
+
unless proxy.respond_to?(name.to_sym)
|
|
100
|
+
raise InvalidReturn, "No pick_lists entry named '#{name}' found in connector."
|
|
101
|
+
end
|
|
102
|
+
# SDK signature: pick_list_name(settings = nil, args = {})
|
|
103
|
+
# Passing nil for settings tells the SDK to reuse the connector's settings.
|
|
104
|
+
proxy.public_send(name.to_sym, nil, args)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def sdk_connector
|
|
110
|
+
@sdk_connector ||= Workato::Connector::Sdk::Connector.from_file(@connector_full_path, connection)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Tries (in order): settings.yaml.enc + master.key, then settings.yaml.
|
|
114
|
+
# Uses Workato::Connector::Sdk::Settings so encryption is handled by the
|
|
115
|
+
# SDK's own loader (same path it uses for `wisco exec`). Returns {} and
|
|
116
|
+
# warns once on any failure.
|
|
117
|
+
def load_connection
|
|
118
|
+
enc_file = File.join(@target_dir, 'settings.yaml.enc')
|
|
119
|
+
key_file = File.join(@target_dir, 'master.key')
|
|
120
|
+
yaml_file = File.join(@target_dir, 'settings.yaml')
|
|
121
|
+
|
|
122
|
+
if File.exist?(enc_file) && File.exist?(key_file)
|
|
123
|
+
read_settings(enc_file) do |name|
|
|
124
|
+
Workato::Connector::Sdk::Settings.from_encrypted_file(enc_file, key_file, name)
|
|
125
|
+
end
|
|
126
|
+
elsif File.exist?(yaml_file)
|
|
127
|
+
read_settings(yaml_file) do |name|
|
|
128
|
+
Workato::Connector::Sdk::Settings.from_file(yaml_file, name)
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
warn_settings("No settings.yaml or settings.yaml.enc found in #{@target_dir}")
|
|
132
|
+
{}
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Reads settings using a Settings.from_* loader. Tries with the configured
|
|
137
|
+
# connection name first; if that key isn't present at the top level
|
|
138
|
+
# (KeyError), falls back to loading the whole file as a flat hash.
|
|
139
|
+
def read_settings(path)
|
|
140
|
+
hash = begin
|
|
141
|
+
yield(@connection_name)
|
|
142
|
+
rescue KeyError
|
|
143
|
+
yield(nil)
|
|
144
|
+
end
|
|
145
|
+
return {} unless hash.respond_to?(:to_h)
|
|
146
|
+
|
|
147
|
+
hash.to_h
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
warn_settings("Failed to load settings from #{path}: #{e.message}")
|
|
150
|
+
{}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def warn_settings(msg)
|
|
154
|
+
return if @warned_settings
|
|
155
|
+
@warned_settings = true
|
|
156
|
+
Wisco::TerminalOutput.emit_warning("[WARN] connection helper: #{msg}. Returning {}.")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
5
|
+
module Wisco
|
|
6
|
+
module Profiles
|
|
7
|
+
PROFILES_DIR = File.join(Dir.home, '.wisco').freeze
|
|
8
|
+
PROFILES_FILENAME = 'profiles.yaml'.freeze
|
|
9
|
+
|
|
10
|
+
HOSTNAMES = [
|
|
11
|
+
{ region: 'US Data Center', hostname: 'www.workato.com' },
|
|
12
|
+
{ region: 'EU Data Center', hostname: 'app.eu.workato.com' },
|
|
13
|
+
{ region: 'JP Data Center', hostname: 'app.jp.workato.com' },
|
|
14
|
+
{ region: 'SG Data Center', hostname: 'app.sg.workato.com' },
|
|
15
|
+
{ region: 'AU Data Center', hostname: 'app.au.workato.com' },
|
|
16
|
+
{ region: 'IL Data Center', hostname: 'app.il.workato.com' },
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
DATACENTER_NAMES = {
|
|
20
|
+
'www.workato.com' => 'us',
|
|
21
|
+
'app.eu.workato.com' => 'eu',
|
|
22
|
+
'app.jp.workato.com' => 'jp',
|
|
23
|
+
'app.sg.workato.com' => 'sg',
|
|
24
|
+
'app.au.workato.com' => 'au',
|
|
25
|
+
'app.il.workato.com' => 'il',
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def profiles_path
|
|
31
|
+
File.join(PROFILES_DIR, PROFILES_FILENAME)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def load_profiles
|
|
35
|
+
return {} unless File.exist?(profiles_path)
|
|
36
|
+
|
|
37
|
+
YAML.safe_load(File.read(profiles_path)) || {}
|
|
38
|
+
rescue Psych::SyntaxError => e
|
|
39
|
+
Wisco::TerminalOutput.emit_error("Error: Could not parse #{profiles_path}: #{e.message}")
|
|
40
|
+
exit 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def save_profiles(data)
|
|
44
|
+
FileUtils.mkdir_p(PROFILES_DIR)
|
|
45
|
+
File.write(profiles_path, YAML.dump(data))
|
|
46
|
+
rescue SystemCallError => e
|
|
47
|
+
Wisco::TerminalOutput.emit_error("Error: Could not write #{profiles_path}: #{e.message}")
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def all
|
|
52
|
+
load_profiles.dig('workato_developer_api') || {}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def get(name)
|
|
56
|
+
all[name]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def set(name, hostname, api_token)
|
|
60
|
+
data = load_profiles
|
|
61
|
+
data['workato_developer_api'] ||= {}
|
|
62
|
+
data['workato_developer_api'][name] = { 'hostname' => hostname, 'api_token' => api_token }
|
|
63
|
+
save_profiles(data)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def delete(name)
|
|
67
|
+
data = load_profiles
|
|
68
|
+
data['workato_developer_api']&.delete(name)
|
|
69
|
+
save_profiles(data)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def exists?(name)
|
|
73
|
+
!get(name).nil?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def find_duplicate(hostname, api_token)
|
|
77
|
+
all.find { |_n, p| p['hostname'] == hostname && p['api_token'] == api_token }&.first
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def suggest_name(hostname)
|
|
81
|
+
DATACENTER_NAMES[hostname]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def mask_token(token)
|
|
85
|
+
return '****' if token.nil? || token.length <= 4
|
|
86
|
+
|
|
87
|
+
"****#{token[-4..]}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def prompt_token(prompt_text = 'API token (input hidden): ')
|
|
91
|
+
print prompt_text
|
|
92
|
+
token = $stdin.noecho(&:gets).strip
|
|
93
|
+
puts
|
|
94
|
+
token
|
|
95
|
+
rescue Errno::ENOTTY
|
|
96
|
+
# stdin is not a tty (e.g. piped input) — read plainly
|
|
97
|
+
print prompt_text
|
|
98
|
+
$stdin.gets.strip
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def prompt_hostname_selection(allow_skip: false)
|
|
102
|
+
puts 'Select your Workato instance:'
|
|
103
|
+
HOSTNAMES.each_with_index do |entry, i|
|
|
104
|
+
puts " #{i + 1}. #{entry[:hostname]} (#{entry[:region]})"
|
|
105
|
+
end
|
|
106
|
+
skip_hint = allow_skip ? ', or Enter to keep current' : ''
|
|
107
|
+
loop do
|
|
108
|
+
print "Enter number (1-#{HOSTNAMES.size})#{skip_hint}: "
|
|
109
|
+
input = $stdin.gets.strip
|
|
110
|
+
return nil if allow_skip && input.empty?
|
|
111
|
+
|
|
112
|
+
index = input.to_i
|
|
113
|
+
if index >= 1 && index <= HOSTNAMES.size
|
|
114
|
+
return HOSTNAMES[index - 1][:hostname]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
warn "Invalid selection. Please enter a number between 1 and #{HOSTNAMES.size}."
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def prompt_name(suggestion: nil)
|
|
122
|
+
if suggestion
|
|
123
|
+
print "Profile name [#{suggestion}]: "
|
|
124
|
+
input = $stdin.gets.strip
|
|
125
|
+
input.empty? ? suggestion : input
|
|
126
|
+
else
|
|
127
|
+
loop do
|
|
128
|
+
print 'Profile name: '
|
|
129
|
+
input = $stdin.gets.strip
|
|
130
|
+
return input unless input.empty?
|
|
131
|
+
|
|
132
|
+
warn 'Name cannot be blank.'
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
data/lib/wisco/version.rb
CHANGED
data/lib/wisco.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Wisco
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
require_relative 'wisco/config'
|
|
18
|
+
require_relative 'wisco/profile'
|
|
18
19
|
require_relative 'wisco/connector'
|
|
19
20
|
require_relative 'wisco/commands/init'
|
|
20
21
|
require_relative 'wisco/commands/list'
|
|
@@ -23,6 +24,7 @@ require_relative 'wisco/commands/fixtures'
|
|
|
23
24
|
require_relative 'wisco/commands/pull'
|
|
24
25
|
require_relative 'wisco/commands/push'
|
|
25
26
|
require_relative 'wisco/commands/schema'
|
|
27
|
+
require_relative 'wisco/commands/profile'
|
|
26
28
|
|
|
27
29
|
module Wisco
|
|
28
30
|
class CLI < Thor
|
|
@@ -53,8 +55,9 @@ module Wisco
|
|
|
53
55
|
|
|
54
56
|
desc 'init [PATH]', "Detect connector and initialise #{WISCO_DIR}/"
|
|
55
57
|
long_desc "Searches PATH (default: current directory) for a valid connector file and writes #{WISCO_DIR}/#{CONFIG_FILENAME}."
|
|
58
|
+
option :profile, type: :string, desc: 'Attach a named connection profile instead of entering credentials manually'
|
|
56
59
|
def init(path = nil)
|
|
57
|
-
Wisco::Commands::Init.run(path || Dir.pwd)
|
|
60
|
+
Wisco::Commands::Init.run(path || Dir.pwd, profile: options[:profile])
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
desc 'list [SUBCOMMAND] [PATH]', 'Show connector structure'
|
|
@@ -124,12 +127,15 @@ module Wisco
|
|
|
124
127
|
get_users auto-detect section
|
|
125
128
|
DESC
|
|
126
129
|
option :overwrite, type: :boolean, default: false, desc: 'Overwrite execute_input.json even if user-edited'
|
|
130
|
+
option :ruby, type: :boolean, default: false,
|
|
131
|
+
desc: 'Also scaffold an execute_input.rb template for dynamic input scripts'
|
|
127
132
|
option :debug, type: :boolean, default: false, desc: 'Print ExecCommand call details'
|
|
128
133
|
def fixtures(path_arg, target_dir = nil)
|
|
129
134
|
Wisco::Commands::Fixtures.run(
|
|
130
135
|
path_arg,
|
|
131
136
|
target_dir || Dir.pwd,
|
|
132
137
|
overwrite: options[:overwrite],
|
|
138
|
+
ruby: options[:ruby],
|
|
133
139
|
debug: options[:debug]
|
|
134
140
|
)
|
|
135
141
|
end
|
|
@@ -179,6 +185,20 @@ module Wisco
|
|
|
179
185
|
)
|
|
180
186
|
end
|
|
181
187
|
|
|
188
|
+
desc 'profile SUBCOMMAND ...ARGS', 'Manage connection profiles (~/.wisco/profiles.yaml)'
|
|
189
|
+
long_desc <<~DESC
|
|
190
|
+
Subcommands:
|
|
191
|
+
add [NAME] Create a new profile
|
|
192
|
+
list List all profiles
|
|
193
|
+
show NAME Show profile details
|
|
194
|
+
edit NAME Update a profile's hostname or API token
|
|
195
|
+
remove NAME Remove a profile
|
|
196
|
+
use NAME Attach a profile to this connector project
|
|
197
|
+
extract [NAME] Save this project's inline credentials as a profile
|
|
198
|
+
current Show which profile this project is using
|
|
199
|
+
DESC
|
|
200
|
+
subcommand 'profile', Wisco::Commands::Profile
|
|
201
|
+
|
|
182
202
|
desc 'schema INPUT_FILE [TARGET_DIR]', 'Generate a schema from a JSON or CSV sample file'
|
|
183
203
|
long_desc <<~DESC
|
|
184
204
|
Calls the Workato API to generate a schema from a sample JSON or CSV file.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wisco
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- mbillington
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -71,12 +71,15 @@ files:
|
|
|
71
71
|
- lib/wisco/commands/fixtures.rb
|
|
72
72
|
- lib/wisco/commands/init.rb
|
|
73
73
|
- lib/wisco/commands/list.rb
|
|
74
|
+
- lib/wisco/commands/profile.rb
|
|
74
75
|
- lib/wisco/commands/pull.rb
|
|
75
76
|
- lib/wisco/commands/push.rb
|
|
76
77
|
- lib/wisco/commands/schema.rb
|
|
77
78
|
- lib/wisco/config.rb
|
|
78
79
|
- lib/wisco/connector.rb
|
|
80
|
+
- lib/wisco/exec_script.rb
|
|
79
81
|
- lib/wisco/path_utils.rb
|
|
82
|
+
- lib/wisco/profile.rb
|
|
80
83
|
- lib/wisco/terminal_output.rb
|
|
81
84
|
- lib/wisco/version.rb
|
|
82
85
|
- lib/wisco/workato_api.rb
|