gelang 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +535 -0
  4. package/bin/ge.js +112 -0
  5. package/package.json +62 -0
  6. package/python/pyeffic/__init__.py +9 -0
  7. package/python/pyeffic/__main__.py +6 -0
  8. package/python/pyeffic/analyzer.py +464 -0
  9. package/python/pyeffic/apisurface.py +238 -0
  10. package/python/pyeffic/autoselect.py +327 -0
  11. package/python/pyeffic/backends.py +87 -0
  12. package/python/pyeffic/bench.py +233 -0
  13. package/python/pyeffic/cli.py +184 -0
  14. package/python/pyeffic/compiler.py +421 -0
  15. package/python/pyeffic/config.py +383 -0
  16. package/python/pyeffic/dartgen.py +441 -0
  17. package/python/pyeffic/deploy.py +586 -0
  18. package/python/pyeffic/diagnostics.py +194 -0
  19. package/python/pyeffic/difftest.py +424 -0
  20. package/python/pyeffic/downloader.py +307 -0
  21. package/python/pyeffic/emitters/__init__.py +11 -0
  22. package/python/pyeffic/emitters/base.py +2359 -0
  23. package/python/pyeffic/emitters/cpp.py +266 -0
  24. package/python/pyeffic/emitters/csharp.py +342 -0
  25. package/python/pyeffic/emitters/dart.py +349 -0
  26. package/python/pyeffic/emitters/go.py +388 -0
  27. package/python/pyeffic/emitters/kotlin.py +314 -0
  28. package/python/pyeffic/emitters/rust.py +314 -0
  29. package/python/pyeffic/emitters/zig.py +411 -0
  30. package/python/pyeffic/ffi.py +49 -0
  31. package/python/pyeffic/frontends/__init__.py +94 -0
  32. package/python/pyeffic/frontends/hybrid.py +709 -0
  33. package/python/pyeffic/frontends/typescript.py +965 -0
  34. package/python/pyeffic/ge_cli.py +1148 -0
  35. package/python/pyeffic/golden.py +348 -0
  36. package/python/pyeffic/idents.py +206 -0
  37. package/python/pyeffic/modules.py +220 -0
  38. package/python/pyeffic/packer.py +222 -0
  39. package/python/pyeffic/pipeline.py +797 -0
  40. package/python/pyeffic/reactgen.py +966 -0
  41. package/python/pyeffic/researcher.py +177 -0
  42. package/python/pyeffic/scaffold.py +397 -0
  43. package/python/pyeffic/stdlib.py +246 -0
  44. package/python/pyeffic/styling.py +220 -0
  45. package/python/pyeffic/templates/desktop_gui/README.md +106 -0
  46. package/python/pyeffic/templates/desktop_gui/app/__init__.py +0 -0
  47. package/python/pyeffic/templates/desktop_gui/app/core/__init__.py +0 -0
  48. package/python/pyeffic/templates/desktop_gui/app/core/add.ge.py +13 -0
  49. package/python/pyeffic/templates/desktop_gui/app/core/factorial.ge.py +20 -0
  50. package/python/pyeffic/templates/desktop_gui/app/core/fibonacci.ge.py +25 -0
  51. package/python/pyeffic/templates/desktop_gui/app/core/gcd.ge.py +19 -0
  52. package/python/pyeffic/templates/desktop_gui/app/core/is_prime.ge.py +24 -0
  53. package/python/pyeffic/templates/desktop_gui/app/core/multiply.ge.py +13 -0
  54. package/python/pyeffic/templates/desktop_gui/app/core/power.ge.py +25 -0
  55. package/python/pyeffic/templates/desktop_gui/app/main.ge.py +49 -0
  56. package/python/pyeffic/templates/desktop_gui/app/memory/__init__.py +0 -0
  57. package/python/pyeffic/templates/desktop_gui/app/memory/buffer.ge.py +26 -0
  58. package/python/pyeffic/templates/desktop_gui/app/memory/limits.ge.py +47 -0
  59. package/python/pyeffic/templates/desktop_gui/app/memory/state.ge.py +44 -0
  60. package/python/pyeffic/templates/desktop_gui/app/ui/__init__.py +0 -0
  61. package/python/pyeffic/templates/desktop_gui/app/ui/layout.ge.py +64 -0
  62. package/python/pyeffic/templates/desktop_gui/app/ui/render.ge.py +87 -0
  63. package/python/pyeffic/templates/desktop_gui/app/ui/theme.ge.py +147 -0
  64. package/python/pyeffic/templates/desktop_gui/app/ui/widgets.ge.py +105 -0
  65. package/python/pyeffic/templates/desktop_gui/desktop/__init__.py +1 -0
  66. package/python/pyeffic/templates/desktop_gui/desktop/main.ge.py +258 -0
  67. package/python/pyeffic/templates/desktop_gui/ge.toml +16 -0
  68. package/python/pyeffic/templates/desktop_gui/tests/__init__.py +0 -0
  69. package/python/pyeffic/templates/desktop_gui/tests/ge_loader.py +76 -0
  70. package/python/pyeffic/templates/desktop_gui/tests/test_app.py +173 -0
  71. package/python/pyeffic/templates/web_react/README.md +115 -0
  72. package/python/pyeffic/templates/web_react/app/__init__.py +0 -0
  73. package/python/pyeffic/templates/web_react/app/core/__init__.py +0 -0
  74. package/python/pyeffic/templates/web_react/app/core/add.ge.py +9 -0
  75. package/python/pyeffic/templates/web_react/app/core/factorial.ge.py +16 -0
  76. package/python/pyeffic/templates/web_react/app/core/fibonacci.ge.py +21 -0
  77. package/python/pyeffic/templates/web_react/app/core/is_prime.ge.py +20 -0
  78. package/python/pyeffic/templates/web_react/app/core/multiply.ge.py +9 -0
  79. package/python/pyeffic/templates/web_react/app/main.ge.py +25 -0
  80. package/python/pyeffic/templates/web_react/app/memory/__init__.py +0 -0
  81. package/python/pyeffic/templates/web_react/app/memory/buffer.ge.py +25 -0
  82. package/python/pyeffic/templates/web_react/app/memory/limits.ge.py +51 -0
  83. package/python/pyeffic/templates/web_react/ge.toml +23 -0
  84. package/python/pyeffic/templates/web_react/tests/__init__.py +0 -0
  85. package/python/pyeffic/templates/web_react/tests/ge_loader.py +68 -0
  86. package/python/pyeffic/templates/web_react/tests/test_app.py +105 -0
  87. package/python/pyeffic/templates/web_react/ui/main.ge.ui +33 -0
  88. package/python/pyeffic/templates/web_react/web/__init__.py +0 -0
  89. package/python/pyeffic/templates/web_react/web/server.ge.py +78 -0
  90. package/python/pyeffic/ts2py.py +657 -0
  91. package/python/pyeffic/typecheck.py +232 -0
  92. package/python/pyeffic/ui.py +154 -0
  93. package/python/pyeffic/ui_dsl.py +618 -0
  94. package/python/pyeffic/widgets.py +87 -0
  95. package/scripts/README.md +42 -0
  96. package/scripts/check-toolchains.py +85 -0
