Skip to content

CLI Internal Modules

codex_django_cli.main

Top-level entrypoint for the interactive codex-django CLI.

codex_django_cli.engine

Template rendering and file generation engine for the CLI.

Examples:

Render a blueprint template manually::

from codex_django_cli.engine import CLIEngine

engine = CLIEngine()
content = engine.render_template("repo/README.md.j2", {"project_name": "demo"})

Classes

CLIEngine

Core engine for CLI template rendering and scaffold generation.

Source code in src/codex_django_cli/engine.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
class CLIEngine:
    """Core engine for CLI template rendering and scaffold generation."""

    def __init__(self, blueprints_dir: str | None = None):
        """Initialize the engine and Jinja environment.

        Args:
            blueprints_dir: Optional path to the blueprint root. Defaults to
                the bundled ``blueprints`` directory next to this module.
        """
        if blueprints_dir is None:
            # Default to the blueprints directory relative to this file
            blueprints_dir = os.path.join(os.path.dirname(__file__), "blueprints")
        self.blueprints_dir = blueprints_dir
        self.env = Environment(
            loader=FileSystemLoader(self.blueprints_dir),
            autoescape=select_autoescape(),
            keep_trailing_newline=True,
        )
        self.logger = logging.getLogger(__name__)

    def render_template(self, template_name: str, context: dict[str, Any]) -> str:
        """Render a blueprint template with the provided context.

        Args:
            template_name: Template path relative to the blueprint root.
            context: Render context passed into Jinja.

        Returns:
            Rendered template text.

        Raises:
            ValueError: If the template cannot be found.
        """
        try:
            template = self.env.get_template(template_name)
        except TemplateNotFound as exc:
            raise ValueError(f"Template not found: {template_name}") from exc
        return template.render(**context)

    def scaffold(self, blueprint_name: str, target_dir: str, context: dict[str, Any], overwrite: bool = False) -> None:
        """Copy a blueprint tree to the target directory.

        The method renders ``.j2`` files through Jinja and copies all other
        files verbatim.

        Args:
            blueprint_name: Blueprint path relative to the blueprint root.
            target_dir: Filesystem directory where files should be written.
            context: Render context passed into Jinja templates.
            overwrite: Whether existing files may be replaced.

        Raises:
            ValueError: If the named blueprint directory does not exist.
        """
        source_dir = os.path.join(self.blueprints_dir, blueprint_name)
        if not os.path.exists(source_dir):
            raise ValueError(f"Blueprint '{blueprint_name}' not found in {self.blueprints_dir}")

        for root, _, files in os.walk(source_dir):
            # Calculate relative path from source_dir
            rel_path = os.path.relpath(root, source_dir)

            # Destination directory
            dest_dir = os.path.normpath(os.path.join(target_dir, rel_path))
            os.makedirs(dest_dir, exist_ok=True)

            # Prefer templated sources when both `foo` and `foo.j2` exist.
            # This keeps scaffold output deterministic and renders the variant.
            preferred_files: dict[str, str] = {}
            for file in sorted(files):
                output_name = file[:-3] if file.endswith(".j2") else file
                current = preferred_files.get(output_name)
                if current is None or file.endswith(".j2"):
                    preferred_files[output_name] = file

            for dest_file_name, file in preferred_files.items():
                source_file = os.path.join(root, file)

                # Determine destination filename
                is_template = bool(file.endswith(".j2"))

                dest_file_path = os.path.join(dest_dir, dest_file_name)

                if os.path.exists(dest_file_path) and not overwrite:
                    self.logger.info(f"Skipping existing file: {dest_file_path}")
                    continue

                if is_template:
                    # Template path relative to blueprints_dir
                    rel_template_path = os.path.relpath(source_file, self.blueprints_dir).replace("\\", "/")
                    content = self.render_template(rel_template_path, context)
                    with open(dest_file_path, "w", encoding="utf-8") as f:
                        f.write(content)
                else:
                    shutil.copy2(source_file, dest_file_path)

                self.logger.info(f"Generated: {dest_file_path}")
