Skip to content

Commit 8f2a66a

Browse files
hyperpolymathclaude
andcommitted
feat(fs-workflow): add --dry-run flag, crate README, improved error messages
- Add --dry-run flag to `rpa-fs run` — validates and displays workflow plan (watch paths, rules, actions) without executing - Add crate README.md with usage, config examples, action/event reference - Improve config loading errors: show file path, JSON line/column on parse failure - --version already supported via clap derive Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9109ead commit 8f2a66a

3 files changed

Lines changed: 188 additions & 3 deletions

File tree

crates/rpa-fs-workflow/README.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<!-- SPDX-License-Identifier: PMPL-1.0-or-later -->
2+
# rpa-fs-workflow
3+
4+
Filesystem workflow automation CLI for [RPA Elysium](../../README.adoc).
5+
6+
## Overview
7+
8+
`rpa-fs` watches directories for file system events (create, modify, delete, rename) and
9+
executes configured actions — copy, move, archive, delete, rename, or plugin-based operations.
10+
11+
## Installation
12+
13+
```bash
14+
cargo install --path .
15+
# or from workspace root:
16+
just install
17+
```
18+
19+
## Usage
20+
21+
```bash
22+
# Generate an example workflow config
23+
rpa-fs init workflow.json
24+
25+
# Validate a config without running
26+
rpa-fs validate workflow.json
27+
28+
# Run a workflow
29+
rpa-fs run workflow.json
30+
31+
# Dry run — validate and display plan without executing
32+
rpa-fs run --dry-run workflow.json
33+
34+
# Verbose output
35+
rpa-fs -v run workflow.json
36+
37+
# Show version
38+
rpa-fs --version
39+
```
40+
41+
## Configuration
42+
43+
Workflow configs are JSON or [Nickel](https://nickel-lang.org/) files.
44+
45+
### JSON Example
46+
47+
```json
48+
{
49+
"name": "backup-documents",
50+
"description": "Auto-backup PDFs and spreadsheets",
51+
"watch": [
52+
{ "path": "/home/user/Downloads", "recursive": true }
53+
],
54+
"rules": [
55+
{
56+
"name": "backup-pdfs",
57+
"patterns": ["*.pdf"],
58+
"events": ["created"],
59+
"actions": [
60+
{ "type": "copy", "destination": "/home/user/Backups/pdf" }
61+
]
62+
},
63+
{
64+
"name": "archive-old-logs",
65+
"patterns": ["*.log"],
66+
"events": ["modified"],
67+
"actions": [
68+
{ "type": "archive", "destination": "/home/user/Archives", "format": "tar_gz" }
69+
]
70+
}
71+
]
72+
}
73+
```
74+
75+
### Nickel Example
76+
77+
See [`examples/workflow.ncl`](../../examples/workflow.ncl) for a Nickel configuration example.
78+
79+
## Action Types
80+
81+
| Action | Description |
82+
|--------|-------------|
83+
| `copy` | Copy file to destination (with optional overwrite and structure preservation) |
84+
| `move` | Move file to destination |
85+
| `archive` | Compress file to tar.gz or zip archive |
86+
| `delete` | Delete the file (with optional trash support) |
87+
| `rename` | Rename file using `{name}` and `{ext}` pattern substitution |
88+
| `plugin` | Execute a WASM plugin action (requires rpa-plugin integration) |
89+
90+
## Event Types
91+
92+
| Event | Triggers when |
93+
|-------|---------------|
94+
| `created` | A new file appears in the watched directory |
95+
| `modified` | An existing file is changed |
96+
| `deleted` | A file is removed |
97+
| `renamed` | A file is renamed or moved within the watch scope |
98+
99+
## Architecture
100+
101+
```
102+
rpa-fs-workflow/
103+
├── src/
104+
│ ├── main.rs # CLI entry point (clap)
105+
│ ├── lib.rs # Public API
106+
│ ├── config.rs # JSON/Nickel config parsing
107+
│ ├── runner.rs # Workflow execution engine
108+
│ ├── watcher.rs # Filesystem event watcher (notify)
109+
│ └── actions/ # Action implementations
110+
│ ├── copy.rs
111+
│ ├── move_file.rs
112+
│ ├── archive.rs
113+
│ ├── delete.rs
114+
│ ├── rename.rs
115+
│ └── plugin.rs
116+
```
117+
118+
## License
119+
120+
PMPL-1.0-or-later

crates/rpa-fs-workflow/src/config.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,22 @@ impl WorkflowConfig {
170170
/// Load from JSON file
171171
fn load_json(path: &Path) -> Result<Self> {
172172
debug!("Loading JSON config from {}", path.display());
173-
let content = std::fs::read_to_string(path)?;
174-
let config: Self = serde_json::from_str(&content)?;
173+
let content = std::fs::read_to_string(path).map_err(|e| {
174+
Error::Config(format!(
175+
"Cannot read config file '{}': {}",
176+
path.display(),
177+
e
178+
))
179+
})?;
180+
let config: Self = serde_json::from_str(&content).map_err(|e| {
181+
Error::Config(format!(
182+
"Invalid JSON in '{}': {} (line {}, column {})",
183+
path.display(),
184+
e,
185+
e.line(),
186+
e.column()
187+
))
188+
})?;
175189
config.validate()?;
176190
Ok(config)
177191
}

crates/rpa-fs-workflow/src/main.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ enum Commands {
5050
Run {
5151
/// Path to the workflow configuration file (.json or .ncl)
5252
config: PathBuf,
53+
54+
/// Validate and display the workflow without executing it
55+
#[arg(long)]
56+
dry_run: bool,
5357
},
5458

5559
/// Generate an example configuration file
@@ -84,7 +88,13 @@ fn main() {
8488
.init();
8589

8690
let result = match cli.command {
87-
Commands::Run { config } => run_workflow(config),
91+
Commands::Run { config, dry_run } => {
92+
if dry_run {
93+
dry_run_workflow(config)
94+
} else {
95+
run_workflow(config)
96+
}
97+
}
8898
Commands::Init { output } => init_workflow(output),
8999
Commands::Validate { config } => validate_workflow(config),
90100
};
@@ -95,6 +105,47 @@ fn main() {
95105
}
96106
}
97107

108+
/// Validate and display workflow plan without executing
109+
fn dry_run_workflow(config_path: PathBuf) -> anyhow::Result<()> {
110+
info!("Dry run — loading workflow from: {}", config_path.display());
111+
112+
let config = WorkflowConfig::load(&config_path)?;
113+
114+
info!("Workflow: {}", config.workflow.name);
115+
if let Some(desc) = &config.workflow.description {
116+
info!(" Description: {}", desc);
117+
}
118+
info!(" Version: {}", config.workflow.version);
119+
info!(" Enabled: {}", config.workflow.enabled);
120+
121+
info!("Watch paths ({}):", config.watch.len());
122+
for watch in &config.watch {
123+
info!(
124+
" - {} (recursive: {})",
125+
watch.path.display(),
126+
watch.recursive
127+
);
128+
if !watch.path.exists() {
129+
info!(" [WARNING] Path does not exist — will be created at runtime");
130+
}
131+
}
132+
133+
info!("Rules ({}):", config.rules.len());
134+
for rule in &config.rules {
135+
info!(
136+
" - {} (enabled: {}, events: {:?}, patterns: {:?}, actions: {})",
137+
rule.name,
138+
rule.enabled,
139+
rule.events,
140+
rule.patterns,
141+
rule.actions.len()
142+
);
143+
}
144+
145+
info!("Dry run complete — configuration is valid, no actions were executed.");
146+
Ok(())
147+
}
148+
98149
fn run_workflow(config_path: PathBuf) -> anyhow::Result<()> {
99150
info!("Loading workflow from: {}", config_path.display());
100151

0 commit comments

Comments
 (0)