Source code for scitex_app.appmaker._scaffold

"""App initializer — generate complete boilerplate for a SciTeX app plugin."""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Optional

from ._license import generate_license_text
from ._scaffold_agent_context import _agents_md
from ._scaffold_html import (
    _agents_json,
    _app_css,
    _apps_py,
    _index_html,
    _index_partial_html,
    _manifest_json,
    _readme_md,
    _skill_py,
    _tests_py,
    _urls_py,
    _views_py,
)
from ._scaffold_react import build_react_files

logger = logging.getLogger(__name__)


[docs] def init_app( target_dir: str | Path, name: str, *, label: str = "", icon: str = "fas fa-puzzle-piece", description: str = "", manifest: Optional[dict] = None, license_id: str = "AGPL-3.0", overwrite: bool = False, frontend_type: str = "html", ) -> list[str]: """Generate complete app boilerplate in target_dir. Parameters ---------- target_dir : path Project directory (e.g. data/users/alice/proj/my_app/). name : str Python module name (must end with _app, e.g. 'my_awesome_app'). label : str Human-readable label (default: derived from name). icon : str Font Awesome icon class (default: 'fas fa-puzzle-piece'). description : str Short description for the app. manifest : dict, optional Extra manifest fields to merge. license_id : str SPDX license identifier (default: 'AGPL-3.0'). overwrite : bool If True, overwrite existing files (default: False). frontend_type : str Frontend type: 'html' (default) or 'react' for React+Vite+Zustand. Returns ------- list[str] Relative paths of created files. """ target = Path(target_dir) if not target.exists(): target.mkdir(parents=True, exist_ok=True) if not label: label = name.replace("_", " ").title().removesuffix(" App") class_name = label.replace(" ", "") + "App" files = _build_all_files( name, label, class_name, icon, description, manifest, license_id, frontend_type=frontend_type, ) created = [] for relpath, content in files.items(): filepath = target / relpath if filepath.exists() and not overwrite: logger.debug("Skipping existing file: %s", relpath) continue filepath.parent.mkdir(parents=True, exist_ok=True) filepath.write_text(content, encoding="utf-8") created.append(relpath) logger.debug("Created: %s", relpath) logger.info("Scaffolded %d/%d files in %s", len(created), len(files), target) return created
def _build_all_files( name, label, class_name, icon, description, manifest, license_id, *, frontend_type: str = "html", ): """Build dict of relpath -> content for all scaffold files.""" use_react = frontend_type == "react" files = {} # __init__.py files["__init__.py"] = f'"""SciTeX Cloud App: {label}."""\n' # apps.py files["apps.py"] = _apps_py(name, label, class_name) # views.py files["views.py"] = _views_py(name, label, description) # urls.py files["urls.py"] = _urls_py(name) # tests.py files["tests.py"] = _tests_py(name, label) # skill.py files["skill.py"] = _skill_py(name, label, description) # manifest.json files["manifest.json"] = _manifest_json( name, label, icon, description, manifest, license_id, frontend_type ) # Templates files[f"templates/{name}/index.html"] = _index_html( name, label, include_js_bundle=use_react ) files[f"templates/{name}/index_partial.html"] = _index_partial_html( name, label, icon, react_mount=use_react ) # Static CSS files[f"static/{name}/css/{name}.css"] = _app_css(name, label) # Agents config files[".agents/agents.json"] = _agents_json(name, label) files["AGENTS.md"] = _agents_md(name, label, icon, description, frontend_type) # Platform docs for agents from ._scaffold_docs import _platform_docs_md files["docs/PLATFORM.md"] = _platform_docs_md(name) # README files["README.md"] = _readme_md( name, label, description, license_id, frontend_type=frontend_type ) # LICENSE license_text = generate_license_text(license_id) if license_text is None: license_text = generate_license_text("AGPL-3.0") files["LICENSE"] = license_text # .gitignore files[".gitignore"] = "\n".join( [ "# Runtime data (created by platform, not app source)", "scitex/", "", "# Python", "__pycache__/", "*.pyc", "*.pyo", "*.egg-info/", "", "# Environment", ".env", ".env.*", ".venv/", "venv/", "node_modules/", "", "# IDE", ".vscode/", ".idea/", "", "# OS", ".DS_Store", "Thumbs.db", "", ] ) # pyproject.toml for dual-mode (standalone + extension) files["pyproject.toml"] = _pyproject_toml(name, label, description, license_id) # CLI with gui command for standalone mode files["_cli.py"] = _cli_py(name, label) # React frontend files if use_react: files.update(build_react_files(name, label, icon)) return files def _cli_py(name, label): """Generate _cli.py — standalone GUI launcher.""" slug = name.replace("_", "-") return f'''"""CLI for {label} — standalone GUI launcher.""" import click @click.group(invoke_without_command=True) @click.pass_context def main(ctx): """{label} — SciTeX Cloud App.""" if ctx.invoked_subcommand is None: click.echo(ctx.get_help()) @main.command() @click.option("--port", "-p", default=8050, help="Server port") @click.option("--host", "-h", default="127.0.0.1", help="Host to bind") @click.option("--no-browser", is_flag=True, help="Don't open browser") @click.option("--force", is_flag=True, help="Kill existing process on port") def gui(port, host, no_browser, force): """Launch standalone GUI with workspace shell.""" if force: import subprocess subprocess.run( ["fuser", "-k", f"{{port}}/tcp"], capture_output=True, ) from scitex_app._standalone import run_standalone run_standalone( app_module="{name}", port=port, host=host, open_browser=not no_browser, ) ''' def _pyproject_toml(name, label, description, license_id): """Generate pyproject.toml for dual-mode app (standalone + scitex-cloud extension).""" slug = name.replace("_", "-") desc = description or f"{label} — a SciTeX Cloud app." return f"""[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "{slug}" version = "0.1.0" description = "{desc}" requires-python = ">=3.10" license = "{license_id}" [project.scripts] {slug} = "{name}._cli:main" [project.optional-dependencies] scitex = ["scitex-app>=0.1.0", "scitex-ui>=0.1.0"] dev = ["pytest>=7.0.0"] [project.entry-points."scitex_modules"] {name} = "{name}:get_module_config" """ # EOF