Functions
__init__(blueprints_dir=None)

Initialize the engine and Jinja environment.

Parameters:

Name Type Description Default
blueprints_dir str | None

Optional path to the blueprint root. Defaults to the bundled blueprints directory next to this module.

None
Source code in src/codex_django_cli/engine.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def __init__(self, blueprints_dir: str | None = None):
    """Initialize the engine and Jinja environment.

    Args:
        blueprints_dir: Optional path to the blueprint root. Defaults to
            the bundled ``blueprints`` directory next to this module.
    """
    if blueprints_dir is None:
        # Default to the blueprints directory relative to this file
        blueprints_dir = os.path.join(os.path.dirname(__file__), "blueprints")
    self.blueprints_dir = blueprints_dir
    self.env = Environment(
        loader=FileSystemLoader(self.blueprints_dir),
        autoescape=select_autoescape(),
        keep_trailing_newline=True,
    )
    self.logger = logging.getLogger(__name__)
render_template(template_name, context)

Render a blueprint template with the provided context.

Parameters:

Name Type Description Default
template_name str

Template path relative to the blueprint root.

required
context dict[str, Any]

Render context passed into Jinja.

required

Returns:

Type Description
str

Rendered template text.

Raises:

Type Description
ValueError

If the template cannot be found.

Source code in src/codex_django_cli/engine.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def render_template(self, template_name: str, context: dict[str, Any]) -> str:
    """Render a blueprint template with the provided context.

    Args:
        template_name: Template path relative to the blueprint root.
        context: Render context passed into Jinja.

    Returns:
        Rendered template text.

    Raises:
        ValueError: If the template cannot be found.
    """
    try:
        template = self.env.get_template(template_name)
    except TemplateNotFound as exc:
        raise ValueError(f"Template not found: {template_name}") from exc
    return template.render(**context)
scaffold(blueprint_name, target_dir, context, overwrite=False)

Copy a blueprint tree to the target directory.

The method renders .j2 files through Jinja and copies all other files verbatim.

Parameters:

Name Type Description Default
blueprint_name str

Blueprint path relative to the blueprint root.

required
target_dir str

Filesystem directory where files should be written.

required
context dict[str, Any]

Render context passed into Jinja templates.

required
overwrite bool

Whether existing files may be replaced.

False

Raises:

Type Description
ValueError

If the named blueprint directory does not exist.

Source code in src/codex_django_cli/engine.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def scaffold(self, blueprint_name: str, target_dir: str, context: dict[str, Any], overwrite: bool = False) -> None:
    """Copy a blueprint tree to the target directory.

    The method renders ``.j2`` files through Jinja and copies all other
    files verbatim.

    Args:
        blueprint_name: Blueprint path relative to the blueprint root.
        target_dir: Filesystem directory where files should be written.
        context: Render context passed into Jinja templates.
        overwrite: Whether existing files may be replaced.

    Raises:
        ValueError: If the named blueprint directory does not exist.
    """
    source_dir = os.path.join(self.blueprints_dir, blueprint_name)
    if not os.path.exists(source_dir):
        raise ValueError(f"Blueprint '{blueprint_name}' not found in {self.blueprints_dir}")

    for root, _, files in os.walk(source_dir):
        # Calculate relative path from source_dir
        rel_path = os.path.relpath(root, source_dir)

        # Destination directory
        dest_dir = os.path.normpath(os.path.join(target_dir, rel_path))
        os.makedirs(dest_dir, exist_ok=True)

        # Prefer templated sources when both `foo` and `foo.j2` exist.
        # This keeps scaffold output deterministic and renders the variant.
        preferred_files: dict[str, str] = {}
        for file in sorted(files):
            output_name = file[:-3] if file.endswith(".j2") else file
            current = preferred_files.get(output_name)
            if current is None or file.endswith(".j2"):
                preferred_files[output_name] = file

        for dest_file_name, file in preferred_files.items():
            source_file = os.path.join(root, file)

            # Determine destination filename
            is_template = bool(file.endswith(".j2"))

            dest_file_path = os.path.join(dest_dir, dest_file_name)

            if os.path.exists(dest_file_path) and not overwrite:
                self.logger.info(f"Skipping existing file: {dest_file_path}")
                continue

            if is_template:
                # Template path relative to blueprints_dir
                rel_template_path = os.path.relpath(source_file, self.blueprints_dir).replace("\\", "/")
                content = self.render_template(rel_template_path, context)
                with open(dest_file_path, "w", encoding="utf-8") as f:
                    f.write(content)
            else:
                shutil.copy2(source_file, dest_file_path)

            self.logger.info(f"Generated: {dest_file_path}")

