wisco 0.4.2 → 0.4.4
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 +24 -8
- data/lib/wisco/commands/settings.rb +363 -0
- data/lib/wisco/settings_store.rb +133 -0
- data/lib/wisco/version.rb +1 -1
- data/lib/wisco.rb +16 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ab77bb62a7ce39484d205397fb9350d3b17f2fbdde6f11ce47517da942608a1
|
|
4
|
+
data.tar.gz: db26dddd97fba3b9f3471d408e0f80750375d0ab18111d36d20d3b603e447daf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5eb44d4bd05b130cfc4fb3f0c59325df601230e3c7be1e1e6b40f608f9ff4202bc0a99ee51a0fff177a2db71aba9e8ecb1dccad2fc7bf1b2a8a747f15a7c0d53
|
|
7
|
+
data.tar.gz: 6887090edc5bf94a7c7aa8d07878738da08ac009d498e18115e65952b2598fb2ec6a98610cf80f976fdecd66d996610874bb74fb81be6c581de7a7bf3973d35f
|
data/lib/wisco/commands/exec.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Wisco
|
|
|
16
16
|
def run(path_arg, target_dir, input: nil, pagination: true, verbose: false, debug: false,
|
|
17
17
|
extended: true, closure: nil, config_fields: nil, continue: nil,
|
|
18
18
|
extended_input_schema: nil, extended_output_schema: nil,
|
|
19
|
-
summary: true, summary_lines: 20)
|
|
19
|
+
summary: true, summary_lines: 20, auto_refresh: true)
|
|
20
20
|
target_dir = File.expand_path(target_dir)
|
|
21
21
|
config_path = Wisco.config_path(target_dir)
|
|
22
22
|
|
|
@@ -40,7 +40,8 @@ module Wisco
|
|
|
40
40
|
|
|
41
41
|
# ── connection test short-circuit ──────────────────────────────────
|
|
42
42
|
if path_arg == 'test'
|
|
43
|
-
run_test(target_dir, connector_full_path, connection, verbose: verbose, debug: debug
|
|
43
|
+
run_test(target_dir, connector_full_path, connection, verbose: verbose, debug: debug,
|
|
44
|
+
auto_refresh: auto_refresh)
|
|
44
45
|
return
|
|
45
46
|
end
|
|
46
47
|
|
|
@@ -68,7 +69,8 @@ module Wisco
|
|
|
68
69
|
closure: closure, config_fields: config_fields, continue: continue,
|
|
69
70
|
extended_input_schema: extended_input_schema,
|
|
70
71
|
extended_output_schema: extended_output_schema, debug: debug,
|
|
71
|
-
summary: summary, summary_lines: summary_lines, batch: batch
|
|
72
|
+
summary: summary, summary_lines: summary_lines, batch: batch,
|
|
73
|
+
auto_refresh: auto_refresh)
|
|
72
74
|
else
|
|
73
75
|
Wisco::TerminalOutput.emit_warning(" Warning: No ready input files found in #{fixture_dir_output}")
|
|
74
76
|
input_template = File.join(fixtures_dir, 'execute_input.json')
|
|
@@ -87,14 +89,16 @@ module Wisco
|
|
|
87
89
|
extended: extended, closure: closure, config_fields: config_fields,
|
|
88
90
|
continue: continue, extended_input_schema: extended_input_schema,
|
|
89
91
|
extended_output_schema: extended_output_schema, debug: debug,
|
|
90
|
-
summary: summary, summary_lines: summary_lines, batch: batch
|
|
92
|
+
summary: summary, summary_lines: summary_lines, batch: batch,
|
|
93
|
+
auto_refresh: auto_refresh)
|
|
91
94
|
else
|
|
92
95
|
execute_one(section, key, input_file, fixtures_dir,
|
|
93
96
|
connector_full_path, connection, pagination: pagination, verbose: verbose,
|
|
94
97
|
extended: extended, closure: closure, config_fields: config_fields,
|
|
95
98
|
continue: continue, extended_input_schema: extended_input_schema,
|
|
96
99
|
extended_output_schema: extended_output_schema, debug: debug,
|
|
97
|
-
summary: summary, summary_lines: summary_lines, batch: batch
|
|
100
|
+
summary: summary, summary_lines: summary_lines, batch: batch,
|
|
101
|
+
auto_refresh: auto_refresh)
|
|
98
102
|
end
|
|
99
103
|
end
|
|
100
104
|
end
|
|
@@ -153,7 +157,8 @@ module Wisco
|
|
|
153
157
|
File.dirname(value) == '.' ? File.join(fixtures_dir, value) : value
|
|
154
158
|
end
|
|
155
159
|
|
|
156
|
-
def run_test(target_dir, connector_full_path, connection, verbose: false, debug: false
|
|
160
|
+
def run_test(target_dir, connector_full_path, connection, verbose: false, debug: false,
|
|
161
|
+
auto_refresh: true)
|
|
157
162
|
puts "Testing connection"
|
|
158
163
|
fixtures_dir = File.join(target_dir, 'fixtures', 'connection', 'test')
|
|
159
164
|
FileUtils.mkdir_p(fixtures_dir)
|
|
@@ -174,6 +179,7 @@ module Wisco
|
|
|
174
179
|
|
|
175
180
|
begin
|
|
176
181
|
cmd = Workato::CLI::ExecCommand.new(path: 'test', options: options)
|
|
182
|
+
apply_auto_refresh(cmd) if auto_refresh
|
|
177
183
|
cmd.call
|
|
178
184
|
rescue StandardError => e
|
|
179
185
|
File.write(error_file, "#{e.class}: #{e.message}\n\n#{e.backtrace.join("\n")}\n")
|
|
@@ -194,7 +200,7 @@ module Wisco
|
|
|
194
200
|
pagination: true, verbose: false, debug: false,
|
|
195
201
|
extended: true, closure: nil, config_fields: nil, continue: nil,
|
|
196
202
|
extended_input_schema: nil, extended_output_schema: nil,
|
|
197
|
-
summary: true, summary_lines: 20, batch: false)
|
|
203
|
+
summary: true, summary_lines: 20, batch: false, auto_refresh: true)
|
|
198
204
|
stem = input_file ? File.basename(input_file, '.*') : 'execute'
|
|
199
205
|
output_file = File.join(fixtures_dir, "output_#{stem}.json")
|
|
200
206
|
error_file = File.join(fixtures_dir, "error_#{stem}.txt")
|
|
@@ -265,6 +271,7 @@ module Wisco
|
|
|
265
271
|
|
|
266
272
|
begin
|
|
267
273
|
cmd = Workato::CLI::ExecCommand.new(path: exec_path, options: options)
|
|
274
|
+
apply_auto_refresh(cmd) if auto_refresh
|
|
268
275
|
cmd.call
|
|
269
276
|
rescue StandardError => e
|
|
270
277
|
FileUtils.rm_f(output_file)
|
|
@@ -295,7 +302,7 @@ module Wisco
|
|
|
295
302
|
pagination: true, verbose: false, debug: false,
|
|
296
303
|
extended: true, closure: nil, config_fields: nil, continue: nil,
|
|
297
304
|
extended_input_schema: nil, extended_output_schema: nil,
|
|
298
|
-
summary: true, summary_lines: 20, batch: false)
|
|
305
|
+
summary: true, summary_lines: 20, batch: false, auto_refresh: true)
|
|
299
306
|
subdir = File.join(fixtures_dir, File.basename(script_path, '.rb'))
|
|
300
307
|
FileUtils.mkdir_p(subdir)
|
|
301
308
|
input_file = File.join(subdir, 'input.json')
|
|
@@ -379,6 +386,7 @@ module Wisco
|
|
|
379
386
|
# Step 3: invoke ExecCommand against the generated input
|
|
380
387
|
begin
|
|
381
388
|
cmd = Workato::CLI::ExecCommand.new(path: exec_path, options: options)
|
|
389
|
+
apply_auto_refresh(cmd) if auto_refresh
|
|
382
390
|
cmd.call
|
|
383
391
|
rescue StandardError => e
|
|
384
392
|
FileUtils.rm_f(input_file)
|
|
@@ -471,6 +479,14 @@ module Wisco
|
|
|
471
479
|
str.length > max ? "#{str[0, max - 1]}…" : str
|
|
472
480
|
end
|
|
473
481
|
|
|
482
|
+
# Overrides ask() on the ExecCommand singleton so the SDK's token-refresh
|
|
483
|
+
# prompt ("Updated settings file with new connection attributes? (Yes or No)")
|
|
484
|
+
# is answered "y" automatically. The refresh lambda captures self (the
|
|
485
|
+
# ExecCommand instance), so the singleton method wins over Thor::Shell::Basic.
|
|
486
|
+
def apply_auto_refresh(cmd)
|
|
487
|
+
cmd.define_singleton_method(:ask) { |*| 'y' }
|
|
488
|
+
end
|
|
489
|
+
|
|
474
490
|
# ── SDK array-args normalisation ───────────────────────────────────────
|
|
475
491
|
# The Workato SDK's InvokePath#invoke_method applies Array.wrap(args.last).flatten(1)
|
|
476
492
|
# when the last parameter is *args, which incorrectly unwraps array-valued positional
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require_relative '../config'
|
|
3
|
+
require_relative '../connector'
|
|
4
|
+
require_relative '../settings_store'
|
|
5
|
+
require_relative '../terminal_output'
|
|
6
|
+
|
|
7
|
+
module Wisco
|
|
8
|
+
module Commands
|
|
9
|
+
class Settings < Thor
|
|
10
|
+
def self.exit_on_failure?
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc 'list', 'List connection sets in the settings file'
|
|
15
|
+
option :format, type: :string, enum: %w[json], desc: 'Machine-readable output (json = array of set names)'
|
|
16
|
+
def list
|
|
17
|
+
store = settings_store
|
|
18
|
+
data = read_settings!(store)
|
|
19
|
+
names = Wisco::SettingsStore.set_names(data)
|
|
20
|
+
|
|
21
|
+
if options[:format] == 'json'
|
|
22
|
+
puts JSON.generate(names)
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
case Wisco::SettingsStore.detect_structure(data)
|
|
27
|
+
when :nested, :mixed
|
|
28
|
+
active = active_connection
|
|
29
|
+
puts "Connection sets (#{store.filename}):\n\n"
|
|
30
|
+
width = names.map(&:length).max || 0
|
|
31
|
+
names.each do |n|
|
|
32
|
+
marker = n == active ? '*' : ' '
|
|
33
|
+
puts " #{marker} #{n.ljust(width)}"
|
|
34
|
+
end
|
|
35
|
+
if active && !active.empty?
|
|
36
|
+
puts
|
|
37
|
+
puts "Active connection (from #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME}): #{active}"
|
|
38
|
+
end
|
|
39
|
+
when :flat
|
|
40
|
+
puts "#{store.filename} contains a single, unnamed connection set."
|
|
41
|
+
puts "No named connection sets are defined. Use 'wisco settings add <name>' to create named sets."
|
|
42
|
+
when :none
|
|
43
|
+
puts "No settings file found in #{store.connector_path}."
|
|
44
|
+
puts "Run 'wisco settings add <name>' to create one, or 'workato edit' to create an encrypted file."
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
desc 'set CONNECTION', "Set the project's active connection set"
|
|
49
|
+
def set(connection)
|
|
50
|
+
store = settings_store
|
|
51
|
+
names = begin
|
|
52
|
+
Wisco::SettingsStore.set_names(store.read_all)
|
|
53
|
+
rescue Wisco::SettingsStore::MissingKeyError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if names.nil?
|
|
58
|
+
Wisco::TerminalOutput.emit_warning("Warning: could not read #{store.filename} to validate (no master key).")
|
|
59
|
+
Wisco::TerminalOutput.emit_warning(' Config updated anyway.')
|
|
60
|
+
elsif !names.include?(connection)
|
|
61
|
+
Wisco::TerminalOutput.emit_warning("Warning: \"#{connection}\" is not currently defined in #{store.filename}.")
|
|
62
|
+
Wisco::TerminalOutput.emit_warning(" Defined sets: #{names.empty? ? '(none)' : names.join(', ')}")
|
|
63
|
+
Wisco::TerminalOutput.emit_warning(" Config updated anyway. Run 'wisco settings add #{connection}' to create it.")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
cfg = config
|
|
67
|
+
cfg['connection'] = connection
|
|
68
|
+
Wisco::Config.save_config(config_path, cfg)
|
|
69
|
+
puts "Active connection set to \"#{connection}\" in #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME}."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
desc 'add CONNECTION', 'Scaffold a new connection set from connector.connection.fields'
|
|
73
|
+
long_desc <<~DESC
|
|
74
|
+
Reads connection.fields from the connector and writes a new named connection
|
|
75
|
+
set with those field names and blank values. Fill the values in afterwards
|
|
76
|
+
with 'workato edit'. If the settings file currently holds a single unnamed
|
|
77
|
+
(flat) connection set, you are prompted to name it so the file can be
|
|
78
|
+
converted to the named (nested) format.
|
|
79
|
+
DESC
|
|
80
|
+
def add(connection)
|
|
81
|
+
field_names = connection_field_names!
|
|
82
|
+
if field_names.empty?
|
|
83
|
+
Wisco::TerminalOutput.emit_error('Error: The connector defines no connection.fields to scaffold.')
|
|
84
|
+
exit 1
|
|
85
|
+
end
|
|
86
|
+
new_set = field_names.each_with_object({}) { |n, h| h[n] = '' }
|
|
87
|
+
|
|
88
|
+
store = settings_store
|
|
89
|
+
data = read_settings!(store)
|
|
90
|
+
structure = Wisco::SettingsStore.detect_structure(data)
|
|
91
|
+
|
|
92
|
+
if Wisco::SettingsStore.set_names(data).include?(connection)
|
|
93
|
+
Wisco::TerminalOutput.emit_error("Error: Connection set \"#{connection}\" already exists in #{store.filename}.")
|
|
94
|
+
Wisco::TerminalOutput.emit_error(" Use 'wisco settings show #{connection}' to view it, or edit it with 'workato edit'.")
|
|
95
|
+
exit 1
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
target =
|
|
99
|
+
case structure
|
|
100
|
+
when :nested
|
|
101
|
+
data.merge(connection => new_set)
|
|
102
|
+
when :none
|
|
103
|
+
{ connection => new_set }
|
|
104
|
+
when :flat
|
|
105
|
+
migrate_flat(data, store).merge(connection => new_set)
|
|
106
|
+
when :mixed
|
|
107
|
+
Wisco::TerminalOutput.emit_error("Error: #{store.filename} mixes flat keys and named sets; resolve it with 'workato edit' before adding.")
|
|
108
|
+
exit 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A brand-new file is written plaintext; existing files keep their form.
|
|
112
|
+
encrypted = structure != :none && store.encrypted?
|
|
113
|
+
written = encrypted ? Wisco::SettingsStore::ENCRYPTED_FILENAME : Wisco::SettingsStore::PLAINTEXT_FILENAME
|
|
114
|
+
store.write_all(target, encrypted: encrypted)
|
|
115
|
+
|
|
116
|
+
puts "Added connection set \"#{connection}\" to #{written} with #{field_names.length} blank field(s):"
|
|
117
|
+
puts " #{field_names.join(', ')}"
|
|
118
|
+
puts "Fill in the values with 'workato edit', then run 'wisco settings set #{connection}'."
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
desc 'show [CONNECTION]', "Show a connection set's field values (passwords masked)"
|
|
122
|
+
option :format, type: :string, enum: %w[json], desc: 'Machine-readable output (json = connection fields with current values)'
|
|
123
|
+
def show(connection = nil)
|
|
124
|
+
store = settings_store
|
|
125
|
+
data = read_settings!(store)
|
|
126
|
+
|
|
127
|
+
name, set =
|
|
128
|
+
case Wisco::SettingsStore.detect_structure(data)
|
|
129
|
+
when :none
|
|
130
|
+
Wisco::TerminalOutput.emit_error("Error: No settings file found in #{store.connector_path}. Nothing to show.")
|
|
131
|
+
exit 1
|
|
132
|
+
when :flat
|
|
133
|
+
if connection
|
|
134
|
+
Wisco::TerminalOutput.emit_error("Error: #{store.filename} has a single unnamed connection set; there is no named set \"#{connection}\".")
|
|
135
|
+
exit 1
|
|
136
|
+
end
|
|
137
|
+
[nil, data]
|
|
138
|
+
when :nested, :mixed
|
|
139
|
+
names = Wisco::SettingsStore.set_names(data)
|
|
140
|
+
if connection.nil?
|
|
141
|
+
Wisco::TerminalOutput.emit_warning('Warning: This settings file has multiple connection sets; specify which one to show.')
|
|
142
|
+
Wisco::TerminalOutput.emit_warning(" Defined sets: #{names.join(', ')}")
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
unless names.include?(connection)
|
|
146
|
+
Wisco::TerminalOutput.emit_error("Error: Connection set \"#{connection}\" not found in #{store.filename}.")
|
|
147
|
+
Wisco::TerminalOutput.emit_error(" Defined sets: #{names.join(', ')}")
|
|
148
|
+
exit 1
|
|
149
|
+
end
|
|
150
|
+
[connection, data[connection]]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if options[:format] == 'json'
|
|
154
|
+
render_set_json(set)
|
|
155
|
+
else
|
|
156
|
+
render_set(name, set, store)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
desc 'current', 'Show which connection set the project points at'
|
|
161
|
+
def current
|
|
162
|
+
active = active_connection.to_s.strip
|
|
163
|
+
store = settings_store
|
|
164
|
+
data = begin
|
|
165
|
+
store.read_all
|
|
166
|
+
rescue Wisco::SettingsStore::MissingKeyError
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
names = data ? Wisco::SettingsStore.set_names(data) : []
|
|
170
|
+
|
|
171
|
+
if active.empty?
|
|
172
|
+
puts 'This project has no named connection selected (config.json has no "connection" key).'
|
|
173
|
+
puts "The connector will use the single connection set in #{store.filename}."
|
|
174
|
+
return
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
puts "This project uses connection set: #{active}"
|
|
178
|
+
if data.nil?
|
|
179
|
+
Wisco::TerminalOutput.emit_warning(" Could not read #{store.filename} to verify (no master key).")
|
|
180
|
+
elsif names.include?(active)
|
|
181
|
+
puts " Defined in: #{store.filename} ✓"
|
|
182
|
+
else
|
|
183
|
+
puts " Not found in #{store.filename}. Run 'wisco settings add #{active}' to create it."
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
desc 'fields', 'List the connector connection fields'
|
|
188
|
+
option :format, type: :string, enum: %w[json], desc: 'Machine-readable output (json)'
|
|
189
|
+
def fields
|
|
190
|
+
raw = connection_fields!
|
|
191
|
+
|
|
192
|
+
if options[:format] == 'json'
|
|
193
|
+
puts JSON.pretty_generate(raw)
|
|
194
|
+
return
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if raw.empty?
|
|
198
|
+
puts 'Connector defines no connection fields.'
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
rows = raw.map do |f|
|
|
203
|
+
[
|
|
204
|
+
field_attr(f, :name).to_s,
|
|
205
|
+
field_attr(f, :label).to_s,
|
|
206
|
+
(field_attr(f, :control_type) || '(default)').to_s,
|
|
207
|
+
field_attr(f, :optional) ? 'no' : 'yes'
|
|
208
|
+
]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
headers = %w[Name Label Type Required]
|
|
212
|
+
widths = headers.each_index.map do |i|
|
|
213
|
+
([headers[i]] + rows.map { |r| r[i] }).map(&:length).max
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
puts "Connection fields (from #{connector_file_basename}):\n\n"
|
|
217
|
+
puts " #{headers.each_with_index.map { |h, i| h.ljust(widths[i]) }.join(' ')}"
|
|
218
|
+
rows.each do |r|
|
|
219
|
+
puts " #{r.each_with_index.map { |c, i| c.ljust(widths[i]) }.join(' ')}"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
no_commands do
|
|
224
|
+
def target_dir
|
|
225
|
+
Dir.pwd
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def config_path
|
|
229
|
+
Wisco.config_path(target_dir)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def config
|
|
233
|
+
@config ||= begin
|
|
234
|
+
unless File.exist?(config_path)
|
|
235
|
+
Wisco::TerminalOutput.emit_error("Error: No #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} found in #{target_dir}.")
|
|
236
|
+
Wisco::TerminalOutput.emit_error(" Run 'wisco init' first.")
|
|
237
|
+
exit 1
|
|
238
|
+
end
|
|
239
|
+
Wisco::Config.load_config(config_path)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def connector_path
|
|
244
|
+
path = config.dig('connector', 'path')
|
|
245
|
+
if path.nil?
|
|
246
|
+
Wisco::TerminalOutput.emit_error("Error: #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} is missing connector path. Run 'wisco init' again.")
|
|
247
|
+
exit 1
|
|
248
|
+
end
|
|
249
|
+
path
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def connector_file_basename
|
|
253
|
+
config.dig('connector', 'file') || 'connector.rb'
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def active_connection
|
|
257
|
+
config['connection']
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def settings_store
|
|
261
|
+
Wisco::SettingsStore.new(connector_path)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Reads the settings file, converting a missing master key into a clean
|
|
265
|
+
# error+exit (used by commands that cannot proceed without the contents).
|
|
266
|
+
def read_settings!(store)
|
|
267
|
+
store.read_all
|
|
268
|
+
rescue Wisco::SettingsStore::MissingKeyError => e
|
|
269
|
+
Wisco::TerminalOutput.emit_error("Error: #{e.message}")
|
|
270
|
+
exit 1
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# connection.fields array from the connector (symbol-keyed hashes).
|
|
274
|
+
# Exits with the connector's load error if it cannot be loaded.
|
|
275
|
+
def connection_fields!
|
|
276
|
+
connector = Wisco::Connector.load_connector_from_config(target_dir)
|
|
277
|
+
Array(connector.is_a?(Hash) ? connector.dig(:connection, :fields) : nil)
|
|
278
|
+
rescue StandardError => e
|
|
279
|
+
Wisco::TerminalOutput.emit_error("Error: #{e.message.strip}")
|
|
280
|
+
exit 1
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def connection_field_names!
|
|
284
|
+
connection_fields!.map { |f| field_attr(f, :name).to_s }.reject(&:empty?)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Password field names for masking. Degrades gracefully (returns []) if
|
|
288
|
+
# the connector cannot be loaded, so `show` still works.
|
|
289
|
+
def password_field_names
|
|
290
|
+
connector = Wisco::Connector.load_connector_from_config(target_dir)
|
|
291
|
+
fields = Array(connector.is_a?(Hash) ? connector.dig(:connection, :fields) : nil)
|
|
292
|
+
fields.select { |f| field_attr(f, :control_type).to_s == 'password' }
|
|
293
|
+
.map { |f| field_attr(f, :name).to_s }
|
|
294
|
+
rescue SystemExit, StandardError
|
|
295
|
+
Wisco::TerminalOutput.emit_warning('Warning: could not load connector to identify password fields; showing all values unmasked.')
|
|
296
|
+
[]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def field_attr(field, key)
|
|
300
|
+
return nil unless field.is_a?(Hash)
|
|
301
|
+
|
|
302
|
+
field[key] || field[key.to_s]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def render_set(name, set, store)
|
|
306
|
+
passwords = password_field_names
|
|
307
|
+
header = name || '(single/unnamed)'
|
|
308
|
+
puts "Connection set: #{header} (#{store.filename})\n\n"
|
|
309
|
+
|
|
310
|
+
if set.nil? || set.empty?
|
|
311
|
+
puts ' (no fields)'
|
|
312
|
+
return
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
width = set.keys.map { |k| k.to_s.length }.max
|
|
316
|
+
set.each do |k, v|
|
|
317
|
+
display =
|
|
318
|
+
if v.to_s.empty?
|
|
319
|
+
'(blank)'
|
|
320
|
+
elsif passwords.include?(k.to_s)
|
|
321
|
+
mask(v.to_s)
|
|
322
|
+
else
|
|
323
|
+
v.to_s
|
|
324
|
+
end
|
|
325
|
+
puts " #{k.to_s.ljust(width)} #{display}"
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def mask(value)
|
|
330
|
+
return '****' if value.length <= 4
|
|
331
|
+
|
|
332
|
+
"****#{value[-4..]}"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Machine-readable form of a connection set: the connector's
|
|
336
|
+
# connection.fields array, each field augmented with a `value` key holding
|
|
337
|
+
# the current stored value (null when the field is unset or blank). Values
|
|
338
|
+
# are unmasked — this output is for tooling, not display.
|
|
339
|
+
def render_set_json(set)
|
|
340
|
+
set ||= {}
|
|
341
|
+
out = connection_fields!.map do |f|
|
|
342
|
+
raw = set[field_attr(f, :name).to_s]
|
|
343
|
+
value = raw.nil? || raw.to_s.empty? ? nil : raw
|
|
344
|
+
f.merge(value: value)
|
|
345
|
+
end
|
|
346
|
+
puts JSON.pretty_generate(out)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def migrate_flat(flat_data, store)
|
|
350
|
+
puts "#{store.filename} currently holds a single unnamed connection set."
|
|
351
|
+
puts 'Adding a named set requires converting the file to the named (nested) format.'
|
|
352
|
+
print 'Enter a name for the existing connection set (or press Enter to skip): '
|
|
353
|
+
name = $stdin.gets.to_s.strip
|
|
354
|
+
if name.empty?
|
|
355
|
+
Wisco::TerminalOutput.emit_error('Aborted: mixing flat and named connection sets is not supported.')
|
|
356
|
+
exit 1
|
|
357
|
+
end
|
|
358
|
+
{ name => flat_data }
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'active_support/encrypted_configuration'
|
|
3
|
+
require 'workato/connector/sdk'
|
|
4
|
+
require_relative 'terminal_output'
|
|
5
|
+
|
|
6
|
+
module Wisco
|
|
7
|
+
# Read/write wrapper over the Workato SDK settings file (settings.yaml /
|
|
8
|
+
# settings.yaml.enc) living in a connector directory. Handles encrypted vs
|
|
9
|
+
# plaintext auto-detection, master-key resolution, and flat-vs-nested
|
|
10
|
+
# connection-set detection. See doc-specs/wisco-10-settings.md.
|
|
11
|
+
class SettingsStore
|
|
12
|
+
PLAINTEXT_FILENAME = 'settings.yaml'.freeze
|
|
13
|
+
ENCRYPTED_FILENAME = 'settings.yaml.enc'.freeze
|
|
14
|
+
MASTER_KEY_FILENAME = 'master.key'.freeze
|
|
15
|
+
MASTER_KEY_ENV = 'WORKATO_CONNECTOR_MASTER_KEY'.freeze
|
|
16
|
+
|
|
17
|
+
class MissingKeyError < StandardError; end
|
|
18
|
+
|
|
19
|
+
attr_reader :connector_path
|
|
20
|
+
|
|
21
|
+
def initialize(connector_path)
|
|
22
|
+
@connector_path = File.expand_path(connector_path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def plaintext_path
|
|
26
|
+
File.join(@connector_path, PLAINTEXT_FILENAME)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def encrypted_path
|
|
30
|
+
File.join(@connector_path, ENCRYPTED_FILENAME)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def master_key_path
|
|
34
|
+
File.join(@connector_path, MASTER_KEY_FILENAME)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# :encrypted (settings.yaml.enc present), :plaintext (settings.yaml present),
|
|
38
|
+
# or :none. Encrypted takes precedence when both exist.
|
|
39
|
+
def form
|
|
40
|
+
if File.exist?(encrypted_path)
|
|
41
|
+
:encrypted
|
|
42
|
+
elsif File.exist?(plaintext_path)
|
|
43
|
+
:plaintext
|
|
44
|
+
else
|
|
45
|
+
:none
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def encrypted?
|
|
50
|
+
form == :encrypted
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def exist?
|
|
54
|
+
form != :none
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Basename for user-facing messages; falls back to the plaintext name when
|
|
58
|
+
# no file exists yet (that is the form `add` would create).
|
|
59
|
+
def filename
|
|
60
|
+
encrypted? ? ENCRYPTED_FILENAME : PLAINTEXT_FILENAME
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def master_key_available?
|
|
64
|
+
!ENV[MASTER_KEY_ENV].to_s.strip.empty? || File.exist?(master_key_path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Reads the entire settings file (all sets) as a plain string-keyed Hash.
|
|
68
|
+
# Returns {} when no file exists. Raises MissingKeyError if the file is
|
|
69
|
+
# encrypted and no master key is available.
|
|
70
|
+
def read_all
|
|
71
|
+
case form
|
|
72
|
+
when :none
|
|
73
|
+
{}
|
|
74
|
+
when :plaintext
|
|
75
|
+
parsed = YAML.safe_load(File.read(plaintext_path), permitted_classes: [::Symbol])
|
|
76
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
77
|
+
when :encrypted
|
|
78
|
+
require_master_key!
|
|
79
|
+
raw = Workato::Connector::Sdk::Settings.from_encrypted_file(encrypted_path, master_key_path)
|
|
80
|
+
raw.respond_to?(:to_hash) ? raw.to_hash : {}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Replaces the whole settings file with `data`. When `encrypted:` is true the
|
|
85
|
+
# content is encrypted with the resolved master key; otherwise a plaintext
|
|
86
|
+
# settings.yaml is written. Rewriting wholesale (rather than the SDK's
|
|
87
|
+
# merge-only #update) is what lets `add` migrate a flat file to nested form.
|
|
88
|
+
def write_all(data, encrypted:)
|
|
89
|
+
if encrypted
|
|
90
|
+
require_master_key!
|
|
91
|
+
config = ActiveSupport::EncryptedConfiguration.new(
|
|
92
|
+
config_path: encrypted_path,
|
|
93
|
+
key_path: master_key_path,
|
|
94
|
+
env_key: MASTER_KEY_ENV,
|
|
95
|
+
raise_if_missing_key: true
|
|
96
|
+
)
|
|
97
|
+
config.write(YAML.dump(data))
|
|
98
|
+
else
|
|
99
|
+
File.write(plaintext_path, YAML.dump(data))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def require_master_key!
|
|
104
|
+
return if master_key_available?
|
|
105
|
+
|
|
106
|
+
raise MissingKeyError,
|
|
107
|
+
"#{ENCRYPTED_FILENAME} is encrypted but no master key was found.\n" \
|
|
108
|
+
" Set #{MASTER_KEY_ENV} or provide a #{MASTER_KEY_FILENAME} file in #{@connector_path}."
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# :none, :flat (all top-level values scalar), :nested (all Hash), or :mixed.
|
|
112
|
+
def self.detect_structure(data)
|
|
113
|
+
return :none if data.nil? || data.empty?
|
|
114
|
+
|
|
115
|
+
values = data.values
|
|
116
|
+
if values.all? { |v| v.is_a?(Hash) }
|
|
117
|
+
:nested
|
|
118
|
+
elsif values.none? { |v| v.is_a?(Hash) }
|
|
119
|
+
:flat
|
|
120
|
+
else
|
|
121
|
+
:mixed
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Names of the named (nested) connection sets — the top-level keys whose
|
|
126
|
+
# value is a Hash. Empty for flat/none files.
|
|
127
|
+
def self.set_names(data)
|
|
128
|
+
return [] if data.nil?
|
|
129
|
+
|
|
130
|
+
data.select { |_k, v| v.is_a?(Hash) }.keys
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
data/lib/wisco/version.rb
CHANGED
data/lib/wisco.rb
CHANGED
|
@@ -25,6 +25,7 @@ require_relative 'wisco/commands/pull'
|
|
|
25
25
|
require_relative 'wisco/commands/push'
|
|
26
26
|
require_relative 'wisco/commands/schema'
|
|
27
27
|
require_relative 'wisco/commands/profile'
|
|
28
|
+
require_relative 'wisco/commands/settings'
|
|
28
29
|
require_relative 'wisco/commands/status'
|
|
29
30
|
|
|
30
31
|
module Wisco
|
|
@@ -103,6 +104,7 @@ module Wisco
|
|
|
103
104
|
option :debug, type: :boolean, default: false, desc: 'Print ExecCommand call details'
|
|
104
105
|
option :summary, type: :boolean, default: true, desc: 'Show output summary after execution (use --no-summary to disable)'
|
|
105
106
|
option :'summary-lines', type: :numeric, default: 20, desc: 'Max output lines to display in full before switching to summary-only'
|
|
107
|
+
option :'auto-refresh', type: :boolean, default: true, desc: 'Automatically confirm token refresh prompts (use --no-auto-refresh to disable)'
|
|
106
108
|
def exec(path_arg, target_dir = nil)
|
|
107
109
|
Wisco::Commands::Exec.run(
|
|
108
110
|
path_arg,
|
|
@@ -118,7 +120,8 @@ module Wisco
|
|
|
118
120
|
extended_output_schema: options[:extended_output_schema],
|
|
119
121
|
debug: options[:debug],
|
|
120
122
|
summary: options[:summary],
|
|
121
|
-
summary_lines: options[:'summary-lines']
|
|
123
|
+
summary_lines: options[:'summary-lines'],
|
|
124
|
+
auto_refresh: options[:'auto-refresh']
|
|
122
125
|
)
|
|
123
126
|
end
|
|
124
127
|
|
|
@@ -215,6 +218,18 @@ module Wisco
|
|
|
215
218
|
DESC
|
|
216
219
|
subcommand 'profile', Wisco::Commands::Profile
|
|
217
220
|
|
|
221
|
+
desc 'settings SUBCOMMAND ...ARGS', 'Manage the connector settings file (credential sets)'
|
|
222
|
+
long_desc <<~DESC
|
|
223
|
+
Subcommands:
|
|
224
|
+
list List connection sets (--format=json for a JSON array of names)
|
|
225
|
+
set <connection> Set the project's active connection set
|
|
226
|
+
add <connection> Scaffold a new connection set from connector.connection.fields
|
|
227
|
+
show [<connection>] Show a connection set's field values (passwords masked)
|
|
228
|
+
current Show which connection set the project points at
|
|
229
|
+
fields List the connector's connection fields (--format=json for JSON)
|
|
230
|
+
DESC
|
|
231
|
+
subcommand 'settings', Wisco::Commands::Settings
|
|
232
|
+
|
|
218
233
|
desc 'schema INPUT_FILE [TARGET_DIR]', 'Generate a schema from a JSON or CSV sample file'
|
|
219
234
|
long_desc <<~DESC
|
|
220
235
|
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.4.
|
|
4
|
+
version: 0.4.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- mbillington
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -75,12 +75,14 @@ files:
|
|
|
75
75
|
- lib/wisco/commands/pull.rb
|
|
76
76
|
- lib/wisco/commands/push.rb
|
|
77
77
|
- lib/wisco/commands/schema.rb
|
|
78
|
+
- lib/wisco/commands/settings.rb
|
|
78
79
|
- lib/wisco/commands/status.rb
|
|
79
80
|
- lib/wisco/config.rb
|
|
80
81
|
- lib/wisco/connector.rb
|
|
81
82
|
- lib/wisco/exec_script.rb
|
|
82
83
|
- lib/wisco/path_utils.rb
|
|
83
84
|
- lib/wisco/profile.rb
|
|
85
|
+
- lib/wisco/settings_store.rb
|
|
84
86
|
- lib/wisco/terminal_output.rb
|
|
85
87
|
- lib/wisco/version.rb
|
|
86
88
|
- lib/wisco/workato_api.rb
|