@@ -0,0 +1,177 @@
1
+ """Backend selection: web research + static heuristics.
2
+
3
+ For each function the researcher builds a query describing the workload, runs a
4
+ real web search (DuckDuckGo HTML endpoint, no API key required), and scores
5
+ C++ vs Rust from the snippets. Static heuristics are always computed as a
6
+ reliable backbone; web results nudge the score. If the network is unavailable
7
+ or `do_research` is off, heuristics alone decide.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import urllib.parse
13
+ import urllib.request
14
+ from dataclasses import dataclass
15
+
16
+ from .analyzer import FuncUnit
17
+
18
+ # Static heuristic weights. Positive => favors Rust, negative => favors C++.
19
+ HEURISTICS: dict[str, int] = {
20
+ "numeric_loop": +2, # Rust bounds-checked SIMD-friendly loops
21
+ "arithmetic": +1,
22
+ "indexing": +1, # Rust safe indexing
23
+ "list_alloc": +1, # Vec is ergonomic
24
+ "list_append": +1,
25
+ "container_iter": 0,
26
+ "math_builtin": 0,
27
+ "len_builtin": 0,
28
+ "comparison": 0,
29
+ "while_loop": 0,
30
+ "io_print": -1, # C++ iostream often lighter for trivial IO
31
+ "class_use": -2, # C++ OOP ergonomics
32
+ "plain": 0,
33
+ }
34
+
35
+ RUST_POS = re.compile(r"\b(rust)\b", re.I)
36
+ CPP_POS = re.compile(r"\b(c\+\+|cpp|std::)\b", re.I)
37
+ RUST_PERF = re.compile(r"rust.{0,40}(faster|safer|zero.cost|simd|bounds)", re.I)
38
+ CPP_PERF = re.compile(r"(c\+\+|cpp).{0,40}(faster|stl|template|simd|legacy)", re.I)
39
+
40
+
41
+ @dataclass
42
+ class Decision:
43
+ backend: str # "rust" | "cpp"
44
+ rust_score: float
45
+ cpp_score: float
46
+ reasons: list[str]
47
+ web_used: bool
48
+
49
+
50
+ def _query_for(unit: FuncUnit) -> str:
51
+ feats = sorted(unit.features)
52
+ keywords = {
53
+ "numeric_loop": "numeric loop SIMD",
54
+ "arithmetic": "arithmetic kernel",
55
+ "indexing": "array indexing",
56
+ "list_alloc": "dynamic array allocation",
57
+ "list_append": "vector append",
58
+ "container_iter": "container iteration",
59
+ "math_builtin": "math builtins",
60
+ "io_print": "console IO",
61
+ "class_use": "object oriented class",
62
+ "comparison": "comparisons",
63
+ "while_loop": "while loop",
64
+ "plain": "plain function",
65
+ }
66
+ terms = [keywords.get(f, f) for f in feats[:3]]
67
+ return "rust vs c++ performance " + " ".join(terms)
68
+
69
+
70
+ def _ddg_search(query: str, timeout: float) -> list[str]:
71
+ """Fetch DuckDuckGo lite results and return snippet strings.
72
+
73
+ Uses the lite endpoint (plain HTML, no JS) with a POST request, which is
74
+ the most scraping-friendly surface DuckDuckGo offers.
75
+ """
76
+ data = urllib.parse.urlencode({"q": query, "kl": "us-en"}).encode()
77
+ req = urllib.request.Request(
78
+ "https://lite.duckduckgo.com/lite/",
79
+ data=data,
80
+ headers={
81
+ "User-Agent": "Mozilla/5.0 (pyeffic/0.1)",
82
+ "Content-Type": "application/x-www-form-urlencoded",
83
+ },
84
+ )
85
+ try:
86
+ with urllib.request.urlopen(req, timeout=timeout) as r:
87
+ html = r.read().decode("utf-8", errors="ignore")
88
+ except Exception:
89
+ return []
90
+ # snippets live in <td class='result-snippet'>...</td>
91
+ snippets = re.findall(r"<td class='result-snippet'>(.*?)</td>", html, re.S)
92
+ clean = [re.sub(r"<[^>]+>", " ", s) for s in snippets]
93
+ return [s.strip() for s in clean if s.strip()][:8]
94
+
95
+
96
+ def decide(unit: FuncUnit, do_research: bool, timeout: float, force: str | None) -> Decision:
97
+ if force in ("rust", "cpp", "csharp", "zig", "go", "kotlin"):
98
+ return Decision(backend=force, rust_score=0, cpp_score=0,
99
+ reasons=[f"forced backend={force}"], web_used=False)
100
+
101
+ # Use the new auto-selector for intelligent multi-backend selection
102
+ from .autoselect import select_backend
103
+ backend, reasons = select_backend(unit)
104
+
105
+ web_used = False
106
+ if do_research:
107
+ q = _query_for(unit)
108
+ snippets = _ddg_search(q, timeout)
109
+ if snippets:
110
+ web_used = True
111
+ # web research can adjust the selection
112
+ r_hits = sum(1 for s in snippets if RUST_POS.search(s))
113
+ c_hits = sum(1 for s in snippets if CPP_POS.search(s))
114
+ r_perf = sum(1 for s in snippets if RUST_PERF.search(s))
115
+ c_perf = sum(1 for s in snippets if CPP_PERF.search(s))
116
+ reasons.append(f"web: rust_hits={r_hits} perf={r_perf}, cpp_hits={c_hits} perf={c_perf} (query='{q}')")
117
+ # if web research strongly favors C++ over Rust, override
118
+ if c_hits + c_perf > r_hits + r_perf + 2 and backend == "rust":
119
+ backend = "cpp"
120
+ reasons.append(f"web override: cpp favored -> {backend}")
121
+ else:
122
+ reasons.append("web: no results, auto-select only")
123
+
124
+ return Decision(backend=backend, rust_score=0, cpp_score=0, reasons=reasons, web_used=web_used)
125
+
126
+
127
+ def decide_program(units: list[FuncUnit], do_research: bool, timeout: float,
128
+ force: str | None) -> tuple[Decision, list[Decision]]:
129
+ """Pick ONE backend for the whole program (so the call graph stays intact).
130
+
131
+ Returns (program_decision, per_function_decisions_for_reporting).
132
+ Uses the auto-selector to pick the best backend for the whole program
133
+ based on aggregated function characteristics.
134
+ """
135
+ per_fn: list[Decision] = []
136
+ any_web = False
137
+ all_reasons: list[str] = []
138
+ # aggregate backend scores across all functions
139
+ from .autoselect import score_backend, DEFAULT_PREFERENCE
140
+ agg_scores = {b: 0.0 for b in DEFAULT_PREFERENCE}
141
+
142
+ for u in units:
143
+ if not u.supported:
144
+ per_fn.append(Decision("cpython", 0, 0, ["unsupported -> cpython fallback"], False))
145
+ continue
146
+ d = decide(u, do_research, timeout, force)
147
+ per_fn.append(d)
148
+ any_web = any_web or d.web_used
149
+ all_reasons.append(f"[{u.name}] " + "; ".join(d.reasons))
150
+ # aggregate scores from auto-selector
151
+ if not force:
152
+ scores = score_backend(u)
153
+ for b in DEFAULT_PREFERENCE:
154
+ agg_scores[b] += scores[b].score
155
+
156
+ if force in ("rust", "cpp", "csharp", "zig", "go", "kotlin"):
157
+ prog = Decision(force, 0, 0, [f"forced backend={force}"], False)
158
+ else:
159
+ # pick the backend with the highest aggregate score
160
+ best_backend = "rust"
161
+ best_score = 0.0
162
+ for b in DEFAULT_PREFERENCE:
163
+ if agg_scores[b] > best_score:
164
+ best_score = agg_scores[b]
165
+ best_backend = b
166
+ if best_score <= 0:
167
+ best_backend = "rust" # default tie-break
168
+ score_summary = ", ".join(f"{b}={agg_scores[b]:.1f}" for b in DEFAULT_PREFERENCE
169
+ if agg_scores[b] != 0)
170
+ prog = Decision(
171
+ backend=best_backend,
172
+ rust_score=agg_scores.get("rust", 0),
173
+ cpp_score=agg_scores.get("cpp", 0),
174
+ reasons=[f"auto-select aggregate: {score_summary} -> {best_backend}"] + all_reasons,
175
+ web_used=any_web,
176
+ )
177
+ return prog, per_fn
@@ -0,0 +1,397 @@
1
+ """GE project scaffolding — `ge create` command.
2
+
3
+ Generates a properly structured GE project with:
4
+ app/ — main application entry (shared logic)
5
+ desktop/ — desktop platform wrapper (Windows/Linux/macOS)
6
+ web/ — web platform wrapper
7
+ mobile/ — mobile platform wrapper (Android/iOS)
8
+ backend/ — native backend source (Rust/C++/C#/Zig/Go/Kotlin)
9
+ ui/ — .ge.ui declarative UI definitions
10
+ tests/ — test scripts
11
+
12
+ The user is prompted for:
13
+ 1. App name
14
+ 2. Platforms (desktop/web/mobile/all) — default: all
15
+ 3. Backend language (all/rust/cpp/csharp/zig/go/kotlin) — default: all
16
+ 4. UI backend (all/dart) — default: all (dart only for now)
17
+
18
+ Non-interactive mode is supported via CLI flags.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ from pathlib import Path
24
+
25
+ ALL_BACKENDS = ["rust", "cpp", "csharp", "zig", "go", "kotlin"]
26
+ ALL_PLATFORMS = ["desktop", "web", "mobile"]
27
+ #: Template name -> directory under pyeffic/templates/
28
+ TEMPLATE_DIRS = {
29
+ "desktop-gui": "desktop_gui",
30
+ "web-react": "web_react",
31
+ }
32
+ ALL_TEMPLATES = ["default"] + list(TEMPLATE_DIRS)
33
+
34
+
35
+ def _sanitize_name(name: str) -> str:
36
+ """Convert app name to a valid package name (lowercase, underscores)."""
37
+ s = re.sub(r"[^a-zA-Z0-9_]", "_", name.strip())
38
+ if s and s[0].isdigit():
39
+ s = "_" + s
40
+ return s.lower() or "ge_app"
41
+
42
+
43
+ def _prompt(message: str, default: str, choices: list[str] | None = None) -> str:
44
+ """Interactive prompt with default and optional choices."""
45
+ hint = f" [{default}]" if default else ""
46
+ choice_hint = ""
47
+ if choices:
48
+ choice_hint = f" ({'/'.join(choices)})"
49
+ while True:
50
+ val = input(f"{message}{choice_hint}{hint}: ").strip()
51
+ if not val:
52
+ return default
53
+ if choices and val not in choices:
54
+ print(f" Invalid choice. Options: {', '.join(choices)}")
55
+ continue
56
+ return val
57
+
58
+
59
+ def _prompt_multi(message: str, default: str, choices: list[str]) -> list[str]:
60
+ """Prompt for comma-separated multi-select. 'all' selects everything."""
61
+ while True:
62
+ val = input(f"{message} (choices: {', '.join(choices)}, or 'all') [{default}]: ").strip()
63
+ if not val:
64
+ val = default
65
+ if val == "all":
66
+ return choices[:]
67
+ parts = [p.strip() for p in val.split(",") if p.strip()]
68
+ invalid = [p for p in parts if p not in choices]
69
+ if invalid:
70
+ print(f" Invalid: {', '.join(invalid)}. Options: {', '.join(choices)}")
71
+ continue
72
+ return parts
73
+
74
+
75
+ # Template files
76
+
77
+ _APP_MAIN_TEMPLATE = '''"""GE application — {app_name}
78
+
79
+ This is the main application logic. Platform-specific code (desktop/web/mobile)
80
+ imports from here. Write your core GE functions in this file.
81
+ """
82
+ from __future__ import annotations
83
+
84
+
85
+ def greet(name: str) -> str:
86
+ return f"Hello, {{name}}! Welcome to {app_name}."
87
+
88
+
89
+ def main() -> None:
90
+ print(greet("World"))
91
+ '''
92
+
93
+ _DESKTOP_MAIN_TEMPLATE = '''"""Desktop entry point for {app_name}.
94
+
95
+ Builds the desktop application using the shared app logic.
96
+ Run with: ge build desktop/main.ge.py --run
97
+ """
98
+ from __future__ import annotations
99
+ from app.main import greet, main
100
+
101
+
102
+ def desktop_main() -> None:
103
+ print("=== {app_name} Desktop ===")
104
+ main()
105
+ '''
106
+
107
+ _WEB_MAIN_TEMPLATE = '''"""Web entry point for {app_name}.
108
+
109
+ Builds the web application using the shared app logic.
110
+ Run with: ge build web/main.ge.py
111
+ """
112
+ from __future__ import annotations
113
+ from app.main import greet, main
114
+
115
+
116
+ def web_main() -> None:
117
+ print("=== {app_name} Web ===")
118
+ main()
119
+ '''
120
+
121
+ _MOBILE_MAIN_TEMPLATE = '''"""Mobile entry point for {app_name}.
122
+
123
+ Builds the mobile (Android/iOS) application using the shared app logic.
124
+ Run with: ge flutter mobile/main.ge.py --app-name {app_name}
125
+ """
126
+ from __future__ import annotations
127
+ from app.main import greet, main
128
+
129
+
130
+ def mobile_main() -> None:
131
+ print("=== {app_name} Mobile ===")
132
+ main()
133
+ '''
134
+
135
+ _UI_TEMPLATE = '''"""GE UI definition for {app_name}.
136
+
137
+ Define your UI declaratively using the .ge.ui DSL.
138
+ This file is parsed by the GE UI parser and generates Flutter widgets.
139
+ """
140
+ Window {{
141
+ title: "{app_name}"
142
+ Column {{
143
+ Text {{ text: "Welcome to {app_name}", style: "headline" }}
144
+ Text {{ text: "Built with GE", style: "body" }}
145
+ }}
146
+ }}
147
+ '''
148
+
149
+ _BACKEND_TEMPLATE = '''"""GE backend functions for {app_name}.
150
+
151
+ These functions are compiled to native code via the selected backend(s).
152
+ Use @rust, @cpp, @csharp, @zig, @go, or @kotlin decorators to force a backend.
153
+ Without a decorator, GE auto-selects the best backend.
154
+ """
155
+ from __future__ import annotations
156
+ from pyeffic.backends import rust, cpp, csharp, zig, go, kotlin
157
+
158
+
159
+ def compute_sum(a: int, b: int) -> int:
160
+ return a + b
161
+
162
+
163
+ def compute_product(a: int, b: int) -> int:
164
+ return a * b
165
+ '''
166
+
167
+ _TEST_TEMPLATE = '''"""Tests for {app_name}.
168
+
169
+ Run with: python -m unittest discover tests
170
+ """
171
+ import unittest
172
+ import sys
173
+ from pathlib import Path
174
+
175
+ # Add project root to path
176
+ sys.path.insert(0, str(Path(__file__).parent.parent))
177
+
178
+
179
+ class TestAppFunctions(unittest.TestCase):
180
+ def test_greet(self):
181
+ from app.main import greet
182
+ self.assertEqual(greet("World"), "Hello, World! Welcome to {app_name}.")
183
+
184
+ def test_compute_sum(self):
185
+ from backend.main import compute_sum
186
+ self.assertEqual(compute_sum(3, 4), 7)
187
+
188
+ def test_compute_product(self):
189
+ from backend.main import compute_product
190
+ self.assertEqual(compute_product(3, 4), 12)
191
+
192
+
193
+ if __name__ == "__main__":
194
+ unittest.main()
195
+ '''
196
+
197
+ _GE_CONFIG_TEMPLATE = '''# GE project configuration for {app_name}
198
+ [project]
199
+ name = "{app_name}"
200
+ version = "0.1.0"
201
+ description = "A GE application"
202
+
203
+ [build]
204
+ # Default backend for all functions (auto selects best per function)
205
+ backend = "auto"
206
+ # Platforms to build for
207
+ platforms = [{platforms}]
208
+
209
+ [backends]
210
+ # Which backends to generate code for (default: all)
211
+ languages = [{backends}]
212
+
213
+ [ui]
214
+ # UI backend (dart for Flutter)
215
+ backend = "dart"
216
+ '''
217
+
218
+ _README_TEMPLATE = '''# {app_name}
219
+
220
+ A GE application built with the [GE programming language](https://github.com/user/ge).
221
+
222
+ ## Structure
223
+
224
+ ```
225
+ {app_name}/
226
+ app/ — main application logic (shared)
227
+ desktop/ — desktop platform entry
228
+ web/ — web platform entry
229
+ mobile/ — mobile platform entry
230
+ backend/ — native backend functions
231
+ ui/ — .ge.ui declarative UI definitions
232
+ tests/ — test scripts
233
+ ge.toml — GE project config
234
+ ```
235
+
236
+ ## Build
237
+
238
+ ```bash
239
+ # Build for desktop
240
+ ge build desktop/main.ge.py --run
241
+
242
+ # Build for web
243
+ ge build web/main.ge.py
244
+
245
+ # Build Flutter app (mobile/desktop)
246
+ ge flutter mobile/main.ge.py --app-name {app_name}
247
+
248
+ # Build all backends
249
+ ge build app/main.ge.py --backend auto
250
+ ```
251
+
252
+ ## Test
253
+
254
+ ```bash
255
+ python -m unittest discover tests
256
+ ```
257
+ '''
258
+
259
+
260
+ def create_project(
261
+ name: str,
262
+ out_dir: Path | str = ".",
263
+ platforms: list[str] | None = None,
264
+ backends: list[str] | None = None,
265
+ interactive: bool = True,
266
+ template: str = "default",
267
+ ) -> Path:
268
+ """Create a new GE project with proper structure.
269
+
270
+ Args:
271
+ name: app name
272
+ out_dir: output directory
273
+ platforms: list of platforms (desktop/web/mobile), or None for all
274
+ backends: list of backend languages, or None for all
275
+ interactive: if True, prompt for missing values
276
+ template: "default" or "desktop-gui"
277
+
278
+ Returns:
279
+ Path to the created project directory.
280
+ """
281
+ if interactive:
282
+ name = _prompt("App name", _sanitize_name(name))
283
+ if template == "default":
284
+ template = _prompt("Template", "default", ALL_TEMPLATES)
285
+ if platforms is None:
286
+ platforms = _prompt_multi("Platforms", "all", ALL_PLATFORMS)
287
+ if backends is None:
288
+ backends = _prompt_multi("Backend languages", "all", ALL_BACKENDS)
289
+ else:
290
+ name = _sanitize_name(name)
291
+ if platforms is None:
292
+ platforms = ALL_PLATFORMS[:]
293
+ if backends is None:
294
+ backends = ALL_BACKENDS[:]
295
+
296
+ if template in TEMPLATE_DIRS:
297
+ return _create_template_project(template, name, out_dir)
298
+
299
+ out = Path(out_dir) / name
300
+ if out.exists():
301
+ raise FileExistsError(f"Directory already exists: {out}")
302
+
303
+ # Create directory structure
304
+ dirs = ["app", "backend", "ui", "tests"]
305
+ for p in platforms:
306
+ dirs.append(p)
307
+ for d in dirs:
308
+ (out / d).mkdir(parents=True, exist_ok=True)
309
+
310
+ # Write files
311
+ (out / "app" / "main.ge.py").write_text(
312
+ _APP_MAIN_TEMPLATE.format(app_name=name), encoding="utf-8")
313
+ (out / "app" / "__init__.py").write_text("", encoding="utf-8")
314
+
315
+ if "desktop" in platforms:
316
+ (out / "desktop" / "main.ge.py").write_text(
317
+ _DESKTOP_MAIN_TEMPLATE.format(app_name=name), encoding="utf-8")
318
+ if "web" in platforms:
319
+ (out / "web" / "main.ge.py").write_text(
320
+ _WEB_MAIN_TEMPLATE.format(app_name=name), encoding="utf-8")
321
+ if "mobile" in platforms:
322
+ (out / "mobile" / "main.ge.py").write_text(
323
+ _MOBILE_MAIN_TEMPLATE.format(app_name=name), encoding="utf-8")
324
+
325
+ (out / "backend" / "main.ge.py").write_text(
326
+ _BACKEND_TEMPLATE.format(app_name=name), encoding="utf-8")
327
+ (out / "backend" / "__init__.py").write_text("", encoding="utf-8")
328
+
329
+ (out / "ui" / "main.ge.ui").write_text(
330
+ _UI_TEMPLATE.format(app_name=name), encoding="utf-8")
331
+
332
+ (out / "tests" / "test_app.py").write_text(
333
+ _TEST_TEMPLATE.format(app_name=name), encoding="utf-8")
334
+ (out / "tests" / "__init__.py").write_text("", encoding="utf-8")
335
+
336
+ platforms_str = ", ".join(f'"{p}"' for p in platforms)
337
+ backends_str = ", ".join(f'"{b}"' for b in backends)
338
+ (out / "ge.toml").write_text(
339
+ _GE_CONFIG_TEMPLATE.format(app_name=name, platforms=platforms_str, backends=backends_str),
340
+ encoding="utf-8")
341
+
342
+ (out / "README.md").write_text(
343
+ _README_TEMPLATE.format(app_name=name), encoding="utf-8")
344
+
345
+ return out
346
+
347
+
348
+ def create_project_noninteractive(
349
+ name: str,
350
+ out_dir: Path | str = ".",
351
+ platforms: list[str] | None = None,
352
+ backends: list[str] | None = None,
353
+ template: str = "default",
354
+ ) -> Path:
355
+ """Create a project without any prompts."""
356
+ return create_project(name, out_dir, platforms, backends, interactive=False,
357
+ template=template)
358
+
359
+
360
+ def _create_template_project(template: str, name: str,
361
+ out_dir: Path | str = ".") -> Path:
362
+ """Create a project from a directory under pyeffic/templates/.
363
+
364
+ Each template is a real file tree (not inline strings) so it stays
365
+ readable, diffable, and independently editable. Every occurrence of
366
+ `{app_name}` in a template file is replaced with the project name.
367
+
368
+ Registered templates:
369
+ desktop-gui Rust shell + C++ core + C++ UI (native Win32)
370
+ web-react React 19 + TypeScript SPA + Rust backend
371
+ """
372
+ dirname = TEMPLATE_DIRS.get(template)
373
+ if dirname is None:
374
+ raise ValueError(f"unknown template: {template}")
375
+ template_root = Path(__file__).parent / "templates" / dirname
376
+ if not template_root.is_dir():
377
+ raise FileNotFoundError(f"template directory missing: {template_root}")
378
+
379
+ out = Path(out_dir) / name
380
+ if out.exists():
381
+ raise FileExistsError(f"Directory already exists: {out}")
382
+
383
+ for src in sorted(template_root.rglob("*")):
384
+ if src.is_dir():
385
+ continue
386
+ rel = src.relative_to(template_root)
387
+ dst = out / rel
388
+ dst.parent.mkdir(parents=True, exist_ok=True)
389
+ content = src.read_text(encoding="utf-8")
390
+ dst.write_text(content.replace("{app_name}", name), encoding="utf-8")
391
+
392
+ return out
393
+
394
+
395
+ def _create_desktop_gui_project(name: str, out_dir: Path | str = ".") -> Path:
396
+ """Backwards-compatible alias for the desktop-gui template."""
397
+ return _create_template_project("desktop-gui", name, out_dir)