codex_django_cli.prompts

Thin wrappers around questionary prompts.

codex_django_cli.utils

Shared helper functions for CLI command execution and bootstrap tasks.

Functions

generate_secret_key()

Generate a Django-compatible SECRET_KEY value.

Source code in src/codex_django_cli/utils.py
11
12
13
14
15
def generate_secret_key() -> str:
    """Generate a Django-compatible SECRET_KEY value."""
    from secrets import token_urlsafe

    return token_urlsafe(50)

generate_field_encryption_key()

Generate a Fernet-compatible key for encrypted model fields.

Source code in src/codex_django_cli/utils.py
18
19
20
def generate_field_encryption_key() -> str:
    """Generate a Fernet-compatible key for encrypted model fields."""
    return base64.urlsafe_b64encode(os.urandom(32)).decode()

run_django_command(args)

Execute a Django management command in-process.

Parameters:

Name Type Description Default
args list[str]

Command arguments without the leading program name.

required
Source code in src/codex_django_cli/utils.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def run_django_command(args: list[str]) -> None:
    """Execute a Django management command in-process.

    Args:
        args: Command arguments without the leading program name.
    """
    management = import_module("django.core.management")
    execute_from_command_line = cast(Any, management.execute_from_command_line)

    # The first argument to execute_from_command_line should be the program name.
    full_args = ["manage.py", *args]
    try:
        execute_from_command_line(full_args)
    except SystemExit as e:
        # Django's execute_from_command_line often calls sys.exit().
        if e.code != 0:
            print(f"\n[red]Command failed with exit code: {e.code}[/red]")
    except Exception as e:
        print(f"\n[red]Error executing command: {e}[/red]")

codex_django_cli.commands.init

CLI handler for project initialization.

Functions

handle_init(name, base_dir, target_dir=None, code_only=False, dev_mode=False, overwrite=False, enable_i18n=False, languages=None, with_cabinet=True, with_booking=False, with_conversations=True, with_public_booking=False, with_sw=False, with_cloud_db=False)

Scaffold a new codex-django project using module-selection orchestration.

Source code in src/codex_django_cli/commands/init.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def handle_init(
    name: str,
    base_dir: str,
    target_dir: str | None = None,
    code_only: bool = False,
    dev_mode: bool = False,
    overwrite: bool = False,
    enable_i18n: bool = False,
    languages: list[str] | None = None,
    with_cabinet: bool = True,
    with_booking: bool = False,
    with_conversations: bool = True,
    with_public_booking: bool = False,
    with_sw: bool = False,
    with_cloud_db: bool = False,
) -> None:
    """Scaffold a new codex-django project using module-selection orchestration."""
    selection = InstallSelection(
        cabinet=with_cabinet,
        booking=with_booking,
        conversations=with_conversations,
        public_booking=with_public_booking,
        sw=with_sw,
        code_only=code_only,
        overwrite=overwrite,
        dev_mode=dev_mode,
        i18n=enable_i18n,
        cloud_db=with_cloud_db,
    )

    if enable_i18n and languages and len(languages) == 1:
        console.print("[cyan]Using translation-aware settings with a single selected language.[/cyan]")

    scaffold_new_project(
        name=name,
        base_dir=base_dir,
        target_dir=target_dir,
        selection=selection,
        languages=languages,
    )

