Read OSS

深入 `specify init`:从用户命令到项目脚手架

中级

前置知识

  • 第 1 篇:架构与项目导航
  • 对 Python CLI 框架(typer)有基本了解
  • 熟悉终端 TUI 的基本概念

深入 specify init:从用户命令到项目脚手架

specify init 是一切的起点。开发者运行的第一条命令,不到十秒钟,便能将一个空目录变成完整的 SDD 项目脚手架——包括针对 AI agent 的命令文件、shell 脚本、文档模板、constitution 文件,以及 git 扩展。本文将完整追踪这条执行路径:从 17 个 CLI 参数,经过交互式 TUI,一直到串联所有子系统的编排流水线(即第 1 篇中我们梳理过的各个模块)。

17 个 CLI 参数及其相互关系

init 命令的签名是整个代码库中参数最多的——__init__.py#L873-L892

@app.command()
def init(
    project_name: str = typer.Argument(None, ...),
    ai_assistant: str = typer.Option(None, "--ai", ...),
    ai_commands_dir: str = typer.Option(None, "--ai-commands-dir", ...),
    script_type: str = typer.Option(None, "--script", ...),
    ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", ...),
    no_git: bool = typer.Option(False, "--no-git", ...),
    here: bool = typer.Option(False, "--here", ...),
    force: bool = typer.Option(False, "--force", ...),
    # ... plus 9 more including deprecated/hidden options
    integration: str = typer.Option(None, "--integration", ...),
    integration_options: str = typer.Option(None, "--integration-options", ...),
):

关键参数的相互约束关系如下:

参数 用途 约束
--ai / --integration 选择目标 agent 互斥——不能同时使用
--here / project_name 在当前目录或新目录中初始化 --here 不能与项目名称同时使用
--force 对非空目录跳过确认提示 仅在 --here 或目录已存在时有效
--script 选择 shps 脚本变体 若省略则根据操作系统自动检测
--preset 初始化时应用预设 从本地路径、内置包或目录中解析
--no-git 跳过 git init 和扩展安装 完全跳过 git 相关步骤
--ai-commands-dir 自定义命令目录 仅在 --ai generic 时需要

--ai 是旧有路径,--integration 是其对应的新版参数。两者最终都通过同一个 INTEGRATION_REGISTRY 进行解析。注意 __init__.py#L64-L66 中定义的别名系统,它将 kiro 这样的缩写映射到 kiro-cli

Integration 解析与别名查找

参数校验完成后,第一个实质性逻辑是解析要使用哪个 integration——__init__.py#L950-L976

flowchart TD
    A["User passes --integration or --ai"] --> B{"Both provided?"}
    B -->|"Yes"| C["Error: mutually exclusive"]
    B -->|"No"| D{"--integration?"}
    D -->|"Yes"| E["get_integration(key)"]
    D -->|"No"| F["Apply alias: AI_ASSISTANT_ALIASES"]
    F --> G["get_integration(aliased_key)"]
    E --> H{"Found?"}
    G --> H
    H -->|"No"| I["Error: unknown integration"]
    H -->|"Yes"| J["resolved_integration ready"]

别名查找的实现很简单,本质上是一个字典——写作本文时内容为 {"kiro": "kiro-cli"}。解析完成后,代码会对旧版参数输出弃用提示,例如 --ai-skills(由于 Claude 等基于 skills 的 integration 已自动处理,该参数不再需要)以及 --ai-commands-dir(已由 --integration generic --integration-options 替代)。

交互式 TUI:select_with_arrows()

当用户未传入 --ai--integration 参数时,CLI 会弹出一个交互式选择菜单。该功能实现于 __init__.py#L215-L288,是一个基于两个库构建的自定义组件:

  • readchar:跨平台的单键捕获(支持方向键及 Ctrl+P/N)
  • rich.Live:无闪烁地刷新选择面板
sequenceDiagram
    participant User
    participant get_key
    participant Live as rich.Live
    participant Panel as Selection Panel

    User->>get_key: Press ↓
    get_key->>Live: Return 'down'
    Live->>Panel: Increment selected_index
    Panel->>Live: Re-render with new highlight
    Live->>User: Updated display
    User->>get_key: Press Enter
    get_key->>Live: Return 'enter'
    Live-->>User: Return selected_key

第 195 行的辅助函数 get_key() 将 readchar 的原始常量转换为语义字符串('up''down''enter''escape'),同时支持方向键和 Emacs 风格的 Ctrl+P/Ctrl+N 快捷键。选择面板由 rich.Table.grid 包裹在 rich.Panel 中构成,每次按键都会触发重新渲染。设置 transient=True 后,菜单会在选择完成后自动消失,不留痕迹。

提示: select_with_arrows 函数接受一个 default_key 参数。在 AI 选择场景下,默认值为 "copilot"——__init__.py#L1068-L1072。这个小小的 UX 设计,透露出 GitHub 的产品意图:Copilot 是默认的起点。

StepTracker:实时进度展示

integration 解析和脚本类型选择完成后,init 从交互模式切换到带进度追踪的编排模式。StepTracker 类提供了一种树形进度展示,其风格灵感来源于 Claude Code 的输出:

⟨Initialize Specify Project⟩
 ● Check required tools (ok)
 ● Select AI assistant (claude)
 ● Select script type (sh)
 ○ Install integration
 ○ Install shared infrastructure
 ○ Ensure scripts executable
 ○ Constitution setup
 ○ Install git extension
 ○ Finalize

