深入 `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 |
选择 sh 或 ps 脚本变体 |
若省略则根据操作系统自动检测 |
--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=8 和 transient=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 专属的指令文件。