feat: コミットレポートツール(コミネコ)初期コミット

Made-with: Cursor
This commit is contained in:
hiroki ito 2026-04-03 19:31:32 +09:00
commit c872debe4b
29 changed files with 4344 additions and 0 deletions

View File

@ -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` を使用
- アイコンはインラインSVGLucide互換
- **全ページにブランチタグと凡例を含める**
**⚠️ アバター画像の注意事項(重要):**
1. **ペアで埋め込む**: ユーザー名とアバターは**必ず同じコミット/authorオブジェクトから**取得
2. サンプルHTMLのプレースホルダーはコピーしない
3. **必ず** GitHub APIから取得した `author.avatar_url` を使用すること
4. サンプルのURLや架空のURLをコピーしない
**HTML生成時の正しいパターン:**
```html
<!-- ✅ 正しい: 同じ contributor オブジェクトから両方取得 -->
<img src="{{ contributor.avatar_url }}">
<span>{{ contributor.login }}</span>
<!-- ❌ 間違い: 別々のリストから取得(ズレる危険) -->
<img src="{{ avatars[i] }}">
<span>{{ logins[i] }}</span>
```
**禁止パターン:**
- ユーザー名リストとアバターリストを別々に作成してインデックスで組み合わせる
- ユニークなユーザー名を抽出した後、別途アバター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ページでサービス状態を確認

View File

@ -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 のタイトル/説明を参照

View File

@ -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 を使って今日のレポートを作成して"
```

View File

