Skip to content

Commit b44e9a1

Browse files
committed
Add interactive mode to concore init CLI
1 parent c898269 commit b44e9a1

2 files changed

Lines changed: 294 additions & 25 deletions

File tree

concore_cli/cli.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44
import sys
55

6-
from .commands.init import init_project
6+
from .commands.init import init_project, init_project_interactive, run_wizard
77
from .commands.run import run_workflow
88
from .commands.validate import validate_workflow
99
from .commands.status import show_status
@@ -24,12 +24,29 @@ def cli():
2424

2525

2626
@cli.command()
27-
@click.argument("name", required=True)
27+
@click.argument("name", required=False, default=None)
2828
@click.option("--template", default="basic", help="Template type to use")
29-
def init(name, template):
29+
@click.option(
30+
"--interactive", "-i",
31+
is_flag=True,
32+
help="Launch guided wizard to select node types",
33+
)
34+
def init(name, template, interactive):
3035
"""Create a new concore project"""
3136
try:
32-
init_project(name, template, console)
37+
if interactive:
38+
if not name:
39+
name = console.input("[cyan]Project name:[/cyan] ").strip()
40+
if not name:
41+
console.print("[red]Error:[/red] Project name is required.")
42+
sys.exit(1)
43+
selected = run_wizard(console)
44+
init_project_interactive(name, selected, console)
45+
else:
46+
if not name:
47+
console.print("[red]Error:[/red] Provide a project name or use --interactive.")
48+
sys.exit(1)
49+
init_project(name, template, console)
3350
except Exception as e:
3451
console.print(f"[red]Error:[/red] {str(e)}")
3552
sys.exit(1)

concore_cli/commands/init.py

Lines changed: 273 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,36 @@
33

44
from .metadata import write_study_metadata
55

6+
# ---------------------------------------------------------------------------
7+
# GraphML templates
8+
# ---------------------------------------------------------------------------
9+
10+
GRAPHML_HEADER = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
11+
<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
12+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
13+
xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd"
14+
xmlns:y="http://www.yworks.com/xml/graphml">
15+
<key for="node" id="d6" yfiles.type="nodegraphics"/>
16+
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
17+
<graph edgedefault="directed" id="1" projectName="{project_name}">
18+
{nodes}
19+
</graph>
20+
</graphml>
21+
"""
22+
23+
GRAPHML_NODE = """ <node id="n{idx}">
24+
<data key="d6">
25+
<y:ShapeNode>
26+
<y:Geometry height="50" width="150" x="100" y="{y}"/>
27+
<y:Fill color="{color}" opacity="1"/>
28+
<y:BorderStyle color="#000000" width="1"/>
29+
<y:NodeLabel>N{idx}:{filename}</y:NodeLabel>
30+
<y:Shape type="rectangle"/>
31+
</y:ShapeNode>
32+
</data>
33+
</node>"""
34+
35+
# Single-node fallback used by non-interactive init
636
SAMPLE_GRAPHML = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
737
<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">
838
<key for="node" id="d6" yfiles.type="nodegraphics"/>
@@ -23,20 +53,124 @@
2353
</graphml>
2454
"""
2555

