From c872debe4b44c5e9f977582de5b2c9515abecba9 Mon Sep 17 00:00:00 2001 From: hiroki ito Date: Fri, 3 Apr 2026 19:31:32 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=82=B3=E3=83=9F=E3=83=83=E3=83=88?= =?UTF-8?q?=E3=83=AC=E3=83=9D=E3=83=BC=E3=83=88=E3=83=84=E3=83=BC=E3=83=AB?= =?UTF-8?q?=EF=BC=88=E3=82=B3=E3=83=9F=E3=83=8D=E3=82=B3=EF=BC=89=E5=88=9D?= =?UTF-8?q?=E6=9C=9F=E3=82=B3=E3=83=9F=E3=83=83=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- .claude/prompts/daily-report.md | 409 ++++++++++++++++++ .claude/skills/code-analyzer/SKILL.md | 140 ++++++ .claude/skills/config-reader/SKILL.md | 185 ++++++++ .claude/skills/diagram-guidelines/SKILL.md | 348 +++++++++++++++ .../examples/branch-summary.html | 183 ++++++++ .../diagram-guidelines/examples/by-app.html | 134 ++++++ .../examples/daily-summary.html | 208 +++++++++ .../diagram-guidelines/examples/timeline.html | 120 +++++ .../diagram-guidelines/examples/tips.html | 118 +++++ .claude/skills/github-api/SKILL.md | 403 +++++++++++++++++ .../scripts/filter-commits-by-path.js | 263 +++++++++++ .../scripts/get-all-branch-commits.js | 315 ++++++++++++++ .../skills/github-api/scripts/get-commits.js | 63 +++ .../github-api/scripts/lib/date-utils.js | 45 ++ .../github-api/scripts/lib/normalize.js | 30 ++ .claude/skills/screenshot-capture/SKILL.md | 75 ++++ .../scripts/capture-batch.js | 143 ++++++ .../screenshot-capture/scripts/capture.js | 119 +++++ .claude/skills/slack-formatting/SKILL.md | 123 ++++++ .../slack-formatting/scripts/post-report.js | 260 +++++++++++ .github/workflows/daily-report.yml | 31 ++ .github/workflows/report-job.yml | 120 +++++ .gitignore | 8 + CLAUDE.md | 58 +++ README.md | 296 +++++++++++++ configs/projects/your-project.yml | 31 ++ configs/repos/your-repo.yml | 42 ++ package-lock.json | 62 +++ package.json | 12 + 29 files changed, 4344 insertions(+) create mode 100644 .claude/prompts/daily-report.md create mode 100644 .claude/skills/code-analyzer/SKILL.md create mode 100644 .claude/skills/config-reader/SKILL.md create mode 100644 .claude/skills/diagram-guidelines/SKILL.md create mode 100644 .claude/skills/diagram-guidelines/examples/branch-summary.html create mode 100644 .claude/skills/diagram-guidelines/examples/by-app.html create mode 100644 .claude/skills/diagram-guidelines/examples/daily-summary.html create mode 100644 .claude/skills/diagram-guidelines/examples/timeline.html create mode 100644 .claude/skills/diagram-guidelines/examples/tips.html create mode 100644 .claude/skills/github-api/SKILL.md create mode 100755 .claude/skills/github-api/scripts/filter-commits-by-path.js create mode 100644 .claude/skills/github-api/scripts/get-all-branch-commits.js create mode 100644 .claude/skills/github-api/scripts/get-commits.js create mode 100644 .claude/skills/github-api/scripts/lib/date-utils.js create mode 100644 .claude/skills/github-api/scripts/lib/normalize.js create mode 100644 .claude/skills/screenshot-capture/SKILL.md create mode 100644 .claude/skills/screenshot-capture/scripts/capture-batch.js create mode 100644 .claude/skills/screenshot-capture/scripts/capture.js create mode 100644 .claude/skills/slack-formatting/SKILL.md create mode 100644 .claude/skills/slack-formatting/scripts/post-report.js create mode 100644 .github/workflows/daily-report.yml create mode 100644 .github/workflows/report-job.yml create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 configs/projects/your-project.yml create mode 100644 configs/repos/your-repo.yml create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.claude/prompts/daily-report.md b/.claude/prompts/daily-report.md new file mode 100644 index 0000000..1d25876 --- /dev/null +++ b/.claude/prompts/daily-report.md @@ -0,0 +1,409 @@ +昨日のコミットレポートを作成してSlackに投稿してください。 + +**重要**: このプロンプトの末尾に指定されたプロジェクト設定ファイルを使用してください。 + +## 参照スキル + +以下のスキルを参照してください: +- **config-reader**: 設定ファイルの読み方 +- **github-api**: GitHub APIでのコミット取得 +- **code-analyzer**: ビジネス視点での記述ルール(**禁止ワード確認**) +- **diagram-guidelines**: HTML図解のデザイン(**examples必読**) +- **slack-formatting**: Slack投稿の方法(複数画像まとめて投稿) +- **screenshot-capture**: スクリーンショット撮影 + +## 処理フロー + +### 1. 設定ファイルを読み込む +- プロジェクト設定ファイル(`configs/projects/*.yml`)を読み込む +- `repo_config` を参照してリポジトリ構造定義(`configs/repos/*.yml`)を読み込む +- `include_apps` / `include_categories` に基づいて対象アプリを特定 + +### 2. 全ブランチの昨日のコミットを一括取得 + +**⚠️ 重要: 必ず以下のスクリプトを使用** + +日付計算とブランチ走査は `github-api` スキルのスクリプトを使用してください。 +**手動で `date` コマンドやブランチ一覧取得を実行しないでください。** + +```bash +# 全ブランチのコミットを一括取得 +# ⚠️ 必ず --output オプションを使用(シェルリダイレクトは使わない) +node .claude/skills/github-api/scripts/get-all-branch-commits.js {owner} {repo} --output /tmp/all-commits.json +``` + +**スクリプトが自動で行うこと:** +1. **GraphQLで全ブランチの最終コミット日を取得**(4回のAPI呼び出し) +2. **最近アクティブなブランチだけ抽出**(過去7日以内 + デフォルトブランチ) +3. `dependabot/`、`renovate/` で始まるブランチの除外 +4. **絞り込んだブランチのみ**コミット取得(リトライ付き) +5. 失敗ブランチの自動再取得(10秒待機後に再試行) +6. それでも失敗した場合は exit 1 で処理中断 + +**最適化の効果:** +- 309ブランチ → 約20ブランチに絞り込み +- API呼び出し約90%削減 +- エラー発生リスクが大幅に低減 + +**環境変数:** +- `ACTIVE_DAYS`: アクティブ判定期間(デフォルト: 7日) + +### 3. パスでフィルタリング + +**⚠️ 重要: 必ず以下のスクリプトを使用。アドホックなフィルタリングコードは禁止。** + +```bash +node .claude/skills/github-api/scripts/filter-commits-by-path.js \ + --input /tmp/all-commits.json \ + --owner {owner} \ + --repo {repo} \ + --paths "{paths}" > /tmp/filtered-commits.json +``` + +`{paths}` は対象アプリの `path` をカンマ区切りで指定(例: `"app/,web/,supabase/"`)。 + +**スクリプトが自動で行うこと:** +- 各コミットの変更ファイルを GitHub API で取得 +- 指定パスに一致するファイルがあるコミットのみ抽出 +- 全コミットを確実に処理(件数制限なし) + +### 4. フィルタ結果を確認 + +フィルタ後の `/tmp/filtered-commits.json` を使用して以降の処理を行う。 + +**出力形式(正規化済み)**: +```json +{ + "metadata": { + "target_date": "2026-01-23", + "start_utc": "2026-01-22T15:00:00Z", + "end_utc": "2026-01-23T14:59:59Z", + "total_branches": 35, + "active_branches": 3, + "total_commits": 15, + "default_branch": "main", + "filter_paths": ["app/", "web/", "supabase/"], + "original_commits": 126, + "filtered_commits": 15 + }, + "branches": { + "main": { + "commits": [ + { + "sha": "abc1234", + "message": "コミットメッセージ", + "date": "2026-01-23T10:00:00Z", + "author": { + "login": "username", + "avatar_url": "https://avatars.githubusercontent.com/u/123?v=4" + }, + "html_url": "https://github.com/...", + "matched_files": [ + { "filename": "app/src/Login.tsx", "status": "modified", "additions": 15, "deletions": 3 } + ] + } + ], + "is_default": true + }, + "drill-dev": { + "commits": [...], + "is_default": false + } + } +} +``` + +**注意**: +- `author.login` と `author.avatar_url` は**スクリプトが保証**(jqで追加抽出不要) +- `author: null` の場合は自動的にGravatar fallbackが適用される +- そのままHTML生成に使用可能 +- `target_authors` が指定されていればフィルタ(空なら全員対象) +- `matched_files` に各コミットのマッチしたファイル情報が含まれる + +**⚠️ コントリビューター情報のデータ構造ルール:** + +スクリプト出力の `author` オブジェクトをそのまま使用すること。 +ユーザー名とアバターURLは**必ずペア(オブジェクト)として保持**。 + +```javascript +// ✅ 正しい: スクリプト出力をそのまま使用 +const contributors = commits.map(c => c.author); +// → [{ login: "user1", avatar_url: "..." }, ...] + +// ❌ 間違い: 分離して保持(絶対にやらない) +const logins = ["user1", "user2"] +const avatars = ["https://...", "https://..."] +``` + +### 5. ブランチの履歴とサマリーを分析 + +**デフォルトブランチとその他で扱いを変える:** + +#### デフォルトブランチ(main等)の場合 +- **「目的」は生成しない**(本番環境は目的を持たない) +- 代わりに「昨日反映された変更」として昨日のコミット内容を要約 +- 累計コミット数は表示しない(意味がないため) + +#### その他のブランチ(作業ブランチ)の場合 +- 直近30件のコミットを取得 +- 「昨日の作業内容」を**AIで1行に要約**(例: 「請求書処理フローの整理」) +- 累計コミット数、開始日(最古のコミット日)を特定 + +```bash +# ブランチの履歴取得(サマリー生成用) +gh api "repos/{owner}/{repo}/commits?sha={branch_name}&per_page=30" +``` + +### 6. コミットをアプリ別に分類 +- フィルタ済みコミットの `matched_files` を使用してアプリに分類 +- 対象アプリの `path` と照合してアプリに分類 +- **対象外のアプリへのコミットは除外** +- **ブランチ情報は維持**(どのブランチからのコミットかを記録) + +### 7. 変更内容を分析 +- 各コミットの差分(patch)を取得 +- **ビジネス視点で変更内容を説明** + - 「ユーザーにとって何が変わったか」を中心に記述 + - 抽象的な表現(バグ修正、調整、改善など)は禁止 + - 具体的な内容(〇〇できるようになった、〇〇の問題を解消など)を記述 + +### 8. HTML図解を生成(個別に) + +**重要: 各HTMLファイルを個別に生成すること。1つに統合しない。** + +**必ず最初に examples を読み込む:** +``` +.claude/skills/diagram-guidelines/examples/daily-summary.html # 昨日の開発(統合版) +.claude/skills/diagram-guidelines/examples/branch-summary.html # ブランチ詳細 +.claude/skills/diagram-guidelines/examples/by-app.html +.claude/skills/diagram-guidelines/examples/timeline.html +.claude/skills/diagram-guidelines/examples/tips.html +``` + +**生成するファイル:** + +1. `/tmp/daily-summary.html` - 昨日の開発(必須・常に1枚) + - 統計情報(コミット数、ブランチ数、コントリビューター数) + - ハイライト(誰が何をしたか、最大4件) + - **必須**: 各ハイライトに「誰がやったか」を表示(アバター + ユーザー名) + - **必須**: 各ハイライトにブランチタグを表示 + - **必須**: 説明文は**40文字以内**で簡潔に(見切れ防止) + - コントリビューター一覧 + +2. `/tmp/branch-summary-{branch}.html` - ブランチ詳細(各ブランチ1枚) + + **デフォルトブランチの場合:** + - ヘッダー: ブランチ名ラベル(緑) + - 「📥 昨日反映された変更」セクション + - 昨日のコミット内容を箇条書きで表示 + - 累計情報は表示しない + + **その他のブランチの場合:** + - ヘッダー: ブランチ名ラベル(青) + - 「📝 昨日の作業内容」セクション(AI生成サマリー) + - 統計: 累計/昨日/開発者数/経過日数 + - 昨日のハイライト(最大4件) + +3. `/tmp/by-app.html` - アプリ別(必須) + - アプリごとにセクション分け + - 各アプリ最大5件の変更を表示 **+ ブランチタグ** + +4. `/tmp/timeline.html` - タイムライン(必須) + - 時系列で作業履歴を表示 **+ ブランチタグ** + - 最大4件(超過分は「+N件」と表示) + +5. `/tmp/tips.html` - ワンポイントTIPS(`tips.enabled: true` の場合のみ) + - 昨日の変更に関連する豆知識を**自動判断**して生成 + - 関連する変更に**ブランチタグ**を表示 + - タイトルは `tips.title` があれば使用、なければ「ワンポイントTIPS」 + + **TIPS内容の自動判断ルール:** + - コード変更が多い場合 → **技術解説**(この機能の仕組み、アーキテクチャ説明) + - UI/デザイン変更 → **デザインTIPS**(なぜこのUIが使いやすいか) + - バグ修正 → **トラブルシューティング**(なぜこの問題が起きたか) + - 新機能追加 → **機能解説**(この機能で何ができるようになったか) + + **生成のポイント:** + - 非エンジニアにもわかりやすく + - 簡単な図解(フロー図やイラスト)を含める + - 「なぜこの仕組みがあるのか」を説明 + - `tips.prompt` が設定されていればその指示に従う + +**ブランチタグの色分けルール:** +| 判定方法 | 背景色 | ドット色 | ラベル | +|---------|--------|---------|--------| +| デフォルトブランチ | bg-green-100 | bg-green-500 | **ブランチ名そのまま**(main等) | +| fix/* prefix | bg-orange-100 | bg-orange-500 | **ブランチ名そのまま** | +| docs/* prefix | bg-purple-100 | bg-purple-500 | **ブランチ名そのまま** | +| その他全て | bg-blue-100 | bg-blue-500 | **ブランチ名そのまま** | + +**重要**: ラベルには常にブランチ名をそのまま表示する。色だけでブランチの種類を区別する。 + +**共通仕様:** +- サイズ: 420x650px 固定 +- アプリ設定の `name`, `short_name`, `color` を使用 +- アイコンはインラインSVG(Lucide互換) +- **全ページにブランチタグと凡例を含める** + +**⚠️ アバター画像の注意事項(重要):** + +1. **ペアで埋め込む**: ユーザー名とアバターは**必ず同じコミット/authorオブジェクトから**取得 +2. サンプルHTMLのプレースホルダーはコピーしない +3. **必ず** GitHub APIから取得した `author.avatar_url` を使用すること +4. サンプルのURLや架空のURLをコピーしない + +**HTML生成時の正しいパターン:** +```html + + +{{ contributor.login }} + + + +{{ logins[i] }} +``` + +**禁止パターン:** +- ユーザー名リストとアバターリストを別々に作成してインデックスで組み合わせる +- ユニークなユーザー名を抽出した後、別途アバターURLを検索して紐付ける + +### 9. スクリーンショットを撮影 +```bash +# 昨日の開発(常に1枚) +node .claude/skills/screenshot-capture/scripts/capture.js /tmp/daily-summary.html /tmp/daily-summary.png + +# ブランチ詳細(各ブランチ1枚) +node .claude/skills/screenshot-capture/scripts/capture.js /tmp/branch-summary-main.html /tmp/branch-summary-main.png +node .claude/skills/screenshot-capture/scripts/capture.js /tmp/branch-summary-feature-video.html /tmp/branch-summary-feature-video.png +# ... アクティブブランチの数だけ繰り返し + +# その他のレポート +node .claude/skills/screenshot-capture/scripts/capture.js /tmp/by-app.html /tmp/by-app.png +node .claude/skills/screenshot-capture/scripts/capture.js /tmp/timeline.html /tmp/timeline.png +node .claude/skills/screenshot-capture/scripts/capture.js /tmp/tips.html /tmp/tips.png # tips.enabled: true の場合 +``` + +### 10. Slackに投稿(まとめて) + +**⚠️ 重要: 必ず以下のスクリプトを使用すること。独自の方法で投稿しないこと。** + +```bash +# 必ずこのスクリプトを使用(curlを直接使わない) +node .claude/skills/slack-formatting/scripts/post-report.js \ + --message "メッセージテキスト" \ + /tmp/daily-summary.png \ + /tmp/branch-summary-*.png \ + /tmp/by-app.png \ + /tmp/timeline.png \ + /tmp/tips.png # tips.enabled: true の場合のみ +``` + +**禁止事項:** +- ❌ curlで直接Slack APIを呼び出す +- ❌ スレッドに画像を投稿する(thread_tsを使わない) +- ❌ テキストと画像を別々に投稿する +- ❌ 独自のSlack投稿ロジックを書く + +**このスクリプトが行うこと:** +- ✅ 複数画像を1メッセージにまとめて投稿 +- ✅ チャンネルに直接投稿(スレッドではない) +- ✅ 環境変数 `SLACK_CHANNEL` からチャンネルIDを取得 +- ✅ 未設定の場合はデバッグチャンネル `YOUR_DEBUG_CHANNEL_ID` に投稿 + +## 注意事項 + +- **対象アプリ以外のコミットはレポートに含めない** +- **すべての説明はビジネス視点で記述**(技術用語を避け、ユーザー影響を中心に) + +## Slack投稿に必ず含める情報 + +すべての投稿(コミットあり・なし両方)に以下を含めること: + +1. **監視期間**: いつからいつまでのコミットを確認したか + - 例: `期間: 2025-12-31 00:00 〜 23:59 (JST)` + +2. **監視対象ディレクトリ**: どのパスを監視しているか + - 対象アプリの `path` を一覧表示 + - 例: `監視対象: app/, web/, supabase/` + +### コミットがある場合のSlackメッセージ例 + +``` +📊 開発レポート - 昨日のコミットレポート + +期間: 2025-12-31 00:00 〜 23:59 (JST) +(実行日: 2026-01-01) +対象: 3ブランチ / 15コミット / 4名 + +🌱 *main* - 昨日反映された変更 (5件) +🔵 *feature/video-player* - 動画プレイヤー開発 (7件) +🟠 *fix/login-issue* - ログイン問題の修正 (3件) + +🐱 コミネコ で自動生成 +``` ++ 画像(daily-summary + 各branch-summary + by-app + timeline + tips) + +### コミットがない場合の投稿例 + +``` +📊 開発レポート - 昨日のコミットレポート + +期間: 2025-12-31 00:00 〜 23:59 (JST) +(実行日: 2026-01-01) +監視対象: app/, web/, supabase/ + +📭 昨日のコミットはありません +上記ディレクトリへの変更はありませんでした。 + +🐱 コミネコ で自動生成 +``` + +## 設定ファイルの指定方法 + +```bash +# レポートを生成 +claude "/daily-report を configs/projects/your-project.yml で実行" +``` + +## トラブルシューティング・チェックリスト + +**コミットが取得できない場合の確認事項:** + +1. **検索期間の確認** + - [ ] 検索開始/終了時刻がUTCで正しく計算されているか + - [ ] JSTの「前日00:00〜23:59」が正しくUTC変換されているか + - 例: JST 2026-01-18 00:00 → UTC 2026-01-17 15:00 + +2. **API呼び出しの確認** + - [ ] `since` と `until` の両方が指定されているか + - [ ] ブランチ名が正しいか(`sha=main` など) + +3. **デバッグ方法** + ```bash + # 検索期間を確認 + echo "検索開始: $YESTERDAY_JST_START" + echo "検索終了: $YESTERDAY_JST_END" + + # 取得件数を確認 + gh api "repos/{owner}/{repo}/commits?since=$YESTERDAY_JST_START&until=$YESTERDAY_JST_END" --jq 'length' + ``` + +**よくあるミス:** +- `date -u` だけ使う → UTCの当日00:00になる(JSTではない) +- `since` のみ指定 → 終了時刻がないため現在時刻まで取得 +- タイムゾーン未指定 → 実行環境のローカル時刻に依存 + +4. **ブランチ取得エラーの確認** + + スクリプトは失敗時に自動で exit 1 するため、手動確認は不要です。 + GitHub Actionsでは失敗として記録され、ワークフローを再実行できます。 + + **自動リカバリの仕組み:** + 1. 各ブランチ取得: 最大3回リトライ(指数バックオフ) + 2. 失敗ブランチ: 10秒後にまとめて再試行 + 3. それでも失敗: exit 1 で処理中断 + + **対処法:** + - GitHub API レート制限 → 時間を空けてワークフロー再実行 + - ネットワークエラー → ワークフロー再実行(自動リカバリで解決する場合が多い) + - 継続的に失敗 → GitHub Statusページでサービス状態を確認 diff --git a/.claude/skills/code-analyzer/SKILL.md b/.claude/skills/code-analyzer/SKILL.md new file mode 100644 index 0000000..4d55a7a --- /dev/null +++ b/.claude/skills/code-analyzer/SKILL.md @@ -0,0 +1,140 @@ +--- +name: code-analyzer +description: コミットのコード変更を分析し、ビジネス視点で具体的な説明を生成する。 +--- + +# Code Analyzer + +コミットの差分を分析し、**ビジネス視点**で具体的な変更内容を生成します。 + +## 原則 + +**「ユーザーにとって何が変わったか」を中心に書く** + +技術的な変更内容ではなく、ユーザーや利用者への影響を説明します。 + +## 禁止ワード + +以下の抽象的な表現は使用禁止です: + +| 禁止 | 代わりに書くべき内容 | +|------|----------------------| +| バグ修正 | 〇〇できなかった問題を解消 | +| 修正 | 〇〇の問題を解消 / 〇〇が正しく動作するように | +| 調整 | 〇〇が見やすく/使いやすくなった | +| 改善 | 〇〇が速く/簡単になった | +| 対応 | 〇〇できるようになった | +| 変更 | 〇〇を〇〇に変えた(具体的に) | +| 更新 | 〇〇を最新の〇〇に対応 | +| リファクタリング | 〇〇の動作が安定した / 〇〇の処理が速くなった | + +## 分析プロセス + +### 1. 変更ファイルのパスを確認 + +ファイルパスから機能を推測: + +``` +app/VideoPlayer/ → 動画再生機能 +app/Comments/ → コメント機能 +app/Profile/ → プロフィール機能 +contents/tools/ → コンテンツ制作ツール +``` + +### 2. 変更内容を確認 + +diff から具体的な変更を特定: + +```diff +- seekTo(position) ++ seekTo(position + offset) +``` +→ 「動画の再生位置に関する変更」 + +### 3. ビジネス視点に変換 + +技術的な変更をユーザー影響に翻訳: + +``` +技術: seekTo() のオフセット計算を修正 + ↓ +ビジネス: 動画を途中から再生したとき、正しい位置から始まるようになった +``` + +## 出力フォーマット + +### 単一の変更 + +``` +動画を途中から再生できなかった問題を解消 +``` + +### 複数の変更 + +``` +動画プレイヤーの操作性を改善 +• ミニプレイヤーに戻るボタンを追加 +• 再開時に正しい位置から再生されるように +``` + +## 良い例・悪い例 + +### 例1: バグ修正 + +``` +❌ 悪い: "バグ修正" +❌ 悪い: "seekTo関数のバグを修正" +✅ 良い: "動画を途中から再生できなかった問題を解消" +``` + +### 例2: UI変更 + +``` +❌ 悪い: "デザイン調整" +❌ 悪い: "paddingを8pxから12pxに変更" +✅ 良い: "コメント欄が読みやすくなった" +``` + +### 例3: 新機能 + +``` +❌ 悪い: "ボタン追加" +❌ 悪い: "ImportanceButtonコンポーネントを追加" +✅ 良い: "コメントに重要度マークを付けられるようになった" +``` + +### 例4: リファクタリング + +``` +❌ 悪い: "リファクタリング" +❌ 悪い: "状態管理をReduxに移行" +✅ 良い: "コメント機能の動作が安定した" +``` + +### 例5: 複数変更 + +``` +❌ 悪い: "モーダル修正、ミニプレイヤー改善、再生問題解消" +✅ 良い: +動画プレイヤーの使い勝手を改善 +• 詳細画面のモーダルが正しく表示されるように +• ミニプレイヤーから元の画面に戻れるように +• 途中再生時の位置ズレを解消 +``` + +## GitHub API での diff 取得 + +```bash +# コミットの変更ファイル一覧 +gh api repos/{owner}/{repo}/commits/{sha} --jq '.files[].filename' + +# コミットの diff を取得 +gh api repos/{owner}/{repo}/commits/{sha} --jq '.files[] | "\(.filename):\n\(.patch)"' +``` + +## 注意事項 + +- 技術用語は避け、一般的な言葉を使う +- 「〜できるようになった」「〜の問題を解消」など結果を書く +- 1つのコミットに複数の変更がある場合は箇条書きで列挙 +- マージコミットの場合は PR のタイトル/説明を参照 diff --git a/.claude/skills/config-reader/SKILL.md b/.claude/skills/config-reader/SKILL.md new file mode 100644 index 0000000..1d6abd0 --- /dev/null +++ b/.claude/skills/config-reader/SKILL.md @@ -0,0 +1,185 @@ +--- +name: config-reader +description: プロジェクト設定ファイルの構造と読み方。設定ファイルを扱うときに参照。 +--- + +# Config Reader + +プロジェクト設定ファイルの構造と使用方法を説明します。 + +## ファイル構造(2ファイル分離) + +``` +configs/ +├── repos/ # リポジトリ構造定義(共有) +│ └── your-repo.yml +└── projects/ # プロジェクト設定(個別) + └── your-project.yml +``` + +### 分離のメリット +- **再利用性**: リポジトリ構造は1箇所で管理、複数プロジェクトから参照 +- **柔軟性**: プロジェクトごとに対象アプリを選択可能 +- **保守性**: アプリ追加時はrepo定義のみ更新 + +--- + +## リポジトリ構造定義(repos/*.yml) + +```yaml +repository: + owner: your-username + name: your-web-app + description: リポジトリの説明 + +# 全アプリ/ツールの定義 +apps: + - id: my-app # 一意のID(プロジェクトから参照) + path: "app/" # ディレクトリパス + name: "Webアプリ" # 正式名称(レポートで使用) + short_name: "アプリ" # 短縮名(タグで使用) + icon: "smartphone" # アイコン名 + color: "blue" # Tailwind色名 + category: "main" # カテゴリID + +# カテゴリ定義 +categories: + main: + name: "Webプラットフォーム" + description: "Webサービスのコアシステム" +``` + +### フィールド説明 + +| フィールド | 用途 | +|-----------|------| +| `id` | アプリの一意識別子(プロジェクトから参照) | +| `path` | コミット分類用のディレクトリパス | +| `name` | 正式名称(アプリ別レポートのセクション名) | +| `short_name` | 短縮名(サマリーのタグ表示) | +| `icon` | Lucideアイコン名 | +| `color` | Tailwind CSS色名 | +| `category` | カテゴリID(グループ化用) | + +--- + +## プロジェクト設定(projects/*.yml) + +```yaml +project: + name: "開発プロジェクト" + description: "プロジェクトの説明" + +# 参照するリポジトリ定義(相対パス) +repo_config: "repos/your-repo.yml" + +# 対象アプリの指定(2つの方法) +include_apps: # 方法1: IDで個別指定 + - my-app + - my-web + - my-backend + +include_categories: # 方法2: カテゴリで一括指定 + - main + +# Slack設定 +slack: + token_env: SLACK_BOT_TOKEN # トークンの環境変数名 + channel_env: SLACK_CHANNEL # チャンネルIDの環境変数名 + channel_name: "#your-channel" # 参考用 + +# レポート対象メンバー(空 = 全員) +target_authors: [] + +# ワンポイントTIPS設定(オプション) +tips: + enabled: true # TIPSを生成するか + # 以下はオプション(省略時はAIが変更内容から自動判断) + # title: "ワンポイントTIPS" # 図解のタイトル(デフォルト: "ワンポイントTIPS") + # prompt: "カスタムプロンプト" # TIPS生成の指示(省略推奨) +``` + +### アプリ指定の優先順位 + +1. `include_apps` が指定されている場合 → そのIDのアプリのみ対象 +2. `include_categories` のみ指定 → そのカテゴリに属するアプリが対象 +3. 両方指定 → `include_apps` と `include_categories` の和集合 + +--- + +## 読み込み手順 + +### 1. プロジェクト設定を読み込む + +```bash +cat configs/projects/your-project.yml +``` + +### 2. repo_config を解決してリポジトリ定義を読み込む + +```bash +# repo_config: "repos/your-repo.yml" の場合 +cat configs/repos/your-repo.yml +``` + +### 3. 対象アプリをフィルタリング + +```python +import yaml + +# プロジェクト設定を読み込み +with open('configs/projects/your-project.yml') as f: + project = yaml.safe_load(f) + +# リポジトリ定義を読み込み +repo_path = f"configs/{project['repo_config']}" +with open(repo_path) as f: + repo = yaml.safe_load(f) + +# 対象アプリをフィルタリング +include_apps = set(project.get('include_apps', [])) +include_categories = set(project.get('include_categories', [])) + +target_apps = [] +for app in repo['apps']: + if app['id'] in include_apps: + target_apps.append(app) + elif app['category'] in include_categories: + target_apps.append(app) + +# include_apps も include_categories も空なら全アプリ対象 +if not include_apps and not include_categories: + target_apps = repo['apps'] +``` + +--- + +## コミットのアプリ分類 + +変更ファイルのパスから、どのアプリに属するか判定: + +```python +def classify_commit(changed_files, target_apps): + """コミットの変更ファイルからアプリを特定""" + app_commits = {} + + for file_path in changed_files: + for app in target_apps: + if file_path.startswith(app['path']): + app_id = app['id'] + if app_id not in app_commits: + app_commits[app_id] = [] + app_commits[app_id].append(file_path) + break + + return app_commits +``` + +--- + +## 使用例 + +```bash +# レポートを生成 +claude "configs/projects/your-project.yml を使って今日のレポートを作成して" +``` diff --git a/.claude/skills/diagram-guidelines/SKILL.md b/.claude/skills/diagram-guidelines/SKILL.md new file mode 100644 index 0000000..87d7325 --- /dev/null +++ b/.claude/skills/diagram-guidelines/SKILL.md @@ -0,0 +1,348 @@ +--- +name: diagram-guidelines +description: HTML図解のデザインガイドライン。図解を作成するときに参照。 +--- + +# 図解デザインガイドライン + +コミット情報をHTML図解に変換する際のデザイン基準です。 + +## 必須手順(最重要) + +**図解生成の前に、以下のファイルを必ず読み込んでください:** + +``` +.claude/skills/diagram-guidelines/examples/daily-summary.html # 今日の開発(統合版) +.claude/skills/diagram-guidelines/examples/branch-summary.html # ブランチ詳細 +.claude/skills/diagram-guidelines/examples/by-app.html +.claude/skills/diagram-guidelines/examples/timeline.html +.claude/skills/diagram-guidelines/examples/tips.html +``` + +これらのexamplesと**同じデザインパターン**で生成してください。自己流で作らないこと。 + +## サイズ仕様(固定) + +| 項目 | 値 | +|------|-----| +| **幅** | 420px 固定 | +| **高さ** | 600px 固定 | +| **スクリーンショット** | 840 x 1200px(Retina 2x) | + +コンテンツが収まらない場合は、項目数を減らすか複数枚に分割してください。 + +## 出力ファイル構成(5種類) + +| ファイル | 名称 | 役割 | +|----------|------|------| +| **daily-summary.html** | 今日の開発 | 統計情報 + ハイライト(誰が何をしたか、最大4件) | +| **branch-summary.html** | ブランチ詳細 | デフォルトブランチ: 今日反映された変更 / その他: 今日の作業内容 | +| by-app.html | アプリ別 | アプリごとの変更一覧(最大5件/アプリ)、ブランチタグ付き | +| timeline.html | タイムライン | 時系列での作業履歴(**最大4件**)、ブランチタグ付き | +| tips.html | ワンポイントTIPS | 今日の変更に関連する豆知識(設定で有効時のみ)、ブランチタグ付き | + +**ページ分割ルール:** +- コンテンツが多い場合は `by-app-1.html`, `by-app-2.html` のように分割 +- タイムラインは4件を超えるとフッターが見切れるため、残りは「+N more commits」で表示 +- TIPSはプロジェクト設定の `tips.enabled` が `true` の場合のみ生成 + +## ブランチタグ(全ページ共通) + +すべてのコミット/ハイライト項目には**ブランチタグ**を表示します。 + +### ブランチの分類 + +**重要**: ブランチ名の prefix(feature/、fix/等)ではなく、**デフォルトブランチかどうか**で判断します。 + +```bash +# デフォルトブランチの取得 +DEFAULT_BRANCH=$(gh api repos/{owner}/{repo} --jq '.default_branch') +``` + +### ブランチタグの色分け + +| 判定方法 | 背景色 | テキスト色 | ドット色 | +|---------|--------|-----------|---------| +| デフォルトブランチ | bg-green-100 | text-green-600/700 | bg-green-500 | +| fix/* prefix | bg-orange-100 | text-orange-600/700 | bg-orange-500 | +| docs/* prefix | bg-purple-100 | text-purple-600/700 | bg-purple-500 | +| その他全て | bg-blue-100 | text-blue-600/700 | bg-blue-500 | + +**重要**: ラベルは常に**ブランチ名をそのまま表示**する(main, feature/xxx, fix/xxx等)。色だけで種類を区別する。 + +### ブランチタグのHTML + +```html + +
+
+ feature/video +
+ + + + +``` + +### フッターの凡例 + +すべてのページに**ブランチ凡例**を含めます(色のみで区別): + +```html +
+ + デフォルト + + + 作業 + + + fix + +
+``` + +※ 凡例は「色の意味」を示すもの。実際のタグにはブランチ名を表示する。 + +## ビジネス視点での記述 + +**重要**: すべての説明は「ユーザーにとって何が変わったか」を中心に記述します。 + +### 禁止表現と置き換え + +| 禁止 | 代わりに書くべき内容 | +|------|----------------------| +| バグ修正 | 〇〇できなかった問題を解消 | +| 調整 | 〇〇が見やすく/使いやすくなった | +| 改善 | 〇〇が速く/簡単になった | +| 対応 | 〇〇できるようになった | +| リファクタリング | 〇〇の動作が安定した | + +詳細は **code-analyzer** スキルを参照してください。 + +## 技術スタック + +- **Tailwind CSS**: CDN版を使用 `` +- **shadcn/ui風デザイン**: slate系カラー、rounded-lg、shadow-sm +- **アイコン**: インラインSVG(Lucide互換)を使用、絵文字は使わない +- **GitHubアバター**: `https://avatars.githubusercontent.com/u/{user_id}?v=4` + +## 基本レイアウト + +```html + + + + + + レポート + + + + +
+ +
+ + +``` + +**重要**: +- 幅 `420px`、高さ `600px` 固定 +- `flex flex-col` でコンテンツを配置 +- `min-height: 100vh` は使わない + +## カラーパレット(shadcn/ui準拠) + +| 用途 | Tailwindクラス | +|------|----------------| +| 背景 | bg-white, bg-slate-50 | +| テキスト(メイン) | text-slate-900 | +| テキスト(サブ) | text-slate-500, text-slate-400 | +| ボーダー | border-slate-200 | +| アクセント(青) | bg-blue-500, text-blue-600 | +| アクセント(紫) | bg-purple-500, text-purple-600 | +| アクセント(緑) | bg-emerald-500, text-emerald-600 | +| アクセント(オレンジ/ハイライト) | bg-amber-500, text-amber-600 | + +## コンポーネント + +### ヘッダー(共通) + +```html +
+
+
+ +
+
+

