Source code for ansys.dyna.core.__main__

# Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""CLI for PyDyna.

Usage:
    python -m ansys.dyna.core agent --env cursor
    python -m ansys.dyna.core agent --env vscode --copy
    python -m ansys.dyna.core agent --env vscode --pointer
    python -m ansys.dyna.core agent --print
"""

import argparse
from datetime import datetime, timezone
import json
from pathlib import Path
import shutil
import sys

# Package metadata
[docs] PACKAGE_NAMESPACE = "ansys.dyna.core"
[docs] PACKAGE_NAME = "ansys-dyna-core"
[docs] PACKAGE_ECOSYSTEM = "pypi"
[docs] def find_workspace_root() -> Path | None: """Find workspace root by walking up from cwd.""" cwd = Path.cwd() for parent in [cwd, *cwd.parents]: if (parent / ".git").exists(): return parent if (parent / "pyproject.toml").exists(): return parent return None
[docs] def get_package_dir() -> Path: """Get the package directory containing agent instructions.""" return Path(__file__).parent
[docs] def get_instructions_path() -> Path: """Get path to agent instructions file.""" return get_package_dir() / "AGENT.md"
[docs] def get_extended_docs_dir() -> Path: """Get path to extended agent documentation directory.""" return get_package_dir() / "agent"
[docs] def get_instructions_content() -> str: """Read the agent instructions from installed package.""" return get_instructions_path().read_text(encoding="utf-8")
[docs] def format_for_cursor(content: str, source_path: Path) -> str: """Wrap content in Cursor rules MDC format.""" return f"""--- description: PyDyna usage instructions for AI assistants globs: ["**/*.py", "**/*.k", "**/*.key"] alwaysApply: false --- <!-- Auto-generated from: {source_path} --> {content} """
[docs] def ensure_gitignore(workspace: Path) -> None: """Add .agent/ to .gitignore.""" gitignore = workspace / ".gitignore" marker = ".agent/" if gitignore.exists(): content = gitignore.read_text() if marker not in content: with gitignore.open("a") as f: f.write(f"\n# Agent instructions (auto-generated)\n{marker}\n") else: gitignore.write_text(f"# Agent instructions (auto-generated)\n{marker}\n")
# ============================================================================= # Manifest Management # =============================================================================
[docs] def load_manifest(agent_dir: Path) -> dict: """Load the manifest.json file, or return empty manifest.""" manifest_file = agent_dir / "manifest.json" if manifest_file.exists(): try: return json.loads(manifest_file.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError): pass return {"version": "1.0", "packages": []}
[docs] def save_manifest(agent_dir: Path, manifest: dict) -> None: """Save the manifest.json file.""" manifest_file = agent_dir / "manifest.json" manifest_file.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
[docs] def update_manifest_entry( manifest: dict, namespace: str, ecosystem: str, package_name: str, entry_file: str, extended_docs: list[str], mode: str, source_path: str, ) -> dict: """Add or update a package entry in the manifest.""" # Find existing entry packages = manifest.get("packages", []) entry = None for pkg in packages: if pkg.get("namespace") == namespace: entry = pkg break if entry is None: entry = {"namespace": namespace} packages.append(entry) # Update entry entry["ecosystem"] = ecosystem entry["package_name"] = package_name entry["entry_file"] = entry_file entry["extended_docs"] = extended_docs entry["mode"] = mode entry["source"] = source_path entry["installed_at"] = datetime.now(timezone.utc).isoformat() manifest["packages"] = packages return manifest
[docs] def generate_readme(agent_dir: Path, manifest: dict) -> None: """Generate README.md from manifest.""" packages = manifest.get("packages", []) lines = [ "# Agent Instructions", "", "This directory contains agent instructions from installed packages.", "Agents can read these files for context about how to use the packages.", "", "## Installed Packages", "", ] if packages: lines.append("| Namespace | Ecosystem | Package | Entry File |") lines.append("|-----------|-----------|---------|------------|") for pkg in packages: namespace = pkg.get("namespace", "?") ecosystem = pkg.get("ecosystem", "?") package_name = pkg.get("package_name", "?") entry_file = pkg.get("entry_file", "?") lines.append(f"| `{namespace}` | {ecosystem} | {package_name} | [{entry_file}]({entry_file}) |") lines.append("") else: lines.append("*No packages installed yet.*") lines.append("") lines.extend( [ "## Usage", "", "Each package provides its own installation command. For Python packages with", "agent instructions, you can typically run:", "", "```bash", "python -m <package> agent --copy", "```", "", "## Scanning Dependencies", "", "To install agent instructions from all packages in your requirements:", "", "```bash", "# Future: python -m agent_instructions scan requirements.txt", "# For now, run each package's agent command individually", "```", "", "---", "*Auto-generated manifest. Regenerate by running package agent commands.*", "", ] ) readme_file = agent_dir / "README.md" readme_file.write_text("\n".join(lines), encoding="utf-8")
# ============================================================================= # File Copy Operations # =============================================================================
[docs] def copy_all_instructions(workspace: Path, mode: str = "copy") -> tuple[Path, list[str]]: """Copy all agent instruction files to workspace with working links. Returns ------- tuple[Path, list[str]] Main file path and list of extended doc relative paths. """ agent_dir = workspace / ".agent" agent_dir.mkdir(parents=True, exist_ok=True) # Main file with rewritten links main_content = get_instructions_content() main_content = rewrite_links_for_copy(main_content, PACKAGE_NAMESPACE) main_file = agent_dir / f"{PACKAGE_NAMESPACE}.md" main_file.write_text(main_content, encoding="utf-8") # Extended docs extended_dir = agent_dir / PACKAGE_NAMESPACE extended_dir.mkdir(parents=True, exist_ok=True) source_dir = get_extended_docs_dir() extended_docs = [] if source_dir.exists(): for src_file in source_dir.glob("*.md"): dst_file = extended_dir / src_file.name shutil.copy2(src_file, dst_file) extended_docs.append(f"{PACKAGE_NAMESPACE}/{src_file.name}") # Update manifest manifest = load_manifest(agent_dir) manifest = update_manifest_entry( manifest=manifest, namespace=PACKAGE_NAMESPACE, ecosystem=PACKAGE_ECOSYSTEM, package_name=PACKAGE_NAME, entry_file=f"{PACKAGE_NAMESPACE}.md", extended_docs=extended_docs, mode=mode, source_path=str(get_instructions_path()), ) save_manifest(agent_dir, manifest) generate_readme(agent_dir, manifest) return main_file, extended_docs
[docs] def register_pointer_in_manifest(workspace: Path, mode: str = "pointer") -> None: """Register a pointer-mode entry in the manifest.""" agent_dir = workspace / ".agent" agent_dir.mkdir(parents=True, exist_ok=True) source_path = get_instructions_path() extended_dir = get_extended_docs_dir() extended_docs = [] if extended_dir.exists(): for doc in extended_dir.glob("*.md"): extended_docs.append(f"agent/{doc.name}") manifest = load_manifest(agent_dir) manifest = update_manifest_entry( manifest=manifest, namespace=PACKAGE_NAMESPACE, ecosystem=PACKAGE_ECOSYSTEM, package_name=PACKAGE_NAME, entry_file=str(source_path), extended_docs=extended_docs, mode=mode, source_path=str(source_path), ) save_manifest(agent_dir, manifest) generate_readme(agent_dir, manifest)
# ============================================================================= # Pointer Block Management # ============================================================================= def _make_pointer_block( pydyna_ref: str, regenerate_cmd: str, extended_docs: list[str] | None = None, ) -> tuple[str, str, str]: """Create a pointer block for single-file instruction tools.""" start = "<!-- pydyna-agent-instructions:start -->" end = "<!-- pydyna-agent-instructions:end -->" extended_hint = "" if extended_docs: extended_hint = " (links to extended topics)" block = f"""{start} ## PyDyna (ansys-dyna-core) **IMPORTANT:** Before writing any code using `ansys.dyna.core`, you MUST: 1. **Read** the file `{pydyna_ref}` in this workspace 2. **Read linked files** as needed for your task{extended_hint} 3. **Search** docs before assuming patterns (NODE/PART use DataFrames, not simple attributes) 4. **Do not guess** - if uncertain, use `help(keywords.KeywordName)` or ask for clarification Regenerate: `{regenerate_cmd}` {end}""" return start, end, block def _update_single_file_instructions( workspace: Path, pydyna_path: Path, target_file: Path, regenerate_cmd: str, default_header: str, extended_docs: list[str] | None = None, ) -> Path: """Append/refresh a pointer section in a single-file instructions file.""" try: rel = pydyna_path.relative_to(workspace) pydyna_ref = str(rel).replace("\\", "/") except ValueError: pydyna_ref = str(pydyna_path).replace("\\", "/") start, end, block = _make_pointer_block(pydyna_ref, regenerate_cmd, extended_docs) if target_file.exists(): text = target_file.read_text(encoding="utf-8", errors="replace") if start in text and end in text and text.index(start) < text.index(end): pre = text[: text.index(start)].rstrip() post = text[text.index(end) + len(end) :].lstrip() if pre: new_text = f"{pre}\n\n{block}\n" else: new_text = f"{block}\n" if post: new_text += f"\n{post}" new_text = new_text.rstrip() + "\n" else: new_text = text.rstrip() + "\n\n" + block + "\n" else: target_file.parent.mkdir(parents=True, exist_ok=True) new_text = f"{default_header}\n\n{block}\n" target_file.write_text(new_text, encoding="utf-8") return target_file
[docs] def update_copilot_instructions( workspace: Path, pydyna_path: Path, mode: str, extended_docs: list[str] | None = None, ) -> Path: """Update .github/copilot-instructions.md with a pointer.""" github_dir = workspace / ".github" github_dir.mkdir(parents=True, exist_ok=True) copilot_file = github_dir / "copilot-instructions.md" return _update_single_file_instructions( workspace=workspace, pydyna_path=pydyna_path, target_file=copilot_file, regenerate_cmd=f"python -m ansys.dyna.core agent --env vscode --{mode}", default_header="# Copilot Instructions", extended_docs=extended_docs, )
[docs] def update_claude_instructions( workspace: Path, pydyna_path: Path, mode: str, extended_docs: list[str] | None = None, ) -> Path: """Update CLAUDE.md with a pointer.""" claude_file = workspace / "CLAUDE.md" return _update_single_file_instructions( workspace=workspace, pydyna_path=pydyna_path, target_file=claude_file, regenerate_cmd=f"python -m ansys.dyna.core agent --env claude --{mode}", default_header="# Claude Code Instructions", extended_docs=extended_docs, )
# ============================================================================= # Command Handlers # =============================================================================
[docs] def cmd_agent_copy(args: argparse.Namespace, workspace: Path) -> int: """Handle --copy mode: copy all files with working links.""" env = args.env if env == "cursor": # Cursor: single self-contained MDC file content = get_instructions_content() formatted = format_for_cursor(content, get_instructions_path()) cursor_dir = workspace / ".cursor" / "rules" cursor_dir.mkdir(parents=True, exist_ok=True) output_path = cursor_dir / "pydyna.mdc" output_path.write_text(formatted, encoding="utf-8") print(f"[OK] PyDyna instructions written to: {output_path}") return 0 # All other environments: copy to .agent/ with extended docs main_file, extended_docs = copy_all_instructions(workspace, mode="copy") ensure_gitignore(workspace) print(f"[OK] PyDyna instructions copied to: {main_file}") if extended_docs: print(f" Extended docs: {len(extended_docs)} files") print(f" Manifest updated: {workspace / '.agent' / 'manifest.json'}") # For single-file tools, also add pointer if env in ("vscode", "copilot"): pointer_file = update_copilot_instructions(workspace, main_file, "copy") print(f"[OK] Added pointer to: {pointer_file}") print(" (Your existing instructions were preserved)") elif env == "claude": pointer_file = update_claude_instructions(workspace, main_file, "copy") print(f"[OK] Added pointer to: {pointer_file}") print(" (Your existing instructions were preserved)") return 0
[docs] def cmd_agent_pointer(args: argparse.Namespace, workspace: Path) -> int: """Handle --pointer mode: just add pointer to installed package.""" env = args.env source_path = get_instructions_path() # Get extended doc paths relative to main file extended_dir = get_extended_docs_dir() extended_docs = [] if extended_dir.exists(): for doc in extended_dir.glob("*.md"): extended_docs.append(f"agent/{doc.name}") if env == "cursor": # Cursor doesn't support pointer mode well - use copy instead print("Note: Cursor works best with --copy mode. Using --copy.", file=sys.stderr) return cmd_agent_copy(args, workspace) # Register in manifest even for pointer mode register_pointer_in_manifest(workspace, mode="pointer") ensure_gitignore(workspace) if env in ("vscode", "copilot"): pointer_file = update_copilot_instructions(workspace, source_path, "pointer", extended_docs) print(f"[OK] Added pointer to installed package in: {pointer_file}") print(f" Points to: {source_path}") print(f" Manifest updated: {workspace / '.agent' / 'manifest.json'}") print(" (Your existing instructions were preserved)") elif env == "claude": pointer_file = update_claude_instructions(workspace, source_path, "pointer", extended_docs) print(f"[OK] Added pointer to installed package in: {pointer_file}") print(f" Points to: {source_path}") print(f" Manifest updated: {workspace / '.agent' / 'manifest.json'}") print(" (Your existing instructions were preserved)") elif env == "generic": print("PyDyna agent instructions are installed at:") print(f" {source_path}") if extended_docs: print("\nExtended documentation:") for doc in extended_docs: print(f" {source_path.parent / doc}") print(f"\nManifest updated: {workspace / '.agent' / 'manifest.json'}") return 0
[docs] def cmd_agent(args: argparse.Namespace) -> int: """Handle 'agent' subcommand.""" content = get_instructions_content() # Print mode if args.print: try: print(content) except UnicodeEncodeError: sys.stdout.buffer.write(content.encode("utf-8")) return 0 # Find workspace if args.workspace: workspace = Path(args.workspace) else: workspace = find_workspace_root() if workspace is None: print("Error: Could not detect workspace root.", file=sys.stderr) print("Run from a git repo or specify --workspace PATH", file=sys.stderr) return 1 # Determine mode if args.pointer: return cmd_agent_pointer(args, workspace) else: # Default to copy mode return cmd_agent_copy(args, workspace)
[docs] def main() -> int: """Main entry point for CLI.""" parser = argparse.ArgumentParser( prog="python -m ansys.dyna.core", description="PyDyna CLI utilities", ) subparsers = parser.add_subparsers(dest="command") # agent subcommand agent_parser = subparsers.add_parser( "agent", help="Install agent instructions for AI assistants", ) agent_parser.add_argument( "--env", choices=["cursor", "vscode", "copilot", "claude", "generic"], default="generic", help="Target environment (default: generic)", ) # Mode selection (mutually exclusive) mode_group = agent_parser.add_mutually_exclusive_group() mode_group.add_argument( "--copy", action="store_true", help="Copy instruction files to workspace (default). Works with sandboxed agents.", ) mode_group.add_argument( "--pointer", action="store_true", help="Just add pointer to installed package. Requires agent has file system access.", ) agent_parser.add_argument( "--print", action="store_true", help="Print to stdout instead of writing file", ) agent_parser.add_argument( "--workspace", help="Workspace root (auto-detected if not specified)", ) args = parser.parse_args() if args.command == "agent": return cmd_agent(args) else: parser.print_help() return 0
if __name__ == "__main__": sys.exit(main())