소스 검색

Merge PR #1613: Add Status column to template list

xcad 1 개월 전
부모
커밋
226b6895e3
5개의 변경된 파일126개의 추가작업 그리고 15개의 파일을 삭제
  1. 16 0
      cli/core/exceptions.py
  2. 22 11
      cli/core/library.py
  3. 53 3
      cli/core/module/base_commands.py
  4. 12 1
      cli/core/template/__init__.py
  5. 23 0
      cli/core/template/template.py

+ 16 - 0
cli/core/exceptions.py

@@ -45,6 +45,22 @@ class TemplateNotFoundError(TemplateError):
         super().__init__(msg)
 
 
+class TemplateDraftError(TemplateError):
+    """Raised when attempting to use a draft template."""
+
+    def __init__(self, template_id: str, module_name: str | None = None):
+        self.template_id = template_id
+        self.module_name = module_name
+
+        module_suffix = f" in module '{module_name}'" if module_name else ""
+        msg = (
+            f"Template '{template_id}' is in draft mode and not yet available for use{module_suffix}.\n"
+            "Draft templates are work-in-progress and cannot be generated yet.\n"
+            "To get updates when published, run 'boilerplates repo update' to sync your library."
+        )
+        super().__init__(msg)
+
+
 class DuplicateTemplateError(TemplateError):
     """Raised when duplicate template IDs are found within the same library."""
 

+ 22 - 11
cli/core/library.py

@@ -6,7 +6,7 @@ from pathlib import Path
 import yaml
 
 from .config import ConfigManager
-from .exceptions import DuplicateTemplateError, LibraryError, TemplateNotFoundError
+from .exceptions import DuplicateTemplateError, LibraryError, TemplateDraftError, TemplateNotFoundError
 
 logger = logging.getLogger(__name__)
 
@@ -53,17 +53,22 @@ class Library:
             return False
 
     def find_by_id(self, module_name: str, template_id: str) -> tuple[Path, str]:
-        """Find a template by its ID in this library.
+        """Find a template by its ID in this library for generation/show operations.
+
+        Note: Draft templates are intentionally excluded from this method.
+        They are visible in list/search commands (via find()) but cannot be
+        used for generation as they are work-in-progress.
 
         Args:
             module_name: The module name (e.g., 'compose', 'terraform')
             template_id: The template ID to find
 
         Returns:
-            Path to the template directory if found
+            Path to the template directory if found and not draft
 
         Raises:
-            FileNotFoundError: If the template ID is not found in this library or is marked as draft
+            TemplateDraftError: If the template exists but is marked as draft
+            TemplateNotFoundError: If the template ID is not found in this library
         """
         logger.debug(f"Looking for template '{template_id}' in module '{module_name}' in library '{self.name}'")
 
@@ -75,23 +80,28 @@ class Library:
             (template_path / f).exists() for f in ("template.yaml", "template.yml")
         )
 
-        if not has_template or self._is_template_draft(template_path):
+        # Template not found at all
+        if not has_template:
             raise TemplateNotFoundError(template_id, module_name)
 
+        # Template exists but is in draft mode
+        if self._is_template_draft(template_path):
+            raise TemplateDraftError(template_id, module_name)
+
         logger.debug(f"Found template '{template_id}' at: {template_path}")
         return template_path, self.name
 
     def find(self, module_name: str, sort_results: bool = False) -> list[tuple[Path, str]]:
         """Find templates in this library for a specific module.
 
-        Excludes templates marked as draft.
+        Includes all templates (both published and draft).
 
         Args:
             module_name: The module name (e.g., 'compose', 'terraform')
             sort_results: Whether to return results sorted alphabetically
 
         Returns:
-            List of Path objects representing template directories (excluding drafts)
+            List of Path objects representing template directories (including drafts)
 
         Raises:
             FileNotFoundError: If the module directory is not found in this library
@@ -111,7 +121,7 @@ class Library:
         try:
             for item in module_path.iterdir():
                 has_template = item.is_dir() and any((item / f).exists() for f in ("template.yaml", "template.yml"))
-                if has_template and not self._is_template_draft(item):
+                if has_template:
                     template_id = item.name
 
                     # Check for duplicate within same library
