Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions concore_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
import sys

from .commands.init import init_project
from .commands.init import init_project, init_project_interactive, run_wizard
from .commands.run import run_workflow
from .commands.validate import validate_workflow
from .commands.status import show_status
Expand All @@ -24,12 +24,32 @@ def cli():


@cli.command()
@click.argument("name", required=True)
@click.argument("name", required=False, default=None)
@click.option("--template", default="basic", help="Template type to use")
def init(name, template):
@click.option(
"--interactive",
"-i",
is_flag=True,
help="Launch guided wizard to select node types",
)
def init(name, template, interactive):
"""Create a new concore project"""
try:
init_project(name, template, console)
if interactive:
if not name:
name = console.input("[cyan]Project name:[/cyan] ").strip()
if not name:
console.print("[red]Error:[/red] Project name is required.")
sys.exit(1)
selected = run_wizard(console)
init_project_interactive(name, selected, console)
else:
if not name:
console.print(
"[red]Error:[/red] Provide a project name or use --interactive."
Comment thread
GREENRAT-K405 marked this conversation as resolved.
)
sys.exit(1)
init_project(name, template, console)
Comment on lines +27 to +52

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing CLI tests (e.g., tests/test_cli.py::test_init_command), but the new init --interactive/-i behavior isn’t covered. This introduces multiple new branches (prompting for missing name, language selection, multi-node GraphML generation) that can regress without tests.

Please add tests that exercise: (1) init -i <name> with a fixed selection via CliRunner.invoke(..., input=...) and assert that the expected stub files + node labels exist, and (2) init -i with empty name input returning a non-zero exit code and the expected error message.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can write the tests separately, will ask mentor

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, tests are an "easy" contribution. Can be a subsequent PR.

except Exception as e:
console.print(f"[red]Error:[/red] {str(e)}")
sys.exit(1)
Expand Down
293 changes: 272 additions & 21 deletions concore_cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@

from .metadata import write_study_metadata

# ---------------------------------------------------------------------------
# GraphML templates
# ---------------------------------------------------------------------------

GRAPHML_HEADER = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd"
xmlns:y="http://www.yworks.com/xml/graphml">
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="1" projectName="{project_name}">
Comment thread
GREENRAT-K405 marked this conversation as resolved.
Outdated
{nodes}
</graph>
</graphml>
"""

GRAPHML_NODE = """ <node id="n{idx}">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="50" width="150" x="100" y="{y}"/>
<y:Fill color="{color}" opacity="1"/>
<y:BorderStyle color="#000000" width="1"/>
<y:NodeLabel>N{idx}:{filename}</y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>"""

# Single-node fallback used by non-interactive init
SAMPLE_GRAPHML = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd" xmlns:y="http://www.yworks.com/xml/graphml">
<key for="node" id="d6" yfiles.type="nodegraphics"/>
Expand All @@ -23,20 +53,124 @@
</graphml>
"""

