diff --git a/src/agentex/lib/cli/commands/init.py b/src/agentex/lib/cli/commands/init.py index 9849e9bbc..a0ca5020b 100644 --- a/src/agentex/lib/cli/commands/init.py +++ b/src/agentex/lib/cli/commands/init.py @@ -99,6 +99,7 @@ def create_project_structure( # Create root files root_templates = { ".dockerignore.j2": ".dockerignore", + ".gitignore.j2": ".gitignore", ".env.example.j2": ".env.example", "manifest.yaml.j2": "manifest.yaml", "README.md.j2": "README.md", @@ -118,7 +119,17 @@ def create_project_structure( for template, output in root_templates.items(): output_path = project_dir / output - output_path.write_text(render_template(template, context, template_type)) + rendered = render_template(template, context, template_type) + if output == ".gitignore" and output_path.exists(): + # Re-running init on an existing project: keep the user's rules and + # append only the scaffold entries that are missing. + existing = output_path.read_text() + existing_lines = {line.strip() for line in existing.splitlines()} + missing = [line for line in rendered.splitlines() if line.strip() and not line.startswith("#") and line.strip() not in existing_lines] + if missing: + output_path.write_text(existing.rstrip("\n") + "\n\n# Added by agentex init\n" + "\n".join(missing) + "\n") + continue + output_path.write_text(rendered) console.print(f"\n[green]✓[/green] Created project structure at: {project_dir}") diff --git a/src/agentex/lib/cli/templates/default-claude-code/.gitignore.j2 b/src/agentex/lib/cli/templates/default-claude-code/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/default-claude-code/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/default-codex/.gitignore.j2 b/src/agentex/lib/cli/templates/default-codex/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/default-codex/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/default-langgraph/.gitignore.j2 b/src/agentex/lib/cli/templates/default-langgraph/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/default-langgraph/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/default-openai-agents/.gitignore.j2 b/src/agentex/lib/cli/templates/default-openai-agents/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/default-openai-agents/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/default-pydantic-ai/.gitignore.j2 b/src/agentex/lib/cli/templates/default-pydantic-ai/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/default-pydantic-ai/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/default/.gitignore.j2 b/src/agentex/lib/cli/templates/default/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/default/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/sync-claude-code/.gitignore.j2 b/src/agentex/lib/cli/templates/sync-claude-code/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-claude-code/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/sync-codex/.gitignore.j2 b/src/agentex/lib/cli/templates/sync-codex/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-codex/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/sync-langgraph/.gitignore.j2 b/src/agentex/lib/cli/templates/sync-langgraph/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-langgraph/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/.gitignore.j2 b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/.gitignore.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-openai-agents/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/sync-pydantic-ai/.gitignore.j2 b/src/agentex/lib/cli/templates/sync-pydantic-ai/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/sync-pydantic-ai/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/sync/.gitignore.j2 b/src/agentex/lib/cli/templates/sync/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/sync/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/temporal-claude-code/.gitignore.j2 b/src/agentex/lib/cli/templates/temporal-claude-code/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/temporal-claude-code/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/temporal-codex/.gitignore.j2 b/src/agentex/lib/cli/templates/temporal-codex/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/temporal-codex/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/temporal-langgraph/.gitignore.j2 b/src/agentex/lib/cli/templates/temporal-langgraph/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/temporal-langgraph/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/temporal-openai-agents/.gitignore.j2 b/src/agentex/lib/cli/templates/temporal-openai-agents/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/temporal-openai-agents/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/temporal-pydantic-ai/.gitignore.j2 b/src/agentex/lib/cli/templates/temporal-pydantic-ai/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/temporal-pydantic-ai/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/src/agentex/lib/cli/templates/temporal/.gitignore.j2 b/src/agentex/lib/cli/templates/temporal/.gitignore.j2 new file mode 100644 index 000000000..661b1317e --- /dev/null +++ b/src/agentex/lib/cli/templates/temporal/.gitignore.j2 @@ -0,0 +1,20 @@ +# Secrets and local config +.env +.env.* +!.env.example + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +build/ +dist/ + +# Tooling +.ipynb_checkpoints/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.DS_Store diff --git a/tests/lib/cli/test_init_templates.py b/tests/lib/cli/test_init_templates.py index ec809cbbf..c61571f5a 100644 --- a/tests/lib/cli/test_init_templates.py +++ b/tests/lib/cli/test_init_templates.py @@ -137,3 +137,29 @@ def test_requirements_include_langgraph_plugin_and_temporal(self, tmp_path: Path requirements = (project_dir / "requirements.txt").read_text() assert "temporalio[langgraph]>=1.27.0" in requirements assert "langchain-openai" in requirements + + +@pytest.mark.parametrize("template_type", list(TemplateType)) +def test_all_templates_ship_a_gitignore_that_excludes_env(tmp_path: Path, template_type: TemplateType): + """Every scaffold ships a .gitignore so .env (API keys) and .venv are never committed.""" + project_dir = _render_project(tmp_path, template_type) + gitignore = project_dir / ".gitignore" + assert gitignore.is_file(), f"{template_type.value} did not render .gitignore" + lines = gitignore.read_text().splitlines() + assert ".env" in lines and ".venv/" in lines + assert "!.env.example" in lines, "the example env file should stay committable" + + +def test_rerunning_init_preserves_existing_gitignore_rules(tmp_path: Path): + """A pre-existing .gitignore keeps its own rules; missing scaffold entries are appended.""" + context = _context(TemplateType.SYNC) + project_dir = tmp_path / context["project_name"] + project_dir.mkdir(parents=True) + (project_dir / ".gitignore").write_text("# mine\n*.log\n.env\n") + + create_project_structure(tmp_path, context, TemplateType.SYNC, use_uv=True) + + lines = (project_dir / ".gitignore").read_text().splitlines() + assert "*.log" in lines, "user rule was dropped" + assert lines.count(".env") == 1, "existing entry duplicated" + assert ".venv/" in lines and "!.env.example" in lines, "scaffold entries not appended"