@@ -120,8 +130,6 @@ class Library:
 
                     seen_ids[template_id] = True
                     template_dirs.append((item, self.name))
-                elif has_template:
-                    logger.debug(f"Skipping draft template: {item.name}")
         except PermissionError as e:
             raise LibraryError(
                 f"Permission denied accessing module '{module_name}' in library '{self.name}': {e}"
@@ -252,7 +260,7 @@ class LibraryManager:
                             template_path, lib_name = library.find_by_id(module_name, base_id)
                             logger.debug(f"Found template '{base_id}' in library '{requested_lib}'")
                             return template_path, lib_name
-                        except TemplateNotFoundError:
+                        except (TemplateNotFoundError, TemplateDraftError):
                             logger.debug(f"Template '{base_id}' not found in library '{requested_lib}'")
                             return None
 
@@ -268,6 +276,9 @@ class LibraryManager:
             except TemplateNotFoundError:
                 # Continue searching in next library
                 continue
+            except TemplateDraftError:
+                # Draft error should propagate immediately (don't search other libraries)
+                raise
 
         logger.debug(f"Template '{template_id}' not found in any library")
         return None

+ 53 - 3
cli/core/module/base_commands.py

@@ -17,7 +17,11 @@ from ..exceptions import (
     TemplateValidationError,
 )
 from ..input import InputManager
-from ..template import Template
+from ..template import (
+    TEMPLATE_STATUS_DRAFT,
+    TEMPLATE_STATUS_PUBLISHED,
+    Template,
+)
 from ..validators import get_validator_registry
 from .helpers import (
     apply_cli_overrides,
@@ -83,6 +87,16 @@ def list_templates(module_instance, raw: bool = False) -> list:
                 tags_list = template.metadata.tags or []
                 tags = ", ".join(tags_list) if tags_list else "-"
                 version = str(template.metadata.version) if template.metadata.version else ""
+
+                # Get status and format it
+                status = template.status
+                if status == TEMPLATE_STATUS_PUBLISHED:
+                    status_display = "[green]Published[/green]"
+                elif status == TEMPLATE_STATUS_DRAFT:
+                    status_display = "[dim]Draft[/dim]"
+                else:  # TEMPLATE_STATUS_INVALID
+                    status_display = "[red]Invalid[/red]"
+
                 schema = template.schema_version if hasattr(template, "schema_version") else "1.0"
                 library_name = template.metadata.library or ""
                 library_type = template.metadata.library_type or "git"
@@ -90,7 +104,19 @@ def list_templates(module_instance, raw: bool = False) -> list:
                 icon = IconManager.UI_LIBRARY_STATIC if library_type == "static" else IconManager.UI_LIBRARY_GIT
                 color = "yellow" if library_type == "static" else "blue"
                 library_display = f"[{color}]{icon} {library_name}[/{color}]"
-                return (template.id, name, tags, version, schema, library_display)
+
+                # Apply dimmed style to entire row if draft
+                if status == TEMPLATE_STATUS_DRAFT:
+                    template_id = f"[dim]{template.id}[/dim]"
+                    name = f"[dim]{name}[/dim]"
+                    tags = f"[dim]{tags}[/dim]"
+                    version = f"[dim]{version}[/dim]"
+                    schema = f"[dim]{schema}[/dim]"
+                    library_display = f"[dim]{icon} {library_name}[/dim]"
+                else:
+                    template_id = template.id
+
+                return (template_id, name, tags, version, status_display, schema, library_display)
 
             module_instance.display.data_table(
                 columns=[
@@ -98,6 +124,7 @@ def list_templates(module_instance, raw: bool = False) -> list:
                     {"name": "Name"},
                     {"name": "Tags"},
                     {"name": "Version", "no_wrap": True},
+                    {"name": "Status", "no_wrap": True},
                     {"name": "Schema", "no_wrap": True},
                     {"name": "Library", "no_wrap": True},
                 ],
@@ -129,6 +156,16 @@ def search_templates(module_instance, query: str) -> list:
             tags_list = template.metadata.tags or []
             tags = ", ".join(tags_list) if tags_list else "-"
             version = str(template.metadata.version) if template.metadata.version else ""
+
+            # Get status and format it
+            status = template.status
+            if status == TEMPLATE_STATUS_PUBLISHED:
+                status_display = "[green]Published[/green]"
+            elif status == TEMPLATE_STATUS_DRAFT:
+                status_display = "[dim]Draft[/dim]"
+            else:  # TEMPLATE_STATUS_INVALID
+                status_display = "[red]Invalid[/red]"
+
             schema = template.schema_version if hasattr(template, "schema_version") else "1.0"
             library_name = template.metadata.library or ""
             library_type = template.metadata.library_type or "git"
@@ -136,7 +173,19 @@ def search_templates(module_instance, query: str) -> list:
             icon = IconManager.UI_LIBRARY_STATIC if library_type == "static" else IconManager.UI_LIBRARY_GIT
             color = "yellow" if library_type == "static" else "blue"
             library_display = f"[{color}]{icon} {library_name}[/{color}]"
-            return (template.id, name, tags, version, schema, library_display)
+
+            # Apply dimmed style to entire row if draft
+            if status == TEMPLATE_STATUS_DRAFT:
+                template_id = f"[dim]{template.id}[/dim]"
+                name = f"[dim]{name}[/dim]"
+                tags = f"[dim]{tags}[/dim]"
+                version = f"[dim]{version}[/dim]"
+                schema = f"[dim]{schema}[/dim]"
+                library_display = f"[dim]{icon} {library_name}[/dim]"
+            else:
+                template_id = template.id
+
+            return (template_id, name, tags, version, status_display, schema, library_display)
 
         module_instance.display.data_table(
             columns=[
@@ -144,6 +193,7 @@ def search_templates(module_instance, query: str) -> list:
                 {"name": "Name"},
                 {"name": "Tags"},
                 {"name": "Version", "no_wrap": True},
+                {"name": "Status", "no_wrap": True},
                 {"name": "Schema", "no_wrap": True},
                 {"name": "Library", "no_wrap": True},
             ],

+ 12 - 1
cli/core/template/__init__.py

@@ -4,12 +4,23 @@ This package provides Template, VariableCollection, VariableSection, and Variabl
 classes for managing templates and their variables.
 """
 
-from .template import Template, TemplateErrorHandler, TemplateFile, TemplateMetadata
+from .template import (
+    TEMPLATE_STATUS_DRAFT,
+    TEMPLATE_STATUS_INVALID,
+    TEMPLATE_STATUS_PUBLISHED,
+    Template,
+    TemplateErrorHandler,
+    TemplateFile,
+    TemplateMetadata,
+)
 from .variable import Variable
 from .variable_collection import VariableCollection
 from .variable_section import VariableSection
 
 __all__ = [
+    "TEMPLATE_STATUS_DRAFT",
+    "TEMPLATE_STATUS_INVALID",
+    "TEMPLATE_STATUS_PUBLISHED",
     "Template",
     "TemplateErrorHandler",
     "TemplateFile",

+ 23 - 0
cli/core/template/template.py

@@ -42,6 +42,11 @@ from .variable_collection import VariableCollection
 
 logger = logging.getLogger(__name__)
 
+# Template Status Constants
+TEMPLATE_STATUS_PUBLISHED = "published"
+TEMPLATE_STATUS_DRAFT = "draft"
+TEMPLATE_STATUS_INVALID = "invalid"
+
 
 class TemplateErrorHandler:
     """Handles parsing and formatting of template rendering errors.
@@ -908,3 +913,21 @@ class Template:
             # Sort sections: required first, then enabled, then disabled
             self.__variables.sort_sections()
         return self.__variables
+
+    @property
+    def status(self) -> str:
+        """Get the status of the template.
+
+        Returns:
+            Status string: 'published' or 'draft'
+
+        Note:
+            The 'invalid' status is reserved for future use when template validation
+            is implemented without impacting list command performance.
+        """
+        # Check if template is marked as draft in metadata
+        if self.metadata.draft:
+            return TEMPLATE_STATUS_DRAFT
+
+        # Template is published (valid and not draft)
+        return TEMPLATE_STATUS_PUBLISHED