codex_django_cli.commands.install

Installation orchestrator for menu-first project scaffolding.

codex_django_cli.commands.repo

Helpers for generating repository-level config files.

Classes

Functions

handle_generate_repo_config(*, name, project_root, include_pyproject=True, include_env_example=True, overwrite=False)

Generate repository-level config files for a project.

Source code in src/codex_django_cli/commands/repo.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def handle_generate_repo_config(
    *,
    name: str,
    project_root: str,
    include_pyproject: bool = True,
    include_env_example: bool = True,
    overwrite: bool = False,
) -> None:
    """Generate repository-level config files for a project."""
    engine = CLIEngine()
    context = {"project_name": name}

    if include_pyproject:
        pyproject_path, is_compare = _resolve_output_path(
            project_root,
            "pyproject.toml",
            f"pyproject.{name}.toml",
            overwrite=overwrite,
        )
        _write_rendered_file(pyproject_path, engine.render_template("repo/pyproject.toml.j2", context))
        label = "comparison file" if is_compare else "file"
        console.print(f"[green]✓[/green] Generated pyproject {label}: [bold]{pyproject_path}[/bold]")

    if include_env_example:
        env_example_path, is_compare = _resolve_output_path(
            project_root,
            ".env.example",
            f".env.{name}.example",
            overwrite=overwrite,
        )
        _write_rendered_file(env_example_path, engine.render_template("repo/.env.example.j2", context))
        label = "comparison file" if is_compare else "file"
        console.print(f"[green]✓[/green] Generated env example {label}: [bold]{env_example_path}[/bold]")

codex_django_cli.commands.deploy

handle_generate_deploy

Generates Docker infrastructure and CI/CD files for a project.

Supports two deployment modes
  • standalone: one project = full stack (nginx, redis, postgres, worker, bot)
  • stack: multiple projects sharing nginx, redis, arq worker

Classes

Functions

handle_generate_deploy(name, project_root, deploy_mode='standalone', domain_name='example.com', with_bot=False, with_worker=False, with_notifications=False, enable_i18n=False, cluster_name=None, python_version='3.13', generate_docker=True, generate_cicd=True)

Generate deploy infrastructure files and CI/CD workflows.

Parameters:

Name Type Description Default
name str

Project name used in templates and generated paths.

required
project_root str

Repository root where deploy assets should be written.

required
deploy_mode str

Deployment topology, for example standalone or stack.

'standalone'
domain_name str

Public domain used in generated configs.

'example.com'
with_bot bool

Whether Telegram bot assets should be included.

False
with_worker bool

Whether worker assets should be included.

False
with_notifications bool

Whether notification-related deploy config is needed.

False
enable_i18n bool

Whether i18n-aware routing/templates should influence templates.

False
cluster_name str | None

Optional cluster identifier for shared-stack mode.

None
python_version str

Python version string injected into templates.

'3.13'
generate_docker bool

Whether Docker assets should be generated.

True
generate_cicd bool

Whether CI/CD workflow files should be generated.