@ -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 1200pxRetina 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` の場合のみ生成
## ブランチタグ(全ページ共通)
すべてのコミット/ハイライト項目には**ブランチタグ**を表示します。
### ブランチの分類
**重要**: ブランチ名の prefixfeature/、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
<!-- インライン版(コンパクト) -->
<div class="flex items-center gap-1 px-1.5 py-0.5 bg-blue-100 rounded text-xs text-blue-600">
<div class="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
<span>feature/video</span>
</div>
<!-- ブランチ名が長い場合は短縮 -->
<!-- feature/video-player → feature/video -->
<!-- fix/login-session-timeout → fix/login -->
```
### フッターの凡例
すべてのページに**ブランチ凡例**を含めます(色のみで区別):
```html
<div class="flex gap-3">
<span class="flex items-center gap-1 text-xs text-green-600">
<span class="w-2 h-2 rounded-full bg-green-500"></span>デフォルト
</span>
<span class="flex items-center gap-1 text-xs text-blue-600">
<span class="w-2 h-2 rounded-full bg-blue-500"></span>作業
</span>
<span class="flex items-center gap-1 text-xs text-orange-600">
<span class="w-2 h-2 rounded-full bg-orange-500"></span>fix
</span>
</div>
```
※ 凡例は「色の意味」を示すもの。実際のタグにはブランチ名を表示する。
## ビジネス視点での記述
**重要**: すべての説明は「ユーザーにとって何が変わったか」を中心に記述します。
### 禁止表現と置き換え
| 禁止 | 代わりに書くべき内容 |
|------|----------------------|
| バグ修正 | 〇〇できなかった問題を解消 |
| 調整 | 〇〇が見やすく/使いやすくなった |
| 改善 | 〇〇が速く/簡単になった |
| 対応 | 〇〇できるようになった |
| リファクタリング | 〇〇の動作が安定した |
詳細は **code-analyzer** スキルを参照してください。
## 技術スタック
- **Tailwind CSS**: CDN版を使用 `<script src="https://cdn.tailwindcss.com"></script>`
- **shadcn/ui風デザイン**: slate系カラー、rounded-lg、shadow-sm
- **アイコン**: インラインSVGLucide互換を使用、絵文字は使わない
- **GitHubアバター**: `https://avatars.githubusercontent.com/u/{user_id}?v=4`
## 基本レイアウト
```html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>レポート</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', sans-serif; }
</style>
</head>
<body class="bg-white p-4">
<div class="w-[420px] h-[600px] mx-auto flex flex-col">
<!-- コンテンツ -->
</div>
</body>
</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
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-slate-900 flex items-center justify-center">
<!-- GitHubロゴSVG -->
</div>
<div>
<h1 class="text-base font-semibold text-slate-900">タイトル</h1>
<p class="text-xs text-slate-500">リポジトリ名</p>
</div>
</div>
<div class="text-sm font-medium text-slate-700">12/25 - 26</div>
</div>
```
### 統計バー(サマリー用)
```html
<div class="flex items-center justify-center gap-6 py-3 mb-4 bg-slate-50 rounded-lg">
<div class="text-center">
<div class="text-xl font-bold text-slate-900">10</div>
<div class="text-xs text-slate-500">commits</div>
</div>
<div class="w-px h-8 bg-slate-200"></div>
<div class="text-center">
<div class="text-xl font-bold text-slate-900">4</div>
<div class="text-xs text-slate-500">contributors</div>
</div>
</div>
```
### ハイライトアイテム(サマリー用)
```html
<div class="flex items-start gap-3 p-3 bg-amber-50 rounded-lg border border-amber-100">
<div class="w-1.5 h-1.5 rounded-full bg-amber-500 mt-2 flex-shrink-0"></div>
<div class="text-sm text-slate-800">動画を途中から再生できるようになった</div>
</div>
```
### アプリ別セクションby-app用
```html
<div class="mb-4">
<div class="flex items-center gap-2 mb-3">
<div class="w-6 h-6 rounded-lg bg-blue-500 flex items-center justify-center">
<!-- アプリアイコン -->
</div>
<div class="text-sm font-semibold text-slate-900">アプリ名</div>
<div class="text-xs text-slate-400">(path/)</div>
<div class="flex-1 h-px bg-slate-200 ml-2"></div>
</div>
<div class="space-y-2 pl-2">
<!-- 変更アイテム -->
</div>
</div>
```
### 変更アイテムby-app用
```html
<div class="flex items-start gap-3 p-3 bg-blue-50 rounded-lg border border-blue-100">
<div class="w-1.5 h-1.5 rounded-full bg-blue-500 mt-2 flex-shrink-0"></div>
<div>
<div class="text-sm text-slate-800">動画を途中から再生できるようになった</div>
<div class="text-xs text-slate-500 mt-1">補足説明</div>
</div>
</div>
```
### タイムラインアイテムtimeline用
```html
<div class="relative pl-6">
<div class="absolute left-0 top-1.5 w-4 h-4 rounded-full bg-blue-500 border-2 border-white"></div>
<div class="bg-slate-50 rounded-lg p-3 border border-slate-100">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-2">
<img src="avatar_url" class="w-5 h-5 rounded-full" alt="">
<span class="text-xs font-medium text-slate-700">username</span>
</div>
<span class="text-xs text-slate-400">12/26 12:01</span>
</div>
<div class="text-sm text-slate-800">変更内容</div>
<div class="flex items-center gap-1.5 mt-2">
<div class="w-2 h-2 rounded bg-blue-500"></div>
<span class="text-xs text-slate-400">アプリ名</span>
</div>
</div>
</div>
```
### フッター(共通)
```html
<div class="border-t border-slate-200 pt-3">
<div class="flex items-center justify-between">
<div class="flex -space-x-2">
<img src="avatar1" class="w-5 h-5 rounded-full border-2 border-white" alt="">
<img src="avatar2" class="w-5 h-5 rounded-full border-2 border-white" alt="">
</div>
<div class="text-xs text-slate-400">Generated by Claude Code</div>
</div>
</div>
```
### ワンポイントTIPStips用
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
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
</svg>
```
### スター(ハイライト)
```html
<svg class="w-5 h-5 text-amber-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/>
</svg>
```
### スマートフォン(モバイルアプリ)
```html
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"/>
</svg>
```
### パズル(ツール/プラグイン)
```html
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"/>
</svg>
```
## スクリーンショット撮影
**screenshot-capture** スキルのスクリプトを使用:
```bash
node .claude/skills/screenshot-capture/scripts/capture.js input.html output.png
```
出力サイズ: 840 x 1200pxRetina 2x
## 注意事項
- 絵文字は使用しない(プロフェッショナルなアイコンを使用)
- GitHubアバターを積極的に使用
- モノレポの場合は必ずアプリ別にグループ化
- 日本語で説明文を記述
- **すべての説明はビジネス視点で記述**
- "Generated by Claude Code" をフッターに含める

View File

@ -0,0 +1,183 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ブランチサマリー</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', sans-serif; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</head>
<body class="bg-white p-4">
<div class="w-[420px] h-[650px] mx-auto flex flex-col">
<!--
============================================================
ブランチサマリーは2パターンあります:
1. デフォルトブランチmain等: 緑
- 「今日反映された変更」セクション
- 累計情報は表示しない(意味がないため)
2. その他のブランチ(作業ブランチ): 青
- 「今日の作業内容」セクション
- 累計/今日/開発者/日目を表示
判定方法:
DEFAULT_BRANCH=$(gh api repos/{owner}/{repo} --jq '.default_branch')
⚠️ ラベルは常にブランチ名をそのまま表示main, feature/xxx, fix/xxx等
============================================================
-->
<!-- Header -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<!--
デフォルトブランチ: bg-green-500
その他: bg-blue-500
fix/*: bg-orange-500
-->
<div class="w-8 h-8 rounded-lg bg-green-500 flex items-center justify-center">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/>
</svg>
</div>
<div>
<h1 class="text-base font-semibold text-slate-900">{branch_name}</h1>
<p class="text-xs text-slate-500">{repository_name}</p>
</div>
</div>
<!--
色の決定(ラベルは常にブランチ名をそのまま表示):
デフォルトブランチ: bg-green-100 text-green-700
その他: bg-blue-100 text-blue-700
fix/*: bg-orange-100 text-orange-700
⚠️ ラベルは常にブランチ名をそのまま表示
-->
<span class="px-2 py-1 bg-green-100 text-green-700 text-xs font-medium rounded">{branch_name}</span>
</div>
<!--
============================================================
【デフォルトブランチ用】今日反映された変更
- 「目的」ではなく「今日反映された内容」を表示
- 累計情報セクションは表示しない
============================================================
-->
<div class="p-3 mb-4 bg-gradient-to-r from-green-50 to-emerald-50 rounded-lg border border-green-100">
<div class="flex items-start gap-2">
<svg class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<p class="text-sm font-medium text-slate-700 mb-1">今日反映された変更</p>
<!-- 今日のコミット内容をまとめて表示 -->
<p class="text-sm text-slate-600">{today_changes_summary}</p>
</div>
</div>
</div>
<!-- デフォルトブランチ: 今日のコミット数のみ表示 -->
<div class="flex items-center justify-center gap-6 py-3 mb-4 bg-slate-50 rounded-lg">
<div class="text-center">
<div class="text-xl font-bold text-green-600">+{today_commits}</div>
<div class="text-xs text-slate-500">今日の反映</div>
</div>
<div class="w-px h-8 bg-slate-200"></div>
<div class="text-center">
<div class="text-xl font-bold text-slate-900">{contributor_count}</div>
<div class="text-xs text-slate-500">コントリビューター</div>
</div>
</div>
<!--
============================================================
【作業ブランチ用(その他)】今日の作業内容
- 以下はコメントアウトしてありますが、作業ブランチの場合はこちらを使用
============================================================
<div class="p-3 mb-4 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg border border-blue-100">
<div class="flex items-start gap-2">
<svg class="w-4 h-4 text-blue-500 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</svg>
<div>
<p class="text-sm font-medium text-slate-700 mb-1">今日の作業内容</p>
<p class="text-sm text-slate-600">{work_summary}</p>
</div>
</div>
</div>
<div class="flex items-center justify-between gap-3 mb-4">
<div class="flex-1 p-3 bg-slate-50 rounded-lg text-center">
<div class="text-lg font-bold text-slate-900">{total_commits}</div>
<div class="text-xs text-slate-500">累計</div>
</div>
<div class="flex-1 p-3 bg-blue-50 rounded-lg text-center border border-blue-100">
<div class="text-lg font-bold text-blue-600">+{today_commits}</div>
<div class="text-xs text-slate-500">今日</div>
</div>
<div class="flex-1 p-3 bg-slate-50 rounded-lg text-center">
<div class="text-lg font-bold text-slate-900">{contributor_count}</div>
<div class="text-xs text-slate-500">開発者</div>
</div>
<div class="flex-1 p-3 bg-slate-50 rounded-lg text-center">
<div class="text-lg font-bold text-slate-900">{days_active}</div>
<div class="text-xs text-slate-500">日目</div>
</div>
</div>
-->
<!-- Today's Highlights (最大4件) -->
<div class="flex-1 overflow-hidden">
<div class="flex items-center gap-2 mb-3">
<svg class="w-5 h-5 text-amber-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/>
</svg>
<span class="text-sm font-semibold text-slate-900">今日のハイライト</span>
</div>
<div class="space-y-2">
<!--
ハイライトカード繰り返し、最大4件
種別タグ: 新機能(green), 改善(blue), 修正(purple)
-->
<div class="p-3 bg-amber-50 rounded-lg border border-amber-100">
<div class="flex items-center gap-2 mb-1.5">
<!-- 種別タグ: + 新機能 / ↑ 改善 / ~ 修正 -->
<div class="flex items-center gap-1.5 px-2 py-0.5 bg-blue-100 rounded text-xs text-blue-700">
<span></span>
<span>改善</span>
</div>
</div>
<!-- ビジネス視点での説明(技術用語を避ける) -->
<div class="text-sm text-slate-800">{highlight_description}</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="border-t border-slate-200 pt-3 mt-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3 text-xs text-slate-500">
<!--
デフォルトブランチ: 「最終: {datetime}」のみ
作業ブランチ: 「開始: {date} • 最終: {datetime}」
-->
<span>最終: {last_commit_datetime}</span>
</div>
<div class="text-xs text-slate-400">Generated by Claude Code</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>アプリ別レポート</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', sans-serif; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</head>
<body class="bg-white p-4">
<div class="w-[420px] h-[650px] mx-auto flex flex-col">
<!-- Header -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-slate-900 flex items-center justify-center">
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
</svg>
</div>
<div>
<h1 class="text-base font-semibold text-slate-900">アプリ別レポート</h1>
<p class="text-xs text-slate-500">your-web-app</p>
</div>
</div>
<div class="text-sm font-medium text-slate-700">1/15 - 16</div>
</div>
<!-- App Section: Webサイト -->
<div class="mb-4">
<div class="flex items-center gap-2 mb-3">
<div class="w-6 h-6 rounded-lg bg-blue-500 flex items-center justify-center">
<svg class="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"/>
</svg>
</div>
<div class="text-sm font-semibold text-slate-900">Webサイト</div>
<div class="text-xs text-slate-400">(web/)</div>
<div class="flex-1 h-px bg-slate-200 ml-2"></div>
</div>
<!--
変更カード(繰り返し)
ブランチタグの色(デフォルトブランチかどうかで判定):
- デフォルトブランチ: bg-green-100 text-green-700, ドット bg-green-500
- fix/*: bg-orange-100 text-orange-700, ドット bg-orange-500
- docs/*: bg-purple-100 text-purple-700, ドット bg-purple-500
- その他全て: bg-blue-100 text-blue-700, ドット bg-blue-500
⚠️ ラベルは常にブランチ名をそのまま表示main, feature/xxx, fix/xxx等
-->
<div class="space-y-1.5 pl-2 border-l-2 border-blue-200">
<div class="flex items-start gap-2 p-2 bg-slate-50 rounded">
<!-- ブランチタグ -->
<div class="flex items-center gap-1 px-1.5 py-0.5 bg-blue-100 rounded text-xs text-blue-600 flex-shrink-0">
<div class="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
<span>{branch_short_name}</span>
</div>
<span class="text-sm text-slate-700">{change_description}</span>
</div>
<!-- 5件を超える場合は省略 -->
<!-- <div class="flex items-center gap-2 pl-2 text-xs text-slate-400"><span>+N more commits</span></div> -->
</div>
<!--
⚠️⚠️⚠️ 重要: アバターとユーザー名の紐付けルール ⚠️⚠️⚠️
各 contributor の avatar_url と login は必ずペアで取得
別々のリストから組み合わせない
-->
<div class="flex items-center gap-2 mt-2 pl-2">
<div class="flex -space-x-1">
<!-- 各 contributor オブジェクトから avatar_url を取得 -->
<img src="{contributor1.avatar_url}" class="w-4 h-4 rounded-full border border-white" alt="">
<img src="{contributor2.avatar_url}" class="w-4 h-4 rounded-full border border-white" alt="">
</div>
<!-- 同じ contributor オブジェクトから login を取得 -->
<span class="text-xs text-slate-400">{contributor1.login}, {contributor2.login}</span>
</div>
</div>
<!-- App Section: APIサーバー -->
<div class="mb-4">
<div class="flex items-center gap-2 mb-3">
<div class="w-6 h-6 rounded-lg bg-purple-500 flex items-center justify-center">
<svg class="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"/>
</svg>
</div>
<div class="text-sm font-semibold text-slate-900">APIサーバー</div>
<div class="text-xs text-slate-400">(api/)</div>
<div class="flex-1 h-px bg-slate-200 ml-2"></div>
</div>
<!-- 同様のブランチタグ付きカード -->
<div class="space-y-1.5 pl-2 border-l-2 border-purple-200">
<div class="flex items-start gap-2 p-2 bg-slate-50 rounded">
<div class="flex items-center gap-1 px-1.5 py-0.5 bg-green-100 rounded text-xs text-green-600 flex-shrink-0">
<div class="w-1.5 h-1.5 rounded-full bg-green-500"></div>
<span>main</span>
</div>
<span class="text-sm text-slate-700">{change_description}</span>
</div>
</div>
<!-- 同様: contributor オブジェクトから avatar_url と login をペアで取得 -->
<div class="flex items-center gap-2 mt-2 pl-2">
<div class="flex -space-x-1">
<img src="{contributor.avatar_url}" class="w-4 h-4 rounded-full border border-white" alt="">
</div>
<span class="text-xs text-slate-400">{contributor.login}</span>
</div>
</div>
<!-- Footer - ブランチ凡例(色のみ) -->
<div class="border-t border-slate-200 pt-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="flex items-center gap-1 text-xs text-green-600"><span class="w-2 h-2 rounded-full bg-green-500"></span>デフォルト</span>
<span class="flex items-center gap-1 text-xs text-blue-600"><span class="w-2 h-2 rounded-full bg-blue-500"></span>作業</span>
<span class="flex items-center gap-1 text-xs text-orange-600"><span class="w-2 h-2 rounded-full bg-orange-500"></span>fix</span>
</div>
<div class="text-xs text-slate-400">Generated by Claude Code</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>今日の開発</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', sans-serif; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</head>
<body class="bg-white p-4">
<div class="w-[420px] h-[650px] mx-auto flex flex-col">
<!--
============================================================
daily-summary: overview と summary を統合したページ
目的: 「今日何があったか」を1枚で伝える
構成:
1. ヘッダー(タイトル + 日付)
2. 統計commits / branches / contributors
3. ハイライト誰が何をしたか、最大4件
4. フッター(コントリビューター一覧)
⚠️ ブランチの詳細リストは不要branch-summaryで見れる
============================================================
-->
<!-- Header -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<div>
<h1 class="text-base font-semibold text-slate-900">今日の開発</h1>
<p class="text-xs text-slate-500">{repository_name}</p>
</div>
</div>
<div class="text-sm font-medium text-slate-700">{date}</div>
</div>
<!-- Stats -->
<div class="flex items-center justify-center gap-6 py-3 mb-4 bg-gradient-to-r from-violet-50 to-purple-50 rounded-lg border border-violet-100">
<div class="text-center">
<div class="text-xl font-bold text-violet-700">{total_commits}</div>
<div class="text-xs text-slate-500">commits</div>
</div>
<div class="w-px h-8 bg-violet-200"></div>
<div class="text-center">
<div class="text-xl font-bold text-violet-700">{branch_count}</div>
<div class="text-xs text-slate-500">branches</div>
</div>
<div class="w-px h-8 bg-violet-200"></div>
<div class="text-center">
<div class="text-xl font-bold text-violet-700">{contributor_count}</div>
<div class="text-xs text-slate-500">contributors</div>
</div>
</div>
<!-- Highlights Section -->
<div class="flex-1 overflow-hidden">
<div class="flex items-center gap-2 mb-3">
<svg class="w-5 h-5 text-amber-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/>
</svg>
<span class="text-sm font-semibold text-slate-900">ハイライト</span>
</div>
<!--
============================================================
ハイライトカード繰り返し、最大4件
============================================================
各カードに必須の情報:
- avatar_url: GitHub APIから取得 (author.avatar_url)
- username: GitHub APIから取得 (author.login)
- branch_name: どのブランチへのコミットか
- description: ビジネス視点での説明
ブランチタグの色:
- デフォルトブランチ: bg-green-100 text-green-700, ドット bg-green-500
- fix/*: bg-orange-100 text-orange-700, ドット bg-orange-500
- docs/*: bg-purple-100 text-purple-700, ドット bg-purple-500
- その他: bg-blue-100 text-blue-700, ドット bg-blue-500
⚠️ ラベルは常にブランチ名をそのまま表示
============================================================
⚠️⚠️⚠️ 重要: アバターとユーザー名の紐付けルール ⚠️⚠️⚠️
============================================================
各ハイライトカードの avatar_url と username は
**必ず同一のコミット/authorオブジェクトから取得すること**
✅ 正しい例:
commit.author.avatar_url と commit.author.login を同時に使用
❌ 間違い例:
avatars配列[i] と usernames配列[i] を別々に取得して組み合わせ
→ インデックスがズレてアバターとユーザー名が不一致になる
============================================================
-->
<div class="space-y-2.5">
<!-- Highlight Card 1 -->
<div class="p-3 bg-amber-50 rounded-lg border border-amber-100">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<!-- ⚠️ src は author.avatar_url から取得 -->
<img src="{avatar_url}" class="w-7 h-7 rounded-full" alt="">
<span class="text-sm font-medium text-slate-900">{username}</span>
</div>
<!-- ブランチタグ -->
<div class="flex items-center gap-1 px-2 py-0.5 bg-green-100 rounded text-xs text-green-700">
<div class="w-1.5 h-1.5 rounded-full bg-green-500"></div>
<span>{branch_name}</span>
</div>
</div>
<div class="text-sm text-slate-700 leading-relaxed line-clamp-2">{highlight_description}</div>
</div>
<!-- Highlight Card 2 -->
<div class="p-3 bg-amber-50 rounded-lg border border-amber-100">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<img src="{avatar_url}" class="w-7 h-7 rounded-full" alt="">
<span class="text-sm font-medium text-slate-900">{username}</span>
</div>
<div class="flex items-center gap-1 px-2 py-0.5 bg-blue-100 rounded text-xs text-blue-700">
<div class="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
<span>{branch_name}</span>
</div>
</div>
<div class="text-sm text-slate-700 leading-relaxed line-clamp-2">{highlight_description}</div>
</div>
<!-- Highlight Card 3 -->
<div class="p-3 bg-amber-50 rounded-lg border border-amber-100">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<img src="{avatar_url}" class="w-7 h-7 rounded-full" alt="">
<span class="text-sm font-medium text-slate-900">{username}</span>
</div>
<div class="flex items-center gap-1 px-2 py-0.5 bg-green-100 rounded text-xs text-green-700">
<div class="w-1.5 h-1.5 rounded-full bg-green-500"></div>
<span>{branch_name}</span>
</div>
</div>
<div class="text-sm text-slate-700 leading-relaxed line-clamp-2">{highlight_description}</div>
</div>
<!-- Highlight Card 4 -->
<div class="p-3 bg-amber-50 rounded-lg border border-amber-100">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<img src="{avatar_url}" class="w-7 h-7 rounded-full" alt="">
<span class="text-sm font-medium text-slate-900">{username}</span>
</div>
<div class="flex items-center gap-1 px-2 py-0.5 bg-blue-100 rounded text-xs text-blue-700">
<div class="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
<span>{branch_name}</span>
</div>
</div>
<div class="text-sm text-slate-700 leading-relaxed line-clamp-2">{highlight_description}</div>
</div>
<!-- 4件を超える場合 -->
<!-- <div class="text-center text-xs text-slate-400 py-1">+{N} more highlights</div> -->
</div>
</div>
<!-- Footer -->
<!--
⚠️ フッターのアバター一覧も同様のルール:
各 contributor オブジェクトから avatar_url と login をペアで取得
別々のリストから組み合わせない
-->
<div class="border-t border-slate-200 pt-3 mt-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="flex -space-x-2">
<img src="{avatar_url_1}" class="w-6 h-6 rounded-full border-2 border-white" alt="">
<img src="{avatar_url_2}" class="w-6 h-6 rounded-full border-2 border-white" alt="">
<img src="{avatar_url_3}" class="w-6 h-6 rounded-full border-2 border-white" alt="">
</div>
<!-- 例: "user-a(5) user-b(3)" -->
<span class="text-xs text-slate-500">{contributor_summary}</span>
</div>
<div class="text-xs text-slate-400">Generated by Claude Code</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タイムライン</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', sans-serif; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</head>
<body class="bg-white p-4">
<div class="w-[420px] h-[650px] mx-auto flex flex-col">
<!-- Header -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-slate-900 flex items-center justify-center">
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
</svg>
</div>
<div>
<h1 class="text-base font-semibold text-slate-900">タイムライン</h1>
<p class="text-xs text-slate-500">your-web-app</p>
</div>
</div>
<div class="text-sm font-medium text-slate-700">1/15 - 16</div>
</div>
<!-- Timeline (max 4 entries to fit 600px height) -->
<div class="relative flex-1">
<div class="absolute left-[7px] top-2 bottom-2 w-0.5 bg-slate-200"></div>
<div class="space-y-2.5">
<!--
============================================================
⚠️⚠️⚠️ 重要: アバターとユーザー名の紐付けルール ⚠️⚠️⚠️
============================================================
各エントリの avatar_url と username は
**必ず同一のコミット/authorオブジェクトから取得すること**
✅ 正しい: commit.author.avatar_url + commit.author.login
❌ 間違い: avatars[i] + usernames[i] (別リストからの組み合わせ)
author.avatar_url = アバター画像URL
author.login = GitHubユーザー名
============================================================
-->
<!--
タイムラインエントリ繰り返し、最大4件
ブランチタグの色(デフォルトブランチかどうかで判定):
- デフォルトブランチ: bg-green-100 text-green-700, ドット bg-green-500
- fix/*: bg-orange-100 text-orange-700, ドット bg-orange-500
- docs/*: bg-purple-100 text-purple-700, ドット bg-purple-500
- その他全て: bg-blue-100 text-blue-700, ドット bg-blue-500
⚠️ ラベルは常にブランチ名をそのまま表示main, feature/xxx, fix/xxx等
⚠️ avatar_url と username は必ずGitHub APIから取得
-->
<div class="relative pl-6">
<div class="absolute left-0 top-1.5 w-4 h-4 rounded-full bg-blue-500 border-2 border-white"></div>
<div class="bg-slate-50 rounded-lg p-2.5 border border-slate-100">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-2">
<!-- src must be author.avatar_url from API -->
<img src="{avatar_url}" class="w-5 h-5 rounded-full" alt="">
<span class="text-xs font-medium text-slate-700">{username}</span>
</div>
<!-- ブランチタグ -->
<div class="flex items-center gap-1 px-1.5 py-0.5 bg-blue-100 rounded text-xs text-blue-600">
<div class="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
<span>{branch_name}</span>
</div>
</div>
<div class="text-sm text-slate-800">{commit_description}</div>
<div class="flex items-center justify-between mt-1.5">
<div class="flex items-center gap-1.5">
<span class="text-xs text-slate-400">{app_name}</span>
</div>
<span class="text-xs text-slate-400">{time}</span>
</div>
</div>
</div>
<!-- 以下同様のエントリを最大4件まで繰り返し -->
<!-- 4件を超える場合はページ分割: timeline-{branch}-2.png -->
</div>
</div>
<!-- More indicator (4件を超える場合) -->
<div class="text-center py-2 text-xs text-slate-400">
+{remaining_commits} more commits
</div>
<!-- Footer - ブランチ凡例(色のみ) -->
<div class="border-t border-slate-200 pt-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="flex items-center gap-1 text-xs text-green-600"><span class="w-2 h-2 rounded-full bg-green-500"></span>デフォルト</span>
<span class="flex items-center gap-1 text-xs text-blue-600"><span class="w-2 h-2 rounded-full bg-blue-500"></span>作業</span>
<span class="flex items-center gap-1 text-xs text-orange-600"><span class="w-2 h-2 rounded-full bg-orange-500"></span>fix</span>
</div>
<div class="text-xs text-slate-400">Generated by Claude Code</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ワンポイントTIPS</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', sans-serif; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
</head>
<body class="bg-white p-4">
<div class="w-[420px] h-[650px] mx-auto flex flex-col">
<!-- Header -->
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-amber-500 flex items-center justify-center">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</div>
<div>
<h1 class="text-base font-semibold text-slate-900">ワンポイントTIPS</h1>
<p class="text-xs text-slate-500">今日の変更から学ぶ</p>
</div>
</div>
<div class="text-sm font-medium text-slate-700">1/15</div>
</div>
<!-- Related Change - ブランチタグ付き -->
<div class="mb-3 p-2.5 bg-blue-50 rounded-lg border border-blue-100">
<div class="flex items-center gap-2 mb-0.5">
<svg class="w-3.5 h-3.5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<span class="text-xs font-medium text-blue-700">関連する変更</span>
<!--
ブランチタグの色(デフォルトブランチかどうかで判定):
- デフォルトブランチ: bg-green-100 text-green-700, ドット bg-green-500
- fix/*: bg-orange-100 text-orange-700, ドット bg-orange-500
- docs/*: bg-purple-100 text-purple-700, ドット bg-purple-500
- その他全て: bg-blue-100 text-blue-700, ドット bg-blue-500
⚠️ ラベルは常にブランチ名をそのまま表示main, feature/xxx, fix/xxx等
-->
<div class="flex items-center gap-1 px-1.5 py-0.5 bg-green-100 rounded text-xs text-green-600 ml-auto">
<div class="w-1.5 h-1.5 rounded-full bg-green-500"></div>
<span>{branch_name}</span>
</div>
</div>
<div class="text-sm text-slate-700">{related_change_description}</div>
</div>
<!-- Topic Title -->
<div class="mb-2">
<h2 class="text-lg font-bold text-slate-900">動画プレイヤーの仕組み</h2>
</div>
<!-- Diagram (3ステップまでに収める) -->
<div class="mb-3 p-3 bg-slate-50 rounded-lg">
<div class="flex flex-col items-center gap-1.5">
<div class="px-3 py-1.5 bg-white rounded-lg border border-slate-200 text-sm text-slate-700">
動画ファイル(圧縮)
</div>
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
</svg>
<div class="px-3 py-1.5 bg-amber-100 rounded-lg border border-amber-200 text-sm text-amber-800">
デコーダー(解凍処理)
</div>
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
</svg>
<div class="px-3 py-1.5 bg-blue-100 rounded-lg border border-blue-200 text-sm text-blue-800">
画面に表示
</div>
</div>
</div>
<!-- Explanation (コンパクトに) -->
<div class="flex-1 space-y-2">
<p class="text-sm text-slate-700 leading-relaxed">
動画は<span class="font-medium text-slate-900">圧縮された状態</span>で保存されており、再生時にリアルタイムで解凍しています。この処理が端末の性能に追いつかないと<span class="font-medium text-slate-900">カクつきやフリーズ</span>の原因になります。
</p>
<div class="p-2.5 bg-amber-50 rounded-lg border border-amber-100">
<div class="flex items-start gap-2">
<svg class="w-4 h-4 text-amber-500 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/>
</svg>
<p class="text-sm text-slate-700">
<span class="font-medium">今回の修正:</span>
Android端末向けに解凍処理を最適化しました
</p>
</div>
</div>
</div>
<!-- Footer -->
<div class="border-t border-slate-200 pt-2.5 mt-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<div class="w-2 h-2 rounded-full bg-amber-500"></div>
<span class="text-xs text-slate-500">技術豆知識</span>
</div>
<div class="text-xs text-slate-400">Generated by Claude Code</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -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 <owner> <repo> --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 <owner> --repo <repo> --paths "app/,web/,supabase/"
```
`get-all-branch-commits.js` の出力を受け取り、指定パスに関連するコミットのみを抽出します。
各コミットの変更ファイルを GitHub API で取得し、パスマッチングを行います。
**⚠️ 重要: コミットのパスフィルタリングは必ずこのスクリプトを使用してください。**
アドホックなフィルタリングコードは全件処理を保証できません。
| 引数 | 必須 | 説明 |
|------|------|------|
| `--input <file>` | No | 入力ファイル省略時stdin |
| `--owner <name>` | Yes | リポジトリオーナー |
| `--repo <name>` | Yes | リポジトリ名 |
| `--paths <list>` | Yes | カンマ区切りパス(例: "app/,web/" |
| `--concurrency <n>` | 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 <owner> <repo> [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` ヘッダーで残り確認可能

View File

@ -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 <file> 入力ファイル省略時stdin
* --owner <name> リポジトリオーナー必須
* --repo <name> リポジトリ名必須
* --paths <list> カンマ区切りパス必須: "app/,web/"
* --concurrency <n> 並列数デフォルト: 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 <owner> --repo <repo> --paths <paths>');
console.error('');
console.error('Required:');
console.error(' --owner <name> Repository owner');
console.error(' --repo <name> Repository name');
console.error(' --paths <list> Comma-separated paths (e.g. "app/,web/,supabase/")');
console.error('');
console.error('Optional:');
console.error(' --input <file> Input file (default: stdin)');
console.error(' --concurrency <n> 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<Array>}
*/
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();

View File

@ -0,0 +1,315 @@
#!/usr/bin/env node
/**
* 全ブランチの前日(JST)コミットを一括取得するスクリプト
*
* 最適化: GraphQLで最近アクティブなブランチのみを抽出してからコミット取得
*
* 使い方:
* node get-all-branch-commits.js <owner> <repo> [--output <file>]
*
* 環境変数:
* 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 <owner> <repo> [--output <file>]');
console.error('');
console.error('Options:');
console.error(' --output, -o <file> 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();

View File

@ -0,0 +1,63 @@
#!/usr/bin/env node
/**
* 前日(JST)のコミットを確実に取得するスクリプト
*
* 使い方:
* node get-commits.js <owner> <repo> [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 <owner> <repo> [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();

View File

@ -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 };

View File

@ -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 };

View File

@ -0,0 +1,75 @@
---
name: screenshot-capture
description: HTMLファイルのスクリーンショット撮影。図解をPNG化するときに使用。
---
# Screenshot Capture
HTMLファイルをPNG画像に変換するスキルです。
## 使い方
```bash
node .claude/skills/screenshot-capture/scripts/capture.js <input.html> <output.png>
```
### 例
```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
```

View File

@ -0,0 +1,143 @@
#!/usr/bin/env node
/**
* Batch Screenshot Capture Script
*
* 複数のHTMLファイルを一括でPNG化します
*
* Usage: node capture-batch.js <file1.html:output1.png> <file2.html:output2.png> ...
*
* 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 <input.html:output.png> ...');
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();

View File

@ -0,0 +1,119 @@
#!/usr/bin/env node
/**
* Screenshot Capture Script
*
* HTMLファイルをPNG画像に変換します
* - Retina対応 (2x)
* - コンテンツサイズに自動フィット余白なし
* - GitHub Actions対応Headless
*
* Usage: node capture.js <input.html> <output.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 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 <input.html> <output.png>');
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();

View File

@ -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` |
| リンク | `<URL\|テキスト>` | <https://github.com\|GitHub> |
| メンション | `<@USER_ID>` | <@U1234> |
| チャンネル | `<#CHANNEL_ID>` | <#C1234> |
---
## トラブルシューティング
| 症状 | 原因 | 対処 |
|------|------|------|
| 画像がスレッドに投稿された | スクリプトを使っていない | `post-report.js` を使用する |
| 画像が1枚しか投稿されない | スクリプトを使っていない | `post-report.js` を使用する |
| 3回投稿された | 投稿処理が複数回呼ばれた | ログで `SLACK投稿開始` の回数を確認 |
| `not_authed` エラー | 環境変数が渡されていない | `SLACK_BOT_TOKEN` を確認 |

View File

@ -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 "テキスト" <image1.png> <image2.png> ...
*
* 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 "テキスト" <image1.png> ...');
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();

31
.github/workflows/daily-report.yml vendored Normal file
View File

@ -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 }}

120
.github/workflows/report-job.yml vendored Normal file
View File

@ -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<<EOF'
cat .claude/prompts/daily-report.md
echo ''
echo '## 設定ファイル'
echo '${{ inputs.config_file }} を使用してください。'
echo 'EOF'
} >> "$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 }}

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules/
.DS_Store
.playwright-mcp/
/tmp/
output/
*.png
.env
.env.local

58
CLAUDE.md Normal file
View File

@ -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 を呼び出す
- 独自の投稿ロジックを書く

296
README.md Normal file
View File

@ -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/ # お手本HTML5種類
├── 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 Tokenrepo スコープ) |
| `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)

View File

@ -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

View File

@ -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: "ドキュメント"

62
package-lock.json generated Normal file
View File

@ -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"
}
}
}
}

12
package.json Normal file
View File

@ -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"
}
}