`specify init` の内側:ユーザーコマンドからスキャフォールドされたプロジェクトまで
前提知識
- ›第 1 回:アーキテクチャとプロジェクトナビゲーション
- ›Python CLI フレームワーク(typer)の基本的な理解
- ›ターミナル TUI の概念への親しみ
specify init の内側:ユーザーコマンドからスキャフォールドされたプロジェクトまで
specify init コマンドは、すべてが集約される場所です。開発者が最初に実行するこのコマンドは、10秒足らずで空のディレクトリを完全にスキャフォールドされたSDDプロジェクトへと変換します。エージェント固有のコマンドやシェルスクリプト、ドキュメントテンプレート、コンスティテューションファイル、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 |
ターゲットエージェントを選択する | 相互排他 — 両方は使えない |
--here / project_name |
現在のディレクトリで初期化するか、新しいディレクトリで行うか | --here と名前の組み合わせは不可 |
--force |
空でないディレクトリに対する確認をスキップ | --here または既存ディレクトリの場合のみ有効 |
--script |
sh または ps スクリプトのバリアントを選択 |
省略時は OS から自動検出 |
--preset |
初期化時にプリセットを適用 | ローカルパス、バンドル済み、またはカタログから解決 |
--no-git |
git init と拡張機能のインストールをスキップ | git ステップ全体をスキップ |
--ai-commands-dir |
カスタムコマンドディレクトリ | --ai generic の場合のみ必要 |
--ai フラグはレガシーのパスで、--integration はその新しい等価物です。どちらも同じ INTEGRATION_REGISTRY を通じて解決されます。kiro を kiro-cli にマッピングするショートカットのエイリアスシステムについては、__init__.py#L64-L66 を参照してください。
インテグレーションの解決とエイリアスの検索
パラメーターの検証が終わると、最初の実質的な処理は使用するインテグレーションの解決です — __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"]
エイリアスの検索はシンプルなdictです(執筆時点では {"kiro": "kiro-cli"})。解決が完了すると、レガシーフラグの非推奨通知が出力されます。例えば --ai-skills はClaudeのようなスキルベースのインテグレーションが自動処理するため不要です。また --ai-commands-dir は --integration generic --integration-options に置き換え済みです。
インタラクティブな TUI:select_with_arrows()
--ai や --integration フラグが渡されない場合、CLI はインタラクティブな選択メニューを表示します。__init__.py#L215-L288 の実装は、2 つのライブラリを使って作られたカスタムウィジェットです:
- readchar — クロスプラットフォームで 1 キー入力をキャプチャ(矢印キーや 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:リアルタイムの進捗表示
インテグレーションが解決され、スクリプトの種類が選ばれると、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
各ステップは 5 つの状態のいずれかを持ちます:pending(○ 薄暗い)、running(○ シアン)、done(● 緑)、error(● 赤)、skipped(○ 黄色)。トラッカーはコールバックパターン(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 ウィジェットを構築し、各ステップをスタイル付きの行にマッピングします。これは refresh_per_second=8 と transient=True を設定した Live コンテキストマネージャーでラップされるため、最終的にツリーは完了パネルに置き換えられます。
8 ステップの Init オーケストレーション
コアのオーケストレーションは、__init__.py#L1144-L1210 から始まる rich.Live コンテキスト内で実行されます。パイプライン全体の流れは次のとおりです:
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 — インテグレーションのセットアップ では、IntegrationManifest を作成し、resolved_integration.setup() を呼び出して、マニフェストをディスクに保存します。実際にフォーマット固有のコマンドファイルが生成されるのはこの setup 呼び出し時です。詳細は第 3 回で掘り下げます。
ステップ 5 — 共有インフラ は _install_shared_infra() が担当します。スクリプトとテンプレートを .specify/ にコピーする際、「既存ファイルを上書きしない」マージセマンティクスを使います。ファイルがすでに存在する場合はスキップされます。これは、既存プロジェクトを再初期化する可能性がある --here ワークフローでは特に重要です。
ステップ 7 — Git 拡張機能 は __init__.py#L1236-L1261 の 2 段階のステップです。まず 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 に保存します。このファイルは後続のツールで参照されます。たとえばプリセットシステムはここを読んでアクティブなスクリプトバリアントを判断し、拡張機能システムは 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 インフラのルートです。ランタイム状態(マニフェスト、レジストリ、オプション)とコンテンツ(スクリプト、テンプレート)の分離がそのまま反映された構造になっています。初期化中に何か問題が起きた場合は、.specify/を削除して再実行すれば、常に安全な状態に戻せます。
次回に向けて
initコマンドが最も興味深い処理を委ねているのは resolved_integration.setup() です。一見シンプルなメソッド呼び出しの裏には洗練されたクラス階層が隠れています。第3回では、Spec Kitの4層インテグレーションシステムを探っていきます。Template Methodパターンを通じてコマンドテンプレートセットを25以上のAIエージェントに適応させる仕組みを解説します。さらに7ステップの process_template() パイプラインが生のMarkdownをエージェント固有の指示へと変換するプロセスも見ていきます。