diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000000..90bcce00f11 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,134 @@ +# .devcontainer + +Claude Code をホスト直接実行せず、隔離されたコンテナ内で動かすための devcontainer 設定です。 +NaCl Claude Code 利用ガイドライン **階層 B(OSS / 自社開発)** に準拠します。 + +## 設計方針 + +- **既存の `docker-compose.yml` の `app` サービスを再利用** する。`docker-compose.devcontainer.yml` で mount のみ追加する override 構成 +- ホスト全体ではなく、**作業に必要なディレクトリのみ** をマウントする(物理アクセス範囲の制限) +- 既存の `bin/dx` / `bin/setup-worktree` / git worktree 運用と共存する +- VS Code Dev Containers と devpod の両方で動作する標準準拠 + +## マウント構成 + +devcontainer のマウントは **共有用** と **個人用** を 2 つの compose ファイルに +分けている。 + +### `docker-compose.devcontainer.yml` (commit、全員共通) + +| ホスト側 | コンテナ内 | 用途 | +|---|---|---| +| `` | `/app` | 作業ディレクトリ(既存 compose と同じ) | +| `~/.gitconfig` | `/root/.gitconfig` (ro) | git ユーザー名・コミット署名 | +| `~/.config/gh` | `/root/.config/gh` | gh CLI 設定 | + +### `docker-compose.local.yml` (`.gitignore`、各自で `*.example` からコピー) + +ghq の有無、Claude Code の利用有無などはユーザーによって違うため、個人ごとに +override する。`.devcontainer/docker-compose.local.yml.example` をコピーして +自分の環境に合わせる: + +```bash +cp .devcontainer/docker-compose.local.yml.example \ + .devcontainer/docker-compose.local.yml +# 不要な mount をコメントアウトして使う +``` + +template には以下がデフォルトで含まれる: + +| ホスト側 | コンテナ内 | 用途 | +|---|---|---| +| `~/ghq` | `/ghq` | 関連 OSS の参照・push (使わない人はコメントアウト) | +| `~/.claude.json` | `/root/.claude.json` | Claude Code 認証 | +| `~/.claude/settings.json` | 同上 (ro) | Claude Code グローバル設定 | +| `~/.claude/skills` | 同上 (ro) | 自作 skills | +| `~/.claude/plugins` | 同上 (ro) | プラグイン | +| `~/.claude/statusline-command.sh` | 同上 (ro) | カスタム statusline | +| `~/.claude/projects/-app` | `/root/.claude/projects/-app` | このプロジェクト固有 memory | + +`docker-compose.local.yml` が無い場合は `initialize.sh` が空の stub を自動生成 +するので、Claude Code を使わない・ghq を使わない人は何もせずそのまま動作する。 + +**マウントしないもの (全員)**: `~/.ssh`, `~/.aws`, `~/Documents`, `~/Downloads`, +`~/Desktop`, `~/Library`, 他案件ディレクトリ、**`~/.claude/projects/`, +`~/.claude/sessions/`, `~/.claude/history.jsonl`, `~/.claude/file-history/`, +`~/.claude/shell-snapshots/`** (他プロジェクトの転写・履歴の漏れを防ぐ)。 + +### Claude Code の認証・設定・memory 共有について + +`~/.claude` のうち、共有して安全なもの(グローバル設定・自作 skills・認証)だけを bind mount し、**他プロジェクトの transcripts や履歴は意図的に隔離** する設計。 + +このプロジェクトのメモリ (`~/.claude/projects//memory/`) はホスト側スラッグがユーザーのパスに依存するため、`initialize.sh` がホスト側に **`~/.claude/projects/-app` → `~/.claude/projects/`** のシンボリックリンクを作成する。コンテナ内では workspace=`/app` のため slug=`-app` で固定され、リンク経由で同じ memory を読み書きできる。他のマシンや他の worktree に切り替えても、`initialize.sh` がそのときの host slug にリンクを張り直す。 + +### gh CLI の認証トークンについて (macOS 必須手順) + +macOS の `gh` は PAT を **Keychain** に保存するため、`~/.config/gh` を +bind mount しても `oauth_token` がコンテナに渡らない。devcontainer 起動前に +ホスト側のシェルで以下を実行して `GH_TOKEN` 環境変数として注入する: + +```bash +export GH_TOKEN=$(gh auth token) +``` + +devcontainer.json の `remoteEnv.GH_TOKEN: "${localEnv:GH_TOKEN}"` 経由で +コンテナ内 `gh` が PAT を読み取れるようになる。未設定で devcontainer を +起動した場合、`initialize.sh` が警告を出す (devcontainer 自体は起動する)。 + +## 起動方法 + +### VS Code Dev Containers + +1. VS Code に「Dev Containers」拡張をインストール +2. リポジトリを VS Code で開く +3. コマンドパレットから **Dev Containers: Reopen in Container** + +### devpod (CLI) + +```bash +# 初回のみ +brew install devpod +devpod provider add docker + +# 起動 +cd +devpod up . + +# Claude Code をコンテナ内で起動 +devpod ssh smalruby3-editor -- claude + +# 停止 +devpod stop smalruby3-editor +``` + +### docker compose 直接 (CLI only) + +```bash +# devcontainer のオーバーレイを適用して起動 +docker compose -f docker-compose.yml -f .devcontainer/docker-compose.devcontainer.yml up -d app + +# コンテナに入る +docker compose -f docker-compose.yml -f .devcontainer/docker-compose.devcontainer.yml exec app bash +``` + +## 既存ワークフローへの影響 + +| 既存仕組み | 影響 | +|---|---| +| `docker compose run --rm app ...` | 変更なし。devcontainer を立てなくても従来通り動く | +| `bin/dx` | 変更なし(devcontainer 内では二重 docker を避けるラッパーを後続フェーズで検討) | +| `bin/setup-worktree` | `.devcontainer/post-create.sh` 経由で **worktree のときだけ** 実行(main checkout では skip) | +| git worktree | compose の `name: smalruby3-editor` 固定で named volume を共有 | + +## AWS CDK deploy について + +`~/.aws` はマウントしません。`cdk deploy` は人間が diff を見て発動する操作なので、 +**ホスト側で実行** することを推奨します。コンテナ内では `cdk synth` / `cdk diff` まで(AWS 認証不要)。 + +必要時のみ短命 STS token を環境変数で注入する運用も可能(要検討)。 + +## トラブルシューティング + +- **gh auth が効かない**: macOS では PAT が keychain に格納されるため、`export GH_TOKEN=$(gh auth token)` を実行してから devcontainer を起動する (上記「gh CLI の認証トークンについて」参照) +- **`/ghq` が空**: `ghq root` の出力が `~/ghq` 以外を指していないか確認 +- **ポート 8601 が見えない**: VS Code は `forwardPorts` で自動転送。devpod / 直接 compose では `docker-compose.yml` の `ports` 設定で `localhost:8601` に公開される diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 00000000000..93d4d3e6533 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": { + "version": "1.0.5", + "resolved": "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a", + "integrity": "sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", + "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..d81dc1f22da --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +{ + "name": "smalruby3-editor (Claude Code)", + "dockerComposeFile": [ + "../docker-compose.yml", + "docker-compose.devcontainer.yml", + "docker-compose.local.yml" + ], + "service": "app", + "workspaceFolder": "/app", + "overrideCommand": false, + "features": { + "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + "remoteEnv": { + "GHQ_ROOT": "/ghq", + "GH_TOKEN": "${localEnv:GH_TOKEN}" + }, + "forwardPorts": [8601], + "initializeCommand": ".devcontainer/initialize.sh", + "postCreateCommand": ".devcontainer/post-create.sh", + "customizations": { + "vscode": { + "extensions": [ + "anthropic.claude-code", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] + } + } +} diff --git a/.devcontainer/docker-compose.devcontainer.yml b/.devcontainer/docker-compose.devcontainer.yml new file mode 100644 index 00000000000..ca3d58072c3 --- /dev/null +++ b/.devcontainer/docker-compose.devcontainer.yml @@ -0,0 +1,14 @@ +services: + app: + # devcontainer 用に長寿命化(既存 compose は `npm start` で短命) + command: sleep infinity + volumes: + # 全開発者で共通のマウント。個人依存のもの (~/ghq, ~/.claude 等) は + # docker-compose.local.yml に置く (gitignore、各自で .example からコピー)。 + - type: bind + source: ${HOME}/.gitconfig + target: /root/.gitconfig + read_only: true + - type: bind + source: ${HOME}/.config/gh + target: /root/.config/gh diff --git a/.devcontainer/docker-compose.local.yml.example b/.devcontainer/docker-compose.local.yml.example new file mode 100644 index 00000000000..242d8a8332d --- /dev/null +++ b/.devcontainer/docker-compose.local.yml.example @@ -0,0 +1,55 @@ +# .devcontainer/docker-compose.local.yml +# +# 個人ごとに devcontainer を拡張するための override。`.gitignore` 対象。 +# 使い方: このファイルを `docker-compose.local.yml` にコピーし、自分の環境に +# 合わせて編集する。 +# +# cp .devcontainer/docker-compose.local.yml.example \ +# .devcontainer/docker-compose.local.yml +# +# devcontainer.json はこのファイルを dockerComposeFile に含めるため、Reopen +# in Container すると自動で適用される。空のままでも害はない。 +# +# 注意: +# - bind mount のソースが存在しないと devcontainer の起動が失敗するため、 +# 不要なエントリはコメントアウトしておく。 +# - Claude Code を使わない人は claude セクションを丸ごと省略してよい。 +# - ghq を使わない人は ghq セクションを省略してよい。 + +services: + app: + volumes: + # --- ghq (関連 OSS の参照) --- + # 自分の ghq root が ~/ghq でない場合は source を書き換える。 + - type: bind + source: ${HOME}/ghq + target: /ghq + + # --- Claude Code: グローバル設定・認証のみ共有 --- + # 他プロジェクトの transcripts / sessions / history は意図的に + # マウントしないことで漏れを防ぐ。詳細は README.md 参照。 + - type: bind + source: ${HOME}/.claude.json + target: /root/.claude.json + - type: bind + source: ${HOME}/.claude/settings.json + target: /root/.claude/settings.json + read_only: true + - type: bind + source: ${HOME}/.claude/skills + target: /root/.claude/skills + read_only: true + - type: bind + source: ${HOME}/.claude/plugins + target: /root/.claude/plugins + read_only: true + - type: bind + source: ${HOME}/.claude/statusline-command.sh + target: /root/.claude/statusline-command.sh + read_only: true + # このプロジェクトの memory のみ共有。initialize.sh が + # ${HOME}/.claude/projects/-app を host slug へのシンボリックリンクと + # して作成する。 + - type: bind + source: ${HOME}/.claude/projects/-app + target: /root/.claude/projects/-app diff --git a/.devcontainer/initialize.sh b/.devcontainer/initialize.sh new file mode 100755 index 00000000000..cbad9e744e1 --- /dev/null +++ b/.devcontainer/initialize.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# .devcontainer/initialize.sh +# +# Runs on the host before the devcontainer starts (devcontainer.json の +# initializeCommand から呼ばれる)。以下を行う: +# +# 1. docker-compose.local.yml が無ければ空の stub を作る +# (devcontainer.json は常にこのファイルを参照するため) +# 2. GH_TOKEN が export されていなければ案内を出す +# (macOS の gh は PAT を keychain に保存するため、~/.config/gh を +# bind mount しただけではコンテナ内 gh から認証できない) +# 3. ホスト側に ~/.claude/projects/-app シンボリックリンクを作成 +# (Claude Code を使う人向けの per-project memory bridge) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# --- 1) docker-compose.local.yml の stub 作成 ----------------------------- +LOCAL_COMPOSE="${SCRIPT_DIR}/docker-compose.local.yml" +if [[ ! -f "${LOCAL_COMPOSE}" ]]; then + cat >"${LOCAL_COMPOSE}" <<'EOF' +# Auto-generated by .devcontainer/initialize.sh. +# 個人ごとの mount を追加したい場合は +# .devcontainer/docker-compose.local.yml.example を参考に書き換える。 +# このファイルは .gitignore 対象なので、個人の変更は他開発者に共有されない。 +services: + app: {} +EOF + echo "initialize: created empty ${LOCAL_COMPOSE} (edit to add personal mounts)" +fi + +# --- 2) GH_TOKEN チェック ------------------------------------------------- +if [[ -z "${GH_TOKEN:-}" ]]; then + if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then + cat <<'MSG' >&2 +warning: GH_TOKEN is not set in the host shell. +The devcontainer mounts ~/.config/gh, but on macOS the token is stored in +the keychain and is not available inside the container. + +To enable `gh` inside the devcontainer, run before launching: + + export GH_TOKEN=$(gh auth token) + +Then start/reopen the devcontainer. +MSG + else + echo "warning: host gh CLI not authenticated. devcontainer-internal gh will be unusable." >&2 + fi +fi + +# --- 3) Claude Code per-project memory bridge ---------------------------- +# Claude Code をホスト側にインストールしていないユーザは何もしない。 +# このセクションは本人が docker-compose.local.yml で +# ~/.claude/projects/-app マウントを有効にしている場合に意味を持つ。 +if [[ -d "${HOME}/.claude/projects" ]]; then + WORKSPACE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + HOST_PROJECT_SLUG="$(printf '%s' "${WORKSPACE_DIR}" | sed 's:/:-:g')" + HOST_PROJECT_DIR="${HOME}/.claude/projects/${HOST_PROJECT_SLUG}" + ALIAS_DIR="${HOME}/.claude/projects/-app" + + mkdir -p "${HOST_PROJECT_DIR}/memory" + + if [[ -L "${ALIAS_DIR}" ]]; then + CURRENT_TARGET="$(readlink "${ALIAS_DIR}")" + if [[ "${CURRENT_TARGET}" != "${HOST_PROJECT_DIR}" ]]; then + rm "${ALIAS_DIR}" + ln -s "${HOST_PROJECT_DIR}" "${ALIAS_DIR}" + fi + elif [[ -e "${ALIAS_DIR}" ]]; then + echo "warning: ${ALIAS_DIR} exists as a non-symlink; not touching it" >&2 + else + ln -s "${HOST_PROJECT_DIR}" "${ALIAS_DIR}" + fi +fi + +exit 0 diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 00000000000..26107b7ff3c --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# .devcontainer/post-create.sh +# +# Runs inside the container after creation (devcontainer.json の +# postCreateCommand から呼ばれる)。 +# +# - worktree であれば bin/setup-worktree を実行 (env コピー + npm install + build:dev) +# - main checkout であれば既存の node_modules / dist / .env がそのままあるので +# 何もしない (husky hooks も既にインストール済みのはず) + +set -euo pipefail + +cd /app + +GIT_COMMON_DIR=$(git rev-parse --git-common-dir 2>/dev/null || echo "") +GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo "") + +if [[ -z "${GIT_COMMON_DIR}" ]]; then + echo "post-create: not a git repository, skipping setup" + exit 0 +fi + +GIT_COMMON_ABS=$(cd "${GIT_COMMON_DIR}" && pwd) +GIT_DIR_ABS=$(cd "${GIT_DIR}" && pwd) + +if [[ "${GIT_COMMON_ABS}" == "${GIT_DIR_ABS}" ]]; then + echo "post-create: main checkout detected, no worktree setup needed" + exit 0 +fi + +echo "post-create: worktree detected, running bin/setup-worktree" +exec bin/setup-worktree diff --git a/.gitignore b/.gitignore index b2e5964b54c..26c1bdffe48 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,6 @@ infra/**/cdk.out.*/ infra/**/tmp/ infra/**/.cdk.staging/ infra/**/cdk-out-*/ + +# devcontainer の個人用 mount override (initialize.sh が空 stub を生成する) +.devcontainer/docker-compose.local.yml