True
Source code in src/codex_django_cli/commands/deploy.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def handle_generate_deploy(
    name: str,
    project_root: str,
    deploy_mode: str = "standalone",
    domain_name: str = "example.com",
    with_bot: bool = False,
    with_worker: bool = False,
    with_notifications: bool = False,
    enable_i18n: bool = False,
    cluster_name: str | None = None,
    python_version: str = "3.13",
    generate_docker: bool = True,
    generate_cicd: bool = True,
) -> None:
    """Generate deploy infrastructure files and CI/CD workflows.

    Args:
        name: Project name used in templates and generated paths.
        project_root: Repository root where deploy assets should be written.
        deploy_mode: Deployment topology, for example ``standalone`` or ``stack``.
        domain_name: Public domain used in generated configs.
        with_bot: Whether Telegram bot assets should be included.
        with_worker: Whether worker assets should be included.
        with_notifications: Whether notification-related deploy config is needed.
        enable_i18n: Whether i18n-aware routing/templates should influence templates.
        cluster_name: Optional cluster identifier for shared-stack mode.
        python_version: Python version string injected into templates.
        generate_docker: Whether Docker assets should be generated.
        generate_cicd: Whether CI/CD workflow files should be generated.
    """
    engine = CLIEngine()

    context = {
        "project_name": name,
        "secret_key": token_urlsafe(50),
        "domain_name": domain_name,
        "python_version": python_version,
        "with_bot": with_bot,
        "with_worker": with_worker,
        "with_notifications": with_notifications,
        "enable_i18n": enable_i18n,
        "deploy_mode": deploy_mode,
        "cluster_name": cluster_name or name,
    }

    deploy_dir = os.path.join(project_root, "deploy")
    workflows_dir = os.path.join(project_root, ".github", "workflows")

    if generate_docker:
        engine.scaffold("deploy/shared", target_dir=deploy_dir, context=context, overwrite=True)
        engine.scaffold(f"deploy/{deploy_mode}", target_dir=deploy_dir, context=context, overwrite=True)
        console.print(f"[green]✓[/green] Docker files generated in [bold]{deploy_dir}[/bold]")

    if generate_cicd:
        engine.scaffold(
            f"deploy/{deploy_mode}_workflows",
            target_dir=workflows_dir,
            context=context,
            overwrite=True,
        )
        console.print(f"[green]✓[/green] CI/CD workflows generated in [bold]{workflows_dir}[/bold]")

    console.print()
    console.print("[bold]Next steps:[/bold]")
    if deploy_mode == "standalone":
        console.print("  1. [cyan]cp .env.example .env[/cyan]  # fill in secrets")
        console.print("  2. [cyan]cd deploy && docker compose up -d[/cyan]  # local dev")
        console.print("  3. Add GitHub Secrets: HOST, USERNAME, SSH_KEY, ENV_FILE, DOMAIN_NAME, REDIS_PASSWORD")
        console.print("  4. Push tag to trigger production deploy: [cyan]git tag v1.0.0 && git push --tags[/cyan]")
    else:
        console.print("  1. [cyan]cp .env.example .env[/cyan]  # fill in shared secrets")
        console.print(
            "  2. [cyan]cd deploy && docker compose -f docker-compose.infra.yml up -d[/cyan]  # start shared infra"
        )
        console.print("  3. [cyan]docker compose -f docker-compose.apps.yml up -d[/cyan]  # start this project")
        console.print("  4. Add GitHub Secrets and configure deploy-cluster.yml for your VPS")

codex_django_cli.commands.quality

handle_configure_precommit

Generates a .pre-commit-config.yaml with standard quality checks.

Functions

handle_configure_precommit(project_root)

Write a standard pre-commit configuration into a project root.

Parameters:

Name Type Description Default
project_root str

Repository root where config files should be created.