每个步骤有五种状态:pending(○ 暗色)、running(○ 青色)、done(● 绿色)、error(● 红色)、skipped(○ 黄色)。StepTracker 采用回调模式——attach_refresh(cb) 钩入 rich.Live.update(),使得任意步骤状态变更时,界面都能自动刷新。

sequenceDiagram
    participant Init as init() command
    participant ST as StepTracker
    participant Live as rich.Live

    Init->>ST: add("integration", "Install integration")
    Init->>ST: start("integration")
    ST->>Live: _maybe_refresh() → live.update(render())
    Note over Live: Shows ○ cyan "Install integration"
    Init->>ST: complete("integration", "Claude Code")
    ST->>Live: _maybe_refresh() → live.update(render())
    Note over Live: Shows ● green "Install integration (Claude Code)"

render() 方法构建一个 rich.Tree 组件,将每个步骤映射为带样式的文本行。整体包裹在一个 Live 上下文管理器中,设置 refresh_per_second=8transient=True,使得流水线完成后,进度树会被最终的完成面板所取代。

8 步初始化编排流水线

核心编排逻辑在 rich.Live 上下文中运行,入口位于 __init__.py#L1144-L1210。完整流水线如下:

flowchart TD
    S1["1. Precheck<br/>Verify tools installed"] --> S2["2. AI Selection<br/>Interactive or --ai flag"]
    S2 --> S3["3. Script Selection<br/>sh/ps auto or --script"]
    S3 --> S4["4. integration.setup()<br/>Create agent commands + manifest"]
    S4 --> S5["5. _install_shared_infra()<br/>Copy scripts + templates to .specify/"]
    S5 --> S6["6. ensure_constitution<br/>Copy constitution template"]
    S6 --> S7["7. Git Extension<br/>init repo + install bundled extension"]
    S7 --> S8["8. Preset + Finalize<br/>Apply preset, save options, done"]

步骤 1–3 在 Live 上下文之前执行,步骤 4–8 在其中运行并附带进度追踪。下面重点介绍几个关键步骤。

步骤 4——Integration 安装:创建 IntegrationManifest,调用 resolved_integration.setup(),并将 manifest 持久化到磁盘。setup() 调用正是实际生成 agent 专属命令文件的地方——我们将在第 3 篇中详细展开。

步骤 5——共享基础设施:由 _install_shared_infra() 负责,将脚本和模板复制到 .specify/ 目录,采用"合并但不覆盖"的策略——若文件已存在则跳过。这对于 --here 工作流尤为重要,因为用户可能会在已有项目上重新执行 init。

步骤 7——Git 扩展:这是 __init__.py#L1236-L1261 中的两阶段步骤。首先,若不存在 git 仓库则执行初始化;然后通过 ExtensionManager.install_from_directory() 安装内置的 git 扩展。该扩展提供 5 条命令和 18 个生命周期钩子,将 git 操作深度集成到每个工作流阶段。

共享基础设施与文件输出

_install_shared_infra() 值得单独审视,因为它完整体现了第 1 篇中介绍的双路径解析模式。它先调用 _locate_core_pack(),若未找到则回退到相对于仓库的路径:

core = _locate_core_pack()
if core and (core / "scripts").is_dir():
    scripts_src = core / "scripts"
else:
    repo_root = Path(__file__).parent.parent.parent
    scripts_src = repo_root / "scripts"

"合并但不覆盖"的行为通过遍历源文件并检查目标是否已存在来实现:

for src_path in variant_src.rglob("*"):
    if src_path.is_file():
        rel_path = src_path.relative_to(variant_src)
        dst_path = dest_variant / rel_path
        if dst_path.exists():
            skipped_files.append(str(dst_path.relative_to(project_path)))
        else:
            dst_path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(src_path, dst_path)

所有步骤完成后,save_init_options() 将 CLI 参数持久化到 .specify/init-options.json。这个文件会被下游工具读取——例如,preset 系统通过它判断当前使用的脚本变体,扩展系统则根据它决定输出 SKILL.md 还是 markdown 格式。

初始化完成后,脚手架项目的结构如下:

my-project/
├── .specify/
│   ├── init-options.json        # Persisted CLI flags
│   ├── integration.json         # Active integration metadata
│   ├── integrations/
│   │   └── claude.manifest.json # SHA-256 tracked file manifest
│   ├── extensions/
│   │   ├── .registry            # Installed extensions registry
│   │   └── git/                 # Git extension files
│   ├── scripts/bash/            # Shell scripts for workflow
│   ├── templates/               # Document templates
│   └── extensions.yml           # Merged hook configuration
├── .claude/skills/              # (or .github/agents/, .windsurf/workflows/, etc.)
│   ├── speckit-specify/SKILL.md
│   ├── speckit-plan/SKILL.md
│   └── ...
└── memory/constitution.md       # Project constitution

提示: .specify/ 目录是项目的 SDD 基础设施根目录,其结构清晰区分了运行时状态(manifests、registries、options)与内容资产(scripts、templates)。如果 init 过程中出现问题,删除 .specify/ 目录后重新执行始终是安全的。

下一步

init 命令将最核心的工作委托给了 resolved_integration.setup()——这一方法调用背后隐藏着一套精巧的类层级结构。在第 3 篇中,我们将深入探讨 Spec Kit 的四层 integration 体系如何通过模板方法模式,将同一套命令模板适配到 25 个以上的 AI agent,以及 7 步 process_template() 流水线如何将原始 markdown 转换为 agent 专属的指令文件。