diff --git a/DEV-GUIDE.md b/DEV-GUIDE.md new file mode 100644 index 0000000..6c0eb77 --- /dev/null +++ b/DEV-GUIDE.md @@ -0,0 +1,120 @@ +# Concore Developer Guide + +## `concore init --interactive` + +The interactive init wizard lets users scaffold a new multi-language concore project without writing any boilerplate by hand. + +### Usage + +```bash +concore init --interactive +# or shorthand +concore init -i +``` + +The wizard prompts for each supported language with a `y/n` question (default: yes): + +``` +Select the node types to include (Enter = yes) + + Include Python node? [Y/n] + Include C++ node? [Y/n] + Include Octave/MATLAB node? [Y/n] + Include Verilog node? [Y/n] + Include Java node? [Y/n] +``` + +### What gets generated + +For each selected language, the wizard: + +1. Creates a **source stub** in `src/` with the correct concore API calls for that language. +2. Adds an **unconnected node** to `workflow.graphml`, colour-coded and vertically positioned for easy viewing in yEd. + +Example output for Python + Java selected: + +``` +my_project/ +├── workflow.graphml ← 2 unconnected nodes +├── src/ +│ ├── script.py ← Python stub +│ └── Script.java ← Java stub +├── README.md +└── STUDY.json +``` + +### Architecture + +The feature lives entirely in `concore_cli/commands/init.py`: + +| Symbol | Role | +|---|---| +| `LANGUAGE_NODES` | Dict mapping language key → `label`, `filename`, node `color`, source `stub` | +| `GRAPHML_HEADER` | XML template for the GraphML wrapper; `project_name` is escaped via `xml.sax.saxutils.quoteattr` | +| `GRAPHML_NODE` | XML template for a single node block | +| `run_wizard()` | Prompts y/n for each language; returns list of selected keys | +| `_build_graphml()` | Assembles the full GraphML string from selected languages | +| `init_project_interactive()` | Orchestrates directory creation, file writes, and success output | + +--- + +## Adding a New Language + +Adding Julia (or any other language) to the interactive wizard is a **one-file change** — just add an entry to the `LANGUAGE_NODES` dictionary in `concore_cli/commands/init.py`. + +### Step-by-step: adding Julia + +**1. Add the entry to `LANGUAGE_NODES`** in `concore_cli/commands/init.py`: + +```python +"julia": { + "label": "Julia", + "filename": "script.jl", + "color": "#9558b2", # Julia purple + "stub": ( + "using Concore\n\n" + "Concore.state.delay = 0.02\n" + "Concore.state.inpath = \"./in\"\n" + "Concore.state.outpath = \"./out\"\n\n" + "maxtime = default_maxtime(100.0)\n" + 'init_val = "[0.0, 0.0]"\n\n' + "while Concore.state.simtime < maxtime\n" + " while unchanged()\n" + ' val = Float64.(concore_read(1, "data", init_val))\n' + " end\n" + " # TODO: process val\n" + " result = val .* 2\n" + ' concore_write(1, "result", result, 0.0)\n' + "end\n" + "println(\"retry=$(Concore.state.retrycount)\")\n" + ), +}, +``` + +Key Julia API points (based on real concore Julia scripts): + +| Element | Julia equivalent | +|---|---| +| Import | `using Concore` | +| Setup | `Concore.state.delay`, `.inpath`, `.outpath` | +| Max time | `default_maxtime(100.0)` — returns the value | +| Sim time | `Concore.state.simtime` | +| Unchanged check | `unchanged()` — no module prefix | +| Read | `concore_read(port, name, initstr)` — snake\_case, no prefix | +| Write | `concore_write(port, name, val, delta)` — snake\_case, no prefix | +| Type cast | `Float64.(concore_read(...))` to ensure numeric type | + +**2. That's it.** No other files need to change — the wizard, GraphML builder, and file writer all iterate over `LANGUAGE_NODES` dynamically. + +### Node colours used + +| Language | Hex colour | +|---|---| +| Python | `#ffcc00` (yellow) | +| C++ | `#ae85ca` (purple) | +| Octave/MATLAB | `#6db3f2` (blue) | +| Verilog | `#f28c8c` (red) | +| Java | `#a8d8a8` (green) | +| Julia *(proposed)* | `#9558b2` (Julia purple) | + +--- diff --git a/concore_cli/cli.py b/concore_cli/cli.py index 92ca6e5..b5336aa 100644 --- a/concore_cli/cli.py +++ b/concore_cli/cli.py @@ -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 @@ -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." + ) + sys.exit(1) + init_project(name, template, console) except Exception as e: console.print(f"[red]Error:[/red] {str(e)}") sys.exit(1) diff --git a/concore_cli/commands/init.py b/concore_cli/commands/init.py index 8551972..64739b5 100644 --- a/concore_cli/commands/init.py +++ b/concore_cli/commands/init.py @@ -1,8 +1,39 @@ from pathlib import Path +from xml.sax.saxutils import quoteattr from rich.panel import Panel from .metadata import write_study_metadata +# --------------------------------------------------------------------------- +# GraphML templates +# --------------------------------------------------------------------------- + +GRAPHML_HEADER = """ + + + + +{nodes} + + +""" + +GRAPHML_NODE = """ + + + + + + N{idx}:{filename} + + + + """ + +# Single-node fallback used by non-interactive init SAMPLE_GRAPHML = """ @@ -23,20 +54,126 @@ """ -SAMPLE_PYTHON = """import concore - -concore.default_maxtime(100) -concore.delay = 0.02 +# --------------------------------------------------------------------------- +# Per-language metadata: label, filename, node colour, source stub +# --------------------------------------------------------------------------- -init_simtime_val = "[0.0, 0.0]" -val = concore.initval(init_simtime_val) - -while(concore.simtime\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 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" + " // TODO: process val (e.g. multiply by 2)\n" + ' concore.write(1, "result", val, 0);\n' + " }\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": ( + "import java.util.List;\n\n" + "public class Script {\n" + " public static void main(String[] args) throws Exception {\n" + " double maxtime = 100;\n" + ' String init_val = "[0.0, 0.0]";\n\n' + " // All concore methods are static\n" + " List val = concore.initVal(init_val);\n" + " while (concore.getSimtime() < maxtime) {\n" + " while (concore.unchanged()) {\n" + ' concore.ReadResult r = concore.read(1, "data", init_val);\n' + " val = r.data;\n" + " }\n" + " // TODO: process val (List)\n" + ' concore.write(1, "result", val, 0);\n' + " }\n" + " }\n" + "}\n" + ), + }, +} README_TEMPLATE = """# {project_name} @@ -59,14 +196,137 @@ ## 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=quoteattr(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), encoding="utf-8") + + # one source stub per selected language + for lang_key in selected_langs: + info = LANGUAGE_NODES[lang_key] + (src_path / info["filename"]).write_text(info["stub"], encoding="utf-8") + + # README + (project_path / "README.md").write_text( + README_TEMPLATE.format(project_name=name), encoding="utf-8" + ) + + # 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(): @@ -78,16 +338,16 @@ def init_project(name, template, console): (project_path / "src").mkdir() workflow_file = project_path / "workflow.graphml" - with open(workflow_file, "w") as f: + with open(workflow_file, "w", encoding="utf-8") as f: f.write(SAMPLE_GRAPHML) - 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"], encoding="utf-8" + ) - 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), encoding="utf-8" + ) metadata_info = "" try: