| #!/usr/bin/env python3 | |
| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| """ | |
| Bootstrap React Plugin CLI Tool. | |
| This script provides a command-line interface to create new React UI plugin | |
| directories based on the airflow-core/ui project structure. It sets up all the | |
| necessary configuration files, dependencies, and basic structure for development | |
| with the same tooling as used in Airflow's core UI. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import re | |
| import shutil | |
| import sys | |
| from pathlib import Path | |
| AI_AGENT_RULES_DIR = "ai-agent-rules" | |
| def get_template_dir() -> Path: | |
| """Get the template directory path.""" | |
| script_dir = Path(__file__).parent | |
| template_dir = script_dir / "react_plugin_template" | |
| if not template_dir.exists(): | |
| print(f"Error: Template directory not found at {template_dir}") | |
| sys.exit(1) | |
| return template_dir | |
| def should_include_ai_agent_rules() -> bool: | |
| """Ask whether AI agent coding rules should be included.""" | |
| answer = input("Include AI agent coding rules? [y/N]: ") | |
| return answer.strip().casefold() in {"y", "yes"} | |
| def replace_template_variables(content: str, project_name: str) -> str: | |
| """Replace template variables in file content.""" | |
| return content.replace("{{PROJECT_NAME}}", project_name) | |
| def remove_apache_license_header(content: str, file_extension: str) -> str: | |
| """Remove Apache license header from file content based on file type.""" | |
| if file_extension in [".ts", ".tsx", ".js", ".jsx"]: | |
| license_pattern = r"/\*!\s*\*\s*Licensed to the Apache Software Foundation.*?\*/\s*" | |
| content = re.sub(license_pattern, "", content, flags=re.DOTALL) | |
| elif file_extension in [".md"]: | |
| license_pattern = r"<!--\s*Licensed to the Apache Software Foundation.*?-->\s*" | |
| content = re.sub(license_pattern, "", content, flags=re.DOTALL) | |
| elif file_extension in [".html"]: | |
| license_pattern = r"<!--\s*Licensed to the Apache Software Foundation.*?-->\s*" | |
| content = re.sub(license_pattern, "", content, flags=re.DOTALL) | |
| return content | |
| def copy_template_files( | |
| template_dir: Path, project_path: Path, project_name: str, *, include_ai_agent_rules: bool | |
| ) -> None: | |
| for item in template_dir.rglob("*"): | |
| if item.is_file(): | |
| # Calculate relative path from template root | |
| rel_path = item.relative_to(template_dir) | |
| if not include_ai_agent_rules and rel_path.parts[0] == AI_AGENT_RULES_DIR: | |
| continue | |
| target_path = project_path / rel_path | |
| target_path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(item, encoding="utf-8") as f: | |
| content = f.read() | |
| content = replace_template_variables(content, project_name) | |
| file_extension = item.suffix.lower() | |
| content = remove_apache_license_header(content, file_extension) | |
| with open(target_path, "w", encoding="utf-8") as f: | |
| f.write(content) | |
| print(f" Created: {rel_path}") | |
| def bootstrap_react_plugin(args) -> None: | |
| """Bootstrap a new React plugin project.""" | |
| project_name = args.name | |
| target_dir = args.dir if args.dir else project_name | |
| project_path = Path(target_dir).resolve() | |
| template_dir = get_template_dir() | |
| if project_path.exists(): | |
| print(f"Error: Directory '{project_path}' already exists!") | |
| sys.exit(1) | |
| if not project_name.replace("-", "").replace("_", "").isalnum(): | |
| print("Error: Project name should only contain letters, numbers, hyphens, and underscores") | |
| sys.exit(1) | |
| include_ai_agent_rules = should_include_ai_agent_rules() | |
| print(f"Creating React plugin project: {project_name}") | |
| print(f"Target directory: {project_path}") | |
| print(f"Template directory: {template_dir}") | |
| project_path.mkdir(parents=True, exist_ok=True) | |
| try: | |
| # Copy template files | |
| print("Copying template files...") | |
| copy_template_files( | |
| template_dir, | |
| project_path, | |
| project_name, | |
| include_ai_agent_rules=include_ai_agent_rules, | |
| ) | |
| print(f"\nā Successfully created {project_name}!") | |
| print("\nNext steps:") | |
| print(f" cd {target_dir}") | |
| print(" pnpm install") | |
| print(" pnpm dev") | |
| print("\nHappy coding! š") | |
| except Exception as e: | |
| print(f"Error creating project: {e}") | |
| if project_path.exists(): | |
| shutil.rmtree(project_path) | |
| sys.exit(1) | |
| def main(): | |
| """Main CLI entry point.""" | |
| parser = argparse.ArgumentParser( | |
| description="Bootstrap a new React UI plugin project", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| python bootstrap.py my-plugin | |
| python bootstrap.py my-plugin --dir /path/to/projects/my-plugin | |
| This will create a new React project with all the necessary configuration | |
| files, dependencies, and structure needed for Airflow plugin development. | |
| """, | |
| ) | |
| parser.add_argument( | |
| "name", | |
| help="Name of the React plugin project (letters, numbers, hyphens, and underscores only)", | |
| ) | |
| parser.add_argument( | |
| "--dir", | |
| "-d", | |
| help="Target directory for the project (defaults to project name)", | |
| ) | |
| parser.add_argument( | |
| "--verbose", | |
| "-v", | |
| action="store_true", | |
| help="Enable verbose output", | |
| ) | |
| args = parser.parse_args() | |
| try: | |
| bootstrap_react_plugin(args) | |
| except KeyboardInterrupt: | |
| print("\n\nOperation cancelled by user.") | |
| sys.exit(1) | |
| except Exception as e: | |
| print(f"Error: {e}") | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() |