flowengine-cli 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ae12fe144b1694228a1fcdaeccab821dbbb4667be164bc01cb2ffd7111a7cba1
4
+ data.tar.gz: 45e96c201167069000eea870f8801777c01c561570cb029fef3c428fc9919609
5
+ SHA512:
6
+ metadata.gz: 3d8f0f0c29129144bdd700e621307b17df356a460c85468806efab0d683409b2fbcdc7da34bec9868b22d0394ab3d503ead856fd78e3ae837292a8a760b08c32
7
+ data.tar.gz: dcb8897fd96a7f5841006937e3448bc913e126829ab772c57967c2b2741440b5e5e708314281eb43a3d4b8b742489c1315f1426dcdb197b65b0ec3087ae56a73
data/.envrc ADDED
@@ -0,0 +1 @@
1
+ PATH_add exe
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,7 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2026-02-27 03:54:38 UTC using RuboCop version 1.85.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-02-26
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Konstantin Gredeskoul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,554 @@
1
+ # FlowEngine CLI
2
+
3
+ [![RSpec](https://github.com/kigster/flowengine-cli/actions/workflows/rspec.yml/badge.svg)](https://github.com/kigster/flowengine-cli/actions/workflows/rspec.yml) [![RuboCop](https://github.com/kigster/flowengine-cli/actions/workflows/rubocop.yml/badge.svg)](https://github.com/kigster/flowengine-cli/actions/workflows/rubocop.yml)
4
+
5
+ Terminal-based interactive wizard runner for [FlowEngine](https://github.com/kigster/flowengine) flows. Define your flow once, run it in the terminal with rich TTY prompts, export Mermaid diagrams, and validate flow definitions -- all from the command line.
6
+
7
+ FlowEngine CLI is a UI adapter that sits on top of the pure-Ruby `flowengine` core gem. The core gem knows nothing about terminals, databases, or web frameworks. This gem provides the terminal interface.
8
+
9
+ ## Installation
10
+
11
+ Add to your Gemfile:
12
+
13
+ ```ruby
14
+ gem "flowengine-cli"
15
+ ```
16
+
17
+ Or install directly:
18
+
19
+ ```bash
20
+ gem install flowengine-cli
21
+ ```
22
+
23
+ ### Requirements
24
+
25
+ - Ruby >= 4.0.1
26
+ - [flowengine](https://github.com/kigster/flowengine) ~> 0.1
27
+
28
+ ## Quick Start
29
+
30
+ ### 1. Define a flow
31
+
32
+ Create a file called `intake.rb`:
33
+
34
+ ```ruby
35
+ FlowEngine.define do
36
+ start :filing_status
37
+
38
+ step :filing_status do
39
+ type :single_select
40
+ question "What is your filing status?"
41
+ options %w[Single Married HeadOfHousehold]
42
+ transition to: :income_types
43
+ end
44
+
45
+ step :income_types do
46
+ type :multi_select
47
+ question "Select all income types that apply:"
48
+ options %w[W2 1099 Business Investment Rental]
49
+ transition to: :business_details, if_rule: contains(:income_types, "Business")
50
+ transition to: :summary
51
+ end
52
+
53
+ step :business_details do
54
+ type :number_matrix
55
+ question "How many of each business type?"
56
+ fields %w[LLC SCorp CCorp]
57
+ transition to: :summary
58
+ end
59
+
60
+ step :summary do
61
+ type :display
62
+ question "Thank you for completing the intake!"
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### 2. Run it
68
+
69
+ ```bash
70
+ flowengine-cli run intake.rb
71
+ ```
72
+
73
+ The CLI walks you through each step interactively, rendering the appropriate TTY prompt for each step type. When complete, it outputs the collected answers as JSON:
74
+
75
+ ```json
76
+ {
77
+ "flow_file": "intake.rb",
78
+ "path_taken": ["filing_status", "income_types", "business_details", "summary"],
79
+ "answers": {
80
+ "filing_status": "Married",
81
+ "income_types": ["W2", "Business"],
82
+ "business_details": { "LLC": 2, "SCorp": 1, "CCorp": 0 }
83
+ },
84
+ "steps_completed": 4,
85
+ "completed_at": "2026-02-26T20:15:00-08:00"
86
+ }
87
+ ```
88
+
89
+ ### 3. Save results to a file
90
+
91
+ ```bash
92
+ flowengine-cli run intake.rb -o results.json
93
+ ```
94
+
95
+ ## Commands
96
+
97
+ ### `run` -- Interactive Wizard
98
+
99
+ ```bash
100
+ flowengine-cli run <flow_file.rb> [-o output.json]
101
+ ```
102
+
103
+ Loads a flow definition, presents each step as an interactive terminal prompt, and outputs the collected answers as JSON when complete.
104
+
105
+ **Arguments:**
106
+ | Argument | Required | Description |
107
+ |----------|----------|-------------|
108
+ | `flow_file` | Yes | Path to a `.rb` file containing a `FlowEngine.define` block |
109
+
110
+ **Options:**
111
+ | Option | Alias | Description |
112
+ |--------|-------|-------------|
113
+ | `--output` | `-o` | Write JSON results to this file |
114
+
115
+ **What happens at runtime:**
116
+
117
+ ```
118
+ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
119
+ ┃ FlowEngine Interactive Wizard ┃
120
+ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
121
+
122
+ Step 1: filing_status
123
+ ────────────────────────────────────
124
+ What is your filing status? (Use ↑/↓ arrow keys, press Enter to select)
125
+ > Single
126
+ Married
127
+ HeadOfHousehold
128
+
129
+ Step 2: income_types
130
+ ────────────────────────────────────
131
+ Select all income types that apply: (Use ↑/↓ arrow keys, press Space to select)
132
+ ◯ W2
133
+ > ◉ 1099
134
+ ◉ Business
135
+ ◯ Investment
136
+ ◯ Rental
137
+
138
+ Step 3: business_details
139
+ ────────────────────────────────────
140
+ How many of each business type?
141
+
142
+ LLC: 2
143
+ SCorp: 1
144
+ CCorp: 0
145
+
146
+ ┌ SUCCESS ─────────────────────────────────────────────────────────┐
147
+ │ Flow completed! │
148
+ └──────────────────────────────────────────────────────────────────┘
149
+ { ... JSON output ... }
150
+ ```
151
+
152
+ ---
153
+
154
+ ### `graph` -- Export Mermaid Diagram
155
+
156
+ ```bash
157
+ flowengine-cli graph <flow_file.rb> [-o diagram.mmd]
158
+ ```
159
+
160
+ Exports the flow definition as a [Mermaid](https://mermaid.js.org/) flowchart diagram. Useful for documentation, visualization, and reviewing flow logic.
161
+
162
+ **Options:**
163
+ | Option | Alias | Description |
164
+ |--------|-------|-------------|
165
+ | `--output` | `-o` | Write diagram to file (default: stdout) |
166
+ | `--format` | | Output format (default: `mermaid`) |
167
+
168
+ **Example:**
169
+
170
+ ```bash
171
+ flowengine-cli graph intake.rb
172
+ ```
173
+
174
+ Outputs:
175
+
176
+ ```mermaid
177
+ flowchart TD
178
+ filing_status["What is your filing status?"]
179
+ filing_status --> income_types
180
+ income_types["Select all income types that apply:"]
181
+ income_types -->|"Business in income_types"| business_details
182
+ income_types --> summary
183
+ business_details["How many of each business type?"]
184
+ business_details --> summary
185
+ summary["Thank you for completing the intake!"]
186
+ ```
187
+
188
+ Save to a file and render with any Mermaid-compatible tool (GitHub, VS Code, mermaid.live):
189
+
190
+ ```bash
191
+ flowengine-cli graph intake.rb -o flow.mmd
192
+ ```
193
+
194
+ ---
195
+
196
+ ### `validate` -- Validate Flow Definition
197
+
198
+ ```bash
199
+ flowengine-cli validate <flow_file.rb>
200
+ ```
201
+
202
+ Validates a flow definition file for structural correctness. Checks for:
203
+
204
+ - **Start step existence** -- is the declared start step defined?
205
+ - **Transition targets** -- do all transitions point to steps that exist?
206
+ - **Reachability** -- are there orphan steps unreachable from the start?
207
+
208
+ **Example (valid flow):**
209
+
210
+ ```bash
211
+ $ flowengine-cli validate intake.rb
212
+ Flow definition is valid!
213
+ Start step: filing_status
214
+ Total steps: 4
215
+ Steps: filing_status, income_types, business_details, summary
216
+ ```
217
+
218
+ **Example (flow with errors):**
219
+
220
+ ```bash
221
+ $ flowengine-cli validate broken_flow.rb
222
+ Flow definition has errors:
223
+ - Step :income_types has transition to unknown step :nonexistent
224
+ - Step :orphan is unreachable from start step :filing_status
225
+ ```
226
+
227
+ ---
228
+
229
+ ### `version` -- Print Version
230
+
231
+ ```bash
232
+ $ flowengine-cli version
233
+ flowengine-cli 0.1.0
234
+ flowengine 0.1.0
235
+ ```
236
+
237
+ ## Step Types & TTY Rendering
238
+
239
+ The `Renderer` maps each `FlowEngine::Node` type to the appropriate [TTY::Prompt](https://github.com/piotrmurach/tty-prompt) widget:
240
+
241
+ | Step Type | DSL | TTY Widget | Returns |
242
+ |-----------|-----|------------|---------|
243
+ | `:single_select` | `type :single_select` | `prompt.select` | `String` |
244
+ | `:multi_select` | `type :multi_select` | `prompt.multi_select` (min: 1) | `Array<String>` |
245
+ | `:number_matrix` | `type :number_matrix` | `prompt.ask` per field (int) | `Hash<String, Integer>` |
246
+ | `:text` | `type :text` | `prompt.ask` | `String` |
247
+ | `:number` | `type :number` | `prompt.ask` (convert: int) | `Integer` |
248
+ | `:boolean` | `type :boolean` | `prompt.yes?` | `true` / `false` |
249
+ | `:display` | `type :display` | Prints text, waits for keypress | `nil` |
250
+
251
+ Unknown types fall back to `:text` rendering.
252
+
253
+ ## Flow Definition DSL
254
+
255
+ Flow files are plain Ruby scripts that call `FlowEngine.define`. The full DSL is provided by the `flowengine` core gem:
256
+
257
+ ```ruby
258
+ FlowEngine.define do
259
+ start :first_step # Required: declare entry point
260
+
261
+ step :first_step do
262
+ type :single_select # Step type (see table above)
263
+ question "Pick one:" # Prompt text shown to user
264
+ options %w[A B C] # Choices (for select types)
265
+ fields %w[X Y Z] # Fields (for number_matrix)
266
+ visible_if not_empty(:some_step) # Optional: DAG visibility rule
267
+
268
+ # Transitions (evaluated top-to-bottom, first match wins)
269
+ transition to: :step_b, if_rule: equals(:first_step, "A")
270
+ transition to: :step_c, if_rule: contains(:first_step, "B")
271
+ transition to: :default_step # Unconditional fallback
272
+ end
273
+ end
274
+ ```
275
+
276
+ ### Available Rules
277
+
278
+ Rules are composable AST objects used in `if_rule:` and `visible_if`:
279
+
280
+ | Rule | DSL Helper | Evaluates |
281
+ |------|-----------|-----------|
282
+ | Contains | `contains(:step, "val")` | `Array(answers[:step]).include?("val")` |
283
+ | Equals | `equals(:step, "val")` | `answers[:step] == "val"` |
284
+ | GreaterThan | `greater_than(:step, 100)` | `answers[:step].to_i > 100` |
285
+ | LessThan | `less_than(:step, 5)` | `answers[:step].to_i < 5` |
286
+ | NotEmpty | `not_empty(:step)` | `answers[:step]` is not nil, "", or [] |
287
+ | All (AND) | `all(rule1, rule2)` | All sub-rules must be true |
288
+ | Any (OR) | `any(rule1, rule2)` | At least one sub-rule must be true |
289
+
290
+ ### Composing Rules
291
+
292
+ ```ruby
293
+ transition to: :special_path,
294
+ if_rule: all(
295
+ equals(:filing_status, "Married"),
296
+ greater_than(:income, 100_000),
297
+ any(
298
+ contains(:income_types, "Business"),
299
+ contains(:income_types, "Investment")
300
+ )
301
+ )
302
+ ```
303
+
304
+ ## Full Example: Tax Intake Flow
305
+
306
+ Here is a realistic 17-step tax preparation intake flow demonstrating conditional branching, composite rules, and multiple paths:
307
+
308
+ ```ruby
309
+ # tax_intake.rb
310
+ FlowEngine.define do
311
+ start :filing_status
312
+
313
+ step :filing_status do
314
+ type :single_select
315
+ question "What is your filing status for 2025?"
316
+ options %w[single married_filing_jointly married_filing_separately head_of_household]
317
+ transition to: :dependents
318
+ end
319
+
320
+ step :dependents do
321
+ type :number
322
+ question "How many dependents do you have?"
323
+ transition to: :income_types
324
+ end
325
+
326
+ step :income_types do
327
+ type :multi_select
328
+ question "Select all income types that apply to you in 2025."
329
+ options %w[W2 1099 Business Investment Rental Retirement]
330
+ transition to: :business_count, if_rule: contains(:income_types, "Business")
331
+ transition to: :investment_details, if_rule: contains(:income_types, "Investment")
332
+ transition to: :rental_details, if_rule: contains(:income_types, "Rental")
333
+ transition to: :state_filing
334
+ end
335
+
336
+ step :business_count do
337
+ type :number
338
+ question "How many total businesses do you own or are a partner in?"
339
+ transition to: :complex_business_info, if_rule: greater_than(:business_count, 2)
340
+ transition to: :business_details
341
+ end
342
+
343
+ step :complex_business_info do
344
+ type :text
345
+ question "With more than 2 businesses, please provide your primary EIN and a brief description."
346
+ transition to: :business_details
347
+ end
348
+
349
+ step :business_details do
350
+ type :number_matrix
351
+ question "How many of each business type do you own?"
352
+ fields %w[RealEstate SCorp CCorp Trust LLC]
353
+ transition to: :investment_details, if_rule: contains(:income_types, "Investment")
354
+ transition to: :rental_details, if_rule: contains(:income_types, "Rental")
355
+ transition to: :state_filing
356
+ end
357
+
358
+ step :investment_details do
359
+ type :multi_select
360
+ question "What types of investments do you hold?"
361
+ options %w[Stocks Bonds Crypto RealEstate MutualFunds]
362
+ transition to: :crypto_details, if_rule: contains(:investment_details, "Crypto")
363
+ transition to: :rental_details, if_rule: contains(:income_types, "Rental")
364
+ transition to: :state_filing
365
+ end
366
+
367
+ step :crypto_details do
368
+ type :text
369
+ question "Describe your cryptocurrency transactions (exchanges, approximate transaction count)."
370
+ transition to: :rental_details, if_rule: contains(:income_types, "Rental")
371
+ transition to: :state_filing
372
+ end
373
+
374
+ step :rental_details do
375
+ type :number_matrix
376
+ question "Provide details about your rental properties."
377
+ fields %w[Residential Commercial Vacation]
378
+ transition to: :state_filing
379
+ end
380
+
381
+ step :state_filing do
382
+ type :multi_select
383
+ question "Which states do you need to file in?"
384
+ options %w[California NewYork Texas Florida Illinois Other]
385
+ transition to: :foreign_accounts
386
+ end
387
+
388
+ step :foreign_accounts do
389
+ type :single_select
390
+ question "Do you have any foreign financial accounts?"
391
+ options %w[yes no]
392
+ transition to: :foreign_account_details, if_rule: equals(:foreign_accounts, "yes")
393
+ transition to: :deduction_types
394
+ end
395
+
396
+ step :foreign_account_details do
397
+ type :number
398
+ question "How many foreign accounts do you have?"
399
+ transition to: :deduction_types
400
+ end
401
+
402
+ step :deduction_types do
403
+ type :multi_select
404
+ question "Which additional deductions apply to you?"
405
+ options %w[Medical Charitable Education Mortgage None]
406
+ transition to: :charitable_amount, if_rule: contains(:deduction_types, "Charitable")
407
+ transition to: :contact_info
408
+ end
409
+
410
+ step :charitable_amount do
411
+ type :number
412
+ question "What is your total estimated charitable contribution amount for 2025?"
413
+ transition to: :charitable_documentation, if_rule: greater_than(:charitable_amount, 5000)
414
+ transition to: :contact_info
415
+ end
416
+
417
+ step :charitable_documentation do
418
+ type :text
419
+ question "For contributions over $5,000, please list the organizations and amounts."
420
+ transition to: :contact_info
421
+ end
422
+
423
+ step :contact_info do
424
+ type :text
425
+ question "Please provide your contact information (name, email, phone)."
426
+ transition to: :review
427
+ end
428
+
429
+ step :review do
430
+ type :text
431
+ question "Thank you! Please review your information. Type 'confirm' to submit."
432
+ end
433
+ end
434
+ ```
435
+
436
+ Run it:
437
+
438
+ ```bash
439
+ flowengine-cli run tax_intake.rb -o tax_results.json
440
+ ```
441
+
442
+ Visualize it:
443
+
444
+ ```bash
445
+ flowengine-cli graph tax_intake.rb -o tax_flow.mmd
446
+ ```
447
+
448
+ The Mermaid output renders as:
449
+
450
+ ```mermaid
451
+ flowchart TD
452
+ filing_status["What is your filing status for 2025?"]
453
+ filing_status --> dependents
454
+ dependents["How many dependents do you have?"]
455
+ dependents --> income_types
456
+ income_types["Select all income types that apply to you in 2025."]
457
+ income_types -->|"Business in income_types"| business_count
458
+ income_types -->|"Investment in income_types"| investment_details
459
+ income_types -->|"Rental in income_types"| rental_details
460
+ income_types --> state_filing
461
+ business_count["How many total businesses do you own or are a part..."]
462
+ business_count -->|"business_count > 2"| complex_business_info
463
+ business_count --> business_details
464
+ complex_business_info["With more than 2 businesses, please provide your p..."]
465
+ complex_business_info --> business_details
466
+ business_details["How many of each business type do you own?"]
467
+ business_details -->|"Investment in income_types"| investment_details
468
+ business_details -->|"Rental in income_types"| rental_details
469
+ business_details --> state_filing
470
+ investment_details["What types of investments do you hold?"]
471
+ investment_details -->|"Crypto in investment_details"| crypto_details
472
+ investment_details -->|"Rental in income_types"| rental_details
473
+ investment_details --> state_filing
474
+ crypto_details["Describe your cryptocurrency transactions..."]
475
+ crypto_details -->|"Rental in income_types"| rental_details
476
+ crypto_details --> state_filing
477
+ rental_details["Provide details about your rental properties."]
478
+ rental_details --> state_filing
479
+ state_filing["Which states do you need to file in?"]
480
+ state_filing --> foreign_accounts
481
+ foreign_accounts["Do you have any foreign financial accounts?"]
482
+ foreign_accounts -->|"foreign_accounts == yes"| foreign_account_details
483
+ foreign_accounts --> deduction_types
484
+ foreign_account_details["How many foreign accounts do you have?"]
485
+ foreign_account_details --> deduction_types
486
+ deduction_types["Which additional deductions apply to you?"]
487
+ deduction_types -->|"Charitable in deduction_types"| charitable_amount
488
+ deduction_types --> contact_info
489
+ charitable_amount["What is your total estimated charitable contributi..."]
490
+ charitable_amount -->|"charitable_amount > 5000"| charitable_documentation
491
+ charitable_amount --> contact_info
492
+ charitable_documentation["For contributions over $5,000, please list the org..."]
493
+ charitable_documentation --> contact_info
494
+ contact_info["Please provide your contact information..."]
495
+ contact_info --> review
496
+ review["Thank you! Please review your information."]
497
+ ```
498
+
499
+ ## Architecture
500
+
501
+ ```
502
+ +-------------------+
503
+ | flowengine |
504
+ | (core gem) |
505
+ |-------------------|
506
+ | DSL + Definition |
507
+ | AST Rules |
508
+ | Evaluator |
509
+ | Engine Runtime |
510
+ | Mermaid Exporter |
511
+ +-------------------+
512
+ ^
513
+ |
514
+ +-----------+-----------+
515
+ | flowengine-cli |
516
+ |-----------------------|
517
+ | Dry::CLI commands |
518
+ | TTY::Prompt renderer |
519
+ | Flow file loader |
520
+ | JSON output |
521
+ +-----------------------+
522
+ ```
523
+
524
+ The core `flowengine` gem has **zero UI dependencies**. It provides the DSL, rule evaluation, and engine runtime. This gem (`flowengine-cli`) is a thin adapter that:
525
+
526
+ 1. **Loads** flow definitions from `.rb` files via `FlowLoader`
527
+ 2. **Renders** each step type to the appropriate TTY::Prompt widget via `Renderer`
528
+ 3. **Drives** the engine loop until completion
529
+ 4. **Outputs** results as structured JSON
530
+
531
+ ## Development
532
+
533
+ ```bash
534
+ git clone https://github.com/kigster/flowengine-cli.git
535
+ cd flowengine-cli
536
+ bin/setup
537
+ bundle exec rspec
538
+ ```
539
+
540
+ Tests use mocked `TTY::Prompt` instances so they run non-interactively. Coverage is enforced at 90% minimum via SimpleCov.
541
+
542
+ ```bash
543
+ bundle exec rspec # Run tests
544
+ bundle exec rubocop # Lint
545
+ bundle exec rake # Both
546
+ ```
547
+
548
+ ## Contributing
549
+
550
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/kigster/flowengine-cli).
551
+
552
+ ## License
553
+
554
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'timeout'
6
+ require 'yard'
7
+
8
+ def shell(*args)
9
+ puts "running: #{args.join(' ')}"
10
+ system(args.join(' '))
11
+ end
12
+
13
+ task :clean do
14
+ shell('rm -rf pkg/ tmp/ coverage/ doc/ ' )
15
+ end
16
+
17
+ task gem: [:build] do
18
+ shell('gem install pkg/*')
19
+ end
20
+
21
+ task permissions: [:clean] do
22
+ shell("chmod -v o+r,g+r * */* */*/* */*/*/* */*/*/*/* */*/*/*/*/*")
23
+ shell("find . -type d -exec chmod o+x,g+x {} \\;")
24
+ end
25
+
26
+ YARD::Rake::YardocTask.new(:doc) do |t|
27
+ t.files = %w(lib/**/*.rb exe/*.rb - README.md LICENSE.txt)
28
+ t.options.unshift('--title', '"FlowEngine CLI is the CLI for validating FlowEngine in the Terminal"')
29
+ t.after = -> { exec('open doc/index.html') } if RUBY_PLATFORM =~ /darwin/
30
+ end
31
+
32
+
33
+ task build: :permissions
34
+
35
+ RSpec::Core::RakeTask.new(:spec)
36
+
37
+ task default: :spec
data/exe/flow ADDED
@@ -0,0 +1 @@
1
+ flowengine-cli
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "flowengine/cli"
6
+
7
+ Dry::CLI.new(FlowEngine::CLI::Commands).call
data/justfile ADDED
@@ -0,0 +1,43 @@
1
+ set shell := ["bash", "-c"]
2
+
3
+ set dotenv-load
4
+
5
+ # Boot the app
6
+ test:
7
+ @bundle check || bundle install -j 12
8
+ @bundle exec rspec --format documentation
9
+
10
+ # Setup Ruby dependencies
11
+ setup-ruby:
12
+ #!/usr/bin/env bash
13
+ [[ -d ~/.rbenv ]] || git clone https://github.com/rbenv/rbenv.git ~/.rbenv
14
+ [[ -d ~/.rbenv/plugins/ruby-build ]] || git clone https://github.com/rbenv/rbenv.git ~/.rbenv
15
+ cd ~/.rbenv/plugins/ruby-build && git pull && cd - >/dev/null
16
+ echo -n "Checking if Ruby $(cat .ruby-version | tr -d '\n') is already installed..."
17
+ rbenv install -s "$(cat .ruby-version | tr -d '\n')" >/dev/null 2>&1 && echo "yes" || echo "it wasn't, but now it is"
18
+ bundle check || bundle install -j 12
19
+
20
+ # Setup NodeJS dependencies with Volta
21
+ setup-node:
22
+ @bash -c "command -v volta > dev/null 2>&1 || brew install volta"
23
+ @volta install node
24
+ @volta install yarn
25
+
26
+ # Setup everything
27
+ setup: setup-node setup-ruby
28
+
29
+ cli *ARGS:
30
+ ./cli {{ARGS}}
31
+
32
+ validate-valid-json:
33
+ ./cli validate-json -f VALID-JSON/sample.json -s VALID-JSON/schema.json
34
+
35
+ format:
36
+ @bundle exec rubocop -a
37
+ @bundle exec rubocop --auto-gen-config
38
+
39
+ lint:
40
+ @bundle exec rubocop
41
+
42
+ check-all: lint test
43
+
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ module CLI
5
+ module Commands
6
+ class Graph < Dry::CLI::Command
7
+ desc "Export a flow definition as a Mermaid diagram"
8
+
9
+ argument :flow_file, required: true, desc: "Path to flow definition (.rb file)"
10
+ option :output, aliases: ["-o"], desc: "Output file (default: stdout)"
11
+ option :format, default: "mermaid", desc: "Output format (mermaid)"
12
+
13
+ def call(flow_file:, **options)
14
+ definition = FlowLoader.load(flow_file)
15
+
16
+ mermaid = FlowEngine::Graph::MermaidExporter.new(definition).export
17
+
18
+ if options[:output]
19
+ File.write(options[:output], mermaid)
20
+ warn "Diagram written to #{options[:output]}"
21
+ else
22
+ puts mermaid
23
+ end
24
+ rescue FlowEngine::CLI::Error => e
25
+ warn "Error: #{e.message}"
26
+ exit 1
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "tty-box"
5
+ require "tty-screen"
6
+
7
+ module FlowEngine
8
+ module CLI
9
+ module Commands
10
+ class Run < Dry::CLI::Command
11
+ desc "Run a flow definition interactively"
12
+
13
+ argument :flow_file, required: true, desc: "Path to flow definition (.rb file)"
14
+ option :output, aliases: ["-o"], desc: "Output file for JSON results"
15
+
16
+ def call(flow_file:, **options)
17
+ engine = run_flow(flow_file)
18
+ json_output = JSON.pretty_generate(build_result(flow_file, engine))
19
+
20
+ display_success("Flow completed!")
21
+ puts json_output
22
+
23
+ write_output(options[:output], json_output) if options[:output]
24
+ rescue FlowEngine::CLI::Error => e
25
+ display_error(e.message)
26
+ exit 1
27
+ rescue FlowEngine::Error => e
28
+ display_error("Engine error: #{e.message}")
29
+ exit 1
30
+ end
31
+
32
+ private
33
+
34
+ def run_flow(flow_file)
35
+ definition = FlowLoader.load(flow_file)
36
+ engine = FlowEngine::Engine.new(definition)
37
+ renderer = Renderer.new
38
+
39
+ display_header("FlowEngine Interactive Wizard")
40
+
41
+ until engine.finished?
42
+ display_step_indicator(engine.current_step_id, engine.history.length)
43
+ engine.answer(renderer.render(engine.current_step))
44
+ end
45
+
46
+ engine
47
+ end
48
+
49
+ def build_result(flow_file, engine)
50
+ {
51
+ flow_file: flow_file,
52
+ path_taken: engine.history,
53
+ answers: engine.answers,
54
+ steps_completed: engine.history.length,
55
+ completed_at: Time.now.iso8601
56
+ }
57
+ end
58
+
59
+ def write_output(path, json_output)
60
+ File.write(path, json_output)
61
+ puts "\nResults saved to #{path}"
62
+ end
63
+
64
+ def display_header(text)
65
+ width = [TTY::Screen.width, 80].min
66
+ puts TTY::Box.frame(text, width: width, padding: 1, align: :center, border: :thick)
67
+ end
68
+
69
+ def display_step_indicator(step_id, step_number)
70
+ puts "\n Step #{step_number}: #{step_id}"
71
+ puts " #{"─" * 40}"
72
+ end
73
+
74
+ def display_success(text)
75
+ width = [TTY::Screen.width, 80].min
76
+ puts TTY::Box.frame(text, width: width, padding: 1, align: :center, title: { top_left: " SUCCESS " })
77
+ end
78
+
79
+ def display_error(text)
80
+ width = [TTY::Screen.width, 80].min
81
+ puts TTY::Box.frame(text, width: width, padding: 1, align: :center, title: { top_left: " ERROR " })
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ module CLI
5
+ module Commands
6
+ class ValidateFlow < Dry::CLI::Command
7
+ desc "Validate a flow definition file"
8
+
9
+ argument :flow_file, required: true, desc: "Path to flow definition (.rb file)"
10
+
11
+ def call(flow_file:, **)
12
+ definition = FlowLoader.load(flow_file)
13
+ errors = validate_definition(definition)
14
+
15
+ if errors.empty?
16
+ print_success(definition)
17
+ else
18
+ print_errors(errors)
19
+ exit 1
20
+ end
21
+ rescue FlowEngine::CLI::Error => e
22
+ warn "Error: #{e.message}"
23
+ exit 1
24
+ rescue FlowEngine::Error => e
25
+ warn "Definition error: #{e.message}"
26
+ exit 1
27
+ end
28
+
29
+ private
30
+
31
+ def print_success(definition)
32
+ puts "Flow definition is valid!"
33
+ puts " Start step: #{definition.start_step_id}"
34
+ puts " Total steps: #{definition.step_ids.length}"
35
+ puts " Steps: #{definition.step_ids.join(", ")}"
36
+ end
37
+
38
+ def print_errors(errors)
39
+ warn "Flow definition has errors:"
40
+ errors.each { |e| warn " - #{e}" }
41
+ end
42
+
43
+ def validate_definition(definition)
44
+ errors = []
45
+
46
+ validate_start_step(definition, errors)
47
+ validate_transition_targets(definition, errors)
48
+ validate_reachability(definition, errors)
49
+
50
+ errors
51
+ end
52
+
53
+ def validate_start_step(definition, errors)
54
+ return if definition.step_ids.include?(definition.start_step_id)
55
+
56
+ errors << "Start step :#{definition.start_step_id} not found in steps"
57
+ end
58
+
59
+ def validate_transition_targets(definition, errors)
60
+ definition.step_ids.each do |step_id|
61
+ step = definition.step(step_id)
62
+ step.transitions.each do |transition|
63
+ next if definition.step_ids.include?(transition.target)
64
+
65
+ errors << "Step :#{step_id} has transition to unknown step :#{transition.target}"
66
+ end
67
+ end
68
+ end
69
+
70
+ def validate_reachability(definition, errors)
71
+ reachable = find_reachable_steps(definition)
72
+ orphans = definition.step_ids - reachable
73
+
74
+ orphans.each do |orphan|
75
+ errors << "Step :#{orphan} is unreachable from start step :#{definition.start_step_id}"
76
+ end
77
+ end
78
+
79
+ def find_reachable_steps(definition)
80
+ visited = Set.new
81
+ queue = [definition.start_step_id]
82
+ known_ids = definition.step_ids
83
+
84
+ until queue.empty?
85
+ current = queue.shift
86
+ next if visited.include?(current)
87
+
88
+ visited << current
89
+ next unless known_ids.include?(current)
90
+
91
+ enqueue_transitions(definition.step(current), known_ids, queue)
92
+ end
93
+
94
+ visited.to_a
95
+ end
96
+
97
+ def enqueue_transitions(step, known_ids, queue)
98
+ step.transitions.each do |t|
99
+ queue << t.target if known_ids.include?(t.target)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ module CLI
5
+ module Commands
6
+ class Version < Dry::CLI::Command
7
+ desc "Print version information"
8
+
9
+ def call(**)
10
+ puts "flowengine-cli #{FlowEngine::CLI::VERSION}"
11
+ puts "flowengine #{FlowEngine::VERSION}"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require_relative "commands/run"
5
+ require_relative "commands/graph"
6
+ require_relative "commands/validate_flow"
7
+ require_relative "commands/version"
8
+
9
+ module FlowEngine
10
+ module CLI
11
+ module Commands
12
+ extend Dry::CLI::Registry
13
+
14
+ register "run", Run
15
+ register "graph", Graph
16
+ register "validate", ValidateFlow
17
+ register "version", Version
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ module CLI
5
+ class FlowLoader
6
+ def self.load(path)
7
+ new(path).load
8
+ end
9
+
10
+ def initialize(path)
11
+ @path = File.expand_path(path)
12
+ validate_path!
13
+ end
14
+
15
+ def load
16
+ content = File.read(@path)
17
+ # Evaluate in a clean binding that has FlowEngine available
18
+ # rubocop:disable Security/Eval
19
+ eval(content, TOPLEVEL_BINDING.dup, @path, 1)
20
+ # rubocop:enable Security/Eval
21
+ rescue SyntaxError => e
22
+ raise FlowEngine::CLI::Error, "Syntax error in #{@path}: #{e.message}"
23
+ end
24
+
25
+ private
26
+
27
+ def validate_path!
28
+ raise FlowEngine::CLI::Error, "File not found: #{@path}" unless File.exist?(@path)
29
+ raise FlowEngine::CLI::Error, "Not a .rb file: #{@path}" unless @path.end_with?(".rb")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+
5
+ module FlowEngine
6
+ module CLI
7
+ class Renderer
8
+ attr_reader :prompt
9
+
10
+ def initialize(prompt: TTY::Prompt.new)
11
+ @prompt = prompt
12
+ end
13
+
14
+ def render(node)
15
+ method_name = :"render_#{node.type}"
16
+
17
+ if respond_to?(method_name, true)
18
+ send(method_name, node)
19
+ else
20
+ render_text(node)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def render_multi_select(node)
27
+ prompt.multi_select(node.question, node.options, min: 1)
28
+ end
29
+
30
+ def render_single_select(node)
31
+ prompt.select(node.question, node.options)
32
+ end
33
+
34
+ def render_number_matrix(node)
35
+ puts "\n#{node.question}\n\n"
36
+ result = {}
37
+ node.fields.each do |field|
38
+ result[field] = prompt.ask(" #{field}:", convert: :int, default: 0)
39
+ end
40
+ result
41
+ end
42
+
43
+ def render_text(node)
44
+ prompt.ask(node.question)
45
+ end
46
+
47
+ def render_number(node)
48
+ prompt.ask(node.question, convert: :int)
49
+ end
50
+
51
+ def render_boolean(node) # rubocop:disable Naming/PredicateMethod
52
+ prompt.yes?(node.question)
53
+ end
54
+
55
+ def render_display(node)
56
+ puts "\n#{node.question}\n"
57
+ prompt.keypress("Press any key to continue...")
58
+ nil
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowEngine
4
+ module CLI
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "flowengine"
4
+ require_relative "cli/version"
5
+ require_relative "cli/flow_loader"
6
+ require_relative "cli/renderer"
7
+ require_relative "cli/commands"
8
+
9
+ module FlowEngine
10
+ module CLI
11
+ class Error < StandardError; end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module Flowengine
2
+ module Cli
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flowengine-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Gredeskoul
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dry-cli
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: flowengine
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: tty-box
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.7'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.7'
54
+ - !ruby/object:Gem::Dependency
55
+ name: tty-prompt
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.23'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.23'
68
+ - !ruby/object:Gem::Dependency
69
+ name: tty-screen
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.8'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.8'
82
+ - !ruby/object:Gem::Dependency
83
+ name: yard
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ description: Provides a TTY-based CLI to run FlowEngine flow definitions interactively,
97
+ export Mermaid diagrams, and validate flow files.
98
+ email:
99
+ - kigster@gmail.com
100
+ executables:
101
+ - flow
102
+ - flowengine-cli
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".envrc"
107
+ - ".rubocop_todo.yml"
108
+ - CHANGELOG.md
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - exe/flow
113
+ - exe/flowengine-cli
114
+ - justfile
115
+ - lib/flowengine/cli.rb
116
+ - lib/flowengine/cli/commands.rb
117
+ - lib/flowengine/cli/commands/graph.rb
118
+ - lib/flowengine/cli/commands/run.rb
119
+ - lib/flowengine/cli/commands/validate_flow.rb
120
+ - lib/flowengine/cli/commands/version.rb
121
+ - lib/flowengine/cli/flow_loader.rb
122
+ - lib/flowengine/cli/renderer.rb
123
+ - lib/flowengine/cli/version.rb
124
+ - sig/flowengine/cli.rbs
125
+ homepage: https://github.com/kigster/flowengine-cli
126
+ licenses:
127
+ - MIT
128
+ metadata:
129
+ allowed_push_host: https://rubygems.org
130
+ homepage_uri: https://github.com/kigster/flowengine-cli
131
+ source_code_uri: https://github.com/kigster/flowengine-cli
132
+ changelog_uri: https://github.com/kigster/flowengine-cli/blob/main/CHANGELOG.md
133
+ rubygems_mfa_required: 'true'
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: 4.0.1
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 4.0.3
149
+ specification_version: 4
150
+ summary: Terminal-based interactive wizard runner for FlowEngine flows
151
+ test_files: []