flowable 1.0.0

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.
data/bin/flowable ADDED
@@ -0,0 +1,510 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require 'json'
6
+ require 'yaml'
7
+ require_relative '../lib/flowable'
8
+
9
+ module FlowableCLI
10
+ class CLI
11
+ CONFIG_FILE = File.expand_path('~/.flowable.yml')
12
+
13
+ def initialize
14
+ @options = load_config
15
+ end
16
+
17
+ def run(args)
18
+ return show_help if args.empty?
19
+
20
+ command = args.shift
21
+ case command
22
+ when 'config'
23
+ configure(args)
24
+ when 'deploy'
25
+ deploy(args)
26
+ when 'definitions', 'defs'
27
+ definitions(args)
28
+ when 'start'
29
+ start_case(args)
30
+ when 'cases'
31
+ list_cases(args)
32
+ when 'case'
33
+ show_case(args)
34
+ when 'tasks'
35
+ list_tasks(args)
36
+ when 'task'
37
+ show_task(args)
38
+ when 'claim'
39
+ claim_task(args)
40
+ when 'complete'
41
+ complete_task(args)
42
+ when 'vars'
43
+ show_variables(args)
44
+ when 'set'
45
+ set_variable(args)
46
+ when 'history'
47
+ show_history(args)
48
+ when 'process-start'
49
+ start_process(args)
50
+ when 'processes'
51
+ list_processes(args)
52
+ when 'help', '-h', '--help'
53
+ show_help
54
+ else
55
+ puts "Unknown command: #{command}"
56
+ show_help
57
+ exit 1
58
+ end
59
+ rescue Flowable::Error => e
60
+ puts "Error: #{e.message}"
61
+ exit 1
62
+ end
63
+
64
+ private
65
+
66
+ def client
67
+ @client ||= Flowable::Client.new(
68
+ host: @options[:host] || 'localhost',
69
+ port: @options[:port] || 8080,
70
+ username: @options[:username] || 'rest-admin',
71
+ password: @options[:password] || 'test',
72
+ use_ssl: @options[:use_ssl] || false
73
+ )
74
+ end
75
+
76
+ def load_config
77
+ return {} unless File.exist?(CONFIG_FILE)
78
+
79
+ YAML.load_file(CONFIG_FILE, symbolize_names: true) || {}
80
+ rescue StandardError
81
+ {}
82
+ end
83
+
84
+ def save_config
85
+ File.write(CONFIG_FILE, @options.to_yaml)
86
+ puts "Config saved to #{CONFIG_FILE}"
87
+ end
88
+
89
+ # === Commands ===
90
+
91
+ def configure(args)
92
+ opts = OptionParser.new do |o|
93
+ o.banner = 'Usage: flowable config [options]'
94
+ o.on('--host HOST', 'Flowable host') { |v| @options[:host] = v }
95
+ o.on('--port PORT', Integer, 'Flowable port') { |v| @options[:port] = v }
96
+ o.on('--username USER', 'Username') { |v| @options[:username] = v }
97
+ o.on('--password PASS', 'Password') { |v| @options[:password] = v }
98
+ o.on('--ssl', 'Use HTTPS') { @options[:use_ssl] = true }
99
+ o.on('--show', 'Show current config') do
100
+ puts 'Current configuration:'
101
+ puts " Host: #{@options[:host] || 'localhost'}"
102
+ puts " Port: #{@options[:port] || 8080}"
103
+ puts " Username: #{@options[:username] || 'rest-admin'}"
104
+ puts " SSL: #{@options[:use_ssl] || false}"
105
+ exit 0
106
+ end
107
+ end
108
+ opts.parse!(args)
109
+ save_config
110
+ end
111
+
112
+ def deploy(args)
113
+ if args.empty?
114
+ puts 'Usage: flowable deploy <file.cmmn.xml|file.bpmn.xml> [--tenant TENANT]'
115
+ exit 1
116
+ end
117
+
118
+ file_path = args.shift
119
+ tenant_id = nil
120
+
121
+ opts = OptionParser.new do |o|
122
+ o.on('--tenant TENANT', 'Tenant ID') { |v| tenant_id = v }
123
+ end
124
+ opts.parse!(args)
125
+
126
+ unless File.exist?(file_path)
127
+ puts "File not found: #{file_path}"
128
+ exit 1
129
+ end
130
+
131
+ # Determine if CMMN or BPMN based on extension
132
+ if file_path.include?('.cmmn')
133
+ result = client.deployments.create(file_path, tenant_id: tenant_id)
134
+ puts 'CMMN Deployment created:'
135
+ else
136
+ result = client.bpmn_deployments.create(file_path, tenant_id: tenant_id)
137
+ puts 'BPMN Deployment created:'
138
+ end
139
+
140
+ puts " ID: #{result['id']}"
141
+ puts " Name: #{result['name']}"
142
+ puts " Time: #{result['deploymentTime']}"
143
+ end
144
+
145
+ def definitions(args)
146
+ type = 'cmmn'
147
+ opts = OptionParser.new do |o|
148
+ o.banner = 'Usage: flowable definitions [options]'
149
+ o.on('--bpmn', 'Show BPMN process definitions') { type = 'bpmn' }
150
+ o.on('--cmmn', 'Show CMMN case definitions (default)') { type = 'cmmn' }
151
+ end
152
+ opts.parse!(args)
153
+
154
+ if type == 'bpmn'
155
+ result = client.process_definitions.list
156
+ puts "Process Definitions (#{result['total']}):"
157
+ else
158
+ result = client.case_definitions.list
159
+ puts "Case Definitions (#{result['total']}):"
160
+ end
161
+ result['data'].each do |d|
162
+ puts " #{d['key']} v#{d['version']} - #{d['name']} [#{d['id']}]"
163
+ end
164
+ end
165
+
166
+ def start_case(args)
167
+ if args.empty?
168
+ puts 'Usage: flowable start <caseKey> [--var key=value ...] [--business-key KEY]'
169
+ exit 1
170
+ end
171
+
172
+ case_key = args.shift
173
+ variables = {}
174
+ business_key = nil
175
+
176
+ opts = OptionParser.new do |o|
177
+ o.on('--var VAR', 'Variable (key=value)') do |v|
178
+ key, value = v.split('=', 2)
179
+ variables[key.to_sym] = parse_value(value)
180
+ end
181
+ o.on('--business-key KEY', 'Business key') { |v| business_key = v }
182
+ end
183
+ opts.parse!(args)
184
+
185
+ result = client.case_instances.start_by_key(
186
+ case_key,
187
+ variables: variables,
188
+ business_key: business_key
189
+ )
190
+
191
+ puts 'Case started:'
192
+ puts " ID: #{result['id']}"
193
+ puts " State: #{result['state']}"
194
+ puts " Definition: #{result['caseDefinitionName']}"
195
+ end
196
+
197
+ def start_process(args)
198
+ if args.empty?
199
+ puts 'Usage: flowable process-start <processKey> [--var key=value ...] [--business-key KEY]'
200
+ exit 1
201
+ end
202
+
203
+ process_key = args.shift
204
+ variables = {}
205
+ business_key = nil
206
+
207
+ opts = OptionParser.new do |o|
208
+ o.on('--var VAR', 'Variable (key=value)') do |v|
209
+ key, value = v.split('=', 2)
210
+ variables[key.to_sym] = parse_value(value)
211
+ end
212
+ o.on('--business-key KEY', 'Business key') { |v| business_key = v }
213
+ end
214
+ opts.parse!(args)
215
+
216
+ result = client.process_instances.start_by_key(
217
+ process_key,
218
+ variables: variables,
219
+ business_key: business_key
220
+ )
221
+
222
+ puts 'Process started:'
223
+ puts " ID: #{result['id']}"
224
+ puts " Ended: #{result['ended']}"
225
+ puts " Definition: #{result['processDefinitionId']}"
226
+ end
227
+
228
+ def list_cases(args)
229
+ filters = {}
230
+ opts = OptionParser.new do |o|
231
+ o.banner = 'Usage: flowable cases [options]'
232
+ o.on('--key KEY', 'Filter by case definition key') { |v| filters[:caseDefinitionKey] = v }
233
+ o.on('--business-key KEY', 'Filter by business key') { |v| filters[:businessKey] = v }
234
+ end
235
+ opts.parse!(args)
236
+
237
+ result = client.case_instances.list(**filters)
238
+ puts "Case Instances (#{result['total']}):"
239
+ result['data'].each do |c|
240
+ puts " [#{c['state']}] #{c['id']} - #{c['caseDefinitionName']} (#{c['businessKey'] || 'no business key'})"
241
+ end
242
+ end
243
+
244
+ def list_processes(args)
245
+ filters = {}
246
+ opts = OptionParser.new do |o|
247
+ o.banner = 'Usage: flowable processes [options]'
248
+ o.on('--key KEY', 'Filter by process definition key') { |v| filters[:processDefinitionKey] = v }
249
+ o.on('--business-key KEY', 'Filter by business key') { |v| filters[:businessKey] = v }
250
+ end
251
+ opts.parse!(args)
252
+
253
+ result = client.process_instances.list(**filters)
254
+ puts "Process Instances (#{result['total']}):"
255
+ result['data'].each do |p|
256
+ status = if p['suspended']
257
+ 'SUSPENDED'
258
+ else
259
+ (p['ended'] ? 'ENDED' : 'ACTIVE')
260
+ end
261
+ puts " [#{status}] #{p['id']} - #{p['processDefinitionId']} (#{p['businessKey'] || 'no business key'})"
262
+ end
263
+ end
264
+
265
+ def show_case(args)
266
+ if args.empty?
267
+ puts 'Usage: flowable case <caseInstanceId>'
268
+ exit 1
269
+ end
270
+
271
+ case_id = args.shift
272
+ result = client.case_instances.get(case_id)
273
+
274
+ puts "Case Instance: #{result['id']}"
275
+ puts " Name: #{result['name']}"
276
+ puts " State: #{result['state']}"
277
+ puts " Definition: #{result['caseDefinitionName']}"
278
+ puts " Business Key: #{result['businessKey']}"
279
+ puts " Start Time: #{result['startTime']}"
280
+ puts " Start User: #{result['startUserId']}"
281
+
282
+ # Show stages
283
+ stages = client.case_instances.stage_overview(case_id) rescue []
284
+ unless stages.empty?
285
+ puts "\n Stages:"
286
+ stages.each do |stage|
287
+ status = if stage['current']
288
+ '▶'
289
+ else
290
+ (stage['ended'] ? '✓' : '○')
291
+ end
292
+ puts " #{status} #{stage['name']}"
293
+ end
294
+ end
295
+ end
296
+
297
+ def list_tasks(args)
298
+ filters = {}
299
+ opts = OptionParser.new do |o|
300
+ o.banner = 'Usage: flowable tasks [options]'
301
+ o.on('--assignee USER', 'Filter by assignee') { |v| filters[:assignee] = v }
302
+ o.on('--candidate USER', 'Filter by candidate user') { |v| filters[:candidateUser] = v }
303
+ o.on('--case ID', 'Filter by case instance ID') { |v| filters[:caseInstanceId] = v }
304
+ o.on('--unassigned', 'Only unassigned tasks') { filters[:unassigned] = true }
305
+ end
306
+ opts.parse!(args)
307
+
308
+ result = client.tasks.list(**filters)
309
+ puts "Tasks (#{result['total']}):"
310
+ result['data'].each do |t|
311
+ assignee = t['assignee'] || 'unassigned'
312
+ puts " #{t['id']} - #{t['name']} [#{assignee}]"
313
+ puts " Case: #{t['caseInstanceId']}" if t['caseInstanceId']
314
+ puts " Due: #{t['dueDate']}" if t['dueDate']
315
+ end
316
+ end
317
+
318
+ def show_task(args)
319
+ if args.empty?
320
+ puts 'Usage: flowable task <taskId>'
321
+ exit 1
322
+ end
323
+
324
+ task_id = args.shift
325
+ result = client.tasks.get(task_id)
326
+
327
+ puts "Task: #{result['id']}"
328
+ puts " Name: #{result['name']}"
329
+ puts " Description: #{result['description']}"
330
+ puts " Assignee: #{result['assignee'] || 'unassigned'}"
331
+ puts " Owner: #{result['owner']}"
332
+ puts " Priority: #{result['priority']}"
333
+ puts " Due Date: #{result['dueDate']}"
334
+ puts " Created: #{result['createTime']}"
335
+ puts " Case Instance: #{result['caseInstanceId']}"
336
+ end
337
+
338
+ def claim_task(args)
339
+ if args.length < 2
340
+ puts 'Usage: flowable claim <taskId> <assignee>'
341
+ exit 1
342
+ end
343
+
344
+ task_id = args.shift
345
+ assignee = args.shift
346
+
347
+ client.tasks.claim(task_id, assignee)
348
+ puts "Task #{task_id} claimed by #{assignee}"
349
+ end
350
+
351
+ def complete_task(args)
352
+ if args.empty?
353
+ puts 'Usage: flowable complete <taskId> [--var key=value ...] [--outcome OUTCOME]'
354
+ exit 1
355
+ end
356
+
357
+ task_id = args.shift
358
+ variables = {}
359
+ outcome = nil
360
+
361
+ opts = OptionParser.new do |o|
362
+ o.on('--var VAR', 'Variable (key=value)') do |v|
363
+ key, value = v.split('=', 2)
364
+ variables[key.to_sym] = parse_value(value)
365
+ end
366
+ o.on('--outcome OUTCOME', 'Task outcome') { |v| outcome = v }
367
+ end
368
+ opts.parse!(args)
369
+
370
+ client.tasks.complete(task_id, variables: variables, outcome: outcome)
371
+ puts "Task #{task_id} completed"
372
+ end
373
+
374
+ def show_variables(args)
375
+ if args.empty?
376
+ puts 'Usage: flowable vars <caseInstanceId|taskId> [--task]'
377
+ exit 1
378
+ end
379
+
380
+ id = args.shift
381
+ is_task = args.include?('--task')
382
+
383
+ result = if is_task
384
+ client.tasks.variables(id)
385
+ else
386
+ client.case_instances.variables(id)
387
+ end
388
+
389
+ puts 'Variables:'
390
+ result.each do |v|
391
+ puts " #{v['name']} = #{v['value']} (#{v['type']})"
392
+ end
393
+ end
394
+
395
+ def set_variable(args)
396
+ if args.length < 2
397
+ puts 'Usage: flowable set <caseInstanceId> <key=value> [key=value ...]'
398
+ exit 1
399
+ end
400
+
401
+ case_id = args.shift
402
+ variables = {}
403
+
404
+ args.each do |arg|
405
+ key, value = arg.split('=', 2)
406
+ variables[key.to_sym] = parse_value(value)
407
+ end
408
+
409
+ client.case_instances.set_variables(case_id, variables)
410
+ puts "Variables updated on case #{case_id}"
411
+ end
412
+
413
+ def show_history(args)
414
+ type = 'cases'
415
+ limit = 10
416
+
417
+ opts = OptionParser.new do |o|
418
+ o.banner = 'Usage: flowable history [options]'
419
+ o.on('--cases', 'Show case history (default)') { type = 'cases' }
420
+ o.on('--tasks', 'Show task history') { type = 'tasks' }
421
+ o.on('--processes', 'Show process history') { type = 'processes' }
422
+ o.on('--limit N', Integer, 'Limit results') { |v| limit = v }
423
+ end
424
+ opts.parse!(args)
425
+
426
+ case type
427
+ when 'cases'
428
+ result = client.history.case_instances(size: limit)
429
+ puts "Historic Case Instances (showing #{[limit, result['total']].min} of #{result['total']}):"
430
+ result['data'].each do |c|
431
+ status = c['endTime'] ? 'ENDED' : 'ACTIVE'
432
+ puts " [#{status}] #{c['id']} - #{c['caseDefinitionName']}"
433
+ puts " Started: #{c['startTime']} by #{c['startUserId']}"
434
+ puts " Ended: #{c['endTime']}" if c['endTime']
435
+ end
436
+ when 'tasks'
437
+ result = client.history.task_instances(size: limit)
438
+ puts "Historic Task Instances (showing #{[limit, result['total']].min} of #{result['total']}):"
439
+ result['data'].each do |t|
440
+ status = t['endTime'] ? 'COMPLETED' : 'ACTIVE'
441
+ puts " [#{status}] #{t['id']} - #{t['name']}"
442
+ puts " Assignee: #{t['assignee']}, Duration: #{t['durationInMillis']}ms"
443
+ end
444
+ when 'processes'
445
+ result = client.bpmn_history.process_instances(size: limit)
446
+ puts "Historic Process Instances (showing #{[limit, result['total']].min} of #{result['total']}):"
447
+ result['data'].each do |p|
448
+ status = p['endTime'] ? 'ENDED' : 'ACTIVE'
449
+ puts " [#{status}] #{p['id']} - #{p['processDefinitionId']}"
450
+ puts " Started: #{p['startTime']} by #{p['startUserId']}"
451
+ end
452
+ end
453
+ end
454
+
455
+ def show_help
456
+ puts <<~HELP
457
+ Flowable CLI - Command line interface for Flowable REST API
458
+
459
+ Configuration:
460
+ flowable config --host HOST --port PORT --username USER --password PASS
461
+ flowable config --show
462
+
463
+ Deployments:
464
+ flowable deploy <file.cmmn.xml|file.bpmn.xml> [--tenant TENANT]
465
+
466
+ Definitions:
467
+ flowable definitions [--cmmn|--bpmn]
468
+ flowable defs
469
+
470
+ CMMN Cases:
471
+ flowable start <caseKey> [--var key=value] [--business-key KEY]
472
+ flowable cases [--key KEY] [--business-key KEY]
473
+ flowable case <caseInstanceId>
474
+
475
+ BPMN Processes:
476
+ flowable process-start <processKey> [--var key=value] [--business-key KEY]
477
+ flowable processes [--key KEY] [--business-key KEY]
478
+
479
+ Tasks:
480
+ flowable tasks [--assignee USER] [--candidate USER] [--case ID] [--unassigned]
481
+ flowable task <taskId>
482
+ flowable claim <taskId> <assignee>
483
+ flowable complete <taskId> [--var key=value] [--outcome OUTCOME]
484
+
485
+ Variables:
486
+ flowable vars <caseInstanceId>
487
+ flowable vars <taskId> --task
488
+ flowable set <caseInstanceId> key=value [key=value ...]
489
+
490
+ History:
491
+ flowable history [--cases|--tasks|--processes] [--limit N]
492
+
493
+ Help:
494
+ flowable help
495
+ HELP
496
+ end
497
+
498
+ def parse_value(value)
499
+ return true if value == 'true'
500
+ return false if value == 'false'
501
+ return value.to_i if value =~ /^\d+$/
502
+ return value.to_f if value =~ /^\d+\.\d+$/
503
+
504
+ value
505
+ end
506
+ end
507
+ end
508
+
509
+ # Run CLI
510
+ FlowableCLI::CLI.new.run(ARGV)