26-
SAMPLE_PYTHON = """import concore
27-
28-
concore.default_maxtime(100)
29-
concore.delay = 0.02
30-
31-
init_simtime_val = "[0.0, 0.0]"
32-
val = concore.initval(init_simtime_val)
56+
# ---------------------------------------------------------------------------
57+
# Per-language metadata: label, filename, node colour, source stub
58+
# ---------------------------------------------------------------------------
3359

34-
while(concore.simtime<concore.maxtime):
35-
while concore.unchanged():
36-
val = concore.read(1,"data",init_simtime_val)
37-
result = [v * 2 for v in val]
38-
concore.write(1,"result",result,delta=0)
39-
"""
60+
LANGUAGE_NODES = {
61+
"python": {
62+
"label": "Python",
63+
"filename": "script.py",
64+
"color": "#ffcc00",
65+
"stub": (
66+
"import concore\n\n"
67+
"concore.default_maxtime(100)\n"
68+
"concore.delay = 0.02\n\n"
69+
'init_val = "[0.0, 0.0]"\n'
70+
"val = concore.initval(init_val)\n\n"
71+
"while concore.simtime < concore.maxtime:\n"
72+
" while concore.unchanged():\n"
73+
' val = concore.read(1, "data", init_val)\n'
74+
" result = [v * 2 for v in val]\n"
75+
' concore.write(1, "result", result, delta=0)\n'
76+
),
77+
},
78+
"cpp": {
79+
"label": "C++",
80+
"filename": "script.cpp",
81+
"color": "#ae85ca",
82+
"stub": (
83+
'#include "concore.hpp"\n'
84+
"#include <vector>\n\n"
85+
"int main() {\n"
86+
" Concore concore;\n"
87+
" concore.default_maxtime(100);\n"
88+
" concore.delay = 0.02;\n\n"
89+
' std::string init_val = "[0.0, 0.0]";\n'
90+
" std::vector<double> val = concore.initval(init_val);\n\n"
91+
" while (concore.simtime < concore.maxtime) {\n"
92+
" while (concore.unchanged()) {\n"
93+
' val = concore.read(1, "data", init_val);\n'
94+
" }\n"
95+
' concore.write(1, "result", val, 0);\n'
96+
" }\n"
97+
" return 0;\n"
98+
"}\n"
99+
),
100+
},
101+
"octave": {
102+
"label": "Octave/MATLAB",
103+
"filename": "script.m",
104+
"color": "#6db3f2",
105+
"stub": (
106+
"global concore;\n"
107+
"import_concore;\n\n"
108+
"concore.delay = 0.02;\n"
109+
"concore_default_maxtime(100);\n\n"
110+
"init_val = '[0.0, 0.0]';\n"
111+
"val = concore_initval(init_val);\n\n"
112+
"while concore.simtime < concore.maxtime\n"
113+
" while concore_unchanged()\n"
114+
" val = concore_read(1, 'data', init_val);\n"
115+
" end\n"
116+
" result = val * 2;\n"
117+
" concore_write(1, 'result', result, 0);\n"
118+
"end\n"
119+
),
120+
},
121+
"verilog": {
122+
"label": "Verilog",
123+
"filename": "script.v",
124+
"color": "#f28c8c",
125+
"stub": (
126+
'`include "concore.v"\n\n'
127+
"module script;\n"
128+
" // concore module provides: simtime, maxtime, readdata, writedata, unchanged\n"
129+
" // data[] and datasize are global arrays filled by readdata\n\n"
130+
" real init_val[1:0]; // [simtime, value]\n"
131+
" integer i;\n\n"
132+
" initial begin\n"
133+
" concore.simtime = 0;\n"
134+
" // set your maxtime (or let concore.maxtime file override)\n\n"
135+
" while (concore.simtime < 100) begin\n"
136+
' while (concore.unchanged(0)) begin\n'
137+
" // readdata fills concore.data[] and updates concore.simtime\n"
138+
' concore.readdata(1, "data", "[0.0,0.0]");\n'
139+
" end\n"
140+
" // TODO: process concore.data[0..datasize-1]\n"
141+
" concore.data[0] = concore.data[0] * 2;\n"
142+
" concore.datasize = 1;\n"
143+
' concore.writedata(1, "result", 0); // delta=0\n'
144+
" end\n"
145+
" $finish;\n"
146+
" end\n"
147+
"endmodule\n"
148+
),
149+
},
150+
"java": {
151+
"label": "Java",
152+
"filename": "Script.java",
153+
"color": "#a8d8a8",
154+
"stub": (
155+
"public class Script {\n"
156+
" public static void main(String[] args) throws Exception {\n"
157+
" concoredocker cd = new concoredocker();\n"
158+
" double maxtime = 100;\n"
159+
" double delay = 0.02;\n"
160+
' String init_val = "[0.0, 0.0]";\n\n'
161+
" String val = cd.initval(init_val);\n"
162+
" while (cd.simtime() < maxtime) {\n"
163+
" while (cd.unchanged()) {\n"
164+
' val = cd.read(1, "data", init_val);\n'
165+
" }\n"
166+
" // TODO: process val\n"
167+
' cd.write(1, "result", val, 0);\n'
168+
" }\n"
169+
" }\n"
170+
"}\n"
171+
),
172+
},
173+
}
40174

41175
README_TEMPLATE = """# {project_name}
42176
@@ -59,14 +193,132 @@
59193
60194
## Next Steps
61195
62-
- Modify `workflow.graphml` to define your processing pipeline
63-
- Add Python/C++/MATLAB scripts to `src/`
196+
- Open `workflow.graphml` in yEd and connect the nodes with edges
64197
- Use `concore validate workflow.graphml` to check your workflow
65198
- Use `concore status` to monitor running processes
66199
"""
67200

68201

