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 +7 -0
- data/.envrc +1 -0
- data/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +554 -0
- data/Rakefile +37 -0
- data/exe/flow +1 -0
- data/exe/flowengine-cli +7 -0
- data/justfile +43 -0
- data/lib/flowengine/cli/commands/graph.rb +31 -0
- data/lib/flowengine/cli/commands/run.rb +86 -0
- data/lib/flowengine/cli/commands/validate_flow.rb +105 -0
- data/lib/flowengine/cli/commands/version.rb +16 -0
- data/lib/flowengine/cli/commands.rb +20 -0
- data/lib/flowengine/cli/flow_loader.rb +33 -0
- data/lib/flowengine/cli/renderer.rb +62 -0
- data/lib/flowengine/cli/version.rb +7 -0
- data/lib/flowengine/cli.rb +13 -0
- data/sig/flowengine/cli.rbs +6 -0
- metadata +151 -0
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
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
|
+
[](https://github.com/kigster/flowengine-cli/actions/workflows/rspec.yml) [](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
|
data/exe/flowengine-cli
ADDED
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,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
|
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: []
|