required
Source code in src/codex_django_cli/commands/quality.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def handle_configure_precommit(project_root: str) -> None:
    """Write a standard pre-commit configuration into a project root.

    Args:
        project_root: Repository root where config files should be created.
    """
    config_path = os.path.join(project_root, ".pre-commit-config.yaml")

    with open(config_path, "w", encoding="utf-8") as f:
        f.write(PRE_COMMIT_CONFIG)

    # Create empty .secrets.baseline if it doesn't exist to avoid hook errors
    baseline_path = os.path.join(project_root, ".secrets.baseline")
    if not os.path.exists(baseline_path):
        import json

        empty_baseline = {
            "version": "1.5.0",
            "plugins_used": [
                {"name": "ArtifactoryDetector"},
                {"name": "AWSKeyDetector"},
                {"name": "Base64HighEntropyString", "limit": 4.5},
                {"name": "BasicAuthDetector"},
                {"name": "CloudantDetector"},
                {"name": "HexHighEntropyString", "limit": 3.0},
                {"name": "IbmCloudIamDetector"},
                {"name": "IbmCosHmacDetector"},
                {"name": "JwtTokenDetector"},
                {"name": "KeywordDetector", "keyword_exclude": ""},
                {"name": "MailchimpDetector"},
                {"name": "NpmDetector"},
                {"name": "PrivateKeyDetector"},
                {"name": "SlackDetector"},
                {"name": "SoftlayerDetector"},
                {"name": "StripeDetector"},
                {"name": "TwilioKeyDetector"},
            ],
            "results": {},
            "exclude": {"files": ".*", "lines": None},
        }
        with open(baseline_path, "w", encoding="utf-8") as f:
            json.dump(empty_baseline, f, indent=2)

    console.print("[green]✓[/green] Created [bold].pre-commit-config.yaml[/bold]")
    console.print("\n[bold]To finish setup, run:[/bold]")
    console.print("  [cyan]pip install pre-commit[/cyan]")
    console.print("  [cyan]pre-commit install[/cyan]\n")

codex_django_cli.dev.project_tree

Project structure tree generator.

Scans a project directory and writes a human-readable tree to a text file. Interactive mode lets the user pick a top-level folder or the full project.

