Limit workspace command seeding to Codex init and update Codex documentation accordingly.

This commit is contained in:
honjo-hiroaki-gtt
2025-09-19 17:53:16 +09:00
parent 312703260c
commit 3a0ae75bfb
2 changed files with 78 additions and 45 deletions

View File

@@ -224,7 +224,7 @@ specify init <project_name> --ai claude --ignore-agent-tools
> [!NOTE] > [!NOTE]
> Codex CLI specifics > 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. > - 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”. > - To let Codex trigger the helper scripts under `scripts/` through slash commands, open `codex /approvals` and enable “Run shell commands”.

View File

@@ -68,8 +68,8 @@ SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
# Claude CLI local installation path after migrate-installer # Claude CLI local installation path after migrate-installer
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude" CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
# Embedded fallback command templates for Codex (used only if packaged templates are unavailable) # Embedded fallback command templates (used if packaged templates are unavailable)
CODEX_CMD_SPECIFY = """--- COMMAND_TEMPLATE_SPECIFY = """---
description: Create or update the feature specification from a natural language feature description. description: Create or update the feature specification from a natural language feature description.
scripts: scripts:
sh: scripts/bash/create-new-feature.sh --json "{ARGS}" 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. 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. description: Execute the implementation planning workflow using the plan template to generate design artifacts.
scripts: scripts:
sh: scripts/bash/setup-plan.sh --json 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. 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. description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
scripts: scripts:
sh: scripts/bash/check-task-prerequisites.sh --json 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) # Utility to ensure command templates use the modern schema (with scripts mapping)
def ensure_codex_command_templates_current(commands_dir: Path) -> None: def ensure_command_templates_current(commands_dir: Path) -> None:
expected = { expected = {
"specify.md": CODEX_CMD_SPECIFY, "specify.md": COMMAND_TEMPLATE_SPECIFY,
"plan.md": CODEX_CMD_PLAN, "plan.md": COMMAND_TEMPLATE_PLAN,
"tasks.md": CODEX_CMD_TASKS, "tasks.md": COMMAND_TEMPLATE_TASKS,
} }
def needs_upgrade(content: str) -> bool: def needs_upgrade(content: str) -> bool:
@@ -888,6 +888,68 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None =
console.print(f" - {f}") 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() @app.command()
def init( def init(
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"), 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"), ("extract", "Extract template"),
("zip-list", "Archive contents"), ("zip-list", "Archive contents"),
("extracted-summary", "Extraction summary"), ("extracted-summary", "Extraction summary"),
("chmod", "Ensure scripts executable"), ("chmod", "Ensure scripts executable"),
("cleanup", "Cleanup"), ("cleanup", "Cleanup"),
("git", "Initialize git repository"), ("git", "Initialize git repository"),
("final", "Finalize") ("final", "Finalize")
@@ -1062,7 +1124,7 @@ def init(
tracker.add(key, label) tracker.add(key, label)
if selected_ai == "codex": 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) # 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: 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) 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 scripts are executable (POSIX)
ensure_executable_scripts(project_path, tracker=tracker) 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 # Git step
if not no_git: if not no_git:
tracker.start("git") tracker.start("git")