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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7db313badb869b62a2bf9eab0b4558cecffe8946bb8acaa0664cb4d65e3641f
4
- data.tar.gz: fa68ec90e00dec3934be909a3f9b5cb122bd93a8dac3801733eba4f066f2f4f4
3
+ metadata.gz: 942295e665d5ecea09078c736425531bb9846bfe29db26223bf52034ff4221b0
4
+ data.tar.gz: c56480bf18d42ac7797dd2b46f9d50146788f1590e673f4056bbe0ce55580347
5
5
  SHA512:
6
- metadata.gz: be79dffcc14ff47e9333e175a0f6c967c28646045ba35094bde7740903b9d64a44a510c98f47ff676b4baf902c651de7bf4beda8b4e0d69f61bc03411e861b41
7
- data.tar.gz: 5340d7f07bd891bc4ac456a576749cffe1cb948df63af61dbe0192e22d92393d4a61b4c3bdb5d642b99446a1af96a48df0d782394728f2b19fda78d4428ef75d
6
+ metadata.gz: 797b1ac153469a31d1edd1781510bac1ac7c05a7f7b2545de34a48d40540a79fcbffd189c851b85d830f9ca63424a5f9006f1a85915bdd9bd6da01f9fe2da392
7
+ data.tar.gz: 7133a41ae9da79285993f77ccd32e65ff49c3b3c0d203140c1f3c128bdfa8657c8cd882d435ab180b1bd51811103e3c51f16f8c49ee67150388e11a5062e1448
@@ -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
- execute_one(section, key, input_file, fixtures_dir,
75
- connector_full_path, connection, pagination: pagination, verbose: verbose,
76
- extended: extended, closure: closure, config_fields: config_fields,
77
- continue: continue, extended_input_schema: extended_input_schema,
78
- extended_output_schema: extended_output_schema, debug: debug)
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_* in fixtures_dir and exclude files still containing the sentinel.
86
- def resolve_input_files(input, fixtures_dir)
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_*')).select do |f|
96
- File.file?(f) && !file_has_sentinel?(f)
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 = '# Remove this comment before updating. Files that include this line will be overwritten.'
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.
@@ -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
- if current && !current.strip.empty?
64
- puts "Current Workato hostname: #{current}"
65
- print 'Keep this? (y/n): '
66
- return if $stdin.gets.strip.downcase == 'y'
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
- puts 'Select your Workato instance:'
70
- HOSTNAMES.each_with_index do |entry, i|
71
- puts " #{i + 1}. #{entry[:hostname]} (#{entry[:region]})"
72
- end
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
- loop do
75
- print "Enter number (1-#{HOSTNAMES.size}): "
76
- input = $stdin.gets.strip
77
- index = input.to_i
78
- if index >= 1 && index <= HOSTNAMES.size
79
- api_cfg['hostname'] = HOSTNAMES[index - 1][:hostname]
80
- puts "Hostname set to: #{api_cfg['hostname']}"
81
- return
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
- hostname = config.dig('workato_developer_api', 'hostname')
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 present in config.
6
- # Prompts the user for any missing values and saves the updated config.
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 = config['workato_developer_api'] ||= {}
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
@@ -1,3 +1,3 @@
1
1
  module Wisco
2
- VERSION = '0.3.5'
2
+ VERSION = '0.3.7'
3
3
  end
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.5
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-06 00:00:00.000000000 Z
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