From 3a0ae75bfbf97eca7c318c8de3ce7fde59371eba Mon Sep 17 00:00:00 2001 From: honjo-hiroaki-gtt Date: Fri, 19 Sep 2025 17:53:16 +0900 Subject: [PATCH] Limit workspace command seeding to Codex init and update Codex documentation accordingly. --- README.md | 2 +- src/specify_cli/__init__.py | 121 +++++++++++++++++++++++------------- 2 files changed, 78 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index b9e32c8..cca5efc 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ specify init --ai claude --ignore-agent-tools > [!NOTE] > Codex CLI specifics -> - Codex CLI loads slash commands from the project workspace instead of from an IDE bundle, so `specify init --ai codex` creates a `commands/` directory (and seeds `specify.md`, `plan.md`, `tasks.md` if templates are missing) to match that expectation. +> - `specify init --ai codex` ensures a workspace-level `commands/` directory exists (seeding `specify.md`, `plan.md`, `tasks.md` if necessary) because Codex CLI loads slash commands from the repo itself. > - Codex persists its working memory in `AGENTS.md`; if you do not see that file yet, run `codex /init` once inside the project to generate it. > - To let Codex trigger the helper scripts under `scripts/` through slash commands, open `codex /approvals` and enable “Run shell commands”. diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index f28443d..842909c 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -68,8 +68,8 @@ SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} # Claude CLI local installation path after migrate-installer CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude" -# Embedded fallback command templates for Codex (used only if packaged templates are unavailable) -CODEX_CMD_SPECIFY = """--- +# Embedded fallback command templates (used if packaged templates are unavailable) +COMMAND_TEMPLATE_SPECIFY = """--- description: Create or update the feature specification from a natural language feature description. scripts: sh: scripts/bash/create-new-feature.sh --json "{ARGS}" @@ -86,7 +86,7 @@ Given the feature description provided as an argument, do this: Note: The script creates and checks out the new branch and initializes the spec file before writing. """ -CODEX_CMD_PLAN = """--- +COMMAND_TEMPLATE_PLAN = """--- description: Execute the implementation planning workflow using the plan template to generate design artifacts. scripts: sh: scripts/bash/setup-plan.sh --json @@ -127,7 +127,7 @@ Given the implementation details provided as an argument, do this: Use absolute paths with the repository root for all file operations to avoid path issues. """ -CODEX_CMD_TASKS = """--- +COMMAND_TEMPLATE_TASKS = """--- description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts. scripts: sh: scripts/bash/check-task-prerequisites.sh --json @@ -191,12 +191,12 @@ The tasks.md should be immediately executable - each task must be specific enoug """ -# Utility to ensure Codex command templates use the modern schema (with scripts mapping) -def ensure_codex_command_templates_current(commands_dir: Path) -> None: +# Utility to ensure command templates use the modern schema (with scripts mapping) +def ensure_command_templates_current(commands_dir: Path) -> None: expected = { - "specify.md": CODEX_CMD_SPECIFY, - "plan.md": CODEX_CMD_PLAN, - "tasks.md": CODEX_CMD_TASKS, + "specify.md": COMMAND_TEMPLATE_SPECIFY, + "plan.md": COMMAND_TEMPLATE_PLAN, + "tasks.md": COMMAND_TEMPLATE_TASKS, } def needs_upgrade(content: str) -> bool: @@ -888,6 +888,68 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = console.print(f" - {f}") +def ensure_workspace_commands(project_path: Path, tracker: StepTracker | None = None) -> None: + """Ensure a workspace-level commands/ directory exists and has up-to-date templates.""" + if tracker: + tracker.start("commands") + + commands_dir = project_path / "commands" + seeded_from: str | None = None + + try: + existed = commands_dir.exists() + if not existed: + commands_dir.mkdir(parents=True, exist_ok=True) + + try: + is_empty = not any(commands_dir.iterdir()) + except FileNotFoundError: + is_empty = True + + should_seed = not existed or is_empty + + if should_seed: + candidates: list[tuple[str, Path]] = [] + + template_commands = project_path / ".specify" / "templates" / "commands" + if template_commands.exists() and template_commands.is_dir(): + candidates.append(("release bundle", template_commands)) + + packaged_commands = None + for ancestor in Path(__file__).resolve().parents: + candidate = ancestor / "templates" / "commands" + if candidate.exists() and candidate.is_dir(): + packaged_commands = candidate + break + if packaged_commands is not None: + candidates.append(("packaged defaults", packaged_commands)) + + for label, source in candidates: + try: + shutil.copytree(source, commands_dir, dirs_exist_ok=True) + seeded_from = label + break + except Exception: + continue + + if seeded_from is None: + seeded_from = "embedded defaults" + + ensure_command_templates_current(commands_dir) + + detail = "verified" if seeded_from is None else seeded_from + if tracker: + tracker.complete("commands", detail) + else: + if seeded_from: + console.print(f"[cyan]Seeded workspace commands from {seeded_from}[/cyan]") + except Exception as exc: + if tracker: + tracker.error("commands", str(exc)) + else: + console.print(f"[yellow]Warning: could not ensure commands directory ({exc})[/yellow]") + + @app.command() def init( project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"), @@ -1054,7 +1116,7 @@ def init( ("extract", "Extract template"), ("zip-list", "Archive contents"), ("extracted-summary", "Extraction summary"), - ("chmod", "Ensure scripts executable"), + ("chmod", "Ensure scripts executable"), ("cleanup", "Cleanup"), ("git", "Initialize git repository"), ("final", "Finalize") @@ -1062,7 +1124,7 @@ def init( tracker.add(key, label) if selected_ai == "codex": - tracker.add("commands", "Ensure Codex commands") + tracker.add("commands", "Ensure workspace commands") # Use transient so live tree is replaced by the final static render (avoids duplicate output) with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live: @@ -1075,42 +1137,13 @@ def init( download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug) + # Ensure /commands directory for Codex CLI workspaces only + if selected_ai == "codex": + ensure_workspace_commands(project_path, tracker=tracker) + # Ensure scripts are executable (POSIX) ensure_executable_scripts(project_path, tracker=tracker) - # Codex only: if commands/ is missing, copy from template (with embedded fallback) - if selected_ai == "codex": - tracker.start("commands") - try: - target_cmds = project_path / "commands" - if not target_cmds.exists(): - commands_src = None - for ancestor in Path(__file__).resolve().parents: - candidate = ancestor / "templates" / "commands" - if candidate.exists() and candidate.is_dir(): - commands_src = candidate - break - if commands_src is not None: - shutil.copytree(commands_src, target_cmds, dirs_exist_ok=True) - ensure_codex_command_templates_current(target_cmds) - tracker.complete("commands", "added") - else: - template_commands = project_path / ".specify" / "templates" / "commands" - if template_commands.exists(): - shutil.copytree(template_commands, target_cmds, dirs_exist_ok=True) - ensure_codex_command_templates_current(target_cmds) - tracker.complete("commands", "added from template") - else: - target_cmds.mkdir(parents=True, exist_ok=True) - (target_cmds / "specify.md").write_text(CODEX_CMD_SPECIFY, encoding="utf-8") - (target_cmds / "plan.md").write_text(CODEX_CMD_PLAN, encoding="utf-8") - (target_cmds / "tasks.md").write_text(CODEX_CMD_TASKS, encoding="utf-8") - tracker.complete("commands", "bootstrapped minimal") - else: - tracker.skip("commands", "already present") - except Exception as codex_error: - tracker.error("commands", str(codex_error)) - # Git step if not no_git: tracker.start("git")