-
Notifications
You must be signed in to change notification settings - Fork 31
Add CLI for concore (#183) #189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Add CLI for concore (#183) #189
Conversation
Features: - concore init: Create new project with templates - concore run: Execute workflows with auto-build option - concore validate: Check GraphML files before running - concore status: Show running processes with details - concore stop: Stop all concore processes safely Tech: - Built with Click and Rich for beautiful output - Cross-platform support (Windows, Linux, macOS) - All 9 tests passing Closes ControlCore-Project#183
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds a Click/Rich-based concore command-line interface to streamline creating, validating, running, and managing concore workflows (closes #183).
Changes:
- Introduces
concore_clipackage withinit,run,validate,status, andstopcommands. - Adds packaging (
setup.py) and updates dependencies (requirements.txt) to distribute the CLI. - Adds CLI documentation and a basic CLI test suite.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 28 comments.
Show a summary per file
| File | Description |
|---|---|
concore_cli/cli.py |
Defines the Click command group and command wiring. |
concore_cli/commands/init.py |
Project scaffolding/template generation for new workflows. |
concore_cli/commands/run.py |
Workflow generation + optional build execution. |
concore_cli/commands/validate.py |
GraphML/XML validation and human-readable reporting. |
concore_cli/commands/status.py |
Process discovery and display of running concore processes. |
concore_cli/commands/stop.py |
Process termination logic for concore-related processes. |
concore_cli/commands/__init__.py |
Exposes command helpers from the commands package. |
concore_cli/__init__.py |
Package export surface for the CLI. |
concore_cli/README.md |
CLI usage documentation. |
setup.py |
Adds distribution metadata and console entrypoint. |
requirements.txt |
Adds runtime dependencies for Click/Rich/psutil. |
tests/test_cli.py |
Adds CLI tests for help/version/init/validate/status/run error paths. |
README.md |
Documents the new CLI at the repository level. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| result = subprocess.run( | ||
| [sys.executable, 'mkconcore.py', str(workflow_path), str(source_path), str(output_path), exec_type], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True | ||
| ) |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run_workflow invokes mkconcore.py via a relative path, so concore run will fail unless the current working directory happens to contain mkconcore.py (e.g., when installed and executed elsewhere). Resolve the generator script path relative to the installed package/repo location and ensure it’s included in the distribution so the CLI works after pip install.
| build_script = output_path / ('build.bat' if exec_type == 'windows' else 'build') | ||
|
|
||
| if build_script.exists(): | ||
| with Progress( | ||
| SpinnerColumn(), | ||
| TextColumn("[progress.description]{task.description}"), | ||
| console=console | ||
| ) as progress: | ||
| task = progress.add_task("Building workflow...", total=None) | ||
|
|
||
| try: | ||
| result = subprocess.run( | ||
| [str(build_script)], | ||
| cwd=output_path, | ||
| capture_output=True, | ||
| text=True, | ||
| shell=True, | ||
| check=True |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto_build is likely broken for the default relative output (e.g., out): build_script is computed as output_path / 'build', but the subprocess uses cwd=output_path and then executes str(build_script) (e.g., out/build from within out/), which won’t be found. Execute the script relative to cwd (e.g., build/build.bat) or pass an absolute path; also avoid combining shell=True with a single-item list unless you specifically need shell semantics.
| show_results(console, errors, warnings, info) | ||
|
|
||
| except FileNotFoundError: | ||
| console.print(f"[red]Error:[/red] File not found: {workflow_path}") | ||
| except Exception as e: |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validation failures only affect printed output; validate_workflow never signals failure to the caller (no exception/return value), so concore validate will exit 0 even when errors is non-empty. Return an explicit success/failure result (or raise on errors) and have the CLI exit non-zero when validation fails.
| long_description=long_description, | ||
| long_description_content_type="text/markdown", | ||
| url="https://github.com/ControlCore-Project/concore", | ||
| packages=find_packages(), |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setup.py uses find_packages(), which will not include top-level modules like concore.py, mkconcore.py, or copy_with_port_portname.py. As a result, an installed package can expose the concore console script but still fail at runtime (notably concore run) and generated templates’ import concore won’t work. Include the required top-level modules (e.g., via py_modules or by packaging them) and consider excluding tests from the distributed packages.
| packages=find_packages(), | |
| packages=find_packages(exclude=("tests", "tests.*")), | |
| py_modules=["concore", "mkconcore", "copy_with_port_portname"], |
| def test_run_command_existing_output(self): | ||
| with self.runner.isolated_filesystem(temp_dir=self.temp_dir): | ||
| result = self.runner.invoke(cli, ['init', 'test-project']) | ||
| Path('output').mkdir() | ||
|
|
||
| result = self.runner.invoke(cli, [ | ||
| 'run', | ||
| 'test-project/workflow.graphml', | ||
| '--source', 'test-project/src', | ||
| '--output', 'output' | ||
| ]) | ||
| self.assertIn('already exists', result.output.lower()) |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test only asserts that the output contains an "already exists" message, but it doesn’t assert a non-zero exit code. If concore run treats an existing output directory as an error (which is typically expected), add an exit_code != 0 assertion here so tests enforce correct CLI semantics.
| except: | ||
| uptime_str = "unknown" | ||
|
|
||
| try: | ||
| mem_mb = proc.info['memory_info'].rss / 1024 / 1024 | ||
| mem_str = f"{mem_mb:.1f} MB" | ||
| except: |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except block directly handles BaseException.
| except: | |
| uptime_str = "unknown" | |
| try: | |
| mem_mb = proc.info['memory_info'].rss / 1024 / 1024 | |
| mem_str = f"{mem_mb:.1f} MB" | |
| except: | |
| except Exception: | |
| uptime_str = "unknown" | |
| try: | |
| mem_mb = proc.info['memory_info'].rss / 1024 / 1024 | |
| mem_str = f"{mem_mb:.1f} MB" | |
| except Exception: |
| except: | ||
| uptime_str = "unknown" | ||
|
|
||
| try: | ||
| mem_mb = proc.info['memory_info'].rss / 1024 / 1024 | ||
| mem_str = f"{mem_mb:.1f} MB" | ||
| except: |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except block directly handles BaseException.
| except: | |
| uptime_str = "unknown" | |
| try: | |
| mem_mb = proc.info['memory_info'].rss / 1024 / 1024 | |
| mem_str = f"{mem_mb:.1f} MB" | |
| except: | |
| except (KeyError, OSError, ValueError, psutil.Error): | |
| uptime_str = "unknown" | |
| try: | |
| mem_mb = proc.info['memory_info'].rss / 1024 / 1024 | |
| mem_str = f"{mem_mb:.1f} MB" | |
| except (KeyError, AttributeError, psutil.Error): |
| proc.kill() | ||
| console.print(f" [yellow]⚠[/yellow] Force killed {name} (PID: {pid})") | ||
| killed_count += 1 | ||
| except: |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except block directly handles BaseException.
| except: | |
| except Exception: |
| zmq_edges += 1 | ||
| else: | ||
| file_edges += 1 | ||
| except: |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except block directly handles BaseException.
| except: | |
| except Exception: |
| zmq_edges += 1 | ||
| else: | ||
| file_edges += 1 | ||
| except: |
Copilot
AI
Feb 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| except: | |
| except Exception: | |
| # Edge label lookup/parsing is best-effort; ignore any issues and continue. |
Features:
Tech:
Closes #183