FluxScript 0.1.0__py3-none-any.whl

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.

Potentially problematic release.


This version of FluxScript might be problematic. Click here for more details.

fluxscript/cli.py ADDED
@@ -0,0 +1,303 @@
1
+ """FluxScript command line interface."""
2
+ from __future__ import annotations
3
+ import argparse
4
+ import json
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from . import __version__
12
+ from .backends import BACKENDS
13
+ from .dependencies import ensure_target_dependencies
14
+ from .analyzer import analyze_path, write_report
15
+ from .integrations import status as integration_status
16
+ from .integrations.execution_service import ExecutionServiceClient, ExecutionServiceError
17
+ from .parser import parse_file, SourceError
18
+ from .project import default_storage, generate_project
19
+
20
+ EXAMPLE = '''"""Describe an application using FluxScript's safe declarative Python API."""
21
+ from fluxscript import App, Page, Heading, Text, Button, Input
22
+
23
+ app = App("Research Manager")
24
+ home = Page("/", title="Research Manager")
25
+ home.add(Heading("Welcome to Research Manager"))
26
+ home.add(Text("Describe your interface once and generate a project for a target platform."))
27
+ home.add(Input("Project name", name="project_name"))
28
+ home.add(Button("Save project", action="save_project"))
29
+ app.add_page(home)
30
+ '''
31
+
32
+ GUIDE = r'''FluxScript 0.1.0 — COMMAND GUIDE
33
+ ================================
34
+
35
+ GET STARTED
36
+ python -m pip install . (run from the extracted source directory)
37
+ # After PyPI publication: python -m pip install --upgrade FluxScript
38
+ fluxscript --version
39
+ fluxscript new my_app
40
+ cd my_app
41
+ fluxscript check app.py
42
+
43
+ BUILDERS (builder commands install the latest required Python validation helper)
44
+ fluxscript website app.py (clean alias; website-builder also works)
45
+ fluxscript website-builder app.py --out build/site
46
+ fluxscript android app.py (clean alias; android-builder also works)
47
+ fluxscript ios app.py (clean alias; ios-builder also works)
48
+ fluxscript cli app.py (clean alias; cli-builder also works)
49
+ fluxscript build app.py --target all --out build/all
50
+ Add --no-install-deps to prevent automatic pip installation.
51
+
52
+ GENERATE PROJECT (creates a directory AND ZIP in storage)
53
+ fluxscript generateproject --source app.py --target web (generate-project also works)
54
+ fluxscript generate-project --source app.py --target android --storage ~/FluxScriptProjects
55
+ fluxscript generate-project --source app.py --target all
56
+ Default storage: /storage/emulated/0/FluxScriptProjects when writable, otherwise
57
+ ~/FluxScriptProjects. Both the generated directory and a ZIP archive are saved there.
58
+
59
+ PROJECT UTILITIES
60
+ fluxscript new [directory] Create a starter source project
61
+ fluxscript check [source.py] Validate source without executing it
62
+ fluxscript targets List supported source generators
63
+ fluxscript doctor Detect local native/toolchain executables
64
+ fluxscript device Detect current runtime device/OS type
65
+ fluxscript developer Show project developer name
66
+ fluxscript integrations Show built-in capabilities and optional toolchains
67
+ fluxscript analyze ./source Inspect source files without executing them
68
+ fluxscript analyze ./source --out report.json
69
+ fluxscript review ./source Alias for the read-only analyzer
70
+ fluxscript guide Print this full command guide
71
+
72
+ REMOTE EXECUTION SERVICE
73
+ fluxscript service status
74
+ export FLUXSCRIPT_SERVICE_URL='http://127.0.0.1:2000/api/v2'
75
+ export FLUXSCRIPT_SERVICE_TOKEN='your-token-if-required'
76
+ fluxscript service runtimes
77
+ fluxscript service run python 3.12 main.py
78
+ fluxscript service run javascript 20.11.1 app.js --stdin-file input.txt
79
+ Remote non-local endpoints must use HTTPS. The service must be separately deployed;
80
+ FluxScript never silently executes submitted code on your own machine.
81
+
82
+ NOTES
83
+ The source format is a restricted declarative Python subset, not arbitrary Python.
84
+ Android compilation needs a JDK, Android SDK and compatible Gradle environment.
85
+ iOS compilation/signing needs macOS and Xcode. pip cannot install those toolchains.
86
+ Generated buttons emit action requests; connect real business logic and APIs yourself.
87
+ '''
88
+
89
+ def _new(path: Path) -> int:
90
+ path = path.expanduser()
91
+ if path.exists() and not path.is_dir():
92
+ print(f"error: not a directory: {path}", file=sys.stderr); return 2
93
+ path.mkdir(parents=True, exist_ok=True)
94
+ source = path / "app.py"
95
+ if source.exists(): print(f"error: refusing to overwrite existing file: {source}", file=sys.stderr); return 2
96
+ source.write_text(EXAMPLE, encoding="utf-8")
97
+ (path / "fluxscript.toml").write_text('[project]\nname = "Research Manager"\nsource = "app.py"\nversion = "0.1.0"\n\n[targets]\ndefault = ["web"]\n', encoding="utf-8")
98
+ print(f"Created project at {path}\nNext: cd {path} && fluxscript check app.py && fluxscript website-builder app.py")
99
+ return 0
100
+
101
+ def _validate_generated(target: str, output: Path) -> None:
102
+ if target == "web":
103
+ from bs4 import BeautifulSoup
104
+ html = (output / "index.html").read_text(encoding="utf-8")
105
+ soup = BeautifulSoup(html, "html.parser")
106
+ if soup.title is None or soup.main is None or not soup.find_all("section", attrs={"data-route": True}):
107
+ raise RuntimeError("Generated web output failed structural HTML validation.")
108
+ if not (output / "styles.css").is_file() or not (output / "app.js").is_file():
109
+ raise RuntimeError("Generated web output is missing CSS or JavaScript.")
110
+ elif target == "android":
111
+ from defusedxml import ElementTree
112
+ manifest = output / "app/src/main/AndroidManifest.xml"
113
+ ElementTree.parse(str(manifest))
114
+ if not (output / "app/src/main/java/dev/fluxscript/generated/MainActivity.kt").is_file():
115
+ raise RuntimeError("Generated Android output is missing its Kotlin entry point.")
116
+ elif target == "ios":
117
+ source = output / "GeneratedApp.swift"
118
+ if not source.is_file() or "@main" not in source.read_text(encoding="utf-8"):
119
+ raise RuntimeError("Generated iOS output is missing its SwiftUI entry point.")
120
+ elif target == "cli":
121
+ import ast
122
+ source = output / "app_cli.py"
123
+ ast.parse(source.read_text(encoding="utf-8"), filename=str(source))
124
+
125
+ def _builder(source: Path, target: str, out: Path, *, no_install_deps: bool = False) -> int:
126
+ try:
127
+ dependencies = ensure_target_dependencies(target, no_install=no_install_deps)
128
+ spec = parse_file(source)
129
+ files = BACKENDS[target](spec, out.expanduser())
130
+ _validate_generated(target, out.expanduser())
131
+ print(f"FluxScript {target} builder: validated {len(files)} generated files in {out}")
132
+ print("Python validation helpers: " + (", ".join(dependencies) if dependencies else "none required"))
133
+ if target == "android": print("Native build requirement: JDK + Android SDK + compatible Gradle/Android Gradle Plugin.")
134
+ if target == "ios": print("Native build requirement: macOS + Xcode; this command generates source, not an IPA.")
135
+ return 0
136
+ except (SourceError, OSError, ValueError, RuntimeError, ImportError) as exc:
137
+ print(f"builder error: {exc}", file=sys.stderr); return 2
138
+
139
+ def _build(source: Path, target: str, out: Path, no_install_deps: bool = False) -> int:
140
+ if target == "all":
141
+ code = 0
142
+ for name in BACKENDS:
143
+ result = _builder(source, name, out / name, no_install_deps=no_install_deps)
144
+ if result: code = result
145
+ return code
146
+ return _builder(source, target, out, no_install_deps=no_install_deps)
147
+
148
+ def _service(args: argparse.Namespace) -> int:
149
+ try:
150
+ client = ExecutionServiceClient(base_url=args.url, token=args.token, timeout=args.timeout)
151
+ if args.service_command == "status":
152
+ endpoint = args.url or os.environ.get("FLUXSCRIPT_SERVICE_URL")
153
+ print(json.dumps({"service": "FluxScript remote execution API", "base_url": client.base_url,
154
+ "endpoint_configured": bool(endpoint), "using_default_endpoint": not bool(endpoint),
155
+ "token_configured": bool(client.token), "network_checked": False}, indent=2))
156
+ return 0
157
+ if args.service_command == "runtimes":
158
+ print(json.dumps(client.runtimes(), indent=2, ensure_ascii=False)); return 0
159
+ if args.service_command == "run":
160
+ code = Path(args.file).read_text(encoding="utf-8")
161
+ stdin = Path(args.stdin_file).read_text(encoding="utf-8") if args.stdin_file else (args.stdin or "")
162
+ result = client.execute(args.language, args.runtime_version, code, stdin=stdin, args=args.arg or [], run_timeout_ms=args.run_timeout)
163
+ print(json.dumps(result, indent=2, ensure_ascii=False))
164
+ run = result.get("run", {})
165
+ return 0 if run.get("code", 0) == 0 and not run.get("signal") else 1
166
+ except (ExecutionServiceError, OSError, ValueError) as exc:
167
+ print(f"service error: {exc}", file=sys.stderr); return 2
168
+ return 2
169
+
170
+ def _device_info() -> dict[str, object]:
171
+ """Return best-effort host/device details without installing extra packages."""
172
+ system = platform.system() or os.name
173
+ release = platform.release()
174
+ machine = platform.machine()
175
+ env = os.environ
176
+ if env.get("ANDROID_ARGUMENT") or env.get("ANDROID_ROOT") or "ANDROID_DATA" in env:
177
+ device_type = "android"
178
+ elif sys.platform == "ios":
179
+ device_type = "ios"
180
+ elif system == "Windows":
181
+ device_type = "windows_pc"
182
+ elif system == "Darwin":
183
+ device_type = "mac"
184
+ elif system == "Linux":
185
+ device_type = "linux_device"
186
+ else:
187
+ device_type = "unknown"
188
+ return {
189
+ "device_name": platform.node() or "unknown",
190
+ "device_type": device_type,
191
+ "system": system,
192
+ "system_release": release,
193
+ "machine_architecture": machine,
194
+ "python_version": platform.python_version(),
195
+ "python_executable": sys.executable,
196
+ "termux": bool(env.get("TERMUX_VERSION")),
197
+ "note": "Best-effort runtime detection; some phones/tablets report the terminal or host name rather than the hardware model.",
198
+ }
199
+
200
+ def _doctor() -> int:
201
+ checks = {"device": _device_info(), "python_version": sys.version.split()[0], "python_supported": sys.version_info >= (3, 10),
202
+ "node": shutil.which("node"), "npm": shutil.which("npm"), "java": shutil.which("java"),
203
+ "gradle": shutil.which("gradle"), "xcodebuild": shutil.which("xcodebuild"),
204
+ "android_home": os.environ.get("ANDROID_HOME") or os.environ.get("ANDROID_SDK_ROOT")}
205
+ print(json.dumps(checks, indent=2)); return 0 if checks["python_supported"] else 1
206
+
207
+ def main(argv: list[str] | None = None) -> int:
208
+ raw_args = list(sys.argv[1:] if argv is None else argv)
209
+ # Friendly command names omit punctuation. Legacy hyphenated forms remain accepted.
210
+ aliases = {
211
+ "new": "new", "check": "check", "build": "build", "guide": "guide",
212
+ "targets": "targets", "doctor": "doctor", "device": "device", "detectdevice": "device",
213
+ "developer": "developer", "author": "developer", "integrations": "integrations",
214
+ "analyze": "analyze", "review": "review", "service": "service",
215
+ "website": "website-builder", "web": "website-builder", "websitebuilder": "website-builder",
216
+ "webbuilder": "website-builder", "android": "android-builder", "androidbuilder": "android-builder",
217
+ "ios": "ios-builder", "iosbuilder": "ios-builder", "cli": "cli-builder", "clibuilder": "cli-builder",
218
+ "generate": "generate-project", "generateproject": "generate-project",
219
+ }
220
+ if raw_args:
221
+ first = raw_args[0].lower()
222
+ normalized = first.replace("-", "").replace("_", "")
223
+ # Also accept familiar two-word command forms such as "website builder".
224
+ if len(raw_args) >= 2 and (first + " " + raw_args[1].lower()) in {
225
+ "website builder", "web builder", "android builder", "ios builder", "cli builder", "generate project"
226
+ }:
227
+ phrase = first + " " + raw_args[1].lower()
228
+ target = {"website builder": "website-builder", "web builder": "website-builder",
229
+ "android builder": "android-builder", "ios builder": "ios-builder",
230
+ "cli builder": "cli-builder", "generate project": "generate-project"}[phrase]
231
+ raw_args[:2] = [target]
232
+ elif normalized in aliases:
233
+ raw_args[0] = aliases[normalized]
234
+ parser = argparse.ArgumentParser(prog="fluxscript", description="FluxScript — a Python-first multi-target project generator")
235
+ parser.add_argument("--version", action="version", version=f"FluxScript {__version__}")
236
+ sub = parser.add_subparsers(dest="command", required=True)
237
+ newp = sub.add_parser("new", help="create a starter project"); newp.add_argument("path", nargs="?", default=".")
238
+ check = sub.add_parser("check", help="validate declarative source without executing it"); check.add_argument("source", nargs="?", default="app.py")
239
+ build = sub.add_parser("build", help="build one or all generated source targets"); build.add_argument("source", nargs="?", default="app.py"); build.add_argument("--target", choices=["web", "android", "ios", "cli", "all"], default="web"); build.add_argument("--out", default="build"); build.add_argument("--no-install-deps", "--noinstalldeps", dest="no_install_deps", action="store_true")
240
+ for command, target, description in [
241
+ ("website-builder", "web", "generate and validate a website project"),
242
+ ("android-builder", "android", "generate and validate an Android project"),
243
+ ("ios-builder", "ios", "generate and validate an iOS source project"),
244
+ ("cli-builder", "cli", "generate a standalone command-line application"),
245
+ ]:
246
+ bp = sub.add_parser(command, help=description); bp.add_argument("source", nargs="?", default="app.py"); bp.add_argument("--out", default=f"build/{target}"); bp.add_argument("--no-install-deps", "--noinstalldeps", dest="no_install_deps", action="store_true"); bp.set_defaults(builder_target=target)
247
+ analyze = sub.add_parser("analyze", help="read-only source/project analysis; never executes analyzed files"); analyze.add_argument("path", nargs="?", default="."); analyze.add_argument("--out", default=None, help="write JSON report to this path")
248
+ review = sub.add_parser("review", help="alias for the read-only source/project analyzer"); review.add_argument("path", nargs="?", default="."); review.add_argument("--out", default=None, help="write JSON report to this path")
249
+ gp = sub.add_parser("generate-project", help="generate project files and export a ZIP into storage")
250
+ gp.add_argument("--source", default="app.py"); gp.add_argument("--target", choices=["web", "android", "ios", "cli", "all"], default="web"); gp.add_argument("--storage", default=None, help="output storage directory (default: shared Android storage or ~/FluxScriptProjects)"); gp.add_argument("--no-install-deps", "--noinstalldeps", dest="no_install_deps", action="store_true")
251
+ sub.add_parser("guide", help="print all FluxScript commands and usage examples")
252
+ sub.add_parser("device", help="detect the current runtime device/operating-system type")
253
+ sub.add_parser("developer", help="show FluxScript developer/author information")
254
+ sub.add_parser("integrations", help="show built-in capabilities and detected toolchains")
255
+ sub.add_parser("targets", help="list available generation targets")
256
+ sub.add_parser("doctor", help="check local development toolchains")
257
+ service = sub.add_parser("service", help="call a configured remote execution service")
258
+ svc = service.add_subparsers(dest="service_command", required=True)
259
+ for name, help_text in [("status", "show local service configuration"), ("runtimes", "fetch available remote runtimes")]:
260
+ sp = svc.add_parser(name, help=help_text); sp.add_argument("--url", default=None); sp.add_argument("--token", default=None); sp.add_argument("--timeout", type=float, default=10.0)
261
+ run = svc.add_parser("run", help="submit source to the remote execution service"); run.add_argument("language"); run.add_argument("runtime_version"); run.add_argument("file"); run.add_argument("--stdin", default=None); run.add_argument("--stdin-file"); run.add_argument("--arg", action="append", default=[]); run.add_argument("--run-timeout", type=int, default=3000); run.add_argument("--url", default=None); run.add_argument("--token", default=None); run.add_argument("--timeout", type=float, default=10.0)
262
+ args = parser.parse_args(raw_args)
263
+ if args.command == "new": return _new(Path(args.path))
264
+ if args.command == "guide": print(GUIDE); return 0
265
+ if args.command == "device": print(json.dumps(_device_info(), indent=2)); return 0
266
+ if args.command == "developer": print(json.dumps({"name": "Md Safarujjaman", "project": "FluxScript", "version": __version__}, indent=2)); return 0
267
+ if args.command == "integrations": print(json.dumps(integration_status(), indent=2)); return 0
268
+ if args.command == "targets":
269
+ print(json.dumps({"web": {"description": "HTML/CSS/JavaScript/TypeScript", "validation": "HTML structure", "native_toolchain": "browser; Node.js optional"}, "android": {"description": "Kotlin/Gradle project", "validation": "XML manifest structure", "native_toolchain": "JDK + Android SDK + Gradle"}, "ios": {"description": "SwiftUI source project", "validation": "SwiftUI entry-point checks", "native_toolchain": "macOS + Xcode"}, "cli": {"description": "Python argparse command-line project", "validation": "Python syntax parsing", "native_toolchain": "Python 3.10+"}}, indent=2)); return 0
270
+ if args.command == "doctor": return _doctor()
271
+ if args.command == "service": return _service(args)
272
+ if args.command == "check":
273
+ try:
274
+ spec = parse_file(args.source); print(f"OK: {spec.name} ({len(spec.pages)} page(s)); source parsed without executing it."); return 0
275
+ except (SourceError, OSError) as exc: print(f"error: {exc}", file=sys.stderr); return 2
276
+ if args.command in {"analyze", "review"}:
277
+ try:
278
+ report = analyze_path(args.path)
279
+ if args.out:
280
+ destination = write_report(report, args.out)
281
+ print(f"Analysis report written to {destination}")
282
+ print(json.dumps(report["summary"], indent=2))
283
+ else:
284
+ print(json.dumps(report, indent=2, ensure_ascii=False))
285
+ return 1 if report["summary"]["errors"] else 0
286
+ except (OSError, ValueError) as exc:
287
+ print(f"analysis error: {exc}", file=sys.stderr); return 2
288
+ if args.command == "build": return _build(Path(args.source), args.target, Path(args.out), args.no_install_deps)
289
+ if args.command in {"website-builder", "android-builder", "ios-builder", "cli-builder"}:
290
+ return _builder(Path(args.source), args.builder_target, Path(args.out), no_install_deps=args.no_install_deps)
291
+ if args.command == "generate-project":
292
+ try:
293
+ storage = Path(args.storage).expanduser() if args.storage else default_storage()
294
+ for target in (["web", "android", "ios", "cli"] if args.target == "all" else [args.target]):
295
+ ensure_target_dependencies(target, no_install=args.no_install_deps)
296
+ directory, archive, files = generate_project(Path(args.source), args.target, storage)
297
+ print(f"Project directory: {directory}\nProject ZIP: {archive}\nGenerated files: {len(files)}")
298
+ return 0
299
+ except (SourceError, OSError, ValueError, RuntimeError, ImportError) as exc:
300
+ print(f"project generation error: {exc}", file=sys.stderr); return 2
301
+ return 2
302
+
303
+ if __name__ == "__main__": raise SystemExit(main())
@@ -0,0 +1,96 @@
1
+ """Safe, on-demand management of FluxScript's build-time helper packages.
2
+
3
+ FluxScript's parser accepts a restricted declarative format and does not execute
4
+ arbitrary application imports, so it intentionally does not guess or install
5
+ third-party runtime dependencies for user applications. This module manages only
6
+ packages used by FluxScript itself to validate generated output.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ import subprocess
12
+ import sys
13
+ from dataclasses import dataclass
14
+ from typing import Iterable
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Dependency:
19
+ distribution: str
20
+ module: str
21
+ purpose: str
22
+
23
+
24
+ # Only third-party packages genuinely used by the generated-output validators.
25
+ # iOS and CLI validators use the Python standard library and need no pip package.
26
+ TARGET_DEPENDENCIES: dict[str, tuple[Dependency, ...]] = {
27
+ "web": (Dependency("beautifulsoup4", "bs4", "validate generated HTML structure"),),
28
+ "android": (Dependency("defusedxml", "defusedxml", "safely parse generated Android XML"),),
29
+ "ios": (),
30
+ "cli": (),
31
+ }
32
+
33
+
34
+ def _is_importable(module: str) -> bool:
35
+ try:
36
+ return importlib.util.find_spec(module) is not None
37
+ except (ImportError, ValueError, AttributeError):
38
+ return False
39
+
40
+
41
+ def ensure_target_dependencies(
42
+ target: str,
43
+ *,
44
+ no_install: bool = False,
45
+ upgrade: bool = True,
46
+ extra_dependencies: Iterable[Dependency] = (),
47
+ ) -> list[str]:
48
+ """Ensure the selected target's validator packages are available.
49
+
50
+ By default, missing packages are installed and already-present packages are
51
+ upgraded when a target builder is invoked. Set ``no_install`` to require all
52
+ packages to be preinstalled. ``upgrade=False`` installs missing packages but
53
+ leaves existing installations unchanged. All pip invocations use the active
54
+ interpreter, disable pip's version-check notice, and fail with an actionable
55
+ error rather than silently continuing.
56
+ """
57
+ if target not in TARGET_DEPENDENCIES:
58
+ raise ValueError(f"Unknown target: {target}")
59
+
60
+ dependencies = (*TARGET_DEPENDENCIES[target], *tuple(extra_dependencies))
61
+ ensured: list[str] = []
62
+ for dep in dependencies:
63
+ installed = _is_importable(dep.module)
64
+ if no_install:
65
+ if not installed:
66
+ raise RuntimeError(
67
+ f"Missing dependency {dep.distribution!r} ({dep.purpose}). "
68
+ "Install it with 'python -m pip install " + dep.distribution +
69
+ "' or rerun without --no-install-deps."
70
+ )
71
+ ensured.append(dep.distribution)
72
+ continue
73
+
74
+ if installed and not upgrade:
75
+ ensured.append(dep.distribution)
76
+ continue
77
+
78
+ action = "upgrade" if installed else "install"
79
+ print(f"FluxScript: {action} {dep.distribution} ({dep.purpose})...", flush=True)
80
+ command = [sys.executable, "-m", "pip", "install", "--disable-pip-version-check"]
81
+ if upgrade and installed:
82
+ command.append("--upgrade")
83
+ command.append(dep.distribution)
84
+ try:
85
+ subprocess.run(command, check=True)
86
+ except (OSError, subprocess.CalledProcessError) as exc:
87
+ raise RuntimeError(
88
+ f"Could not {action} {dep.distribution}. Check internet access and pip permissions. "
89
+ f"If it is already installed, rerun with --no-install-deps to skip pip."
90
+ ) from exc
91
+ if not _is_importable(dep.module):
92
+ raise RuntimeError(
93
+ f"pip reported success, but module {dep.module!r} is still not importable."
94
+ )
95
+ ensured.append(dep.distribution)
96
+ return ensured
fluxscript/dsl.py ADDED
@@ -0,0 +1,25 @@
1
+ """Declarative Python API. The compiler parses source; it never imports this file."""
2
+ from .models import AppSpec, PageSpec, ElementSpec
3
+
4
+ class App:
5
+ def __init__(self, name: str): self.spec = AppSpec(str(name))
6
+ def add_page(self, page: "Page") -> "App": self.spec.pages.append(page.spec); return self
7
+ class Page:
8
+ def __init__(self, path: str = "/", title: str = ""):
9
+ self.spec = PageSpec(str(path), str(title))
10
+ def add(self, element: "Element") -> "Page": self.spec.elements.append(element.spec); return self
11
+ class Element:
12
+ def __init__(self, spec: ElementSpec): self.spec = spec
13
+ class Text(Element):
14
+ def __init__(self, text: str): super().__init__(ElementSpec("text", str(text)))
15
+ class Heading(Element):
16
+ def __init__(self, text: str, level: int = 1):
17
+ level = int(level)
18
+ if level not in (1,2,3,4,5,6): raise ValueError("Heading level must be 1–6")
19
+ super().__init__(ElementSpec("heading", str(text), attributes={"level": level}))
20
+ class Button(Element):
21
+ def __init__(self, text: str, action: str): super().__init__(ElementSpec("button", str(text), action=str(action)))
22
+ class Input(Element):
23
+ def __init__(self, label: str, name: str, input_type: str = "text"):
24
+ if input_type not in {"text", "email", "number", "password", "date"}: raise ValueError("Unsupported input_type")
25
+ super().__init__(ElementSpec("input", str(label), name=str(name), attributes={"input_type": input_type}))
@@ -0,0 +1,23 @@
1
+ """FluxScript's built-in capability registry."""
2
+ from importlib.util import find_spec
3
+ from shutil import which
4
+
5
+ _CAPABILITIES = (
6
+ {"name": "web-builder", "purpose": "Generate HTML, CSS, JavaScript and TypeScript", "kind": "built-in", "module": None, "command": None},
7
+ {"name": "android-builder", "purpose": "Generate Kotlin and Gradle project source", "kind": "built-in", "module": None, "command": None},
8
+ {"name": "ios-builder", "purpose": "Generate SwiftUI project source", "kind": "built-in", "module": None, "command": None},
9
+ {"name": "cli-builder", "purpose": "Generate a standalone argparse-based CLI", "kind": "built-in", "module": None, "command": None},
10
+ {"name": "source-analyzer", "purpose": "Read-only source inventory and static review", "kind": "built-in", "module": None, "command": None},
11
+ {"name": "remote-execution", "purpose": "Call a configured remote source execution API", "kind": "optional-service", "module": None, "command": None},
12
+ {"name": "node-toolchain", "purpose": "Optional JavaScript and TypeScript validation", "kind": "local-toolchain", "module": None, "command": "node"},
13
+ {"name": "android-toolchain", "purpose": "Optional Android native compilation", "kind": "local-toolchain", "module": None, "command": "gradle"},
14
+ {"name": "ios-toolchain", "purpose": "Optional Apple-platform compilation (macOS only)", "kind": "local-toolchain", "module": None, "command": "xcodebuild"},
15
+ )
16
+
17
+ def status() -> list[dict[str, str | bool]]:
18
+ result = []
19
+ for item in _CAPABILITIES:
20
+ row = dict(item)
21
+ row["available"] = True if item["kind"] == "built-in" else bool(item["command"] and which(item["command"]))
22
+ result.append(row)
23
+ return result
@@ -0,0 +1,29 @@
1
+ """Explicit, bounded external CLI invocation helpers. Never uses a shell."""
2
+ from __future__ import annotations
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ class ExternalToolError(RuntimeError): pass
8
+
9
+ def run_tool(executable: str, args: list[str], *, cwd: str | Path | None = None,
10
+ timeout: float = 30.0, output_limit: int = 1_000_000) -> dict:
11
+ """Run an explicitly named installed tool; callers must obtain user approval first.
12
+
13
+ The function does not interpret shell syntax. It does not download or install tools.
14
+ Use only with trusted executable names/paths and reviewed arguments.
15
+ """
16
+ resolved = shutil.which(executable)
17
+ if not resolved: raise ExternalToolError(f"Required external executable is not installed: {executable}")
18
+ if not isinstance(args, list) or any(not isinstance(x, str) for x in args):
19
+ raise TypeError("args must be a list of strings")
20
+ try:
21
+ completed = subprocess.run([resolved, *args], cwd=str(cwd) if cwd else None,
22
+ capture_output=True, text=True, timeout=max(1.0, min(timeout, 300.0)),
23
+ shell=False, check=False)
24
+ except subprocess.TimeoutExpired as exc:
25
+ raise ExternalToolError(f"External tool timed out after {exc.timeout} seconds") from exc
26
+ stdout, stderr = completed.stdout or "", completed.stderr or ""
27
+ if len(stdout) > output_limit: stdout = stdout[:output_limit] + "\n[output truncated]"
28
+ if len(stderr) > output_limit: stderr = stderr[:output_limit] + "\n[output truncated]"
29
+ return {"command": resolved, "returncode": completed.returncode, "stdout": stdout, "stderr": stderr}
@@ -0,0 +1,50 @@
1
+ """Explicit, bounded client for an operator-configured FluxScript-compatible execution API."""
2
+ from __future__ import annotations
3
+ import ipaddress, json, os
4
+ from urllib.parse import urlsplit
5
+ from urllib.request import Request, urlopen
6
+ from urllib.error import HTTPError, URLError
7
+
8
+ class ExecutionServiceError(RuntimeError): pass
9
+ class ExecutionServiceClient:
10
+ def __init__(self, base_url: str | None = None, token: str | None = None, timeout: float = 10.0):
11
+ self.base_url = (base_url or os.environ.get("FLUXSCRIPT_SERVICE_URL", "http://127.0.0.1:2000/api/v2")).rstrip("/")
12
+ self.token = token if token is not None else os.environ.get("FLUXSCRIPT_SERVICE_TOKEN")
13
+ self.timeout = max(1.0, min(float(timeout), 60.0))
14
+ parts = urlsplit(self.base_url)
15
+ if parts.scheme not in {"http", "https"} or not parts.hostname or parts.username or parts.password or parts.query or parts.fragment:
16
+ raise ValueError("Execution service URL must be an HTTP(S) base URL without credentials, query, or fragment")
17
+ if parts.scheme == "http":
18
+ host = parts.hostname.lower().rstrip(".")
19
+ local = host == "localhost"
20
+ try: local = local or ipaddress.ip_address(host).is_loopback
21
+ except ValueError: pass
22
+ if not local: raise ValueError("Non-local execution-service endpoints must use HTTPS")
23
+ def _request(self, path: str, payload: dict | None = None):
24
+ data = None if payload is None else json.dumps(payload).encode("utf-8")
25
+ headers = {"Accept": "application/json", "User-Agent": "FluxScript/0.1.0"}
26
+ if data is not None: headers["Content-Type"] = "application/json"
27
+ if self.token: headers["Authorization"] = f"Bearer {self.token}"
28
+ req = Request(f"{self.base_url}/{path.lstrip('/')}", data=data, headers=headers, method="POST" if data is not None else "GET")
29
+ try:
30
+ with urlopen(req, timeout=self.timeout) as response:
31
+ raw = response.read(2_000_001)
32
+ if len(raw) > 2_000_000: raise ExecutionServiceError("Execution service response exceeded 2 MB limit")
33
+ result = json.loads(raw.decode("utf-8"))
34
+ except HTTPError as exc: raise ExecutionServiceError(f"Execution service returned HTTP {exc.code}") from exc
35
+ except (URLError, TimeoutError, OSError) as exc: raise ExecutionServiceError(f"Could not reach configured execution service ({exc.__class__.__name__})") from exc
36
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise ExecutionServiceError("Execution service returned invalid JSON") from exc
37
+ return result
38
+ def runtimes(self) -> list[dict]:
39
+ result = self._request("runtimes")
40
+ if not isinstance(result, list) or any(not isinstance(x, dict) for x in result): raise ExecutionServiceError("Unexpected runtimes response schema")
41
+ return result
42
+ def execute(self, language: str, version: str, code: str, *, stdin: str = "", args: list[str] | None = None, compile_timeout_ms: int = 10_000, run_timeout_ms: int = 3_000) -> dict:
43
+ if not isinstance(language, str) or not language.strip() or not isinstance(version, str) or not version.strip(): raise ValueError("language and version are required")
44
+ if not isinstance(code, str) or len(code.encode("utf-8")) > 1_000_000: raise ValueError("Source must be text no larger than 1 MB")
45
+ if not isinstance(stdin, str) or len(stdin.encode("utf-8")) > 100_000: raise ValueError("stdin must be text no larger than 100 KB")
46
+ if args is not None and (not isinstance(args, list) or any(not isinstance(x, str) for x in args)): raise ValueError("args must be a list of strings")
47
+ payload = {"language": language, "version": version, "files": [{"name": "main", "content": code}], "stdin": stdin, "args": list(args or []), "compile_timeout": max(100, min(int(compile_timeout_ms), 30_000)), "run_timeout": max(100, min(int(run_timeout_ms), 10_000)), "compile_memory_limit": 128_000_000, "run_memory_limit": 128_000_000}
48
+ result = self._request("execute", payload)
49
+ if not isinstance(result, dict) or not isinstance(result.get("run"), dict): raise ExecutionServiceError("Unexpected execute response schema")
50
+ return result
fluxscript/models.py ADDED
@@ -0,0 +1,56 @@
1
+ """Stable, JSON-serializable semantic model shared by FluxScript backends."""
2
+ from dataclasses import dataclass, field
3
+ from typing import Any
4
+
5
+ @dataclass
6
+ class ElementSpec:
7
+ kind: str
8
+ text: str = ""
9
+ action: str | None = None
10
+ name: str | None = None
11
+ attributes: dict[str, Any] = field(default_factory=dict)
12
+
13
+ @dataclass
14
+ class PageSpec:
15
+ path: str = "/"
16
+ title: str = ""
17
+ elements: list[ElementSpec] = field(default_factory=list)
18
+
19
+ @dataclass
20
+ class AppSpec:
21
+ name: str
22
+ pages: list[PageSpec] = field(default_factory=list)
23
+ metadata: dict[str, Any] = field(default_factory=dict)
24
+
25
+ def validate(self) -> list[str]:
26
+ issues: list[str] = []
27
+ if not isinstance(self.name, str) or not self.name.strip():
28
+ issues.append("Application name must be a non-empty string.")
29
+ if len(self.name) > 120:
30
+ issues.append("Application name must be at most 120 characters.")
31
+ if not self.pages:
32
+ issues.append("Application must define at least one page.")
33
+ paths: set[str] = set()
34
+ for page in self.pages:
35
+ if not page.path.startswith("/") or ".." in page.path.split("/") or "\\" in page.path:
36
+ issues.append(f"Page path must be an absolute, normalized URL path: {page.path!r}")
37
+ if page.path in paths:
38
+ issues.append(f"Duplicate page path: {page.path}")
39
+ paths.add(page.path)
40
+ if len(page.title) > 160:
41
+ issues.append(f"Page title for {page.path!r} must be at most 160 characters.")
42
+ for element in page.elements:
43
+ if element.kind not in {"text", "heading", "button", "input"}:
44
+ issues.append(f"Unsupported element kind: {element.kind!r}")
45
+ if len(element.text) > 10_000:
46
+ issues.append("Element text exceeds 10000 characters.")
47
+ if element.kind == "button":
48
+ if not element.action or not element.action.strip(): issues.append(f"Button {element.text!r} has no action name.")
49
+ elif not element.action.isidentifier(): issues.append(f"Button action must be a valid identifier: {element.action!r}")
50
+ if element.kind == "input":
51
+ if not element.name or not element.name.isidentifier(): issues.append(f"Input field name must be a valid identifier: {element.name!r}")
52
+ if element.attributes.get("input_type", "text") not in {"text", "email", "number", "password", "date"}:
53
+ issues.append(f"Unsupported input type: {element.attributes.get('input_type')!r}")
54
+ if element.kind == "heading" and element.attributes.get("level", 1) not in (1,2,3,4,5,6):
55
+ issues.append("Heading level must be between 1 and 6.")
56
+ return issues