Read OSS

`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 を通じて解決されます。kirokiro-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.gridrich.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=8transient=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をエージェント固有の指示へと変換するプロセスも見ていきます。