タイトル

+

リポジトリ名

+
+
+
12/25 - 26
+
+``` + +### 統計バー(サマリー用) + +```html +
+
+
10
+
commits
+
+
+
+
4
+
contributors
+
+
+``` + +### ハイライトアイテム(サマリー用) + +```html +
+
+
動画を途中から再生できるようになった
+
+``` + +### アプリ別セクション(by-app用) + +```html +
+
+
+ +
+
アプリ名
+
(path/)
+
+
+ +
+ +
+
+``` + +### 変更アイテム(by-app用) + +```html +
+
+
+
動画を途中から再生できるようになった
+
補足説明
+
+
+``` + +### タイムラインアイテム(timeline用) + +```html +
+
+
+
+
+ + username +
+ 12/26 12:01 +
+
変更内容
+
+
+ アプリ名 +
+
+
+``` + +### フッター(共通) + +```html +
+
+
+ + +
+
Generated by Claude Code
+
+
+``` + +### ワンポイントTIPS(tips用) + +TIPSは以下の構成で生成する: + +1. **ヘッダー**: 電球アイコン + タイトル(設定の `tips.title` を使用) +2. **関連する変更**: どの変更に関連するTIPSか +3. **トピックタイトル**: 解説するテーマ +4. **図解**: 簡単なフロー図やイラスト(bg-slate-50の中にボックスを並べる) +5. **解説テキスト**: わかりやすい説明 +6. **今回の修正**: このTIPSと今日の変更の関連 +7. **フッター**: カテゴリラベル + Generated by Claude Code + +**レイアウト制約(重要):** +- **図解は3ステップまで**に収める(4ステップ以上は見切れる原因) +- 各要素の margin/padding を小さめに(mb-3, p-2.5 など) +- 解説テキストは2段落以内に +- 600px内に収まるか確認してからスクリーンショット + +TIPSの内容はプロジェクト設定の `tips.prompt` に従って生成する(省略時は変更内容から自動判断)。 + +## よく使うアイコン(インラインSVG) + +### GitHubロゴ +```html + + + +``` + +### スター(ハイライト) +```html + + + +``` + +### スマートフォン(モバイルアプリ) +```html + + + +``` + +### パズル(ツール/プラグイン) +```html + + + +``` + +## スクリーンショット撮影 + +**screenshot-capture** スキルのスクリプトを使用: + +```bash +node .claude/skills/screenshot-capture/scripts/capture.js input.html output.png +``` + +出力サイズ: 840 x 1200px(Retina 2x) + +## 注意事項 + +- 絵文字は使用しない(プロフェッショナルなアイコンを使用) +- GitHubアバターを積極的に使用 +- モノレポの場合は必ずアプリ別にグループ化 +- 日本語で説明文を記述 +- **すべての説明はビジネス視点で記述** +- "Generated by Claude Code" をフッターに含める diff --git a/.claude/skills/diagram-guidelines/examples/branch-summary.html b/.claude/skills/diagram-guidelines/examples/branch-summary.html new file mode 100644 index 0000000..459e7e0 --- /dev/null +++ b/.claude/skills/diagram-guidelines/examples/branch-summary.html @@ -0,0 +1,183 @@ + + + + + + ブランチサマリー + + + + +
+ + + + +
+
+ +
+ + + +
+
+

{branch_name}

+

{repository_name}

+
+
+ + {branch_name} +
+ + +
+
+ + + +
+

今日反映された変更

+ +

{today_changes_summary}

+
+
+
+ + +
+
+
+{today_commits}
+
今日の反映
+
+
+
+
{contributor_count}
+
コントリビューター
+
+
+ + + + +
+
+ + + + 今日のハイライト +
+
+ +
+
+ +
+ + 改善 +
+
+ +
{highlight_description}
+
+
+
+ + +
+
+
+ + 最終: {last_commit_datetime} +
+
Generated by Claude Code
+
+
+ +
+ + diff --git a/.claude/skills/diagram-guidelines/examples/by-app.html b/.claude/skills/diagram-guidelines/examples/by-app.html new file mode 100644 index 0000000..2319caa --- /dev/null +++ b/.claude/skills/diagram-guidelines/examples/by-app.html @@ -0,0 +1,134 @@ + + + + + + アプリ別レポート + + + + +
+ + +
+
+
+ + + +
+
+

アプリ別レポート

+

your-web-app

+
+
+
1/15 - 16
+
+ + +
+
+
+ + + +
+
Webサイト
+
(web/)
+
+
+ + +
+
+ +
+
+ {branch_short_name} +
+ {change_description} +
+ + +
+ +
+
+ + + +
+ + {contributor1.login}, {contributor2.login} +
+
+ + +
+
+
+ + + +
+
APIサーバー
+
(api/)
+
+
+ + +
+
+
+
+ main +
+ {change_description} +
+
+ +
+
+ +
+ {contributor.login} +
+
+ + +
+
+
+ デフォルト + 作業 + fix +
+
Generated by Claude Code
+
+
+ +
+ + diff --git a/.claude/skills/diagram-guidelines/examples/daily-summary.html b/.claude/skills/diagram-guidelines/examples/daily-summary.html new file mode 100644 index 0000000..67a712d --- /dev/null +++ b/.claude/skills/diagram-guidelines/examples/daily-summary.html @@ -0,0 +1,208 @@ + + + + + + 今日の開発 + + + + +
+ + + + +
+
+
+ + + +
+
+

今日の開発

+

{repository_name}

+
+
+
{date}
+
+ + +
+
+
{total_commits}
+
commits
+
+
+
+
{branch_count}
+
branches
+
+
+
+
{contributor_count}
+
contributors
+
+
+ + +
+
+ + + + ハイライト +
+ + +
+ + +
+
+
+ + + {username} +
+ +
+
+ {branch_name} +
+
+
{highlight_description}
+
+ + +
+
+
+ + {username} +
+
+
+ {branch_name} +
+
+
{highlight_description}
+
+ + +
+
+
+ + {username} +
+
+
+ {branch_name} +
+
+
{highlight_description}
+
+ + +
+
+
+ + {username} +
+
+
+ {branch_name} +
+
+
{highlight_description}
+
+ + + + +
+
+ + + +
+
+
+
+ + + +
+ + {contributor_summary} +
+
Generated by Claude Code
+
+
+ +
+ + diff --git a/.claude/skills/diagram-guidelines/examples/timeline.html b/.claude/skills/diagram-guidelines/examples/timeline.html new file mode 100644 index 0000000..2091d72 --- /dev/null +++ b/.claude/skills/diagram-guidelines/examples/timeline.html @@ -0,0 +1,120 @@ + + + + + + タイムライン + + + + +
+ + +
+
+
+ + + +
+
+

タイムライン

+

your-web-app

+
+
+
1/15 - 16
+
+ + +
+
+ +
+ + + + +
+
+
+
+
+ + + {username} +
+ +
+
+ {branch_name} +
+
+
{commit_description}
+
+
+ {app_name} +
+ {time} +
+
+
+ + + + +
+
+ + +
+ +{remaining_commits} more commits +
+ + +
+
+
+ デフォルト + 作業 + fix +
+
Generated by Claude Code
+
+
+ +
+ + diff --git a/.claude/skills/diagram-guidelines/examples/tips.html b/.claude/skills/diagram-guidelines/examples/tips.html new file mode 100644 index 0000000..c7326ca --- /dev/null +++ b/.claude/skills/diagram-guidelines/examples/tips.html @@ -0,0 +1,118 @@ + + + + + + ワンポイントTIPS + + + + +
+ + +
+
+
+ + + +
+
+

ワンポイントTIPS

+

今日の変更から学ぶ

+
+
+
1/15
+
+ + +
+
+ + + + 関連する変更 + +
+
+ {branch_name} +
+
+
{related_change_description}
+
+ + +
+

動画プレイヤーの仕組み

+
+ + +
+
+
+ 動画ファイル(圧縮) +
+ + + +
+ デコーダー(解凍処理) +
+ + + +
+ 画面に表示 +
+
+
+ + +
+

+ 動画は圧縮された状態で保存されており、再生時にリアルタイムで解凍しています。この処理が端末の性能に追いつかないとカクつきやフリーズの原因になります。 +

+
+
+ + + +

+ 今回の修正: + Android端末向けに解凍処理を最適化しました +

+
+
+
+ + +
+
+
+
+ 技術豆知識 +
+
Generated by Claude Code
+
+
+ +
+ + diff --git a/.claude/skills/github-api/SKILL.md b/.claude/skills/github-api/SKILL.md new file mode 100644 index 0000000..7a87079 --- /dev/null +++ b/.claude/skills/github-api/SKILL.md @@ -0,0 +1,403 @@ +--- +name: github-api +description: GitHub REST APIの使い方。コミット取得、差分取得、リポジトリ情報取得に使用。 +allowed-tools: Bash(node:*), Bash(gh:*) +--- + +# GitHub API ガイド + +GitHub REST API の使用方法を説明します。 + +## ⚠️ 重要: コミット取得はスクリプトを使用 + +**前日のコミット取得は必ず以下のスクリプトを使用してください。** +日付計算を手動で行わないでください。 + +### 全ブランチ一括取得(推奨) + +```bash +# ⚠️ 必ず --output オプションを使用(シェルリダイレクトは使わない) +node .claude/skills/github-api/scripts/get-all-branch-commits.js --output /tmp/all-commits.json +``` + +全ブランチのコミットを一括で取得します。daily-reportではこちらを使用してください。 + +**重要**: `--output` オプションを使用してファイルに直接書き込んでください。 +シェルリダイレクト(`> file`)は stdout/stderr が混在する問題があります。 + +**最適化版**: GraphQLで最近アクティブなブランチのみを抽出してからコミット取得。 +309ブランチ → 約20ブランチに絞り込み(約90%のAPI呼び出し削減)。 + +環境変数 `ACTIVE_DAYS` でアクティブ判定期間を変更可能(デフォルト: 7日)。 + +### パスフィルタリング + +```bash +node .claude/skills/github-api/scripts/filter-commits-by-path.js \ + --owner --repo --paths "app/,web/,supabase/" +``` + +`get-all-branch-commits.js` の出力を受け取り、指定パスに関連するコミットのみを抽出します。 +各コミットの変更ファイルを GitHub API で取得し、パスマッチングを行います。 + +**⚠️ 重要: コミットのパスフィルタリングは必ずこのスクリプトを使用してください。** +アドホックなフィルタリングコードは全件処理を保証できません。 + +| 引数 | 必須 | 説明 | +|------|------|------| +| `--input ` | No | 入力ファイル(省略時stdin) | +| `--owner ` | Yes | リポジトリオーナー | +| `--repo ` | Yes | リポジトリ名 | +| `--paths ` | Yes | カンマ区切りパス(例: "app/,web/") | +| `--concurrency ` | No | 並列数(デフォルト: 5) | + +使用例: +```bash +# パイプライン使用 +node .claude/skills/github-api/scripts/get-all-branch-commits.js owner repo \ + | node .claude/skills/github-api/scripts/filter-commits-by-path.js \ + --owner owner --repo repo --paths "app/,web/,supabase/" + +# ファイル入力 +node .claude/skills/github-api/scripts/filter-commits-by-path.js \ + --input /tmp/commits.json \ + --owner owner --repo repo --paths "app/,web/,supabase/" +``` + +出力形式は入力と同じ構造 + 追加フィールド: +- `metadata.filter_paths`: フィルタ対象パス +- `metadata.original_commits`: フィルタ前のコミット数 +- `metadata.filtered_commits`: フィルタ後のコミット数 +- 各コミットに `matched_files` フィールド(マッチしたファイル情報) + +### 単一ブランチ取得 + +```bash +node .claude/skills/github-api/scripts/get-commits.js [branch] +``` + +## 出力形式 + +スクリプトは正規化されたJSONを出力します。**jqでの追加処理は不要です。** + +### 全ブランチ一括取得の出力 + +```json +{ + "metadata": { + "target_date": "2026-01-23", + "start_utc": "2026-01-22T15:00:00Z", + "end_utc": "2026-01-23T14:59:59Z", + "total_branches": 308, + "checked_branches": 18, + "active_branches": 6, + "total_commits": 31, + "default_branch": "main", + "active_days_filter": 7, + "failed_branches": 0, + "has_errors": false + }, + "branches": { + "main": { + "commits": [...], + "is_default": true + }, + "feature-branch": { + "commits": [...], + "is_default": false + } + } +} +``` + +| フィールド | 説明 | +|-----------|------| +| `metadata.target_date` | 対象日(JST) | +| `metadata.total_branches` | 全ブランチ数 | +| `metadata.checked_branches` | チェックしたブランチ数(最適化後) | +| `metadata.active_branches` | コミットがあるブランチ数 | +| `metadata.default_branch` | デフォルトブランチ名 | +| `metadata.active_days_filter` | アクティブ判定期間(日数) | +| `metadata.failed_branches` | 取得失敗したブランチ数 | +| `metadata.has_errors` | エラーがあったかどうか | +| `branches[name].commits` | コミット配列 | +| `branches[name].is_default` | デフォルトブランチかどうか | +| `errors` | 失敗したブランチの詳細(エラー時のみ) | + +### エラーハンドリング(自動リカバリ) + +スクリプトは以下の自動リカバリ機能を持ちます: + +1. **個別リトライ**: 各ブランチ取得は最大3回リトライ(指数バックオフ: 1秒、2秒、4秒) +2. **一括再試行**: 1回目のループで失敗したブランチは、10秒後にまとめて再試行 +3. **確実な失敗検出**: 再試行でも失敗した場合、exit 1 で終了 + +**動作フロー:** +``` +1回目のループ (308ブランチ) + → 各ブランチ最大3回リトライ + → 失敗: 2ブランチ + +10秒待機 + +再試行ループ (2ブランチ) + → 各ブランチ最大3回リトライ + → 失敗: 0ブランチ → 正常終了 (exit 0) + → 失敗: 1ブランチ以上 → 異常終了 (exit 1) +``` + +**GitHub Actionsでの動作:** +- exit 1 → ワークフロー失敗として記録 +- 「Re-run jobs」で再実行可能 +- 一時的なAPI不安定は自動リカバリで解決する場合が多い + +### 単一ブランチ取得の出力 + +### 保証される項目 +| フィールド | 説明 | +|-----------|------| +| `sha` | コミットハッシュ | +| `message` | コミットメッセージ | +| `date` | コミット日時(ISO 8601) | +| `author.login` | ユーザー名(**必ず存在**) | +| `author.avatar_url` | アバターURL(**必ず存在**) | +| `html_url` | GitHubへのリンク | + +### 出力例 +```json +[ + { + "sha": "abc1234567890", + "message": "コミットメッセージ", + "date": "2026-01-19T10:00:00Z", + "author": { + "login": "username", + "avatar_url": "https://avatars.githubusercontent.com/u/123?v=4" + }, + "html_url": "https://github.com/..." + } +] +``` + +### フォールバック +GitHub APIで `author: null` の場合(メールがGitHubに紐付いていない): +- `login`: コミッター名を使用 +- `avatar_url`: Gravatar identicon(メールハッシュから生成) + +フォールバック発生時は標準エラーにログ出力: +``` +[Avatar] フォールバック: Unknown User (8cca2ef) +``` + +### 例 +```bash +# your-username/your-web-app の main ブランチ +node .claude/skills/github-api/scripts/get-commits.js your-username your-web-app main + +# 出力例(標準エラー): +# === 検索期間 === +# 対象日(JST): 2026-01-18 +# 開始(UTC): 2026-01-17T15:00:00Z +# 終了(UTC): 2026-01-18T14:59:59Z +# ブランチ: main +# ================ +``` + +### 環境変数 +- `GH_TOKEN`: GitHub API認証トークン(必須) +- `TARGET_DATE`: 対象日を指定(省略時は前日JST)形式: `YYYY-MM-DD` + +## 禁止事項 + +❌ `date -u` や `date -d` で日付計算を手動実行 +❌ `since=$TODAY` のような変数を自分で定義 +❌ タイムゾーン変換を手動で計算 + +## 理由 + +日付計算は以下の問題が起きやすい: +- `date -u` はUTCの「当日」を返す(JSTではない) +- macOS と Linux で `date` のオプションが異なる +- タイムゾーン変換の計算ミス + +スクリプトはテスト済みで、毎回同じ結果を保証します。 + +--- + +## 認証 + +環境変数 `GH_TOKEN` を使用: + +```bash +curl -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/..." +``` + +## エンドポイント + +### コミット一覧取得 + +``` +GET /repos/{owner}/{repo}/commits +``` + +パラメータ: +- `since`: ISO 8601形式の日時(この日時以降のコミット) +- `author`: 作者でフィルタ + +レスポンス: +```json +[ + { + "sha": "abc1234567890", + "commit": { + "message": "コミットメッセージ", + "author": { + "name": "Author Name", + "date": "2025-01-01T12:00:00Z" + } + }, + "author": { + "login": "github-username", + "id": 1234567, + "avatar_url": "https://avatars.githubusercontent.com/u/1234567?v=4" + }, + "html_url": "https://github.com/..." + } +] +``` + +### ⚠️ 図解で使用する重要フィールド + +HTML図解を生成する際、以下のフィールドを**必ず**APIレスポンスから取得すること: + +| フィールド | 用途 | 注意 | +|-----------|------|------| +| `author.login` | ユーザー名表示 | サンプルの値をコピーしない | +| `author.avatar_url` | アバター画像 | **必ずAPIから取得** | +| `commit.author.date` | 日時表示 | ISO 8601形式 | + +**禁止事項:** +- サンプルHTMLのURLをそのまま使用 +- 架空のavatar_urlを生成 +- 他のユーザーのavatar_urlを流用 + +### コミット詳細取得(差分含む) + +``` +GET /repos/{owner}/{repo}/commits/{sha} +``` + +レスポンス: +```json +{ + "sha": "abc1234567890", + "files": [ + { + "filename": "src/index.ts", + "status": "modified", + "additions": 10, + "deletions": 5, + "patch": "@@ -1,5 +1,10 @@\n-old\n+new" + } + ], + "stats": { + "additions": 10, + "deletions": 5, + "total": 15 + } +} +``` + +### リポジトリ情報取得 + +``` +GET /repos/{owner}/{repo} +``` + +レスポンス: +```json +{ + "name": "repo-name", + "description": "Repository description", + "language": "TypeScript", + "default_branch": "main" +} +``` + +### ブランチ一覧取得 + +``` +GET /repos/{owner}/{repo}/branches +``` + +パラメータ: +- `per_page`: 1ページあたりの件数(最大100) + +例: +```bash +curl -H "Authorization: Bearer $GH_TOKEN" \ + "https://api.github.com/repos/owner/repo/branches?per_page=100" +``` + +レスポンス: +```json +[ + { + "name": "main", + "commit": { + "sha": "abc1234567890", + "url": "https://api.github.com/repos/owner/repo/commits/abc1234567890" + }, + "protected": true + }, + { + "name": "feature/video-player", + "commit": { + "sha": "def5678901234", + "url": "https://api.github.com/repos/owner/repo/commits/def5678901234" + }, + "protected": false + } +] +``` + +### ブランチ別コミット取得 + +特定ブランチのコミットを取得するには `sha` パラメータにブランチ名を指定: + +``` +GET /repos/{owner}/{repo}/commits?sha={branch_name}&since={date} +``` + +**⚠️ 日付計算は必ずスクリプトを使用してください**(上記「禁止事項」参照)。 + +### ブランチの累計コミット取得 + +ブランチの履歴を把握するため、直近N件のコミットを取得: + +```bash +# 直近30件のコミットを取得(ブランチの概要把握用) +curl -H "Authorization: Bearer $GH_TOKEN" \ + "https://api.github.com/repos/owner/repo/commits?sha=feature/video-player&per_page=30" +``` + +**用途:** +- ブランチの目的をAIで要約 +- 累計コミット数の表示 +- ブランチ開始日の特定(最古のコミット日時) + +## エラーハンドリング + +| ステータス | 意味 | 対処 | +|-----------|------|------| +| 401 | 認証エラー | GH_TOKEN を確認 | +| 403 | レート制限 | 待機または認証確認 | +| 404 | リポジトリ不在 | owner/repo を確認 | + +## レート制限 + +- 認証済み: 5000リクエスト/時 +- `X-RateLimit-Remaining` ヘッダーで残り確認可能 diff --git a/.claude/skills/github-api/scripts/filter-commits-by-path.js b/.claude/skills/github-api/scripts/filter-commits-by-path.js new file mode 100755 index 0000000..d361273 --- /dev/null +++ b/.claude/skills/github-api/scripts/filter-commits-by-path.js @@ -0,0 +1,263 @@ +#!/usr/bin/env node +/** + * コミットをパスでフィルタリングするスクリプト + * + * get-all-branch-commits.js の出力を受け取り、指定パスに関連するコミットのみを抽出。 + * 各コミットの変更ファイルを GitHub API で取得し、パスマッチングを行う。 + * + * 使い方: + * # パイプライン + * node get-all-branch-commits.js owner repo | node filter-commits-by-path.js --owner owner --repo repo --paths "app/,web/" + * + * # ファイル入力 + * node filter-commits-by-path.js --input /tmp/commits.json --owner owner --repo repo --paths "app/,web/" + * + * 引数: + * --input 入力ファイル(省略時stdin) + * --owner リポジトリオーナー(必須) + * --repo リポジトリ名(必須) + * --paths カンマ区切りパス(必須)例: "app/,web/" + * --concurrency 並列数(デフォルト: 5) + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); + +/** + * コマンドライン引数を解析する + * @returns {{ input: string|null, owner: string, repo: string, paths: string[], concurrency: number }} + */ +function parseArgs() { + const args = process.argv.slice(2); + const parsed = { input: null, owner: null, repo: null, paths: null, concurrency: 5 }; + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--input': + parsed.input = args[++i]; + break; + case '--owner': + parsed.owner = args[++i]; + break; + case '--repo': + parsed.repo = args[++i]; + break; + case '--paths': + parsed.paths = args[++i]; + break; + case '--concurrency': + parsed.concurrency = parseInt(args[++i], 10); + break; + } + } + + if (!parsed.owner || !parsed.repo || !parsed.paths) { + console.error('Usage: node filter-commits-by-path.js --owner --repo --paths '); + console.error(''); + console.error('Required:'); + console.error(' --owner Repository owner'); + console.error(' --repo Repository name'); + console.error(' --paths Comma-separated paths (e.g. "app/,web/,supabase/")'); + console.error(''); + console.error('Optional:'); + console.error(' --input Input file (default: stdin)'); + console.error(' --concurrency Parallel requests (default: 5)'); + process.exit(1); + } + + return { + input: parsed.input, + owner: parsed.owner, + repo: parsed.repo, + paths: parsed.paths.split(',').map(p => p.trim()).filter(Boolean), + concurrency: parsed.concurrency + }; +} + +/** + * stdinまたはファイルからJSONを読み込む + * @param {string|null} inputFile + * @returns {object} + */ +function readInput(inputFile) { + let raw; + if (inputFile) { + raw = fs.readFileSync(inputFile, 'utf-8'); + } else { + raw = fs.readFileSync(0, 'utf-8'); // stdin + } + + try { + return JSON.parse(raw); + } catch (e) { + console.error('JSON解析エラー:', e.message); + process.exit(1); + } +} + +/** + * コミットの変更ファイル一覧を GitHub API で取得する + * @param {string} owner + * @param {string} repo + * @param {string} sha + * @returns {{ filename: string, status: string, additions: number, deletions: number }[] | null} + */ +function getCommitFiles(owner, repo, sha) { + const cmd = `gh api "repos/${owner}/${repo}/commits/${sha}" --jq '[.files[] | {filename, status, additions, deletions}]'`; + try { + const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 }); + return JSON.parse(result); + } catch (error) { + console.error(`[${sha.substring(0, 7)}] ファイル取得エラー: ${error.message.split('\n')[0]}`); + return null; + } +} + +/** + * セマフォパターンで並列数を制限しながら処理を実行する + * @param {Array} items + * @param {function} processor + * @param {number} concurrency + * @returns {Promise} + */ +async function processWithConcurrency(items, processor, concurrency) { + const results = new Array(items.length); + let index = 0; + let completed = 0; + + async function worker() { + while (index < items.length) { + const i = index++; + results[i] = await processor(items[i], i); + completed++; + if (completed % 10 === 0) { + console.error(` 進捗: ${completed}/${items.length}`); + } + } + } + + const workers = []; + for (let i = 0; i < Math.min(concurrency, items.length); i++) { + workers.push(worker()); + } + await Promise.all(workers); + + return results; +} + +/** + * メイン処理 + */ +async function main() { + const { input, owner, repo, paths, concurrency } = parseArgs(); + + console.error('=== パスフィルタリング ==='); + console.error(`リポジトリ: ${owner}/${repo}`); + console.error(`対象パス: ${paths.join(', ')}`); + console.error(`並列数: ${concurrency}`); + console.error('========================='); + + // 入力読み込み + const data = readInput(input); + + // 全ブランチから全コミットを収集(重複排除) + /** @type {{ sha: string, message: string, date: string, author: object, html_url: string, branch: string }[]} */ + const allCommits = []; + const seenShas = new Set(); + + for (const [branchName, branchData] of Object.entries(data.branches)) { + for (const commit of branchData.commits) { + if (!seenShas.has(commit.sha)) { + seenShas.add(commit.sha); + allCommits.push({ ...commit, branch: branchName }); + } + } + } + + const originalCount = allCommits.length; + console.error(`全コミット数: ${originalCount}(重複排除済み)`); + + if (originalCount === 0) { + // コミットがない場合はそのまま出力 + const result = { + metadata: { + ...data.metadata, + filter_paths: paths, + original_commits: 0, + filtered_commits: 0 + }, + branches: {} + }; + console.log(JSON.stringify(result, null, 2)); + return; + } + + // 並列でファイル取得 & パスマッチング + console.error(`ファイル情報を取得中(並列数: ${concurrency})...`); + + const matchResults = await processWithConcurrency( + allCommits, + (commit) => { + const files = getCommitFiles(owner, repo, commit.sha); + if (!files) return { commit, matchedFiles: [] }; + + const matchedFiles = files.filter(f => + paths.some(p => f.filename.startsWith(p)) + ); + return { commit, matchedFiles }; + }, + concurrency + ); + + // マッチしたコミットのみでブランチ構造を再構築 + const filteredBranches = {}; + let filteredCount = 0; + + for (const { commit, matchedFiles } of matchResults) { + if (matchedFiles.length === 0) continue; + + filteredCount++; + const branchName = commit.branch; + + if (!filteredBranches[branchName]) { + const originalBranch = data.branches[branchName]; + filteredBranches[branchName] = { + commits: [], + is_default: originalBranch.is_default + }; + } + + // ブランチ情報はコミットから除外(出力形式を元の構造と合わせる) + const { branch, ...commitData } = commit; + filteredBranches[branchName].commits.push({ + ...commitData, + matched_files: matchedFiles + }); + } + + // 結果出力 + const result = { + metadata: { + ...data.metadata, + filter_paths: paths, + original_commits: originalCount, + filtered_commits: filteredCount + }, + branches: filteredBranches + }; + + // 更新されたメタデータ + result.metadata.active_branches = Object.keys(filteredBranches).length; + result.metadata.total_commits = filteredCount; + + console.error(''); + console.error(`=== フィルタ結果 ===`); + console.error(`元のコミット: ${originalCount}`); + console.error(`フィルタ後: ${filteredCount}`); + console.error(`アクティブブランチ: ${Object.keys(filteredBranches).length}`); + console.error('===================='); + + console.log(JSON.stringify(result, null, 2)); +} + +main(); diff --git a/.claude/skills/github-api/scripts/get-all-branch-commits.js b/.claude/skills/github-api/scripts/get-all-branch-commits.js new file mode 100644 index 0000000..bd372a3 --- /dev/null +++ b/.claude/skills/github-api/scripts/get-all-branch-commits.js @@ -0,0 +1,315 @@ +#!/usr/bin/env node +/** + * 全ブランチの前日(JST)コミットを一括取得するスクリプト + * + * 最適化: GraphQLで最近アクティブなブランチのみを抽出してからコミット取得 + * + * 使い方: + * node get-all-branch-commits.js [--output ] + * + * 環境変数: + * GH_TOKEN: GitHub API認証トークン + * TARGET_DATE: 対象日(省略時は前日JST)形式: YYYY-MM-DD + * ACTIVE_DAYS: アクティブとみなす日数(省略時は7日) + * + * 出力: + * 全ブランチのコミットをJSON形式で出力 + * --output 指定時はファイルに書き込み(シェルリダイレクトより確実) + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const { normalizeCommits } = require('./lib/normalize'); +const { getYesterdayJST, getTargetDateJST } = require('./lib/date-utils'); + +// 除外するブランチのプレフィックス +const EXCLUDED_PREFIXES = ['dependabot/', 'renovate/']; + +// GraphQLでブランチ一覧と最終コミット日を取得(ページネーション対応) +function getAllBranchesWithDates(owner, repo) { + const allBranches = []; + let hasNextPage = true; + let cursor = null; + + while (hasNextPage) { + const afterClause = cursor ? `, after: "${cursor}"` : ''; + const query = ` + { + repository(owner: "${owner}", name: "${repo}") { + defaultBranchRef { + name + } + refs(refPrefix: "refs/heads/", first: 100${afterClause}) { + nodes { + name + target { + ... on Commit { + committedDate + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + }`; + + try { + const result = execSync( + `gh api graphql -f query='${query.replace(/'/g, "'\\''")}'`, + { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 } + ); + const data = JSON.parse(result); + const refs = data.data.repository.refs; + const defaultBranch = data.data.repository.defaultBranchRef?.name || 'main'; + + for (const node of refs.nodes) { + if (!EXCLUDED_PREFIXES.some(prefix => node.name.startsWith(prefix))) { + allBranches.push({ + name: node.name, + lastCommitDate: node.target?.committedDate || null, + isDefault: node.name === defaultBranch + }); + } + } + + hasNextPage = refs.pageInfo.hasNextPage; + cursor = refs.pageInfo.endCursor; + } catch (error) { + console.error('GraphQLエラー:', error.message); + process.exit(1); + } + } + + return allBranches; +} + +// 特定ブランチのコミットを取得(リトライ機能付き) +function getBranchCommits(owner, repo, branch, startUTC, endUTC, maxRetries = 3) { + const cmd = `gh api "repos/${owner}/${repo}/commits?sha=${encodeURIComponent(branch)}&since=${startUTC}&until=${endUTC}" --paginate`; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const rawResult = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 }); + const rawCommits = JSON.parse(rawResult); + return { success: true, commits: normalizeCommits(rawCommits) }; + } catch (error) { + const errorMsg = error.message.split('\n')[0]; + if (attempt < maxRetries) { + console.error(`[${branch}] リトライ ${attempt}/${maxRetries}: ${errorMsg}`); + const waitMs = Math.pow(2, attempt - 1) * 1000; + execSync(`sleep ${waitMs / 1000}`); + } else { + console.error(`[${branch}] ⚠️ 取得失敗(${maxRetries}回リトライ後): ${errorMsg}`); + return { success: false, commits: [], error: errorMsg }; + } + } + } + return { success: false, commits: [], error: 'Unknown error' }; +} + +// アクティブブランチをフィルタリング +function filterActiveBranches(allBranches, activeDays, targetDateStr) { + const targetDate = new Date(targetDateStr); + const cutoffDate = new Date(targetDate); + cutoffDate.setDate(cutoffDate.getDate() - activeDays); + const cutoffStr = cutoffDate.toISOString(); + + return allBranches.filter(branch => { + // デフォルトブランチは常に含める + if (branch.isDefault) return true; + // 最終コミット日がない場合はスキップ + if (!branch.lastCommitDate) return false; + // 最終コミット日がカットオフ以降なら含める + return branch.lastCommitDate >= cutoffStr; + }); +} + +// 引数パース +function parseArgs() { + const args = process.argv.slice(2); + let owner = null; + let repo = null; + let outputFile = null; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--output' || args[i] === '-o') { + outputFile = args[++i]; + } else if (!owner) { + owner = args[i]; + } else if (!repo) { + repo = args[i]; + } + } + + return { owner, repo, outputFile }; +} + +// メイン処理 +function main() { + const { owner, repo, outputFile } = parseArgs(); + + if (!owner || !repo) { + console.error('Usage: node get-all-branch-commits.js [--output ]'); + console.error(''); + console.error('Options:'); + console.error(' --output, -o Write JSON to file instead of stdout (recommended)'); + console.error(''); + console.error('Environment variables:'); + console.error(' GH_TOKEN: GitHub API token (required)'); + console.error(' TARGET_DATE: Target date in YYYY-MM-DD format (optional, defaults to yesterday JST)'); + console.error(' ACTIVE_DAYS: Days to consider a branch active (optional, defaults to 7)'); + process.exit(1); + } + + // 日付計算 + const targetDate = process.env.TARGET_DATE; + const activeDays = parseInt(process.env.ACTIVE_DAYS || '7', 10); + const { date, startUTC, endUTC } = targetDate + ? getTargetDateJST(targetDate) + : getYesterdayJST(); + + console.error('=== 全ブランチコミット取得(最適化版) ==='); + console.error(`対象日(JST): ${date}`); + console.error(`開始(UTC): ${startUTC}`); + console.error(`終了(UTC): ${endUTC}`); + console.error(`アクティブ判定: 過去${activeDays}日以内`); + console.error('=========================================='); + + // ステップ1: GraphQLで全ブランチと最終コミット日を取得 + console.error(''); + console.error('[ステップ1] GraphQLでブランチ一覧を取得中...'); + const allBranches = getAllBranchesWithDates(owner, repo); + const defaultBranch = allBranches.find(b => b.isDefault)?.name || 'main'; + console.error(`総ブランチ数: ${allBranches.length}`); + console.error(`デフォルトブランチ: ${defaultBranch}`); + + // ステップ2: アクティブブランチをフィルタリング + console.error(''); + console.error('[ステップ2] アクティブブランチをフィルタリング...'); + const activeBranches = filterActiveBranches(allBranches, activeDays, date); + console.error(`アクティブブランチ: ${activeBranches.length}件(${allBranches.length}件中)`); + activeBranches.forEach(b => { + const marker = b.isDefault ? '(default)' : ''; + console.error(` - ${b.name} ${marker}`); + }); + + // ステップ3: アクティブブランチのコミットを取得 + console.error(''); + console.error('[ステップ3] コミットを取得中...'); + const branches = {}; + let activeBranchCount = 0; + let totalCommits = 0; + let failedBranches = []; + + for (let i = 0; i < activeBranches.length; i++) { + const branch = activeBranches[i]; + console.error(`[${i + 1}/${activeBranches.length}] [${branch.name}] コミット取得中...`); + const result = getBranchCommits(owner, repo, branch.name, startUTC, endUTC); + + if (!result.success) { + failedBranches.push({ name: branch.name, error: result.error }); + } + + if (result.commits.length > 0) { + branches[branch.name] = { + commits: result.commits, + is_default: branch.isDefault + }; + activeBranchCount++; + totalCommits += result.commits.length; + console.error(`[${branch.name}] ${result.commits.length}件のコミットを検出`); + } + } + + // 失敗したブランチがあれば、10秒待ってから再試行 + if (failedBranches.length > 0) { + console.error(''); + console.error(`=== 失敗ブランチの再取得 (${failedBranches.length}件) ===`); + console.error('10秒待機してから再試行...'); + execSync('sleep 10'); + + const stillFailed = []; + for (let i = 0; i < failedBranches.length; i++) { + const branchName = failedBranches[i].name; + const branchInfo = activeBranches.find(b => b.name === branchName); + console.error(`[再試行 ${i + 1}/${failedBranches.length}] [${branchName}] コミット取得中...`); + const result = getBranchCommits(owner, repo, branchName, startUTC, endUTC); + + if (!result.success) { + stillFailed.push({ name: branchName, error: result.error }); + } else if (result.commits.length > 0) { + branches[branchName] = { + commits: result.commits, + is_default: branchInfo?.isDefault || false + }; + activeBranchCount++; + totalCommits += result.commits.length; + console.error(`[${branchName}] ✅ 再取得成功: ${result.commits.length}件のコミット`); + } else { + console.error(`[${branchName}] ✅ 再取得成功: コミットなし`); + } + } + + failedBranches = stillFailed; + console.error(`再取得完了: 残り失敗 ${failedBranches.length}件`); + } + + // 結果を出力 + const result = { + metadata: { + target_date: date, + start_utc: startUTC, + end_utc: endUTC, + total_branches: allBranches.length, + checked_branches: activeBranches.length, + active_branches: activeBranchCount, + total_commits: totalCommits, + default_branch: defaultBranch, + active_days_filter: activeDays, + failed_branches: failedBranches.length, + has_errors: failedBranches.length > 0 + }, + branches: branches + }; + + if (failedBranches.length > 0) { + result.errors = failedBranches; + } + + console.error(''); + console.error(`=== 結果 ===`); + console.error(`総ブランチ: ${allBranches.length}`); + console.error(`チェック対象: ${activeBranches.length}(最適化で${Math.round((1 - activeBranches.length / allBranches.length) * 100)}%削減)`); + console.error(`コミットあり: ${activeBranchCount}`); + console.error(`総コミット数: ${totalCommits}`); + if (failedBranches.length > 0) { + console.error(`❌ 取得失敗ブランチ: ${failedBranches.length}件`); + failedBranches.forEach(fb => { + console.error(` - ${fb.name}: ${fb.error}`); + }); + } + console.error('============'); + + // JSON出力 + const jsonOutput = JSON.stringify(result, null, 2); + if (outputFile) { + // ファイルに書き込み(stdout/stderrの混在を防ぐ) + fs.writeFileSync(outputFile, jsonOutput, 'utf-8'); + console.error(`✅ 出力ファイル: ${outputFile}`); + } else { + // 標準出力(後方互換性) + console.log(jsonOutput); + } + + // 失敗があれば exit 1 で終了 + if (failedBranches.length > 0) { + console.error(''); + console.error('❌ 一部ブランチの取得に失敗しました。処理を中断します。'); + process.exit(1); + } +} + +main(); diff --git a/.claude/skills/github-api/scripts/get-commits.js b/.claude/skills/github-api/scripts/get-commits.js new file mode 100644 index 0000000..c8e515c --- /dev/null +++ b/.claude/skills/github-api/scripts/get-commits.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * 前日(JST)のコミットを確実に取得するスクリプト + * + * 使い方: + * node get-commits.js [branch] + * + * 環境変数: + * GH_TOKEN: GitHub API認証トークン + * TARGET_DATE: 対象日(省略時は前日JST)形式: YYYY-MM-DD + * + * 出力: + * 正規化されたJSONを標準出力に出力(jq処理不要) + * - sha, message, date, author.login, author.avatar_url, html_url を保証 + */ + +const { execSync } = require('child_process'); +const { normalizeCommits } = require('./lib/normalize'); +const { getYesterdayJST, getTargetDateJST } = require('./lib/date-utils'); + +// メイン処理 +function main() { + const [owner, repo, branch = 'main'] = process.argv.slice(2); + + if (!owner || !repo) { + console.error('Usage: node get-commits.js [branch]'); + console.error(''); + console.error('Environment variables:'); + console.error(' GH_TOKEN: GitHub API token (required)'); + console.error(' TARGET_DATE: Target date in YYYY-MM-DD format (optional, defaults to yesterday JST)'); + process.exit(1); + } + + // 日付計算 + const targetDate = process.env.TARGET_DATE; + const { date, startUTC, endUTC } = targetDate + ? getTargetDateJST(targetDate) + : getYesterdayJST(); + + // ログ出力(デバッグ用、標準エラーへ) + console.error('=== 検索期間 ==='); + console.error(`対象日(JST): ${date}`); + console.error(`開始(UTC): ${startUTC}`); + console.error(`終了(UTC): ${endUTC}`); + console.error(`ブランチ: ${branch}`); + console.error('================'); + + // gh API呼び出し + const cmd = `gh api "repos/${owner}/${repo}/commits?sha=${branch}&since=${startUTC}&until=${endUTC}" --paginate`; + + try { + const rawResult = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 }); + const rawCommits = JSON.parse(rawResult); + const normalized = normalizeCommits(rawCommits); + // 正規化されたJSON出力(標準出力) + console.log(JSON.stringify(normalized, null, 2)); + } catch (error) { + console.error('API呼び出しエラー:', error.message); + process.exit(1); + } +} + +main(); diff --git a/.claude/skills/github-api/scripts/lib/date-utils.js b/.claude/skills/github-api/scripts/lib/date-utils.js new file mode 100644 index 0000000..e74e0a7 --- /dev/null +++ b/.claude/skills/github-api/scripts/lib/date-utils.js @@ -0,0 +1,45 @@ +function getYesterdayJST() { + const now = new Date(); + const jstOffset = 9 * 60 * 60 * 1000; + const jstNow = new Date(now.getTime() + jstOffset); + + const yesterday = new Date(jstNow); + yesterday.setUTCDate(yesterday.getUTCDate() - 1); + + const year = yesterday.getUTCFullYear(); + const month = yesterday.getUTCMonth(); + const day = yesterday.getUTCDate(); + + const start = new Date(Date.UTC(year, month, day, 0, 0, 0)); + start.setTime(start.getTime() - jstOffset); + + const end = new Date(Date.UTC(year, month, day, 23, 59, 59)); + end.setTime(end.getTime() - jstOffset); + + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + + return { + date: dateStr, + startUTC: start.toISOString().replace('.000Z', 'Z'), + endUTC: end.toISOString().replace('.000Z', 'Z') + }; +} + +function getTargetDateJST(dateStr) { + const [year, month, day] = dateStr.split('-').map(Number); + const jstOffset = 9 * 60 * 60 * 1000; + + const start = new Date(Date.UTC(year, month - 1, day, 0, 0, 0)); + start.setTime(start.getTime() - jstOffset); + + const end = new Date(Date.UTC(year, month - 1, day, 23, 59, 59)); + end.setTime(end.getTime() - jstOffset); + + return { + date: dateStr, + startUTC: start.toISOString().replace('.000Z', 'Z'), + endUTC: end.toISOString().replace('.000Z', 'Z') + }; +} + +module.exports = { getYesterdayJST, getTargetDateJST }; diff --git a/.claude/skills/github-api/scripts/lib/normalize.js b/.claude/skills/github-api/scripts/lib/normalize.js new file mode 100644 index 0000000..b573b5f --- /dev/null +++ b/.claude/skills/github-api/scripts/lib/normalize.js @@ -0,0 +1,30 @@ +const crypto = require('crypto'); + +function normalizeCommits(rawCommits) { + return rawCommits.map(commit => { + let author; + if (commit.author?.login && commit.author?.avatar_url) { + author = { login: commit.author.login, avatar_url: commit.author.avatar_url }; + } else { + const email = commit.commit?.author?.email || ''; + const name = commit.commit?.author?.name || 'Unknown'; + const hash = email + ? crypto.createHash('md5').update(email.toLowerCase().trim()).digest('hex') + : '00000000000000000000000000000000'; + author = { + login: name, + avatar_url: `https://www.gravatar.com/avatar/${hash}?d=identicon&s=80` + }; + console.error(`[Avatar] フォールバック: ${name} (${commit.sha.substring(0, 7)})`); + } + return { + sha: commit.sha, + message: commit.commit.message, + date: commit.commit.author.date, + author, + html_url: commit.html_url + }; + }); +} + +module.exports = { normalizeCommits }; diff --git a/.claude/skills/screenshot-capture/SKILL.md b/.claude/skills/screenshot-capture/SKILL.md new file mode 100644 index 0000000..bc4f260 --- /dev/null +++ b/.claude/skills/screenshot-capture/SKILL.md @@ -0,0 +1,75 @@ +--- +name: screenshot-capture +description: HTMLファイルのスクリーンショット撮影。図解をPNG化するときに使用。 +--- + +# Screenshot Capture + +HTMLファイルをPNG画像に変換するスキルです。 + +## 使い方 + +```bash +node .claude/skills/screenshot-capture/scripts/capture.js +``` + +### 例 + +```bash +# 単一ファイル +node .claude/skills/screenshot-capture/scripts/capture.js /tmp/diagram.html /tmp/diagram.png + +# 複数ファイル +node .claude/skills/screenshot-capture/scripts/capture.js /tmp/summary.html /tmp/summary.png +node .claude/skills/screenshot-capture/scripts/capture.js /tmp/timeline.html /tmp/timeline.png +``` + +## 特徴 + +- **Retina対応**: deviceScaleFactor: 2 で高解像度 +- **余白なし**: コンテンツサイズに自動フィット +- **Headless対応**: GitHub Actions でも動作 + +## 依存関係 + +初回実行時に自動インストールされます: + +```bash +npm install playwright +npx playwright install chromium +``` + +## オプション + +環境変数で動作を制御できます: + +| 環境変数 | デフォルト | 説明 | +|----------|------------|------| +| `SCREENSHOT_SCALE` | `2` | デバイスピクセル比 | +| `SCREENSHOT_WIDTH` | `450` | ビューポート幅 | +| `SCREENSHOT_WAIT` | `500` | 読み込み待機時間(ms) | + +## GitHub Actions での使用 + +```yaml +- name: Take screenshots + run: | + npm install playwright + npx playwright install chromium + node .claude/skills/screenshot-capture/scripts/capture.js /tmp/report.html /tmp/report.png +``` + +## トラブルシューティング + +### Chromiumがインストールされていない + +```bash +npx playwright install chromium +``` + +### フォントが表示されない (GitHub Actions) + +```yaml +- name: Install fonts + run: sudo apt-get install -y fonts-noto-cjk +``` diff --git a/.claude/skills/screenshot-capture/scripts/capture-batch.js b/.claude/skills/screenshot-capture/scripts/capture-batch.js new file mode 100644 index 0000000..5211fe1 --- /dev/null +++ b/.claude/skills/screenshot-capture/scripts/capture-batch.js @@ -0,0 +1,143 @@ +#!/usr/bin/env node +/** + * Batch Screenshot Capture Script + * + * 複数のHTMLファイルを一括でPNG化します。 + * + * Usage: node capture-batch.js ... + * + * Example: + * node capture-batch.js \ + * /tmp/summary.html:/tmp/summary.png \ + * /tmp/timeline.html:/tmp/timeline.png \ + * /tmp/commits.html:/tmp/commits.png + */ + +const { chromium } = require('playwright'); +const path = require('path'); + +// 設定 +const CONFIG = { + scale: parseInt(process.env.SCREENSHOT_SCALE || '2', 10), + width: parseInt(process.env.SCREENSHOT_WIDTH || '450', 10), + wait: parseInt(process.env.SCREENSHOT_WAIT || '500', 10), +}; + +async function captureAll(files) { + console.log(`Capturing ${files.length} files...`); + console.log(`Scale: ${CONFIG.scale}x, Width: ${CONFIG.width}px`); + console.log(''); + + const browser = await chromium.launch({ headless: true }); + + try { + // 高解像度コンテキストを作成(ブラウザは1つを再利用) + const context = await browser.newContext({ + deviceScaleFactor: CONFIG.scale, + viewport: { width: CONFIG.width, height: 800 }, + }); + + const results = []; + + for (const { input, output } of files) { + const page = await context.newPage(); + + try { + const fileUrl = input.startsWith('file://') + ? input + : `file://${path.resolve(input)}`; + + console.log(`[${results.length + 1}/${files.length}] ${path.basename(input)}`); + + await page.goto(fileUrl, { waitUntil: 'networkidle' }); + await page.waitForTimeout(CONFIG.wait); + + // コンテンツの高さを取得 + const contentHeight = await page.evaluate(() => { + const container = document.body.firstElementChild; + if (container) { + const rect = container.getBoundingClientRect(); + const style = window.getComputedStyle(document.body); + const paddingTop = parseFloat(style.paddingTop) || 0; + const paddingBottom = parseFloat(style.paddingBottom) || 0; + return Math.ceil(rect.height + paddingTop + paddingBottom); + } + return document.body.scrollHeight; + }); + + // ビューポートをコンテンツサイズに合わせる + await page.setViewportSize({ + width: CONFIG.width, + height: contentHeight + }); + await page.waitForTimeout(100); + + // スクリーンショット撮影 + await page.screenshot({ path: output, type: 'png' }); + + results.push({ input, output, success: true, height: contentHeight }); + console.log(` -> ${output} (${contentHeight}px)`); + + } catch (error) { + results.push({ input, output, success: false, error: error.message }); + console.error(` -> Error: ${error.message}`); + } finally { + await page.close(); + } + } + + await context.close(); + return results; + + } finally { + await browser.close(); + } +} + +async function main() { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.error('Usage: node capture-batch.js ...'); + console.error(''); + console.error('Example:'); + console.error(' node capture-batch.js \\'); + console.error(' /tmp/summary.html:/tmp/summary.png \\'); + console.error(' /tmp/timeline.html:/tmp/timeline.png'); + process.exit(1); + } + + // 引数をパース + const files = args.map(arg => { + const [input, output] = arg.split(':'); + if (!input || !output) { + console.error(`Invalid argument: ${arg}`); + console.error('Format should be: input.html:output.png'); + process.exit(1); + } + return { input, output }; + }); + + try { + const results = await captureAll(files); + + console.log(''); + console.log('Summary:'); + const success = results.filter(r => r.success).length; + const failed = results.filter(r => !r.success).length; + console.log(` Success: ${success}, Failed: ${failed}`); + + // 結果をJSON出力(パイプ処理用) + if (process.env.OUTPUT_JSON === '1') { + console.log(JSON.stringify(results)); + } + + process.exit(failed > 0 ? 1 : 0); + + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +} + +main(); diff --git a/.claude/skills/screenshot-capture/scripts/capture.js b/.claude/skills/screenshot-capture/scripts/capture.js new file mode 100644 index 0000000..af3786e --- /dev/null +++ b/.claude/skills/screenshot-capture/scripts/capture.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node +/** + * Screenshot Capture Script + * + * HTMLファイルをPNG画像に変換します。 + * - Retina対応 (2x) + * - コンテンツサイズに自動フィット(余白なし) + * - GitHub Actions対応(Headless) + * + * Usage: node capture.js + */ + +const { chromium } = require('playwright'); +const path = require('path'); + +// 設定 +const CONFIG = { + scale: parseInt(process.env.SCREENSHOT_SCALE || '2', 10), + width: parseInt(process.env.SCREENSHOT_WIDTH || '450', 10), + wait: parseInt(process.env.SCREENSHOT_WAIT || '500', 10), +}; + +async function capture(inputPath, outputPath) { + // 入力パスをfile:// URLに変換 + const fileUrl = inputPath.startsWith('file://') + ? inputPath + : `file://${path.resolve(inputPath)}`; + + console.log(`Capturing: ${inputPath}`); + console.log(`Output: ${outputPath}`); + console.log(`Scale: ${CONFIG.scale}x, Width: ${CONFIG.width}px`); + + const browser = await chromium.launch({ + headless: true, + }); + + try { + // 高解像度コンテキストを作成 + const context = await browser.newContext({ + deviceScaleFactor: CONFIG.scale, + viewport: { width: CONFIG.width, height: 800 }, // 初期高さ(後で調整) + }); + + const page = await context.newPage(); + await page.goto(fileUrl, { waitUntil: 'networkidle' }); + await page.waitForTimeout(CONFIG.wait); + + // コンテンツの実際の高さを取得 + const contentHeight = await page.evaluate(() => { + // body直下の最初のdivを探す(通常はメインコンテナ) + const container = document.body.firstElementChild; + if (container) { + const rect = container.getBoundingClientRect(); + // パディングを考慮 + const style = window.getComputedStyle(document.body); + const paddingTop = parseFloat(style.paddingTop) || 0; + const paddingBottom = parseFloat(style.paddingBottom) || 0; + return Math.ceil(rect.height + paddingTop + paddingBottom); + } + // フォールバック: body全体の高さ + return document.body.scrollHeight; + }); + + console.log(`Content height: ${contentHeight}px`); + + // ビューポートをコンテンツサイズに合わせる + await page.setViewportSize({ + width: CONFIG.width, + height: contentHeight + }); + await page.waitForTimeout(100); // レイアウト安定化 + + // スクリーンショット撮影(fullPageではなくビューポートのみ) + await page.screenshot({ + path: outputPath, + type: 'png', + // fullPage: false がデフォルト + }); + + console.log(`Done! Saved to ${outputPath}`); + + await context.close(); + } finally { + await browser.close(); + } +} + +// メイン処理 +async function main() { + const args = process.argv.slice(2); + + if (args.length < 2) { + console.error('Usage: node capture.js '); + console.error(''); + console.error('Environment variables:'); + console.error(' SCREENSHOT_SCALE - Device pixel ratio (default: 2)'); + console.error(' SCREENSHOT_WIDTH - Viewport width (default: 450)'); + console.error(' SCREENSHOT_WAIT - Wait time in ms (default: 500)'); + process.exit(1); + } + + const [inputPath, outputPath] = args; + + try { + await capture(inputPath, outputPath); + } catch (error) { + console.error('Error:', error.message); + + if (error.message.includes('Executable doesn\'t exist')) { + console.error(''); + console.error('Chromium is not installed. Run:'); + console.error(' npx playwright install chromium'); + } + + process.exit(1); + } +} + +main(); diff --git a/.claude/skills/slack-formatting/SKILL.md b/.claude/skills/slack-formatting/SKILL.md new file mode 100644 index 0000000..d1b8450 --- /dev/null +++ b/.claude/skills/slack-formatting/SKILL.md @@ -0,0 +1,123 @@ +--- +name: slack-formatting +description: Slackチャンネルに複数画像をまとめて投稿する。Slack投稿、レポート投稿、画像アップロード時に使用。curlは使わずpost-report.jsを実行すること。 +allowed-tools: Bash(node:*), Read +--- + +# Slack Formatting ガイド + +Slack への投稿方法を説明します。 + +## ⚠️ 重要: 必ずスクリプトを使用すること + +**Slack投稿は必ず以下のスクリプトを使用してください。** +curlで直接APIを呼び出したり、独自の投稿ロジックを書いたりしないでください。 + +```bash +# 必ずこのスクリプトを使用 +node .claude/skills/slack-formatting/scripts/post-report.js \ + --message "メッセージテキスト" \ + /tmp/image1.png /tmp/image2.png ... +``` + +## スクリプト使用方法 + +### 基本的な使い方 + +```bash +node .claude/skills/slack-formatting/scripts/post-report.js \ + --message "📊 今日のコミットレポート + +期間: 2026-01-06 00:00 〜 23:59 (JST) +対象: 1ブランチ / 18コミット / 5名 + +🐱 コミネコ で自動生成" \ + /tmp/daily-summary.png \ + /tmp/branch-summary-main.png \ + /tmp/by-app.png \ + /tmp/timeline.png \ + /tmp/tips.png +``` + +### 環境変数 + +| 環境変数 | 用途 | 必須 | +|----------|------|------| +| `SLACK_BOT_TOKEN` | Bot User OAuth Token | ✅ | +| `SLACK_CHANNEL` | 投稿先チャンネルID | ※未設定時はデバッグチャンネル | + +**デバッグ用チャンネル**: `YOUR_DEBUG_CHANNEL_ID` + +### スクリプトが行うこと + +- ✅ 複数画像を**1メッセージにまとめて**投稿 +- ✅ **チャンネルに直接投稿**(スレッドではない) +- ✅ 詳細なログ出力(原因特定用) +- ✅ エラーハンドリング + +### スクリプトの出力例 + +``` +======================================== +=== SLACK投稿開始 === +======================================== +[2026-01-07T07:15:00.000Z] 投稿先チャンネル: C0XXXXXXXXX +[2026-01-07T07:15:00.000Z] 画像数: 5 +[2026-01-07T07:15:00.000Z] メッセージ: 📊 今日のコミットレポート... +[2026-01-07T07:15:00.000Z] [1] daily-summary.png +[2026-01-07T07:15:00.000Z] [2] branch-summary-main.png +... +======================================== +=== SLACK投稿完了 === +======================================== +[2026-01-07T07:15:10.000Z] チャンネル: C0XXXXXXXXX +[2026-01-07T07:15:10.000Z] 画像数: 5 +[2026-01-07T07:15:10.000Z] ✅ 投稿成功! +``` + +--- + +## 禁止事項 + +以下の方法は**絶対に使用しないでください**: + +```bash +# ❌ 禁止: curlで直接APIを呼び出す +curl -X POST https://slack.com/api/chat.postMessage ... + +# ❌ 禁止: スレッドに投稿する +curl ... -d '{"thread_ts": "xxx"}' ... + +# ❌ 禁止: テキストと画像を別々に投稿 +curl ... chat.postMessage # テキスト +curl ... files.upload # 画像(スレッドに) +``` + +--- + +## Block Kit 構造(参考) + +テキストメッセージのフォーマットに使用できます。 + +### mrkdwn 記法 + +| 書式 | 記法 | 例 | +|------|------|-----| +| 太字 | `*text*` | *太字* | +| イタリック | `_text_` | _イタリック_ | +| 取消線 | `~text~` | ~取消線~ | +| コード | `` `code` `` | `code` | +| リンク | `` | | +| メンション | `<@USER_ID>` | <@U1234> | +| チャンネル | `<#CHANNEL_ID>` | <#C1234> | + +--- + +## トラブルシューティング + +| 症状 | 原因 | 対処 | +|------|------|------| +| 画像がスレッドに投稿された | スクリプトを使っていない | `post-report.js` を使用する | +| 画像が1枚しか投稿されない | スクリプトを使っていない | `post-report.js` を使用する | +| 3回投稿された | 投稿処理が複数回呼ばれた | ログで `SLACK投稿開始` の回数を確認 | +| `not_authed` エラー | 環境変数が渡されていない | `SLACK_BOT_TOKEN` を確認 | diff --git a/.claude/skills/slack-formatting/scripts/post-report.js b/.claude/skills/slack-formatting/scripts/post-report.js new file mode 100644 index 0000000..4070c88 --- /dev/null +++ b/.claude/skills/slack-formatting/scripts/post-report.js @@ -0,0 +1,260 @@ +#!/usr/bin/env node +/** + * Slack レポート投稿スクリプト + * + * 複数の画像を1メッセージにまとめてSlackチャンネルに投稿します。 + * ※スレッドではなく、チャンネルに直接投稿します + * + * Required Environment Variables: + * SLACK_BOT_TOKEN - Bot User OAuth Token (xoxb-...) + * SLACK_CHANNEL - Channel ID (C...) + * + * Usage: + * node post-report.js --message "テキスト" ... + * + * Example: + * node post-report.js --message "📊 今日のレポート" \ + * /tmp/daily-summary.png /tmp/by-app.png /tmp/timeline.png + */ + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +// 環境変数(SLACK_CHANNEL または SLACK_CHANNEL_ID をサポート) +const SLACK_TOKEN = process.env.SLACK_BOT_TOKEN; +const CHANNEL_ID = process.env.SLACK_CHANNEL || process.env.SLACK_CHANNEL_ID; +const DEBUG_CHANNEL = 'YOUR_DEBUG_CHANNEL_ID'; + +// ======================================== +// ログ出力(原因特定用) +// ======================================== +function log(message) { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] ${message}`); +} + +function logSection(title) { + console.log(''); + console.log('========================================'); + console.log(`=== ${title} ===`); + console.log('========================================'); +} + +// ======================================== +// HTTPS リクエスト +// ======================================== +function httpsRequest(options, postData) { + return new Promise((resolve, reject) => { + const req = https.request(options, (res) => { + res.setEncoding('utf8'); + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (e) { + resolve({ ok: false, error: 'parse_error', raw: data }); + } + }); + }); + req.on('error', reject); + if (postData) req.write(postData, 'utf8'); + req.end(); + }); +} + +// ======================================== +// Slack API 関数 +// ======================================== +async function getUploadUrl(filename, fileSize) { + const params = new URLSearchParams({ + filename, + length: fileSize.toString() + }); + + return await httpsRequest({ + hostname: 'slack.com', + path: '/api/files.getUploadURLExternal', + method: 'POST', + headers: { + 'Authorization': `Bearer ${SLACK_TOKEN}`, + 'Content-Type': 'application/x-www-form-urlencoded' + } + }, params.toString()); +} + +async function uploadFile(uploadUrl, filePath) { + const fileContent = fs.readFileSync(filePath); + const filename = path.basename(filePath); + + const boundary = '----FormBoundary' + Math.random().toString(36).substring(2); + const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: application/octet-stream\r\n\r\n`; + const footer = `\r\n--${boundary}--\r\n`; + + const body = Buffer.concat([ + Buffer.from(header, 'utf-8'), + fileContent, + Buffer.from(footer, 'utf-8') + ]); + + const url = new URL(uploadUrl); + + return new Promise((resolve, reject) => { + const req = https.request({ + hostname: url.hostname, + path: url.pathname + url.search, + method: 'POST', + headers: { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'Content-Length': body.length + } + }, (res) => { + res.setEncoding('utf8'); + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => resolve(data)); + }); + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +async function completeUpload(fileIds, message, channelId) { + const files = fileIds.map(id => ({ id })); + + const body = JSON.stringify({ + files, + channel_id: channelId, + initial_comment: message || '' + }); + + return await httpsRequest({ + hostname: 'slack.com', + path: '/api/files.completeUploadExternal', + method: 'POST', + headers: { + 'Authorization': `Bearer ${SLACK_TOKEN}`, + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': Buffer.byteLength(body) + } + }, body); +} + +// ======================================== +// メイン処理 +// ======================================== +async function postReport(imagePaths, message) { + const channelId = CHANNEL_ID || DEBUG_CHANNEL; + + logSection('SLACK投稿開始'); + log(`投稿先チャンネル: ${channelId}`); + log(`画像数: ${imagePaths.length}`); + log(`メッセージ: ${message.length > 50 ? message.substring(0, 50) + '...' : message}`); + + // 画像一覧を表示 + imagePaths.forEach((p, i) => { + log(` [${i + 1}] ${path.basename(p)}`); + }); + + const fileIds = []; + + // 各画像をアップロード + for (let i = 0; i < imagePaths.length; i++) { + const imagePath = imagePaths[i]; + const filename = path.basename(imagePath); + const fileSize = fs.statSync(imagePath).size; + + log(`アップロード中: ${filename} (${Math.round(fileSize / 1024)}KB)`); + + // Step 1: Get upload URL + const urlResponse = await getUploadUrl(filename, fileSize); + if (!urlResponse.ok) { + log(` エラー: ${urlResponse.error}`); + continue; + } + + // Step 2: Upload file + await uploadFile(urlResponse.upload_url, imagePath); + log(` 完了: ${urlResponse.file_id}`); + + fileIds.push(urlResponse.file_id); + } + + if (fileIds.length === 0) { + logSection('SLACK投稿エラー'); + log('アップロードされたファイルがありません'); + return { ok: false, error: 'no_files_uploaded' }; + } + + // Step 3: Complete upload (posts all files at once to channel, NOT thread) + log(''); + log('チャンネルに投稿中...'); + const result = await completeUpload(fileIds, message, channelId); + + if (result.ok) { + logSection('SLACK投稿完了'); + log(`チャンネル: ${channelId}`); + log(`画像数: ${fileIds.length}`); + log('✅ 投稿成功!'); + } else { + logSection('SLACK投稿エラー'); + log(`エラー: ${result.error}`); + if (result.raw) log(`詳細: ${result.raw}`); + } + + return result; +} + +async function main() { + // 環境変数チェック + if (!SLACK_TOKEN) { + console.error('エラー: SLACK_BOT_TOKEN 環境変数が必要です'); + process.exit(1); + } + + if (!CHANNEL_ID) { + log(`警告: SLACK_CHANNEL が未設定のため、デバッグチャンネル (${DEBUG_CHANNEL}) に投稿します`); + } + + // 引数パース + const args = process.argv.slice(2); + let message = ''; + const imagePaths = []; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--message' || args[i] === '-m') { + message = args[++i] || ''; + } else if (args[i].startsWith('-')) { + console.error(`不明なオプション: ${args[i]}`); + process.exit(1); + } else { + imagePaths.push(args[i]); + } + } + + // 使い方表示 + if (imagePaths.length === 0) { + console.error('使い方: node post-report.js --message "テキスト" ...'); + console.error(''); + console.error('必須環境変数:'); + console.error(' SLACK_BOT_TOKEN - Bot OAuth Token'); + console.error(' SLACK_CHANNEL - チャンネルID'); + process.exit(1); + } + + // ファイル存在確認 + const missingFiles = imagePaths.filter(p => !fs.existsSync(p)); + if (missingFiles.length > 0) { + console.error('ファイルが見つかりません:'); + missingFiles.forEach(f => console.error(` - ${f}`)); + process.exit(1); + } + + // 投稿実行 + const result = await postReport(imagePaths, message); + process.exit(result.ok ? 0 : 1); +} + +main(); diff --git a/.github/workflows/daily-report.yml b/.github/workflows/daily-report.yml new file mode 100644 index 0000000..a5d0b67 --- /dev/null +++ b/.github/workflows/daily-report.yml @@ -0,0 +1,31 @@ +name: Daily Commit Report + +on: + schedule: + # - cron: '0 22 * * *' # 毎日7:00 JST(有効にするにはコメントを外す) + workflow_dispatch: + inputs: + debug: + description: 'デバッグモード(テスト用チャンネルに投稿)' + required: false + default: false + type: boolean + target_date: + description: '対象日(YYYY-MM-DD、省略時は前日)' + required: false + default: '' + type: string + +jobs: + report: + uses: ./.github/workflows/report-job.yml + with: + project_name: your-project + config_file: configs/projects/your-project.yml + debug_channel: ${{ github.event.inputs.debug == 'true' && 'YOUR_DEBUG_CHANNEL_ID' || '' }} + target_date: ${{ github.event.inputs.target_date }} + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} diff --git a/.github/workflows/report-job.yml b/.github/workflows/report-job.yml new file mode 100644 index 0000000..ac3b5dd --- /dev/null +++ b/.github/workflows/report-job.yml @@ -0,0 +1,120 @@ +name: Report Job (Reusable) + +on: + workflow_call: + inputs: + project_name: + description: 'プロジェクト名(表示用)' + required: true + type: string + config_file: + description: '設定ファイルパス' + required: true + type: string + debug_channel: + description: 'デバッグ用チャンネルID(指定時はこちらに投稿)' + required: false + type: string + default: '' + target_date: + description: '対象日(YYYY-MM-DD形式、省略時は前日)' + required: false + type: string + default: '' + secrets: + ANTHROPIC_API_KEY: + required: true + GH_TOKEN: + required: true + SLACK_BOT_TOKEN: + required: true + SLACK_CHANNEL: + required: true + +jobs: + report: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + id: checkout + + - name: Install Japanese fonts + id: fonts + run: | + sudo apt-get update + sudo apt-get install -y fonts-noto-cjk + + - name: Setup Node.js + id: node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Playwright + id: playwright + run: | + npm ci + npx playwright install chromium + + - name: Read prompt file + id: read-prompt + run: | + { + echo 'content<> "$GITHUB_OUTPUT" + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GH_TOKEN }} + prompt: ${{ steps.read-prompt.outputs.content }} + claude_args: "--allowedTools 'Bash,Read,Write,Edit,Glob,Grep,Skill,TodoWrite'" + show_full_output: true # 常に詳細ログを出力(原因特定用) + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL: ${{ inputs.debug_channel != '' && inputs.debug_channel || secrets.SLACK_CHANNEL }} + TARGET_DATE: ${{ inputs.target_date }} + + - name: Notify failure to Slack + if: failure() + # このステップは GitHub Actions のシェルで実行されるため curl を使用。 + # CLAUDE.md の「curl 禁止」は Claude Code エージェント向けのルールです。 + run: | + # 失敗したステップを特定 + FAILED_STEP="" + if [[ "${{ steps.checkout.outcome }}" == "failure" ]]; then + FAILED_STEP="チェックアウト" + elif [[ "${{ steps.fonts.outcome }}" == "failure" ]]; then + FAILED_STEP="日本語フォントのインストール" + elif [[ "${{ steps.node.outcome }}" == "failure" ]]; then + FAILED_STEP="Node.jsのセットアップ" + elif [[ "${{ steps.playwright.outcome }}" == "failure" ]]; then + FAILED_STEP="Playwrightのインストール" + elif [[ "${{ steps.read-prompt.outcome }}" == "failure" ]]; then + FAILED_STEP="プロンプトファイルの読み込み" + elif [[ "${{ steps.claude.outcome }}" == "failure" ]]; then + FAILED_STEP="Claude Codeの実行" + else + FAILED_STEP="不明なステップ" + fi + + curl -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "channel": "'"${SLACK_CHANNEL:-YOUR_DEBUG_CHANNEL_ID}"'", + "text": ":x: *コミネコ エラー発生*\n\nプロジェクト: ${{ inputs.project_name }}\n失敗箇所: '"$FAILED_STEP"'\nワークフロー実行: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|詳細を確認>" + }' + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL: ${{ inputs.debug_channel != '' && inputs.debug_channel || secrets.SLACK_CHANNEL }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d48eb19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +.DS_Store +.playwright-mcp/ +/tmp/ +output/ +*.png +.env +.env.local diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ab6e9f4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# コミネコ + +GitHubコミットを図解してSlackに投稿するシステム。 + +## プロジェクト構造 + +``` +configs/ +├── repos/ # リポジトリ内のアプリ定義(共有) +└── projects/ # プロジェクト別設定(Slack先、対象アプリ) + +.claude/ +├── prompts/ +│ └── daily-report.md # メイン処理フロー +└── skills/ # 各処理の詳細知識 +``` + +## 実行方法 + +```bash +claude "/daily-report を configs/projects/your-project.yml で実行" +``` + +## スキル参照ガイド + +| タイミング | スキル | 内容 | +|-----------|--------|------| +| 設定読み込み時 | config-reader | 2層構造の読み方、アプリフィルタ | +| コミット取得時 | github-api | REST APIエンドポイント | +| 差分分析時 | code-analyzer | ビジネス視点への変換ルール | +| 図解生成前 | diagram-guidelines | デザイン基準、examples | +| スクショ時 | screenshot-capture | Playwrightスクリプト | +| Slack投稿時 | slack-formatting | 複数画像まとめ投稿 | + +## GitHub Secrets + +GitHub Actionsで使用するシークレット一覧: + +| シークレット名 | 用途 | +|---------------|------| +| `ANTHROPIC_API_KEY` | Claude API認証 | +| `GH_TOKEN` | GitHub API認証(コミット取得) | +| `SLACK_BOT_TOKEN` | Slack Bot認証 | +| `SLACK_CHANNEL` | 投稿先チャンネルID | + +## Slack投稿ルール + +Slackへの投稿は **必ず `slack-formatting` スキルを使用** してください。 + +```bash +node .claude/skills/slack-formatting/scripts/post-report.js \ + --message "メッセージ" \ + /tmp/image1.png /tmp/image2.png +``` + +**禁止事項:** +- curl で直接 Slack API を呼び出す +- 独自の投稿ロジックを書く diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd2bdbc --- /dev/null +++ b/README.md @@ -0,0 +1,296 @@ +# コミットレポートツール + +GitHubのコミットをAIで分析し、図解レポートをSlackに自動投稿するツールです。愛称は「コミネコ」。 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 📊 昨日の開発レポート 2026-01-15 │ +│ │ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ 5 commits │ │ 2 branches │ │ 3 members │ │ +│ └────────────┘ └────────────┘ └────────────┘ │ +│ │ +│ ⭐ ハイライト │ +│ • ログインページのデザインを刷新 │ +│ • 検索機能の応答速度が向上 │ +│ │ +│ 🐱 コミネコ で自動生成 │ +└──────────────────────────────────────────────────────────────┘ +``` + +Slackにはこのような図解レポートの画像が毎朝届きます。 + +--- + +## 料金について + +このツールで使う外部サービスの料金です。 + +| サービス | 用途 | 料金 | +|---|---|---| +| GitHub Actions | 定期実行(トリガー) | 毎月 2,000 分無料 | +| GitHub API | コミットデータ取得 | 無料(5,000 リクエスト/時) | +| Claude API | AIによる分析・図解生成 | 従量課金(1回あたり約 $0.05〜0.20) | +| Slack | レポート画像の通知先 | 無料ワークスペースで可 | + +--- + +## このツールの4パーツ + +第3回講義で学んだ「4パーツ」が、このツールではどのファイルに対応しているかを示します。 + +``` +┌─────────────┐ +│ トリガー │ .github/workflows/daily-report.yml +│ (いつ動く) │ → 毎朝7時 or 手動実行 +└──────┬──────┘ + ▼ +┌─────────────┐ +│ ソース元 │ .claude/skills/github-api/scripts/ +│ (データ) │ → GitHub API からコミットを取得 +└──────┬──────┘ + ▼ +┌─────────────┐ .claude/skills/code-analyzer/ ← ビジネス視点で分析 +│ 処理する場所 │ .claude/skills/diagram-guidelines/ ← HTML図解を生成 +│ (加工) │ .claude/skills/screenshot-capture/ ← 画像に変換 +└──────┬──────┘ + ▼ +┌─────────────┐ +│ 届ける先 │ .claude/skills/slack-formatting/scripts/ +│ (配信) │ → Slack に画像付きで投稿 +└─────────────┘ +``` + +--- + +## ファイル構成 + +``` +.claude/ +├── prompts/ +│ └── daily-report.md # メイン処理フロー +└── skills/ + ├── code-analyzer/ # ビジネス視点での分析ルール + ├── config-reader/ # 設定ファイルの読み方 + ├── diagram-guidelines/ # HTML図解のデザイン + │ └── examples/ # お手本HTML(5種類) + ├── github-api/ # GitHub API 操作 + │ └── scripts/ # コミット取得スクリプト + ├── screenshot-capture/ # スクリーンショット撮影 + │ └── scripts/ # Playwright スクリプト + └── slack-formatting/ # Slack投稿 + └── scripts/ # 画像まとめ投稿スクリプト + +configs/ +├── repos/your-repo.yml # リポジトリ構造定義 +└── projects/your-project.yml # プロジェクト設定 + +.github/workflows/ +├── daily-report.yml # GitHub Actions 定義 +└── report-job.yml # 再利用ワークフロー +``` + +--- + +## セットアップ + +### 前提 + +- **GitHub アカウント**と監視したいリポジトリが必要です +- **Slack ワークスペース**が必要です(無料プランで可) +- このツールは GitHub Actions が全自動で実行します。あなたのPCで `npm install` を実行する必要はありません + +### Part A: リポジトリを準備する + +1. このリポジトリを自分の GitHub アカウントにフォーク(またはコピー)する +2. **リポジトリは Private にしてください**(APIキーなどの設定を含むため) + +### Part B: リポジトリの情報を設定する + +**まず2行だけ書き換えてください。** 残りはそのままで動きます。 + +`configs/repos/your-repo.yml` を開いて: + +```yaml +repository: + owner: your-username # ← ここをあなたのGitHubユーザー名に + name: your-web-app # ← ここを監視したいリポジトリ名に +``` + +これだけでリポジトリ全体のコミットがレポート対象になります。 +アプリ別の分類が必要な場合は「カスタマイズ」セクションで設定します。 + +### Part C: Slack App を作成する + +1. https://api.slack.com/apps にアクセスし「Create New App」をクリック +2. 「From scratch」を選択し、アプリ名(例: コミネコ)とワークスペースを設定 +3. 左メニュー「OAuth & Permissions」→ Bot Token Scopes に以下を追加: + - `files:write` + - `chat:write` +4. 「Install to Workspace」→ 許可する +5. 表示される **Bot User OAuth Token**(`xoxb-` で始まる)をコピー +6. レポートを投稿したいチャンネルにボットを追加(チャンネル設定 → インテグレーション → アプリを追加) +7. チャンネルの **チャンネルID** を確認(チャンネル名を右クリック → チャンネル詳細 → 最下部に表示) + +### Part D: GitHub Secrets を設定する + +リポジトリの Settings → Secrets and variables → Actions → New repository secret で以下を登録: + +| Secret名 | 値 | +|---|---| +| `ANTHROPIC_API_KEY` | Claude API キー(https://console.anthropic.com/ で取得) | +| `GH_TOKEN` | GitHub Personal Access Token(repo スコープ) | +| `SLACK_BOT_TOKEN` | Part C でコピーした Bot User OAuth Token | +| `SLACK_CHANNEL` | Part C で確認したチャンネルID | + +### Part E: ローカル検証(オプション) + +GitHub Actions を動かす前に、設定が正しいか確認できます。Part B で設定した `owner` と `name` の値を使ってください。 + +**macOS / Linux:** +```bash +# Node.js が必要です(AIに「Node.jsをインストールして」と依頼してください) +GH_TOKEN=あなたのトークン node .claude/skills/github-api/scripts/get-commits.js あなたのユーザー名 リポジトリ名 +``` + +**Windows(コマンドプロンプト):** +```cmd +set GH_TOKEN=あなたのトークン +node .claude/skills/github-api/scripts/get-commits.js あなたのユーザー名 リポジトリ名 +``` + +コミットデータが表示されれば「ソース元」の設定は正しいです。 + +### Part F: GitHub Actions を実行する + +1. リポジトリの「Actions」タブを開く +2. 左メニューから「Daily Commit Report」を選択 +3. 「Run workflow」→「Run workflow」をクリック +4. 数分待つと Slack にレポートが届きます + +### チェックポイント + +- [ ] Slack チャンネルに図解レポートの画像が届いた +- [ ] レポートに自分のリポジトリのコミット情報が含まれている + +--- + +## アーキテクチャ + +このツールは Claude Code の **Skills** 機能を活用しています。各スキルが特定の役割を担い、メインプロンプト(`daily-report.md`)がそれらを順番に呼び出します。 + +``` +daily-report.md(メイン処理フロー) + │ + ├→ config-reader 設定ファイルを読み込む + ├→ github-api コミットデータを取得する + ├→ code-analyzer ビジネス視点で要約する + ├→ diagram-guidelines HTML図解を生成する + ├→ screenshot-capture HTMLをPNG画像に変換する + └→ slack-formatting Slackに画像を投稿する +``` + +### 生成されるレポート(5種類) + +| レポート | 内容 | +|---|---| +| **昨日の開発** | 統計情報(コミット数、ブランチ数)とハイライト | +| **ブランチ詳細** | 各ブランチの作業内容(1ブランチ1枚) | +| **アプリ別** | アプリごとにグループ化された変更一覧 | +| **タイムライン** | 時系列での作業履歴 | +| **ワンポイントTIPS** | 変更内容に関連する豆知識(オプション) | + +### 設定ファイルの2層構造 + +``` +configs/repos/your-repo.yml ← リポジトリ内のアプリを定義(共有) + ↑ 参照 +configs/projects/your-project.yml ← どのアプリを対象にするか(個別) +``` + +この分離により、1つのリポジトリ定義を複数のプロジェクトから参照できます。 + +--- + +## カスタマイズ + +### アプリ別の分類を設定する(モノレポ対応) + +`configs/repos/your-repo.yml` の `apps` セクションを編集: + +```yaml +apps: + - id: your-app + path: "src/app/" # ← このディレクトリのコミットが対象 + name: "あなたのアプリ名" + short_name: "App" + icon: "smartphone" # Lucide アイコン名 + color: "blue" # Tailwind CSS 色名 + category: "main" +``` + +不要なアプリ定義は行ごと削除してください。 + +### 定期実行を有効にする + +`.github/workflows/daily-report.yml` の cron 行のコメントを外す: + +```yaml +on: + schedule: + - cron: '0 22 * * *' # 毎日7:00 JST +``` + +### ワンポイントTIPSを無効にする + +`configs/projects/your-project.yml`: + +```yaml +tips: + enabled: false +``` + +### 図解のデザインを変更する + +`.claude/skills/diagram-guidelines/examples/` 内のHTMLを編集すると、Claudeが生成するレポートのデザインが変わります。 + +--- + +## セキュリティ + +- **リポジトリは Private に**: APIキーが含まれるため、公開リポジトリにしないでください +- **GitHub Secrets**: すべてのAPIキーは GitHub Secrets に保存されます。コードに直接書かないでください +- **Slack Bot Token**: Bot Token Scopes は `files:write` と `chat:write` のみに制限してください +- **GH_TOKEN**: Personal Access Token は `repo` スコープのみ必要です + +--- + +## 困ったとき + +1. **まずAIに聞く**: Cursorで「セットアップで〇〇のエラーが出ました」と伝えてください +2. **GitHub Actions のログを確認**: Actions タブ → 失敗したジョブ → ログを読む +3. **よくある問題**: + - 「コミットがない」→ 対象日のコミットがないか、リポジトリ名が間違っている + - 「not_authed」→ SLACK_BOT_TOKEN が間違っているか期限切れ + - 「Bad credentials」→ GH_TOKEN が間違っているか期限切れ +4. **Slackで相談**: ADS Slackの質問チャンネルに投稿してください + +--- + +## 技術スタック + +| 項目 | 選定 | 理由 | +|---|---|---| +| 実行基盤 | GitHub Actions | 無料枠2,000分/月、並列Job対応 | +| AI実行 | Claude Code Action | Skills機能で知識を分離・再利用 | +| 図解生成 | HTML + Tailwind CSS | Claude が生成しやすく、デザイン品質が高い | +| スクリーンショット | Playwright | Headless対応、GitHub Actionsで動作確認済み | +| 通知 | Slack API | 複数画像をまとめて投稿可能 | + +--- + +## 参考 + +- [Claude Code Skills](https://docs.anthropic.com/en/docs/claude-code/skills) +- [Claude Code Action](https://github.com/anthropics/claude-code-action) +- [Slack API - files.getUploadURLExternal](https://api.slack.com/methods/files.getUploadURLExternal) diff --git a/configs/projects/your-project.yml b/configs/projects/your-project.yml new file mode 100644 index 0000000..4e03545 --- /dev/null +++ b/configs/projects/your-project.yml @@ -0,0 +1,31 @@ +# ============================================================ +# プロジェクト設定 +# レポートの対象範囲と通知先を定義します +# ============================================================ + +project: + name: "開発レポート" # ← Slack に表示されるプロジェクト名 + description: "日次コミットレポート" + +# どのリポジトリ定義を使うか(configs/ からの相対パス) +repo_config: "repos/your-repo.yml" + +# レポートに含めるアプリ(repos/*.yml の id を指定) +# 不要なアプリは行ごと削除してください +include_apps: + - frontend + - api + - docs + +# Slack 設定(環境変数名を指定。値は GitHub Secrets に登録する) +slack: + token_env: SLACK_BOT_TOKEN + channel_env: SLACK_CHANNEL + channel_name: "#your-channel" # ← 参考用(実際の投稿先は Secrets の値で決まる) + +# レポート対象メンバー(空 = 全員対象) +target_authors: [] + +# ワンポイントTIPS(変更内容に関連する豆知識を自動生成) +tips: + enabled: true diff --git a/configs/repos/your-repo.yml b/configs/repos/your-repo.yml new file mode 100644 index 0000000..f554f57 --- /dev/null +++ b/configs/repos/your-repo.yml @@ -0,0 +1,42 @@ +# ============================================================ +# リポジトリ構造定義 +# あなたのリポジトリに合わせて書き換えてください +# ============================================================ + +repository: + owner: your-username # ← あなたの GitHub ユーザー名(または Organization 名) + name: your-web-app # ← 監視したいリポジトリ名 + +# リポジトリ内のアプリ/ディレクトリを定義 +# モノレポでない場合は1つだけ定義すればOK +apps: + - id: frontend # 一意のID(プロジェクト設定から参照する) + path: "src/" # ← コミットを分類するディレクトリパス + name: "Webサイト" # ← レポートに表示される名前(自分のアプリ名に変更) + short_name: "Web" # ← タグに表示される短縮名 + icon: "globe" # Lucide アイコン名 + color: "blue" # Tailwind CSS の色名 + category: "main" + + - id: api + path: "api/" + name: "APIサーバー" + short_name: "API" + icon: "database" + color: "emerald" + category: "main" + + # 不要なアプリ定義は削除してください + - id: docs + path: "docs/" + name: "ドキュメント" + short_name: "Docs" + icon: "book-open" + color: "purple" + category: "docs" + +categories: + main: + name: "メインアプリ" + docs: + name: "ドキュメント" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..972d3b1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,62 @@ +{ + "name": "comi-neko", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "comi-neko", + "version": "1.0.0", + "dependencies": { + "playwright": "^1.57.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..00f8def --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "comi-neko", + "version": "1.0.0", + "description": "GitHubコミットをAIで分析・図解し、Slackに自動投稿するツール", + "private": true, + "engines": { + "node": ">=18" + }, + "dependencies": { + "playwright": "^1.57.0" + } +}