Usage (in a project's tools/dev/generate_project_tree.py):

from pathlib import Path
from codex_django_cli.dev.project_tree import ProjectTreeGenerator

if __name__ == "__main__":
    ProjectTreeGenerator(Path(__file__).parent.parent.parent).interactive()

Classes

ProjectTreeGenerator

Source code in src/codex_django_cli/dev/project_tree.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
class ProjectTreeGenerator:
    DEFAULT_IGNORE_DIRS: frozenset[str] = frozenset(
        {
            ".git",
            ".github",
            "venv",
            ".venv",
            "__pycache__",
            ".idea",
            ".vscode",
            "data",
            "logs",
            ".pytest_cache",
            ".mypy_cache",
            ".ruff_cache",
            ".gemini",
            "node_modules",
            "site-packages",
            "site",
        }
    )
    DEFAULT_IGNORE_EXTENSIONS: frozenset[str] = frozenset(
        {
            ".pyc",
            ".png",
            ".jpg",
            ".jpeg",
            ".gif",
            ".svg",
            ".db",
            ".sqlite3",
            ".ico",
            ".woff",
            ".woff2",
        }
    )

    def __init__(
        self,
        root: Path,
        ignore_dirs: frozenset[str] | None = None,
        ignore_extensions: frozenset[str] | None = None,
    ) -> None:
        self.root = root.resolve()
        self.ignore_dirs = ignore_dirs or self.DEFAULT_IGNORE_DIRS
        self.ignore_extensions = ignore_extensions or self.DEFAULT_IGNORE_EXTENSIONS

    def _top_level_dirs(self) -> list[str]:
        return sorted(d for d in os.listdir(self.root) if os.path.isdir(self.root / d) and d not in self.ignore_dirs)

    def generate(self, target_dir: str | None, output: Path) -> None:
        """Write tree to output file.

        Args:
            target_dir: Sub-directory name to scan, or None for the full project.
            output: Path to the output .txt file.
        """
        start = self.root / target_dir if target_dir else self.root
        title = f"Project Structure: {target_dir or 'Full Project'}"

        with open(output, "w", encoding="utf-8") as f:
            f.write(f"{title}\n{'=' * len(title)}\n\n")

            for current_root, dirs, files in os.walk(start, topdown=True):
                dirs[:] = sorted(d for d in dirs if d not in self.ignore_dirs)

                rel = os.path.relpath(current_root, start)
                if rel == ".":
                    level = 0
                    name = os.path.basename(start)
                else:
                    level = rel.count(os.sep) + 1
                    name = os.path.basename(current_root)

                indent = "    " * level
                f.write(f"{indent}📂 {name}/\n")

                sub = "    " * (level + 1)
                for file in sorted(files):
                    if not any(file.endswith(ext) for ext in self.ignore_extensions):
                        f.write(f"{sub}📄 {file}\n")

    def interactive(self, output: Path | None = None) -> None:
        """Show interactive folder selection menu and generate tree."""
        output = output or (self.root / "project_structure.txt")
        top_dirs = self._top_level_dirs()

        print(f"\n🔍 Project root: {self.root}")
        print("Select scope:\n")
        print("   0. 🌳 Full project")
        for idx, folder in enumerate(top_dirs, 1):
            print(f"   {idx}. 📁 {folder}/")

        while True:
            try:
                choice = input(f"\nEnter number (0-{len(top_dirs)}): ").strip()
                if not choice.isdigit():
                    raise ValueError
                idx = int(choice)
                if 0 <= idx <= len(top_dirs):
                    break
                print("Invalid number, try again.")
            except ValueError:
                print("Enter a number.")

        target = top_dirs[idx - 1] if idx > 0 else None
        label = target or "Full project"
        print(f"\n⚙️  Generating: {label}...")
        self.generate(target, output)
        print(f"✅ Saved to: {output}")
Functions
generate(target_dir, output)

Write tree to output file.

Parameters:

Name Type Description Default
target_dir str | None

Sub-directory name to scan, or None for the full project.

required
output Path

Path to the output .txt file.

required
Source code in src/codex_django_cli/dev/project_tree.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def generate(self, target_dir: str | None, output: Path) -> None:
    """Write tree to output file.

    Args:
        target_dir: Sub-directory name to scan, or None for the full project.
        output: Path to the output .txt file.
    """
    start = self.root / target_dir if target_dir else self.root
    title = f"Project Structure: {target_dir or 'Full Project'}"

    with open(output, "w", encoding="utf-8") as f:
        f.write(f"{title}\n{'=' * len(title)}\n\n")

        for current_root, dirs, files in os.walk(start, topdown=True):
            dirs[:] = sorted(d for d in dirs if d not in self.ignore_dirs)

            rel = os.path.relpath(current_root, start)
            if rel == ".":
                level = 0
                name = os.path.basename(start)
            else:
                level = rel.count(os.sep) + 1
                name = os.path.basename(current_root)

            indent = "    " * level
            f.write(f"{indent}📂 {name}/\n")

            sub = "    " * (level + 1)
            for file in sorted(files):
                if not any(file.endswith(ext) for ext in self.ignore_extensions):
                    f.write(f"{sub}📄 {file}\n")
interactive(output=None)

Show interactive folder selection menu and generate tree.

Source code in src/codex_django_cli/dev/project_tree.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def interactive(self, output: Path | None = None) -> None:
    """Show interactive folder selection menu and generate tree."""
    output = output or (self.root / "project_structure.txt")
    top_dirs = self._top_level_dirs()

    print(f"\n🔍 Project root: {self.root}")
    print("Select scope:\n")
    print("   0. 🌳 Full project")
    for idx, folder in enumerate(top_dirs, 1):
        print(f"   {idx}. 📁 {folder}/")

    while True:
        try:
            choice = input(f"\nEnter number (0-{len(top_dirs)}): ").strip()
            if not choice.isdigit():
                raise ValueError
            idx = int(choice)
            if 0 <= idx <= len(top_dirs):
                break
            print("Invalid number, try again.")
        except ValueError:
            print("Enter a number.")

    target = top_dirs[idx - 1] if idx > 0 else None
    label = target or "Full project"
    print(f"\n⚙️  Generating: {label}...")
    self.generate(target, output)
    print(f"✅ Saved to: {output}")