SAMPLE_PYTHON = """import concore

concore.default_maxtime(100)
concore.delay = 0.02

init_simtime_val = "[0.0, 0.0]"
val = concore.initval(init_simtime_val)
# ---------------------------------------------------------------------------
# Per-language metadata: label, filename, node colour, source stub
# ---------------------------------------------------------------------------

while(concore.simtime<concore.maxtime):
while concore.unchanged():
val = concore.read(1,"data",init_simtime_val)
result = [v * 2 for v in val]
concore.write(1,"result",result,delta=0)
"""
LANGUAGE_NODES = {
"python": {
"label": "Python",
"filename": "script.py",
"color": "#ffcc00",
"stub": (
"import concore\n\n"
"concore.default_maxtime(100)\n"
"concore.delay = 0.02\n\n"
'init_val = "[0.0, 0.0]"\n'
"val = concore.initval(init_val)\n\n"
"while concore.simtime < concore.maxtime:\n"
" while concore.unchanged():\n"
' val = concore.read(1, "data", init_val)\n'
" result = [v * 2 for v in val]\n"
' concore.write(1, "result", result, delta=0)\n'
),
},
"cpp": {
"label": "C++",
"filename": "script.cpp",
"color": "#ae85ca",
"stub": (
'#include "concore.hpp"\n'
"#include <vector>\n\n"
"int main() {\n"
" Concore concore;\n"
" concore.default_maxtime(100);\n"
" concore.delay = 0.02;\n\n"
' std::string init_val = "[0.0, 0.0]";\n'
" std::vector<double> val = concore.initval(init_val);\n\n"
" while (concore.simtime < concore.maxtime) {\n"
" while (concore.unchanged()) {\n"
' val = concore.read(1, "data", init_val);\n'
" }\n"
' concore.write(1, "result", val, 0);\n'
Comment thread
GREENRAT-K405 marked this conversation as resolved.
" }\n"
" return 0;\n"
"}\n"
),
},
"octave": {
"label": "Octave/MATLAB",
"filename": "script.m",
"color": "#6db3f2",
"stub": (
"global concore;\n"
"import_concore;\n\n"
"concore.delay = 0.02;\n"
"concore_default_maxtime(100);\n\n"
"init_val = '[0.0, 0.0]';\n"
"val = concore_initval(init_val);\n\n"
"while concore.simtime < concore.maxtime\n"
" while concore_unchanged()\n"
" val = concore_read(1, 'data', init_val);\n"
" end\n"
" result = val * 2;\n"
" concore_write(1, 'result', result, 0);\n"
"end\n"
),
},
"verilog": {
"label": "Verilog",
"filename": "script.v",
"color": "#f28c8c",
"stub": (
'`include "concore.v"\n\n'
"module script;\n"
" // concore module provides: simtime, maxtime, readdata, writedata, unchanged\n"
" // data[] and datasize are global arrays filled by readdata\n\n"
" real init_val[1:0]; // [simtime, value]\n"
" integer i;\n\n"
" initial begin\n"
" concore.simtime = 0;\n"
" // set your maxtime (or let concore.maxtime file override)\n\n"
" while (concore.simtime < 100) begin\n"
" while (concore.unchanged(0)) begin\n"
" // readdata fills concore.data[] and updates concore.simtime\n"
' concore.readdata(1, "data", "[0.0,0.0]");\n'
" end\n"
" // TODO: process concore.data[0..datasize-1]\n"
" concore.data[0] = concore.data[0] * 2;\n"
" concore.datasize = 1;\n"
' concore.writedata(1, "result", 0); // delta=0\n'
" end\n"
" $finish;\n"
" end\n"
"endmodule\n"
),
},
"java": {
"label": "Java",
"filename": "Script.java",
"color": "#a8d8a8",
"stub": (
"public class Script {\n"
" public static void main(String[] args) throws Exception {\n"
" concoredocker cd = new concoredocker();\n"
" double maxtime = 100;\n"
" double delay = 0.02;\n"
' String init_val = "[0.0, 0.0]";\n\n'
" String val = cd.initval(init_val);\n"
" while (cd.simtime() < maxtime) {\n"
" while (cd.unchanged()) {\n"
' val = cd.read(1, "data", init_val);\n'
" }\n"
" // TODO: process val\n"
' cd.write(1, "result", val, 0);\n'

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Java stub as generated here won’t compile against the repo’s Java APIs: concoredocker exposes static methods like initVal(...), read(...) returns a ReadResult, and simtime is accessed via getSimtime(). The stub currently calls non-existent instance methods (initval, simtime()) and treats read as returning String.

Please update the Java stub to match the actual concoredocker/concore API in this repository (method names/casing, static access, and handling of ReadResult.data).

Suggested change
" concoredocker cd = new concoredocker();\n"
" double maxtime = 100;\n"
" double delay = 0.02;\n"
' String init_val = "[0.0, 0.0]";\n\n'
" String val = cd.initval(init_val);\n"
" while (cd.simtime() < maxtime) {\n"
" while (cd.unchanged()) {\n"
' val = cd.read(1, "data", init_val);\n'
" }\n"
" // TODO: process val\n"
' cd.write(1, "result", val, 0);\n'
" double maxtime = 100;\n"
" double delay = 0.02;\n"
' String init_val = "[0.0, 0.0]";\n\n'
" String val = concoredocker.initVal(init_val);\n"
" while (concoredocker.getSimtime() < maxtime) {\n"
" while (concoredocker.unchanged()) {\n"
' concoredocker.ReadResult result = concoredocker.read(1, "data", init_val);\n'
" val = result.data;\n"
" }\n"
" // TODO: process val\n"
' concoredocker.write(1, "result", val, 0);\n'

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can improve after java has been properly integrated in concore

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@avinxshKD has largely implemented Java version, and so our Java implementation is probably beyond the "toy" phase. What do you say, @avinxshKD?

" }\n"
" }\n"
"}\n"
),
},
}