202+
# ---------------------------------------------------------------------------
203+
# Interactive wizard
204+
# ---------------------------------------------------------------------------
205+
206+
def run_wizard(console):
207+
"""Ask y/n for each supported language. Returns list of selected lang keys."""
208+
console.print()
209+
console.print(
210+
"[bold cyan]Select the node types to include[/bold cyan] "
211+
"[dim](Enter = yes)[/dim]"
212+
)
213+
console.print()
214+
215+
selected = []
216+
for key, info in LANGUAGE_NODES.items():
217+
raw = console.input(
218+
f" Include [bold]{info['label']}[/bold] node? [Y/n] "
219+
).strip().lower()
220+
if raw in ("", "y", "yes"):
221+
selected.append(key)
222+
223+
return selected
224+
225+
226+
# ---------------------------------------------------------------------------
227+
# GraphML builder
228+
# ---------------------------------------------------------------------------
229+
230+
def _build_graphml(project_name, selected_langs):
231+
"""Return a GraphML string with one unconnected node per selected language."""
232+
node_blocks = []
233+
for idx, lang_key in enumerate(selected_langs, start=1):
234+
info = LANGUAGE_NODES[lang_key]
235+
node_blocks.append(
236+
GRAPHML_NODE.format(
237+
idx=idx,
238+
y=100 + (idx - 1) * 100, # stack vertically, 100 px apart
239+
color=info["color"],
240+
filename=info["filename"],
241+
)
242+
)
243+
return GRAPHML_HEADER.format(
244+
project_name=project_name,
245+
nodes="\n".join(node_blocks),
246+
)
247+
248+
249+
# ---------------------------------------------------------------------------
250+
# Public entry points
251+
# ---------------------------------------------------------------------------
252+
253+
def init_project_interactive(name, selected_langs, console):
254+
"""Create a project with one node per selected language (no edges)."""
255+
project_path = Path(name)
256+
257+
if project_path.exists():
258+
raise FileExistsError(f"Directory '{name}' already exists")
259+
260+
if not selected_langs:
261+
console.print("[yellow]No languages selected — nothing to create.[/yellow]")
262+
return
263+
264+
console.print()
265+
console.print(f"[cyan]Creating project:[/cyan] {name}")
266+
267+
project_path.mkdir()
268+
src_path = project_path / "src"
269+
src_path.mkdir()
270+
271+
# workflow.graphml
272+
workflow_file = project_path / "workflow.graphml"
273+
workflow_file.write_text(_build_graphml(name, selected_langs))
274+
275+
# one source stub per selected language
276+
for lang_key in selected_langs:
277+
info = LANGUAGE_NODES[lang_key]
278+
(src_path / info["filename"]).write_text(info["stub"])
279+
280+
# README
281+
(project_path / "README.md").write_text(
282+
README_TEMPLATE.format(project_name=name)
283+
)
284+
285+
# Metadata
286+
metadata_info = ""
287+
try:
288+
metadata_path = write_study_metadata(
289+
project_path,
290+
generated_by="concore init --interactive",
291+
workflow_file=workflow_file,
292+
)
293+
metadata_info = f"Metadata:\n {metadata_path.name}\n\n"
294+
except Exception as exc:
295+
console.print(
296+
f"[yellow]Warning:[/yellow] Failed to write study metadata: {exc}"
297+
)
298+
299+
node_lines = "\n".join(
300+
f" N{i}: {LANGUAGE_NODES[k]['filename']}"
301+
for i, k in enumerate(selected_langs, 1)
302+
)
303+
304+
console.print()
305+
console.print(
306+
Panel.fit(
307+
f"[green]✓[/green] Project created with {len(selected_langs)} node(s)!\n\n"
308+
f"{metadata_info}"
309+
f"Nodes (unconnected — connect them in yEd):\n{node_lines}\n\n"
310+
f"Next steps:\n"
311+
f" cd {name}\n"
312+
f" concore validate workflow.graphml\n"
313+
f" concore run workflow.graphml",
314+
title="Success",
315+
border_style="green",
316+
)
317+
)
318+
319+
69320
def init_project(name, template, console):
321+
"""Non-interactive init — single Python node skeleton."""
70322
project_path = Path(name)
71323

72324
if project_path.exists():
@@ -81,13 +333,13 @@ def init_project(name, template, console):
81333
with open(workflow_file, "w") as f:
82334
f.write(SAMPLE_GRAPHML)
83335

84-
sample_script = project_path / "src" / "script.py"
85-
with open(sample_script, "w") as f:
86-
f.write(SAMPLE_PYTHON)
336+
(project_path / "src" / "script.py").write_text(
337+
LANGUAGE_NODES["python"]["stub"]
338+
)
87339

88-
readme_file = project_path / "README.md"
89-
with open(readme_file, "w") as f:
90-
f.write(README_TEMPLATE.format(project_name=name))
340+
(project_path / "README.md").write_text(
341+
README_TEMPLATE.format(project_name=name)
342+
)
91343

92344
metadata_info = ""
93345
try:

0 commit comments

Comments
 (0)