README_TEMPLATE = """# {project_name}

Expand All @@ -59,14 +193,135 @@

## Next Steps

- Modify `workflow.graphml` to define your processing pipeline
- Add Python/C++/MATLAB scripts to `src/`
- Open `workflow.graphml` in yEd and connect the nodes with edges
- Use `concore validate workflow.graphml` to check your workflow
- Use `concore status` to monitor running processes
"""


# ---------------------------------------------------------------------------
# Interactive wizard
# ---------------------------------------------------------------------------


def run_wizard(console):
"""Ask y/n for each supported language. Returns list of selected lang keys."""
console.print()
console.print(
"[bold cyan]Select the node types to include[/bold cyan] "
"[dim](Enter = yes)[/dim]"
)
console.print()

selected = []
for key, info in LANGUAGE_NODES.items():
raw = (
console.input(f" Include [bold]{info['label']}[/bold] node? [Y/n] ")
.strip()
.lower()
)
if raw in ("", "y", "yes"):
selected.append(key)

return selected


# ---------------------------------------------------------------------------
# GraphML builder
# ---------------------------------------------------------------------------


def _build_graphml(project_name, selected_langs):
"""Return a GraphML string with one unconnected node per selected language."""
node_blocks = []
for idx, lang_key in enumerate(selected_langs, start=1):
info = LANGUAGE_NODES[lang_key]
node_blocks.append(
GRAPHML_NODE.format(
idx=idx,
y=100 + (idx - 1) * 100, # stack vertically, 100 px apart
color=info["color"],
filename=info["filename"],
)
)
return GRAPHML_HEADER.format(
project_name=project_name,
nodes="\n".join(node_blocks),
)


# ---------------------------------------------------------------------------
# Public entry points
# ---------------------------------------------------------------------------


def init_project_interactive(name, selected_langs, console):
"""Create a project with one node per selected language (no edges)."""
project_path = Path(name)

if project_path.exists():
raise FileExistsError(f"Directory '{name}' already exists")

if not selected_langs:
console.print("[yellow]No languages selected — nothing to create.[/yellow]")
return

console.print()
console.print(f"[cyan]Creating project:[/cyan] {name}")

project_path.mkdir()
src_path = project_path / "src"
src_path.mkdir()

# workflow.graphml
workflow_file = project_path / "workflow.graphml"
workflow_file.write_text(_build_graphml(name, selected_langs))

# one source stub per selected language
for lang_key in selected_langs:
info = LANGUAGE_NODES[lang_key]
Comment thread
GREENRAT-K405 marked this conversation as resolved.
(src_path / info["filename"]).write_text(info["stub"])

# README
(project_path / "README.md").write_text(README_TEMPLATE.format(project_name=name))

# Metadata
metadata_info = ""
try:
metadata_path = write_study_metadata(
project_path,
generated_by="concore init --interactive",
workflow_file=workflow_file,
)
metadata_info = f"Metadata:\n {metadata_path.name}\n\n"
except Exception as exc:
console.print(
f"[yellow]Warning:[/yellow] Failed to write study metadata: {exc}"
)

node_lines = "\n".join(
f" N{i}: {LANGUAGE_NODES[k]['filename']}"
for i, k in enumerate(selected_langs, 1)
)

console.print()
console.print(
Panel.fit(
f"[green]✓[/green] Project created with {len(selected_langs)} node(s)!\n\n"
f"{metadata_info}"
f"Nodes (unconnected — connect them in yEd):\n{node_lines}\n\n"
f"Next steps:\n"
f" cd {name}\n"
f" concore validate workflow.graphml\n"
f" concore run workflow.graphml",
title="Success",
border_style="green",
)
)


def init_project(name, template, console):
"""Non-interactive init — single Python node skeleton."""
project_path = Path(name)

if project_path.exists():
Expand All @@ -81,13 +336,9 @@ def init_project(name, template, console):
with open(workflow_file, "w") as f:
f.write(SAMPLE_GRAPHML)
Comment thread
GREENRAT-K405 marked this conversation as resolved.

sample_script = project_path / "src" / "script.py"
with open(sample_script, "w") as f:
f.write(SAMPLE_PYTHON)
(project_path / "src" / "script.py").write_text(LANGUAGE_NODES["python"]["stub"])

readme_file = project_path / "README.md"
with open(readme_file, "w") as f:
f.write(README_TEMPLATE.format(project_name=name))
(project_path / "README.md").write_text(README_TEMPLATE.format(project_name=name))

metadata_info = ""
try:
Expand Down
Loading