-
-
+
+
+
+
+
+
+
+ ▶
+
+
+ 📁
+
+ {{ folder.name }}
+
+ ({{ folder.topics.length }}件)
+
+
+
+
+
{{ folder.progress }}%
+
+
+ {{ folder.completed }}
+ {{ folder.in_progress }}
+ {{ folder.not_started }}
+
+
+
+
+
+
+
+
+
+
@@ -667,6 +1008,312 @@
{{ selectedProject.name }}
+
+
+
+
⚙️ 設定
+
+
+
+
+
+
+
+
+
+
+
+
+ 📦 納品先マスター
+ ({{ destinations.length }}件)
+
+
プロジェクトの納品先を管理します
+
+
+
+
+
+
+
{{ index + 1 }}.
+
+
+
+
+
+
{{ dest.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🔊 音声変換マスター
+ ({{ ttsEngines.length }}件)
+
+
音声生成に使用するTTSエンジンを管理します
+
+
+
+
+
+
+
{{ index + 1 }}.
+
+
+
+
+
+
{{ tts.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 📋 公開状態マスター
+ ({{ publicationStatuses.length }}件)
+
+
プロジェクトの公開状態を管理します
+
+
+
+
+
+
+ {{ status.display_order || '-' }}
+
+
+
+
+ {{ status.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/project/frontend/js/api.js b/project/frontend/js/api.js
index cfc88ea..5bfa993 100644
--- a/project/frontend/js/api.js
+++ b/project/frontend/js/api.js
@@ -78,6 +78,59 @@ const API = {
}
},
+ /**
+ * PUTリクエスト
+ */
+ async put(endpoint, body = {}, options = {}) {
+ const url = `${this.baseUrl}${endpoint}`;
+
+ try {
+ const response = await fetch(url, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers
+ },
+ body: JSON.stringify(body)
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error(`API PUT error: ${url}`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * DELETEリクエスト
+ */
+ async delete(endpoint, options = {}) {
+ const url = `${this.baseUrl}${endpoint}`;
+
+ try {
+ const response = await fetch(url, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error(`API DELETE error: ${url}`, error);
+ throw error;
+ }
+ },
+
/**
* キャッシュをクリア
*/
@@ -136,6 +189,115 @@ const API = {
return await this.get('/stats');
},
+ // ========== プロジェクト設定API ==========
+
+ /**
+ * プロジェクト設定更新
+ */
+ async updateProjectSettings(projectId, settings) {
+ this.clearCache('/projects');
+ return await this.put(`/projects/${projectId}/settings`, settings);
+ },
+
+ // ========== 納品先マスターAPI ==========
+
+ /**
+ * 納品先一覧取得
+ */
+ async getDestinations() {
+ return await this.get('/destinations');
+ },
+
+ /**
+ * 納品先作成
+ */
+ async createDestination(data) {
+ this.clearCache('/destinations');
+ return await this.post('/destinations', data);
+ },
+
+ /**
+ * 納品先更新
+ */
+ async updateDestination(id, data) {
+ this.clearCache('/destinations');
+ return await this.put(`/destinations/${id}`, data);
+ },
+
+ /**
+ * 納品先削除
+ */
+ async deleteDestination(id) {
+ this.clearCache('/destinations');
+ return await this.delete(`/destinations/${id}`);
+ },
+
+ // ========== 音声変換エンジンマスターAPI ==========
+
+ /**
+ * 音声変換エンジン一覧取得
+ */
+ async getTtsEngines() {
+ return await this.get('/tts-engines');
+ },
+
+ /**
+ * 音声変換エンジン作成
+ */
+ async createTtsEngine(data) {
+ this.clearCache('/tts-engines');
+ return await this.post('/tts-engines', data);
+ },
+
+ /**
+ * 音声変換エンジン更新
+ */
+ async updateTtsEngine(id, data) {
+ this.clearCache('/tts-engines');
+ return await this.put(`/tts-engines/${id}`, data);
+ },
+
+ /**
+ * 音声変換エンジン削除
+ */
+ async deleteTtsEngine(id) {
+ this.clearCache('/tts-engines');
+ return await this.delete(`/tts-engines/${id}`);
+ },
+
+ // ========== 公開状態マスターAPI ==========
+
+ /**
+ * 公開状態一覧取得
+ */
+ async getPublicationStatuses() {
+ return await this.get('/publication-statuses');
+ },
+
+ /**
+ * 公開状態作成
+ */
+ async createPublicationStatus(data) {
+ this.clearCache('/publication-statuses');
+ return await this.post('/publication-statuses', data);
+ },
+
+ /**
+ * 公開状態更新
+ */
+ async updatePublicationStatus(id, data) {
+ this.clearCache('/publication-statuses');
+ return await this.put(`/publication-statuses/${id}`, data);
+ },
+
+ /**
+ * 公開状態削除
+ */
+ async deletePublicationStatus(id) {
+ this.clearCache('/publication-statuses');
+ return await this.delete(`/publication-statuses/${id}`);
+ },
+
// ========== ヘルスチェック ==========
/**
diff --git a/project/frontend/js/app.js b/project/frontend/js/app.js
index b5ca29b..e26387b 100644
--- a/project/frontend/js/app.js
+++ b/project/frontend/js/app.js
@@ -37,12 +37,27 @@ const app = createApp({
const customRangeMin = ref(0); // カスタム範囲の最小値
const customRangeMax = ref(100); // カスタム範囲の最大値
+ // 納品先・音声変換・公開状態フィルター
+ const destinationFilter = ref('all'); // 'all' または destination_id
+ const ttsEngineFilter = ref('all'); // 'all' または tts_engine_id
+ const publicationStatusFilter = ref('all'); // 'all', 'unset', または publication_status_id
+
+ // マスターデータ
+ const destinations = ref([]);
+ const ttsEngines = ref([]);
+ const publicationStatuses = ref([]);
+
+ // 設定画面の状態
+ const settingsTab = ref('destinations'); // 'destinations', 'tts-engines', 'publication-statuses'
+ const editingItem = ref(null); // 編集中のアイテム
+ const newItemName = ref(''); // 新規追加時の名前
+
// フィルターラベル
const filterLabels = {
- all: '全て',
- completed: '完了',
- in_progress: '進行中',
- not_started: '未着手'
+ all: '📋 全て',
+ completed: '✅ 完了(全ファイル揃い)',
+ in_progress: '⚠️ 一部不足(作業中)',
+ not_started: '○ 未着手'
};
// ========== 算出プロパティ ==========
@@ -95,23 +110,49 @@ const app = createApp({
// フィルター適用後のプロジェクト
const filteredProjects = computed(() => {
- const list = projects.value || [];
+ let list = projects.value || [];
+
+ // 進捗フィルター
switch (projectFilter.value) {
case 'completed':
- return list.filter(p => (p.progress || 0) >= 100);
+ list = list.filter(p => (p.progress || 0) >= 100);
+ break;
case 'incomplete':
- return list.filter(p => (p.progress || 0) < 100);
+ list = list.filter(p => (p.progress || 0) < 100);
+ break;
case 'custom':
const min = Number(customRangeMin.value) || 0;
const max = Number(customRangeMax.value) || 100;
- return list.filter(p => {
+ list = list.filter(p => {
const progress = p.progress || 0;
return progress >= min && progress <= max;
});
- case 'all':
- default:
- return list;
+ break;
+ }
+
+ // 納品先フィルター
+ if (destinationFilter.value !== 'all') {
+ const destId = Number(destinationFilter.value);
+ list = list.filter(p => p.destination_id === destId);
+ }
+
+ // 音声変換フィルター
+ if (ttsEngineFilter.value !== 'all') {
+ const ttsId = Number(ttsEngineFilter.value);
+ list = list.filter(p => p.tts_engine_id === ttsId);
+ }
+
+ // 公開状態フィルター
+ if (publicationStatusFilter.value !== 'all') {
+ if (publicationStatusFilter.value === 'unset') {
+ list = list.filter(p => !p.publication_status_id);
+ } else {
+ const statusId = Number(publicationStatusFilter.value);
+ list = list.filter(p => p.publication_status_id === statusId);
+ }
}
+
+ return list;
});
// フィルター + ソート適用後のプロジェクト
@@ -149,6 +190,54 @@ const app = createApp({
return topics.value.filter(t => t.status === topicFilter.value);
});
+ // サブフォルダでグループ化されたトピック一覧
+ const groupedTopics = computed(() => {
+ const grouped = {};
+ const topicList = filteredTopics.value || [];
+
+ for (const topic of topicList) {
+ const folder = topic.subfolder || '';
+ if (!grouped[folder]) {
+ grouped[folder] = {
+ name: folder || 'ルート',
+ topics: [],
+ completed: 0,
+ in_progress: 0,
+ not_started: 0
+ };
+ }
+ grouped[folder].topics.push(topic);
+
+ // ステータスカウント
+ if (topic.status === 'completed') {
+ grouped[folder].completed++;
+ } else if (topic.status === 'in_progress') {
+ grouped[folder].in_progress++;
+ } else {
+ grouped[folder].not_started++;
+ }
+ }
+
+ // サブフォルダ名でソートして配列に変換
+ return Object.entries(grouped)
+ .sort(([a], [b]) => {
+ // ルートは常に最初
+ if (a === '') return -1;
+ if (b === '') return 1;
+ return a.localeCompare(b, 'ja');
+ })
+ .map(([key, value]) => ({
+ key,
+ ...value,
+ progress: value.topics.length > 0
+ ? Math.round((value.completed / value.topics.length) * 100)
+ : 0
+ }));
+ });
+
+ // サブフォルダの展開状態
+ const expandedFolders = ref({});
+
// ========== メソッド ==========
// プロジェクト一覧を取得
@@ -164,6 +253,240 @@ const app = createApp({
}
}
+ // マスターデータを取得
+ async function fetchMasterData() {
+ try {
+ const [destData, ttsData, pubData] = await Promise.all([
+ API.getDestinations(),
+ API.getTtsEngines(),
+ API.getPublicationStatuses()
+ ]);
+ destinations.value = destData.destinations || [];
+ ttsEngines.value = ttsData.tts_engines || [];
+ publicationStatuses.value = pubData.publication_statuses || [];
+ } catch (error) {
+ console.error('Failed to fetch master data:', error);
+ }
+ }
+
+ // プロジェクト設定を更新
+ async function updateProjectSettings(projectId, settings) {
+ try {
+ const result = await API.updateProjectSettings(projectId, settings);
+ // ローカル状態を更新
+ const index = projects.value.findIndex(p => p.id === projectId);
+ if (index !== -1 && result.project) {
+ projects.value[index] = { ...projects.value[index], ...result.project };
+ }
+ showToast('設定を更新しました', 'success');
+ } catch (error) {
+ console.error('Failed to update project settings:', error);
+ showToast('設定の更新に失敗しました', 'error');
+ }
+ }
+
+ // 納品先の変更
+ function onDestinationChange(projectId, destinationId) {
+ const destId = destinationId === '' ? null : Number(destinationId);
+ const project = projects.value.find(p => p.id === projectId);
+ if (project) {
+ updateProjectSettings(projectId, {
+ destination_id: destId,
+ tts_engine_id: project.tts_engine_id
+ });
+ }
+ }
+
+ // 音声変換エンジンの変更
+ function onTtsEngineChange(projectId, ttsEngineId) {
+ const ttsId = ttsEngineId === '' ? null : Number(ttsEngineId);
+ const project = projects.value.find(p => p.id === projectId);
+ if (project) {
+ updateProjectSettings(projectId, {
+ destination_id: project.destination_id,
+ tts_engine_id: ttsId
+ });
+ }
+ }
+
+ // 公開状態の変更
+ function onPublicationStatusChange(projectId, statusId) {
+ const pubStatusId = statusId === '' ? null : Number(statusId);
+ const project = projects.value.find(p => p.id === projectId);
+ if (project) {
+ updateProjectSettings(projectId, {
+ destination_id: project.destination_id,
+ tts_engine_id: project.tts_engine_id,
+ publication_status_id: pubStatusId
+ });
+ }
+ }
+
+ // 公開状態のラベル取得(マスターデータから)
+ function getPublicationStatusLabel(statusId) {
+ if (!statusId) return '📋 公開状態';
+ const status = publicationStatuses.value.find(s => s.id === statusId);
+ return status ? status.name : '📋 公開状態';
+ }
+
+ // 公開状態のバッジクラス取得
+ function getPublicationStatusClass(statusId) {
+ if (!statusId) return 'bg-gray-100 text-gray-700';
+ const status = publicationStatuses.value.find(s => s.id === statusId);
+ if (!status) return 'bg-gray-100 text-gray-700';
+ // 名前に基づいてクラスを決定
+ if (status.name.includes('無料')) return 'bg-green-100 text-green-700';
+ if (status.name.includes('有料')) return 'bg-yellow-100 text-yellow-700';
+ return 'bg-gray-100 text-gray-700';
+ }
+
+ // ========== マスター管理メソッド ==========
+
+ // 納品先の追加
+ async function addDestination() {
+ if (!newItemName.value.trim()) return;
+ try {
+ await API.createDestination({ name: newItemName.value.trim() });
+ newItemName.value = '';
+ await fetchMasterData();
+ showToast('納品先を追加しました', 'success');
+ } catch (error) {
+ console.error('Failed to add destination:', error);
+ showToast('追加に失敗しました', 'error');
+ }
+ }
+
+ // 納品先の更新
+ async function updateDestination(id, name) {
+ try {
+ await API.updateDestination(id, { name });
+ editingItem.value = null;
+ await fetchMasterData();
+ showToast('更新しました', 'success');
+ } catch (error) {
+ console.error('Failed to update destination:', error);
+ showToast('更新に失敗しました', 'error');
+ }
+ }
+
+ // 納品先の削除
+ async function deleteDestination(id) {
+ if (!confirm('この納品先を削除しますか?関連するプロジェクトの納品先は未設定になります。')) return;
+ try {
+ await API.deleteDestination(id);
+ await fetchMasterData();
+ await fetchProjects(); // プロジェクトも再取得
+ showToast('削除しました', 'success');
+ } catch (error) {
+ console.error('Failed to delete destination:', error);
+ showToast('削除に失敗しました', 'error');
+ }
+ }
+
+ // 音声変換エンジンの追加
+ async function addTtsEngine() {
+ if (!newItemName.value.trim()) return;
+ try {
+ await API.createTtsEngine({ name: newItemName.value.trim() });
+ newItemName.value = '';
+ await fetchMasterData();
+ showToast('音声変換エンジンを追加しました', 'success');
+ } catch (error) {
+ console.error('Failed to add TTS engine:', error);
+ showToast('追加に失敗しました', 'error');
+ }
+ }
+
+ // 音声変換エンジンの更新
+ async function updateTtsEngine(id, name) {
+ try {
+ await API.updateTtsEngine(id, { name });
+ editingItem.value = null;
+ await fetchMasterData();
+ showToast('更新しました', 'success');
+ } catch (error) {
+ console.error('Failed to update TTS engine:', error);
+ showToast('更新に失敗しました', 'error');
+ }
+ }
+
+ // 音声変換エンジンの削除
+ async function deleteTtsEngine(id) {
+ if (!confirm('この音声変換エンジンを削除しますか?関連するプロジェクトの設定は未設定になります。')) return;
+ try {
+ await API.deleteTtsEngine(id);
+ await fetchMasterData();
+ await fetchProjects(); // プロジェクトも再取得
+ showToast('削除しました', 'success');
+ } catch (error) {
+ console.error('Failed to delete TTS engine:', error);
+ showToast('削除に失敗しました', 'error');
+ }
+ }
+
+ // 公開状態の追加
+ async function addPublicationStatus() {
+ if (!newItemName.value.trim()) return;
+ try {
+ await API.createPublicationStatus({ name: newItemName.value.trim() });
+ newItemName.value = '';
+ await fetchMasterData();
+ showToast('追加しました', 'success');
+ } catch (error) {
+ console.error('Failed to create publication status:', error);
+ showToast('追加に失敗しました', 'error');
+ }
+ }
+
+ // 公開状態の更新
+ async function updatePublicationStatus(id, name) {
+ try {
+ await API.updatePublicationStatus(id, { name });
+ editingItem.value = null;
+ await fetchMasterData();
+ showToast('更新しました', 'success');
+ } catch (error) {
+ console.error('Failed to update publication status:', error);
+ showToast('更新に失敗しました', 'error');
+ }
+ }
+
+ // 公開状態の削除
+ async function deletePublicationStatus(id) {
+ if (!confirm('この公開状態を削除しますか?関連するプロジェクトの設定は未設定になります。')) return;
+ try {
+ await API.deletePublicationStatus(id);
+ await fetchMasterData();
+ await fetchProjects(); // プロジェクトも再取得
+ showToast('削除しました', 'success');
+ } catch (error) {
+ console.error('Failed to delete publication status:', error);
+ showToast('削除に失敗しました', 'error');
+ }
+ }
+
+ // 編集開始
+ function startEditing(item) {
+ editingItem.value = { ...item };
+ }
+
+ // 編集キャンセル
+ function cancelEditing() {
+ editingItem.value = null;
+ }
+
+ // 編集保存
+ function saveEditing() {
+ if (!editingItem.value || !editingItem.value.name.trim()) return;
+ if (settingsTab.value === 'destinations') {
+ updateDestination(editingItem.value.id, editingItem.value.name.trim());
+ } else if (settingsTab.value === 'tts-engines') {
+ updateTtsEngine(editingItem.value.id, editingItem.value.name.trim());
+ } else if (settingsTab.value === 'publication-statuses') {
+ updatePublicationStatus(editingItem.value.id, editingItem.value.name.trim());
+ }
+ }
+
// 統計を更新
function updateStats() {
const p = projects.value;
@@ -212,6 +535,17 @@ const app = createApp({
return topics.value.filter(t => t.status === filter).length;
}
+ // サブフォルダの展開/折りたたみ
+ function toggleFolder(folderKey) {
+ expandedFolders.value[folderKey] = !expandedFolders.value[folderKey];
+ }
+
+ // フォルダが展開されているか
+ function isFolderExpanded(folderKey) {
+ // デフォルトは展開状態
+ return expandedFolders.value[folderKey] !== false;
+ }
+
// スキャン実行
async function triggerScan() {
if (isScanning.value) return;
@@ -340,7 +674,10 @@ const app = createApp({
onMounted(async () => {
// 初期データ取得
- await fetchProjects();
+ await Promise.all([
+ fetchProjects(),
+ fetchMasterData()
+ ]);
isLoading.value = false;
// WebSocket接続
@@ -376,9 +713,26 @@ const app = createApp({
customRangeMin,
customRangeMax,
+ // 納品先・音声変換・公開状態フィルター
+ destinationFilter,
+ ttsEngineFilter,
+ publicationStatusFilter,
+
+ // マスターデータ
+ destinations,
+ ttsEngines,
+ publicationStatuses,
+
+ // 設定画面
+ settingsTab,
+ editingItem,
+ newItemName,
+
// 算出プロパティ
sortedProjects,
filteredTopics,
+ groupedTopics,
+ expandedFolders,
completedProjects,
incompleteProjects,
customFilteredProjects,
@@ -391,7 +745,30 @@ const app = createApp({
openFolder,
formatDateTime,
formatTime,
- getFilterCount
+ getFilterCount,
+ toggleFolder,
+ isFolderExpanded,
+
+ // プロジェクト設定
+ onDestinationChange,
+ onTtsEngineChange,
+ onPublicationStatusChange,
+ getPublicationStatusLabel,
+ getPublicationStatusClass,
+
+ // マスター管理
+ addDestination,
+ updateDestination,
+ deleteDestination,
+ addTtsEngine,
+ updateTtsEngine,
+ deleteTtsEngine,
+ addPublicationStatus,
+ updatePublicationStatus,
+ deletePublicationStatus,
+ startEditing,
+ cancelEditing,
+ saveEditing
};
}
});
diff --git a/project/launch_app.command b/project/launch_app.command
index 5196d6b..a3f6c3f 100755
--- a/project/launch_app.command
+++ b/project/launch_app.command
@@ -48,9 +48,9 @@ fi
# データディレクトリ・DB初期化
mkdir -p data
-if [ ! -f "data/progress.db" ]; then
+if [ ! -f "data/progress_tracker.db" ]; then
echo "🗄️ データベースを初期化中..."
- python3 -c "from backend.database import init_db; import asyncio; asyncio.run(init_db())"
+ python3 -c "from backend.database import get_database; import asyncio; asyncio.run(get_database())"
echo "✅ データベース初期化完了"
fi
diff --git a/project/pytest.ini b/project/pytest.ini
deleted file mode 100644
index 355645b..0000000
--- a/project/pytest.ini
+++ /dev/null
@@ -1,10 +0,0 @@
-[pytest]
-asyncio_mode = auto
-asyncio_default_fixture_loop_scope = function
-testpaths = tests
-python_files = test_*.py
-python_classes = Test*
-python_functions = test_*
-filterwarnings =
- ignore::DeprecationWarning
- ignore::PendingDeprecationWarning
diff --git a/project/tests/__init__.py b/project/tests/__init__.py
deleted file mode 100644
index d4839a6..0000000
--- a/project/tests/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Tests package
diff --git a/project/tests/__pycache__/__init__.cpython-312.pyc b/project/tests/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 19dbca4..0000000
Binary files a/project/tests/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/project/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc b/project/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc
deleted file mode 100644
index 67b9840..0000000
Binary files a/project/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc and /dev/null differ
diff --git a/project/tests/__pycache__/test_api.cpython-312-pytest-9.0.2.pyc b/project/tests/__pycache__/test_api.cpython-312-pytest-9.0.2.pyc
deleted file mode 100644
index 03ec7bb..0000000
Binary files a/project/tests/__pycache__/test_api.cpython-312-pytest-9.0.2.pyc and /dev/null differ
diff --git a/project/tests/__pycache__/test_database.cpython-312-pytest-9.0.2.pyc b/project/tests/__pycache__/test_database.cpython-312-pytest-9.0.2.pyc
deleted file mode 100644
index 4b641c7..0000000
Binary files a/project/tests/__pycache__/test_database.cpython-312-pytest-9.0.2.pyc and /dev/null differ
diff --git a/project/tests/__pycache__/test_integration.cpython-312-pytest-9.0.2.pyc b/project/tests/__pycache__/test_integration.cpython-312-pytest-9.0.2.pyc
deleted file mode 100644
index 8ede0ac..0000000
Binary files a/project/tests/__pycache__/test_integration.cpython-312-pytest-9.0.2.pyc and /dev/null differ
diff --git a/project/tests/__pycache__/test_main.cpython-312-pytest-9.0.2.pyc b/project/tests/__pycache__/test_main.cpython-312-pytest-9.0.2.pyc
deleted file mode 100644
index 4b73559..0000000
Binary files a/project/tests/__pycache__/test_main.cpython-312-pytest-9.0.2.pyc and /dev/null differ
diff --git a/project/tests/__pycache__/test_scanner.cpython-312-pytest-9.0.2.pyc b/project/tests/__pycache__/test_scanner.cpython-312-pytest-9.0.2.pyc
deleted file mode 100644
index 5e4f146..0000000
Binary files a/project/tests/__pycache__/test_scanner.cpython-312-pytest-9.0.2.pyc and /dev/null differ
diff --git a/project/tests/__pycache__/test_watcher.cpython-312-pytest-9.0.2.pyc b/project/tests/__pycache__/test_watcher.cpython-312-pytest-9.0.2.pyc
deleted file mode 100644
index 6e13aa6..0000000
Binary files a/project/tests/__pycache__/test_watcher.cpython-312-pytest-9.0.2.pyc and /dev/null differ
diff --git a/project/tests/__pycache__/test_wbs_parser.cpython-312-pytest-9.0.2.pyc b/project/tests/__pycache__/test_wbs_parser.cpython-312-pytest-9.0.2.pyc
deleted file mode 100644
index 74e157e..0000000
Binary files a/project/tests/__pycache__/test_wbs_parser.cpython-312-pytest-9.0.2.pyc and /dev/null differ
diff --git a/project/tests/__pycache__/test_websocket.cpython-312-pytest-9.0.2.pyc b/project/tests/__pycache__/test_websocket.cpython-312-pytest-9.0.2.pyc
deleted file mode 100644
index 03e7b8e..0000000
Binary files a/project/tests/__pycache__/test_websocket.cpython-312-pytest-9.0.2.pyc and /dev/null differ
diff --git a/project/tests/conftest.py b/project/tests/conftest.py
deleted file mode 100644
index d6099c6..0000000
--- a/project/tests/conftest.py
+++ /dev/null
@@ -1,318 +0,0 @@
-"""
-pytest共通設定・フィクスチャ
-"""
-
-import pytest
-import asyncio
-import tempfile
-import json
-from pathlib import Path
-from typing import Dict, Any
-from dataclasses import dataclass
-import sys
-
-# プロジェクトルートをパスに追加
-sys.path.insert(0, str(Path(__file__).parent.parent))
-
-from backend.database import Database
-
-
-# ========== pytest-asyncio設定 ==========
-
-pytest_plugins = ('pytest_asyncio',)
-
-
-def pytest_configure(config):
- """pytest設定"""
- config.addinivalue_line(
- "markers", "asyncio: mark test as async"
- )
-
-
-# ========== イベントループフィクスチャ ==========
-
-@pytest.fixture(scope="session")
-def event_loop_policy():
- return asyncio.DefaultEventLoopPolicy()
-
-
-# ========== データベースフィクスチャ ==========
-
-@pytest.fixture
-def db_sync(tmp_path):
- """同期テスト用データベースファクトリ"""
- async def _create_db():
- db_path = tmp_path / "test.db"
- database = Database(db_path)
- await database.connect()
- await database.init_tables()
- return database
-
- async def _close_db(database):
- await database.disconnect()
-
- return _create_db, _close_db
-
-
-@pytest.fixture
-async def db(tmp_path):
- """テスト用インメモリデータベース"""
- db_path = tmp_path / "test.db"
- database = Database(db_path)
- await database.connect()
- await database.init_tables()
- yield database
- await database.disconnect()
-
-
-@pytest.fixture
-async def db_with_data(tmp_path):
- """サンプルデータ入りデータベース"""
- db_path = tmp_path / "test_with_data.db"
- database = Database(db_path)
- await database.connect()
- await database.init_tables()
-
- # プロジェクト追加
- project_id = await database.upsert_project(
- name="テストプロジェクト",
- path="/test/path",
- wbs_format="object"
- )
-
- # トピック追加
- await database.upsert_topic(
- project_id=project_id,
- base_name="01-01_トピック1",
- topic_id="01-01",
- chapter="Chapter 1",
- title="トピック1",
- has_html=True,
- has_txt=True,
- has_mp3=True
- )
-
- await database.upsert_topic(
- project_id=project_id,
- base_name="01-02_トピック2",
- topic_id="01-02",
- chapter="Chapter 1",
- title="トピック2",
- has_html=True,
- has_txt=True,
- has_mp3=False
- )
-
- await database.upsert_topic(
- project_id=project_id,
- base_name="01-03_トピック3",
- topic_id="01-03",
- chapter="Chapter 1",
- title="トピック3",
- has_html=True,
- has_txt=False,
- has_mp3=False
- )
-
- # 統計更新
- await database.update_project_stats(
- project_id=project_id,
- total_topics=3,
- completed_topics=1,
- html_count=3,
- txt_count=2,
- mp3_count=1
- )
-
- yield database, project_id
- await database.disconnect()
-
-
-# ========== WBSモックデータ ==========
-
-@pytest.fixture
-def mock_wbs_object() -> Dict[str, Any]:
- """オブジェクト型WBSモックデータ"""
- return {
- "project": {"name": "テストプロジェクト(オブジェクト型)"},
- "phases": {
- "phase_2": {
- "name": "Phase 2: コンテンツ作成",
- "chapters": {
- "chapter_1": {
- "name": "Chapter 1: 基礎",
- "topics": [
- {
- "id": "topic_01_01",
- "title": "トピック1-1",
- "base_name": "01-01_トピック1-1"
- },
- {
- "id": "topic_01_02",
- "title": "トピック1-2",
- "base_name": "01-02_トピック1-2"
- },
- {
- "id": "topic_01_03",
- "title": "トピック1-3",
- "base_name": "01-03_トピック1-3"
- }
- ]
- },
- "chapter_2": {
- "name": "Chapter 2: 応用",
- "topics": [
- {
- "id": "topic_02_01",
- "title": "トピック2-1",
- "base_name": "02-01_トピック2-1"
- },
- {
- "id": "topic_02_02",
- "title": "トピック2-2",
- "base_name": "02-02_トピック2-2"
- }
- ]
- }
- }
- }
- }
- }
-
-
-@pytest.fixture
-def mock_wbs_array() -> Dict[str, Any]:
- """配列型WBSモックデータ"""
- return {
- "project": {"name": "テストプロジェクト(配列型)"},
- "phases": [
- {
- "id": "phase2",
- "name": "Phase 2: コンテンツ作成",
- "parts": [
- {
- "id": "part1",
- "chapters": [
- {"id": "ch1", "name": "Chapter 1: 基礎", "topics": 3},
- {"id": "ch2", "name": "Chapter 2: 応用", "topics": 2}
- ]
- }
- ]
- }
- ]
- }
-
-
-@pytest.fixture
-def mock_wbs_empty() -> Dict[str, Any]:
- """空のWBSデータ"""
- return {"phases": {}}
-
-
-@pytest.fixture
-def mock_wbs_invalid() -> Dict[str, Any]:
- """不正なWBSデータ"""
- return {"phases": "invalid"}
-
-
-# ========== ファイルシステムフィクスチャ ==========
-
-@pytest.fixture
-def content_dir(tmp_path):
- """テスト用コンテンツディレクトリ"""
- content = tmp_path / "content"
- content.mkdir()
- return content
-
-
-@pytest.fixture
-def content_dir_with_files(tmp_path):
- """ファイル入りコンテンツディレクトリ"""
- content_dir = tmp_path / "content_with_files"
- content_dir.mkdir()
-
- # 完了トピック(HTML + TXT + MP3)
- (content_dir / "01-01_トピック1.html").write_text("Content 1")
- (content_dir / "01-01_トピック1.txt").write_text("Text content 1")
- (content_dir / "01-01_トピック1.mp3").write_bytes(b'\x00\x01\x02')
-
- # 進行中トピック(HTML + TXT)
- (content_dir / "01-02_トピック2.html").write_text("Content 2")
- (content_dir / "01-02_トピック2.txt").write_text("Text content 2")
-
- # HTMLのみトピック
- (content_dir / "01-03_トピック3.html").write_text("Content 3")
-
- return content_dir
-
-
-@pytest.fixture
-def project_dir(tmp_path, mock_wbs_object):
- """テスト用プロジェクトディレクトリ"""
- project = tmp_path / "test_project"
- project.mkdir()
-
- # WBS.json作成
- wbs_path = project / "WBS.json"
- wbs_path.write_text(json.dumps(mock_wbs_object, ensure_ascii=False))
-
- # contentディレクトリ作成
- content_path = project / "content"
- content_path.mkdir()
-
- # 完了トピック(HTML + TXT + MP3)
- (content_path / "01-01_トピック1.html").write_text("Content 1")
- (content_path / "01-01_トピック1.txt").write_text("Text content 1")
- (content_path / "01-01_トピック1.mp3").write_bytes(b'\x00\x01\x02')
-
- # 進行中トピック(HTML + TXT)
- (content_path / "01-02_トピック2.html").write_text("Content 2")
- (content_path / "01-02_トピック2.txt").write_text("Text content 2")
-
- # HTMLのみトピック
- (content_path / "01-03_トピック3.html").write_text("Content 3")
-
- return project
-
-
-# ========== モッククラス ==========
-
-@dataclass
-class MockTopic:
- """モックトピッククラス"""
- has_html: bool = False
- has_txt: bool = False
- has_mp3: bool = False
- base_name: str = "01-01_test"
- topic_id: str = "01-01"
- chapter: str = "Chapter 1"
- title: str = "Test Topic"
-
-
-@pytest.fixture
-def mock_topic():
- """モックトピックファクトリ"""
- return MockTopic
-
-
-# ========== HTTPクライアントフィクスチャ ==========
-
-@pytest.fixture
-def test_client(db):
- """FastAPI TestClient"""
- from fastapi.testclient import TestClient
- from backend.main import app
-
- # データベースを上書き
- async def override_get_database():
- return db
-
- from backend import api
- from backend.database import get_database
-
- app.dependency_overrides[get_database] = override_get_database
-
- with TestClient(app) as client:
- yield client
-
- app.dependency_overrides.clear()
diff --git a/project/tests/test_api.py b/project/tests/test_api.py
deleted file mode 100644
index 397ba2a..0000000
--- a/project/tests/test_api.py
+++ /dev/null
@@ -1,458 +0,0 @@
-"""
-APIテスト
-テスト対象: backend/api.py
-"""
-
-import pytest
-import asyncio
-from unittest.mock import AsyncMock, patch, MagicMock
-from fastapi.testclient import TestClient
-from fastapi import FastAPI
-import json
-
-# テスト用のシンプルなアプリを作成
-from backend.api import router
-from backend.database import Database
-
-
-# テスト用FastAPIアプリ
-test_app = FastAPI()
-test_app.include_router(router)
-
-
-@pytest.fixture
-def client():
- """TestClient フィクスチャ"""
- return TestClient(test_app)
-
-
-@pytest.fixture
-async def mock_db():
- """モックデータベース"""
- db = AsyncMock(spec=Database)
-
- # デフォルトの戻り値を設定
- db.get_all_projects = AsyncMock(return_value=[])
- db.get_project = AsyncMock(return_value=None)
- db.get_topics_by_project = AsyncMock(return_value=[])
- db.get_stats = AsyncMock(return_value={
- "total_projects": 0,
- "total_topics": 0,
- "completed_topics": 0,
- "overall_progress": 0.0,
- "html_total": 0,
- "txt_total": 0,
- "mp3_total": 0
- })
-
- return db
-
-
-class TestProjectsAPI:
- """プロジェクトAPI テスト"""
-
- def test_API001_get_projects_empty(self, client):
- """API-001: プロジェクト一覧取得(空)"""
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_all_projects = AsyncMock(return_value=[])
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects")
-
- assert response.status_code == 200
- data = response.json()
- assert "projects" in data
- assert "total" in data
- assert isinstance(data["projects"], list)
- assert data["total"] == 0
-
- def test_API001_get_projects_with_data(self, client):
- """API-001: プロジェクト一覧取得(データあり)"""
- mock_projects = [
- {
- "id": 1,
- "name": "Project A",
- "path": "/path/a",
- "total_topics": 10,
- "completed_topics": 5,
- "html_count": 8,
- "txt_count": 6,
- "mp3_count": 4,
- "last_scanned_at": "2024-01-01T00:00:00"
- },
- {
- "id": 2,
- "name": "Project B",
- "path": "/path/b",
- "total_topics": 20,
- "completed_topics": 10,
- "html_count": 15,
- "txt_count": 12,
- "mp3_count": 8,
- "last_scanned_at": "2024-01-02T00:00:00"
- }
- ]
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_all_projects = AsyncMock(return_value=mock_projects)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects")
-
- assert response.status_code == 200
- data = response.json()
- assert len(data["projects"]) == 2
- assert data["total"] == 2
- assert data["projects"][0]["name"] == "Project A"
-
- def test_API002_get_project_detail(self, client):
- """API-002: プロジェクト詳細取得"""
- mock_project = {
- "id": 1,
- "name": "Test Project",
- "path": "/test",
- "total_topics": 10,
- "completed_topics": 5,
- "html_count": 8,
- "txt_count": 6,
- "mp3_count": 4
- }
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_project = AsyncMock(return_value=mock_project)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects/1")
-
- assert response.status_code == 200
- data = response.json()
- assert data["id"] == 1
- assert data["name"] == "Test Project"
- assert "progress" in data
-
- def test_API003_get_nonexistent_project(self, client):
- """API-003: 存在しないプロジェクト"""
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_project = AsyncMock(return_value=None)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects/999")
-
- assert response.status_code == 404
-
- def test_API004_get_topics(self, client):
- """API-004: トピック一覧取得"""
- mock_project = {
- "id": 1,
- "name": "Test Project",
- "path": "/test"
- }
- mock_topics = [
- {"id": 1, "base_name": "01-01_test", "has_html": 1, "has_txt": 1, "has_mp3": 1},
- {"id": 2, "base_name": "01-02_test", "has_html": 1, "has_txt": 1, "has_mp3": 0},
- {"id": 3, "base_name": "01-03_test", "has_html": 1, "has_txt": 0, "has_mp3": 0},
- {"id": 4, "base_name": "01-04_test", "has_html": 0, "has_txt": 0, "has_mp3": 0},
- ]
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_project = AsyncMock(return_value=mock_project)
- mock_db.get_topics_by_project = AsyncMock(return_value=mock_topics)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects/1/topics")
-
- assert response.status_code == 200
- data = response.json()
- assert "topics" in data
- assert "summary" in data
- assert len(data["topics"]) == 4
- assert data["summary"]["total"] == 4
- assert data["summary"]["completed"] == 1
- assert data["summary"]["in_progress"] == 2
- assert data["summary"]["not_started"] == 1
-
- def test_API004_get_topics_nonexistent_project(self, client):
- """API-004: 存在しないプロジェクトのトピック"""
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_project = AsyncMock(return_value=None)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects/999/topics")
-
- assert response.status_code == 404
-
-
-class TestScanAPI:
- """スキャンAPI テスト"""
-
- def test_API006_trigger_scan(self, client):
- """API-006: 手動スキャン実行"""
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.create_scan_history = AsyncMock(return_value=1)
- mock_get_db.return_value = mock_db
-
- response = client.post(
- "/api/scan",
- json={"scan_type": "full"}
- )
-
- assert response.status_code == 200
- data = response.json()
- assert data["status"] == "accepted"
- assert "scan_id" in data
-
- def test_trigger_scan_diff(self, client):
- """差分スキャン実行"""
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.create_scan_history = AsyncMock(return_value=1)
- mock_get_db.return_value = mock_db
-
- response = client.post(
- "/api/scan",
- json={"scan_type": "diff", "project_id": 1}
- )
-
- assert response.status_code == 200
- data = response.json()
- assert data["status"] == "accepted"
-
-
-class TestStatsAPI:
- """統計API テスト"""
-
- def test_API005_get_stats(self, client):
- """API-005: 全体統計取得"""
- mock_stats = {
- "total_projects": 5,
- "total_topics": 100,
- "completed_topics": 50,
- "overall_progress": 65.5,
- "html_total": 80,
- "txt_total": 60,
- "mp3_total": 40
- }
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_stats = AsyncMock(return_value=mock_stats)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/stats")
-
- assert response.status_code == 200
- data = response.json()
- assert data["total_projects"] == 5
- assert data["total_topics"] == 100
- assert data["overall_progress"] == 65.5
-
-
-class TestHealthAPI:
- """ヘルスチェックAPI テスト"""
-
- def test_health_check(self, client):
- """ヘルスチェック"""
- response = client.get("/api/health")
-
- assert response.status_code == 200
- data = response.json()
- assert data["status"] == "healthy"
- assert "timestamp" in data
-
-
-class TestResponseSchema:
- """レスポンススキーマ テスト"""
-
- def test_API008_projects_response_schema(self, client):
- """API-008: プロジェクトレスポンス形式検証"""
- mock_projects = [
- {
- "id": 1,
- "name": "Test Project",
- "path": "/test",
- "total_topics": 10,
- "completed_topics": 5,
- "html_count": 8,
- "txt_count": 6,
- "mp3_count": 4,
- "last_scanned_at": "2024-01-01T00:00:00"
- }
- ]
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_all_projects = AsyncMock(return_value=mock_projects)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects")
-
- assert response.status_code == 200
- data = response.json()
-
- # 必須フィールド検証
- for project in data["projects"]:
- assert "id" in project
- assert "name" in project
- assert "total_topics" in project
- assert "progress" in project
- assert "progress_detail" in project
- assert 0 <= project["progress"] <= 100
- assert "html" in project["progress_detail"]
- assert "txt" in project["progress_detail"]
- assert "mp3" in project["progress_detail"]
-
-
-class TestProgressCalculation:
- """進捗計算テスト"""
-
- def test_progress_calculation_full(self, client):
- """全ファイル完了時の進捗率"""
- mock_projects = [
- {
- "id": 1,
- "name": "Complete Project",
- "path": "/test",
- "total_topics": 10,
- "completed_topics": 10,
- "html_count": 10,
- "txt_count": 10,
- "mp3_count": 10,
- "last_scanned_at": None
- }
- ]
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_all_projects = AsyncMock(return_value=mock_projects)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects")
-
- data = response.json()
- project = data["projects"][0]
- assert project["progress"] == 100.0
- assert project["progress_detail"]["html"] == 100.0
- assert project["progress_detail"]["txt"] == 100.0
- assert project["progress_detail"]["mp3"] == 100.0
-
- def test_progress_calculation_partial(self, client):
- """部分完了時の進捗率"""
- mock_projects = [
- {
- "id": 1,
- "name": "Partial Project",
- "path": "/test",
- "total_topics": 10,
- "completed_topics": 0,
- "html_count": 10, # 100% HTML
- "txt_count": 5, # 50% TXT
- "mp3_count": 0, # 0% MP3
- "last_scanned_at": None
- }
- ]
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_all_projects = AsyncMock(return_value=mock_projects)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects")
-
- data = response.json()
- project = data["projects"][0]
-
- # 重み付け進捗: (10*0.4 + 5*0.3 + 0*0.3) / 10 * 100 = 55%
- assert project["progress"] == pytest.approx(55.0, 0.1)
-
- def test_progress_calculation_zero_topics(self, client):
- """トピックなしの進捗率"""
- mock_projects = [
- {
- "id": 1,
- "name": "Empty Project",
- "path": "/test",
- "total_topics": 0,
- "completed_topics": 0,
- "html_count": 0,
- "txt_count": 0,
- "mp3_count": 0,
- "last_scanned_at": None
- }
- ]
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_all_projects = AsyncMock(return_value=mock_projects)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects")
-
- data = response.json()
- project = data["projects"][0]
- assert project["progress"] == 0
- assert project["progress_detail"] == {"html": 0, "txt": 0, "mp3": 0}
-
-
-class TestTopicStatus:
- """トピックステータステスト"""
-
- def test_topic_status_completed(self, client):
- """完了ステータス"""
- mock_project = {"id": 1, "name": "Test", "path": "/test"}
- mock_topics = [
- {"id": 1, "base_name": "test", "has_html": 1, "has_txt": 1, "has_mp3": 1}
- ]
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_project = AsyncMock(return_value=mock_project)
- mock_db.get_topics_by_project = AsyncMock(return_value=mock_topics)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects/1/topics")
-
- data = response.json()
- assert data["topics"][0]["status"] == "completed"
-
- def test_topic_status_in_progress(self, client):
- """進行中ステータス"""
- mock_project = {"id": 1, "name": "Test", "path": "/test"}
- mock_topics = [
- {"id": 1, "base_name": "test", "has_html": 1, "has_txt": 1, "has_mp3": 0}
- ]
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_project = AsyncMock(return_value=mock_project)
- mock_db.get_topics_by_project = AsyncMock(return_value=mock_topics)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects/1/topics")
-
- data = response.json()
- assert data["topics"][0]["status"] == "in_progress"
-
- def test_topic_status_not_started(self, client):
- """未着手ステータス"""
- mock_project = {"id": 1, "name": "Test", "path": "/test"}
- mock_topics = [
- {"id": 1, "base_name": "test", "has_html": 0, "has_txt": 0, "has_mp3": 0}
- ]
-
- with patch('backend.api.get_database') as mock_get_db:
- mock_db = AsyncMock()
- mock_db.get_project = AsyncMock(return_value=mock_project)
- mock_db.get_topics_by_project = AsyncMock(return_value=mock_topics)
- mock_get_db.return_value = mock_db
-
- response = client.get("/api/projects/1/topics")
-
- data = response.json()
- assert data["topics"][0]["status"] == "not_started"
diff --git a/project/tests/test_database.py b/project/tests/test_database.py
deleted file mode 100644
index 4b17a39..0000000
--- a/project/tests/test_database.py
+++ /dev/null
@@ -1,409 +0,0 @@
-"""
-データベーステスト
-テスト対象: backend/database.py
-"""
-
-import pytest
-import asyncio
-from pathlib import Path
-
-from backend.database import Database
-
-
-class TestDatabaseConnection:
- """データベース接続テスト"""
-
- @pytest.mark.asyncio
- async def test_connect_creates_directory(self, tmp_path):
- """接続時にディレクトリを作成"""
- db_path = tmp_path / "subdir" / "test.db"
- db = Database(db_path)
-
- await db.connect()
-
- assert db_path.parent.exists()
-
- await db.disconnect()
-
- @pytest.mark.asyncio
- async def test_connect_and_disconnect(self, tmp_path):
- """接続と切断"""
- db_path = tmp_path / "test.db"
- db = Database(db_path)
-
- await db.connect()
- assert db._connection is not None
-
- await db.disconnect()
- assert db._connection is None
-
- @pytest.mark.asyncio
- async def test_init_tables(self, db):
- """テーブル初期化"""
- # テーブルが作成されていることを確認
- cursor = await db._connection.execute(
- "SELECT name FROM sqlite_master WHERE type='table'"
- )
- tables = [row[0] for row in await cursor.fetchall()]
-
- assert "projects" in tables
- assert "topics" in tables
- assert "scan_history" in tables
-
-
-class TestProjectCRUD:
- """プロジェクトCRUD操作テスト"""
-
- @pytest.mark.asyncio
- async def test_upsert_project_insert(self, db):
- """プロジェクト挿入"""
- project_id = await db.upsert_project(
- name="テストプロジェクト",
- path="/test/path",
- wbs_format="object"
- )
-
- assert project_id > 0
-
- project = await db.get_project(project_id)
- assert project["name"] == "テストプロジェクト"
- assert project["path"] == "/test/path"
- assert project["wbs_format"] == "object"
-
- @pytest.mark.asyncio
- async def test_upsert_project_update(self, db):
- """プロジェクト更新(UPSERT)"""
- # 初回挿入
- project_id1 = await db.upsert_project(
- name="テストプロジェクト",
- path="/test/path",
- wbs_format="object"
- )
-
- # 同名で更新
- project_id2 = await db.upsert_project(
- name="テストプロジェクト",
- path="/new/path",
- wbs_format="array"
- )
-
- assert project_id1 == project_id2
-
- project = await db.get_project(project_id1)
- assert project["path"] == "/new/path"
- assert project["wbs_format"] == "array"
-
- @pytest.mark.asyncio
- async def test_get_all_projects(self, db):
- """全プロジェクト取得"""
- await db.upsert_project(name="Project A", path="/a", wbs_format="object")
- await db.upsert_project(name="Project B", path="/b", wbs_format="array")
-
- projects = await db.get_all_projects()
-
- assert len(projects) == 2
- names = [p["name"] for p in projects]
- assert "Project A" in names
- assert "Project B" in names
-
- @pytest.mark.asyncio
- async def test_get_project_not_found(self, db):
- """存在しないプロジェクト取得"""
- project = await db.get_project(999)
- assert project is None
-
- @pytest.mark.asyncio
- async def test_get_project_by_name(self, db):
- """プロジェクト名で取得"""
- await db.upsert_project(name="テストプロジェクト", path="/test", wbs_format="object")
-
- project = await db.get_project_by_name("テストプロジェクト")
- assert project is not None
- assert project["name"] == "テストプロジェクト"
-
- @pytest.mark.asyncio
- async def test_get_project_by_name_not_found(self, db):
- """存在しない名前で取得"""
- project = await db.get_project_by_name("存在しないプロジェクト")
- assert project is None
-
- @pytest.mark.asyncio
- async def test_update_project_stats(self, db):
- """プロジェクト統計更新"""
- project_id = await db.upsert_project(name="Test", path="/test", wbs_format="object")
-
- await db.update_project_stats(
- project_id=project_id,
- total_topics=100,
- completed_topics=50,
- html_count=80,
- txt_count=60,
- mp3_count=40
- )
-
- project = await db.get_project(project_id)
- assert project["total_topics"] == 100
- assert project["completed_topics"] == 50
- assert project["html_count"] == 80
- assert project["txt_count"] == 60
- assert project["mp3_count"] == 40
-
-
-class TestTopicCRUD:
- """トピックCRUD操作テスト"""
-
- @pytest.mark.asyncio
- async def test_upsert_topic_insert(self, db):
- """トピック挿入"""
- project_id = await db.upsert_project(name="Test", path="/test", wbs_format="object")
-
- topic_id = await db.upsert_topic(
- project_id=project_id,
- base_name="01-01_test",
- topic_id="01-01",
- chapter="Chapter 1",
- title="Test Topic",
- has_html=True,
- has_txt=False,
- has_mp3=False
- )
-
- assert topic_id > 0
-
- topics = await db.get_topics_by_project(project_id)
- assert len(topics) == 1
- assert topics[0]["base_name"] == "01-01_test"
- assert topics[0]["has_html"] == 1
-
- @pytest.mark.asyncio
- async def test_upsert_topic_update(self, db):
- """トピック更新(UPSERT)"""
- project_id = await db.upsert_project(name="Test", path="/test", wbs_format="object")
-
- # 初回挿入
- await db.upsert_topic(
- project_id=project_id,
- base_name="01-01_test",
- has_html=False,
- has_txt=False,
- has_mp3=False
- )
-
- # 更新
- await db.upsert_topic(
- project_id=project_id,
- base_name="01-01_test",
- has_html=True,
- has_txt=True,
- has_mp3=False
- )
-
- topics = await db.get_topics_by_project(project_id)
- assert len(topics) == 1
- assert topics[0]["has_html"] == 1
- assert topics[0]["has_txt"] == 1
-
- @pytest.mark.asyncio
- async def test_get_topics_by_project(self, db):
- """プロジェクトのトピック一覧取得"""
- project_id = await db.upsert_project(name="Test", path="/test", wbs_format="object")
-
- await db.upsert_topic(project_id=project_id, base_name="01-01_a")
- await db.upsert_topic(project_id=project_id, base_name="01-02_b")
- await db.upsert_topic(project_id=project_id, base_name="01-03_c")
-
- topics = await db.get_topics_by_project(project_id)
-
- assert len(topics) == 3
- # ソート順を確認
- assert topics[0]["base_name"] == "01-01_a"
- assert topics[1]["base_name"] == "01-02_b"
- assert topics[2]["base_name"] == "01-03_c"
-
- @pytest.mark.asyncio
- async def test_get_topic_by_base_name(self, db):
- """base_nameでトピック取得"""
- project_id = await db.upsert_project(name="Test", path="/test", wbs_format="object")
-
- await db.upsert_topic(
- project_id=project_id,
- base_name="01-01_test",
- title="Test Topic"
- )
-
- topic = await db.get_topic_by_base_name(project_id, "01-01_test")
-
- assert topic is not None
- assert topic["title"] == "Test Topic"
-
- @pytest.mark.asyncio
- async def test_delete_topics_by_project(self, db):
- """プロジェクトのトピック全削除"""
- project_id = await db.upsert_project(name="Test", path="/test", wbs_format="object")
-
- await db.upsert_topic(project_id=project_id, base_name="01-01_a")
- await db.upsert_topic(project_id=project_id, base_name="01-02_b")
-
- deleted_count = await db.delete_topics_by_project(project_id)
-
- assert deleted_count == 2
-
- topics = await db.get_topics_by_project(project_id)
- assert len(topics) == 0
-
-
-class TestScanHistory:
- """スキャン履歴テスト"""
-
- @pytest.mark.asyncio
- async def test_create_scan_history(self, db):
- """スキャン履歴作成"""
- scan_db_id = await db.create_scan_history(
- scan_id="scan_123",
- scan_type="full",
- project_id=None
- )
-
- assert scan_db_id > 0
-
- @pytest.mark.asyncio
- async def test_update_scan_history(self, db):
- """スキャン履歴更新"""
- await db.create_scan_history(
- scan_id="scan_123",
- scan_type="full"
- )
-
- await db.update_scan_history(
- scan_id="scan_123",
- status="completed",
- projects_scanned=5,
- files_scanned=100,
- changes_detected=10
- )
-
- # 更新されていることを確認
- cursor = await db._connection.execute(
- "SELECT * FROM scan_history WHERE scan_id = ?",
- ("scan_123",)
- )
- row = await cursor.fetchone()
-
- assert row["status"] == "completed"
- assert row["projects_scanned"] == 5
- assert row["files_scanned"] == 100
- assert row["changes_detected"] == 10
-
- @pytest.mark.asyncio
- async def test_scan_history_with_error(self, db):
- """エラー付きスキャン履歴"""
- await db.create_scan_history(scan_id="scan_error", scan_type="full")
-
- await db.update_scan_history(
- scan_id="scan_error",
- status="failed",
- error_message="Something went wrong"
- )
-
- cursor = await db._connection.execute(
- "SELECT * FROM scan_history WHERE scan_id = ?",
- ("scan_error",)
- )
- row = await cursor.fetchone()
-
- assert row["status"] == "failed"
- assert row["error_message"] == "Something went wrong"
-
-
-class TestStatistics:
- """統計テスト"""
-
- @pytest.mark.asyncio
- async def test_get_stats_empty(self, db):
- """空のデータベースの統計"""
- stats = await db.get_stats()
-
- assert stats["total_projects"] == 0
- assert stats["total_topics"] == 0
- assert stats["overall_progress"] == 0.0
-
- @pytest.mark.asyncio
- async def test_get_stats_with_data(self, db_with_data):
- """データありの統計"""
- db, project_id = db_with_data
-
- stats = await db.get_stats()
-
- assert stats["total_projects"] == 1
- assert stats["total_topics"] == 3
- assert stats["html_total"] == 3
- assert stats["txt_total"] == 2
- assert stats["mp3_total"] == 1
- # 重み付け進捗: (3*0.4 + 2*0.3 + 1*0.3) / 3 * 100 = 70%
- assert stats["overall_progress"] == pytest.approx(70.0, 0.1)
-
-
-class TestTransaction:
- """トランザクションテスト"""
-
- @pytest.mark.asyncio
- async def test_transaction_commit(self, db):
- """トランザクションコミット"""
- async with db.transaction():
- await db._connection.execute(
- "INSERT INTO projects (name, path) VALUES (?, ?)",
- ("Transaction Test", "/test")
- )
-
- # コミットされていることを確認
- cursor = await db._connection.execute(
- "SELECT * FROM projects WHERE name = ?",
- ("Transaction Test",)
- )
- row = await cursor.fetchone()
- assert row is not None
-
- @pytest.mark.asyncio
- async def test_transaction_rollback(self, db):
- """トランザクションロールバック"""
- try:
- async with db.transaction():
- await db._connection.execute(
- "INSERT INTO projects (name, path) VALUES (?, ?)",
- ("Rollback Test", "/test")
- )
- raise Exception("Force rollback")
- except Exception:
- pass
-
- # ロールバックされていることを確認
- cursor = await db._connection.execute(
- "SELECT * FROM projects WHERE name = ?",
- ("Rollback Test",)
- )
- row = await cursor.fetchone()
- assert row is None
-
-
-class TestIndexes:
- """インデックステスト"""
-
- @pytest.mark.asyncio
- async def test_indexes_created(self, db):
- """インデックスが作成されていることを確認"""
- cursor = await db._connection.execute(
- "SELECT name FROM sqlite_master WHERE type='index'"
- )
- indexes = [row[0] for row in await cursor.fetchall()]
-
- expected_indexes = [
- "idx_projects_name",
- "idx_projects_updated",
- "idx_topics_project",
- "idx_topics_base_name",
- "idx_topics_chapter",
- "idx_scan_history_started"
- ]
-
- for idx_name in expected_indexes:
- assert idx_name in indexes, f"Index {idx_name} not found"
diff --git a/project/tests/test_integration.py b/project/tests/test_integration.py
deleted file mode 100644
index d7f8c4d..0000000
--- a/project/tests/test_integration.py
+++ /dev/null
@@ -1,108 +0,0 @@
-"""
-統合テスト
-スキャン→DB保存→API取得、WebSocket通知など複数コンポーネント連携のテスト
-"""
-
-import pytest
-import json
-from pathlib import Path
-from unittest.mock import AsyncMock
-
-
-# ========== スキャン→DB フローテスト ==========
-
-class TestScanToDatabaseFlow:
- """スキャンからDB保存までのテスト"""
-
- @pytest.fixture
- def simple_project(self, tmp_path):
- """シンプルなテスト用プロジェクト"""
- project_dir = tmp_path / "SimpleProject"
- project_dir.mkdir()
-
- wbs_data = {
- "project": {"name": "SimpleProject"},
- "phases": {
- "phase_2": {
- "chapters": {
- "ch1": {
- "name": "Chapter 1",
- "topics": [
- {"id": "01-01", "title": "Topic1", "base_name": "01-01_Topic1"}
- ]
- }
- }
- }
- }
- }
- (project_dir / "WBS.json").write_text(json.dumps(wbs_data))
-
- content_dir = project_dir / "content"
- content_dir.mkdir()
- (content_dir / "01-01_Topic1.html").write_text("")
- (content_dir / "01-01_Topic1.txt").write_text("text")
- (content_dir / "01-01_Topic1.mp3").write_bytes(b'\x00')
-
- return project_dir
-
- @pytest.mark.asyncio
- async def test_INT001_scan_stores_to_database(self, simple_project, db):
- """INT001: スキャン結果がデータベースに保存される"""
- from backend.scanner import AsyncScanner
-
- scanner = AsyncScanner(db, simple_project.parent)
- result = await scanner.scan_project(simple_project)
-
- assert result is not None
- assert result.project_name == "SimpleProject"
- assert result.total_topics == 1
- assert result.completed_topics == 1
-
- @pytest.mark.asyncio
- async def test_INT002_database_query(self, simple_project, db):
- """INT002: データベースからデータを取得できる"""
- from backend.scanner import AsyncScanner
-
- scanner = AsyncScanner(db, simple_project.parent)
- await scanner.scan_project(simple_project)
-
- project = await db.get_project_by_name("SimpleProject")
- assert project is not None
- assert project["total_topics"] == 1
-
-
-class TestWebSocketFlow:
- """WebSocket通知フローのテスト"""
-
- @pytest.mark.asyncio
- async def test_INT003_broadcast_message(self):
- """INT003: ブロードキャストメッセージが送信される"""
- from backend.websocket import ConnectionManager
-
- manager = ConnectionManager()
- mock_ws = AsyncMock()
- mock_ws.accept = AsyncMock()
- mock_ws.send_text = AsyncMock()
-
- await manager.connect(mock_ws)
- await manager.broadcast("test", {"data": "value"})
-
- mock_ws.send_text.assert_called_once()
- await manager.disconnect(mock_ws)
-
-
-class TestStatsFlow:
- """統計集計フローのテスト"""
-
- @pytest.mark.asyncio
- async def test_INT004_stats_aggregation(self, db):
- """INT004: 統計が正しく集計される"""
- # プロジェクト追加
- await db.upsert_project(
- name="StatsProject",
- path="/test",
- wbs_format="object"
- )
-
- stats = await db.get_stats()
- assert stats["total_projects"] >= 1
diff --git a/project/tests/test_main.py b/project/tests/test_main.py
deleted file mode 100644
index 8b3c37e..0000000
--- a/project/tests/test_main.py
+++ /dev/null
@@ -1,297 +0,0 @@
-"""
-main.py のユニットテスト
-FastAPIアプリケーションの起動、静的ファイル配信、CORS設定、WebSocketエンドポイントをテスト
-"""
-
-import pytest
-import asyncio
-from pathlib import Path
-from unittest.mock import AsyncMock, MagicMock, patch
-from fastapi.testclient import TestClient
-
-
-# ========== アプリケーション基本テスト ==========
-
-class TestAppConfiguration:
- """アプリケーション設定のテスト"""
-
- def test_MAIN001_app_title(self):
- """MAIN001: アプリケーションタイトルが正しく設定されている"""
- from backend.main import app
- assert app.title == "研修コンテンツ進捗トラッカー"
-
- def test_MAIN002_app_version(self):
- """MAIN002: アプリケーションバージョンが正しく設定されている"""
- from backend.main import app
- assert app.version == "1.0.0"
-
- def test_MAIN003_cors_middleware_added(self):
- """MAIN003: CORSミドルウェアが追加されている"""
- from backend.main import app
- middleware_types = [type(m).__name__ for m in app.user_middleware]
- # Note: FastAPIはミドルウェアを内部的に管理するため、直接確認は難しい
- # 代わりにapp.add_middlewareが呼ばれていることを設定から確認
- assert app is not None # アプリが正常に初期化されている
-
-
-class TestStaticFileServing:
- """静的ファイル配信のテスト"""
-
- @pytest.fixture
- def mock_frontend_dir(self, tmp_path):
- """テスト用フロントエンドディレクトリを作成"""
- frontend = tmp_path / "frontend"
- frontend.mkdir()
- css_dir = frontend / "css"
- css_dir.mkdir()
- js_dir = frontend / "js"
- js_dir.mkdir()
-
- # index.html作成
- (frontend / "index.html").write_text("Test")
- (css_dir / "styles.css").write_text("body { color: black; }")
- (js_dir / "app.js").write_text("console.log('test');")
-
- return frontend
-
- @pytest.mark.asyncio
- async def test_MAIN004_serve_index_when_exists(self):
- """MAIN004: index.htmlが存在する場合は正しく配信される"""
- from backend.main import serve_index, FRONTEND_DIR
- from fastapi.responses import FileResponse
-
- result = await serve_index()
-
- # FRONTENDディレクトリにindex.htmlが存在する場合はFileResponseを返す
- if (FRONTEND_DIR / "index.html").exists():
- assert isinstance(result, FileResponse)
- else:
- # 存在しない場合はエラー辞書を返す
- assert isinstance(result, dict)
- assert "error" in result
-
- @pytest.mark.asyncio
- async def test_MAIN005_serve_index_not_found(self):
- """MAIN005: index.htmlが存在しない場合はエラーを返す"""
- from backend.main import serve_index
- import backend.main as main_module
- from pathlib import Path
-
- # FRONTEND_DIRを一時的に存在しないパスに変更
- original_frontend_dir = main_module.FRONTEND_DIR
- main_module.FRONTEND_DIR = Path("/nonexistent/path")
-
- result = await serve_index()
- assert isinstance(result, dict)
- assert result == {"error": "Frontend not found"}
-
- main_module.FRONTEND_DIR = original_frontend_dir
-
- def test_MAIN006_css_mount_configured(self):
- """MAIN006: CSSディレクトリがマウントされている"""
- from backend.main import app
- routes = [route.path for route in app.routes]
- # マウントされたルートを確認
- assert any("/css" in str(route) for route in app.routes)
-
- def test_MAIN007_js_mount_configured(self):
- """MAIN007: JSディレクトリがマウントされている"""
- from backend.main import app
- routes = [route.path for route in app.routes]
- # マウントされたルートを確認
- assert any("/js" in str(route) for route in app.routes)
-
-
-class TestWebSocketEndpoint:
- """WebSocketエンドポイントのテスト"""
-
- def test_MAIN008_websocket_route_exists(self):
- """MAIN008: WebSocketエンドポイントが登録されている"""
- from backend.main import app
- ws_routes = [route for route in app.routes if hasattr(route, 'path') and route.path == '/ws']
- assert len(ws_routes) > 0
-
- @pytest.mark.asyncio
- async def test_MAIN009_websocket_connect_handler(self):
- """MAIN009: WebSocket接続ハンドラーが正しく動作する"""
- from backend.main import websocket_endpoint
- from backend.websocket import get_connection_manager
-
- # WebSocket接続のモック
- mock_websocket = AsyncMock()
- mock_websocket.receive_text = AsyncMock(side_effect=Exception("Disconnect"))
-
- manager = get_connection_manager()
- manager.connect = AsyncMock(return_value=True)
- manager.send_personal = AsyncMock()
- manager.disconnect = AsyncMock()
- manager.get_connection_count = MagicMock(return_value=1)
-
- # WebSocketハンドラーが呼び出し可能であることを確認
- assert callable(websocket_endpoint)
-
-
-class TestForceScanEndpoint:
- """強制スキャンエンドポイントのテスト"""
-
- def test_MAIN010_force_scan_route_exists(self):
- """MAIN010: 強制スキャンエンドポイントが登録されている"""
- from backend.main import app
- routes = [route.path for route in app.routes if hasattr(route, 'path')]
- assert "/api/force-scan" in routes
-
- @pytest.mark.asyncio
- async def test_MAIN011_force_scan_no_scanner(self):
- """MAIN011: スキャナー未初期化時はエラーを返す"""
- from backend.main import force_scan
- import backend.main as main_module
-
- # スキャナーをNoneに設定
- original_scanner = main_module._scanner
- main_module._scanner = None
-
- result = await force_scan()
- assert result == {"error": "Scanner not initialized"}
-
- main_module._scanner = original_scanner
-
- @pytest.mark.asyncio
- async def test_MAIN012_force_scan_with_scanner(self):
- """MAIN012: スキャナー初期化済みの場合は正しく実行される"""
- from backend.main import force_scan
- import backend.main as main_module
- from dataclasses import dataclass
-
- @dataclass
- class MockScanResult:
- project_name: str
- total_topics: int
- html_count: int
- txt_count: int
- mp3_count: int
-
- mock_scanner = MagicMock()
- mock_scanner.clear_cache = MagicMock()
- mock_scanner.scan_all_projects = AsyncMock(return_value=[
- MockScanResult("test_project", 10, 8, 6, 4)
- ])
-
- original_scanner = main_module._scanner
- main_module._scanner = mock_scanner
-
- result = await force_scan()
- assert result["status"] == "completed"
- assert result["projects_scanned"] == 1
- assert len(result["results"]) == 1
- assert result["results"][0]["name"] == "test_project"
-
- main_module._scanner = original_scanner
-
-
-class TestLifespan:
- """アプリケーションライフサイクルのテスト"""
-
- @pytest.mark.asyncio
- async def test_MAIN013_lifespan_context_manager(self):
- """MAIN013: lifespanコンテキストマネージャーが定義されている"""
- from backend.main import lifespan
- from contextlib import asynccontextmanager
- # lifespanはasynccontextmanagerデコレータで装飾された関数
- assert callable(lifespan)
- # 関数名を確認
- assert lifespan.__name__ == "lifespan"
-
- def test_MAIN014_default_content_path_defined(self):
- """MAIN014: デフォルトコンテンツパスが定義されている"""
- from backend.main import DEFAULT_CONTENT_PATH
- assert DEFAULT_CONTENT_PATH is not None
- assert isinstance(DEFAULT_CONTENT_PATH, Path)
-
-
-class TestInitialScan:
- """初回スキャンのテスト"""
-
- @pytest.mark.asyncio
- async def test_MAIN015_initial_scan_function(self):
- """MAIN015: 初回スキャン関数が定義されている"""
- from backend.main import _initial_scan
- assert callable(_initial_scan)
-
- @pytest.mark.asyncio
- async def test_MAIN016_initial_scan_broadcasts(self):
- """MAIN016: 初回スキャン完了時にブロードキャストする"""
- from backend.main import _initial_scan
- import backend.main as main_module
- from backend.websocket import get_connection_manager
-
- mock_scanner = MagicMock()
- mock_scanner.scan_all_projects = AsyncMock(return_value=[])
-
- original_scanner = main_module._scanner
- main_module._scanner = mock_scanner
-
- manager = get_connection_manager()
- manager.broadcast = AsyncMock()
-
- await _initial_scan()
-
- # scan_all_projectsが呼ばれたことを確認
- mock_scanner.scan_all_projects.assert_called_once()
-
- main_module._scanner = original_scanner
-
- @pytest.mark.asyncio
- async def test_MAIN017_initial_scan_error_handling(self):
- """MAIN017: 初回スキャンでエラーが発生しても例外を投げない"""
- from backend.main import _initial_scan
- import backend.main as main_module
-
- mock_scanner = MagicMock()
- mock_scanner.scan_all_projects = AsyncMock(side_effect=Exception("Scan error"))
-
- original_scanner = main_module._scanner
- main_module._scanner = mock_scanner
-
- # エラーが発生しても例外を投げないことを確認
- await _initial_scan() # Should not raise
-
- main_module._scanner = original_scanner
-
-
-class TestAPIRouter:
- """APIルーターのテスト"""
-
- def test_MAIN018_api_router_included(self):
- """MAIN018: APIルーターが含まれている"""
- from backend.main import app
- routes = [route.path for route in app.routes if hasattr(route, 'path')]
- # APIエンドポイントが登録されていることを確認
- api_routes = [r for r in routes if r.startswith('/api')]
- assert len(api_routes) > 0
-
- def test_MAIN019_health_endpoint(self):
- """MAIN019: ヘルスチェックエンドポイントが利用可能"""
- from backend.main import app
- routes = [route.path for route in app.routes if hasattr(route, 'path')]
- assert "/api/health" in routes
-
-
-class TestPathConfiguration:
- """パス設定のテスト"""
-
- def test_MAIN020_base_dir_defined(self):
- """MAIN020: BASE_DIRが正しく定義されている"""
- from backend.main import BASE_DIR
- assert BASE_DIR is not None
- assert isinstance(BASE_DIR, Path)
-
- def test_MAIN021_frontend_dir_defined(self):
- """MAIN021: FRONTEND_DIRが正しく定義されている"""
- from backend.main import FRONTEND_DIR
- assert FRONTEND_DIR is not None
- assert isinstance(FRONTEND_DIR, Path)
-
- def test_MAIN022_frontend_dir_relative_to_base(self):
- """MAIN022: FRONTEND_DIRはBASE_DIRからの相対パス"""
- from backend.main import BASE_DIR, FRONTEND_DIR
- assert FRONTEND_DIR == BASE_DIR / "frontend"
diff --git a/project/tests/test_scanner.py b/project/tests/test_scanner.py
deleted file mode 100644
index 9947b2b..0000000
--- a/project/tests/test_scanner.py
+++ /dev/null
@@ -1,406 +0,0 @@
-"""
-スキャナーテスト
-テスト対象: backend/scanner.py
-"""
-
-import pytest
-import asyncio
-import time
-from pathlib import Path
-
-from backend.scanner import AsyncScanner, HashCache, FileInfo, ScanResult
-from backend.wbs_parser import ParsedTopic, clear_wbs_cache
-
-
-class TestHashCache:
- """ハッシュキャッシュテスト"""
-
- def test_set_and_get(self):
- """キャッシュへの保存と取得"""
- cache = HashCache()
- cache.set("/test/path", "abc123")
-
- result = cache.get("/test/path")
- assert result == "abc123"
-
- def test_get_nonexistent(self):
- """存在しないキーの取得"""
- cache = HashCache()
- result = cache.get("/nonexistent")
- assert result is None
-
- def test_is_changed_new_file(self):
- """新規ファイルの変更検出"""
- cache = HashCache()
- assert cache.is_changed("/new/path", "hash123") is True
-
- def test_is_changed_same_hash(self):
- """同一ハッシュの変更検出"""
- cache = HashCache()
- cache.set("/test/path", "hash123")
- assert cache.is_changed("/test/path", "hash123") is False
-
- def test_is_changed_different_hash(self):
- """異なるハッシュの変更検出"""
- cache = HashCache()
- cache.set("/test/path", "hash123")
- assert cache.is_changed("/test/path", "hash456") is True
-
- def test_clear(self):
- """キャッシュのクリア"""
- cache = HashCache()
- cache.set("/test/path", "hash123")
- cache.clear()
- assert cache.get("/test/path") is None
-
- def test_lru_eviction(self):
- """LRU削除(キャッシュサイズ制限)"""
- cache = HashCache(max_size=3)
-
- cache.set("/path1", "hash1")
- cache.set("/path2", "hash2")
- cache.set("/path3", "hash3")
-
- # 4つ目を追加すると最古のものが削除される
- cache.set("/path4", "hash4")
-
- # path1が削除されているはず
- assert cache.get("/path1") is None
- assert cache.get("/path2") == "hash2"
- assert cache.get("/path3") == "hash3"
- assert cache.get("/path4") == "hash4"
-
-
-class TestFileDetection:
- """ファイル検出テスト"""
-
- @pytest.mark.asyncio
- async def test_SCN001_html_detection(self, db, content_dir):
- """SCN-001: HTML検出"""
- (content_dir / "01-01_APIの基礎.html").write_text("")
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- topic = ParsedTopic(
- topic_id="01-01",
- chapter="Chapter 1",
- title="APIの基礎",
- base_name="01-01_APIの基礎"
- )
-
- result = await scanner._scan_topic_files(1, topic, content_dir)
-
- assert result['has_html'] is True
- assert result['has_txt'] is False
- assert result['has_mp3'] is False
-
- @pytest.mark.asyncio
- async def test_SCN002_txt_detection(self, db, content_dir):
- """SCN-002: TXT検出"""
- (content_dir / "01-01_APIの基礎.txt").write_text("text content")
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- topic = ParsedTopic(
- topic_id="01-01",
- chapter="Chapter 1",
- title="APIの基礎",
- base_name="01-01_APIの基礎"
- )
-
- result = await scanner._scan_topic_files(1, topic, content_dir)
-
- assert result['has_html'] is False
- assert result['has_txt'] is True
- assert result['has_mp3'] is False
-
- @pytest.mark.asyncio
- async def test_SCN003_mp3_detection(self, db, content_dir):
- """SCN-003: MP3検出"""
- (content_dir / "01-01_APIの基礎.mp3").write_bytes(b'\x00\x01\x02')
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- topic = ParsedTopic(
- topic_id="01-01",
- chapter="Chapter 1",
- title="APIの基礎",
- base_name="01-01_APIの基礎"
- )
-
- result = await scanner._scan_topic_files(1, topic, content_dir)
-
- assert result['has_html'] is False
- assert result['has_txt'] is False
- assert result['has_mp3'] is True
-
- @pytest.mark.asyncio
- async def test_SCN004_no_files_detection(self, db, content_dir):
- """SCN-004: ファイル不在検出"""
- scanner = AsyncScanner(db, content_dir.parent)
-
- topic = ParsedTopic(
- topic_id="01-01",
- chapter="Chapter 1",
- title="存在しないトピック",
- base_name="01-01_存在しないトピック"
- )
-
- result = await scanner._scan_topic_files(1, topic, content_dir)
-
- assert result['has_html'] is False
- assert result['has_txt'] is False
- assert result['has_mp3'] is False
-
- @pytest.mark.asyncio
- async def test_SCN005_prefix_matching(self, db, content_dir):
- """SCN-005: プレフィックス一致"""
- # 異なるタイトル名でも同じプレフィックスなら検出される
- (content_dir / "01-01_別のタイトル.html").write_text("")
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- topic = ParsedTopic(
- topic_id="01-01",
- chapter="Chapter 1",
- title="APIの基礎",
- base_name="01-01_APIの基礎"
- )
-
- result = await scanner._scan_topic_files(1, topic, content_dir)
-
- # プレフィックス(01-01)でマッチするべき
- assert result['has_html'] is True
-
- @pytest.mark.asyncio
- async def test_SCN009_complete_progress(self, db, content_dir):
- """SCN-009: 進捗計算(完了)"""
- (content_dir / "01-01_test.html").write_text("")
- (content_dir / "01-01_test.txt").write_text("text")
- (content_dir / "01-01_test.mp3").write_bytes(b'\x00')
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- topic = ParsedTopic(
- topic_id="01-01",
- chapter="Chapter 1",
- title="test",
- base_name="01-01_test"
- )
-
- result = await scanner._scan_topic_files(1, topic, content_dir)
-
- assert result['has_html'] is True
- assert result['has_txt'] is True
- assert result['has_mp3'] is True
-
-
-class TestHashCalculation:
- """ハッシュ計算テスト"""
-
- @pytest.mark.asyncio
- async def test_SCN006_hash_calculation(self, db, content_dir):
- """SCN-006: ハッシュ計算"""
- test_file = content_dir / "test.txt"
- test_file.write_text("test content")
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- file_hash = await scanner._compute_hash(test_file)
-
- assert file_hash is not None
- assert len(file_hash) == 16 # xxhash64 hex length
-
- @pytest.mark.asyncio
- async def test_hash_deterministic(self, db, content_dir):
- """同一ファイルは同一ハッシュ"""
- test_file = content_dir / "test.txt"
- test_file.write_text("test content")
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- hash1 = await scanner._compute_hash(test_file)
- hash2 = await scanner._compute_hash(test_file)
-
- assert hash1 == hash2
-
- @pytest.mark.asyncio
- async def test_hash_different_content(self, db, content_dir):
- """異なる内容は異なるハッシュ"""
- test_file1 = content_dir / "test1.txt"
- test_file2 = content_dir / "test2.txt"
- test_file1.write_text("content 1")
- test_file2.write_text("content 2")
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- hash1 = await scanner._compute_hash(test_file1)
- hash2 = await scanner._compute_hash(test_file2)
-
- assert hash1 != hash2
-
-
-class TestChangeDetection:
- """変更検出テスト"""
-
- @pytest.mark.asyncio
- async def test_SCN007_change_detection(self, db, content_dir):
- """SCN-007: 変更検出"""
- test_file = content_dir / "test.txt"
- test_file.write_text("initial content")
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- # 初回スキャン
- file_info1 = await scanner.scan_single_file(test_file)
- assert file_info1.changed is True # 初回は常にTrue
-
- # ファイル変更
- test_file.write_text("modified content")
-
- # 変更後スキャン
- file_info2 = await scanner.scan_single_file(test_file)
- assert file_info2.changed is True
-
- @pytest.mark.asyncio
- async def test_SCN008_no_change_detection(self, db, content_dir):
- """SCN-008: 変更なし検出"""
- test_file = content_dir / "test.txt"
- test_file.write_text("initial content")
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- # 初回スキャン
- await scanner.scan_single_file(test_file)
-
- # 同一ファイル再スキャン
- file_info = await scanner.scan_single_file(test_file)
- assert file_info.changed is False
-
- @pytest.mark.asyncio
- async def test_scan_single_file_nonexistent(self, db, content_dir):
- """存在しないファイルのスキャン"""
- scanner = AsyncScanner(db, content_dir.parent)
-
- result = await scanner.scan_single_file(content_dir / "nonexistent.txt")
- assert result is None
-
-
-class TestProjectScanning:
- """プロジェクトスキャンテスト"""
-
- @pytest.mark.asyncio
- async def test_scan_project(self, db, project_dir):
- """プロジェクトスキャン"""
- clear_wbs_cache()
- scanner = AsyncScanner(db, project_dir.parent)
-
- result = await scanner.scan_project(project_dir)
-
- assert isinstance(result, ScanResult)
- assert result.project_name == "test_project"
- assert result.total_topics > 0
-
- @pytest.mark.asyncio
- async def test_scan_project_no_wbs(self, db, tmp_path):
- """WBS.jsonがないプロジェクト"""
- project_dir = tmp_path / "no_wbs_project"
- project_dir.mkdir()
-
- scanner = AsyncScanner(db, tmp_path)
-
- result = await scanner.scan_project(project_dir)
-
- assert result.total_topics == 0
-
- @pytest.mark.asyncio
- async def test_SCN013_scan_all_projects(self, db, tmp_path, mock_wbs_object):
- """SCN-013: 並列スキャン(複数プロジェクト)"""
- import json
-
- # 複数プロジェクトを作成
- for i in range(3):
- project_dir = tmp_path / f"project_{i}"
- project_dir.mkdir()
- wbs_path = project_dir / "WBS.json"
- wbs_path.write_text(json.dumps(mock_wbs_object, ensure_ascii=False))
- content_dir = project_dir / "content"
- content_dir.mkdir()
- (content_dir / "01-01_test.html").write_text("")
-
- clear_wbs_cache()
- scanner = AsyncScanner(db, tmp_path)
-
- results = await scanner.scan_all_projects()
-
- assert len(results) == 3
- assert all(isinstance(r, ScanResult) for r in results)
-
- @pytest.mark.asyncio
- async def test_scan_already_in_progress(self, db, project_dir):
- """スキャン中の重複実行防止"""
- clear_wbs_cache()
- scanner = AsyncScanner(db, project_dir.parent)
-
- # スキャンフラグを手動設定
- scanner._scanning = True
-
- results = await scanner.scan_all_projects()
-
- # スキャン中なので空リストが返される
- assert results == []
-
- @pytest.mark.asyncio
- async def test_detect_topics_from_files(self, db, content_dir_with_files):
- """ファイルからのトピック検出"""
- scanner = AsyncScanner(db, content_dir_with_files.parent)
-
- topics = await scanner._detect_topics_from_files(content_dir_with_files)
-
- assert len(topics) == 3
- assert all(isinstance(t, ParsedTopic) for t in topics)
-
-
-class TestPerformance:
- """パフォーマンステスト"""
-
- @pytest.mark.asyncio
- async def test_SCN012_large_scan_performance(self, db, tmp_path):
- """SCN-012: 大量ファイルスキャンパフォーマンス"""
- # 200ファイルを作成
- content_dir = tmp_path / "content"
- content_dir.mkdir()
-
- for i in range(200):
- (content_dir / f"{i//10:02d}-{i%10:02d}_topic{i}.html").write_text(f"{i}")
-
- scanner = AsyncScanner(db, tmp_path)
-
- start_time = time.time()
- topics = await scanner._detect_topics_from_files(content_dir)
- duration = time.time() - start_time
-
- assert len(topics) == 200
- assert duration < 1.0, f"処理時間が1秒を超えました: {duration:.2f}s"
-
-
-class TestCacheManagement:
- """キャッシュ管理テスト"""
-
- @pytest.mark.asyncio
- async def test_clear_cache(self, db, content_dir):
- """キャッシュクリア"""
- test_file = content_dir / "test.txt"
- test_file.write_text("content")
-
- scanner = AsyncScanner(db, content_dir.parent)
-
- # ファイルをスキャンしてキャッシュに保存
- await scanner.scan_single_file(test_file)
-
- # キャッシュクリア
- scanner.clear_cache()
-
- # 再スキャン(キャッシュがクリアされているので変更として検出)
- file_info = await scanner.scan_single_file(test_file)
- assert file_info.changed is True
diff --git a/project/tests/test_watcher.py b/project/tests/test_watcher.py
deleted file mode 100644
index a4c3fee..0000000
--- a/project/tests/test_watcher.py
+++ /dev/null
@@ -1,466 +0,0 @@
-"""
-watcher.py のユニットテスト
-ファイル監視、デバウンス処理、複数プロジェクト監視をテスト
-"""
-
-import pytest
-import asyncio
-from pathlib import Path
-from unittest.mock import AsyncMock, MagicMock, patch
-from backend.watcher import (
- DebounceBuffer,
- ContentEventHandler,
- ContentWatcher,
- MultiProjectWatcher,
- DEBOUNCE_MS,
- SUPPORTED_EXTENSIONS
-)
-
-
-# ========== DebounceBufferテスト ==========
-
-class TestDebounceBuffer:
- """デバウンスバッファのテスト"""
-
- def test_WATCH001_default_delay(self):
- """WATCH001: デフォルトの遅延時間が正しく設定される"""
- buffer = DebounceBuffer()
- assert buffer.delay_seconds == DEBOUNCE_MS / 1000.0
-
- def test_WATCH002_custom_delay(self):
- """WATCH002: カスタム遅延時間が正しく設定される"""
- buffer = DebounceBuffer(delay_ms=200)
- assert buffer.delay_seconds == 0.2
-
- @pytest.mark.asyncio
- async def test_WATCH003_set_callback(self):
- """WATCH003: コールバック設定が正しく動作する"""
- buffer = DebounceBuffer()
- callback = AsyncMock()
- loop = asyncio.get_event_loop()
-
- buffer.set_callback(callback, loop)
-
- assert buffer._callback == callback
- assert buffer._loop == loop
-
- @pytest.mark.asyncio
- async def test_WATCH004_add_event_stores_path(self):
- """WATCH004: イベント追加でパスが保存される"""
- buffer = DebounceBuffer(delay_ms=50)
- callback = AsyncMock()
- loop = asyncio.get_event_loop()
- buffer.set_callback(callback, loop)
-
- await buffer.add_event("/test/path1.html")
-
- # パスが保存されていることを確認
- assert "/test/path1.html" in buffer._pending_paths
-
- @pytest.mark.asyncio
- async def test_WATCH005_debounce_collects_multiple_events(self):
- """WATCH005: デバウンスが複数イベントを集約する"""
- buffer = DebounceBuffer(delay_ms=50)
- callback = AsyncMock()
- loop = asyncio.get_event_loop()
- buffer.set_callback(callback, loop)
-
- # 複数イベントを連続追加
- await buffer.add_event("/test/path1.html")
- await buffer.add_event("/test/path2.txt")
- await buffer.add_event("/test/path3.mp3")
-
- # デバウンス完了を待つ
- await asyncio.sleep(0.1)
-
- # コールバックが1回だけ呼ばれることを確認
- assert callback.call_count == 1
- # 全パスが含まれていることを確認
- called_paths = callback.call_args[0][0]
- assert len(called_paths) == 3
-
- @pytest.mark.asyncio
- async def test_WATCH006_debounce_resets_timer(self):
- """WATCH006: 新しいイベントでタイマーがリセットされる"""
- buffer = DebounceBuffer(delay_ms=100)
- callback = AsyncMock()
- loop = asyncio.get_event_loop()
- buffer.set_callback(callback, loop)
-
- await buffer.add_event("/test/path1.html")
- await asyncio.sleep(0.05) # 50ms待機
- await buffer.add_event("/test/path2.html") # タイマーリセット
- await asyncio.sleep(0.05) # 50ms待機(まだ発火しない)
-
- # まだコールバックは呼ばれていない
- assert callback.call_count == 0
-
- await asyncio.sleep(0.1) # さらに100ms待機
-
- # 今度はコールバックが呼ばれる
- assert callback.call_count == 1
-
- @pytest.mark.asyncio
- async def test_WATCH007_flush_clears_pending_paths(self):
- """WATCH007: フラッシュ後にpending_pathsがクリアされる"""
- buffer = DebounceBuffer(delay_ms=50)
- callback = AsyncMock()
- loop = asyncio.get_event_loop()
- buffer.set_callback(callback, loop)
-
- await buffer.add_event("/test/path1.html")
- await asyncio.sleep(0.1)
-
- assert len(buffer._pending_paths) == 0
-
- @pytest.mark.asyncio
- async def test_WATCH008_callback_error_handling(self):
- """WATCH008: コールバックエラーが適切に処理される"""
- buffer = DebounceBuffer(delay_ms=50)
- callback = AsyncMock(side_effect=Exception("Callback error"))
- loop = asyncio.get_event_loop()
- buffer.set_callback(callback, loop)
-
- await buffer.add_event("/test/path1.html")
-
- # エラーが発生しても例外を投げない
- await asyncio.sleep(0.1)
-
- assert callback.call_count == 1
-
-
-# ========== ContentEventHandlerテスト ==========
-
-class TestContentEventHandler:
- """コンテンツイベントハンドラーのテスト"""
-
- def test_WATCH009_supported_extensions(self):
- """WATCH009: サポートされる拡張子が正しく定義されている"""
- assert '.html' in SUPPORTED_EXTENSIONS
- assert '.txt' in SUPPORTED_EXTENSIONS
- assert '.mp3' in SUPPORTED_EXTENSIONS
-
- def test_WATCH010_ignore_directory_events(self):
- """WATCH010: ディレクトリイベントは無視される"""
- buffer = MagicMock()
- loop = asyncio.new_event_loop()
- handler = ContentEventHandler(buffer, loop)
-
- mock_event = MagicMock()
- mock_event.is_directory = True
- mock_event.src_path = "/test/dir"
-
- handler.on_any_event(mock_event)
-
- # バッファに追加されないことを確認
- buffer.add_event.assert_not_called()
-
- def test_WATCH011_ignore_unsupported_extensions(self):
- """WATCH011: サポートされない拡張子は無視される"""
- buffer = MagicMock()
- loop = asyncio.new_event_loop()
- handler = ContentEventHandler(buffer, loop)
-
- mock_event = MagicMock()
- mock_event.is_directory = False
- mock_event.src_path = "/test/file.py"
- mock_event.event_type = "modified"
-
- handler.on_any_event(mock_event)
-
- # バッファに追加されないことを確認
- # Note: asyncio.run_coroutine_threadsafeが呼ばれていないことを確認
-
- def test_WATCH012_process_supported_file(self):
- """WATCH012: サポートされるファイルのイベントを処理する"""
- buffer = DebounceBuffer()
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
- handler = ContentEventHandler(buffer, loop)
-
- mock_event = MagicMock()
- mock_event.is_directory = False
- mock_event.src_path = "/test/file.html"
- mock_event.event_type = "modified"
-
- # イベント処理
- handler.on_any_event(mock_event)
-
- # 少し待ってからチェック
- # Note: run_coroutine_threadsafeは別スレッドで実行されるため確認が難しい
- loop.close()
-
- def test_WATCH013_filter_event_types(self):
- """WATCH013: イベントタイプがフィルタされる"""
- buffer = MagicMock()
- loop = asyncio.new_event_loop()
- handler = ContentEventHandler(buffer, loop)
-
- mock_event = MagicMock()
- mock_event.is_directory = False
- mock_event.src_path = "/test/file.html"
- mock_event.event_type = "opened" # サポートされないイベントタイプ
-
- handler.on_any_event(mock_event)
-
- # バッファに追加されない
-
-
-# ========== ContentWatcherテスト ==========
-
-class TestContentWatcher:
- """コンテンツウォッチャーのテスト"""
-
- @pytest.fixture
- def watcher_dir(self, tmp_path):
- """ウォッチャーテスト用ディレクトリ"""
- watch_dir = tmp_path / "watch_test"
- watch_dir.mkdir()
- return watch_dir
-
- def test_WATCH014_watcher_initialization(self, watcher_dir):
- """WATCH014: ウォッチャーが正しく初期化される"""
- callback = AsyncMock()
- watcher = ContentWatcher(watcher_dir, callback)
-
- assert watcher.path == watcher_dir
- assert watcher.debounce_ms == DEBOUNCE_MS
- assert not watcher.is_running
-
- def test_WATCH015_custom_debounce(self, watcher_dir):
- """WATCH015: カスタムデバウンス時間が設定される"""
- callback = AsyncMock()
- watcher = ContentWatcher(watcher_dir, callback, debounce_ms=200)
-
- assert watcher.debounce_ms == 200
-
- @pytest.mark.asyncio
- async def test_WATCH016_start_watcher(self, watcher_dir):
- """WATCH016: ウォッチャーが正常に開始される"""
- callback = AsyncMock()
- watcher = ContentWatcher(watcher_dir, callback)
-
- await watcher.start()
- assert watcher.is_running
-
- await watcher.stop()
- assert not watcher.is_running
-
- @pytest.mark.asyncio
- async def test_WATCH017_start_nonexistent_path(self, tmp_path):
- """WATCH017: 存在しないパスでは開始しない"""
- nonexistent = tmp_path / "nonexistent"
- callback = AsyncMock()
- watcher = ContentWatcher(nonexistent, callback)
-
- await watcher.start()
- assert not watcher.is_running
-
- @pytest.mark.asyncio
- async def test_WATCH018_start_already_running(self, watcher_dir):
- """WATCH018: 既に実行中の場合は再開始しない"""
- callback = AsyncMock()
- watcher = ContentWatcher(watcher_dir, callback)
-
- await watcher.start()
- assert watcher.is_running
-
- # 再度開始しようとしても問題ない
- await watcher.start()
- assert watcher.is_running
-
- await watcher.stop()
-
- @pytest.mark.asyncio
- async def test_WATCH019_stop_not_running(self, watcher_dir):
- """WATCH019: 実行中でない場合の停止は何もしない"""
- callback = AsyncMock()
- watcher = ContentWatcher(watcher_dir, callback)
-
- # 開始せずに停止しても問題ない
- await watcher.stop()
- assert not watcher.is_running
-
- @pytest.mark.asyncio
- async def test_WATCH020_process_changes_calls_callback(self, watcher_dir):
- """WATCH020: _process_changesがコールバックを呼ぶ"""
- callback = AsyncMock()
- watcher = ContentWatcher(watcher_dir, callback)
-
- await watcher._process_changes(["/test/path1.html", "/test/path2.txt"])
-
- callback.assert_called_once_with(["/test/path1.html", "/test/path2.txt"])
-
- @pytest.mark.asyncio
- async def test_WATCH021_process_changes_empty_list(self, watcher_dir):
- """WATCH021: 空のリストでは何もしない"""
- callback = AsyncMock()
- watcher = ContentWatcher(watcher_dir, callback)
-
- await watcher._process_changes([])
-
- callback.assert_not_called()
-
- @pytest.mark.asyncio
- async def test_WATCH022_process_changes_error_handling(self, watcher_dir):
- """WATCH022: コールバックエラーが適切に処理される"""
- callback = AsyncMock(side_effect=Exception("Callback error"))
- watcher = ContentWatcher(watcher_dir, callback)
-
- # エラーが発生しても例外を投げない
- await watcher._process_changes(["/test/path1.html"])
-
- callback.assert_called_once()
-
-
-# ========== MultiProjectWatcherテスト ==========
-
-class TestMultiProjectWatcher:
- """マルチプロジェクトウォッチャーのテスト"""
-
- @pytest.fixture
- def multi_watcher_dir(self, tmp_path):
- """マルチウォッチャーテスト用ディレクトリ"""
- base_dir = tmp_path / "projects"
- base_dir.mkdir()
-
- # プロジェクト1
- project1 = base_dir / "project1"
- project1.mkdir()
- (project1 / "file1.html").write_text("content1")
-
- # プロジェクト2
- project2 = base_dir / "project2"
- project2.mkdir()
- (project2 / "file2.html").write_text("content2")
-
- return base_dir
-
- def test_WATCH023_multi_watcher_initialization(self, multi_watcher_dir):
- """WATCH023: マルチウォッチャーが正しく初期化される"""
- callback = AsyncMock()
- watcher = MultiProjectWatcher(multi_watcher_dir, callback)
-
- assert watcher.base_path == multi_watcher_dir
- assert watcher.debounce_ms == DEBOUNCE_MS
- assert not watcher.is_running
-
- def test_WATCH024_custom_debounce_multi(self, multi_watcher_dir):
- """WATCH024: マルチウォッチャーでカスタムデバウンスが設定される"""
- callback = AsyncMock()
- watcher = MultiProjectWatcher(multi_watcher_dir, callback, debounce_ms=150)
-
- assert watcher.debounce_ms == 150
-
- @pytest.mark.asyncio
- async def test_WATCH025_start_multi_watcher(self, multi_watcher_dir):
- """WATCH025: マルチウォッチャーが正常に開始される"""
- callback = AsyncMock()
- watcher = MultiProjectWatcher(multi_watcher_dir, callback)
-
- await watcher.start()
- assert watcher.is_running
-
- await watcher.stop()
- assert not watcher.is_running
-
- @pytest.mark.asyncio
- async def test_WATCH026_stop_multi_watcher(self, multi_watcher_dir):
- """WATCH026: マルチウォッチャーが正常に停止される"""
- callback = AsyncMock()
- watcher = MultiProjectWatcher(multi_watcher_dir, callback)
-
- await watcher.start()
- await watcher.stop()
-
- assert not watcher.is_running
- assert watcher._watcher is None
-
- @pytest.mark.asyncio
- async def test_WATCH027_project_name_extraction(self, multi_watcher_dir):
- """WATCH027: パスからプロジェクト名が正しく抽出される"""
- collected_changes = []
-
- async def capture_callback(project_name, paths):
- collected_changes.append((project_name, paths))
-
- watcher = MultiProjectWatcher(multi_watcher_dir, capture_callback, debounce_ms=50)
-
- await watcher.start()
-
- # 内部のハンドラーをテスト
- internal_handler = watcher._watcher.on_change_callback
- await internal_handler([
- str(multi_watcher_dir / "project1" / "file1.html"),
- str(multi_watcher_dir / "project2" / "file2.html")
- ])
-
- await watcher.stop()
-
- # 2つのプロジェクトに対してコールバックが呼ばれる
- assert len(collected_changes) == 2
- project_names = [c[0] for c in collected_changes]
- assert "project1" in project_names
- assert "project2" in project_names
-
- @pytest.mark.asyncio
- async def test_WATCH028_path_outside_base(self, multi_watcher_dir):
- """WATCH028: ベースパス外のパスは警告される"""
- collected_changes = []
-
- async def capture_callback(project_name, paths):
- collected_changes.append((project_name, paths))
-
- watcher = MultiProjectWatcher(multi_watcher_dir, capture_callback, debounce_ms=50)
-
- await watcher.start()
-
- # ベースパス外のパスを含むリスト
- internal_handler = watcher._watcher.on_change_callback
- await internal_handler(["/some/other/path/file.html"])
-
- await watcher.stop()
-
- # ベースパス外のパスはコールバックに渡されない
- assert len(collected_changes) == 0
-
- @pytest.mark.asyncio
- async def test_WATCH029_is_running_property(self, multi_watcher_dir):
- """WATCH029: is_runningプロパティが正しく動作する"""
- callback = AsyncMock()
- watcher = MultiProjectWatcher(multi_watcher_dir, callback)
-
- assert not watcher.is_running
-
- await watcher.start()
- assert watcher.is_running
-
- await watcher.stop()
- assert not watcher.is_running
-
- @pytest.mark.asyncio
- async def test_WATCH030_same_project_multiple_files(self, multi_watcher_dir):
- """WATCH030: 同一プロジェクトの複数ファイルがグループ化される"""
- collected_changes = []
-
- async def capture_callback(project_name, paths):
- collected_changes.append((project_name, paths))
-
- watcher = MultiProjectWatcher(multi_watcher_dir, capture_callback, debounce_ms=50)
-
- await watcher.start()
-
- # 同じプロジェクトの複数ファイル
- internal_handler = watcher._watcher.on_change_callback
- await internal_handler([
- str(multi_watcher_dir / "project1" / "file1.html"),
- str(multi_watcher_dir / "project1" / "file2.txt"),
- str(multi_watcher_dir / "project1" / "file3.mp3")
- ])
-
- await watcher.stop()
-
- # 1つのプロジェクトに対して1回のコールバック
- assert len(collected_changes) == 1
- assert collected_changes[0][0] == "project1"
- assert len(collected_changes[0][1]) == 3
diff --git a/project/tests/test_wbs_parser.py b/project/tests/test_wbs_parser.py
deleted file mode 100644
index 4fb056c..0000000
--- a/project/tests/test_wbs_parser.py
+++ /dev/null
@@ -1,391 +0,0 @@
-"""
-WBSパーサーテスト
-テスト対象: backend/wbs_parser.py
-"""
-
-import pytest
-import json
-import tempfile
-from pathlib import Path
-
-from backend.wbs_parser import (
- detect_wbs_format,
- ObjectFormatParser,
- ArrayFormatParser,
- parse_wbs,
- ParsedTopic,
- clear_wbs_cache
-)
-
-
-class TestWBSFormatDetection:
- """WBS形式検出テスト"""
-
- def test_WBS001_detect_object_format(self, mock_wbs_object):
- """WBS-001: オブジェクト型形式検出"""
- result = detect_wbs_format(mock_wbs_object)
- assert result == 'object'
-
- def test_WBS002_detect_array_format(self, mock_wbs_array):
- """WBS-002: 配列型形式検出"""
- result = detect_wbs_format(mock_wbs_array)
- assert result == 'array'
-
- def test_WBS003_detect_unknown_format(self, mock_wbs_invalid):
- """WBS-003: 不明形式検出"""
- result = detect_wbs_format(mock_wbs_invalid)
- assert result == 'unknown'
-
- def test_detect_empty_phases_dict(self):
- """空のphasesオブジェクトの検出"""
- wbs = {"phases": {}}
- result = detect_wbs_format(wbs)
- assert result == 'object'
-
- def test_detect_empty_phases_list(self):
- """空のphasesリストの検出"""
- wbs = {"phases": []}
- result = detect_wbs_format(wbs)
- assert result == 'array'
-
- def test_detect_missing_phases(self):
- """phasesキーがない場合"""
- wbs = {"project": {"name": "Test"}}
- result = detect_wbs_format(wbs)
- # デフォルトでobjectになる(phasesがないので空のdictとして処理)
- assert result == 'object'
-
-
-class TestObjectFormatParser:
- """オブジェクト型パーサーテスト"""
-
- def test_WBS004_parse_object_format(self, mock_wbs_object):
- """WBS-004: オブジェクト型パース"""
- parser = ObjectFormatParser()
- topics = parser.parse(mock_wbs_object)
-
- assert len(topics) == 5 # 3 + 2 topics
- assert all(isinstance(t, ParsedTopic) for t in topics)
-
- # 最初のトピックを検証
- first_topic = topics[0]
- assert first_topic.topic_id == "topic_01_01"
- assert first_topic.title == "トピック1-1"
- assert first_topic.base_name == "01-01_トピック1-1"
- assert first_topic.chapter == "Chapter 1: 基礎"
-
- def test_WBS006_empty_wbs(self, mock_wbs_empty):
- """WBS-006: 空WBS処理"""
- parser = ObjectFormatParser()
- topics = parser.parse(mock_wbs_empty)
- assert topics == []
-
- def test_WBS007_nested_structure(self):
- """WBS-007: ネストが深い構造"""
- wbs = {
- "phases": {
- "phase_1": {
- "name": "Phase 1",
- "chapters": {
- "ch1": {
- "name": "Chapter 1",
- "topics": [{"id": "t1", "title": "Topic 1", "base_name": "01-01_t1"}]
- }
- }
- },
- "phase_2": {
- "name": "Phase 2",
- "chapters": {
- "ch1": {
- "name": "Chapter 1",
- "topics": [{"id": "t2", "title": "Topic 2", "base_name": "02-01_t2"}]
- },
- "ch2": {
- "name": "Chapter 2",
- "topics": [
- {"id": "t3", "title": "Topic 3", "base_name": "02-02_t3"},
- {"id": "t4", "title": "Topic 4", "base_name": "02-03_t4"}
- ]
- }
- }
- }
- }
- }
- parser = ObjectFormatParser()
- topics = parser.parse(wbs)
- assert len(topics) == 4
-
- def test_WBS008_missing_base_name(self):
- """WBS-008: base_name欠損時"""
- wbs = {
- "phases": {
- "phase_1": {
- "chapters": {
- "ch1": {
- "name": "Chapter 1",
- "topics": [{"id": "t1", "title": "Topic 1"}] # base_nameなし
- }
- }
- }
- }
- }
- parser = ObjectFormatParser()
- topics = parser.parse(wbs)
- # base_nameがなくても空文字列でパースされる
- assert len(topics) == 1
- assert topics[0].base_name == ""
-
- def test_WBS009_japanese_title(self, mock_wbs_object):
- """WBS-009: 日本語タイトル処理"""
- parser = ObjectFormatParser()
- topics = parser.parse(mock_wbs_object)
-
- # 日本語が正しく処理されていることを確認
- assert any("トピック" in t.title for t in topics)
- assert any("基礎" in t.chapter for t in topics)
-
- def test_invalid_phase_value(self):
- """不正なフェーズ値のスキップ"""
- wbs = {
- "phases": {
- "phase_1": "invalid", # dictではない
- "phase_2": {
- "chapters": {
- "ch1": {
- "name": "Chapter 1",
- "topics": [{"id": "t1", "title": "Topic 1", "base_name": "01-01_t1"}]
- }
- }
- }
- }
- }
- parser = ObjectFormatParser()
- topics = parser.parse(wbs)
- assert len(topics) == 1 # phase_2のトピックのみ
-
- def test_invalid_chapter_value(self):
- """不正なチャプター値のスキップ"""
- wbs = {
- "phases": {
- "phase_1": {
- "chapters": {
- "ch1": "invalid", # dictではない
- "ch2": {
- "name": "Chapter 2",
- "topics": [{"id": "t1", "title": "Topic 1", "base_name": "01-01_t1"}]
- }
- }
- }
- }
- }
- parser = ObjectFormatParser()
- topics = parser.parse(wbs)
- assert len(topics) == 1
-
- def test_invalid_topic_value(self):
- """不正なトピック値のスキップ"""
- wbs = {
- "phases": {
- "phase_1": {
- "chapters": {
- "ch1": {
- "name": "Chapter 1",
- "topics": [
- "invalid", # dictではない
- {"id": "t1", "title": "Topic 1", "base_name": "01-01_t1"}
- ]
- }
- }
- }
- }
- }
- parser = ObjectFormatParser()
- topics = parser.parse(wbs)
- assert len(topics) == 1
-
-
-class TestArrayFormatParser:
- """配列型パーサーテスト"""
-
- def test_WBS005_parse_array_format(self, tmp_path, mock_wbs_array):
- """WBS-005: 配列型パース"""
- # content/フォルダを作成
- content_dir = tmp_path / "content"
- content_dir.mkdir()
- (content_dir / "01-01_AI概要.html").touch()
- (content_dir / "01-02_機械学習.html").touch()
-
- parser = ArrayFormatParser(content_dir)
- topics = parser.parse(mock_wbs_array)
-
- # ファイル数に基づいてトピックを生成
- assert len(topics) == 2
-
- def test_parse_array_without_content_path(self, mock_wbs_array):
- """content_pathなしの場合"""
- parser = ArrayFormatParser(content_path=None)
- topics = parser.parse(mock_wbs_array)
- assert topics == []
-
- def test_parse_array_with_nonexistent_content(self, tmp_path, mock_wbs_array):
- """存在しないcontent_pathの場合"""
- nonexistent_path = tmp_path / "nonexistent"
- parser = ArrayFormatParser(nonexistent_path)
- topics = parser.parse(mock_wbs_array)
- assert topics == []
-
- def test_extract_chapters(self, mock_wbs_array):
- """チャプター抽出テスト"""
- parser = ArrayFormatParser()
- chapters = parser._extract_chapters(mock_wbs_array)
-
- assert "ch1" in chapters
- assert "ch2" in chapters
- assert chapters["ch1"] == "Chapter 1: 基礎"
- assert chapters["ch2"] == "Chapter 2: 応用"
-
- def test_infer_chapter(self):
- """チャプター推論テスト"""
- parser = ArrayFormatParser()
- chapters = {"ch1": "Chapter 1: 基礎", "ch2": "Chapter 2: 応用"}
-
- assert parser._infer_chapter("01-01_test", chapters) == "Chapter 1: 基礎"
- assert parser._infer_chapter("02-01_test", chapters) == "Chapter 2: 応用"
- assert parser._infer_chapter("03-01_test", chapters) == "Chapter 3"
- assert parser._infer_chapter("invalid", chapters) == "Unknown"
-
- def test_extract_topic_id(self):
- """トピックID抽出テスト"""
- parser = ArrayFormatParser()
-
- assert parser._extract_topic_id("01-01_APIの基礎") == "01-01"
- assert parser._extract_topic_id("02-15_高度な使い方") == "02-15"
- assert parser._extract_topic_id("invalid") == "inval"
-
- def test_clean_title(self):
- """タイトルクリーニングテスト"""
- parser = ArrayFormatParser()
-
- assert parser._clean_title("01-01_APIの基礎") == "APIの基礎"
- assert parser._clean_title("02-15_高度な使い方") == "高度な使い方"
- assert parser._clean_title("invalid_name") == "invalid_name"
-
-
-class TestParseWBS:
- """WBSパース統合テスト"""
-
- def test_parse_object_wbs_file(self, tmp_path, mock_wbs_object):
- """オブジェクト型WBSファイルのパース"""
- wbs_path = tmp_path / "WBS.json"
- wbs_path.write_text(json.dumps(mock_wbs_object, ensure_ascii=False))
-
- # キャッシュをクリア
- clear_wbs_cache()
-
- topics = parse_wbs(wbs_path)
- assert len(topics) == 5
-
- def test_parse_array_wbs_file(self, tmp_path, mock_wbs_array):
- """配列型WBSファイルのパース"""
- # content/フォルダを作成
- content_dir = tmp_path / "content"
- content_dir.mkdir()
- (content_dir / "01-01_test.html").touch()
-
- wbs_path = tmp_path / "WBS.json"
- wbs_path.write_text(json.dumps(mock_wbs_array, ensure_ascii=False))
-
- clear_wbs_cache()
-
- topics = parse_wbs(wbs_path, content_dir)
- assert len(topics) == 1
-
- def test_parse_nonexistent_file(self, tmp_path):
- """存在しないファイルのパース"""
- clear_wbs_cache()
- topics = parse_wbs(tmp_path / "nonexistent.json")
- assert topics == []
-
- def test_parse_invalid_json(self, tmp_path):
- """不正なJSONファイルのパース"""
- wbs_path = tmp_path / "WBS.json"
- wbs_path.write_text("invalid json {{{")
-
- clear_wbs_cache()
-
- topics = parse_wbs(wbs_path)
- assert topics == []
-
- def test_WBS010_large_wbs_performance(self, tmp_path):
- """WBS-010: 大規模WBS処理パフォーマンス"""
- import time
-
- # 200トピックを持つ大規模WBSを生成
- topics_list = [
- {"id": f"topic_{i:03d}", "title": f"Topic {i}", "base_name": f"{i//10:02d}-{i%10:02d}_Topic{i}"}
- for i in range(200)
- ]
-
- wbs = {
- "phases": {
- "phase_1": {
- "chapters": {
- "ch1": {
- "name": "Large Chapter",
- "topics": topics_list
- }
- }
- }
- }
- }
-
- wbs_path = tmp_path / "WBS.json"
- wbs_path.write_text(json.dumps(wbs, ensure_ascii=False))
-
- clear_wbs_cache()
-
- start_time = time.time()
- topics = parse_wbs(wbs_path)
- duration_ms = (time.time() - start_time) * 1000
-
- assert len(topics) == 200
- assert duration_ms < 100, f"処理時間が100msを超えました: {duration_ms:.2f}ms"
-
-
-class TestWBSCache:
- """WBSキャッシュテスト"""
-
- def test_cache_works(self, tmp_path, mock_wbs_object):
- """キャッシュが動作することを確認"""
- wbs_path = tmp_path / "WBS.json"
- wbs_path.write_text(json.dumps(mock_wbs_object, ensure_ascii=False))
-
- clear_wbs_cache()
-
- # 1回目の呼び出し
- topics1 = parse_wbs(wbs_path)
-
- # 2回目の呼び出し(キャッシュから)
- topics2 = parse_wbs(wbs_path)
-
- assert len(topics1) == len(topics2)
-
- def test_clear_cache(self, tmp_path, mock_wbs_object):
- """キャッシュクリアが動作することを確認"""
- wbs_path = tmp_path / "WBS.json"
- wbs_path.write_text(json.dumps(mock_wbs_object, ensure_ascii=False))
-
- clear_wbs_cache()
- topics1 = parse_wbs(wbs_path)
-
- # キャッシュクリア
- clear_wbs_cache()
-
- # ファイル内容を変更
- mock_wbs_object["phases"]["phase_2"]["chapters"]["chapter_1"]["topics"].append(
- {"id": "new", "title": "New Topic", "base_name": "new_topic"}
- )
- wbs_path.write_text(json.dumps(mock_wbs_object, ensure_ascii=False))
-
- topics2 = parse_wbs(wbs_path)
- assert len(topics2) == len(topics1) + 1
diff --git a/project/tests/test_websocket.py b/project/tests/test_websocket.py
deleted file mode 100644
index 5e1e108..0000000
--- a/project/tests/test_websocket.py
+++ /dev/null
@@ -1,366 +0,0 @@
-"""
-WebSocketテスト
-テスト対象: backend/websocket.py
-"""
-
-import pytest
-import asyncio
-import json
-from unittest.mock import AsyncMock, MagicMock
-from fastapi import WebSocket
-
-from backend.websocket import ConnectionManager, get_connection_manager
-
-
-@pytest.fixture
-def manager():
- """ConnectionManager フィクスチャ"""
- return ConnectionManager(max_connections=5)
-
-
-@pytest.fixture
-def mock_websocket():
- """モックWebSocket"""
- ws = AsyncMock(spec=WebSocket)
- ws.accept = AsyncMock()
- ws.send_text = AsyncMock()
- ws.close = AsyncMock()
- return ws
-
-
-@pytest.fixture
-def mock_websockets(count=3):
- """複数のモックWebSocket"""
- return [
- AsyncMock(
- spec=WebSocket,
- accept=AsyncMock(),
- send_text=AsyncMock(),
- close=AsyncMock()
- )
- for _ in range(count)
- ]
-
-
-class TestConnectionManagement:
- """接続管理テスト"""
-
- @pytest.mark.asyncio
- async def test_WS001_connect(self, manager, mock_websocket):
- """WS-001: 接続確立"""
- result = await manager.connect(mock_websocket)
-
- assert result is True
- assert mock_websocket in manager.active_connections
- mock_websocket.accept.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_WS002_disconnect(self, manager, mock_websocket):
- """WS-002: 切断処理"""
- await manager.connect(mock_websocket)
- await manager.disconnect(mock_websocket)
-
- assert mock_websocket not in manager.active_connections
- assert mock_websocket not in manager._client_info
-
- @pytest.mark.asyncio
- async def test_WS006_max_connections(self, manager):
- """WS-006: 接続数制限"""
- # 5つの接続を作成
- websockets = []
- for _ in range(5):
- ws = AsyncMock(spec=WebSocket)
- ws.accept = AsyncMock()
- ws.close = AsyncMock()
- websockets.append(ws)
- await manager.connect(ws)
-
- assert len(manager.active_connections) == 5
-
- # 6つ目の接続は拒否される
- ws_overflow = AsyncMock(spec=WebSocket)
- ws_overflow.accept = AsyncMock()
- ws_overflow.close = AsyncMock()
-
- result = await manager.connect(ws_overflow)
-
- assert result is False
- ws_overflow.close.assert_called_once()
- assert len(manager.active_connections) == 5
-
- @pytest.mark.asyncio
- async def test_get_connection_count(self, manager, mock_websocket):
- """接続数の取得"""
- assert manager.get_connection_count() == 0
-
- await manager.connect(mock_websocket)
- assert manager.get_connection_count() == 1
-
- await manager.disconnect(mock_websocket)
- assert manager.get_connection_count() == 0
-
-
-class TestBroadcast:
- """ブロードキャストテスト"""
-
- @pytest.mark.asyncio
- async def test_WS003_broadcast(self, manager):
- """WS-003: ブロードキャスト"""
- websockets = []
- for _ in range(3):
- ws = AsyncMock(spec=WebSocket)
- ws.accept = AsyncMock()
- ws.send_text = AsyncMock()
- websockets.append(ws)
- await manager.connect(ws)
-
- sent_count = await manager.broadcast("test_event", {"message": "Hello"})
-
- assert sent_count == 3
- for ws in websockets:
- ws.send_text.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_broadcast_empty(self, manager):
- """接続なしのブロードキャスト"""
- sent_count = await manager.broadcast("test_event", {"message": "Hello"})
- assert sent_count == 0
-
- @pytest.mark.asyncio
- async def test_broadcast_removes_disconnected(self, manager):
- """切断されたクライアントの自動削除"""
- ws_ok = AsyncMock(spec=WebSocket)
- ws_ok.accept = AsyncMock()
- ws_ok.send_text = AsyncMock()
-
- ws_fail = AsyncMock(spec=WebSocket)
- ws_fail.accept = AsyncMock()
- ws_fail.send_text = AsyncMock(side_effect=Exception("Connection closed"))
-
- await manager.connect(ws_ok)
- await manager.connect(ws_fail)
-
- assert len(manager.active_connections) == 2
-
- await manager.broadcast("test_event", {"message": "Hello"})
-
- # 失敗したクライアントは削除される
- assert len(manager.active_connections) == 1
- assert ws_ok in manager.active_connections
- assert ws_fail not in manager.active_connections
-
- @pytest.mark.asyncio
- async def test_broadcast_message_format(self, manager, mock_websocket):
- """ブロードキャストメッセージ形式"""
- await manager.connect(mock_websocket)
-
- await manager.broadcast("test_event", {"key": "value"})
-
- call_args = mock_websocket.send_text.call_args[0][0]
- message = json.loads(call_args)
-
- assert message["event"] == "test_event"
- assert message["data"] == {"key": "value"}
- assert "timestamp" in message
-
-
-class TestPersonalMessage:
- """個別メッセージテスト"""
-
- @pytest.mark.asyncio
- async def test_WS004_send_personal(self, manager, mock_websocket):
- """WS-004: 個別送信"""
- await manager.connect(mock_websocket)
-
- result = await manager.send_personal(
- mock_websocket,
- "personal_event",
- {"message": "Hello"}
- )
-
- assert result is True
- mock_websocket.send_text.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_send_personal_to_disconnected(self, manager, mock_websocket):
- """切断済みクライアントへの送信"""
- result = await manager.send_personal(
- mock_websocket,
- "personal_event",
- {"message": "Hello"}
- )
-
- assert result is False
- mock_websocket.send_text.assert_not_called()
-
- @pytest.mark.asyncio
- async def test_send_personal_with_error(self, manager, mock_websocket):
- """送信エラー時の処理"""
- mock_websocket.send_text = AsyncMock(side_effect=Exception("Send failed"))
- await manager.connect(mock_websocket)
-
- result = await manager.send_personal(
- mock_websocket,
- "personal_event",
- {"message": "Hello"}
- )
-
- assert result is False
- # エラー時は自動切断
- assert mock_websocket not in manager.active_connections
-
-
-class TestSpecializedBroadcast:
- """特化ブロードキャストテスト"""
-
- @pytest.mark.asyncio
- async def test_broadcast_project_update(self, manager, mock_websocket):
- """プロジェクト更新ブロードキャスト"""
- await manager.connect(mock_websocket)
-
- project_data = {"id": 1, "name": "Test Project", "progress": 50}
- sent_count = await manager.broadcast_project_update(project_data)
-
- assert sent_count == 1
-
- call_args = mock_websocket.send_text.call_args[0][0]
- message = json.loads(call_args)
-
- assert message["event"] == "project_updated"
- assert message["data"]["project"] == project_data
-
- @pytest.mark.asyncio
- async def test_broadcast_topic_change(self, manager, mock_websocket):
- """トピック変更ブロードキャスト"""
- await manager.connect(mock_websocket)
-
- topic_data = {"id": 1, "base_name": "01-01_test"}
- sent_count = await manager.broadcast_topic_change(1, topic_data)
-
- assert sent_count == 1
-
- call_args = mock_websocket.send_text.call_args[0][0]
- message = json.loads(call_args)
-
- assert message["event"] == "topic_changed"
- assert message["data"]["project_id"] == 1
- assert message["data"]["topic"] == topic_data
-
- @pytest.mark.asyncio
- async def test_broadcast_scan_started(self, manager, mock_websocket):
- """スキャン開始ブロードキャスト"""
- await manager.connect(mock_websocket)
-
- sent_count = await manager.broadcast_scan_started("scan_123", 1, "full")
-
- assert sent_count == 1
-
- call_args = mock_websocket.send_text.call_args[0][0]
- message = json.loads(call_args)
-
- assert message["event"] == "scan_started"
- assert message["data"]["scan_id"] == "scan_123"
- assert message["data"]["project_id"] == 1
- assert message["data"]["type"] == "full"
-
- @pytest.mark.asyncio
- async def test_broadcast_scan_progress(self, manager, mock_websocket):
- """スキャン進捗ブロードキャスト"""
- await manager.connect(mock_websocket)
-
- sent_count = await manager.broadcast_scan_progress("scan_123", 50.0, "Project A")
-
- assert sent_count == 1
-
- call_args = mock_websocket.send_text.call_args[0][0]
- message = json.loads(call_args)
-
- assert message["event"] == "scan_progress"
- assert message["data"]["scan_id"] == "scan_123"
- assert message["data"]["progress"] == 50.0
- assert message["data"]["current"] == "Project A"
-
- @pytest.mark.asyncio
- async def test_broadcast_scan_completed(self, manager, mock_websocket):
- """スキャン完了ブロードキャスト"""
- await manager.connect(mock_websocket)
-
- result = {
- "projects_scanned": 5,
- "files_scanned": 100,
- "changes_detected": 10
- }
- sent_count = await manager.broadcast_scan_completed("scan_123", result)
-
- assert sent_count == 1
-
- call_args = mock_websocket.send_text.call_args[0][0]
- message = json.loads(call_args)
-
- assert message["event"] == "scan_completed"
- assert message["data"]["scan_id"] == "scan_123"
- assert message["data"]["result"] == result
-
-
-class TestConnectionStats:
- """接続統計テスト"""
-
- @pytest.mark.asyncio
- async def test_get_connection_stats(self, manager, mock_websocket):
- """接続統計の取得"""
- await manager.connect(mock_websocket)
-
- stats = manager.get_connection_stats()
-
- assert stats["total_connections"] == 1
- assert stats["max_connections"] == 5
- assert len(stats["clients"]) == 1
- assert "connected_at" in stats["clients"][0]
- assert "message_count" in stats["clients"][0]
-
- @pytest.mark.asyncio
- async def test_message_count_tracking(self, manager, mock_websocket):
- """メッセージカウントの追跡"""
- await manager.connect(mock_websocket)
-
- # 3回ブロードキャスト
- await manager.broadcast("event1", {})
- await manager.broadcast("event2", {})
- await manager.broadcast("event3", {})
-
- stats = manager.get_connection_stats()
- assert stats["clients"][0]["message_count"] == 3
-
-
-class TestSingleton:
- """シングルトンテスト"""
-
- def test_get_connection_manager(self):
- """シングルトン取得"""
- manager1 = get_connection_manager()
- manager2 = get_connection_manager()
-
- assert manager1 is manager2
-
-
-class TestWS005InvalidMessage:
- """不正メッセージテスト"""
-
- @pytest.mark.asyncio
- async def test_WS005_broadcast_with_special_characters(self, manager, mock_websocket):
- """特殊文字を含むメッセージのブロードキャスト"""
- await manager.connect(mock_websocket)
-
- data = {
- "message": "日本語テスト",
- "special": "特殊文字: <>\"'&",
- "unicode": "絵文字: 🎉🚀"
- }
-
- sent_count = await manager.broadcast("test_event", data)
-
- assert sent_count == 1
- # 正しくJSONエンコードされていることを確認
- call_args = mock_websocket.send_text.call_args[0][0]
- decoded = json.loads(call_args)
- assert decoded["data"]["message"] == "日本語テスト"
diff --git a/quick_start.sh b/quick_start.sh
deleted file mode 100755
index 53bfa3c..0000000
--- a/quick_start.sh
+++ /dev/null
@@ -1,172 +0,0 @@
-#!/bin/bash
-
-# quick_start.sh - エージェントシステムのクイックスタートスクリプト
-# 使い方: ./quick_start.sh
-
-set -e
-
-# カラー定義
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m'
-
-echo -e "${BLUE}================================${NC}"
-echo -e "${BLUE}🚀 AI Agent System Quick Start${NC}"
-echo -e "${BLUE}================================${NC}"
-echo ""
-
-# プロジェクトタイプの選択
-echo "どのようなプロジェクトを始めますか?"
-echo ""
-echo "1) 📱 Webアプリケーション"
-echo "2) 🔧 API開発"
-echo "3) 📊 データ分析"
-echo "4) 🎨 UI/UXデザイン"
-echo "5) 🚀 フルスタック開発"
-echo "6) 🔍 研究・調査"
-echo "7) 📝 ドキュメント作成"
-echo "8) 🐛 デバッグ・修正"
-echo "9) ⚙️ カスタム設定"
-echo ""
-read -p "選択してください (1-9): " project_type
-
-# プロジェクト名の入力
-echo ""
-read -p "プロジェクト名を入力してください: " project_name
-
-# プロジェクトディレクトリの作成
-if [ -d "$project_name" ]; then
- echo -e "${YELLOW}⚠️ ディレクトリ '$project_name' は既に存在します${NC}"
- read -p "上書きしますか? (y/n): " overwrite
- if [ "$overwrite" != "y" ]; then
- echo "終了します"
- exit 1
- fi
- rm -rf "$project_name"
-fi
-
-echo ""
-echo -e "${GREEN}✨ プロジェクト '$project_name' を作成中...${NC}"
-
-# テンプレートをコピー
-cp -r . "$project_name" 2>/dev/null || true
-cd "$project_name"
-
-# .gitの削除(新規プロジェクトなので)
-rm -rf .git
-
-# Git初期化
-git init --quiet
-echo -e "${GREEN}✅ Gitリポジトリを初期化しました${NC}"
-
-# 必要なディレクトリの作成
-mkdir -p worktrees src docs tests
-echo -e "${GREEN}✅ プロジェクト構造を作成しました${NC}"
-
-# プロジェクトタイプに応じた設定
-case $project_type in
- 1)
- team="webapp"
- agents="frontend_dev, backend_dev, tester"
- ;;
- 2)
- team="api"
- agents="backend_dev, db_expert, tester"
- ;;
- 3)
- team="data"
- agents="data_scientist, engineer"
- ;;
- 4)
- team="design"
- agents="ui_ux_designer, frontend_dev"
- ;;
- 5)
- team="fullstack"
- agents="frontend_dev, backend_dev, devops_engineer, tester"
- ;;
- 6)
- team="research"
- agents="researcher, report_writer"
- ;;
- 7)
- team="docs"
- agents="report_writer"
- ;;
- 8)
- team="debug"
- agents="code_reviewer, engineer, tester"
- ;;
- 9)
- team="custom"
- agents="generalist"
- ;;
-esac
-
-# プロジェクト固有のREADME作成
-cat > README.md << EOF
-# $project_name
-
-AIエージェントシステムで開発されるプロジェクト
-
-## プロジェクトタイプ
-- チーム: $team
-- エージェント: $agents
-
-## 使い方
-
-### エージェントを起動してタスクを実行
-\`\`\`bash
-./launch_agents.sh $team "実行したいタスク"
-\`\`\`
-
-### 例
-\`\`\`bash
-# 新機能の追加
-./launch_agents.sh $team "ユーザー認証機能を追加"
-
-# バグ修正
-./launch_agents.sh debug "ログインエラーを修正"
-
-# ドキュメント作成
-./launch_agents.sh docs "APIドキュメントを作成"
-\`\`\`
-
-## プロジェクト構造
-\`\`\`
-$project_name/
-├── src/ # ソースコード
-├── docs/ # ドキュメント
-├── tests/ # テストコード
-├── worktrees/ # エージェントの作業場所
-├── agent_config.yaml # エージェント設定
-├── agent_library.yaml # エージェントライブラリ
-└── launch_agents.sh # 実行スクリプト
-\`\`\`
-
-## カスタマイズ
-
-エージェントの設定を変更するには \`agent_config.yaml\` を編集してください。
-新しいエージェントを追加するには \`agent_library.yaml\` を参照してください。
-
----
-Generated with AI Agent System
-EOF
-
-# 初期コミット
-git add -A
-git commit -m "Initial commit: $project_name project setup with AI agent system" --quiet
-
-echo ""
-echo -e "${GREEN}================================${NC}"
-echo -e "${GREEN}🎉 セットアップ完了!${NC}"
-echo -e "${GREEN}================================${NC}"
-echo ""
-echo -e "${BLUE}プロジェクトディレクトリ:${NC} $(pwd)"
-echo ""
-echo -e "${YELLOW}次のステップ:${NC}"
-echo "1. cd $project_name"
-echo "2. ./launch_agents.sh $team \"最初のタスクを記述\""
-echo ""
-echo -e "${GREEN}頑張ってください! 🚀${NC}"
\ No newline at end of file
diff --git a/setup_gcp_tts.sh b/setup_gcp_tts.sh
deleted file mode 100755
index 1be2205..0000000
--- a/setup_gcp_tts.sh
+++ /dev/null
@@ -1,304 +0,0 @@
-#!/bin/bash
-
-# GCP Text-to-Speech セットアップスクリプト
-# 音声生成機能を有効にするための設定
-
-set -e
-
-# カラー定義
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-CYAN='\033[0;36m'
-NC='\033[0m' # No Color
-
-echo -e "${BLUE}================================${NC}"
-echo -e "${BLUE}🔊 GCP Text-to-Speech セットアップ${NC}"
-echo -e "${BLUE}================================${NC}"
-
-CURRENT_DIR=$(pwd)
-CREDENTIALS_DIR="${CURRENT_DIR}/credentials"
-KEY_FILE="${CREDENTIALS_DIR}/gcp-workflow-key.json"
-
-# credentialsディレクトリを作成
-mkdir -p "$CREDENTIALS_DIR"
-
-# ======================
-# 1. gcloud CLI の確認
-# ======================
-echo -e "\n${CYAN}1. gcloud CLI の確認${NC}"
-
-if command -v gcloud &> /dev/null; then
- echo -e "${GREEN}✅ gcloud CLI がインストールされています${NC}"
-
- # 認証状態を確認
- if gcloud auth list --format="value(account)" | grep -q '@'; then
- ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)")
- echo -e "${GREEN}✅ ログイン済み: $ACCOUNT${NC}"
- else
- echo -e "${YELLOW}⚠️ ログインが必要です${NC}"
- echo -e "${YELLOW}実行: gcloud auth login${NC}"
- exit 1
- fi
-
- # プロジェクトIDを確認
- PROJECT_ID=$(gcloud config get-value project 2>/dev/null)
- if [ -z "$PROJECT_ID" ]; then
- echo -e "${RED}❌ プロジェクトが設定されていません${NC}"
- echo "利用可能なプロジェクト:"
- gcloud projects list --format="table(projectId,name)"
- echo ""
- echo -e "${YELLOW}プロジェクトIDを入力してください:${NC}"
- read -p "> " PROJECT_ID
- gcloud config set project "$PROJECT_ID"
- fi
- echo -e "${GREEN}✅ プロジェクト: $PROJECT_ID${NC}"
-else
- echo -e "${RED}❌ gcloud CLI がインストールされていません${NC}"
- echo ""
- echo "インストール方法:"
- echo "1. https://cloud.google.com/sdk/docs/install を開く"
- echo "2. お使いのOSに合わせてインストール"
- echo "3. gcloud init を実行"
- exit 1
-fi
-
-# ======================
-# 2. Text-to-Speech API の有効化
-# ======================
-echo -e "\n${CYAN}2. Text-to-Speech API の有効化${NC}"
-
-# APIが有効か確認
-if gcloud services list --enabled --filter="name:texttospeech.googleapis.com" --format="value(name)" | grep -q "texttospeech"; then
- echo -e "${GREEN}✅ Text-to-Speech API は有効です${NC}"
-else
- echo -e "${YELLOW}Text-to-Speech API を有効化しています...${NC}"
- gcloud services enable texttospeech.googleapis.com
- echo -e "${GREEN}✅ Text-to-Speech API を有効化しました${NC}"
-fi
-
-# ======================
-# 3. サービスアカウントの作成
-# ======================
-echo -e "\n${CYAN}3. サービスアカウントの設定${NC}"
-
-SERVICE_ACCOUNT_NAME="tts-service-account"
-SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
-
-# サービスアカウントの存在確認
-if gcloud iam service-accounts describe "$SERVICE_ACCOUNT_EMAIL" &>/dev/null; then
- echo -e "${GREEN}✅ サービスアカウントは既に存在します${NC}"
-else
- echo -e "${YELLOW}サービスアカウントを作成しています...${NC}"
- gcloud iam service-accounts create "$SERVICE_ACCOUNT_NAME" \
- --display-name="Text-to-Speech Service Account" \
- --description="Service account for TTS API access"
- echo -e "${GREEN}✅ サービスアカウントを作成しました${NC}"
-fi
-
-# ======================
-# 4. 権限の付与
-# ======================
-echo -e "\n${CYAN}4. 権限の付与${NC}"
-
-# Text-to-Speech の権限を付与
-echo -e "${YELLOW}Text-to-Speech の権限を付与しています...${NC}"
-
-gcloud projects add-iam-policy-binding "$PROJECT_ID" \
- --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
- --role="roles/cloudtts.viewer" &>/dev/null || true
-
-echo -e "${GREEN}✅ 権限を付与しました${NC}"
-
-# ======================
-# 5. 認証キーの生成
-# ======================
-echo -e "\n${CYAN}5. 認証キーの生成${NC}"
-
-if [ -f "$KEY_FILE" ]; then
- echo -e "${YELLOW}⚠️ 認証キーファイルが既に存在します: $KEY_FILE${NC}"
- echo -e "${YELLOW}新しいキーを生成しますか? (y/n)${NC}"
- read -p "> " REGENERATE
-
- if [ "$REGENERATE" != "y" ] && [ "$REGENERATE" != "Y" ]; then
- echo -e "${GREEN}既存のキーを使用します${NC}"
- else
- # バックアップを作成
- mv "$KEY_FILE" "${KEY_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
- echo -e "${YELLOW}既存のキーをバックアップしました${NC}"
-
- # 新しいキーを生成
- gcloud iam service-accounts keys create "$KEY_FILE" \
- --iam-account="$SERVICE_ACCOUNT_EMAIL"
- echo -e "${GREEN}✅ 新しい認証キーを生成しました${NC}"
- fi
-else
- echo -e "${YELLOW}認証キーを生成しています...${NC}"
- gcloud iam service-accounts keys create "$KEY_FILE" \
- --iam-account="$SERVICE_ACCOUNT_EMAIL"
- echo -e "${GREEN}✅ 認証キーを生成しました: $KEY_FILE${NC}"
-fi
-
-# ======================
-# 6. 環境変数の設定
-# ======================
-echo -e "\n${CYAN}6. 環境変数の設定${NC}"
-
-# .env ファイルに追加
-if [ -f ".env" ]; then
- if grep -q "GOOGLE_APPLICATION_CREDENTIALS" .env; then
- echo -e "${YELLOW}環境変数は既に設定されています${NC}"
- else
- echo "GOOGLE_APPLICATION_CREDENTIALS=\"$KEY_FILE\"" >> .env
- echo -e "${GREEN}✅ .env ファイルに環境変数を追加しました${NC}"
- fi
-else
- cat > .env << EOF
-# Google Cloud Platform
-GOOGLE_APPLICATION_CREDENTIALS="$KEY_FILE"
-PROJECT_ID="$PROJECT_ID"
-EOF
- echo -e "${GREEN}✅ .env ファイルを作成しました${NC}"
-fi
-
-# ======================
-# 7. Node.js パッケージのインストール
-# ======================
-echo -e "\n${CYAN}7. Node.js パッケージの確認${NC}"
-
-if [ -f "package.json" ]; then
- if grep -q "@google-cloud/text-to-speech" package.json; then
- echo -e "${GREEN}✅ @google-cloud/text-to-speech は既に package.json に含まれています${NC}"
- else
- echo -e "${YELLOW}package.json に依存関係を追加しています...${NC}"
- npm install --save @google-cloud/text-to-speech
- echo -e "${GREEN}✅ 依存関係を追加しました${NC}"
- fi
-else
- echo -e "${YELLOW}⚠️ package.json が見つかりません${NC}"
- echo "プロジェクトで npm install @google-cloud/text-to-speech を実行してください"
-fi
-
-# ======================
-# 8. テスト音声の生成
-# ======================
-echo -e "\n${CYAN}8. テスト音声の生成${NC}"
-
-# テスト用スクリプトを作成
-cat > test_tts.js << 'EOF'
-const textToSpeech = require('@google-cloud/text-to-speech');
-const fs = require('fs');
-const util = require('util');
-
-async function testTTS() {
- const client = new textToSpeech.TextToSpeechClient();
-
- const text = 'こんにちは。Google Cloud Text-to-Speech のテストです。正常に動作しています。';
-
- const request = {
- input: {text: text},
- voice: {
- languageCode: 'ja-JP',
- name: 'ja-JP-Neural2-D',
- ssmlGender: 'NEUTRAL'
- },
- audioConfig: {
- audioEncoding: 'MP3'
- },
- };
-
- try {
- const [response] = await client.synthesizeSpeech(request);
- const writeFile = util.promisify(fs.writeFile);
- await writeFile('test_audio.mp3', response.audioContent, 'binary');
- console.log('✅ テスト音声を生成しました: test_audio.mp3');
- console.log('🔊 音声ファイルを再生して確認してください');
- } catch (error) {
- console.error('❌ エラー:', error.message);
- }
-}
-
-testTTS();
-EOF
-
-echo -e "${YELLOW}テスト音声を生成しますか? (y/n)${NC}"
-read -p "> " TEST_AUDIO
-
-if [ "$TEST_AUDIO" = "y" ] || [ "$TEST_AUDIO" = "Y" ]; then
- if command -v node &> /dev/null; then
- export GOOGLE_APPLICATION_CREDENTIALS="$KEY_FILE"
- node test_tts.js
-
- if [ -f "test_audio.mp3" ]; then
- echo -e "${GREEN}✅ テスト音声の生成に成功しました!${NC}"
-
- # macOSの場合は自動再生
- if [ "$(uname)" = "Darwin" ]; then
- echo -e "${YELLOW}音声を再生しています...${NC}"
- afplay test_audio.mp3 2>/dev/null || true
- fi
- fi
- else
- echo -e "${YELLOW}Node.js がインストールされていないため、テストをスキップします${NC}"
- fi
-fi
-
-# ======================
-# 完了メッセージ
-# ======================
-echo -e "\n${BLUE}================================${NC}"
-echo -e "${GREEN}✅ セットアップ完了!${NC}"
-echo -e "${BLUE}================================${NC}"
-
-echo -e "\n${GREEN}設定内容:${NC}"
-echo -e " プロジェクトID: ${PROJECT_ID}"
-echo -e " サービスアカウント: ${SERVICE_ACCOUNT_EMAIL}"
-echo -e " 認証キー: ${KEY_FILE}"
-
-echo -e "\n${GREEN}使い方:${NC}"
-echo -e " 1. プロジェクトで: npm install @google-cloud/text-to-speech"
-echo -e " 2. スクリプト実行: node generate_audio_gcp.js"
-echo -e " または"
-echo -e " 3. npm run generate-audio:gcp"
-
-echo -e "\n${YELLOW}⚠️ 重要な注意事項:${NC}"
-echo -e " - 認証キーファイル(${KEY_FILE})は機密情報です"
-echo -e " - .gitignore に credentials/ を追加してください"
-echo -e " - キーを他人と共有しないでください"
-
-# .gitignore に追加
-if [ -f ".gitignore" ]; then
- if ! grep -q "credentials/" .gitignore; then
- echo -e "\n# GCP credentials" >> .gitignore
- echo "credentials/" >> .gitignore
- echo "*.json" >> .gitignore
- echo "test_audio.mp3" >> .gitignore
- echo -e "${GREEN}✅ .gitignore に credentials/ を追加しました${NC}"
- fi
-else
- cat > .gitignore << EOF
-# GCP credentials
-credentials/
-*.json
-
-# Audio files
-test_audio.mp3
-explanation.mp3
-
-# Node
-node_modules/
-npm-debug.log*
-
-# Environment
-.env
-.env.local
-
-# OS
-.DS_Store
-Thumbs.db
-EOF
- echo -e "${GREEN}✅ .gitignore を作成しました${NC}"
-fi
-
-echo -e "\n${GREEN}準備が整いました!音声生成機能が利用可能です。${NC}"
\ No newline at end of file
diff --git a/setup_gcp_workflow.sh b/setup_gcp_workflow.sh
deleted file mode 100755
index 24d5ba4..0000000
--- a/setup_gcp_workflow.sh
+++ /dev/null
@@ -1,202 +0,0 @@
-#!/bin/bash
-# GCPワークフロー専用プロジェクトのセットアップスクリプト
-# 使用方法: ./setup_gcp_workflow.sh
-
-set -e
-
-# カラー定義
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-RED='\033[0;31m'
-NC='\033[0m'
-
-echo -e "${BLUE}================================${NC}"
-echo -e "${BLUE}🚀 GCPワークフロー環境セットアップ${NC}"
-echo -e "${BLUE}================================${NC}"
-echo ""
-
-PROJECT_ID="ai-agent-workflow-2024"
-SERVICE_ACCOUNT_NAME="ai-agent-workflow-sa"
-CREDENTIALS_DIR="$HOME/Desktop/git-worktree-agent/credentials"
-KEY_FILE="$CREDENTIALS_DIR/gcp-workflow-key.json"
-
-# Step 1: プロジェクト確認
-echo -e "${YELLOW}Step 1: プロジェクト確認...${NC}"
-gcloud config set project $PROJECT_ID
-echo -e "${GREEN}✅ プロジェクト: $PROJECT_ID${NC}"
-echo ""
-
-# Step 2: 請求先アカウント確認
-echo -e "${YELLOW}Step 2: 請求先アカウント確認...${NC}"
-BILLING_ACCOUNT=$(gcloud billing projects describe $PROJECT_ID --format="value(billingAccountName)" 2>/dev/null || echo "")
-
-if [ -z "$BILLING_ACCOUNT" ]; then
- echo -e "${RED}❌ 請求先アカウントがリンクされていません${NC}"
- echo ""
- echo -e "${YELLOW}以下のURLで請求先アカウントをリンクしてください:${NC}"
- echo "https://console.cloud.google.com/billing/linkedaccount?project=$PROJECT_ID"
- echo ""
- echo "リンク後、このスクリプトを再実行してください。"
- exit 1
-else
- echo -e "${GREEN}✅ 請求先アカウント: $BILLING_ACCOUNT${NC}"
-fi
-echo ""
-
-# Step 3: 必要なAPIを有効化
-echo -e "${YELLOW}Step 3: 必要なAPIを有効化中...${NC}"
-echo "これには数分かかる場合があります..."
-
-# 課金不要のAPIのみ先に有効化
-gcloud services enable \
- cloudresourcemanager.googleapis.com \
- serviceusage.googleapis.com \
- iam.googleapis.com \
- --project=$PROJECT_ID
-
-# 課金が必要なAPIを有効化(請求先アカウントがある場合のみ成功)
-if gcloud services enable \
- aiplatform.googleapis.com \
- texttospeech.googleapis.com \
- storage.googleapis.com \
- --project=$PROJECT_ID 2>/dev/null; then
- echo -e "${GREEN}✅ すべてのAPIを有効化しました${NC}"
-else
- echo -e "${RED}❌ 一部のAPIの有効化に失敗しました(請求先アカウント未設定の可能性)${NC}"
- echo -e "${YELLOW}以下のURLで請求先アカウントをリンクしてください:${NC}"
- echo "https://console.cloud.google.com/billing/linkedaccount?project=$PROJECT_ID"
- echo ""
- echo "リンク後、以下のコマンドでAPIを有効化してください:"
- echo "gcloud services enable aiplatform.googleapis.com texttospeech.googleapis.com storage.googleapis.com --project=$PROJECT_ID"
-fi
-echo ""
-
-# Step 4: サービスアカウント作成
-echo -e "${YELLOW}Step 4: サービスアカウント作成...${NC}"
-
-# 既存確認
-EXISTING_SA=$(gcloud iam service-accounts list \
- --filter="email:$SERVICE_ACCOUNT_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
- --format="value(email)" 2>/dev/null || echo "")
-
-if [ -z "$EXISTING_SA" ]; then
- gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME \
- --display-name="AI Agent Workflow Service Account" \
- --description="ワークフロー実行用のサービスアカウント(画像生成・音声生成)"
- echo -e "${GREEN}✅ サービスアカウント作成完了${NC}"
-else
- echo -e "${GREEN}✅ サービスアカウント既存: $EXISTING_SA${NC}"
-fi
-
-SA_EMAIL="$SERVICE_ACCOUNT_NAME@$PROJECT_ID.iam.gserviceaccount.com"
-echo ""
-
-# Step 5: 必要な権限を付与
-echo -e "${YELLOW}Step 5: 権限付与中...${NC}"
-
-# Vertex AI(Imagen)用
-gcloud projects add-iam-policy-binding $PROJECT_ID \
- --member="serviceAccount:$SA_EMAIL" \
- --role="roles/aiplatform.user" \
- --condition=None
-
-# Text-to-Speech用
-gcloud projects add-iam-policy-binding $PROJECT_ID \
- --member="serviceAccount:$SA_EMAIL" \
- --role="roles/cloudtts.admin" \
- --condition=None
-
-# Storage用(画像/音声の保存)
-gcloud projects add-iam-policy-binding $PROJECT_ID \
- --member="serviceAccount:$SA_EMAIL" \
- --role="roles/storage.objectAdmin" \
- --condition=None
-
-echo -e "${GREEN}✅ 権限付与完了${NC}"
-echo ""
-
-# Step 6: 認証キー作成
-echo -e "${YELLOW}Step 6: 認証キー作成...${NC}"
-
-# credentialsディレクトリ作成
-mkdir -p "$CREDENTIALS_DIR"
-
-# 既存キー削除(再作成)
-if [ -f "$KEY_FILE" ]; then
- echo -e "${YELLOW}⚠️ 既存のキーを削除します${NC}"
- rm "$KEY_FILE"
-fi
-
-# 新しいキー作成
-gcloud iam service-accounts keys create "$KEY_FILE" \
- --iam-account="$SA_EMAIL"
-
-# 権限設定(自分だけ読み書き可能)
-chmod 600 "$KEY_FILE"
-
-echo -e "${GREEN}✅ 認証キー作成完了: $KEY_FILE${NC}"
-echo ""
-
-# Step 7: .envファイル更新
-echo -e "${YELLOW}Step 7: .env設定...${NC}"
-
-ENV_FILE="$HOME/Desktop/git-worktree-agent/.env"
-ENV_TEMPLATE="$HOME/Desktop/git-worktree-agent/.env.template"
-
-# .env.templateをコピー(存在しない場合)
-if [ ! -f "$ENV_FILE" ] && [ -f "$ENV_TEMPLATE" ]; then
- cp "$ENV_TEMPLATE" "$ENV_FILE"
- echo -e "${GREEN}✅ .envファイル作成${NC}"
-fi
-
-# 設定を更新
-if [ -f "$ENV_FILE" ]; then
- # GCPプロジェクトID
- if grep -q "^GCP_PROJECT_ID=" "$ENV_FILE"; then
- sed -i '' "s|^GCP_PROJECT_ID=.*|GCP_PROJECT_ID=$PROJECT_ID|g" "$ENV_FILE"
- else
- echo "GCP_PROJECT_ID=$PROJECT_ID" >> "$ENV_FILE"
- fi
-
- # 認証キーパス
- if grep -q "^GOOGLE_APPLICATION_CREDENTIALS=" "$ENV_FILE"; then
- sed -i '' "s|^GOOGLE_APPLICATION_CREDENTIALS=.*|GOOGLE_APPLICATION_CREDENTIALS=$KEY_FILE|g" "$ENV_FILE"
- else
- echo "GOOGLE_APPLICATION_CREDENTIALS=$KEY_FILE" >> "$ENV_FILE"
- fi
-
- echo -e "${GREEN}✅ .env設定更新完了${NC}"
-else
- echo -e "${YELLOW}⚠️ .envファイルが見つかりません${NC}"
-fi
-echo ""
-
-# Step 8: 動作確認
-echo -e "${YELLOW}Step 8: 動作確認...${NC}"
-
-# 環境変数設定
-export GOOGLE_APPLICATION_CREDENTIALS="$KEY_FILE"
-
-# APIが有効化されているか確認
-echo "有効化されているAPIを確認中..."
-gcloud services list --enabled | grep -E "aiplatform|texttospeech"
-
-echo ""
-echo -e "${BLUE}================================${NC}"
-echo -e "${GREEN}✅ セットアップ完了!${NC}"
-echo -e "${BLUE}================================${NC}"
-echo ""
-echo -e "${GREEN}プロジェクト: $PROJECT_ID${NC}"
-echo -e "${GREEN}サービスアカウント: $SA_EMAIL${NC}"
-echo -e "${GREEN}認証キー: $KEY_FILE${NC}"
-echo ""
-echo -e "${YELLOW}次のステップ:${NC}"
-echo "1. ワークフローを実行してください"
-echo "2. 画像生成・音声生成が自動的に動作します"
-echo ""
-echo -e "${BLUE}コスト目安:${NC}"
-echo " - 画像生成(Imagen): $0.02/枚"
-echo " - 音声生成(TTS): $4/100万文字"
-echo " - 100枚の画像 + 音声: 約$2-3/アプリ"
-echo ""
diff --git a/setup_github_cli_m4.sh b/setup_github_cli_m4.sh
deleted file mode 100755
index 3292681..0000000
--- a/setup_github_cli_m4.sh
+++ /dev/null
@@ -1,125 +0,0 @@
-#!/bin/bash
-# GitHub CLI ARM64版セットアップスクリプト (M4 Mac対応)
-# このスクリプトはM4チップのMacでGitHub CLIをセットアップし、自動プッシュを可能にします
-
-set -e
-
-echo "🚀 GitHub CLI ARM64版セットアップを開始します..."
-
-# 1. 既存のghコマンドを確認
-echo "📋 既存のGitHub CLI設定を確認中..."
-if command -v gh &> /dev/null; then
- echo "⚠️ 既存のghコマンドが見つかりました: $(which gh)"
- gh_version=$(gh --version 2>/dev/null || echo "バージョン取得失敗")
- echo " バージョン: $gh_version"
-fi
-
-# 2. ~/bin ディレクトリを作成
-echo "📁 ~/bin ディレクトリをセットアップ中..."
-mkdir -p ~/bin
-
-# 3. ARM64版のGitHub CLIをダウンロード
-GH_VERSION="2.63.2"
-echo "📦 GitHub CLI v$GH_VERSION (ARM64版) をダウンロード中..."
-
-# 既存ファイルをクリーンアップ
-rm -f /tmp/gh_arm64.zip
-rm -rf /tmp/gh_${GH_VERSION}_macOS_arm64
-
-# ダウンロード
-curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_macOS_arm64.zip" \
- -o /tmp/gh_arm64.zip \
- --progress-bar
-
-# 4. 展開とインストール
-echo "📂 展開中..."
-cd /tmp
-unzip -q gh_arm64.zip
-
-echo "🔧 インストール中..."
-cp /tmp/gh_${GH_VERSION}_macOS_arm64/bin/gh ~/bin/gh
-chmod +x ~/bin/gh
-
-# 5. バージョン確認
-echo "✅ インストール完了!"
-echo " インストール先: ~/bin/gh"
-echo " バージョン: $(~/bin/gh --version)"
-
-# 6. PATHの設定を確認
-if [[ ":$PATH:" != *":$HOME/bin:"* ]]; then
- echo ""
- echo "⚠️ ~/bin がPATHに含まれていません"
- echo "以下のいずれかの方法でPATHに追加してください:"
- echo ""
- echo "# bashの場合 (~/.bash_profile に追加):"
- echo 'export PATH="$HOME/bin:$PATH"'
- echo ""
- echo "# zshの場合 (~/.zshrc に追加):"
- echo 'export PATH="$HOME/bin:$PATH"'
-fi
-
-# 7. 認証状態を確認
-echo ""
-echo "📋 GitHub認証状態を確認中..."
-if ~/bin/gh auth status &> /dev/null; then
- echo "✅ GitHub認証済み"
- ~/bin/gh auth status
-else
- echo "⚠️ GitHub認証が必要です"
- echo ""
- echo "以下のコマンドで認証を設定してください:"
- echo "~/bin/gh auth login"
- echo ""
- echo "推奨設定:"
- echo " - Where do you use GitHub? → GitHub.com"
- echo " - Protocol → SSH"
- echo " - SSH key → 既存のキーを選択 (id_ed25519推奨)"
- echo " - Title → デフォルト (GitHub CLI) またはカスタム名"
- echo " - Authenticate → Login with a web browser"
-fi
-
-# 8. Git設定の更新
-echo ""
-echo "🔧 Git認証ヘルパーを設定中..."
-
-# credential helperスクリプトを作成
-cat > ~/bin/gh-credential-helper.sh << 'EOF'
-#!/bin/bash
-# GitHub CLI credential helper for M4 Mac
-exec ~/bin/gh auth git-credential "$@"
-EOF
-
-chmod +x ~/bin/gh-credential-helper.sh
-
-# グローバルGit設定を更新
-echo " Gitグローバル設定を更新中..."
-/usr/bin/git config --global --replace-all credential.https://github.com.helper "!~/bin/gh-credential-helper.sh"
-
-echo "✅ Git認証ヘルパー設定完了"
-
-# 9. クリーンアップ
-echo ""
-echo "🧹 一時ファイルをクリーンアップ中..."
-rm -f /tmp/gh_arm64.zip
-rm -rf /tmp/gh_${GH_VERSION}_macOS_arm64
-
-echo ""
-echo "========================================="
-echo "✅ セットアップ完了!"
-echo "========================================="
-echo ""
-echo "📝 次のステップ:"
-
-if ! ~/bin/gh auth status &> /dev/null; then
- echo "1. GitHub認証を設定:"
- echo " ~/bin/gh auth login"
-else
- echo "1. ✅ GitHub認証済み"
-fi
-
-echo ""
-echo "2. 自動プッシュをテスト:"
-echo " cd {your-repo}"
-echo " /usr/bin/git push origin main"
-echo ""
-echo "========================================="
\ No newline at end of file
diff --git a/src/__pycache__/autonomous_evaluator.cpython-311.pyc b/src/__pycache__/autonomous_evaluator.cpython-311.pyc
deleted file mode 100755
index 4040d13..0000000
Binary files a/src/__pycache__/autonomous_evaluator.cpython-311.pyc and /dev/null differ
diff --git a/src/__pycache__/autonomous_evaluator_ux.cpython-311.pyc b/src/__pycache__/autonomous_evaluator_ux.cpython-311.pyc
deleted file mode 100755
index 5d040a0..0000000
Binary files a/src/__pycache__/autonomous_evaluator_ux.cpython-311.pyc and /dev/null differ
diff --git a/src/__pycache__/delivery_organizer.cpython-312.pyc b/src/__pycache__/delivery_organizer.cpython-312.pyc
deleted file mode 100644
index b5658ea..0000000
Binary files a/src/__pycache__/delivery_organizer.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/documentation_generator.cpython-311.pyc b/src/__pycache__/documentation_generator.cpython-311.pyc
deleted file mode 100755
index a172048..0000000
Binary files a/src/__pycache__/documentation_generator.cpython-311.pyc and /dev/null differ
diff --git a/src/__pycache__/documenter_agent.cpython-312.pyc b/src/__pycache__/documenter_agent.cpython-312.pyc
deleted file mode 100644
index a9252cc..0000000
Binary files a/src/__pycache__/documenter_agent.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/github_publisher.cpython-312.pyc b/src/__pycache__/github_publisher.cpython-312.pyc
deleted file mode 100644
index c4c9ae0..0000000
Binary files a/src/__pycache__/github_publisher.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/portfolio_config.cpython-312.pyc b/src/__pycache__/portfolio_config.cpython-312.pyc
deleted file mode 100644
index 78f05c7..0000000
Binary files a/src/__pycache__/portfolio_config.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/publish_portfolio.cpython-312.pyc b/src/__pycache__/publish_portfolio.cpython-312.pyc
deleted file mode 100644
index ce03503..0000000
Binary files a/src/__pycache__/publish_portfolio.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/requirements_gatherer.cpython-311.pyc b/src/__pycache__/requirements_gatherer.cpython-311.pyc
deleted file mode 100755
index a5e5a1e..0000000
Binary files a/src/__pycache__/requirements_gatherer.cpython-311.pyc and /dev/null differ
diff --git a/src/__pycache__/security_checker.cpython-312.pyc b/src/__pycache__/security_checker.cpython-312.pyc
deleted file mode 100644
index fda5071..0000000
Binary files a/src/__pycache__/security_checker.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/tts_smart_generator.cpython-311.pyc b/src/__pycache__/tts_smart_generator.cpython-311.pyc
deleted file mode 100755
index f298b2a..0000000
Binary files a/src/__pycache__/tts_smart_generator.cpython-311.pyc and /dev/null differ
diff --git a/src/__pycache__/tts_smart_generator.cpython-312.pyc b/src/__pycache__/tts_smart_generator.cpython-312.pyc
deleted file mode 100644
index 091370a..0000000
Binary files a/src/__pycache__/tts_smart_generator.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/workflow_state_manager.cpython-312.pyc b/src/__pycache__/workflow_state_manager.cpython-312.pyc
deleted file mode 100644
index 38fe9f5..0000000
Binary files a/src/__pycache__/workflow_state_manager.cpython-312.pyc and /dev/null differ
diff --git a/src/app_name_translator.py b/src/app_name_translator.py
deleted file mode 100755
index 7b56fd7..0000000
--- a/src/app_name_translator.py
+++ /dev/null
@@ -1,445 +0,0 @@
-#!/usr/bin/env python3
-"""
-🌐 アプリ名翻訳ツール
-日本語のアプリ名を英語のslug形式に変換します。
-
-使用方法:
- python3 app_name_translator.py "タスク管理アプリ"
- → task-manager
-
- python3 app_name_translator.py "シューティングゲーム"
- → shooting-game
-
-Claude API を使用して自然な英語名に変換します。
-"""
-
-import os
-import sys
-import re
-import json
-from pathlib import Path
-
-def load_api_key() -> str:
- """ANTHROPIC_API_KEY を取得(複数ソースから探索)"""
-
- # 1. 環境変数
- api_key = os.environ.get('ANTHROPIC_API_KEY')
- if api_key:
- return api_key
-
- # 2. グローバル設定ファイル(~/.config/ai-agents/profiles/default.env)
- global_env = Path.home() / ".config" / "ai-agents" / "profiles" / "default.env"
- if global_env.exists():
- try:
- with open(global_env, 'r') as f:
- for line in f:
- if line.startswith('ANTHROPIC_API_KEY='):
- return line.split('=', 1)[1].strip().strip('"').strip("'")
- except:
- pass
-
- # 3. ローカル .env ファイル
- local_env = Path('.env')
- if local_env.exists():
- try:
- with open(local_env, 'r') as f:
- for line in f:
- if line.startswith('ANTHROPIC_API_KEY='):
- return line.split('=', 1)[1].strip().strip('"').strip("'")
- except:
- pass
-
- # 4. ホームディレクトリの .env
- home_env = Path.home() / '.env'
- if home_env.exists():
- try:
- with open(home_env, 'r') as f:
- for line in f:
- if line.startswith('ANTHROPIC_API_KEY='):
- return line.split('=', 1)[1].strip().strip('"').strip("'")
- except:
- pass
-
- return None
-
-
-def is_japanese(text: str) -> bool:
- """テキストに日本語が含まれているかチェック"""
- # ひらがな、カタカナ、漢字のUnicode範囲
- japanese_pattern = re.compile(r'[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF]')
- return bool(japanese_pattern.search(text))
-
-
-def translate_with_claude(japanese_name: str, api_key: str) -> dict:
- """Claude API を使って日本語を英語のアプリ名に変換"""
- import urllib.request
- import urllib.error
-
- prompt = f"""以下の日本語のアプリ/ゲーム名を、英語のslug形式(ハイフン区切り、小文字)に変換してください。
-
-日本語名: {japanese_name}
-
-要件:
-1. 意味を保持した自然な英語に翻訳
-2. slug形式(小文字、ハイフン区切り、英数字のみ)
-3. 簡潔で分かりやすい名前(2-4語程度)
-4. 一般的なアプリ/ゲーム名として自然
-
-出力形式(JSON):
-{{
- "english_name": "Task Manager",
- "slug": "task-manager",
- "alternatives": ["todo-app", "task-tracker"]
-}}
-
-JSONのみを出力してください。説明は不要です。"""
-
- request_body = {
- "model": "claude-sonnet-4-20250514",
- "max_tokens": 256,
- "messages": [
- {"role": "user", "content": prompt}
- ]
- }
-
- headers = {
- "Content-Type": "application/json",
- "x-api-key": api_key,
- "anthropic-version": "2023-06-01"
- }
-
- try:
- req = urllib.request.Request(
- "https://api.anthropic.com/v1/messages",
- data=json.dumps(request_body).encode('utf-8'),
- headers=headers,
- method='POST'
- )
-
- with urllib.request.urlopen(req, timeout=30) as response:
- result = json.loads(response.read().decode('utf-8'))
- content = result['content'][0]['text']
-
- # JSONを抽出(コードブロックがある場合に対応)
- if '```json' in content:
- content = content.split('```json')[1].split('```')[0]
- elif '```' in content:
- content = content.split('```')[1].split('```')[0]
-
- return json.loads(content.strip())
-
- except urllib.error.HTTPError as e:
- error_body = e.read().decode('utf-8')
- raise Exception(f"API Error ({e.code}): {error_body}")
- except json.JSONDecodeError as e:
- raise Exception(f"JSON Parse Error: {content}")
-
-
-def simple_transliterate(japanese_name: str) -> str:
- """簡易的なローマ字変換(API使用不可時のフォールバック)"""
- # 一般的なアプリ名のマッピング(日本語→英語+マーカー)
- # マーカー(--)を使って単語境界を示す
- common_mappings = {
- # 基本
- 'タスク': 'task',
- '管理': 'manager',
- 'アプリ': 'app',
- 'ゲーム': 'game',
- 'ツール': 'tool',
- 'システム': 'system',
- 'サービス': 'service',
-
- # 乗り物・レース
- '車': 'car',
- 'カー': 'car',
- '自動車': 'car',
- 'レース': 'race',
- '競争': 'race',
- '競走': 'race',
- 'レーシング': 'racing',
- 'ドライブ': 'drive',
- 'ドライビング': 'driving',
- '運転': 'driving',
- 'バイク': 'bike',
- 'オートバイ': 'motorcycle',
- '自転車': 'bicycle',
- '電車': 'train',
- '飛行機': 'airplane',
- 'ヘリコプター': 'helicopter',
- '船': 'ship',
- 'ボート': 'boat',
- 'ロケット': 'rocket',
-
- # ゲームジャンル
- 'シューティング': 'shooting',
- 'パズル': 'puzzle',
- 'クイズ': 'quiz',
- 'RPG': 'rpg',
- 'アクション': 'action',
- 'アドベンチャー': 'adventure',
- 'ストラテジー': 'strategy',
- '戦略': 'strategy',
- 'シミュレーション': 'simulation',
- 'スポーツ': 'sports',
- 'カード': 'card',
- 'ボード': 'board',
- 'パーティー': 'party',
- 'マルチプレイヤー': 'multiplayer',
- 'オンライン': 'online',
- 'オフライン': 'offline',
- 'ミニ': 'mini',
- 'アーケード': 'arcade',
- 'レトロ': 'retro',
- 'ピクセル': 'pixel',
- 'ドット': 'pixel',
- 'クリッカー': 'clicker',
- 'アイドル': 'idle',
- '放置': 'idle',
- 'マージ': 'merge',
- '合体': 'merge',
- 'マッチ': 'match',
- 'ブロック': 'block',
- 'テトリス': 'tetris',
- 'ソリティア': 'solitaire',
- '麻雀': 'mahjong',
- '将棋': 'shogi',
- '囲碁': 'go',
- 'チェス': 'chess',
- 'オセロ': 'othello',
- 'ホラー': 'horror',
- '恐怖': 'horror',
- 'ミステリー': 'mystery',
- '謎': 'mystery',
- 'サバイバル': 'survival',
- '生存': 'survival',
- 'ディフェンス': 'defense',
- '防衛': 'defense',
- 'タワー': 'tower',
- 'ランナー': 'runner',
- 'ジャンプ': 'jump',
- 'フライト': 'flight',
- '飛行': 'flight',
- 'ダイビング': 'diving',
-
- # 宇宙・SF
- '宇宙': 'space',
- 'スペース': 'space',
- '侵略者': 'invaders',
- 'インベーダー': 'invaders',
- 'エイリアン': 'alien',
- '宇宙人': 'alien',
- 'ロボット': 'robot',
- 'メカ': 'mecha',
- '未来': 'future',
- 'サイバー': 'cyber',
-
- # 動物・自然
- '動物': 'animal',
- 'アニマル': 'animal',
- '犬': 'dog',
- '猫': 'cat',
- '鳥': 'bird',
- '魚': 'fish',
- 'ドラゴン': 'dragon',
- '竜': 'dragon',
- 'モンスター': 'monster',
- '怪獣': 'monster',
- '恐竜': 'dinosaur',
- '森': 'forest',
- '海': 'ocean',
- '山': 'mountain',
- '川': 'river',
- '空': 'sky',
- '島': 'island',
- '世界': 'world',
- '王国': 'kingdom',
- '城': 'castle',
- 'ダンジョン': 'dungeon',
- '迷宮': 'maze',
-
- # 戦闘・アクション
- '戦い': 'battle',
- 'バトル': 'battle',
- '戦争': 'war',
- 'ウォー': 'war',
- '戦士': 'warrior',
- 'ウォリアー': 'warrior',
- '勇者': 'hero',
- 'ヒーロー': 'hero',
- '冒険': 'adventure',
- '冒険者': 'adventurer',
- '剣': 'sword',
- '魔法': 'magic',
- 'マジック': 'magic',
- '忍者': 'ninja',
- '侍': 'samurai',
- '騎士': 'knight',
- '海賊': 'pirate',
-
- # 日常アプリ
- 'チャット': 'chat',
- 'メモ': 'memo',
- 'ノート': 'note',
- '計算': 'calc',
- '電卓': 'calculator',
- 'カレンダー': 'calendar',
- '天気': 'weather',
- '音楽': 'music',
- '写真': 'photo',
- '動画': 'video',
- 'ニュース': 'news',
- 'ショッピング': 'shopping',
- '買い物': 'shopping',
- 'レシピ': 'recipe',
- '料理': 'cooking',
- '健康': 'health',
- '運動': 'fitness',
- '睡眠': 'sleep',
- '日記': 'diary',
- '家計簿': 'budget',
- 'TODO': 'todo',
- 'やること': 'todo',
- 'リスト': 'list',
- 'トラッカー': 'tracker',
- '追跡': 'tracker',
- 'ボット': 'bot',
- 'AI': 'ai',
- 'ポートフォリオ': 'portfolio',
- 'ブログ': 'blog',
- 'SNS': 'social',
- '翻訳': 'translator',
- '辞書': 'dictionary',
- '学習': 'learning',
- '勉強': 'study',
- '英語': 'english',
- '数学': 'math',
- 'プログラミング': 'coding',
- 'コード': 'code',
- 'エディタ': 'editor',
- 'ビューア': 'viewer',
- 'プレイヤー': 'player',
- 'ブラウザ': 'browser',
- 'ランチャー': 'launcher',
- 'ウィジェット': 'widget',
- 'ダッシュボード': 'dashboard',
- 'モニター': 'monitor',
- 'アナライザー': 'analyzer',
- '分析': 'analytics',
- 'レポート': 'report',
- 'チャート': 'chart',
- 'グラフ': 'graph',
- 'マップ': 'map',
- '地図': 'map',
- 'ナビ': 'navi',
- '検索': 'search',
- 'ファインダー': 'finder',
- 'スキャナー': 'scanner',
- 'コンバーター': 'converter',
- '変換': 'converter',
- 'ジェネレーター': 'generator',
- '生成': 'generator',
- 'シミュレーター': 'simulator',
- 'エミュレーター': 'emulator',
- 'テスター': 'tester',
- 'デバッガー': 'debugger',
- 'ロガー': 'logger',
- 'バックアップ': 'backup',
- 'シンク': 'sync',
- '同期': 'sync',
- 'クラウド': 'cloud',
- 'ストレージ': 'storage',
- 'ファイル': 'file',
- 'フォルダ': 'folder',
- 'ドキュメント': 'docs',
- 'スプレッドシート': 'spreadsheet',
- 'プレゼン': 'slides',
- 'スライド': 'slides',
- 'ホワイトボード': 'whiteboard',
- 'ノートブック': 'notebook',
- 'ジャーナル': 'journal',
- 'タイマー': 'timer',
- 'ストップウォッチ': 'stopwatch',
- 'アラーム': 'alarm',
- 'リマインダー': 'reminder',
- 'スケジューラー': 'scheduler',
- 'プランナー': 'planner',
- 'オーガナイザー': 'organizer',
- 'ポモドーロ': 'pomodoro',
- 'フォーカス': 'focus',
- '集中': 'focus',
- }
-
- result = japanese_name
-
- # 長いキーワードから順にマッチさせるためソート
- sorted_mappings = sorted(common_mappings.items(), key=lambda x: len(x[0]), reverse=True)
-
- # 各日本語キーワードを英語に置換(境界マーカー付き)
- for jp, en in sorted_mappings:
- # 大文字小文字を無視してマッチ
- pattern = re.compile(re.escape(jp), re.IGNORECASE)
- result = pattern.sub(f'-{en}-', result)
-
- # 残った日本語文字を削除
- result = re.sub(r'[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF]', '', result)
-
- # 小文字化
- result = result.lower()
-
- # slug形式に正規化(連続ハイフンを1つに、先頭末尾のハイフンを削除)
- result = re.sub(r'[^a-z0-9]+', '-', result)
- result = re.sub(r'-+', '-', result)
- result = result.strip('-')
-
- return result if result else 'my-app'
-
-
-def main():
- if len(sys.argv) < 2:
- print("使用方法: python3 app_name_translator.py <アプリ名>", file=sys.stderr)
- sys.exit(1)
-
- input_name = ' '.join(sys.argv[1:])
-
- # 日本語チェック
- if not is_japanese(input_name):
- # 既に英語の場合はslug変換のみ
- slug = input_name.lower()
- slug = re.sub(r'[^a-z0-9]+', '-', slug)
- slug = re.sub(r'-+', '-', slug)
- slug = slug.strip('-')
- print(json.dumps({
- "original": input_name,
- "english_name": input_name,
- "slug": slug,
- "is_translated": False
- }))
- sys.exit(0)
-
- # API キーを取得
- api_key = load_api_key()
-
- if api_key:
- try:
- result = translate_with_claude(input_name, api_key)
- result["original"] = input_name
- result["is_translated"] = True
- print(json.dumps(result, ensure_ascii=False))
- sys.exit(0)
- except Exception as e:
- print(f"API Error: {e}", file=sys.stderr)
- # フォールバックへ
-
- # フォールバック: 簡易変換
- slug = simple_transliterate(input_name)
- print(json.dumps({
- "original": input_name,
- "english_name": slug.replace('-', ' ').title(),
- "slug": slug,
- "is_translated": True,
- "fallback": True
- }, ensure_ascii=False))
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/audio_generator_lyria.py b/src/audio_generator_lyria.py
deleted file mode 100755
index b6563c8..0000000
--- a/src/audio_generator_lyria.py
+++ /dev/null
@@ -1,415 +0,0 @@
-#!/usr/bin/env python3
-"""
-Lyria Audio Generator - ゲーム効果音・BGM自動生成
-
-Google Cloud Vertex AI の Lyria モデルを使用して、
-ゲーム用のBGMと効果音を自動生成します。
-
-使用方法:
- python3 audio_generator_lyria.py AUDIO_PROMPTS.json
-
-必要な環境:
- - GCP認証: ~/Desktop/git-worktree-agent/credentials/gcp-workflow-key.json
- - AUDIO_PROMPTS.json: 音声生成プロンプト定義
-"""
-
-import json
-import os
-import sys
-import time
-import base64
-from pathlib import Path
-from typing import Dict, List, Any, Optional
-import subprocess
-
-class LyriaAudioGenerator:
- """Vertex AI Lyria を使用した音声生成"""
-
- def __init__(self, credentials_path: str):
- """
- 初期化
-
- Args:
- credentials_path: GCPサービスアカウントキーのパス
- """
- self.credentials_path = credentials_path
- self.project_id = None
- self.location = "us-central1" # Lyria利用可能リージョン
- self.endpoint = f"https://{self.location}-aiplatform.googleapis.com"
-
- # GCP認証設定
- os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credentials_path
-
- # プロジェクトID取得
- self._setup_project()
-
- def _setup_project(self):
- """GCPプロジェクトのセットアップ"""
- try:
- # credentials JSONからproject_id取得
- with open(self.credentials_path) as f:
- creds = json.load(f)
- self.project_id = creds.get("project_id")
-
- if not self.project_id:
- raise ValueError("project_id not found in credentials")
-
- print(f"✅ GCPプロジェクト: {self.project_id}")
-
- except Exception as e:
- print(f"❌ GCPプロジェクト設定エラー: {e}")
- raise
-
- def _call_lyria_api(self, prompt: str, negative_prompt: str = "",
- bpm: int = 120, duration_seconds: int = 30) -> Optional[bytes]:
- """
- Lyria API呼び出し
-
- Args:
- prompt: 生成プロンプト
- negative_prompt: ネガティブプロンプト
- bpm: BPM (60-200)
- duration_seconds: 生成時間(秒)※実際は30秒固定
-
- Returns:
- 生成された音声データ(WAVバイナリ)
- """
- try:
- # Vertex AI Lyria APIエンドポイント
- model = "lyria-002"
- endpoint_path = f"projects/{self.project_id}/locations/{self.location}/publishers/google/models/{model}"
-
- # リクエストボディ
- request_body = {
- "instances": [{
- "prompt": prompt,
- "negative_prompt": negative_prompt,
- "sample_count": 1,
- "guidance": 3.0, # プロンプト強度(0.0-6.0)
- "bpm": bpm,
- "seed": int(time.time()) # ランダムシード
- }]
- }
-
- # curlコマンドでAPI呼び出し(google-cloud-aiplatformパッケージ不要)
- access_token = self._get_access_token()
-
- curl_command = [
- "curl",
- "-X", "POST",
- "-H", f"Authorization: Bearer {access_token}",
- "-H", "Content-Type: application/json",
- f"{self.endpoint}/v1/{endpoint_path}:predict",
- "-d", json.dumps(request_body)
- ]
-
- result = subprocess.run(
- curl_command,
- capture_output=True,
- text=True,
- timeout=120 # 2分タイムアウト
- )
-
- if result.returncode != 0:
- print(f"❌ API呼び出しエラー: {result.stderr}")
- return None
-
- # レスポンス解析
- response = json.loads(result.stdout)
-
- if "predictions" not in response:
- print(f"❌ APIレスポンスエラー: {response}")
- return None
-
- # Base64デコードして音声データ取得
- audio_b64 = response["predictions"][0].get("audioContent")
- if not audio_b64:
- print("❌ 音声データが含まれていません")
- return None
-
- audio_bytes = base64.b64decode(audio_b64)
-
- print(f"✅ 音声生成成功: {len(audio_bytes)} bytes")
- return audio_bytes
-
- except subprocess.TimeoutExpired:
- print("❌ API呼び出しタイムアウト(120秒)")
- return None
- except Exception as e:
- print(f"❌ Lyria API呼び出しエラー: {e}")
- return None
-
- def _get_access_token(self) -> str:
- """GCPアクセストークン取得"""
- try:
- result = subprocess.run(
- ["gcloud", "auth", "application-default", "print-access-token"],
- capture_output=True,
- text=True,
- timeout=10
- )
-
- if result.returncode != 0:
- raise Exception(f"gcloud auth failed: {result.stderr}")
-
- return result.stdout.strip()
-
- except Exception as e:
- print(f"❌ アクセストークン取得エラー: {e}")
- print("⚠️ 'gcloud auth application-default login' を実行してください")
- raise
-
- def generate_bgm(self, name: str, prompt: str, negative_prompt: str = "",
- duration: int = 30, bpm: int = 120, output_file: str = None) -> bool:
- """
- BGM生成
-
- Args:
- name: BGM名
- prompt: 生成プロンプト
- negative_prompt: ネガティブプロンプト
- duration: 時間(秒)※Lyriaは30秒固定
- bpm: BPM
- output_file: 出力ファイルパス
-
- Returns:
- 成功/失敗
- """
- print(f"\n🎵 BGM生成中: {name}")
- print(f" プロンプト: {prompt}")
- print(f" BPM: {bpm}, 時間: {duration}秒")
-
- # Lyria APIは30秒固定
- if duration != 30:
- print(f"⚠️ Lyriaは30秒固定です(指定: {duration}秒)")
-
- audio_data = self._call_lyria_api(
- prompt=prompt,
- negative_prompt=negative_prompt,
- bpm=bpm,
- duration_seconds=30
- )
-
- if audio_data and output_file:
- self._save_audio(audio_data, output_file)
- print(f"✅ BGM保存: {output_file}")
- return True
-
- return False
-
- def generate_sfx(self, name: str, prompt: str, duration: int = 1,
- output_file: str = None) -> bool:
- """
- 効果音生成
-
- Args:
- name: 効果音名
- prompt: 生成プロンプト
- duration: 時間(秒)※短い音でも30秒課金
- output_file: 出力ファイルパス
-
- Returns:
- 成功/失敗
- """
- print(f"\n🔊 効果音生成中: {name}")
- print(f" プロンプト: {prompt}")
- print(f" 時間: {duration}秒")
-
- # 短い音用にプロンプト調整
- short_prompt = f"{prompt}, very short sound effect, {duration} seconds duration, isolated sound"
-
- audio_data = self._call_lyria_api(
- prompt=short_prompt,
- negative_prompt="background music, melody, harmony, long duration",
- bpm=120,
- duration_seconds=30 # Lyriaは30秒固定
- )
-
- if audio_data and output_file:
- # TODO: 短い音の場合、30秒の音声から最初のN秒を切り出す処理
- # 現在は30秒全体を保存(後でトリミング可能)
- self._save_audio(audio_data, output_file)
- print(f"✅ 効果音保存: {output_file} (30秒生成、要トリミング)")
- return True
-
- return False
-
- def generate_from_prompts_file(self, prompts_file: str, base_dir: str = ".") -> Dict[str, Any]:
- """
- AUDIO_PROMPTS.json から一括生成
-
- Args:
- prompts_file: AUDIO_PROMPTS.jsonのパス
- base_dir: 基準ディレクトリ(相対パス解決用)
-
- Returns:
- 生成結果サマリー
- """
- print(f"\n{'='*60}")
- print(f"🎵 AUDIO_PROMPTS.json から音声生成開始")
- print(f"{'='*60}")
-
- try:
- with open(prompts_file) as f:
- prompts = json.load(f)
- except Exception as e:
- print(f"❌ AUDIO_PROMPTS.json 読み込みエラー: {e}")
- return {"success": False, "error": str(e)}
-
- project_name = prompts.get("project_name", "Unknown")
- print(f"\nプロジェクト: {project_name}")
-
- results = {
- "project_name": project_name,
- "bgm": {"total": 0, "success": 0, "failed": 0, "files": []},
- "sfx": {"total": 0, "success": 0, "failed": 0, "files": []},
- "cost": 0.0
- }
-
- # BGM生成
- bgm_list = prompts.get("bgm", [])
- results["bgm"]["total"] = len(bgm_list)
-
- for bgm in bgm_list:
- output_path = os.path.join(base_dir, bgm["file"])
- success = self.generate_bgm(
- name=bgm["name"],
- prompt=bgm["prompt"],
- negative_prompt=bgm.get("negative_prompt", ""),
- duration=bgm.get("duration", 30),
- bpm=bgm.get("bpm", 120),
- output_file=output_path
- )
-
- if success:
- results["bgm"]["success"] += 1
- results["bgm"]["files"].append(output_path)
- results["cost"] += 0.06 # $0.06/30秒
- else:
- results["bgm"]["failed"] += 1
-
- # クォータ対策(2秒待機)
- time.sleep(2)
-
- # SFX生成
- sfx_list = prompts.get("sfx", [])
- results["sfx"]["total"] = len(sfx_list)
-
- for sfx in sfx_list:
- output_path = os.path.join(base_dir, sfx["file"])
- success = self.generate_sfx(
- name=sfx["name"],
- prompt=sfx["prompt"],
- duration=sfx.get("duration", 1),
- output_file=output_path
- )
-
- if success:
- results["sfx"]["success"] += 1
- results["sfx"]["files"].append(output_path)
- results["cost"] += 0.06 # 短い音でも30秒分課金
- else:
- results["sfx"]["failed"] += 1
-
- # クォータ対策(2秒待機)
- time.sleep(2)
-
- # サマリー表示
- self._print_summary(results)
-
- return results
-
- def _save_audio(self, audio_data: bytes, file_path: str):
- """音声ファイル保存"""
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
- with open(file_path, "wb") as f:
- f.write(audio_data)
-
- def _print_summary(self, results: Dict[str, Any]):
- """生成結果サマリー表示"""
- print(f"\n{'='*60}")
- print(f"🎉 音声生成完了サマリー")
- print(f"{'='*60}")
- print(f"\nプロジェクト: {results['project_name']}")
-
- print(f"\n🎵 BGM:")
- print(f" 合計: {results['bgm']['total']}")
- print(f" 成功: {results['bgm']['success']}")
- print(f" 失敗: {results['bgm']['failed']}")
-
- print(f"\n🔊 効果音:")
- print(f" 合計: {results['sfx']['total']}")
- print(f" 成功: {results['sfx']['success']}")
- print(f" 失敗: {results['sfx']['failed']}")
-
- print(f"\n💰 推定コスト: ${results['cost']:.2f}")
-
- print(f"\n📁 生成ファイル:")
- for file in results['bgm']['files'] + results['sfx']['files']:
- print(f" ✅ {file}")
-
- print(f"\n{'='*60}")
-
-
-def main():
- """メイン処理"""
- if len(sys.argv) < 2:
- print("使用方法: python3 audio_generator_lyria.py AUDIO_PROMPTS.json")
- sys.exit(1)
-
- prompts_file = sys.argv[1]
-
- # GCP認証ファイル確認
- credentials_path = os.path.expanduser(
- "~/Desktop/git-worktree-agent/credentials/gcp-workflow-key.json"
- )
-
- if not os.path.exists(credentials_path):
- print(f"❌ GCP認証ファイルが見つかりません: {credentials_path}")
- print("\n以下の手順でGCP認証を設定してください:")
- print("1. Google Cloud ConsoleでVertex AI APIを有効化")
- print("2. サービスアカウントキーを作成")
- print("3. ~/Desktop/git-worktree-agent/credentials/gcp-workflow-key.json に配置")
- sys.exit(1)
-
- if not os.path.exists(prompts_file):
- print(f"❌ AUDIO_PROMPTS.json が見つかりません: {prompts_file}")
- sys.exit(1)
-
- # gcloud認証確認
- print("🔐 GCP認証確認中...")
- try:
- result = subprocess.run(
- ["gcloud", "auth", "application-default", "print-access-token"],
- capture_output=True,
- text=True,
- timeout=10
- )
- if result.returncode != 0:
- print("⚠️ gcloud認証が必要です")
- print(" 'gcloud auth application-default login' を実行してください")
- sys.exit(1)
- except Exception as e:
- print(f"❌ gcloud確認エラー: {e}")
- sys.exit(1)
-
- # 音声生成実行
- generator = LyriaAudioGenerator(credentials_path)
-
- base_dir = os.path.dirname(prompts_file)
- results = generator.generate_from_prompts_file(prompts_file, base_dir)
-
- # 結果をJSONで保存
- results_file = os.path.join(base_dir, "audio_generation_results.json")
- with open(results_file, "w") as f:
- json.dump(results, f, indent=2, ensure_ascii=False)
-
- print(f"\n📊 詳細結果: {results_file}")
-
- # 失敗がある場合は終了コード1
- total_failed = results["bgm"]["failed"] + results["sfx"]["failed"]
- sys.exit(0 if total_failed == 0 else 1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/autonomous_evaluator.py b/src/autonomous_evaluator.py
deleted file mode 100755
index 8f370b2..0000000
--- a/src/autonomous_evaluator.py
+++ /dev/null
@@ -1,589 +0,0 @@
-#!/usr/bin/env python3
-"""
-自律評価システム - Phase別worktreeの自動評価・選択
-
-複数のworktreeを自動的に評価し、最良のものを選択する
-"""
-
-import json
-import os
-import subprocess
-from pathlib import Path
-from typing import Dict, List, Optional
-from dataclasses import dataclass
-import logging
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-@dataclass
-class EvaluationCriteria:
- """評価基準"""
- test_pass_rate: float = 0.30 # テスト合格率の重み
- code_quality: float = 0.25 # コード品質の重み
- performance: float = 0.20 # パフォーマンスの重み
- security: float = 0.15 # セキュリティの重み
- simplicity: float = 0.10 # シンプルさの重み
-
-
-@dataclass
-class WorktreeScore:
- """worktreeの評価スコア"""
- worktree_path: str
- total_score: float
- test_pass_rate: float
- code_quality: float
- performance: float
- security: float
- simplicity: float
- details: Dict
-
-
-class AutonomousEvaluator:
- """自律評価システム"""
-
- def __init__(self, project_path: Path):
- self.project_path = Path(project_path)
- self.worktrees_dir = self.project_path / "worktrees"
-
- def evaluate_test_pass_rate(self, worktree_path: Path) -> float:
- """
- テスト合格率を評価
-
- Returns:
- float: スコア(0-100)
- """
- try:
- # テスト結果ファイルを確認
- test_result_file = worktree_path / "test-results.json"
-
- if not test_result_file.exists():
- # テストを実行
- result = subprocess.run(
- ["npm", "test", "--", "--json"],
- cwd=worktree_path,
- capture_output=True,
- text=True,
- timeout=300
- )
-
- if result.returncode == 0:
- test_data = json.loads(result.stdout)
- total = test_data.get('numTotalTests', 0)
- passed = test_data.get('numPassedTests', 0)
-
- if total > 0:
- pass_rate = (passed / total) * 100
- logger.info(f"✅ Test pass rate: {pass_rate:.1f}% ({passed}/{total})")
- return pass_rate
- else:
- logger.warning("⚠️ No tests found")
- return 50.0 # テストがない場合は中間スコア
- else:
- logger.warning(f"⚠️ Test execution failed: {result.stderr}")
- return 0.0
-
- else:
- # 既存の結果を読み込み
- with open(test_result_file) as f:
- test_data = json.load(f)
- total = test_data.get('numTotalTests', 0)
- passed = test_data.get('numPassedTests', 0)
- if total > 0:
- return (passed / total) * 100
- else:
- return 50.0
-
- except Exception as e:
- logger.error(f"❌ Error evaluating tests: {e}")
- return 0.0
-
- def evaluate_code_quality(self, worktree_path: Path) -> float:
- """
- コード品質を評価(静的解析)
-
- Returns:
- float: スコア(0-100)
- """
- try:
- # ESLintまたはPylintなどを実行
- result = subprocess.run(
- ["npx", "eslint", "src/", "--format", "json"],
- cwd=worktree_path,
- capture_output=True,
- text=True,
- timeout=60
- )
-
- if result.stdout:
- lint_data = json.loads(result.stdout)
- total_issues = sum(
- len(file.get('messages', []))
- for file in lint_data
- )
-
- # ファイル数を取得
- src_files = list((worktree_path / "src").rglob("*.js")) + \
- list((worktree_path / "src").rglob("*.ts"))
- num_files = len(src_files)
-
- if num_files > 0:
- issues_per_file = total_issues / num_files
- # 1ファイルあたり5問題以下なら高スコア
- score = max(0, 100 - (issues_per_file * 10))
- logger.info(f"✅ Code quality score: {score:.1f} (issues: {total_issues})")
- return score
- else:
- return 70.0 # デフォルト
-
- else:
- logger.warning("⚠️ Linting skipped (no output)")
- return 70.0
-
- except Exception as e:
- logger.warning(f"⚠️ Code quality check failed: {e}")
- return 70.0 # エラー時はデフォルトスコア
-
- def evaluate_performance(self, worktree_path: Path) -> float:
- """
- パフォーマンスを評価(ベンチマーク)
-
- Returns:
- float: スコア(0-100)
- """
- try:
- # ベンチマークファイルを確認
- benchmark_file = worktree_path / "benchmark-results.json"
-
- if not benchmark_file.exists():
- # ベンチマークを実行
- result = subprocess.run(
- ["npm", "run", "benchmark"],
- cwd=worktree_path,
- capture_output=True,
- text=True,
- timeout=120
- )
-
- if result.returncode == 0:
- # 結果ファイルを再確認
- if benchmark_file.exists():
- with open(benchmark_file) as f:
- bench_data = json.load(f)
- avg_response_time = bench_data.get('avg_response_time_ms', 1000)
-
- # 100ms以下なら満点、1000ms以上なら0点
- if avg_response_time <= 100:
- score = 100
- elif avg_response_time >= 1000:
- score = 0
- else:
- score = 100 - ((avg_response_time - 100) / 9)
-
- logger.info(f"✅ Performance score: {score:.1f} (avg: {avg_response_time}ms)")
- return score
- else:
- logger.warning("⚠️ Benchmark file not found after execution")
- return 70.0
- else:
- logger.warning("⚠️ Benchmark execution failed")
- return 70.0
-
- else:
- # 既存の結果を読み込み
- with open(benchmark_file) as f:
- bench_data = json.load(f)
- avg_response_time = bench_data.get('avg_response_time_ms', 1000)
- if avg_response_time <= 100:
- return 100
- elif avg_response_time >= 1000:
- return 0
- else:
- return 100 - ((avg_response_time - 100) / 9)
-
- except Exception as e:
- logger.warning(f"⚠️ Performance evaluation failed: {e}")
- return 70.0
-
- def evaluate_security(self, worktree_path: Path) -> float:
- """
- セキュリティを評価(脆弱性スキャン)
-
- Returns:
- float: スコア(0-100)
- """
- try:
- # npm audit を実行
- result = subprocess.run(
- ["npm", "audit", "--json"],
- cwd=worktree_path,
- capture_output=True,
- text=True,
- timeout=60
- )
-
- if result.stdout:
- audit_data = json.loads(result.stdout)
- vulnerabilities = audit_data.get('metadata', {}).get('vulnerabilities', {})
-
- critical = vulnerabilities.get('critical', 0)
- high = vulnerabilities.get('high', 0)
- moderate = vulnerabilities.get('moderate', 0)
- low = vulnerabilities.get('low', 0)
-
- # スコア計算(critical: -20, high: -10, moderate: -5, low: -2)
- score = 100 - (critical * 20 + high * 10 + moderate * 5 + low * 2)
- score = max(0, score)
-
- logger.info(f"✅ Security score: {score:.1f} (critical: {critical}, high: {high})")
- return score
- else:
- logger.warning("⚠️ Security audit skipped")
- return 80.0
-
- except Exception as e:
- logger.warning(f"⚠️ Security evaluation failed: {e}")
- return 80.0
-
- def evaluate_simplicity(self, worktree_path: Path) -> float:
- """
- シンプルさを評価(コード行数、複雑度)
-
- Returns:
- float: スコア(0-100)
- """
- try:
- src_dir = worktree_path / "src"
- if not src_dir.exists():
- return 70.0
-
- # 行数をカウント
- total_lines = 0
- for file in src_dir.rglob("*.js"):
- with open(file) as f:
- total_lines += len(f.readlines())
- for file in src_dir.rglob("*.ts"):
- with open(file) as f:
- total_lines += len(f.readlines())
-
- # 1000行以下なら満点、5000行以上なら0点
- if total_lines <= 1000:
- score = 100
- elif total_lines >= 5000:
- score = 0
- else:
- score = 100 - ((total_lines - 1000) / 40)
-
- logger.info(f"✅ Simplicity score: {score:.1f} (lines: {total_lines})")
- return max(0, score)
-
- except Exception as e:
- logger.warning(f"⚠️ Simplicity evaluation failed: {e}")
- return 70.0
-
- def evaluate_worktree(
- self,
- worktree_path: Path,
- criteria: EvaluationCriteria
- ) -> WorktreeScore:
- """
- worktreeを総合評価
-
- Args:
- worktree_path: 評価対象のworktreeパス
- criteria: 評価基準
-
- Returns:
- WorktreeScore: 評価結果
- """
- logger.info(f"\n📊 Evaluating: {worktree_path.name}")
- logger.info("=" * 60)
-
- # 各項目を評価
- test_score = self.evaluate_test_pass_rate(worktree_path)
- quality_score = self.evaluate_code_quality(worktree_path)
- perf_score = self.evaluate_performance(worktree_path)
- security_score = self.evaluate_security(worktree_path)
- simplicity_score = self.evaluate_simplicity(worktree_path)
-
- # 加重平均で総合スコア計算
- total_score = (
- test_score * criteria.test_pass_rate +
- quality_score * criteria.code_quality +
- perf_score * criteria.performance +
- security_score * criteria.security +
- simplicity_score * criteria.simplicity
- )
-
- result = WorktreeScore(
- worktree_path=str(worktree_path),
- total_score=total_score,
- test_pass_rate=test_score,
- code_quality=quality_score,
- performance=perf_score,
- security=security_score,
- simplicity=simplicity_score,
- details={
- "test_pass_rate": f"{test_score:.1f}",
- "code_quality": f"{quality_score:.1f}",
- "performance": f"{perf_score:.1f}",
- "security": f"{security_score:.1f}",
- "simplicity": f"{simplicity_score:.1f}"
- }
- )
-
- logger.info(f"\n✅ Total Score: {total_score:.1f}/100")
- logger.info("=" * 60)
-
- return result
-
- def select_best_worktree(
- self,
- worktree_names: List[str],
- criteria: Optional[EvaluationCriteria] = None
- ) -> tuple[str, WorktreeScore]:
- """
- 複数のworktreeから最良を自動選択
-
- Args:
- worktree_names: 評価対象のworktree名のリスト
- criteria: 評価基準(省略時はデフォルト)
-
- Returns:
- tuple: (選択されたworktree名, 評価結果)
- """
- if criteria is None:
- criteria = EvaluationCriteria()
-
- results = {}
-
- logger.info("\n🚀 Starting autonomous evaluation...")
- logger.info(f"📋 Evaluating {len(worktree_names)} worktrees")
-
- for wt_name in worktree_names:
- wt_path = self.worktrees_dir / wt_name
- if wt_path.exists():
- score_result = self.evaluate_worktree(wt_path, criteria)
- results[wt_name] = score_result
- else:
- logger.warning(f"⚠️ Worktree not found: {wt_name}")
-
- if not results:
- raise ValueError("No valid worktrees found for evaluation")
-
- # 最高スコアを選択
- best_name = max(results, key=lambda k: results[k].total_score)
- best_score = results[best_name]
-
- logger.info("\n" + "=" * 60)
- logger.info("🏆 EVALUATION RESULTS")
- logger.info("=" * 60)
-
- for name, score in sorted(results.items(), key=lambda x: x[1].total_score, reverse=True):
- logger.info(f"{name}: {score.total_score:.1f}/100")
-
- logger.info("\n✅ SELECTED: " + best_name)
- logger.info(f" Score: {best_score.total_score:.1f}/100")
- logger.info("=" * 60)
-
- # 結果をJSONで保存
- report_path = self.project_path / "EVALUATION_REPORT.json"
- with open(report_path, 'w') as f:
- json.dump({
- "selected": best_name,
- "results": {
- name: {
- "total_score": score.total_score,
- "details": score.details
- }
- for name, score in results.items()
- },
- "criteria": {
- "test_pass_rate": criteria.test_pass_rate,
- "code_quality": criteria.code_quality,
- "performance": criteria.performance,
- "security": criteria.security,
- "simplicity": criteria.simplicity
- }
- }, f, indent=2)
-
- logger.info(f"\n📄 Evaluation report saved: {report_path}")
-
- return best_name, best_score
-
- def merge_to_main_and_sync(self, selected_worktree: str, phase: str = None, skip_file_check: bool = False) -> bool:
- """選択されたworktreeをmainにマージし、他のworktreeに同期
-
- Args:
- selected_worktree: 選択されたworktree名(例: "phase1-planning-a")
- phase: フェーズ名(例: "phase1")- 自動判定も可能
- skip_file_check: 重要ファイルチェックをスキップするか(Phase 1-Aでは True)
-
- Returns:
- bool: 成功したらTrue
- """
- try:
- # フェーズを自動判定
- if phase is None:
- if 'phase1' in selected_worktree:
- phase = 'phase1'
- elif 'phase2' in selected_worktree:
- phase = 'phase2'
- elif 'phase4' in selected_worktree:
- phase = 'phase4'
-
- logger.info("\n" + "=" * 60)
- logger.info(f"🔄 Merging {selected_worktree} to main...")
- logger.info("=" * 60)
-
- # ブランチ名を推定(worktree名からphaseN-プレフィックスを除去)
- branch_name = selected_worktree
- for prefix in ['phase1-', 'phase2-', 'phase3-', 'phase4-', 'phase5-']:
- branch_name = branch_name.replace(prefix, 'phase/')
-
- # mainにマージ(M4 Mac対応)
- git_cmd = '/usr/bin/git' if os.path.exists('/usr/bin/git') else 'git'
- subprocess.run(
- [git_cmd, 'merge', '--no-edit', branch_name],
- cwd=self.project_path,
- check=True
- )
- logger.info(f"✅ Merged {branch_name} to main")
-
- # Phase別の重要ファイル確認(skip_file_check=Trueの場合はスキップ)
- if not skip_file_check:
- # Phase別に確認すべきファイルを定義
- phase_required_files = {
- 'phase1': {
- 'required': ['REQUIREMENTS.md', 'SPEC.md'], # Phase 1-A完了時点での必須
- 'optional': ['IMAGE_PROMPTS.json', 'AUDIO_PROMPTS.json', 'TECH_STACK.md', 'WBS.json'] # Phase 1-B完了時点
- },
- 'phase2': {
- 'required': ['src/', 'index.html'],
- 'optional': ['tests/', 'assets/']
- },
- 'phase4': {
- 'required': [],
- 'optional': ['benchmark-results.json', 'coverage/']
- }
- }
-
- if phase in phase_required_files:
- config = phase_required_files[phase]
- missing_required = []
- missing_optional = []
-
- for file in config.get('required', []):
- file_path = self.project_path / file
- if file_path.exists():
- logger.info(f" ✅ {file} - 存在確認(必須)")
- else:
- missing_required.append(file)
- logger.error(f" ❌ {file} - 必須ファイルが見つかりません")
-
- for file in config.get('optional', []):
- file_path = self.project_path / file
- if file_path.exists():
- logger.info(f" ✅ {file} - 存在確認(オプション)")
- else:
- missing_optional.append(file)
- logger.info(f" ℹ️ {file} - オプションファイル(未生成)")
-
- if missing_required:
- logger.error(f"\n❌ 必須ファイルが不足しています: {', '.join(missing_required)}")
- logger.error(" → このPhaseの成果物が不完全です。再実行を検討してください。")
-
- if missing_optional:
- logger.info(f"\nℹ️ オプションファイル(後続Phaseで生成予定): {', '.join(missing_optional)}")
-
- # すべてのworktreeに同期
- logger.info("\n🔄 Syncing to all worktrees...")
- sync_success = 0
- sync_failed = 0
-
- if self.worktrees_dir.exists():
- for worktree in self.worktrees_dir.iterdir():
- if worktree.is_dir() and worktree.name != selected_worktree:
- try:
- # git merge main を各worktreeで実行
- subprocess.run(
- [git_cmd, 'merge', '--no-edit', 'main'],
- cwd=worktree,
- check=True,
- capture_output=True
- )
- logger.info(f" ✅ Synced to {worktree.name}")
- sync_success += 1
- except subprocess.CalledProcessError:
- logger.warning(f" ⚠️ Failed to sync to {worktree.name}")
- sync_failed += 1
-
- logger.info(f"\n✅ Merge and sync completed! (Success: {sync_success}, Failed: {sync_failed})")
- return True
-
- except subprocess.CalledProcessError as e:
- logger.error(f"❌ Merge failed: {e}")
- return False
- except Exception as e:
- logger.error(f"❌ Unexpected error: {e}")
- return False
-
-
-def main():
- """CLI エントリーポイント"""
- import sys
-
- if len(sys.argv) < 2:
- print("Usage: python3 autonomous_evaluator.py
[worktree1] [worktree2] ... [options]")
- print("\nOptions:")
- print(" --auto-merge 選択されたworktreeを自動でmainにマージし全worktreeに同期")
- print(" --skip-file-check Phase別の重要ファイルチェックをスキップ(Phase 1前半用)")
- print(" --phase= フェーズを明示的に指定(phase1, phase2, phase4)")
- print("\nExample:")
- print(" python3 autonomous_evaluator.py ~/Desktop/AI-Apps/myapp-agent phase2-impl-prototype-a phase2-impl-prototype-b")
- print(" python3 autonomous_evaluator.py . phase1-planning-a phase1-planning-b --auto-merge")
- print(" python3 autonomous_evaluator.py . phase1-planning-a phase1-planning-b --auto-merge --skip-file-check")
- sys.exit(1)
-
- # オプションを抽出
- args = sys.argv[1:]
- options = [a for a in args if a.startswith('--')]
- non_options = [a for a in args if not a.startswith('--')]
-
- project_path = Path(non_options[0]) if non_options else Path('.')
- worktree_names = non_options[1:] if len(non_options) > 1 else []
-
- # オプション解析
- auto_merge = '--auto-merge' in options
- skip_file_check = '--skip-file-check' in options
- phase = None
- for opt in options:
- if opt.startswith('--phase='):
- phase = opt.split('=')[1]
-
- # worktree_namesからオプションを除外
- worktree_names = [w for w in worktree_names if not w.startswith('--')]
-
- if not worktree_names:
- # worktrees/配下の全フォルダを評価
- worktrees_dir = project_path / "worktrees"
- if worktrees_dir.exists():
- worktree_names = [d.name for d in worktrees_dir.iterdir() if d.is_dir()]
-
- evaluator = AutonomousEvaluator(project_path)
- best_name, best_score = evaluator.select_best_worktree(worktree_names)
-
- print(f"\n🎉 Best worktree: {best_name}")
- print(f" Total score: {best_score.total_score:.1f}/100")
-
- # 自動マージ・同期(--auto-mergeオプション)
- if auto_merge:
- print("\n🔄 Auto-merge enabled - merging to main and syncing...")
- if skip_file_check:
- print("ℹ️ File check skipped (--skip-file-check)")
- evaluator.merge_to_main_and_sync(best_name, phase=phase, skip_file_check=skip_file_check)
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/autonomous_evaluator_ux.py b/src/autonomous_evaluator_ux.py
deleted file mode 100755
index 60cbd0a..0000000
--- a/src/autonomous_evaluator_ux.py
+++ /dev/null
@@ -1,830 +0,0 @@
-#!/usr/bin/env python3
-"""
-自律評価システム - UX最優先版
-
-Phase別worktreeを自動評価し、UXが最も優れたものを選択する
-
-評価軸(合計100%):
- - ユーザー体験(UX): 35%
- - 機能完成度: 20%
- - パフォーマンス: 15%
- - テスト品質: 15%
- - セキュリティ: 10%
- - 保守性: 5%
-"""
-
-import json
-import subprocess
-import re
-import os
-from pathlib import Path
-from typing import Dict, List, Optional, Tuple
-from dataclasses import dataclass
-from html.parser import HTMLParser
-import logging
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-@dataclass
-class UXEvaluationCriteria:
- """UX重視の評価基準"""
- user_experience: float = 0.35 # UX(最優先)
- feature_completeness: float = 0.20 # 機能完成度
- performance: float = 0.15 # パフォーマンス
- test_quality: float = 0.15 # テスト品質
- security: float = 0.10 # セキュリティ
- maintainability: float = 0.05 # 保守性
-
-
-@dataclass
-class WorktreeScore:
- """worktreeの評価スコア"""
- worktree_path: str
- total_score: float
- ux_score: float
- feature_score: float
- performance_score: float
- test_score: float
- security_score: float
- maintainability_score: float
- details: Dict
- ux_breakdown: Dict
-
-
-class HTMLAnalyzer(HTMLParser):
- """HTML構造を解析してUX評価"""
-
- def __init__(self):
- super().__init__()
- self.has_nav = False
- self.has_search = False
- self.has_breadcrumb = False
- self.aria_labels = 0
- self.interactive_elements = 0
- self.tabindex_count = 0
- self.forms = 0
- self.buttons = 0
- self.links = 0
-
- def handle_starttag(self, tag, attrs):
- attrs_dict = dict(attrs)
-
- # ナビゲーション
- if tag == 'nav':
- self.has_nav = True
-
- # 検索
- if tag == 'input' and attrs_dict.get('type') == 'search':
- self.has_search = True
-
- # パンくず
- if 'class' in attrs_dict and 'breadcrumb' in attrs_dict['class']:
- self.has_breadcrumb = True
-
- # アクセシビリティ
- if 'aria-label' in attrs_dict or 'aria-labelledby' in attrs_dict:
- self.aria_labels += 1
-
- if 'tabindex' in attrs_dict:
- self.tabindex_count += 1
-
- # インタラクティブ要素
- if tag in ['button', 'a', 'input', 'select', 'textarea']:
- self.interactive_elements += 1
-
- if tag == 'form':
- self.forms += 1
- if tag == 'button':
- self.buttons += 1
- if tag == 'a':
- self.links += 1
-
-
-class UXAutonomousEvaluator:
- """UX重視の自律評価システム"""
-
- def __init__(self, project_path: Path):
- self.project_path = Path(project_path)
- self.worktrees_dir = self.project_path / "worktrees"
-
- def evaluate_user_experience(self, worktree_path: Path) -> Tuple[float, Dict]:
- """
- ユーザー体験(UX)を総合評価(35点満点)
-
- Returns:
- tuple: (UXスコア, 詳細内訳)
- """
- logger.info(" 🎨 Evaluating User Experience...")
-
- ux_score = 0
- breakdown = {}
-
- # 1. パフォーマンスUX(10点)
- perf_ux = self._evaluate_performance_ux(worktree_path)
- ux_score += perf_ux
- breakdown['performance_ux'] = perf_ux
-
- # 2. 直感性・使いやすさ(10点)
- usability = self._evaluate_usability(worktree_path)
- ux_score += usability
- breakdown['usability'] = usability
-
- # 3. アクセシビリティ(8点)
- accessibility = self._evaluate_accessibility(worktree_path)
- ux_score += accessibility
- breakdown['accessibility'] = accessibility
-
- # 4. レスポンシブ対応(7点)
- responsive = self._evaluate_responsive_design(worktree_path)
- ux_score += responsive
- breakdown['responsive'] = responsive
-
- # 100点満点に正規化
- ux_score_normalized = (ux_score / 35) * 100
-
- logger.info(f" ✅ UX Score: {ux_score_normalized:.1f}/100 (raw: {ux_score:.1f}/35)")
- logger.info(f" Performance UX: {perf_ux:.1f}/10")
- logger.info(f" Usability: {usability:.1f}/10")
- logger.info(f" Accessibility: {accessibility:.1f}/8")
- logger.info(f" Responsive: {responsive:.1f}/7")
-
- return ux_score_normalized, breakdown
-
- def _evaluate_performance_ux(self, worktree_path: Path) -> float:
- """パフォーマンスUX評価(10点満点)"""
- score = 0
-
- # package.jsonでフレームワーク確認
- package_json = worktree_path / "package.json"
- if package_json.exists():
- with open(package_json) as f:
- pkg_data = json.load(f)
- dependencies = pkg_data.get('dependencies', {})
-
- # 高速フレームワークにボーナス
- if 'next' in dependencies:
- score += 3 # Next.js(App Router、SSR対応)
- elif 'vite' in pkg_data.get('devDependencies', {}):
- score += 2 # Vite(高速ビルド)
-
- # パフォーマンス最適化ライブラリ
- if 'react-lazy-load' in dependencies or 'react-lazyload' in dependencies:
- score += 1
- if '@vercel/analytics' in dependencies:
- score += 1
-
- # HTMLでローディング表示確認
- html_files = list(worktree_path.rglob("*.html"))
- for html_file in html_files[:3]: # 最初の3ファイルのみチェック
- try:
- with open(html_file, encoding='utf-8') as f:
- content = f.read().lower()
- if 'loading' in content or 'spinner' in content:
- score += 1
- break
- except:
- pass
-
- # JSでOptimistic UI確認
- js_files = list(worktree_path.rglob("*.js")) + list(worktree_path.rglob("*.jsx"))
- for js_file in js_files[:5]:
- try:
- with open(js_file, encoding='utf-8') as f:
- content = f.read()
- if 'optimistic' in content.lower() or 'useMutation' in content:
- score += 2
- break
- except:
- pass
-
- return min(score, 10)
-
- def _evaluate_usability(self, worktree_path: Path) -> float:
- """使いやすさ評価(10点満点)"""
- score = 0
-
- html_files = list(worktree_path.rglob("*.html"))
-
- if not html_files:
- return 5.0 # HTMLがない場合(CLI等)は中間スコア
-
- for html_file in html_files[:5]: # 最大5ファイルチェック
- try:
- with open(html_file, encoding='utf-8') as f:
- content = f.read()
-
- analyzer = HTMLAnalyzer()
- analyzer.feed(content)
-
- # ナビゲーション
- if analyzer.has_nav:
- score += 2
- if analyzer.has_search:
- score += 1
- if analyzer.has_breadcrumb:
- score += 1
-
- # インタラクティブ要素の充実度
- if analyzer.buttons > 3:
- score += 1
- if analyzer.forms > 0:
- score += 1
-
- # 最初のHTMLで評価完了
- break
-
- except Exception as e:
- logger.warning(f" ⚠️ Error analyzing {html_file.name}: {e}")
-
- # JSでエラーハンドリング確認
- js_files = list(worktree_path.rglob("*.js")) + list(worktree_path.rglob("*.jsx"))
- for js_file in js_files[:5]:
- try:
- with open(js_file, encoding='utf-8') as f:
- content = f.read()
-
- # try-catch
- if 'try {' in content and 'catch' in content:
- score += 1
-
- # ユーザーフレンドリーなエラー表示
- if any(word in content for word in ['toast', 'notification', 'alert', 'snackbar']):
- score += 2
- break
-
- except:
- pass
-
- return min(score, 10)
-
- def _evaluate_accessibility(self, worktree_path: Path) -> float:
- """アクセシビリティ評価(8点満点)"""
- score = 0
-
- html_files = list(worktree_path.rglob("*.html"))
-
- if not html_files:
- return 4.0 # HTMLがない場合は中間スコア
-
- for html_file in html_files[:5]:
- try:
- with open(html_file, encoding='utf-8') as f:
- content = f.read()
-
- analyzer = HTMLAnalyzer()
- analyzer.feed(content)
-
- # ARIA属性
- if analyzer.aria_labels > 5:
- score += 3
- elif analyzer.aria_labels > 0:
- score += 1
-
- # キーボードナビゲーション
- if analyzer.tabindex_count > analyzer.interactive_elements * 0.3:
- score += 3
- elif analyzer.tabindex_count > 0:
- score += 1
-
- # セマンティックHTML
- if '' in content or '' in content or '' in content:
- score += 2
-
- break
-
- except Exception as e:
- logger.warning(f" ⚠️ Error analyzing accessibility: {e}")
-
- return min(score, 8)
-
- def _evaluate_responsive_design(self, worktree_path: Path) -> float:
- """レスポンシブ対応評価(7点満点)"""
- score = 0
-
- # CSSでメディアクエリ確認
- css_files = list(worktree_path.rglob("*.css"))
- for css_file in css_files[:5]:
- try:
- with open(css_file, encoding='utf-8') as f:
- content = f.read()
-
- # メディアクエリの数
- media_queries = content.count('@media')
- if media_queries >= 3:
- score += 4
- elif media_queries > 0:
- score += 2
-
- # Flexbox/Grid
- if 'display: flex' in content or 'display: grid' in content:
- score += 2
-
- break
-
- except:
- pass
-
- # HTMLでviewport meta確認
- html_files = list(worktree_path.rglob("*.html"))
- for html_file in html_files[:1]:
- try:
- with open(html_file, encoding='utf-8') as f:
- content = f.read()
- if 'viewport' in content and 'width=device-width' in content:
- score += 1
- break
- except:
- pass
-
- return min(score, 7)
-
- def evaluate_feature_completeness(self, worktree_path: Path) -> float:
- """機能完成度評価(0-100)"""
- logger.info(" ✨ Evaluating Feature Completeness...")
-
- score = 70.0 # ベーススコア
-
- # REQUIREMENTS.mdがあれば、実装率を確認
- requirements_file = worktree_path / "REQUIREMENTS.md"
- if requirements_file.exists():
- try:
- with open(requirements_file, encoding='utf-8') as f:
- content = f.read()
-
- # チェックボックスの実装率
- total_features = content.count('- [ ]') + content.count('- [x]')
- completed_features = content.count('- [x]')
-
- if total_features > 0:
- completion_rate = (completed_features / total_features) * 100
- score = completion_rate
- logger.info(f" ✅ Feature completion: {completion_rate:.1f}% ({completed_features}/{total_features})")
- else:
- logger.info(" ⚠️ No feature checklist found")
-
- except Exception as e:
- logger.warning(f" ⚠️ Error reading REQUIREMENTS.md: {e}")
- else:
- # ファイル数で推定
- src_files = list((worktree_path / "src").rglob("*")) if (worktree_path / "src").exists() else []
- if len(src_files) > 10:
- score = 85.0
- elif len(src_files) > 5:
- score = 75.0
-
- logger.info(f" ✅ Feature Score: {score:.1f}/100")
- return score
-
- def evaluate_performance(self, worktree_path: Path) -> float:
- """パフォーマンス評価(0-100)"""
- logger.info(" ⚡ Evaluating Performance...")
-
- try:
- benchmark_file = worktree_path / "benchmark-results.json"
-
- if benchmark_file.exists():
- with open(benchmark_file) as f:
- bench_data = json.load(f)
- avg_response_time = bench_data.get('avg_response_time_ms', 1000)
-
- if avg_response_time <= 100:
- score = 100
- elif avg_response_time >= 1000:
- score = 0
- else:
- score = 100 - ((avg_response_time - 100) / 9)
-
- logger.info(f" ✅ Performance: {score:.1f}/100 (avg: {avg_response_time}ms)")
- return score
- else:
- logger.info(" ⚠️ No benchmark results, using default score")
- return 75.0
-
- except Exception as e:
- logger.warning(f" ⚠️ Performance evaluation failed: {e}")
- return 75.0
-
- def evaluate_test_quality(self, worktree_path: Path) -> float:
- """テスト品質評価(0-100)"""
- logger.info(" 🧪 Evaluating Test Quality...")
-
- try:
- # テスト実行
- result = subprocess.run(
- ["npm", "test", "--", "--json", "--passWithNoTests"],
- cwd=worktree_path,
- capture_output=True,
- text=True,
- timeout=300
- )
-
- if result.returncode == 0 or "passWithNoTests" in result.stdout:
- try:
- test_data = json.loads(result.stdout)
- total = test_data.get('numTotalTests', 0)
- passed = test_data.get('numPassedTests', 0)
-
- if total > 0:
- pass_rate = (passed / total) * 100
- logger.info(f" ✅ Test Quality: {pass_rate:.1f}% ({passed}/{total})")
- return pass_rate
- else:
- logger.info(" ⚠️ No tests found")
- return 50.0
- except json.JSONDecodeError:
- logger.info(" ⚠️ Test output parsing failed")
- return 70.0
- else:
- logger.warning(f" ⚠️ Tests failed")
- return 0.0
-
- except subprocess.TimeoutExpired:
- logger.warning(" ⚠️ Test execution timeout")
- return 50.0
- except Exception as e:
- logger.warning(f" ⚠️ Test evaluation failed: {e}")
- return 70.0
-
- def evaluate_security(self, worktree_path: Path) -> float:
- """セキュリティ評価(0-100)"""
- logger.info(" 🔐 Evaluating Security...")
-
- try:
- result = subprocess.run(
- ["npm", "audit", "--json"],
- cwd=worktree_path,
- capture_output=True,
- text=True,
- timeout=60
- )
-
- if result.stdout:
- audit_data = json.loads(result.stdout)
- vulnerabilities = audit_data.get('metadata', {}).get('vulnerabilities', {})
-
- critical = vulnerabilities.get('critical', 0)
- high = vulnerabilities.get('high', 0)
- moderate = vulnerabilities.get('moderate', 0)
- low = vulnerabilities.get('low', 0)
-
- score = 100 - (critical * 20 + high * 10 + moderate * 5 + low * 2)
- score = max(0, score)
-
- logger.info(f" ✅ Security: {score:.1f}/100 (C:{critical}, H:{high}, M:{moderate}, L:{low})")
- return score
- else:
- logger.info(" ⚠️ No npm audit data")
- return 85.0
-
- except Exception as e:
- logger.warning(f" ⚠️ Security evaluation failed: {e}")
- return 85.0
-
- def evaluate_maintainability(self, worktree_path: Path) -> float:
- """保守性評価(0-100)"""
- logger.info(" 🔧 Evaluating Maintainability...")
-
- try:
- src_dir = worktree_path / "src"
- if not src_dir.exists():
- return 70.0
-
- total_lines = 0
- for file in src_dir.rglob("*.js"):
- with open(file, encoding='utf-8') as f:
- total_lines += len(f.readlines())
- for file in src_dir.rglob("*.ts"):
- with open(file, encoding='utf-8') as f:
- total_lines += len(f.readlines())
-
- # 1000行以下なら満点、5000行以上なら0点
- if total_lines <= 1000:
- score = 100
- elif total_lines >= 5000:
- score = 30
- else:
- score = 100 - ((total_lines - 1000) / 40)
-
- logger.info(f" ✅ Maintainability: {score:.1f}/100 (lines: {total_lines})")
- return max(30, score)
-
- except Exception as e:
- logger.warning(f" ⚠️ Maintainability evaluation failed: {e}")
- return 70.0
-
- def evaluate_worktree(
- self,
- worktree_path: Path,
- criteria: UXEvaluationCriteria
- ) -> WorktreeScore:
- """worktreeを総合評価(UX重視)"""
-
- logger.info(f"\n📊 Evaluating: {worktree_path.name}")
- logger.info("=" * 60)
-
- # 各項目を評価
- ux_score, ux_breakdown = self.evaluate_user_experience(worktree_path)
- feature_score = self.evaluate_feature_completeness(worktree_path)
- perf_score = self.evaluate_performance(worktree_path)
- test_score = self.evaluate_test_quality(worktree_path)
- security_score = self.evaluate_security(worktree_path)
- maintainability_score = self.evaluate_maintainability(worktree_path)
-
- # 加重平均で総合スコア計算
- total_score = (
- ux_score * criteria.user_experience +
- feature_score * criteria.feature_completeness +
- perf_score * criteria.performance +
- test_score * criteria.test_quality +
- security_score * criteria.security +
- maintainability_score * criteria.maintainability
- )
-
- result = WorktreeScore(
- worktree_path=str(worktree_path),
- total_score=total_score,
- ux_score=ux_score,
- feature_score=feature_score,
- performance_score=perf_score,
- test_score=test_score,
- security_score=security_score,
- maintainability_score=maintainability_score,
- details={
- "user_experience": f"{ux_score:.1f}",
- "feature_completeness": f"{feature_score:.1f}",
- "performance": f"{perf_score:.1f}",
- "test_quality": f"{test_score:.1f}",
- "security": f"{security_score:.1f}",
- "maintainability": f"{maintainability_score:.1f}"
- },
- ux_breakdown=ux_breakdown
- )
-
- logger.info(f"\n✅ Total Score: {total_score:.1f}/100")
- logger.info(f" UX (35%): {ux_score:.1f} × 0.35 = {ux_score * 0.35:.1f}")
- logger.info(f" Feature (20%): {feature_score:.1f} × 0.20 = {feature_score * 0.20:.1f}")
- logger.info(f" Performance (15%): {perf_score:.1f} × 0.15 = {perf_score * 0.15:.1f}")
- logger.info(f" Test Quality (15%): {test_score:.1f} × 0.15 = {test_score * 0.15:.1f}")
- logger.info(f" Security (10%): {security_score:.1f} × 0.10 = {security_score * 0.10:.1f}")
- logger.info(f" Maintainability (5%): {maintainability_score:.1f} × 0.05 = {maintainability_score * 0.05:.1f}")
- logger.info("=" * 60)
-
- return result
-
- def select_best_worktree(
- self,
- worktree_names: List[str],
- criteria: Optional[UXEvaluationCriteria] = None
- ) -> Tuple[str, WorktreeScore]:
- """複数のworktreeから最良を自動選択(UX重視)"""
-
- if criteria is None:
- criteria = UXEvaluationCriteria()
-
- results = {}
-
- logger.info("\n🚀 Starting UX-focused autonomous evaluation...")
- logger.info(f"📋 Evaluating {len(worktree_names)} worktrees")
- logger.info("\n📊 Evaluation Criteria:")
- logger.info(f" User Experience (UX): {criteria.user_experience * 100:.0f}%")
- logger.info(f" Feature Completeness: {criteria.feature_completeness * 100:.0f}%")
- logger.info(f" Performance: {criteria.performance * 100:.0f}%")
- logger.info(f" Test Quality: {criteria.test_quality * 100:.0f}%")
- logger.info(f" Security: {criteria.security * 100:.0f}%")
- logger.info(f" Maintainability: {criteria.maintainability * 100:.0f}%")
-
- for wt_name in worktree_names:
- wt_path = self.worktrees_dir / wt_name
- if wt_path.exists():
- score_result = self.evaluate_worktree(wt_path, criteria)
- results[wt_name] = score_result
- else:
- logger.warning(f"⚠️ Worktree not found: {wt_name}")
-
- if not results:
- raise ValueError("No valid worktrees found for evaluation")
-
- # 最高スコアを選択
- best_name = max(results, key=lambda k: results[k].total_score)
- best_score = results[best_name]
-
- logger.info("\n" + "=" * 60)
- logger.info("🏆 EVALUATION RESULTS (UX-Focused)")
- logger.info("=" * 60)
-
- for name, score in sorted(results.items(), key=lambda x: x[1].total_score, reverse=True):
- logger.info(f"\n{name}:")
- logger.info(f" Total: {score.total_score:.1f}/100")
- logger.info(f" UX: {score.ux_score:.1f}, Feature: {score.feature_score:.1f}, Perf: {score.performance_score:.1f}")
-
- logger.info("\n" + "=" * 60)
- logger.info("✅ SELECTED: " + best_name)
- logger.info(f" Total Score: {best_score.total_score:.1f}/100")
- logger.info(f" UX Score: {best_score.ux_score:.1f}/100 (35% weight)")
- logger.info("=" * 60)
-
- # 結果をJSONで保存
- report_path = self.project_path / "EVALUATION_REPORT_UX.json"
- with open(report_path, 'w', encoding='utf-8') as f:
- json.dump({
- "selected": best_name,
- "evaluation_type": "UX-Focused",
- "results": {
- name: {
- "total_score": score.total_score,
- "scores": score.details,
- "ux_breakdown": score.ux_breakdown
- }
- for name, score in results.items()
- },
- "criteria": {
- "user_experience": criteria.user_experience,
- "feature_completeness": criteria.feature_completeness,
- "performance": criteria.performance,
- "test_quality": criteria.test_quality,
- "security": criteria.security,
- "maintainability": criteria.maintainability
- }
- }, f, indent=2, ensure_ascii=False)
-
- logger.info(f"\n📄 UX Evaluation report saved: {report_path}")
-
- return best_name, best_score
-
- def merge_to_main_and_sync(self, selected_worktree: str, phase: str = None, skip_file_check: bool = False) -> bool:
- """選択されたworktreeをmainにマージし、他のworktreeに同期
-
- Args:
- selected_worktree: 選択されたworktree名(例: "phase2-impl-prototype-a")
- phase: フェーズ名(例: "phase2")- 自動判定も可能
- skip_file_check: 重要ファイルチェックをスキップするか
-
- Returns:
- bool: 成功したらTrue
- """
- try:
- # フェーズを自動判定
- if phase is None:
- if 'phase1' in selected_worktree:
- phase = 'phase1'
- elif 'phase2' in selected_worktree:
- phase = 'phase2'
- elif 'phase4' in selected_worktree:
- phase = 'phase4'
-
- logger.info("\n" + "=" * 60)
- logger.info(f"🔄 Merging {selected_worktree} to main...")
- logger.info("=" * 60)
-
- # ブランチ名を推定(worktree名からphaseN-プレフィックスを除去)
- branch_name = selected_worktree
- for prefix in ['phase1-', 'phase2-', 'phase3-', 'phase4-', 'phase5-']:
- branch_name = branch_name.replace(prefix, 'phase/')
-
- # mainにマージ(M4 Mac対応)
- git_cmd = '/usr/bin/git' if os.path.exists('/usr/bin/git') else 'git'
- subprocess.run(
- [git_cmd, 'merge', '--no-edit', branch_name],
- cwd=self.project_path,
- check=True
- )
- logger.info(f"✅ Merged {branch_name} to main")
-
- # Phase別の重要ファイル確認(skip_file_check=Trueの場合はスキップ)
- if not skip_file_check:
- # Phase別に確認すべきファイルを定義
- phase_required_files = {
- 'phase1': {
- 'required': ['REQUIREMENTS.md', 'SPEC.md'],
- 'optional': ['IMAGE_PROMPTS.json', 'AUDIO_PROMPTS.json', 'TECH_STACK.md', 'WBS.json']
- },
- 'phase2': {
- 'required': ['src/', 'index.html'],
- 'optional': ['tests/', 'assets/']
- },
- 'phase4': {
- 'required': [],
- 'optional': ['benchmark-results.json', 'coverage/']
- }
- }
-
- if phase in phase_required_files:
- config = phase_required_files[phase]
- missing_required = []
- missing_optional = []
-
- for file in config.get('required', []):
- file_path = self.project_path / file
- if file_path.exists():
- logger.info(f" ✅ {file} - 存在確認(必須)")
- else:
- missing_required.append(file)
- logger.error(f" ❌ {file} - 必須ファイルが見つかりません")
-
- for file in config.get('optional', []):
- file_path = self.project_path / file
- if file_path.exists():
- logger.info(f" ✅ {file} - 存在確認(オプション)")
- else:
- missing_optional.append(file)
- logger.info(f" ℹ️ {file} - オプションファイル(未生成)")
-
- if missing_required:
- logger.error(f"\n❌ 必須ファイルが不足しています: {', '.join(missing_required)}")
- logger.error(" → このPhaseの成果物が不完全です。再実行を検討してください。")
-
- if missing_optional:
- logger.info(f"\nℹ️ オプションファイル(後続Phaseで生成予定): {', '.join(missing_optional)}")
-
- # すべてのworktreeに同期
- logger.info("\n🔄 Syncing to all worktrees...")
- sync_success = 0
- sync_failed = 0
-
- if self.worktrees_dir.exists():
- for worktree in self.worktrees_dir.iterdir():
- if worktree.is_dir() and worktree.name != selected_worktree:
- try:
- # git merge main を各worktreeで実行
- subprocess.run(
- [git_cmd, 'merge', '--no-edit', 'main'],
- cwd=worktree,
- check=True,
- capture_output=True
- )
- logger.info(f" ✅ Synced to {worktree.name}")
- sync_success += 1
- except subprocess.CalledProcessError:
- logger.warning(f" ⚠️ Failed to sync to {worktree.name}")
- sync_failed += 1
-
- logger.info(f"\n✅ Merge and sync completed! (Success: {sync_success}, Failed: {sync_failed})")
- return True
-
- except subprocess.CalledProcessError as e:
- logger.error(f"❌ Merge failed: {e}")
- return False
- except Exception as e:
- logger.error(f"❌ Unexpected error: {e}")
- return False
-
-
-def main():
- """CLI エントリーポイント"""
- import sys
-
- if len(sys.argv) < 2:
- print("Usage: python3 autonomous_evaluator_ux.py [worktree1] [worktree2] ... [options]")
- print("\nOptions:")
- print(" --auto-merge 選択されたworktreeを自動でmainにマージし全worktreeに同期")
- print(" --skip-file-check Phase別の重要ファイルチェックをスキップ")
- print(" --phase= フェーズを明示的に指定(phase1, phase2, phase4)")
- print("\nExample:")
- print(" python3 autonomous_evaluator_ux.py . phase2-impl-prototype-a phase2-impl-prototype-b phase2-impl-prototype-c")
- print(" python3 autonomous_evaluator_ux.py . phase2-impl-prototype-a phase2-impl-prototype-b --auto-merge")
- sys.exit(1)
-
- # オプションを抽出
- args = sys.argv[1:]
- options = [a for a in args if a.startswith('--')]
- non_options = [a for a in args if not a.startswith('--')]
-
- project_path = Path(non_options[0]) if non_options else Path('.')
- worktree_names = non_options[1:] if len(non_options) > 1 else []
-
- # オプション解析
- auto_merge = '--auto-merge' in options
- skip_file_check = '--skip-file-check' in options
- phase = None
- for opt in options:
- if opt.startswith('--phase='):
- phase = opt.split('=')[1]
-
- # worktree_namesからオプションを除外
- worktree_names = [w for w in worktree_names if not w.startswith('--')]
-
- if not worktree_names:
- # worktrees/配下の全フォルダを評価
- worktrees_dir = project_path / "worktrees"
- if worktrees_dir.exists():
- worktree_names = [d.name for d in worktrees_dir.iterdir() if d.is_dir()]
-
- evaluator = UXAutonomousEvaluator(project_path)
- best_name, best_score = evaluator.select_best_worktree(worktree_names)
-
- print(f"\n🎉 Best worktree (UX-focused): {best_name}")
- print(f" Total score: {best_score.total_score:.1f}/100")
- print(f" UX score: {best_score.ux_score:.1f}/100")
-
- # 自動マージ・同期(--auto-mergeオプション)
- if auto_merge:
- print("\n🔄 Auto-merge enabled - merging to main and syncing...")
- if skip_file_check:
- print("ℹ️ File check skipped (--skip-file-check)")
- evaluator.merge_to_main_and_sync(best_name, phase=phase, skip_file_check=skip_file_check)
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/claude_agent_executor.py b/src/claude_agent_executor.py
deleted file mode 100755
index db645bf..0000000
--- a/src/claude_agent_executor.py
+++ /dev/null
@@ -1,228 +0,0 @@
-#!/usr/bin/env python3
-"""
-Claude Agent Executor - Claude APIを使ったエージェント実行
-実際のClaude APIコールを行い、Taskツールを使ってエージェントを起動
-"""
-
-import os
-import sys
-import json
-import time
-from pathlib import Path
-from typing import Dict, Any, Optional
-import logging
-
-logger = logging.getLogger(__name__)
-
-class ClaudeAgentExecutor:
- """
- Claude APIを使用してエージェントタスクを実行
- 実際の実装では anthropic パッケージを使用
- """
-
- def __init__(self, worktree_path: Path):
- self.worktree_path = worktree_path
-
- def execute_agent(self, agent_type: str, task_description: str) -> Dict:
- """
- Claudeエージェントを実行
-
- Args:
- agent_type: エージェントのタイプ
- task_description: タスクの説明
-
- Returns:
- 実行結果
- """
- logger.info(f"🤖 Executing Claude agent: {agent_type}")
-
- # エージェントごとのプロンプトを構築
- prompt = self._build_agent_prompt(agent_type, task_description)
-
- # Claude APIを呼び出す(ここでは実際のTaskツールを使用)
- # 実際の実装では、以下のようなコードになります:
- """
- from anthropic import Anthropic
-
- client = Anthropic()
- message = client.messages.create(
- model="claude-3-opus-20240229",
- max_tokens=4096,
- tools=[{
- "name": "Task",
- "description": "Launch agent",
- "input_schema": {...}
- }],
- messages=[
- {"role": "user", "content": prompt}
- ]
- )
-
- # Taskツールの呼び出し結果を処理
- for tool_use in message.tool_uses:
- if tool_use.name == "Task":
- # エージェントの実行結果を取得
- result = self._process_agent_result(tool_use)
- """
-
- # シミュレーション結果を返す
- return self._simulate_agent_execution(agent_type, task_description)
-
- def _build_agent_prompt(self, agent_type: str, task_description: str) -> str:
- """エージェント実行用のプロンプトを構築"""
- prompts = {
- 'requirements_analyst': f"""
- あなたは要件定義アナリストです。
- 以下のタスクを実行してください:
- {task_description}
-
- 作業ディレクトリ: {self.worktree_path}
-
- 1. ユーザー要件を明確化
- 2. 機能要件と非機能要件を分類
- 3. 成功基準を定義
- 4. REQUIREMENTS.mdファイルを作成
- """,
-
- 'test_designer': f"""
- あなたはテスト設計エンジニアです。
- 以下のタスクを実行してください:
- {task_description}
-
- 作業ディレクトリ: {self.worktree_path}
-
- 1. テストケースを設計
- 2. ユニットテストを作成
- 3. 統合テストを作成
- 4. E2Eテストを作成
- 5. tests/ディレクトリに保存
- """,
-
- 'frontend_dev': f"""
- あなたはフロントエンド開発者です。
- 以下のタスクを実行してください:
- {task_description}
-
- 作業ディレクトリ: {self.worktree_path}
-
- 1. UIコンポーネントを実装
- 2. レスポンシブデザインを適用
- 3. インタラクティブ機能を追加
- 4. テストに合格するよう実装
- """,
-
- 'backend_dev': f"""
- あなたはバックエンド開発者です。
- 以下のタスクを実行してください:
- {task_description}
-
- 作業ディレクトリ: {self.worktree_path}
-
- 1. APIエンドポイントを実装
- 2. ビジネスロジックを実装
- 3. データベース接続を設定
- 4. テストに合格するよう実装
- """,
-
- 'evaluator': f"""
- あなたはテスト評価エージェントです。
- 以下のタスクを実行してください:
- {task_description}
-
- 作業ディレクトリ: {self.worktree_path}
-
- 1. テストを実行
- 2. 結果を分析
- 3. 問題点を特定
- 4. レポートを生成
- """,
-
- 'improvement_planner': f"""
- あなたは改善計画エージェントです。
- 以下のタスクを実行してください:
- {task_description}
-
- 作業ディレクトリ: {self.worktree_path}
-
- 1. テスト結果を分析
- 2. 修正方針を策定
- 3. 優先順位を決定
- 4. 改善計画書を作成
- """,
-
- 'fixer': f"""
- あなたはコード修正エージェントです。
- 以下のタスクを実行してください:
- {task_description}
-
- 作業ディレクトリ: {self.worktree_path}
-
- 1. 改善計画に基づいて修正
- 2. コードを更新
- 3. テストを再実行
- 4. 修正結果を報告
- """,
-
- 'documenter': f"""
- あなたはドキュメント作成エージェントです。
- 以下のタスクを実行してください:
- {task_description}
-
- 作業ディレクトリ: {self.worktree_path}
-
- 1. README.mdを作成
- 2. API仕様書を作成
- 3. アーキテクチャ図を作成
- 4. docs/ディレクトリに保存
- """
- }
-
- return prompts.get(agent_type, f"""
- あなたは{agent_type}エージェントです。
- 以下のタスクを実行してください:
- {task_description}
- 作業ディレクトリ: {self.worktree_path}
- """)
-
- def _simulate_agent_execution(self, agent_type: str, task_description: str) -> Dict:
- """エージェント実行のシミュレーション(開発用)"""
-
- # 実行時間をシミュレート
- execution_time = {
- 'requirements_analyst': 3,
- 'test_designer': 5,
- 'frontend_dev': 8,
- 'backend_dev': 8,
- 'evaluator': 3,
- 'improvement_planner': 2,
- 'fixer': 5,
- 'documenter': 4
- }.get(agent_type, 3)
-
- time.sleep(execution_time)
-
- # ファイル作成をシミュレート
- files_created = {
- 'requirements_analyst': ['REQUIREMENTS.md'],
- 'test_designer': ['tests/test_unit.js', 'tests/test_integration.js'],
- 'frontend_dev': ['index.html', 'style.css', 'app.js'],
- 'backend_dev': ['server.js', 'api.js'],
- 'evaluator': ['TEST_REPORT.md'],
- 'improvement_planner': ['IMPROVEMENT_PLAN.md'],
- 'fixer': ['[Updated files]'],
- 'documenter': ['docs/README.md', 'docs/API.md', 'docs/ARCHITECTURE.md']
- }.get(agent_type, [])
-
- # 結果を返す
- return {
- 'agent': agent_type,
- 'status': 'completed',
- 'execution_time': execution_time,
- 'files_created': files_created,
- 'tests_passed': True if 'test' not in agent_type else None,
- 'output': f"Successfully executed {agent_type} task",
- 'metrics': {
- 'lines_of_code': 100 * execution_time,
- 'test_coverage': 85.0 if 'test' in agent_type else None
- }
- }
\ No newline at end of file
diff --git a/src/client_document_generator.py b/src/client_document_generator.py
deleted file mode 100755
index 74d68bc..0000000
--- a/src/client_document_generator.py
+++ /dev/null
@@ -1,122 +0,0 @@
-#!/usr/bin/env python3
-"""
-Client向け納品ドキュメント生成
-最小限の実装版
-"""
-
-import os
-import sys
-from pathlib import Path
-from datetime import datetime
-
-def generate_requirements_doc(project_name):
- """要件定義書の生成(簡易版)"""
- content = f"""
-# 要件定義書
-
-プロジェクト名: {project_name}
-作成日: {datetime.now().strftime('%Y年%m月%d日')}
-
-## 1. プロジェクト概要
-(REQUIREMENTS.mdから自動取得予定)
-
-## 2. 機能要件
-- 主要機能1
-- 主要機能2
-- 主要機能3
-
-## 3. 非機能要件
-- パフォーマンス要件
-- セキュリティ要件
-- 可用性要件
-
-## 4. 制約事項
-- 技術的制約
-- 予算的制約
-- 期間的制約
-"""
- return content
-
-def generate_test_report(project_name):
- """テスト結果報告書の生成(簡易版)"""
- content = f"""
-# テスト結果報告書
-
-プロジェクト名: {project_name}
-実施日: {datetime.now().strftime('%Y年%m月%d日')}
-
-## 1. テスト実施概要
-- 単体テスト: 実施済み
-- 統合テスト: 実施済み
-- 受入テスト: 実施済み
-
-## 2. テスト結果
-- 総テストケース数: 50
-- 成功: 50
-- 失敗: 0
-- カバレッジ: 85%
-
-## 3. 品質評価
-すべてのテストが合格し、品質基準を満たしています。
-"""
- return content
-
-def generate_user_manual(project_name):
- """操作マニュアルの生成(簡易版)"""
- content = f"""
-# 操作マニュアル
-
-プロジェクト名: {project_name}
-バージョン: 1.0.0
-
-## 1. はじめに
-本マニュアルは{project_name}の操作方法について説明します。
-
-## 2. 起動方法
-1. launch_app.commandをダブルクリック
-2. ブラウザが自動的に起動します
-
-## 3. 基本操作
-(README.mdから自動取得予定)
-
-## 4. トラブルシューティング
-- Q: 起動しない場合
-- A: Node.jsがインストールされているか確認してください
-"""
- return content
-
-def main():
- # プロジェクト情報の取得
- project_info_path = Path("PROJECT_INFO.yaml")
- project_name = "プロジェクト"
-
- if project_info_path.exists():
- with open(project_info_path, 'r') as f:
- for line in f:
- if 'name:' in line:
- project_name = line.split(':')[1].strip()
- break
-
- # deliverables/01_documentsディレクトリ作成
- docs_dir = Path("deliverables/01_documents")
- docs_dir.mkdir(parents=True, exist_ok=True)
-
- # 各ドキュメントの生成
- documents = {
- "要件定義書.md": generate_requirements_doc(project_name),
- "テスト結果報告書.md": generate_test_report(project_name),
- "操作マニュアル.md": generate_user_manual(project_name)
- }
-
- for filename, content in documents.items():
- filepath = docs_dir / filename
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(content)
- print(f"✅ {filename} を生成しました")
-
- # TODO: PDF変換(要追加ライブラリ)
- print("\n📝 Markdown形式で生成完了。PDF変換は別途実施してください。")
- print("推奨: pandoc や wkhtmltopdf を使用したPDF変換")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/credential_checker.py b/src/credential_checker.py
deleted file mode 100755
index 66a9075..0000000
--- a/src/credential_checker.py
+++ /dev/null
@@ -1,299 +0,0 @@
-#!/usr/bin/env python3
-"""
-🔐 認証情報チェッカー
-API認証の状態を確認し、必要な設定を案内
-"""
-
-import os
-import sys
-import json
-from pathlib import Path
-from typing import Dict, List, Tuple, Optional
-from dataclasses import dataclass
-import subprocess
-
-# dotenv サポート(オプショナル)
-try:
- from dotenv import load_dotenv
- DOTENV_AVAILABLE = True
-except ImportError:
- DOTENV_AVAILABLE = False
-
-
-@dataclass
-class CredentialStatus:
- """認証情報の状態"""
- service: str
- status: str # 'ok', 'missing', 'invalid', 'unconfigured'
- message: str
- path: Optional[str] = None
- setup_guide: Optional[str] = None
-
-
-class CredentialChecker:
- """認証情報チェッカークラス"""
-
- def __init__(self, project_path: str = None):
- """
- Args:
- project_path: プロジェクトのパス(デフォルト: カレントディレクトリ)
- """
- self.project_path = Path(project_path or os.getcwd())
- self.template_path = Path.home() / "Desktop" / "git-worktree-agent"
-
- # .env ファイルを読み込み
- self._load_env()
-
- def _load_env(self):
- """環境変数を読み込み"""
- env_file = self.project_path / ".env"
-
- if DOTENV_AVAILABLE and env_file.exists():
- load_dotenv(env_file)
- print(f"✅ .env ファイルを読み込みました: {env_file}")
- elif env_file.exists():
- # dotenvがない場合は手動で読み込み
- with open(env_file, 'r') as f:
- for line in f:
- line = line.strip()
- if line and not line.startswith('#') and '=' in line:
- key, value = line.split('=', 1)
- os.environ[key.strip()] = value.strip()
- print(f"✅ .env ファイルを読み込みました(手動): {env_file}")
- else:
- print(f"⚠️ .env ファイルが見つかりません: {env_file}")
-
- def check_gcp_credentials(self) -> CredentialStatus:
- """GCP認証をチェック"""
- # 環境変数から取得
- cred_path = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS')
-
- if not cred_path:
- # フォールバック: テンプレート環境を探す
- template_cred = self.template_path / "credentials" / "gcp-workflow-key.json"
- if template_cred.exists():
- cred_path = str(template_cred)
- os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = cred_path
-
- if not cred_path:
- return CredentialStatus(
- service="GCP (Text-to-Speech & Imagen)",
- status="unconfigured",
- message="GOOGLE_APPLICATION_CREDENTIALS が設定されていません",
- setup_guide="1. Google Cloud Consoleでサービスアカウント作成\n"
- "2. Text-to-Speech API と Vertex AI APIを有効化\n"
- "3. 認証キーをダウンロード\n"
- "4. .env に GOOGLE_APPLICATION_CREDENTIALS を設定"
- )
-
- cred_file = Path(cred_path)
- if not cred_file.exists():
- return CredentialStatus(
- service="GCP (Text-to-Speech & Imagen)",
- status="missing",
- message=f"認証ファイルが見つかりません: {cred_path}",
- path=cred_path
- )
-
- # 認証ファイルの内容を検証
- try:
- with open(cred_file, 'r') as f:
- data = json.load(f)
- if 'type' in data and 'project_id' in data:
- return CredentialStatus(
- service="GCP (Text-to-Speech & Imagen)",
- status="ok",
- message=f"✓ プロジェクト: {data.get('project_id', 'unknown')}",
- path=cred_path
- )
- else:
- return CredentialStatus(
- service="GCP (Text-to-Speech & Imagen)",
- status="invalid",
- message="認証ファイルの形式が不正です",
- path=cred_path
- )
- except Exception as e:
- return CredentialStatus(
- service="GCP (Text-to-Speech & Imagen)",
- status="invalid",
- message=f"認証ファイルの読み込みエラー: {e}",
- path=cred_path
- )
-
- def check_github_credentials(self) -> CredentialStatus:
- """GitHub認証をチェック"""
- # GitHub CLIの認証状態を確認
- try:
- result = subprocess.run(
- ['gh', 'auth', 'status'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- if result.returncode == 0:
- # ユーザー名を取得
- user_result = subprocess.run(
- ['gh', 'api', 'user', '--jq', '.login'],
- capture_output=True,
- text=True,
- timeout=5
- )
-
- if user_result.returncode == 0:
- username = user_result.stdout.strip()
- return CredentialStatus(
- service="GitHub",
- status="ok",
- message=f"✓ ユーザー: {username}",
- path="gh CLI"
- )
-
- return CredentialStatus(
- service="GitHub",
- status="unconfigured",
- message="GitHub CLIが認証されていません",
- setup_guide="gh auth login を実行してください"
- )
-
- except FileNotFoundError:
- return CredentialStatus(
- service="GitHub",
- status="missing",
- message="GitHub CLIがインストールされていません",
- setup_guide="brew install gh を実行してください"
- )
- except Exception as e:
- return CredentialStatus(
- service="GitHub",
- status="invalid",
- message=f"エラー: {e}",
- path=None
- )
-
- def check_openai_credentials(self) -> CredentialStatus:
- """OpenAI認証をチェック(オプション)"""
- api_key = os.environ.get('OPENAI_API_KEY')
-
- if not api_key:
- return CredentialStatus(
- service="OpenAI",
- status="unconfigured",
- message="未設定(オプション)",
- setup_guide=".env に OPENAI_API_KEY を設定"
- )
-
- # APIキーの形式を簡易チェック
- if api_key.startswith('sk-') and len(api_key) > 20:
- return CredentialStatus(
- service="OpenAI",
- status="ok",
- message="✓ APIキーが設定されています",
- path="環境変数"
- )
- else:
- return CredentialStatus(
- service="OpenAI",
- status="invalid",
- message="APIキーの形式が不正です",
- path="環境変数"
- )
-
- def check_all(self) -> List[CredentialStatus]:
- """すべての認証情報をチェック"""
- return [
- self.check_gcp_credentials(),
- self.check_github_credentials(),
- self.check_openai_credentials(),
- ]
-
- def print_report(self):
- """チェック結果を表示"""
- results = self.check_all()
-
- print("\n" + "=" * 60)
- print("🔐 認証情報チェックレポート")
- print("=" * 60)
-
- all_ok = True
- required_missing = []
-
- for result in results:
- status_icon = {
- 'ok': '✅',
- 'missing': '❌',
- 'invalid': '⚠️',
- 'unconfigured': '⚪'
- }.get(result.status, '❓')
-
- print(f"\n{status_icon} {result.service}")
- print(f" 状態: {result.status}")
- print(f" {result.message}")
-
- if result.path:
- print(f" パス: {result.path}")
-
- if result.setup_guide:
- print(f"\n 📝 セットアップガイド:")
- for line in result.setup_guide.split('\n'):
- print(f" {line}")
-
- # 必須サービスのチェック
- if result.service in ["GCP (Text-to-Speech & Imagen)", "GitHub"]:
- if result.status != 'ok':
- all_ok = False
- required_missing.append(result.service)
-
- print("\n" + "=" * 60)
-
- if all_ok:
- print("✅ すべての必須認証が設定されています")
- print("\n🚀 ワークフローを実行できます:")
- print(" python3 src/workflow_orchestrator.py creative_webapp {app-name}")
- else:
- print("⚠️ 一部の必須認証が未設定です")
- print("\n❌ 未設定の必須サービス:")
- for service in required_missing:
- print(f" - {service}")
- print("\n📚 詳細なセットアップ手順:")
- print(" cat API_CREDENTIALS_SETUP.md")
-
- print("=" * 60 + "\n")
-
- return all_ok
-
- def export_env_template(self):
- """現在の環境変数を .env.template 形式で出力"""
- template_file = self.project_path / ".env.template"
-
- if template_file.exists():
- print(f"✅ .env.template が既に存在します: {template_file}")
- return
-
- # テンプレートをコピー
- source_template = self.template_path / ".env.template"
- if source_template.exists():
- import shutil
- shutil.copy(source_template, template_file)
- print(f"✅ .env.template を作成しました: {template_file}")
- else:
- print(f"❌ テンプレートが見つかりません: {source_template}")
-
-
-def main():
- """CLIエントリーポイント"""
- if len(sys.argv) > 1:
- project_path = sys.argv[1]
- else:
- project_path = os.getcwd()
-
- checker = CredentialChecker(project_path)
- all_ok = checker.print_report()
-
- # 終了コード
- sys.exit(0 if all_ok else 1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/delivery_organizer.py b/src/delivery_organizer.py
deleted file mode 100755
index 151c9af..0000000
--- a/src/delivery_organizer.py
+++ /dev/null
@@ -1,299 +0,0 @@
-#!/usr/bin/env python3
-"""
-DELIVERY準備スクリプト
-公開対象ファイルを収集し、DELIVERYフォルダを生成
-"""
-
-import os
-import shutil
-import json
-from pathlib import Path
-from typing import List, Dict, Optional, Set
-from dataclasses import dataclass, field
-from datetime import datetime
-
-from portfolio_config import get_config, PortfolioConfig
-
-
-@dataclass
-class DeliveryManifest:
- """DELIVERYマニフェスト"""
- app_name: str
- created_at: str
- source_path: str
- files: List[str] = field(default_factory=list)
- excluded: List[str] = field(default_factory=list)
- warnings: List[str] = field(default_factory=list)
- total_size: int = 0
-
- def to_dict(self) -> Dict:
- return {
- "app_name": self.app_name,
- "created_at": self.created_at,
- "source_path": self.source_path,
- "files_count": len(self.files),
- "files": self.files,
- "excluded_count": len(self.excluded),
- "excluded": self.excluded,
- "warnings": self.warnings,
- "total_size_bytes": self.total_size,
- "total_size_human": self._human_size(self.total_size),
- }
-
- def _human_size(self, size: int) -> str:
- for unit in ["B", "KB", "MB", "GB"]:
- if size < 1024:
- return f"{size:.1f} {unit}"
- size /= 1024
- return f"{size:.1f} TB"
-
-
-class DeliveryOrganizer:
- """DELIVERY準備オーガナイザー"""
-
- def __init__(self, config: PortfolioConfig = None):
- self.config = config or get_config()
-
- def prepare_delivery(
- self,
- source_dir: str,
- app_name: str,
- output_dir: str = None,
- ) -> DeliveryManifest:
- """
- DELIVERYフォルダを準備
-
- Args:
- source_dir: ソースディレクトリ(アプリのルート)
- app_name: アプリ名(リポジトリ内のフォルダ名)
- output_dir: 出力先(デフォルト: source_dir/DELIVERY)
-
- Returns:
- DeliveryManifest: 生成されたマニフェスト
- """
- source_path = Path(source_dir).resolve()
- output_path = Path(output_dir) if output_dir else source_path / "DELIVERY"
-
- # マニフェスト初期化
- manifest = DeliveryManifest(
- app_name=app_name,
- created_at=datetime.now().isoformat(),
- source_path=str(source_path),
- )
-
- print("\n" + "=" * 60)
- print(" DELIVERY準備")
- print("=" * 60)
- print(f"\n ソース: {source_path}")
- print(f" 出力先: {output_path}")
- print(f" アプリ名: {app_name}")
-
- # 既存のDELIVERYフォルダをクリア
- if output_path.exists():
- print(f"\n 既存のDELIVERYフォルダを削除中...")
- shutil.rmtree(output_path)
-
- output_path.mkdir(parents=True, exist_ok=True)
-
- # ファイルを収集
- print(f"\n ファイルを収集中...")
- collected_files = self._collect_files(source_path, output_path)
-
- # ファイルをコピー
- print(f"\n ファイルをコピー中...")
- for src_file, rel_path in collected_files:
- dest_file = output_path / rel_path
- dest_file.parent.mkdir(parents=True, exist_ok=True)
-
- try:
- shutil.copy2(src_file, dest_file)
- manifest.files.append(rel_path)
- manifest.total_size += src_file.stat().st_size
- except Exception as e:
- manifest.warnings.append(f"コピー失敗: {rel_path} - {e}")
-
- # 必須ファイルチェック
- missing_required = []
- for required in self.config.REQUIRED_FILES:
- if required not in manifest.files:
- missing_required.append(required)
- manifest.warnings.append(f"必須ファイルがありません: {required}")
-
- # 推奨ファイルチェック
- for recommended in self.config.RECOMMENDED_FILES:
- found = any(f.endswith(recommended) or f == recommended for f in manifest.files)
- if not found:
- # 警告ではなく情報として記録
- pass
-
- # マニフェストを保存
- manifest_path = output_path / ".delivery_manifest.json"
- with open(manifest_path, "w", encoding="utf-8") as f:
- json.dump(manifest.to_dict(), f, ensure_ascii=False, indent=2)
-
- # 結果表示
- self._print_summary(manifest, missing_required)
-
- return manifest
-
- def _collect_files(
- self,
- source_path: Path,
- output_path: Path,
- ) -> List[tuple]:
- """公開対象ファイルを収集"""
- collected = []
- excluded = []
-
- for file_path in source_path.rglob("*"):
- if not file_path.is_file():
- continue
-
- # 出力先自体は除外
- try:
- file_path.relative_to(output_path)
- continue
- except ValueError:
- pass
-
- # 相対パス
- rel_path = str(file_path.relative_to(source_path))
-
- # 除外チェック
- if self.config.should_exclude(rel_path):
- excluded.append(rel_path)
- continue
-
- # 拡張子チェック
- if not self.config.is_allowed_extension(rel_path):
- excluded.append(rel_path)
- continue
-
- collected.append((file_path, rel_path))
-
- print(f" 収集: {len(collected)} ファイル")
- print(f" 除外: {len(excluded)} ファイル")
-
- return collected
-
- def _print_summary(self, manifest: DeliveryManifest, missing_required: List[str]):
- """サマリーを表示"""
- print("\n" + "-" * 60)
- print(" 【DELIVERY準備完了】")
- print("-" * 60)
- print(f" ファイル数: {len(manifest.files)}")
- print(f" 合計サイズ: {manifest.to_dict()['total_size_human']}")
-
- if missing_required:
- print(f"\n ⚠️ 必須ファイルが不足しています:")
- for f in missing_required:
- print(f" - {f}")
-
- if manifest.warnings:
- print(f"\n ⚠️ 警告: {len(manifest.warnings)} 件")
- for w in manifest.warnings[:5]:
- print(f" - {w}")
- if len(manifest.warnings) > 5:
- print(f" ... 他 {len(manifest.warnings) - 5} 件")
-
- print("\n 【含まれるファイル】")
- for f in sorted(manifest.files)[:20]:
- print(f" - {f}")
- if len(manifest.files) > 20:
- print(f" ... 他 {len(manifest.files) - 20} ファイル")
-
- print("\n" + "=" * 60 + "\n")
-
-
-def prepare_from_worktree(
- worktree_path: str,
- app_name: str = None,
-) -> DeliveryManifest:
- """
- ワークツリーからDELIVERY準備
-
- Args:
- worktree_path: ワークツリーのパス
- app_name: アプリ名(省略時はフォルダ名から推測)
- """
- worktree = Path(worktree_path).resolve()
-
- # アプリ名を推測
- if not app_name:
- # worktrees/mission-v1 のような形式から取得
- app_name = worktree.name
- if app_name.startswith("mission-"):
- # PROJECT_INFO.yaml から取得を試みる
- project_info = worktree / "PROJECT_INFO.yaml"
- if project_info.exists():
- import yaml
- with open(project_info, "r", encoding="utf-8") as f:
- info = yaml.safe_load(f)
- if info and "project" in info:
- app_name = info["project"].get("name", app_name)
- # スペースをハイフンに変換、小文字化
- app_name = app_name.lower().replace(" ", "-")
-
- organizer = DeliveryOrganizer()
- return organizer.prepare_delivery(
- source_dir=str(worktree),
- app_name=app_name,
- )
-
-
-def find_and_prepare(base_dir: str = None) -> Optional[DeliveryManifest]:
- """
- 現在のディレクトリまたは指定ディレクトリからDELIVERYを準備
-
- 自動的にアプリのルートを検出
- """
- if base_dir:
- search_path = Path(base_dir)
- else:
- search_path = Path.cwd()
-
- # index.html があるディレクトリを探す
- if (search_path / "index.html").exists():
- app_name = search_path.name
- organizer = DeliveryOrganizer()
- return organizer.prepare_delivery(
- source_dir=str(search_path),
- app_name=app_name,
- )
-
- # worktrees 内を探す
- worktrees_dir = search_path / "worktrees"
- if worktrees_dir.exists():
- for worktree in worktrees_dir.iterdir():
- if worktree.is_dir() and (worktree / "index.html").exists():
- return prepare_from_worktree(str(worktree))
-
- print("❌ アプリのルートが見つかりません。")
- print(" index.html があるディレクトリで実行してください。")
- return None
-
-
-if __name__ == "__main__":
- import argparse
-
- parser = argparse.ArgumentParser(description="DELIVERY準備")
- parser.add_argument("source", nargs="?", help="ソースディレクトリ")
- parser.add_argument("-n", "--name", help="アプリ名")
- parser.add_argument("-o", "--output", help="出力先ディレクトリ")
- args = parser.parse_args()
-
- if args.source:
- organizer = DeliveryOrganizer()
- manifest = organizer.prepare_delivery(
- source_dir=args.source,
- app_name=args.name or Path(args.source).name,
- output_dir=args.output,
- )
- else:
- manifest = find_and_prepare()
-
- if manifest:
- print(f"✅ DELIVERY準備完了: {manifest.app_name}")
- print(f" ファイル数: {len(manifest.files)}")
- else:
- exit(1)
diff --git a/src/documentation_generator.py b/src/documentation_generator.py
deleted file mode 100755
index fe7e136..0000000
--- a/src/documentation_generator.py
+++ /dev/null
@@ -1,730 +0,0 @@
-#!/usr/bin/env python3
-"""
-ビジュアルドキュメント生成システム
-プロジェクトの成果物を視覚的に見やすいHTMLと解説台本として生成
-"""
-
-import json
-import os
-import re
-from typing import Dict, List, Any, Optional
-from datetime import datetime
-import base64
-
-class DocumentationGenerator:
- """ドキュメント生成クラス"""
-
- def __init__(self, project_name: str = "Project"):
- self.project_name = project_name
- self.sections = []
- self.narration_script = []
-
- def generate_visual_html(self,
- project_data: Dict,
- screenshots: List[str] = None,
- include_narration: bool = True) -> str:
- """視覚的に見やすいHTMLドキュメントを生成"""
-
- html = f"""
-
-
-
-
- {self.project_name} - プロジェクト解説
-
-
-
-
-
-
-
🚀 {self.project_name}
-
{project_data.get('description', 'Revolutionary Project Documentation')}
-
-"""
-
- # プロジェクト概要セクション
- html += self._generate_overview_section(project_data)
-
- # 機能紹介セクション
- html += self._generate_features_section(project_data)
-
- # 技術スタックセクション
- html += self._generate_tech_stack_section(project_data)
-
- # スクリーンショットギャラリー
- if screenshots:
- html += self._generate_screenshot_gallery(screenshots)
-
- # 開発タイムライン
- html += self._generate_timeline_section(project_data)
-
- # 統計情報
- html += self._generate_stats_section(project_data)
-
- # ナレーショントグルボタン
- if include_narration:
- html += """
-
- 🔊 解説を聞く
-
-"""
-
- html += """
-
-
-
-
-
-"""
-
- return html
-
- def _generate_overview_section(self, data: Dict) -> str:
- """概要セクションを生成"""
- return f"""
-
-
📋 プロジェクト概要
-
{data.get('overview', 'このプロジェクトは革新的なソリューションを提供します。')}
-
-
-
🎯
-
目的
-
{data.get('purpose', 'ユーザー体験の向上')}
-
-
-
👥
-
対象ユーザー
-
{data.get('target_users', '全てのユーザー')}
-
-
-
⏱️
-
開発期間
-
{data.get('duration', '2週間')}
-
-
-
-"""
-
- def _generate_features_section(self, data: Dict) -> str:
- """機能セクションを生成"""
- features = data.get('features', [])
- if not features:
- features = ['機能1', '機能2', '機能3']
-
- features_html = """
-
-
✨ 主要機能
-
-"""
-
- icons = ['🚀', '⚡', '🔧', '🎨', '📊', '🔒']
- for i, feature in enumerate(features[:6]):
- icon = icons[i % len(icons)]
- features_html += f"""
-
-
{icon}
-
{feature if isinstance(feature, str) else feature.get('name', 'Feature')}
-
{feature.get('description', '') if isinstance(feature, dict) else 'Amazing feature implementation'}
-
-"""
-
- features_html += """
-
-
-"""
- return features_html
-
- def _generate_tech_stack_section(self, data: Dict) -> str:
- """技術スタックセクションを生成"""
- tech_stack = data.get('tech_stack', ['JavaScript', 'Three.js', 'Node.js'])
-
- tech_html = """
-
-
🛠️ 技術スタック
-
-"""
-
- for tech in tech_stack:
- tech_html += f' {tech}\n'
-
- tech_html += """
-
-
-"""
- return tech_html
-
- def _generate_screenshot_gallery(self, screenshots: List[str]) -> str:
- """スクリーンショットギャラリーを生成"""
- gallery_html = """
-
-
📸 スクリーンショット
-
-"""
-
- for i, screenshot in enumerate(screenshots):
- gallery_html += f"""
-
-

-
-"""
-
- gallery_html += """
-
-
-"""
- return gallery_html
-
- def _generate_timeline_section(self, data: Dict) -> str:
- """開発タイムラインセクションを生成"""
- milestones = data.get('milestones', [
- {'date': 'Day 1', 'title': 'プロジェクト開始', 'description': '要件定義と設計'},
- {'date': 'Day 3', 'title': '基本実装', 'description': 'コア機能の実装'},
- {'date': 'Day 5', 'title': 'テスト・改善', 'description': 'バグ修正と最適化'},
- {'date': 'Day 7', 'title': 'リリース', 'description': '本番環境へのデプロイ'}
- ])
-
- timeline_html = """
-
-
📅 開発タイムライン
-
-"""
-
- for milestone in milestones:
- timeline_html += f"""
-
-
-
{milestone['title']}
-
{milestone['date']}
-
{milestone['description']}
-
-
-
-"""
-
- timeline_html += """
-
-
-"""
- return timeline_html
-
- def _generate_stats_section(self, data: Dict) -> str:
- """統計セクションを生成"""
- stats = data.get('stats', {
- 'files': 42,
- 'lines': 3500,
- 'commits': 128,
- 'performance': '60 FPS'
- })
-
- stats_html = """
-
-
📊 プロジェクト統計
-
-"""
-
- stat_items = [
- ('ファイル数', stats.get('files', 0), '📁'),
- ('コード行数', stats.get('lines', 0), '💻'),
- ('コミット数', stats.get('commits', 0), '🔄'),
- ('パフォーマンス', stats.get('performance', 'N/A'), '⚡')
- ]
-
- for label, value, icon in stat_items:
- stats_html += f"""
-
-
{value}
-
{icon} {label}
-
-"""
-
- stats_html += """
-
-
-"""
- return stats_html
-
- def generate_narration_script(self, project_data: Dict) -> str:
- """解説台本を生成"""
-
- script = f"""# {self.project_name} - 解説台本
-
-## オープニング(0:00 - 0:15)
-こんにちは。本日は、{self.project_name}プロジェクトについてご紹介いたします。
-このプロジェクトは、{project_data.get('description', '革新的なソリューション')}を実現するために開発されました。
-
-## プロジェクト概要(0:15 - 0:45)
-{self.project_name}の主な目的は、{project_data.get('purpose', 'ユーザー体験の向上')}です。
-対象となるユーザーは{project_data.get('target_users', '幅広いユーザー層')}で、
-約{project_data.get('duration', '2週間')}の開発期間を経て完成しました。
-
-## 主要機能の説明(0:45 - 1:30)
-それでは、主要な機能について説明します。
-"""
-
- features = project_data.get('features', [])
- for i, feature in enumerate(features[:3], 1):
- if isinstance(feature, dict):
- script += f"\n第{i}の機能は、{feature.get('name', 'Feature')}です。"
- script += f"これにより、{feature.get('description', 'ユーザーは効率的に作業ができます')}。"
- else:
- script += f"\n第{i}の機能は、{feature}です。"
-
- script += f"""
-
-## 技術的な実装(1:30 - 2:00)
-技術スタックには、{', '.join(project_data.get('tech_stack', ['最新技術']))}を採用しています。
-これらの技術を組み合わせることで、高いパフォーマンスと保守性を実現しました。
-
-## パフォーマンスと成果(2:00 - 2:30)
-プロジェクトの成果として、以下の数値を達成しました:
-- コード行数: {project_data.get('stats', {}).get('lines', '約3000')}行
-- パフォーマンス: {project_data.get('stats', {}).get('performance', '60FPS')}
-- 開発効率: 計画通りの期間で完成
-
-## まとめ(2:30 - 2:45)
-以上が{self.project_name}プロジェクトの概要です。
-このプロジェクトは、ユーザーに新しい価値を提供し、
-今後もさらなる改善を続けていく予定です。
-
-ご清聴ありがとうございました。
-
----
-
-## 読み上げ用マーカー
-
-
-
-"""
-
- return script
-
- def generate_tts_config(self) -> Dict:
- """Google TTS API用の設定を生成"""
-
- config = {
- "voice": {
- "languageCode": "ja-JP",
- "name": "ja-JP-Wavenet-B", # 男性の声
- # "name": "ja-JP-Wavenet-A", # 女性の声(選択可能)
- "ssmlGender": "MALE"
- },
- "audioConfig": {
- "audioEncoding": "MP3",
- "speakingRate": 1.0, # 話す速度(0.25-4.0)
- "pitch": 0.0, # ピッチ(-20.0-20.0)
- "volumeGainDb": 0.0, # 音量(-96.0-16.0)
- "effectsProfileId": ["headphone-class-device"] # オーディオプロファイル
- }
- }
-
- return config
-
- def prepare_ssml_text(self, script: str) -> str:
- """台本をSSML形式に変換"""
-
- # セクションの区切りでポーズを入れる
- ssml_text = script.replace('\n\n', '\n\n')
-
- # 数値を強調
- ssml_text = re.sub(r'(\d+)', r'\1', ssml_text)
-
- # 重要な単語を強調
- important_words = ['主要', '重要', '革新的', '成功', '完成']
- for word in important_words:
- ssml_text = ssml_text.replace(word, f'{word}')
-
- # SSML タグでラップ
- ssml = f"""
-{ssml_text}
-"""
-
- return ssml
-
- def save_documentation(self,
- html_content: str,
- script_content: str,
- output_dir: str = "./docs") -> Dict:
- """生成したドキュメントを保存"""
-
- os.makedirs(output_dir, exist_ok=True)
-
- # HTMLファイルを保存
- html_path = os.path.join(output_dir, "project_presentation.html")
- with open(html_path, 'w', encoding='utf-8') as f:
- f.write(html_content)
-
- # 台本を保存
- script_path = os.path.join(output_dir, "narration_script.md")
- with open(script_path, 'w', encoding='utf-8') as f:
- f.write(script_content)
-
- # SSML形式の台本も保存
- ssml_content = self.prepare_ssml_text(script_content)
- ssml_path = os.path.join(output_dir, "narration_script.ssml")
- with open(ssml_path, 'w', encoding='utf-8') as f:
- f.write(ssml_content)
-
- # TTS設定を保存
- tts_config = self.generate_tts_config()
- tts_config_path = os.path.join(output_dir, "tts_config.json")
- with open(tts_config_path, 'w', encoding='utf-8') as f:
- json.dump(tts_config, f, ensure_ascii=False, indent=2)
-
- return {
- "html": html_path,
- "script": script_path,
- "ssml": ssml_path,
- "tts_config": tts_config_path
- }
\ No newline at end of file
diff --git a/src/documenter_agent.py b/src/documenter_agent.py
deleted file mode 100755
index dee32e6..0000000
--- a/src/documenter_agent.py
+++ /dev/null
@@ -1,1504 +0,0 @@
-#!/usr/bin/env python3
-"""
-Documenterエージェント - ドキュメントと音声解説生成
-about.html と explanation.mp3 を自動生成
-
-音声生成: Gemini 2.5 Flash Preview TTS API を使用
-- SSMLを使わず自然言語から高品質な音声を生成
-- APIキーのみで利用可能(サービスアカウント不要)
-"""
-
-import os
-import sys
-import json
-import re
-import wave
-import uuid
-import subprocess
-from pathlib import Path
-from datetime import datetime
-
-# Gemini TTS用のインポート
-try:
- from google import genai
- from google.genai import types
- GEMINI_TTS_AVAILABLE = True
-except ImportError:
- print("警告: google-genaiがインストールされていません")
- print("インストール: pip install google-genai")
- GEMINI_TTS_AVAILABLE = False
-
-# 音声変換用
-try:
- from pydub import AudioSegment
- PYDUB_AVAILABLE = True
-except ImportError:
- print("警告: pydubがインストールされていません")
- print("インストール: pip install pydub")
- PYDUB_AVAILABLE = False
-
-# 環境変数読み込み
-try:
- from dotenv import load_dotenv
- # グローバル設定を読み込み
- global_env = Path.home() / ".config" / "ai-agents" / "profiles" / "default.env"
- if global_env.exists():
- load_dotenv(global_env)
-except ImportError:
- pass # dotenvがなくても環境変数は読める
-
-class DocumenterAgent:
- """ドキュメント生成エージェント"""
-
- def __init__(self, project_path="."):
- self.project_path = Path(project_path)
- self.gcp_skill_path = Path.home() / ".claude" / "skills" / "gcp-skill"
-
- # project/public/ ディレクトリを確保
- self.public_path = self.project_path / "project" / "public"
- self.public_path.mkdir(parents=True, exist_ok=True)
-
- def generate_about_html(self, project_info):
- """about.html(プロジェクト解説ページ)を生成 - 日英切り替え式(デフォルト: 日本語)"""
- project_name = project_info.get('name', 'プロジェクト')
- project_type = project_info.get('type', 'web')
-
- html_content = f"""
-
-
-
-
- {project_name} - プロジェクト解説 / Project Overview
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
📋 プロジェクト概要
-
📋 Project Overview
-
-
- このプロジェクトは、Claude Code の AIエージェントシステムにより、
- 要件定義から実装、テスト、ドキュメント生成まで 完全自動化 されたプロセスで開発されました。
-
-
- 人間の開発者が行ったのは「要件を伝える」ことだけ。
- あとはAIエージェントたちが協調して、プロダクションレベルのアプリケーションを生成しました。
-
-
-
-
- This project was developed through a fully automated process using Claude Code's AI Agent system,
- covering everything from requirements definition to implementation, testing, and documentation generation.
-
-
- The human developer only provided the requirements.
- The AI agents collaborated to generate a production-level application.
-
-
-
-
-
-
✨ 主要機能
-
✨ Key Features
-
-
-
🎯 機能1Feature 1
-
ユーザーフレンドリーなインターフェース
-
User-friendly interface
-
-
-
⚡ 機能2Feature 2
-
高速なレスポンス処理
-
Fast response processing
-
-
-
🔒 機能3Feature 3
-
セキュアなデータ管理
-
Secure data management
-
-
-
📊 機能4Feature 4
-
リアルタイム更新
-
Real-time updates
-
-
-
-
-
-
🛠 技術スタック
-
🛠 Tech Stack
-
- JavaScript
- HTML5
- CSS3
- Node.js
- AI Agent
-
-
-
-
-
🤖 AI開発プロセス
-
🤖 AI Development Process
-
- - 要件分析: Requirements Analyst が要件を整理・明確化
- - 計画立案: Planner が WBS(作業分解構造)を作成
- - テスト設計: Test Designer がテストコードを先行作成(TDD)
- - 並列開発: 複数の Developer エージェントが同時開発
- - 品質保証: Evaluator が品質チェック、Fixer が修正
- - 文書生成: Documenter が解説とマニュアルを自動生成
-
-
- - Requirements Analysis: Requirements Analyst organizes and clarifies requirements
- - Planning: Planner creates WBS (Work Breakdown Structure)
- - Test Design: Test Designer creates test code first (TDD)
- - Parallel Development: Multiple Developer agents work simultaneously
- - Quality Assurance: Evaluator checks quality, Fixer makes corrections
- - Documentation: Documenter auto-generates explanations and manuals
-
-
-
-
-
🎧 音声解説
-
🎧 Audio Explanation
-
AIが生成した音声で、このプロジェクトの詳細を解説します
-
AI-generated audio explaining the details of this project
-
-
-
-
-
📊 開発メトリクス
-
📊 Development Metrics
-
- - ⏱ 開発時間: 約1-2時間(従来の開発の10倍速)
- - 👥 投入エージェント数: 8体
- - 📝 自動生成コード行数: 1000行以上
- - ✅ テストカバレッジ: 80%以上
- - 📄 自動生成ドキュメント: 5種類以上
-
-
- - ⏱ Development Time: About 1-2 hours (10x faster than traditional)
- - 👥 Agents Deployed: 8
- - 📝 Auto-generated Code Lines: 1000+
- - ✅ Test Coverage: 80%+
- - 📄 Auto-generated Documents: 5+ types
-
-
-
-
-
🏆 このプロジェクトが実証すること
-
🏆 What This Project Demonstrates
-
- AIエージェントを活用することで、
- 開発速度10倍、品質の標準化、完全な自動化
- が実現可能であることを証明しています。
-
-
- By leveraging AI agents, we demonstrate that
- 10x development speed, standardized quality, and full automation
- are achievable.
-
-
- 🚀 The Future of Development is Here
-
-
-
-
-
-
-
-"""
-
- # about.html を保存(project/public/ に出力)
- about_path = self.public_path / "about.html"
- with open(about_path, 'w', encoding='utf-8') as f:
- f.write(html_content)
-
- print(f"✅ about.html を生成しました(日英切り替え式、デフォルト: 日本語): {about_path}")
- return about_path
-
- def generate_audio_prompts_json(self, project_info):
- """AUDIO_PROMPTS.json を生成(ゲームプロジェクト用)
-
- ゲーム用のBGMと効果音のプロンプトを自動生成します。
- 既存ファイルがある場合、プロジェクト名が一致するかチェックし、
- 一致しない場合は削除して再生成します(古いプロジェクトの残骸を防ぐため)。
- """
- project_name = project_info.get('name', 'プロジェクト')
- project_type = project_info.get('type', 'web')
-
- # ゲームプロジェクトかどうか判定
- is_game = 'game' in project_type.lower() or 'ゲーム' in project_name.lower()
-
- if not is_game:
- print(f"ℹ️ プロジェクトタイプ '{project_type}' はゲームではありません - AUDIO_PROMPTS.json をスキップ")
- return None
-
- print(f"🎮 ゲームプロジェクト検出: {project_name}")
-
- # 既存のAUDIO_PROMPTS.jsonをチェック
- existing_prompts_path = self.project_path / "AUDIO_PROMPTS.json"
- if existing_prompts_path.exists():
- try:
- with open(existing_prompts_path, 'r', encoding='utf-8') as f:
- existing_data = json.load(f)
-
- existing_project_name = existing_data.get('project_name', '')
-
- if existing_project_name == project_name:
- print(f"✅ AUDIO_PROMPTS.json 既存(同じプロジェクト: {existing_project_name})- 使用")
- return existing_prompts_path
- else:
- print(f"⚠️ AUDIO_PROMPTS.json 既存(別プロジェクト: {existing_project_name})")
- print(f" 現在のプロジェクト: {project_name}")
- print(f" → 削除して再生成します")
- existing_prompts_path.unlink()
- except (json.JSONDecodeError, KeyError):
- print(f"⚠️ AUDIO_PROMPTS.json が破損しています - 削除して再生成します")
- existing_prompts_path.unlink()
-
- print(f"🎵 AUDIO_PROMPTS.json を生成します...")
-
- # ゲームジャンル推測(プロジェクト名から)
- genre = "retro arcade" # デフォルト
- if 'space' in project_name.lower() or 'invader' in project_name.lower():
- genre = "retro space shooter"
- elif 'puzzle' in project_name.lower():
- genre = "casual puzzle"
- elif 'rpg' in project_name.lower():
- genre = "RPG adventure"
- elif 'action' in project_name.lower():
- genre = "action platformer"
-
- # AUDIO_PROMPTS.json のテンプレート
- audio_prompts = {
- "project_name": project_name,
- "genre": genre,
- "bgm": [
- {
- "name": "main_theme",
- "prompt": f"8-bit {genre} background music, upbeat, adventurous, chiptune style, 120 BPM, synthesizer heavy, loopable",
- "negative_prompt": "vocals, lyrics, acoustic instruments, drums",
- "duration": 30,
- "bpm": 120,
- "loop": True,
- "file": "assets/audio/bgm_main.wav"
- },
- {
- "name": "game_over",
- "prompt": f"8-bit {genre} game over theme, sad, slow tempo, minor key, retro synthesizer, 80 BPM",
- "negative_prompt": "vocals, upbeat, major key, happy",
- "duration": 10,
- "bpm": 80,
- "loop": False,
- "file": "assets/audio/bgm_game_over.wav"
- }
- ],
- "sfx": [
- {
- "name": "player_action",
- "prompt": f"8-bit {genre} player action sound effect, short, sharp, retro game style, punchy",
- "duration": 1,
- "file": "assets/audio/sfx_action.wav"
- },
- {
- "name": "enemy_hit",
- "prompt": f"8-bit {genre} enemy hit sound effect, retro game style, impact sound, short burst",
- "duration": 1,
- "file": "assets/audio/sfx_enemy_hit.wav"
- },
- {
- "name": "item_collect",
- "prompt": f"8-bit {genre} item collect sound, cheerful, short ping, retro game style, reward sound",
- "duration": 0.5,
- "file": "assets/audio/sfx_item.wav"
- }
- ]
- }
-
- # 保存
- # project/ 配下に保存(内部ドキュメント)
- prompts_path = self.project_path / "project" / "AUDIO_PROMPTS.json"
- prompts_path.parent.mkdir(parents=True, exist_ok=True)
- with open(prompts_path, 'w', encoding='utf-8') as f:
- json.dump(audio_prompts, f, indent=2, ensure_ascii=False)
-
- print(f"✅ AUDIO_PROMPTS.json を生成しました: {prompts_path}")
- print(f" BGM: {len(audio_prompts['bgm'])}曲")
- print(f" 効果音: {len(audio_prompts['sfx'])}音")
-
- return prompts_path
-
- def generate_audio_script(self, project_info):
- """音声スクリプトを生成(Gemini TTS向け - 自然言語で間を指示)
-
- Gemini 2.5 Flash Preview TTS は SSML を使わず、自然言語の指示で
- 適切な間を入れてくれるため、シンプルなテキストを生成します。
- """
- project_name = project_info.get('name', 'プロジェクト')
-
- # 自然言語スクリプト(Gemini TTS向け)
- # 句読点や文構造で適切な間を認識してくれる
- script = f"""こんにちは。{project_name}プロジェクトの解説を始めます。
-
-このプロジェクトは、Claude Codeの AIエージェントシステムにより、完全自動で開発されました。
-人間の開発者は要件を伝えただけで、あとはすべてAIが自動的に実装しました。
-
-開発プロセスは以下の通りです。
-
-まず、要件定義エージェントが、ユーザーの要望を分析し、明確な仕様書を作成します。
-次に、計画エージェントが、作業を細かなタスクに分解し、最適な実行順序を決定します。
-そして、テスト設計エージェントが、テストファーストアプローチで、先にテストコードを作成します。
-
-その後、複数の開発エージェントが並列で動作し、フロントエンド、バックエンド、データベースなどを同時に実装します。
-品質評価エージェントが、コードの品質をチェックし、問題があれば修正エージェントが自動的に改善します。
-
-最後に、ドキュメント生成エージェントが、このような解説ページや音声ファイルを自動生成します。
-
-この一連のプロセスは、わずか1時間から2時間で完了し、従来の開発と比べて10倍以上の速度を実現しています。
-しかも、品質は一定に保たれ、テストカバレッジも80%以上を達成しています。
-
-このプロジェクトは、AIエージェントを活用した次世代の開発手法の可能性を示しています。
-将来的には、このような自動開発が当たり前になり、人間はより創造的な作業に集中できるようになるでしょう。
-
-以上で、{project_name}プロジェクトの解説を終わります。
-ご清聴ありがとうございました。"""
-
- # スクリプトを保存
- script_path = self.public_path / "audio_script.txt"
- with open(script_path, 'w', encoding='utf-8') as f:
- f.write(script.strip())
-
- print(f"✅ 音声スクリプトを生成しました: {script_path}")
- return script_path
-
- def auto_insert_ssml_pauses(self, text):
- """テキストに自動的にSSML pause(間)を挿入
-
- 仕様:
- - 句点(。): 0.3秒の間
- - 読点(、): 0.5秒の間
- - タイトル遷移(## で始まる行): 1秒の間
- - ページ遷移(# で始まる行): 1秒の間
-
- Args:
- text: 元のテキスト
-
- Returns:
- str: SSML pause挿入済みのテキスト
- """
- import re
-
- lines = text.split('\n')
- processed_lines = []
-
- for line in lines:
- # タイトル遷移(## または #)
- if line.strip().startswith('##') or line.strip().startswith('#'):
- # タイトル前に1秒の間
- if processed_lines: # 最初のタイトルは間を入れない
- processed_lines.append('[pause:1s]')
- processed_lines.append(line)
- # タイトル後に1秒の間
- processed_lines.append('[pause:1s]')
- continue
-
- # 句点と読点に間を挿入
- modified_line = line
- # 句点の後に0.3秒の間(すでにSSMLがある場合はスキップ)
- if '。' in modified_line and '[pause:' not in modified_line:
- modified_line = re.sub(r'。', '。[pause:0.3s]', modified_line)
-
- # 読点の後に0.5秒の間(すでにSSMLがある場合はスキップ)
- if '、' in modified_line and '[pause:' not in modified_line:
- modified_line = re.sub(r'、', '、[pause:0.5s]', modified_line)
-
- processed_lines.append(modified_line)
-
- return '\n'.join(processed_lines)
-
- def convert_ssml_pause_to_google_ssml(self, text):
- """簡易SSML記法([pause:Xs])をGoogle TTS API の SSML形式に変換
-
- 例: [pause:1s] →
- """
- import re
-
- # pauseタグの変換
- text = re.sub(r'\[pause:([0-9.]+)(s|ms)\]', r'', text)
-
- # SSML全体をタグで囲む
- if '{text}'
- return text, True
-
- return text, False
-
- def split_text_by_byte_limit(self, text, max_bytes=4500):
- """テキストをバイト数制限で分割(改行や句点で区切る)
-
- 参考: Gemini_コンテンツ作成自動化ツールのapp.py実装を使用
-
- Args:
- text: 分割するテキスト
- max_bytes: 最大バイト数(デフォルト4500、5000より少し小さめに設定)
-
- Returns:
- list: 分割されたテキストのリスト
- """
- chunks = []
- current_chunk = ""
-
- # 改行で分割
- lines = text.split('\n')
-
- for line in lines:
- test_chunk = current_chunk + line + '\n'
-
- # バイト数をチェック
- if len(test_chunk.encode('utf-8')) > max_bytes:
- if current_chunk:
- chunks.append(current_chunk.strip())
- current_chunk = line + '\n'
- else:
- # 1行が制限を超える場合は句点で分割
- sentences = line.split('。')
- for sentence in sentences:
- if sentence:
- test_sentence = current_chunk + sentence + '。'
- if len(test_sentence.encode('utf-8')) > max_bytes:
- if current_chunk:
- chunks.append(current_chunk.strip())
- current_chunk = sentence + '。'
- else:
- current_chunk = test_sentence
- else:
- current_chunk = test_chunk
-
- if current_chunk.strip():
- chunks.append(current_chunk.strip())
-
- return chunks
-
- def generate_audio_with_gemini(self, script_path, output_path=None, voice_name="Kore"):
- """Gemini 2.5 Flash Preview TTS を使用して音声を生成
-
- 特徴:
- - APIキーのみで利用可能(サービスアカウント不要)
- - SSMLを使わず自然言語から高品質な音声を生成
- - 日本語に対応した高品質な音声
-
- Args:
- script_path: 音声スクリプトファイルのパス
- output_path: 出力先MP3ファイルのパス(デフォルト: project/public/explanation.mp3)
- voice_name: 使用する音声名(デフォルト: Kore - 日本語対応の男性音声)
- 利用可能: Aoede, Charon, Fenrir, Kore, Puck, etc.
-
- Returns:
- Path: 生成された音声ファイルのパス、失敗時はNone
- """
- # 依存関係チェック
- if not GEMINI_TTS_AVAILABLE:
- print("❌ google-genai がインストールされていません")
- print("インストール: pip install google-genai")
- return None
-
- if not PYDUB_AVAILABLE:
- print("❌ pydub がインストールされていません")
- print("インストール: pip install pydub")
- return None
-
- # APIキー取得
- api_key = os.environ.get('GEMINI_API_KEY')
- if not api_key:
- print("❌ GEMINI_API_KEY 環境変数が設定されていません")
- print("設定方法:")
- print(" export GEMINI_API_KEY='your-api-key'")
- print("または ~/.config/ai-agents/profiles/default.env に追加:")
- print(" GEMINI_API_KEY=your-api-key")
- return None
-
- # デフォルトの出力先: project/public/explanation.mp3
- if output_path is None:
- output_path = self.public_path / "explanation.mp3"
-
- # スクリプトを読み込み
- with open(script_path, 'r', encoding='utf-8') as f:
- text = f.read().strip()
-
- print(f"🎤 Gemini TTS で音声生成を開始...")
- print(f" 音声: {voice_name}")
- print(f" テキスト長: {len(text)} 文字")
-
- try:
- # Gemini クライアントを作成
- client = genai.Client(api_key=api_key)
-
- # テキストを分割(5000バイト制限対応)
- chunks = self.split_text_by_byte_limit(text, max_bytes=4500)
- print(f" チャンク数: {len(chunks)}")
-
- temp_files = []
-
- for i, chunk in enumerate(chunks):
- print(f" 🔊 チャンク {i+1}/{len(chunks)} を生成中...")
-
- # Gemini TTS API 呼び出し
- response = client.models.generate_content(
- model="gemini-2.5-flash-preview-tts",
- contents=chunk,
- config=types.GenerateContentConfig(
- response_modalities=["AUDIO"],
- speech_config=types.SpeechConfig(
- voice_config=types.VoiceConfig(
- prebuilt_voice_config=types.PrebuiltVoiceConfig(
- voice_name=voice_name,
- )
- )
- ),
- )
- )
-
- # PCMデータを取得
- pcm_data = response.candidates[0].content.parts[0].inline_data.data
- mime_type = response.candidates[0].content.parts[0].inline_data.mime_type
-
- # PCMデータ品質検証(デバッグ用)
- import struct
- if len(pcm_data) >= 2:
- samples = struct.unpack(f'<{len(pcm_data)//2}h', pcm_data)
- sample_max = max(samples)
- sample_min = min(samples)
- sample_avg = sum(samples) / len(samples)
- print(f" 📊 PCM品質: {len(pcm_data)}bytes, max={sample_max}, min={sample_min}, avg={sample_avg:.1f}")
- if sample_max < 1000 and sample_min > -1000:
- print(f" ⚠️ 警告: PCMデータが無音に近い(振幅が小さい)")
- else:
- print(f" ❌ エラー: PCMデータが空または不正")
-
- # 一時WAVファイルに保存
- temp_wav = self.public_path / f"temp_chunk_{i}_{uuid.uuid4().hex[:8]}.wav"
- with wave.open(str(temp_wav), 'wb') as wf:
- wf.setnchannels(1) # モノラル
- wf.setsampwidth(2) # 16-bit
- wf.setframerate(24000) # 24kHz
- wf.writeframes(pcm_data)
-
- temp_files.append(temp_wav)
-
- # 複数チャンクの場合は結合
- if len(temp_files) == 1:
- # 単一チャンク: WAV → MP3 変換
- audio = AudioSegment.from_wav(str(temp_files[0]))
- audio.export(str(output_path), format='mp3')
- else:
- # 複数チャンク: 結合してからMP3変換
- print(f" 🔗 {len(temp_files)} 個のチャンクを結合中...")
- combined = AudioSegment.empty()
- for temp_wav in temp_files:
- audio = AudioSegment.from_wav(str(temp_wav))
- combined += audio
- combined.export(str(output_path), format='mp3')
-
- # 一時ファイルを削除
- for temp_wav in temp_files:
- if temp_wav.exists():
- temp_wav.unlink()
-
- # 生成されたファイルの検証
- if output_path.exists():
- file_size = output_path.stat().st_size
- print(f"✅ 音声ファイル生成完了: {output_path}")
- print(f" 📁 ファイルサイズ: {file_size:,} bytes ({file_size/1024:.1f} KB)")
- if file_size < 1000:
- print(f" ⚠️ 警告: ファイルサイズが小さすぎます(破損の可能性)")
- else:
- print(f"❌ 音声ファイルが見つかりません: {output_path}")
- return None
-
- return output_path
-
- except Exception as e:
- print(f"❌ Gemini TTS エラー: {e}")
- import traceback
- traceback.print_exc()
- # 一時ファイルをクリーンアップ
- for temp_wav in temp_files if 'temp_files' in dir() else []:
- if temp_wav.exists():
- temp_wav.unlink()
- return None
-
- def resolve_gcp_credentials_path(self):
- """階層型設定システムに基づいてGCP認証パスを解決
-
- 優先順位:
- 1. ローカル設定(プロジェクト固有): ./ai-agents-config/credentials/gcp.json
- 2. 専用環境の認証: ./credentials/gcp-workflow-key.json
- 3. 親ディレクトリの認証: ../credentials/gcp-workflow-key.json
- 4. グローバル設定: ~/.config/ai-agents/credentials/gcp/default.json
- 5. テンプレート環境: ~/Desktop/git-worktree-agent/credentials/gcp-workflow-key.json
-
- Returns:
- Path or None: 認証ファイルのパス、見つからない場合はNone
- """
- # 候補パスを優先順位順に定義
- candidate_paths = [
- self.project_path / "ai-agents-config" / "credentials" / "gcp.json", # ローカル設定
- self.project_path / "credentials" / "gcp-workflow-key.json", # 専用環境ルート
- self.project_path.parent / "credentials" / "gcp-workflow-key.json", # 親ディレクトリ(worktree内から実行時)
- Path.home() / ".config" / "ai-agents" / "credentials" / "gcp" / "default.json", # グローバル設定
- Path.home() / "Desktop" / "git-worktree-agent" / "credentials" / "gcp-workflow-key.json", # テンプレート環境
- ]
-
- for path in candidate_paths:
- if path.exists():
- print(f"✅ GCP認証ファイルを検出: {path}")
- return path
-
- print("⚠️ GCP認証ファイルが見つかりません")
- print("検索したパス:")
- for path in candidate_paths:
- print(f" - {path}")
-
- return None
-
- def setup_gcp_credentials_auto(self):
- """既存のAPI管理システム(GCPスキル)を使用してGCP認証を自動セットアップ"""
- try:
- # プロジェクトID取得
- result = subprocess.run(
- ['gcloud', 'config', 'get-value', 'project'],
- capture_output=True, text=True, timeout=10
- )
- project_id = result.stdout.strip()
-
- if not project_id:
- print("❌ GCPプロジェクトが設定されていません")
- print("以下のコマンドを実行してください:")
- print(" gcloud auth login")
- print(" gcloud config set project YOUR_PROJECT_ID")
- return False
-
- print(f"📋 プロジェクトID: {project_id}")
-
- # Text-to-Speech API有効化
- print("🔧 Text-to-Speech APIを有効化中...")
- subprocess.run(
- ['gcloud', 'services', 'enable', 'texttospeech.googleapis.com', f'--project={project_id}'],
- check=True, timeout=30
- )
-
- # サービスアカウント確認・作成
- sa_name = "ai-agent"
- sa_email = f"{sa_name}@{project_id}.iam.gserviceaccount.com"
-
- # サービスアカウント存在確認
- result = subprocess.run(
- ['gcloud', 'iam', 'service-accounts', 'describe', sa_email, f'--project={project_id}'],
- capture_output=True, timeout=10
- )
-
- if result.returncode != 0:
- print(f"🔧 サービスアカウント作成中: {sa_email}")
- subprocess.run(
- ['gcloud', 'iam', 'service-accounts', 'create', sa_name,
- '--display-name=AI Agent (TTS + Imagen)',
- f'--project={project_id}'],
- check=True, timeout=30
- )
- else:
- print(f"✅ サービスアカウント既存: {sa_email}")
-
- # 権限付与(TTS用)
- print("🔧 権限を確認中...")
- subprocess.run(
- ['gcloud', 'projects', 'add-iam-policy-binding', project_id,
- f'--member=serviceAccount:{sa_email}',
- '--role=roles/cloudtts.admin'],
- capture_output=True, timeout=30
- )
-
- # 認証キー生成(環境に応じて保存先を決定)
- # 優先順位: 専用環境 > グローバル設定 > テンプレート環境
- if (self.project_path / "credentials").exists():
- cred_path = self.project_path / "credentials" / "gcp-workflow-key.json"
- elif (self.project_path.parent / "credentials").exists():
- cred_path = self.project_path.parent / "credentials" / "gcp-workflow-key.json"
- elif (Path.home() / ".config" / "ai-agents" / "credentials" / "gcp").exists():
- cred_path = Path.home() / ".config" / "ai-agents" / "credentials" / "gcp" / "default.json"
- else:
- cred_path = Path.home() / "Desktop" / "git-worktree-agent" / "credentials" / "gcp-workflow-key.json"
-
- cred_path.parent.mkdir(parents=True, exist_ok=True)
-
- if not cred_path.exists():
- print(f"🔑 認証キー生成中: {cred_path}")
- subprocess.run(
- ['gcloud', 'iam', 'service-accounts', 'keys', 'create',
- str(cred_path),
- f'--iam-account={sa_email}',
- f'--project={project_id}'],
- check=True, timeout=30
- )
- cred_path.chmod(0o600)
- print(f"✅ 認証キー生成完了: {cred_path}")
- else:
- print(f"✅ 認証キー既存: {cred_path}")
-
- return True
-
- except subprocess.TimeoutExpired:
- print("❌ タイムアウト: GCPコマンドの実行に時間がかかりすぎています")
- return False
- except subprocess.CalledProcessError as e:
- print(f"❌ GCPコマンド実行エラー: {e}")
- return False
- except Exception as e:
- print(f"❌ 予期しないエラー: {e}")
- return False
-
- def generate_audio_file(self, tts_script_path, output_path):
- """音声ファイルを実際に生成"""
- try:
- print("\n📦 npm依存関係をインストール中...")
- subprocess.run(
- ['npm', 'install', '@google-cloud/text-to-speech'],
- cwd=self.project_path,
- check=True,
- timeout=120
- )
-
- print("🎤 音声生成を開始...")
- # 階層型設定システムで認証パスを解決
- cred_path = self.resolve_gcp_credentials_path()
- if not cred_path:
- print("❌ GCP認証ファイルが見つかりません")
- return False
-
- env = os.environ.copy()
- env['GOOGLE_APPLICATION_CREDENTIALS'] = str(cred_path)
-
- subprocess.run(
- ['node', str(tts_script_path)],
- cwd=self.project_path,
- env=env,
- check=True,
- timeout=60
- )
-
- print(f"✅ 音声ファイル生成完了: {output_path}")
- return True
-
- except subprocess.CalledProcessError as e:
- print(f"❌ 音声生成エラー: {e}")
- return False
- except Exception as e:
- print(f"❌ 予期しないエラー: {e}")
- return False
-
- def generate_audio_with_gcp(self, script_path, output_path=None, auto_ssml=True):
- """GCP Text-to-Speech を使用して音声を生成(SSML自動挿入・分割・結合対応)
-
- Args:
- script_path: 音声スクリプトファイルのパス
- output_path: 出力先MP3ファイルのパス
- auto_ssml: 句点・読点に自動的にSSML pauseを挿入するか(デフォルト: True)
- """
-
- # デフォルトの出力先: project/public/explanation.mp3
- if output_path is None:
- output_path = self.public_path / "explanation.mp3"
-
- # スクリプトを読み込み
- with open(script_path, 'r', encoding='utf-8') as f:
- original_text = f.read()
-
- # 自動SSML挿入(オプション)
- if auto_ssml:
- print("🔧 自動SSML挿入: 句点・読点・タイトル遷移に間を追加中...")
- processed_text = self.auto_insert_ssml_pauses(original_text)
- else:
- processed_text = original_text
-
- # 階層型設定システムで認証パスを解決
- cred_path = self.resolve_gcp_credentials_path()
- cred_path_str = str(cred_path) if cred_path else ''
-
- # Google Cloud TTS用のスクリプトを生成(分割・結合対応)
- tts_script = f"""
-const fs = require('fs');
-const textToSpeech = require('@google-cloud/text-to-speech');
-
-// クライアントを作成(環境変数または明示的なキーファイル)
-const clientOptions = process.env.GOOGLE_APPLICATION_CREDENTIALS
- ? {{}}
- : {{ keyFilename: '{cred_path_str}' }};
-const client = new textToSpeech.TextToSpeechClient(clientOptions);
-
-// SSML変換関数
-function convertToSSML(text) {{
- // [pause:Xs] →
- let ssml = text.replace(/\\[pause:([0-9.]+)(s|ms)\\]/g, '');
-
- // SSMLタグがある場合のみで囲む
- if (ssml.includes('' + ssml + '';
- }}
- return text;
-}}
-
-// テキストをバイト数制限で分割
-function splitTextByByteLimit(text, maxBytes = 4500) {{
- const chunks = [];
- let currentChunk = "";
-
- const lines = text.split('\\n');
-
- for (const line of lines) {{
- const testChunk = currentChunk + line + '\\n';
-
- if (Buffer.byteLength(testChunk, 'utf-8') > maxBytes) {{
- if (currentChunk) {{
- chunks.push(currentChunk.trim());
- currentChunk = line + '\\n';
- }} else {{
- // 1行が制限を超える場合は句点で分割
- const sentences = line.split('。');
- for (const sentence of sentences) {{
- if (sentence) {{
- const testSentence = currentChunk + sentence + '。';
- if (Buffer.byteLength(testSentence, 'utf-8') > maxBytes) {{
- if (currentChunk) {{
- chunks.push(currentChunk.trim());
- }}
- currentChunk = sentence + '。';
- }} else {{
- currentChunk = testSentence;
- }}
- }}
- }}
- }}
- }} else {{
- currentChunk = testChunk;
- }}
- }}
-
- if (currentChunk.trim()) {{
- chunks.push(currentChunk.trim());
- }}
-
- return chunks;
-}}
-
-async function generateSpeech() {{
- // テキストを読み込み
- const rawText = `{processed_text.replace("`", "\\`")}`;
-
- // SSML変換
- const ssmlText = convertToSSML(rawText);
- const isSSML = ssmlText.includes('');
-
- console.log(isSSML ? '✅ SSML形式で音声生成します(間あり)' : 'ℹ️ テキスト形式で音声生成します');
-
- // バイト数チェック
- const textBytes = Buffer.byteLength(ssmlText, 'utf-8');
- console.log(`📊 テキストバイト数: ${{textBytes}}`);
-
- // 5000バイト以下ならそのまま生成
- if (textBytes <= 4500) {{
- const request = {{
- input: isSSML ? {{ ssml: ssmlText }} : {{ text: rawText }},
- voice: {{
- languageCode: 'ja-JP',
- name: 'ja-JP-Neural2-B',
- ssmlGender: 'MALE'
- }},
- audioConfig: {{
- audioEncoding: 'MP3',
- speakingRate: 1.0,
- pitch: 0.0,
- effectsProfileId: ['headphone-class-device']
- }},
- }};
-
- const [response] = await client.synthesizeSpeech(request);
- fs.writeFileSync('{output_path}', response.audioContent, 'binary');
- console.log('✅ 音声ファイルを生成しました: {output_path}');
- return;
- }}
-
- // 5000バイト超える場合は分割処理
- console.log('⚠️ テキストが長いため分割して生成します');
-
- // SSMLタグを除去してテキストのみ分割
- const textOnly = isSSML ? ssmlText.replace(//g, '').replace(/<\\/speak>/g, '') : ssmlText;
-
- // テキストを分割
- const chunks = splitTextByByteLimit(textOnly, 4500);
- console.log(`📦 テキストを${{chunks.length}}個に分割しました`);
-
- // 各チャンクで音声生成
- const tempFiles = [];
- for (let i = 0; i < chunks.length; i++) {{
- console.log(`🎤 チャンク ${{i+1}}/${{chunks.length}} を生成中...`);
-
- const chunkText = isSSML ? `${{chunks[i]}}` : chunks[i];
-
- const request = {{
- input: isSSML ? {{ ssml: chunkText }} : {{ text: chunks[i] }},
- voice: {{
- languageCode: 'ja-JP',
- name: 'ja-JP-Neural2-B',
- ssmlGender: 'MALE'
- }},
- audioConfig: {{
- audioEncoding: 'MP3',
- speakingRate: 1.0,
- pitch: 0.0,
- effectsProfileId: ['headphone-class-device']
- }},
- }};
-
- const [response] = await client.synthesizeSpeech(request);
-
- const tempFile = `temp_${{i}}.mp3`;
- fs.writeFileSync(tempFile, response.audioContent, 'binary');
- tempFiles.push(tempFile);
- }}
-
- // pydubで音声ファイルを結合(Pythonスクリプト呼び出し)
- console.log('🔗 音声ファイルを結合中...');
-
- const combineScript = `
-import sys
-from pydub import AudioSegment
-
-temp_files = ${{JSON.stringify(tempFiles)}}
-output_path = '{output_path}'
-
-combined = AudioSegment.empty()
-for temp_file in temp_files:
- audio = AudioSegment.from_mp3(temp_file)
- combined += audio
-
-combined.export(output_path, format='mp3')
-
-# 一時ファイルを削除
-import os
-for temp_file in temp_files:
- os.remove(temp_file)
-
-print(f'✅ ${{len(temp_files)}}個のチャンクを結合しました: {{output_path}}')
-`;
-
- fs.writeFileSync('combine_audio.py', combineScript);
-
- const {{ execSync }} = require('child_process');
- execSync('python3 combine_audio.py', {{ stdio: 'inherit' }});
-
- // クリーンアップ
- fs.unlinkSync('combine_audio.py');
-
- console.log('✅ 音声ファイル生成完了: {output_path}');
-}}
-
-generateSpeech().catch(console.error);
-"""
-
- # 一時的なNode.jsスクリプトを作成
- tts_script_path = self.project_path / "generate_audio_gcp.js"
- with open(tts_script_path, 'w', encoding='utf-8') as f:
- f.write(tts_script)
-
- print(f"✅ TTS生成スクリプトを作成しました: {tts_script_path}")
-
- # package.json に依存関係を追加(存在する場合)
- package_json_path = self.project_path / "package.json"
- if package_json_path.exists():
- with open(package_json_path, 'r') as f:
- package_data = json.load(f)
-
- if 'dependencies' not in package_data:
- package_data['dependencies'] = {}
-
- package_data['dependencies']['@google-cloud/text-to-speech'] = "^4.2.0"
-
- # スクリプトも追加
- if 'scripts' not in package_data:
- package_data['scripts'] = {}
- package_data['scripts']['generate-audio:gcp'] = 'node generate_audio_gcp.js'
-
- with open(package_json_path, 'w') as f:
- json.dump(package_data, f, indent=2)
-
- print("✅ package.json に GCP TTS 依存関係を追加しました")
-
- # 認証情報ファイルの存在を確認し、必要なら自動セットアップ
- # 注: cred_pathは上で既に階層型設定システムで解決済み
- if not cred_path:
- print("\n⚠️ GCP認証情報が見つかりません")
- print("⚠️ use the gcp skill")
- print("\n🔧 既存のAPI管理システムを使用して自動セットアップを試みます...\n")
-
- # 既存のAPI管理システム(GCPスキル + CLAUDE.mdの手順)を使用
- if self.setup_gcp_credentials_auto():
- print("✅ GCP認証セットアップ完了")
- # 認証パスを再解決
- cred_path = self.resolve_gcp_credentials_path()
- if cred_path:
- # 音声生成を続行
- self.generate_audio_file(tts_script_path, output_path)
- else:
- print("❌ セットアップ後も認証ファイルが見つかりません")
- return None
- else:
- print(f"""
-⚠️ GCP認証の自動セットアップに失敗しました
-
-音声生成を有効にするには(手動):
-1. Google Cloud Console で Text-to-Speech API を有効化
-2. サービスアカウントキーを作成
-3. {cred_path} に保存
-4. npm install @google-cloud/text-to-speech
-5. npm run generate-audio:gcp
-
-または、gcloudコマンドで:
-gcloud services enable texttospeech.googleapis.com
-gcloud iam service-accounts create tts-service-account
-gcloud iam service-accounts keys create {cred_path} \\
- --iam-account tts-service-account@PROJECT_ID.iam.gserviceaccount.com
-""")
- return None
- else:
- # 認証ファイルが既存の場合、音声生成を実行
- print(f"\n✅ GCP認証ファイル既存: {cred_path}")
- print("🎤 音声生成を開始します...\n")
- self.generate_audio_file(tts_script_path, output_path)
-
- return output_path
-
- def generate_all_documents(self, project_info=None):
- """すべてのドキュメントと音声を生成
-
- 音声生成の優先順位:
- 1. Gemini 2.5 Flash Preview TTS(推奨 - APIキーのみで利用可能)
- 2. Google Cloud TTS(フォールバック - サービスアカウント必要)
- """
- if project_info is None:
- # PROJECT_INFO.yaml から読み込み
- project_info_path = self.project_path / "PROJECT_INFO.yaml"
- if project_info_path.exists():
- import yaml
- with open(project_info_path, 'r') as f:
- data = yaml.safe_load(f)
- project_info = data.get('project', {})
- else:
- project_info = {'name': 'Project', 'type': 'web'}
-
- print("📄 ドキュメント生成を開始...")
-
- # 0. AUDIO_PROMPTS.json を生成(ゲームプロジェクトの場合)
- audio_prompts_path = self.generate_audio_prompts_json(project_info)
-
- # 1. about.html を生成
- about_path = self.generate_about_html(project_info)
-
- # 2. 音声スクリプトを生成(explanation.mp3用)
- script_path = self.generate_audio_script(project_info)
-
- # 3. 音声生成(Gemini TTS 優先、GCP TTS フォールバック)
- audio_path = None
- audio_method = None
-
- # 3-1. Gemini TTS を試行(推奨)
- print(f"\n📋 TTS選択診断:")
- print(f" GEMINI_TTS_AVAILABLE: {GEMINI_TTS_AVAILABLE}")
- print(f" GEMINI_API_KEY設定: {bool(os.environ.get('GEMINI_API_KEY'))}")
- print(f" PYDUB_AVAILABLE: {PYDUB_AVAILABLE}")
-
- if GEMINI_TTS_AVAILABLE and os.environ.get('GEMINI_API_KEY'):
- print("\n🎤 Gemini 2.5 Flash Preview TTS で音声生成を試行...")
- audio_path = self.generate_audio_with_gemini(script_path)
- if audio_path:
- audio_method = "Gemini TTS"
-
- # 3-2. Gemini 失敗時は GCP TTS にフォールバック
- if audio_path is None:
- print("\n🎤 Google Cloud TTS にフォールバック...")
- audio_path = self.generate_audio_with_gcp(script_path)
- if audio_path:
- audio_method = "GCP TTS"
-
- print("\n✅ ドキュメント生成完了!")
- if audio_prompts_path:
- print(f" - {audio_prompts_path} (ゲーム音声プロンプト)")
- print(f" - {about_path}")
- print(f" - {script_path}")
- if audio_path:
- print(f" - {audio_path} ({audio_method})")
- else:
- print(f" - ⚠️ 音声生成スキップ(GEMINI_API_KEY または GCP認証が必要)")
-
- # 4. GitHub Pages用パス検証(自動実行)
- print("\n🔍 GitHub Pages用パス検証を実行中...")
- validation_result = self.validate_github_pages_paths()
-
- if validation_result['status'] == 'success':
- print("✅ パス検証完了: すべてのパスが相対パスです")
- elif validation_result['status'] == 'fixed':
- print(f"✅ パス検証完了: {validation_result['fixes']}個のパスを自動修正しました")
- else:
- print(f"⚠️ パス検証で警告が出ました(詳細は path_validator.py を実行してください)")
-
- return {
- 'audio_prompts_json': str(audio_prompts_path) if audio_prompts_path else None,
- 'about_html': str(about_path),
- 'audio_script': str(script_path),
- 'audio_file': str(audio_path) if audio_path else None,
- 'path_validation': validation_result
- }
-
- def validate_github_pages_paths(self):
- """GitHub Pages用のパス検証を実行
-
- Returns:
- dict: 検証結果 {'status': 'success'|'fixed'|'warning', 'issues': [...], 'fixes': int}
- """
- try:
- # path_validator.py を実行
- validator_path = Path(__file__).parent / "path_validator.py"
-
- if not validator_path.exists():
- return {'status': 'skipped', 'reason': 'path_validator.py not found'}
-
- result = subprocess.run(
- ['python3', str(validator_path), str(self.public_path)],
- capture_output=True,
- text=True,
- timeout=30
- )
-
- # 出力を表示
- if result.stdout:
- print(result.stdout)
-
- if result.returncode == 0:
- # 修正があった場合は 'fixed'、なければ 'success'
- if 'auto-fix' in result.stdout.lower() or '修正' in result.stdout:
- return {'status': 'fixed', 'fixes': result.stdout.count('✅ 修正')}
- else:
- return {'status': 'success', 'issues': 0}
- else:
- return {'status': 'warning', 'output': result.stdout}
-
- except subprocess.TimeoutExpired:
- return {'status': 'error', 'reason': 'timeout'}
- except Exception as e:
- return {'status': 'error', 'reason': str(e)}
-
-def main():
- """メイン処理"""
- documenter = DocumenterAgent()
- results = documenter.generate_all_documents()
-
- print("\n📚 生成されたドキュメント:")
- for key, value in results.items():
- if value:
- print(f" - {key}: {value}")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/documenter_agent_v2.py b/src/documenter_agent_v2.py
deleted file mode 100755
index beac5e8..0000000
--- a/src/documenter_agent_v2.py
+++ /dev/null
@@ -1,497 +0,0 @@
-#!/usr/bin/env python3
-"""
-Documenterエージェント v2.0 - アプリ特化型ドキュメント生成
-- アプリの機能と特徴を中心に解説
-- frontend-design スキルの必須使用
-- GCP TTS サービスアカウントキーの適切なパス
-"""
-
-import os
-import sys
-import json
-import yaml
-import subprocess
-from pathlib import Path
-from datetime import datetime
-
-class DocumenterAgentV2:
- """アプリ特化型ドキュメント生成エージェント"""
-
- def __init__(self, project_path="."):
- self.project_path = Path(project_path)
- # 実際のサービスアカウントキーのパス候補
- self.gcp_key_candidates = [
- Path.home() / "Desktop" / "SKILLS" / "tts-api-key.json",
- Path.home() / "Desktop" / "delete" / "credentials" / "gcp-workflow-key.json",
- Path.home() / "Desktop" / "git-worktree-agent" / "credentials" / "gcp-workflow-key.json"
- ]
-
- def get_project_details(self):
- """プロジェクトの詳細情報を取得"""
- # PROJECT_INFO.yaml から読み込み
- project_info_path = self.project_path / "PROJECT_INFO.yaml"
- if project_info_path.exists():
- with open(project_info_path, 'r', encoding='utf-8') as f:
- data = yaml.safe_load(f)
- return data
-
- # requirements.md から抽出
- req_path = self.project_path / "docs" / "requirements.md"
- if req_path.exists():
- with open(req_path, 'r', encoding='utf-8') as f:
- content = f.read()
- # 要件から主要機能を抽出するロジック
- return self.parse_requirements(content)
-
- return {
- 'project_name': 'アプリケーション',
- 'project_type': 'web',
- 'features': [],
- 'tech_stack': []
- }
-
- def parse_requirements(self, content):
- """要件書から情報を抽出"""
- # 簡易的なパース処理
- features = []
- lines = content.split('\n')
- for line in lines:
- if '機能' in line or 'Feature' in line:
- features.append(line.strip('- #'))
-
- return {
- 'features': features[:5] if features else []
- }
-
- def generate_about_html_prompt(self, project_info):
- """frontend-design スキル用のプロンプトを生成"""
-
- project_name = project_info.get('project_name', 'アプリケーション')
- project_type = project_info.get('project_type', 'web')
- features = project_info.get('features', [])
- tech_stack = project_info.get('tech_stack', [])
-
- # ゲームプロジェクトの場合の特別処理
- if project_type == 'game':
- game_genre = project_info.get('game_genre', 'action')
- return f"""
-# {project_name} - ゲーム紹介ページ
-
-## 必須要件
-- **frontend-design スキルを必ず使用してください**
-- アプリ(ゲーム)の機能と特徴を中心に説明
-- AIエージェント開発の説明は最小限に
-
-## ゲーム情報
-- ゲーム名: {project_name}
-- ジャンル: {game_genre}
-- プラットフォーム: Web ブラウザ
-
-## 主要なゲーム機能(これをメインに説明)
-{self.extract_game_features(project_info)}
-
-## デザイン要件
-1. ゲームのスクリーンショットエリア(プレースホルダーでOK)
-2. 操作方法の説明セクション
-3. ゲームの特徴を視覚的にアピール
-4. プレイボタン(目立つCTA)
-5. スコアランキングエリア(あれば)
-
-## 技術スタック
-- Canvas/WebGL
-- JavaScript
-- {', '.join(tech_stack) if tech_stack else 'HTML5 Game Technologies'}
-
-## 重要
-- ゲームの楽しさと特徴を前面に
-- どのようなゲームプレイかを明確に
-- AIで開発したことは補足程度に
-"""
-
- # 通常のWebアプリの場合
- return f"""
-# {project_name} - アプリケーション紹介ページ
-
-## 必須要件
-- **frontend-design スキルを必ず使用してください**
-- アプリの機能と価値を中心に説明
-- ユーザーにとってのメリットを強調
-
-## アプリケーション情報
-- アプリ名: {project_name}
-- タイプ: {project_type}
-
-## 主要機能(これをメインコンテンツに)
-{self.format_features(features)}
-
-## デザイン要件
-1. ヒーローセクション(アプリの価値提案)
-2. 機能紹介(ビジュアル付き)
-3. 使い方の3ステップ
-4. 技術スタック(サブセクション)
-5. CTAボタン(今すぐ使う)
-
-## 技術情報
-{', '.join(tech_stack) if tech_stack else 'Modern Web Technologies'}
-
-## 注意点
-- アプリの価値と機能を最優先で説明
-- ユーザー視点でのメリットを強調
-- AI開発については最後に軽く触れる程度
-"""
-
- def extract_game_features(self, project_info):
- """ゲームの特徴を抽出"""
- features = []
-
- # ゲーム固有の特徴を追加
- if project_info.get('game_genre') == 'shooting':
- features = [
- "- 爽快なシューティングアクション",
- "- 多彩な敵キャラクター",
- "- パワーアップシステム",
- "- ハイスコアチャレンジ",
- "- ステージ進行システム"
- ]
- elif project_info.get('game_genre') == 'puzzle':
- features = [
- "- 頭を使うパズル要素",
- "- 段階的な難易度",
- "- ヒントシステム",
- "- タイムアタックモード",
- "- 実績システム"
- ]
- else:
- features = project_info.get('features', [])
-
- return '\n'.join(features)
-
- def format_features(self, features):
- """機能リストをフォーマット"""
- if not features:
- return """
-- ユーザーフレンドリーなインターフェース
-- 高速なレスポンス
-- 安全なデータ管理
-- クロスプラットフォーム対応
-- リアルタイム同期
-"""
- return '\n'.join(f"- {f}" for f in features[:5])
-
- def create_about_with_frontend_skill(self, project_info):
- """frontend-design スキルを使用してabout.htmlを生成"""
-
- prompt = self.generate_about_html_prompt(project_info)
-
- # frontend-design スキル用のファイルを作成
- skill_request_path = self.project_path / "about_design_request.md"
- with open(skill_request_path, 'w', encoding='utf-8') as f:
- f.write(prompt)
-
- print(f"""
-📝 frontend-design スキル使用の準備完了
-
-以下の手順でabout.htmlを生成してください:
-
-1. frontend-design スキルを起動
-2. {skill_request_path} の内容でデザイン依頼
-3. アプリの機能を中心とした紹介ページを生成
-
-重要: 必ずfrontend-designスキルを使用してください
-""")
-
- return skill_request_path
-
- def generate_audio_script(self, project_info):
- """アプリ中心の音声スクリプトを生成"""
-
- project_name = project_info.get('project_name', 'アプリケーション')
- project_type = project_info.get('project_type', 'web')
- features = project_info.get('features', [])
-
- if project_type == 'game':
- game_genre = project_info.get('game_genre', 'action')
- script = f"""
-こんにちは。{project_name}の紹介を始めます。
-
-{project_name}は、{game_genre}タイプのWebゲームです。
-ブラウザ上で手軽に楽しめる、エキサイティングなゲーム体験を提供します。
-
-ゲームの特徴をご紹介します。
-
-{self.generate_game_feature_narration(project_info)}
-
-操作は簡単で、キーボードやマウスだけで直感的にプレイできます。
-初心者から上級者まで、幅広いレベルのプレイヤーが楽しめる設計になっています。
-
-このゲームは最新のWeb技術を使用して開発されており、
-スムーズなアニメーションと美しいグラフィックを実現しています。
-
-なお、このゲームはAIエージェントシステムにより自動開発されました。
-要件定義から実装、テストまで、すべてAIが行っています。
-
-ぜひ、{project_name}をプレイして、楽しいゲーム体験をお楽しみください。
-
-以上で、{project_name}の紹介を終わります。
-"""
- else:
- script = f"""
-こんにちは。{project_name}の紹介を始めます。
-
-{project_name}は、{self.describe_app_purpose(project_info)}を実現するWebアプリケーションです。
-
-主な機能をご紹介します。
-
-{self.generate_feature_narration(features)}
-
-このアプリケーションは、使いやすさを第一に設計されています。
-直感的なインターフェースで、誰でも簡単に利用できます。
-
-技術面では、最新のWeb技術を採用し、
-高速で安定した動作を実現しています。
-
-このアプリケーションは、AIエージェントシステムにより、
-要件定義から実装まで自動的に開発されました。
-
-{project_name}をぜひご利用いただき、
-便利な機能をお役立てください。
-
-以上で、{project_name}の紹介を終わります。
-"""
-
- # スクリプトを保存
- script_path = self.project_path / "audio_script.txt"
- with open(script_path, 'w', encoding='utf-8') as f:
- f.write(script.strip())
-
- print(f"✅ アプリ中心の音声スクリプトを生成: {script_path}")
- return script_path
-
- def describe_app_purpose(self, project_info):
- """アプリの目的を説明"""
- project_type = project_info.get('project_type', 'web')
-
- purposes = {
- 'web': 'ユーザーの作業効率を向上させること',
- 'mobile': 'いつでもどこでも便利な機能を提供すること',
- 'desktop': '強力な機能を快適に使用できること',
- 'api': 'システム間の連携を簡単にすること'
- }
-
- return purposes.get(project_type, 'ユーザーの課題を解決すること')
-
- def generate_feature_narration(self, features):
- """機能の音声説明を生成"""
- if not features:
- return """
-第一に、シンプルで使いやすいインターフェース。
-第二に、高速な処理と応答性。
-第三に、安全なデータ管理機能。
-第四に、複数デバイスでの同期機能。
-第五に、カスタマイズ可能な設定。
-"""
-
- narrations = []
- order = ['第一に', '第二に', '第三に', '第四に', '第五に']
- for i, feature in enumerate(features[:5]):
- narrations.append(f"{order[i]}、{feature}。")
-
- return '\n'.join(narrations)
-
- def generate_game_feature_narration(self, project_info):
- """ゲーム機能の音声説明を生成"""
- game_genre = project_info.get('game_genre', 'action')
-
- if game_genre == 'shooting':
- return """
-第一に、爽快なシューティングアクション。敵を倒す快感が味わえます。
-第二に、多彩な敵キャラクター。それぞれ異なる攻撃パターンを持っています。
-第三に、パワーアップシステム。武器を強化して強敵に立ち向かいます。
-第四に、ハイスコアチャレンジ。世界中のプレイヤーと競い合えます。
-第五に、美しいエフェクト。爆発や弾幕が画面を彩ります。
-"""
-
- return """
-第一に、エキサイティングなゲームプレイ。
-第二に、段階的に上がる難易度。
-第三に、達成感のある進行システム。
-第四に、美しいビジュアル表現。
-第五に、中毒性のあるゲーム性。
-"""
-
- def find_gcp_key(self):
- """利用可能なGCPキーを探す"""
- for key_path in self.gcp_key_candidates:
- if key_path.exists():
- print(f"✅ GCPキーを発見: {key_path}")
- return key_path
-
- print(f"⚠️ GCPキーが見つかりません。以下の場所を確認しました:")
- for path in self.gcp_key_candidates:
- print(f" - {path}")
-
- return None
-
- def generate_audio_with_gcp(self, script_path, output_path="explanation.mp3"):
- """GCP Text-to-Speech を使用して音声を生成(改良版)"""
-
- # 利用可能なキーを探す
- key_path = self.find_gcp_key()
-
- if not key_path:
- # キーがない場合の代替処理
- print("""
-⚠️ GCP認証情報が見つかりません。
-
-音声生成を有効にする方法:
-
-方法1: 既存のキーをコピー
- cp ~/Desktop/SKILLS/tts-api-key.json ~/Desktop/git-worktree-agent/credentials/gcp-workflow-key.json
-
-方法2: 新規作成
- 1. Google Cloud Console で Text-to-Speech API を有効化
- 2. サービスアカウントキーを作成
- 3. 以下のいずれかの場所に保存:
- - ~/Desktop/SKILLS/tts-api-key.json
- - ~/Desktop/git-worktree-agent/credentials/gcp-workflow-key.json
-""")
- return None
-
- # Google Cloud TTS用のスクリプトを生成
- tts_script = f"""
-const fs = require('fs');
-const textToSpeech = require('@google-cloud/text-to-speech');
-
-// クライアントを作成
-const client = new textToSpeech.TextToSpeechClient({{
- keyFilename: '{key_path}'
-}});
-
-async function generateSpeech() {{
- try {{
- // テキストを読み込み
- const text = fs.readFileSync('{script_path}', 'utf-8');
-
- // リクエストを構築
- const request = {{
- input: {{ text: text }},
- voice: {{
- languageCode: 'ja-JP',
- name: 'ja-JP-Neural2-D', // 男性の自然な声
- ssmlGender: 'MALE'
- }},
- audioConfig: {{
- audioEncoding: 'MP3',
- speakingRate: 1.0,
- pitch: 0.0,
- effectsProfileId: ['headphone-class-device']
- }},
- }};
-
- console.log('🎤 音声生成中...');
-
- // API呼び出し
- const [response] = await client.synthesizeSpeech(request);
-
- // 音声ファイルを保存
- fs.writeFileSync('{output_path}', response.audioContent, 'binary');
- console.log('✅ 音声ファイルを生成しました: {output_path}');
- }} catch (error) {{
- console.error('❌ エラー:', error.message);
- if (error.code === 7) {{
- console.log('認証エラー: サービスアカウントキーを確認してください');
- console.log('キーパス: {key_path}');
- }}
- }}
-}}
-
-generateSpeech();
-"""
-
- # 一時的なNode.jsスクリプトを作成
- tts_script_path = self.project_path / "generate_audio_gcp.js"
- with open(tts_script_path, 'w', encoding='utf-8') as f:
- f.write(tts_script)
-
- print(f"✅ TTS生成スクリプトを作成: {tts_script_path}")
- print(f"📍 使用するGCPキー: {key_path}")
-
- # package.json に依存関係を追加
- self.update_package_json()
-
- return output_path
-
- def update_package_json(self):
- """package.jsonを更新"""
- package_json_path = self.project_path / "package.json"
-
- if package_json_path.exists():
- with open(package_json_path, 'r') as f:
- package_data = json.load(f)
- else:
- package_data = {
- "name": "app",
- "version": "1.0.0",
- "description": "AI Generated App"
- }
-
- # 依存関係を追加
- if 'dependencies' not in package_data:
- package_data['dependencies'] = {}
-
- package_data['dependencies']['@google-cloud/text-to-speech'] = "^4.2.0"
-
- # スクリプトを追加
- if 'scripts' not in package_data:
- package_data['scripts'] = {}
-
- package_data['scripts']['generate-audio'] = 'node generate_audio_gcp.js'
-
- with open(package_json_path, 'w') as f:
- json.dump(package_data, f, indent=2)
-
- print("✅ package.json を更新しました")
-
- def generate_all_documents(self):
- """すべてのドキュメントを生成"""
- print("📄 アプリ特化型ドキュメント生成を開始...")
-
- # プロジェクト情報を取得
- project_info = self.get_project_details()
-
- # 1. frontend-design スキル用のプロンプトを生成
- design_request = self.create_about_with_frontend_skill(project_info)
-
- # 2. アプリ中心の音声スクリプトを生成
- script_path = self.generate_audio_script(project_info)
-
- # 3. GCP TTS用スクリプトを生成(改良版パス)
- audio_path = self.generate_audio_with_gcp(script_path)
-
- print("\n✅ ドキュメント生成完了!")
- print(f" - デザイン依頼: {design_request}")
- print(f" - 音声スクリプト: {script_path}")
-
- if audio_path:
- print(f" - 音声生成準備: 完了")
- print("\n📢 音声を生成するには:")
- print(" npm install")
- print(" npm run generate-audio")
-
- return {
- 'design_request': str(design_request),
- 'audio_script': str(script_path),
- 'audio_ready': audio_path is not None
- }
-
-def main():
- """メイン処理"""
- documenter = DocumenterAgentV2()
- results = documenter.generate_all_documents()
-
- print("\n📚 次のステップ:")
- print("1. frontend-design スキルでabout.htmlを生成")
- print("2. npm install && npm run generate-audio で音声生成")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/enhanced_client_document_generator.py b/src/enhanced_client_document_generator.py
deleted file mode 100755
index d7208c8..0000000
--- a/src/enhanced_client_document_generator.py
+++ /dev/null
@@ -1,623 +0,0 @@
-#!/usr/bin/env python3
-"""
-Client向け納品ドキュメント生成(強化版)
-実際のプロジェクトデータから情報を抽出して生成
-"""
-
-import os
-import sys
-import json
-import yaml
-import subprocess
-from pathlib import Path
-from datetime import datetime
-import re
-
-class ClientDocumentGenerator:
- def __init__(self, project_path="."):
- self.project_path = Path(project_path)
- self.project_info = self.load_project_info()
- self.package_info = self.load_package_info()
- self.test_results = self.get_test_results()
- self.git_info = self.get_git_info()
-
- def load_project_info(self):
- """PROJECT_INFO.yamlから情報取得"""
- info_path = self.project_path / "PROJECT_INFO.yaml"
- if info_path.exists():
- with open(info_path, 'r', encoding='utf-8') as f:
- return yaml.safe_load(f)
- return {"project": {"name": "プロジェクト", "type": "client"}}
-
- def load_package_info(self):
- """package.jsonから情報取得"""
- package_path = self.project_path / "package.json"
- if package_path.exists():
- with open(package_path, 'r', encoding='utf-8') as f:
- return json.load(f)
- return {}
-
- def get_test_results(self):
- """テスト結果を取得"""
- results = {
- "total": 0,
- "passed": 0,
- "failed": 0,
- "coverage": 0
- }
-
- # npm testの結果を取得(存在する場合)
- if (self.project_path / "package.json").exists():
- try:
- result = subprocess.run(
- ["npm", "test", "--", "--json"],
- capture_output=True,
- text=True,
- cwd=self.project_path,
- timeout=30
- )
- # 結果をパース(形式はテストフレームワークによって異なる)
- if result.returncode == 0:
- results["passed"] = results["total"]
- except:
- pass
-
- # カバレッジレポートがあれば読み込み
- coverage_path = self.project_path / "coverage" / "coverage-summary.json"
- if coverage_path.exists():
- with open(coverage_path, 'r') as f:
- coverage_data = json.load(f)
- if "total" in coverage_data:
- results["coverage"] = coverage_data["total"]["lines"]["pct"]
-
- return results
-
- def get_git_info(self):
- """Git情報を取得"""
- info = {
- "commits": [],
- "authors": set(),
- "files_changed": 0
- }
-
- try:
- # コミット履歴を取得
- log_result = subprocess.run(
- ["git", "log", "--oneline", "-10"],
- capture_output=True,
- text=True,
- cwd=self.project_path
- )
- if log_result.returncode == 0:
- info["commits"] = log_result.stdout.strip().split('\n')
-
- # 変更ファイル数を取得
- diff_result = subprocess.run(
- ["git", "diff", "--stat", "HEAD~1"],
- capture_output=True,
- text=True,
- cwd=self.project_path
- )
- if diff_result.returncode == 0:
- lines = diff_result.stdout.strip().split('\n')
- if lines:
- # 最終行から変更ファイル数を抽出
- match = re.search(r'(\d+) files? changed', lines[-1])
- if match:
- info["files_changed"] = int(match.group(1))
- except:
- pass
-
- return info
-
- def extract_requirements(self):
- """REQUIREMENTS.mdから要件を抽出"""
- req_path = self.project_path / "REQUIREMENTS.md"
- if req_path.exists():
- with open(req_path, 'r', encoding='utf-8') as f:
- return f.read()
- return ""
-
- def extract_readme(self):
- """README.mdから情報を抽出"""
- readme_path = self.project_path / "README.md"
- if readme_path.exists():
- with open(readme_path, 'r', encoding='utf-8') as f:
- return f.read()
- return ""
-
- def get_directory_tree(self):
- """ディレクトリツリーを生成"""
- tree_result = subprocess.run(
- ["tree", "-I", "node_modules|__pycache__|.git", "-L", "3"],
- capture_output=True,
- text=True,
- cwd=self.project_path
- )
- if tree_result.returncode == 0:
- return tree_result.stdout
- return ""
-
- def generate_requirements_doc(self):
- """要件定義書の生成(強化版)"""
- project_name = self.project_info.get("project", {}).get("name", "プロジェクト")
- client_name = self.project_info.get("client", {}).get("client_name", "お客様")
- requirements = self.extract_requirements()
-
- content = f"""# 要件定義書
-
-## プロジェクト情報
-- **プロジェクト名**: {project_name}
-- **クライアント名**: {client_name}
-- **作成日**: {datetime.now().strftime('%Y年%m月%d日')}
-- **バージョン**: 1.0.0
-
-## 1. プロジェクト概要
-{requirements if requirements else "※REQUIREMENTS.mdから自動取得予定"}
-
-## 2. システム構成
-### 2.1 技術スタック
-"""
-
- # package.jsonから依存関係を抽出
- if self.package_info:
- content += "#### フロントエンド\n"
- if "dependencies" in self.package_info:
- for dep in list(self.package_info["dependencies"].keys())[:5]:
- content += f"- {dep}\n"
-
- content += "\n#### 開発ツール\n"
- if "devDependencies" in self.package_info:
- for dep in list(self.package_info["devDependencies"].keys())[:5]:
- content += f"- {dep}\n"
-
- content += f"""
-
-## 3. 機能要件
-### 3.1 必須機能
-- ユーザー認証機能
-- データ管理機能
-- レポート出力機能
-
-### 3.2 オプション機能
-- 拡張検索機能
-- データエクスポート機能
-
-## 4. 非機能要件
-### 4.1 パフォーマンス要件
-- レスポンスタイム: 3秒以内
-- 同時接続数: 100ユーザー
-
-### 4.2 セキュリティ要件
-- HTTPS通信の実装
-- データ暗号化
-
-### 4.3 可用性要件
-- 稼働率: 99%以上
-- バックアップ: 日次
-
-## 5. 制約事項
-- ブラウザ対応: Chrome, Firefox, Safari, Edge(最新版)
-- モバイル対応: レスポンシブデザイン
-
-## 6. 成果物
-- ソースコード一式
-- ドキュメント一式
-- テスト結果報告書
-
-## 改訂履歴
-| 版数 | 日付 | 内容 | 作成者 |
-|------|------|------|--------|
-| 1.0.0 | {datetime.now().strftime('%Y/%m/%d')} | 初版作成 | AIエージェント |
-"""
- return content
-
- def generate_test_report(self):
- """テスト結果報告書の生成(強化版)"""
- project_name = self.project_info.get("project", {}).get("name", "プロジェクト")
-
- # 実際のテスト結果を使用
- total = self.test_results["total"] if self.test_results["total"] > 0 else 50
- passed = self.test_results["passed"] if self.test_results["passed"] > 0 else total
- failed = total - passed
- coverage = self.test_results["coverage"] if self.test_results["coverage"] > 0 else 85
-
- content = f"""# テスト結果報告書
-
-## プロジェクト情報
-- **プロジェクト名**: {project_name}
-- **実施日**: {datetime.now().strftime('%Y年%m月%d日')}
-- **実施者**: AIエージェントシステム
-
-## 1. テスト実施概要
-### 1.1 テスト環境
-- OS: macOS/Linux/Windows
-- Node.js: v18.0.0以上
-- ブラウザ: Chrome 120+
-
-### 1.2 テスト範囲
-- ✅ 単体テスト: 実施済み
-- ✅ 統合テスト: 実施済み
-- ✅ E2Eテスト: 実施済み
-- ✅ パフォーマンステスト: 実施済み
-
-## 2. テスト結果サマリー
-| テスト種別 | 総数 | 成功 | 失敗 | スキップ | 成功率 |
-|------------|------|------|------|----------|--------|
-| 単体テスト | {total} | {passed} | {failed} | 0 | {(passed/total*100):.1f}% |
-| 統合テスト | 20 | 20 | 0 | 0 | 100% |
-| E2Eテスト | 10 | 10 | 0 | 0 | 100% |
-| **合計** | {total + 30} | {passed + 30} | {failed} | 0 | {((passed+30)/(total+30)*100):.1f}% |
-
-## 3. カバレッジ
-- **ラインカバレッジ**: {coverage}%
-- **ブランチカバレッジ**: {coverage - 5}%
-- **関数カバレッジ**: {coverage + 5}%
-
-## 4. パフォーマンステスト結果
-| 測定項目 | 目標値 | 実測値 | 判定 |
-|----------|--------|--------|------|
-| 初回読み込み | < 3秒 | 2.1秒 | ✅ 合格 |
-| API レスポンス | < 500ms | 230ms | ✅ 合格 |
-| メモリ使用量 | < 100MB | 65MB | ✅ 合格 |
-
-## 5. 発見された問題と対応
-### 5.1 修正済みの問題
-| No | 重要度 | 概要 | 対応状況 |
-|----|--------|------|----------|
-| 1 | 低 | スタイルの微調整 | 修正済み |
-| 2 | 中 | エラーメッセージの改善 | 修正済み |
-
-### 5.2 既知の問題
-- なし
-
-## 6. Git履歴(最新10件)
-```
-{chr(10).join(self.git_info["commits"][:10]) if self.git_info["commits"] else "No commits found"}
-```
-
-## 7. 品質評価
-### 7.1 総合評価
-- **品質スコア**: A (基準をすべて満たしている)
-- **リリース可否**: ✅ リリース可能
-
-### 7.2 評価詳細
-- ✅ 機能要件をすべて満たしている
-- ✅ 非機能要件を満たしている
-- ✅ テストカバレッジが基準値以上
-- ✅ パフォーマンス基準を満たしている
-- ✅ セキュリティ脆弱性なし
-
-## 8. 結論
-すべてのテストが完了し、品質基準を満たしていることを確認しました。
-本システムは本番環境へのリリースが可能な状態です。
-
-## 承認
-| 役割 | 氏名 | 日付 | 承認 |
-|------|------|------|------|
-| プロジェクトマネージャー | _____ | ____ | □ |
-| 品質保証責任者 | _____ | ____ | □ |
-| クライアント代表 | _____ | ____ | □ |
-"""
- return content
-
- def generate_user_manual(self):
- """操作マニュアルの生成(強化版)"""
- project_name = self.project_info.get("project", {}).get("name", "プロジェクト")
- readme_content = self.extract_readme()
- tree_structure = self.get_directory_tree()
-
- content = f"""# 操作マニュアル
-
-## システム情報
-- **プロジェクト名**: {project_name}
-- **バージョン**: 1.0.0
-- **最終更新日**: {datetime.now().strftime('%Y年%m月%d日')}
-
-## 1. はじめに
-本マニュアルは{project_name}の操作方法について説明します。
-
-## 2. システム要件
-### 2.1 動作環境
-- OS: Windows 10以降 / macOS 10.15以降 / Ubuntu 20.04以降
-- Node.js: 18.0.0以降
-- メモリ: 4GB以上推奨
-- ディスク: 500MB以上の空き容量
-
-### 2.2 対応ブラウザ
-- Google Chrome (最新版)
-- Mozilla Firefox (最新版)
-- Microsoft Edge (最新版)
-- Safari (最新版)
-
-## 3. インストール手順
-### 3.1 事前準備
-1. Node.jsのインストール
- - [Node.js公式サイト](https://nodejs.org/)から最新版をダウンロード
- - インストーラーを実行
-
-### 3.2 アプリケーションのセットアップ
-```bash
-# 1. プロジェクトフォルダに移動
-cd {project_name}
-
-# 2. 依存関係のインストール
-npm install
-
-# 3. 環境設定(必要な場合)
-cp .env.example .env
-# .envファイルを編集して設定を調整
-```
-
-## 4. 起動方法
-### 4.1 簡単起動(推奨)
-1. `launch_app.command`をダブルクリック
-2. 自動的にブラウザが起動します
-
-### 4.2 手動起動
-```bash
-# 開発モード
-npm run dev
-
-# 本番モード
-npm start
-```
-
-## 5. 基本操作
-{readme_content if readme_content else "※README.mdから自動取得予定"}
-
-## 6. 画面説明
-### 6.1 メイン画面
-- ヘッダー: ナビゲーションメニュー
-- サイドバー: 機能一覧
-- メインエリア: コンテンツ表示
-- フッター: システム情報
-
-### 6.2 各機能の説明
-(各機能の詳細な説明をここに記載)
-
-## 7. ディレクトリ構成
-```
-{tree_structure if tree_structure else "※ディレクトリツリーを自動生成予定"}
-```
-
-## 8. トラブルシューティング
-### 8.1 よくある質問
-
-**Q: アプリケーションが起動しない**
-A: 以下を確認してください:
-- Node.jsが正しくインストールされているか
-- `npm install`を実行したか
-- ポート3000が他のアプリで使用されていないか
-
-**Q: エラーが表示される**
-A: エラーメッセージを確認し、以下を試してください:
-- `npm install`を再実行
-- `node_modules`フォルダを削除して再インストール
-- ブラウザのキャッシュをクリア
-
-**Q: データが保存されない**
-A: 以下を確認してください:
-- ブラウザのローカルストレージが有効か
-- ディスクの空き容量が十分か
-
-### 8.2 エラーコード一覧
-| コード | 意味 | 対処法 |
-|--------|------|--------|
-| E001 | ネットワークエラー | インターネット接続を確認 |
-| E002 | 認証エラー | ログイン情報を確認 |
-| E003 | データエラー | データを再読み込み |
-
-## 9. メンテナンス
-### 9.1 バックアップ
-- データフォルダを定期的にバックアップ
-- 設定ファイルのバックアップ
-
-### 9.2 アップデート
-```bash
-# 最新版の取得
-git pull
-
-# 依存関係の更新
-npm update
-```
-
-## 10. サポート情報
-### 10.1 お問い合わせ
-- メール: support@example.com
-- 電話: 03-XXXX-XXXX(平日9:00-18:00)
-
-### 10.2 参考資料
-- [プロジェクトドキュメント](./docs/)
-- [API仕様書](./docs/API.md)
-- [開発者ガイド](./docs/DEVELOPER.md)
-
-## 付録
-### A. キーボードショートカット
-| キー | 動作 |
-|------|------|
-| Ctrl+S | 保存 |
-| Ctrl+Z | 元に戻す |
-| Ctrl+Y | やり直す |
-| F1 | ヘルプ |
-
-### B. 用語集
-- **API**: Application Programming Interface
-- **UI**: User Interface
-- **UX**: User Experience
-
----
-© 2024 {project_name}. All Rights Reserved.
-"""
- return content
-
- def generate_basic_design(self):
- """基本設計書の生成"""
- project_name = self.project_info.get("project", {}).get("name", "プロジェクト")
-
- content = f"""# 基本設計書
-
-## プロジェクト情報
-- **プロジェクト名**: {project_name}
-- **作成日**: {datetime.now().strftime('%Y年%m月%d日')}
-- **バージョン**: 1.0.0
-
-## 1. システム構成
-### 1.1 アーキテクチャ概要
-```
-┌─────────────┐ ┌─────────────┐ ┌─────────────┐
-│ Frontend │────▶│ Backend │────▶│ Database │
-│ (React) │ │ (Node.js) │ │ (MongoDB) │
-└─────────────┘ └─────────────┘ └─────────────┘
-```
-
-### 1.2 技術スタック
-- **フロントエンド**: React, TypeScript, Material-UI
-- **バックエンド**: Node.js, Express, TypeScript
-- **データベース**: MongoDB / PostgreSQL
-- **インフラ**: Docker, AWS/GCP
-
-## 2. 機能設計
-### 2.1 機能一覧
-| No | 機能名 | 概要 | 優先度 |
-|----|--------|------|--------|
-| 1 | ユーザー認証 | ログイン/ログアウト機能 | 高 |
-| 2 | データ管理 | CRUD操作 | 高 |
-| 3 | レポート出力 | PDF/Excel出力 | 中 |
-| 4 | 通知機能 | メール/プッシュ通知 | 低 |
-
-### 2.2 画面遷移図
-```
-[ログイン] ──▶ [ダッシュボード] ──▶ [各機能画面]
- │
- ▼
- [設定画面]
-```
-
-## 3. データ設計
-### 3.1 ER図
-```
-[Users] 1───N [Posts]
- │ │
- │ │
- N───────────N
- [Comments]
-```
-
-### 3.2 主要テーブル定義
-#### Users テーブル
-| カラム名 | 型 | 制約 | 説明 |
-|----------|-----|------|------|
-| id | UUID | PK | ユーザーID |
-| email | VARCHAR(255) | UNIQUE, NOT NULL | メールアドレス |
-| name | VARCHAR(100) | NOT NULL | ユーザー名 |
-| created_at | TIMESTAMP | NOT NULL | 作成日時 |
-
-## 4. API設計
-### 4.1 エンドポイント一覧
-| メソッド | パス | 説明 |
-|----------|------|------|
-| GET | /api/users | ユーザー一覧取得 |
-| GET | /api/users/:id | ユーザー詳細取得 |
-| POST | /api/users | ユーザー作成 |
-| PUT | /api/users/:id | ユーザー更新 |
-| DELETE | /api/users/:id | ユーザー削除 |
-
-## 5. セキュリティ設計
-- JWT認証
-- HTTPS通信
-- XSS/CSRF対策
-- SQLインジェクション対策
-- レート制限
-
-## 改訂履歴
-| 版数 | 日付 | 内容 | 作成者 |
-|------|------|------|--------|
-| 1.0.0 | {datetime.now().strftime('%Y/%m/%d')} | 初版作成 | AIエージェント |
-"""
- return content
-
- def generate_all_documents(self):
- """全ドキュメントを生成"""
- # deliverables/01_documentsディレクトリ作成
- docs_dir = self.project_path / "deliverables" / "01_documents"
- docs_dir.mkdir(parents=True, exist_ok=True)
-
- # 各ドキュメントの生成
- documents = {
- "01_要件定義書.md": self.generate_requirements_doc(),
- "02_基本設計書.md": self.generate_basic_design(),
- "03_テスト結果報告書.md": self.generate_test_report(),
- "04_操作マニュアル.md": self.generate_user_manual()
- }
-
- for filename, content in documents.items():
- filepath = docs_dir / filename
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(content)
- print(f"✅ {filename} を生成しました")
-
- # 納品チェックリストを生成
- self.generate_delivery_checklist()
-
- return docs_dir
-
- def generate_delivery_checklist(self):
- """納品チェックリストの生成"""
- checklist_content = f"""# 納品チェックリスト
-
-## プロジェクト: {self.project_info.get("project", {}).get("name", "プロジェクト")}
-## 納品日: {datetime.now().strftime('%Y年%m月%d日')}
-
-## 1. ドキュメント
-- [ ] 要件定義書
-- [ ] 基本設計書
-- [ ] 詳細設計書
-- [ ] テスト仕様書
-- [ ] テスト結果報告書
-- [ ] 操作マニュアル
-- [ ] 保守マニュアル
-
-## 2. ソースコード
-- [ ] ソースコード一式
-- [ ] README.md
-- [ ] ライセンスファイル
-- [ ] 環境設定ファイル(.env.example)
-
-## 3. 実行環境
-- [ ] インストーラー/起動スクリプト
-- [ ] 依存関係リスト
-- [ ] 動作確認済み
-
-## 4. テスト
-- [ ] 単体テスト実施
-- [ ] 統合テスト実施
-- [ ] 受入テスト準備
-
-## 5. その他
-- [ ] 納品書
-- [ ] 検収条件確認
-- [ ] サポート体制説明
-
-## 確認者署名
-- 開発者: ________
-- 確認者: ________
-- 承認者: ________
-"""
-
- checklist_path = self.project_path / "deliverables" / "納品チェックリスト.md"
- with open(checklist_path, 'w', encoding='utf-8') as f:
- f.write(checklist_content)
- print("✅ 納品チェックリスト.md を生成しました")
-
-def main():
- generator = ClientDocumentGenerator()
- docs_dir = generator.generate_all_documents()
-
- print(f"\n📝 ドキュメント生成完了: {docs_dir}")
- print("PDF変換は以下のコマンドで実行できます:")
- print(" npm install -g markdown-pdf")
- print(" markdown-pdf deliverables/01_documents/*.md")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/error_handler.sh b/src/error_handler.sh
deleted file mode 100755
index 1775347..0000000
--- a/src/error_handler.sh
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/bin/bash
-
-# エラーハンドリング共通関数
-
-# エラー時の処理
-handle_error() {
- local exit_code=$?
- local line_no=$1
- local script_name=$(basename $0)
-
- echo -e "\033[0;31m❌ エラーが発生しました!\033[0m"
- echo -e "スクリプト: $script_name"
- echo -e "行番号: $line_no"
- echo -e "終了コード: $exit_code"
-
- # ログファイルに記録
- echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR in $script_name at line $line_no (exit: $exit_code)" >> error.log
-
- exit $exit_code
-}
-
-# トラップ設定
-set_error_trap() {
- set -eE
- trap 'handle_error $LINENO' ERR
-}
-
-# リトライ機能
-retry_command() {
- local max_attempts=${1:-3}
- local delay=${2:-5}
- local command="${@:3}"
- local attempt=0
-
- while [ $attempt -lt $max_attempts ]; do
- attempt=$((attempt + 1))
- echo "実行中 (試行 $attempt/$max_attempts): $command"
-
- if eval $command; then
- return 0
- fi
-
- if [ $attempt -lt $max_attempts ]; then
- echo "失敗。${delay}秒後に再試行..."
- sleep $delay
- fi
- done
-
- echo "最大試行回数に達しました。失敗。"
- return 1
-}
-
-# プログレス表示
-show_progress() {
- local current=$1
- local total=$2
- local width=50
- local percent=$((current * 100 / total))
- local filled=$((width * current / total))
-
- printf "\r["
- printf "%${filled}s" | tr ' ' '='
- printf "%$((width - filled))s" | tr ' ' ' '
- printf "] %3d%% (%d/%d)" $percent $current $total
-
- if [ $current -eq $total ]; then
- echo ""
- fi
-}
diff --git a/src/gh-credential-helper.sh b/src/gh-credential-helper.sh
deleted file mode 100755
index d06c7f8..0000000
--- a/src/gh-credential-helper.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-# GitHub CLI credential helper for M4 Mac
-# This script acts as a Git credential helper using GitHub CLI
-# Required for automatic push in M4 Mac environments
-
-# Check if gh is available
-GH_PATH=""
-if [ -x "$HOME/bin/gh" ]; then
- GH_PATH="$HOME/bin/gh"
-elif command -v gh &> /dev/null; then
- GH_PATH="gh"
-else
- echo "Error: GitHub CLI (gh) not found" >&2
- echo "Please run: ./setup_github_cli_m4.sh" >&2
- exit 1
-fi
-
-# Execute gh auth git-credential with all arguments
-exec "$GH_PATH" auth git-credential "$@"
\ No newline at end of file
diff --git a/src/github_publisher.py b/src/github_publisher.py
deleted file mode 100755
index b9f4892..0000000
--- a/src/github_publisher.py
+++ /dev/null
@@ -1,417 +0,0 @@
-#!/usr/bin/env python3
-"""
-GitHub公開スクリプト
-DELIVERYフォルダをポートフォリオリポジトリに公開
-"""
-
-import os
-import subprocess
-import shutil
-import json
-from pathlib import Path
-from typing import Optional, Tuple, List
-from dataclasses import dataclass
-from datetime import datetime
-
-from portfolio_config import get_config, PortfolioConfig
-
-
-@dataclass
-class PublishResult:
- """公開結果"""
- success: bool
- app_name: str
- app_url: str
- commit_hash: str
- message: str
- files_added: int
- files_modified: int
- files_deleted: int
-
-
-class GitHubPublisher:
- """GitHub公開パブリッシャー"""
-
- def __init__(self, config: PortfolioConfig = None):
- self.config = config or get_config()
- self.repo_local_path = Path.home() / "Desktop" / "GitHub" / self.config.github_repo
-
- def ensure_repo_cloned(self) -> bool:
- """リポジトリがクローンされていることを確認"""
- if self.repo_local_path.exists() and (self.repo_local_path / ".git").exists():
- print(f" リポジトリ存在確認: ✅ {self.repo_local_path}")
- return True
-
- print(f" リポジトリをクローン中...")
- self.repo_local_path.parent.mkdir(parents=True, exist_ok=True)
-
- result = subprocess.run(
- ["git", "clone", self.config.repo_clone_url, str(self.repo_local_path)],
- capture_output=True,
- text=True,
- )
-
- if result.returncode != 0:
- print(f" ❌ クローン失敗: {result.stderr}")
- return False
-
- print(f" ✅ クローン完了: {self.repo_local_path}")
- return True
-
- def pull_latest(self) -> bool:
- """最新の変更をプル"""
- print(f" 最新の変更をプル中...")
-
- result = subprocess.run(
- ["git", "pull", "origin", "main"],
- cwd=self.repo_local_path,
- capture_output=True,
- text=True,
- )
-
- if result.returncode != 0:
- # rebaseを試みる
- result = subprocess.run(
- ["git", "pull", "--rebase", "origin", "main"],
- cwd=self.repo_local_path,
- capture_output=True,
- text=True,
- )
- if result.returncode != 0:
- print(f" ⚠️ プル失敗(続行可能): {result.stderr[:100]}")
- return True # 続行を許可
-
- print(f" ✅ プル完了")
- return True
-
- def copy_delivery_to_repo(
- self,
- delivery_path: str,
- app_name: str,
- ) -> Tuple[int, int, int]:
- """
- DELIVERYフォルダの内容をリポジトリにコピー
-
- Returns:
- (added, modified, deleted): ファイル数
- """
- delivery = Path(delivery_path)
- app_dest = self.repo_local_path / "apps" / app_name
-
- # 既存のアプリフォルダがあれば削除
- if app_dest.exists():
- shutil.rmtree(app_dest)
-
- # コピー
- app_dest.mkdir(parents=True, exist_ok=True)
-
- copied = 0
- for src_file in delivery.rglob("*"):
- if not src_file.is_file():
- continue
-
- # マニフェストは除外
- if src_file.name == ".delivery_manifest.json":
- continue
-
- rel_path = src_file.relative_to(delivery)
- dest_file = app_dest / rel_path
- dest_file.parent.mkdir(parents=True, exist_ok=True)
- shutil.copy2(src_file, dest_file)
- copied += 1
-
- print(f" ✅ {copied} ファイルをコピーしました")
-
- # git status で変更を確認
- result = subprocess.run(
- ["git", "status", "--porcelain"],
- cwd=self.repo_local_path,
- capture_output=True,
- text=True,
- )
-
- added = modified = deleted = 0
- for line in result.stdout.strip().split("\n"):
- if not line:
- continue
- status = line[:2]
- if "A" in status or "?" in status:
- added += 1
- elif "M" in status:
- modified += 1
- elif "D" in status:
- deleted += 1
-
- return added, modified, deleted
-
- def get_diff_summary(self) -> str:
- """差分のサマリーを取得"""
- result = subprocess.run(
- ["git", "diff", "--stat", "HEAD"],
- cwd=self.repo_local_path,
- capture_output=True,
- text=True,
- )
- return result.stdout
-
- def get_staged_files(self) -> List[str]:
- """ステージされたファイル一覧を取得"""
- result = subprocess.run(
- ["git", "diff", "--cached", "--name-only"],
- cwd=self.repo_local_path,
- capture_output=True,
- text=True,
- )
- return [f for f in result.stdout.strip().split("\n") if f]
-
- def stage_changes(self, app_name: str) -> bool:
- """変更をステージング"""
- app_path = f"apps/{app_name}"
-
- # アプリフォルダのみをステージ
- result = subprocess.run(
- ["git", "add", app_path],
- cwd=self.repo_local_path,
- capture_output=True,
- text=True,
- )
-
- if result.returncode != 0:
- print(f" ❌ ステージング失敗: {result.stderr}")
- return False
-
- print(f" ✅ 変更をステージしました")
- return True
-
- def create_commit(self, app_name: str, is_update: bool = False) -> Optional[str]:
- """コミットを作成"""
- action = "Update" if is_update else "Add"
- message = f"{action} {app_name} to portfolio\n\nPublished via AI Agent Workflow"
-
- result = subprocess.run(
- ["git", "commit", "-m", message],
- cwd=self.repo_local_path,
- capture_output=True,
- text=True,
- )
-
- if result.returncode != 0:
- if "nothing to commit" in result.stdout or "nothing to commit" in result.stderr:
- print(f" ℹ️ 変更がありません(コミット不要)")
- return None
- print(f" ❌ コミット失敗: {result.stderr}")
- return None
-
- # コミットハッシュを取得
- result = subprocess.run(
- ["git", "rev-parse", "HEAD"],
- cwd=self.repo_local_path,
- capture_output=True,
- text=True,
- )
-
- commit_hash = result.stdout.strip()[:8]
- print(f" ✅ コミット作成: {commit_hash}")
- return commit_hash
-
- def push_to_remote(self) -> bool:
- """リモートにプッシュ"""
- print(f" プッシュ中...")
-
- result = subprocess.run(
- ["git", "push", "origin", "main"],
- cwd=self.repo_local_path,
- capture_output=True,
- text=True,
- )
-
- if result.returncode != 0:
- print(f" ❌ プッシュ失敗: {result.stderr}")
- return False
-
- print(f" ✅ プッシュ完了")
- return True
-
- def publish(
- self,
- delivery_path: str,
- app_name: str,
- dry_run: bool = False,
- skip_push: bool = False,
- ) -> PublishResult:
- """
- DELIVERYフォルダを公開
-
- Args:
- delivery_path: DELIVERYフォルダのパス
- app_name: アプリ名
- dry_run: True の場合、実際のプッシュは行わない
- skip_push: True の場合、コミットまで行いプッシュはスキップ
- """
- print("\n" + "=" * 60)
- print(" GitHub公開")
- print("=" * 60)
-
- app_url = self.config.get_app_url(app_name)
-
- # 1. リポジトリ準備
- if not self.ensure_repo_cloned():
- return PublishResult(
- success=False,
- app_name=app_name,
- app_url=app_url,
- commit_hash="",
- message="リポジトリのクローンに失敗しました",
- files_added=0,
- files_modified=0,
- files_deleted=0,
- )
-
- # 2. 最新をプル
- self.pull_latest()
-
- # 3. ファイルをコピー
- print(f"\n ファイルをコピー中...")
- added, modified, deleted = self.copy_delivery_to_repo(delivery_path, app_name)
- print(f" 追加: {added}, 変更: {modified}, 削除: {deleted}")
-
- # 4. ステージング
- if not self.stage_changes(app_name):
- return PublishResult(
- success=False,
- app_name=app_name,
- app_url=app_url,
- commit_hash="",
- message="ステージングに失敗しました",
- files_added=added,
- files_modified=modified,
- files_deleted=deleted,
- )
-
- # 5. 差分サマリー表示
- staged_files = self.get_staged_files()
- print(f"\n 【公開されるファイル: {len(staged_files)} 件】")
- for f in staged_files[:20]:
- print(f" - {f}")
- if len(staged_files) > 20:
- print(f" ... 他 {len(staged_files) - 20} ファイル")
-
- if dry_run:
- print(f"\n 🔍 ドライラン: 実際の公開は行いません")
- return PublishResult(
- success=True,
- app_name=app_name,
- app_url=app_url,
- commit_hash="(dry-run)",
- message="ドライラン完了",
- files_added=added,
- files_modified=modified,
- files_deleted=deleted,
- )
-
- # 6. コミット作成
- is_update = (self.repo_local_path / "apps" / app_name).exists()
- commit_hash = self.create_commit(app_name, is_update)
-
- if commit_hash is None:
- return PublishResult(
- success=True,
- app_name=app_name,
- app_url=app_url,
- commit_hash="(no changes)",
- message="変更がありませんでした",
- files_added=added,
- files_modified=modified,
- files_deleted=deleted,
- )
-
- if skip_push:
- print(f"\n ⏸️ プッシュはスキップされました(--skip-push)")
- return PublishResult(
- success=True,
- app_name=app_name,
- app_url=app_url,
- commit_hash=commit_hash,
- message="コミット作成完了(プッシュ待ち)",
- files_added=added,
- files_modified=modified,
- files_deleted=deleted,
- )
-
- # 7. プッシュ
- if not self.push_to_remote():
- return PublishResult(
- success=False,
- app_name=app_name,
- app_url=app_url,
- commit_hash=commit_hash,
- message="プッシュに失敗しました",
- files_added=added,
- files_modified=modified,
- files_deleted=deleted,
- )
-
- print("\n" + "=" * 60)
- print(" ✅ 公開完了!")
- print("=" * 60)
- print(f"\n アプリURL: {app_url}")
- print(f" コミット: {commit_hash}")
- print("=" * 60 + "\n")
-
- return PublishResult(
- success=True,
- app_name=app_name,
- app_url=app_url,
- commit_hash=commit_hash,
- message="公開成功",
- files_added=added,
- files_modified=modified,
- files_deleted=deleted,
- )
-
- def execute_push(self) -> bool:
- """保留中のコミットをプッシュ"""
- return self.push_to_remote()
-
-
-def publish_delivery(
- delivery_path: str,
- app_name: str,
- dry_run: bool = False,
- skip_push: bool = False,
-) -> PublishResult:
- """
- DELIVERYフォルダを公開(便利関数)
- """
- publisher = GitHubPublisher()
- return publisher.publish(
- delivery_path=delivery_path,
- app_name=app_name,
- dry_run=dry_run,
- skip_push=skip_push,
- )
-
-
-if __name__ == "__main__":
- import argparse
-
- parser = argparse.ArgumentParser(description="GitHub公開")
- parser.add_argument("delivery_path", help="DELIVERYフォルダのパス")
- parser.add_argument("app_name", help="アプリ名")
- parser.add_argument("--dry-run", action="store_true", help="ドライラン(実際の公開なし)")
- parser.add_argument("--skip-push", action="store_true", help="コミットまで行いプッシュはスキップ")
- args = parser.parse_args()
-
- result = publish_delivery(
- delivery_path=args.delivery_path,
- app_name=args.app_name,
- dry_run=args.dry_run,
- skip_push=args.skip_push,
- )
-
- if result.success:
- print(f"✅ {result.message}")
- print(f" URL: {result.app_url}")
- else:
- print(f"❌ {result.message}")
- exit(1)
diff --git a/src/github_publisher_v8.py b/src/github_publisher_v8.py
deleted file mode 100755
index 373c752..0000000
--- a/src/github_publisher_v8.py
+++ /dev/null
@@ -1,299 +0,0 @@
-#!/usr/bin/env python3
-"""
-🚀 GitHub公開スクリプト v8.0 - 厳格版
-DELIVERYフォルダのみを https://github.com/sohei-t/ai-agent-portfolio にプッシュ
-"""
-
-import os
-import sys
-import subprocess
-import shutil
-import re
-import json
-from pathlib import Path
-from datetime import datetime
-from typing import Dict, Optional
-
-class GitHubPublisherV8:
- """厳格なGitHub公開ルールを実装"""
-
- def __init__(self, project_path: str = None):
- """
- Args:
- project_path: プロジェクトのパス(AI-Apps内のフォルダ)
- """
- self.project_path = Path(project_path or os.getcwd())
- self.delivery_path = self.project_path / "DELIVERY"
-
- # ハードコードされたリポジトリ設定
- self.github_username = "sohei-t"
- self.repo_name = "ai-agent-portfolio"
- self.portfolio_repo = Path.home() / "Desktop" / "GitHub" / self.repo_name
- self.remote_url = f"https://github.com/{self.github_username}/{self.repo_name}.git"
-
- def _run_command(self, cmd: str, cwd: Path = None) -> bool:
- """コマンド実行"""
- try:
- result = subprocess.run(
- cmd,
- shell=True,
- cwd=cwd or self.project_path,
- capture_output=True,
- text=True
- )
- if result.returncode != 0:
- print(f"❌ エラー: {result.stderr}")
- return False
- return True
- except Exception as e:
- print(f"❌ コマンド実行エラー: {e}")
- return False
-
- def get_slug(self, project_name: str = None) -> str:
- """プロジェクト名からslugを生成(日付除去)"""
- if not project_name:
- project_name = self.project_path.name
-
- # 日付プレフィックス除去(YYYYMMDD- or YYYY-MM-DD-)
- slug = re.sub(r'^\d{8}-', '', project_name)
- slug = re.sub(r'^\d{4}-\d{2}-\d{2}-', '', slug)
-
- # -agent サフィックス除去
- slug = re.sub(r'-agent$', '', slug)
-
- # 正規化
- slug = slug.lower()
- slug = re.sub(r'[^a-z0-9]+', '-', slug)
- slug = slug.strip('-')
-
- return slug
-
- def validate_delivery(self) -> bool:
- """DELIVERYフォルダの検証"""
- if not self.delivery_path.exists():
- print("❌ DELIVERYフォルダが見つかりません")
- print(" 先に以下を実行してください:")
- print(" 1. delivery_organizer.py でDELIVERYフォルダ作成")
- print(" 2. documenter_agent_v2.py でドキュメント生成")
- return False
-
- # 必須ファイルチェック
- required_files = ['README.md', 'about.html', 'index.html']
- missing = []
- for file in required_files:
- if not (self.delivery_path / file).exists():
- missing.append(file)
-
- if missing:
- print(f"⚠️ 必須ファイルが不足: {', '.join(missing)}")
- print(" delivery_organizer.py を実行してください")
- return False
-
- print("✅ DELIVERYフォルダ検証OK")
- return True
-
- def clean_delivery(self):
- """DELIVERYフォルダから不要ファイルを除去"""
- exclude_patterns = [
- '.git', '.gitignore', '.env', '.env.*',
- '__pycache__', '*.pyc', '.pytest_cache',
- '.DS_Store', 'Thumbs.db',
- 'node_modules', 'venv', '.venv',
- '*.log', '*.tmp', '*.bak',
- 'test_*', '*_test.py'
- ]
-
- print("🧹 不要ファイルをクリーニング中...")
- for pattern in exclude_patterns:
- for path in self.delivery_path.rglob(pattern):
- if path.is_file():
- path.unlink()
- elif path.is_dir():
- shutil.rmtree(path)
-
- def prepare_portfolio_repo(self) -> bool:
- """ポートフォリオリポジトリの準備"""
- if not self.portfolio_repo.exists():
- print(f"📁 ポートフォリオリポジトリを作成: {self.portfolio_repo}")
- self.portfolio_repo.mkdir(parents=True, exist_ok=True)
-
- # Git初期化
- self._run_command("git init", cwd=self.portfolio_repo)
- self._run_command(f"git remote add origin {self.remote_url}", cwd=self.portfolio_repo)
-
- # 基本ファイル作成
- readme = self.portfolio_repo / "README.md"
- readme.write_text(f"# AI Agent Portfolio\n\nAI-generated applications showcase\n")
-
- gitignore = self.portfolio_repo / ".gitignore"
- gitignore.write_text(".DS_Store\nnode_modules/\n.env\n")
-
- self._run_command("git add .", cwd=self.portfolio_repo)
- self._run_command('git commit -m "Initial commit"', cwd=self.portfolio_repo)
-
- return True
-
- def copy_delivery_to_app_folder(self, slug: str) -> Path:
- """DELIVERYフォルダをアプリ専用フォルダにコピー"""
- # apps/フォルダ内にアプリ専用フォルダを作成
- apps_dir = self.portfolio_repo / "apps"
- apps_dir.mkdir(exist_ok=True)
-
- target_path = apps_dir / slug
-
- # 既存フォルダがある場合は削除
- if target_path.exists():
- print(f"🔄 既存の {slug} を更新中...")
- shutil.rmtree(target_path)
-
- print(f"📦 DELIVERYフォルダを apps/{slug} にコピー中...")
- shutil.copytree(self.delivery_path, target_path)
-
- print(f"✅ コピー完了: {target_path}")
- return target_path
-
- def update_portfolio_index(self, slug: str):
- """ポートフォリオのindex.htmlを更新"""
- index_path = self.portfolio_repo / "index.html"
-
- if not index_path.exists():
- # 新規作成
- content = self._create_portfolio_index()
- else:
- content = index_path.read_text()
-
- # アプリリンクを追加(重複チェック付き)
- if f'href="apps/{slug}/"' not in content:
- app_link = f'{slug} - About\n'
- content = content.replace('', app_link + '')
-
- index_path.write_text(content)
- print("✅ ポートフォリオindex.html更新")
-
- def _create_portfolio_index(self) -> str:
- """ポートフォリオのindex.html テンプレート"""
- return """
-
-
- AI Agent Portfolio
-
-
-
-
- 🚀 AI Agent Portfolio
- AI-generated applications showcase by sohei-t
- 📱 Applications
-
-
-"""
-
- def git_operations(self, slug: str, update_type: str = "add") -> bool:
- """Git操作(add, commit, push)"""
- print("\n📤 GitHubにプッシュ中...")
-
- # Git操作
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-
- if update_type == "update":
- commit_msg = f"update: {slug} - Updated at {timestamp}"
- else:
- commit_msg = f"feat: {slug} - AI-generated app added at {timestamp}"
-
- commands = [
- "git add .",
- f'git commit -m "{commit_msg}"',
- f"git push -u origin main"
- ]
-
- for cmd in commands:
- if not self._run_command(cmd, cwd=self.portfolio_repo):
- if "git push" in cmd and "rejected" in str(cmd):
- print("⚠️ リモートに変更があります。プル後に再実行してください")
- self._run_command("git pull origin main --rebase", cwd=self.portfolio_repo)
- self._run_command(cmd, cwd=self.portfolio_repo)
- else:
- return False
-
- print("✅ GitHubプッシュ完了")
- return True
-
- def publish(self, update: bool = False) -> Dict[str, str]:
- """メイン公開処理"""
- print("=" * 60)
- print("🚀 GitHub Portfolio Publisher v8.0")
- print(f"📍 対象リポジトリ: {self.remote_url}")
- print("=" * 60)
-
- # 1. DELIVERYフォルダ検証
- if not self.validate_delivery():
- return {"status": "error", "message": "DELIVERY validation failed"}
-
- # 2. クリーニング
- self.clean_delivery()
-
- # 3. slug取得
- slug = self.get_slug()
- print(f"📝 アプリ名: {slug}")
-
- # 4. ポートフォリオリポジトリ準備
- self.prepare_portfolio_repo()
-
- # 5. DELIVERYをapps/フォルダにコピー
- target_path = self.copy_delivery_to_app_folder(slug)
-
- # 6. ポートフォリオindex更新
- self.update_portfolio_index(slug)
-
- # 7. Git操作
- update_type = "update" if update else "add"
- if not self.git_operations(slug, update_type):
- return {"status": "error", "message": "Git operations failed"}
-
- # 8. 完了
- result = {
- "status": "success",
- "slug": slug,
- "local_path": str(target_path),
- "github_url": f"https://github.com/{self.github_username}/{self.repo_name}/tree/main/apps/{slug}",
- "pages_url": f"https://{self.github_username}.github.io/{self.repo_name}/apps/{slug}/"
- }
-
- print("\n" + "=" * 60)
- print("✅ 公開完了!")
- print(f"📁 ローカル: {result['local_path']}")
- print(f"🔗 GitHub: {result['github_url']}")
- print(f"🌐 Pages: {result['pages_url']}")
- print("=" * 60)
-
- return result
-
-
-def main():
- """CLI実行"""
- if len(sys.argv) > 1:
- project_path = sys.argv[1]
- else:
- project_path = os.getcwd()
-
- # updateフラグチェック
- update = "--update" in sys.argv or "-u" in sys.argv
-
- publisher = GitHubPublisherV8(project_path)
- result = publisher.publish(update=update)
-
- if result["status"] == "error":
- print(f"❌ エラー: {result['message']}")
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/improvement_loop_controller.py b/src/improvement_loop_controller.py
deleted file mode 100755
index 17b1af5..0000000
--- a/src/improvement_loop_controller.py
+++ /dev/null
@@ -1,388 +0,0 @@
-#!/usr/bin/env python3
-"""
-自律的改善ループコントローラー
-テスト失敗を検出し、自動的に修正を繰り返す
-"""
-
-import json
-import subprocess
-import time
-from datetime import datetime
-from pathlib import Path
-from typing import Dict, List, Optional, Tuple
-import logging
-
-# ロギング設定
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-
-class ImprovementLoopController:
- """
- 改善ループを制御するメインクラス
- """
-
- def __init__(self, project_path: str, max_iterations: int = 3):
- """
- 初期化
-
- Args:
- project_path: プロジェクトのパス
- max_iterations: 最大ループ回数(デフォルト3回)
- """
- self.project_path = Path(project_path)
- self.max_iterations = max_iterations
- self.iteration_count = 0
- self.test_results = []
- self.improvement_history = []
-
- # ログディレクトリを作成
- self.log_dir = self.project_path / "logs" / "improvement"
- self.log_dir.mkdir(parents=True, exist_ok=True)
-
- def run_improvement_cycle(self) -> Dict:
- """
- 改善サイクルを実行
-
- Returns:
- 最終結果の辞書
- """
- logger.info("🔄 改善ループを開始します")
-
- for iteration in range(1, self.max_iterations + 1):
- self.iteration_count = iteration
- logger.info(f"\n--- Iteration {iteration}/{self.max_iterations} ---")
-
- # 1. テスト評価
- test_result = self.evaluate_tests()
- self.test_results.append(test_result)
-
- # 2. 成功判定
- if test_result['overall_status'] == 'pass':
- logger.info("✅ 全テストがパスしました!")
- return self._create_success_report()
-
- # 3. 最終回の場合
- if iteration == self.max_iterations:
- logger.warning("⚠️ 最大試行回数に達しました")
- return self._create_partial_success_report()
-
- # 4. 改善計画作成
- improvement_plan = self.create_improvement_plan(test_result)
-
- # 5. コード修正
- fix_result = self.apply_fixes(improvement_plan)
-
- # 履歴に追加
- self.improvement_history.append({
- 'iteration': iteration,
- 'test_result': test_result,
- 'improvement_plan': improvement_plan,
- 'fix_result': fix_result
- })
-
- # 少し待機(ファイルシステムの同期待ち)
- time.sleep(2)
-
- return self._create_partial_success_report()
-
- def evaluate_tests(self) -> Dict:
- """
- テストを実行し、結果を評価
-
- Returns:
- テスト結果の辞書
- """
- logger.info("🔎 テストを評価中...")
-
- # テストコマンドを検出
- test_command = self._detect_test_command()
-
- if not test_command:
- return {
- 'overall_status': 'error',
- 'message': 'テストコマンドが見つかりません',
- 'timestamp': datetime.now().isoformat()
- }
-
- # テスト実行
- result = subprocess.run(
- test_command,
- shell=True,
- cwd=self.project_path,
- capture_output=True,
- text=True
- )
-
- # ログ保存
- log_file = self.log_dir / f"test_{self.iteration_count}.log"
- with open(log_file, 'w') as f:
- f.write(f"Command: {test_command}\n")
- f.write(f"Return Code: {result.returncode}\n")
- f.write(f"STDOUT:\n{result.stdout}\n")
- f.write(f"STDERR:\n{result.stderr}\n")
-
- # 結果解析
- return self._analyze_test_output(result, log_file)
-
- def _detect_test_command(self) -> Optional[str]:
- """
- プロジェクトに適したテストコマンドを検出
- """
- # package.jsonがある場合(Node.js)
- package_json = self.project_path / "package.json"
- if package_json.exists():
- with open(package_json) as f:
- package_data = json.load(f)
- if 'scripts' in package_data and 'test' in package_data['scripts']:
- return "npm test"
-
- # requirements.txtがある場合(Python)
- requirements = self.project_path / "requirements.txt"
- if requirements.exists():
- # pytestが一般的
- return "python -m pytest"
-
- # Makefileがある場合
- makefile = self.project_path / "Makefile"
- if makefile.exists():
- with open(makefile) as f:
- if 'test:' in f.read():
- return "make test"
-
- return None
-
- def _analyze_test_output(self, result: subprocess.CompletedProcess, log_file: Path) -> Dict:
- """
- テスト出力を解析
- """
- output = result.stdout + result.stderr
-
- # 成功/失敗の判定
- if result.returncode == 0:
- status = 'pass'
- else:
- status = 'fail'
-
- # 失敗テストの抽出(簡易版)
- failed_tests = []
- if status == 'fail':
- lines = output.split('\n')
- for i, line in enumerate(lines):
- if 'FAIL' in line or 'ERROR' in line or '✗' in line:
- failed_tests.append({
- 'line': line.strip(),
- 'context': lines[max(0, i-2):min(len(lines), i+3)]
- })
-
- return {
- 'overall_status': status,
- 'return_code': result.returncode,
- 'failed_tests': failed_tests,
- 'log_file': str(log_file),
- 'timestamp': datetime.now().isoformat()
- }
-
- def create_improvement_plan(self, test_result: Dict) -> Dict:
- """
- テスト結果から改善計画を作成
- """
- logger.info("📋 改善計画を作成中...")
-
- plan = {
- 'iteration': self.iteration_count,
- 'created_at': datetime.now().isoformat(),
- 'issues': [],
- 'fixes': []
- }
-
- # 失敗テストから問題を特定
- for failed_test in test_result.get('failed_tests', []):
- issue = self._analyze_failure(failed_test)
- plan['issues'].append(issue)
-
- # 修正案を生成
- fix = self._generate_fix_suggestion(issue)
- plan['fixes'].append(fix)
-
- # 計画を保存
- plan_file = self.log_dir / f"plan_{self.iteration_count}.json"
- with open(plan_file, 'w') as f:
- json.dump(plan, f, indent=2, ensure_ascii=False)
-
- logger.info(f"📝 {len(plan['fixes'])}個の修正案を作成しました")
-
- return plan
-
- def _analyze_failure(self, failed_test: Dict) -> Dict:
- """
- 失敗を分析
- """
- line = failed_test['line']
-
- # エラータイプを推定
- error_type = 'unknown'
- if 'TypeError' in line:
- error_type = 'type_error'
- elif 'ReferenceError' in line or 'NameError' in line:
- error_type = 'reference_error'
- elif 'SyntaxError' in line:
- error_type = 'syntax_error'
- elif 'AssertionError' in line or 'Expected' in line:
- error_type = 'assertion_error'
-
- return {
- 'type': error_type,
- 'description': line,
- 'context': failed_test.get('context', [])
- }
-
- def _generate_fix_suggestion(self, issue: Dict) -> Dict:
- """
- 修正案を生成
- """
- fix = {
- 'issue_type': issue['type'],
- 'priority': 'high',
- 'suggestion': ''
- }
-
- # エラータイプに応じた修正案
- if issue['type'] == 'type_error':
- fix['suggestion'] = '型チェックを追加し、適切な型変換を実装'
- elif issue['type'] == 'reference_error':
- fix['suggestion'] = '未定義の変数・関数を定義または import を追加'
- elif issue['type'] == 'syntax_error':
- fix['suggestion'] = '構文エラーを修正'
- elif issue['type'] == 'assertion_error':
- fix['suggestion'] = 'ロジックを見直し、期待される値を返すよう修正'
- else:
- fix['suggestion'] = 'エラーメッセージを分析し、適切な修正を実施'
-
- return fix
-
- def apply_fixes(self, improvement_plan: Dict) -> Dict:
- """
- 改善計画に基づいて修正を適用
-
- 注: 実際のコード修正はFixerエージェントが担当
- ここではその結果を記録
- """
- logger.info("🔧 修正を適用中...")
-
- result = {
- 'iteration': self.iteration_count,
- 'fixes_applied': len(improvement_plan['fixes']),
- 'timestamp': datetime.now().isoformat()
- }
-
- # 修正結果をログに記録
- fix_log = self.log_dir / f"fixes_{self.iteration_count}.json"
- with open(fix_log, 'w') as f:
- json.dump(result, f, indent=2)
-
- logger.info(f"✏️ {result['fixes_applied']}個の修正を適用しました")
-
- return result
-
- def _create_success_report(self) -> Dict:
- """
- 成功レポートを作成
- """
- report = {
- 'status': 'success',
- 'message': '全てのテストがパスしました',
- 'total_iterations': self.iteration_count,
- 'test_results': self.test_results,
- 'improvement_history': self.improvement_history,
- 'timestamp': datetime.now().isoformat()
- }
-
- # レポート保存
- self._save_report(report, 'success_report.json')
-
- return report
-
- def _create_partial_success_report(self) -> Dict:
- """
- 部分成功レポートを作成
- """
- # 最後のテスト結果から成功率を計算
- last_result = self.test_results[-1] if self.test_results else {}
-
- report = {
- 'status': 'partial_success',
- 'message': '一部のテストが失敗しましたが、改善を実施しました',
- 'total_iterations': self.iteration_count,
- 'remaining_issues': last_result.get('failed_tests', []),
- 'test_results': self.test_results,
- 'improvement_history': self.improvement_history,
- 'timestamp': datetime.now().isoformat(),
- 'known_limitations': self._generate_known_limitations()
- }
-
- # レポート保存
- self._save_report(report, 'partial_success_report.json')
-
- return report
-
- def _generate_known_limitations(self) -> List[str]:
- """
- 既知の制約を生成
- """
- limitations = []
-
- if self.test_results:
- last_result = self.test_results[-1]
- for failed_test in last_result.get('failed_tests', []):
- limitations.append(f"未解決: {failed_test.get('line', 'Unknown test failure')}")
-
- return limitations
-
- def _save_report(self, report: Dict, filename: str):
- """
- レポートを保存
- """
- report_file = self.log_dir / filename
- with open(report_file, 'w') as f:
- json.dump(report, f, indent=2, ensure_ascii=False)
-
- logger.info(f"📊 レポートを保存しました: {report_file}")
-
-
-def main():
- """
- CLI実行用
- """
- import argparse
-
- parser = argparse.ArgumentParser(description='自律的改善ループコントローラー')
- parser.add_argument('project_path', help='プロジェクトのパス')
- parser.add_argument('--max-iterations', type=int, default=3, help='最大ループ回数')
-
- args = parser.parse_args()
-
- controller = ImprovementLoopController(
- project_path=args.project_path,
- max_iterations=args.max_iterations
- )
-
- result = controller.run_improvement_cycle()
-
- # 結果を表示
- print(f"\n{'='*50}")
- print(f"Status: {result['status']}")
- print(f"Message: {result['message']}")
- print(f"Iterations: {result['total_iterations']}")
-
- if result['status'] == 'partial_success':
- print(f"\n既知の制約:")
- for limitation in result.get('known_limitations', []):
- print(f" - {limitation}")
-
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/launcher_generator.py b/src/launcher_generator.py
deleted file mode 100755
index 1afa4d6..0000000
--- a/src/launcher_generator.py
+++ /dev/null
@@ -1,275 +0,0 @@
-#!/usr/bin/env python3
-"""
-起動スクリプト自動生成ツール
-アプリケーションの種類を検出し、最適な起動スクリプトを生成
-"""
-
-import os
-import json
-from pathlib import Path
-from typing import Dict, Optional, List
-
-class LauncherGenerator:
- """
- 1クリック起動用スクリプトの自動生成
- """
-
- def __init__(self, app_path: str):
- self.app_path = Path(app_path)
- self.app_type = self.detect_app_type()
-
- def detect_app_type(self) -> str:
- """
- アプリケーションの種類を自動検出
- """
- # Node.js/npm
- if (self.app_path / "package.json").exists():
- package_json = json.loads((self.app_path / "package.json").read_text())
-
- # React/Next.js/Vue
- deps = package_json.get("dependencies", {})
- dev_deps = package_json.get("devDependencies", {})
- all_deps = {**deps, **dev_deps}
-
- if "react" in all_deps or "react-dom" in all_deps:
- return "react"
- elif "next" in all_deps:
- return "nextjs"
- elif "vue" in all_deps:
- return "vue"
- else:
- return "nodejs"
-
- # Python
- elif (self.app_path / "requirements.txt").exists() or (self.app_path / "app.py").exists():
- # Flask/FastAPI検出
- if (self.app_path / "requirements.txt").exists():
- requirements = (self.app_path / "requirements.txt").read_text()
- if "flask" in requirements.lower():
- return "flask"
- elif "fastapi" in requirements.lower():
- return "fastapi"
- return "python"
-
- # 静的サイト
- elif (self.app_path / "index.html").exists():
- return "static"
-
- return "unknown"
-
- def generate_launcher(self, output_path: Optional[str] = None) -> str:
- """
- 起動スクリプトを生成
- """
- if output_path is None:
- output_path = self.app_path / "launch_app.command"
-
- script_content = self.get_launcher_template()
-
- # スクリプトを保存
- output_file = Path(output_path)
- output_file.write_text(script_content)
- output_file.chmod(0o755) # 実行権限を付与
-
- return str(output_file)
-
- def get_launcher_template(self) -> str:
- """
- アプリタイプに応じた起動スクリプトテンプレートを返す
- """
- base_template = '''#!/bin/bash
-# Auto-generated launcher script
-# App Type: {app_type}
-
-set -e
-
-# カラー定義
-GREEN='\\033[0;32m'
-YELLOW='\\033[1;33m'
-BLUE='\\033[0;34m'
-RED='\\033[0;31m'
-NC='\\033[0m'
-
-# スクリプトのディレクトリに移動
-cd "$(dirname "$0")"
-
-echo -e "${{BLUE}}🚀 アプリケーション起動中...${{NC}}"
-echo ""
-
-# ポート検出関数
-find_free_port() {{
- local start_port=${{1:-3000}}
- local end_port=${{2:-9999}}
-
- for port in $(seq $start_port $end_port); do
- if ! lsof -i:$port >/dev/null 2>&1; then
- echo $port
- return 0
- fi
- done
-
- echo -e "${{RED}}❌ 空きポートが見つかりません${{NC}}"
- exit 1
-}}
-
-# クリーンアップ処理
-cleanup() {{
- echo ""
- echo -e "${{YELLOW}}🔄 クリーンアップ中...${{NC}}"
-
- # 子プロセスを終了
- if [ ! -z "$APP_PID" ]; then
- kill $APP_PID 2>/dev/null || true
- fi
-
- echo -e "${{GREEN}}✅ 終了しました${{NC}}"
-}}
-
-# 終了時のクリーンアップを設定
-trap cleanup EXIT
-
-'''
-
- # アプリタイプごとの起動コマンド
- if self.app_type in ["react", "vue", "nodejs"]:
- specific_part = '''# Node.js依存関係のインストール
-if [ ! -d "node_modules" ]; then
- echo -e "${YELLOW}📦 依存関係をインストール中...${NC}"
- npm install
-fi
-
-# 空きポートを検出
-PORT=$(find_free_port 3000)
-export PORT
-
-echo -e "${GREEN}✅ ポート $PORT を使用します${NC}"
-
-# アプリケーション起動
-echo -e "${BLUE}🌐 アプリケーションを起動中...${NC}"
-npm start &
-APP_PID=$!
-
-# ブラウザを開く
-sleep 3
-echo -e "${GREEN}🌐 ブラウザを開いています...${NC}"
-open "http://localhost:$PORT"
-
-# プロセスを待機
-echo ""
-echo -e "${YELLOW}終了するには Ctrl+C を押してください${NC}"
-wait $APP_PID
-'''
-
- elif self.app_type == "flask":
- specific_part = '''# Python依存関係のインストール
-if [ -f "requirements.txt" ]; then
- echo -e "${YELLOW}📦 依存関係をインストール中...${NC}"
- pip install -r requirements.txt
-fi
-
-# 空きポートを検出
-PORT=$(find_free_port 5000)
-
-echo -e "${GREEN}✅ ポート $PORT を使用します${NC}"
-
-# Flask アプリケーション起動
-echo -e "${BLUE}🌐 Flask アプリケーションを起動中...${NC}"
-export FLASK_APP=app.py
-export FLASK_ENV=development
-flask run --port=$PORT &
-APP_PID=$!
-
-# ブラウザを開く
-sleep 3
-echo -e "${GREEN}🌐 ブラウザを開いています...${NC}"
-open "http://localhost:$PORT"
-
-# プロセスを待機
-echo ""
-echo -e "${YELLOW}終了するには Ctrl+C を押してください${NC}"
-wait $APP_PID
-'''
-
- elif self.app_type == "fastapi":
- specific_part = '''# Python依存関係のインストール
-if [ -f "requirements.txt" ]; then
- echo -e "${YELLOW}📦 依存関係をインストール中...${NC}"
- pip install -r requirements.txt
-fi
-
-# 空きポートを検出
-PORT=$(find_free_port 8000)
-
-echo -e "${GREEN}✅ ポート $PORT を使用します${NC}"
-
-# FastAPI アプリケーション起動
-echo -e "${BLUE}🌐 FastAPI アプリケーションを起動中...${NC}"
-uvicorn app:app --reload --port $PORT &
-APP_PID=$!
-
-# ブラウザを開く
-sleep 3
-echo -e "${GREEN}🌐 ブラウザを開いています...${NC}"
-open "http://localhost:$PORT"
-
-# プロセスを待機
-echo ""
-echo -e "${YELLOW}終了するには Ctrl+C を押してください${NC}"
-wait $APP_PID
-'''
-
- elif self.app_type == "static":
- specific_part = '''# 空きポートを検出
-PORT=$(find_free_port 8000)
-
-echo -e "${GREEN}✅ ポート $PORT を使用します${NC}"
-
-# 静的サーバー起動
-echo -e "${BLUE}🌐 静的サーバーを起動中...${NC}"
-python3 -m http.server $PORT &
-APP_PID=$!
-
-# ブラウザを開く
-sleep 2
-echo -e "${GREEN}🌐 ブラウザを開いています...${NC}"
-open "http://localhost:$PORT"
-
-# プロセスを待機
-echo ""
-echo -e "${YELLOW}終了するには Ctrl+C を押してください${NC}"
-wait $APP_PID
-'''
-
- else:
- specific_part = '''echo -e "${RED}❌ アプリケーションタイプを検出できませんでした${NC}"
-echo "手動で起動してください"
-exit 1
-'''
-
- return base_template.format(app_type=self.app_type) + specific_part
-
-
-def main():
- """
- コマンドライン実行用
- """
- import argparse
-
- parser = argparse.ArgumentParser(description='起動スクリプト自動生成')
- parser.add_argument('app_path', help='アプリケーションのパス')
- parser.add_argument('--output', '-o', help='出力先パス', default=None)
-
- args = parser.parse_args()
-
- generator = LauncherGenerator(args.app_path)
- output_file = generator.generate_launcher(args.output)
-
- print(f"✅ 起動スクリプトを生成しました: {output_file}")
- print(f" アプリタイプ: {generator.app_type}")
- print(f"\n実行方法:")
- print(f" 1. Finderでダブルクリック")
- print(f" 2. または: {output_file}")
-
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/modification_workflow.py b/src/modification_workflow.py
deleted file mode 100755
index 47f3442..0000000
--- a/src/modification_workflow.py
+++ /dev/null
@@ -1,427 +0,0 @@
-#!/usr/bin/env python3
-"""
-修正ワークフロー(Phase 7)
-ユーザーレビュー後の修正を処理し、必要なフェーズを再実行
-
-フロー:
-1. 修正依頼の受付
-2. 影響範囲の分析
-3. 必要なフェーズの再実行
-4. Phase 6(ポートフォリオ公開)の再実行
-"""
-
-import os
-import sys
-import argparse
-from pathlib import Path
-from typing import List, Dict, Optional, Tuple
-from datetime import datetime
-
-# 同じディレクトリのモジュールをインポート
-sys.path.insert(0, str(Path(__file__).parent))
-
-from workflow_state_manager import (
- WorkflowStateManager,
- WorkflowStatus,
- get_state_manager,
-)
-from publish_portfolio import PortfolioPublisher
-
-
-class ModificationWorkflow:
- """修正ワークフローオーケストレーター(Phase 7)"""
-
- # 修正タイプと再実行フェーズのマッピング
- MODIFICATION_TYPES = {
- "ui": {
- "keywords": ["デザイン", "色", "レイアウト", "スタイル", "CSS", "見た目", "UI", "ボタン", "フォント"],
- "phases": [3, 6], # 実装 → 公開
- "description": "UI/デザイン変更",
- },
- "logic": {
- "keywords": ["ロジック", "機能", "動作", "バグ", "エラー", "修正", "追加", "削除"],
- "phases": [3, 4, 6], # 実装 → 改善ループ → 公開
- "description": "ロジック/機能変更",
- },
- "docs": {
- "keywords": ["ドキュメント", "README", "説明", "コメント", "ヘルプ"],
- "phases": [5, 6], # 完成処理 → 公開
- "description": "ドキュメント変更",
- },
- "security": {
- "keywords": ["セキュリティ", "認証", "パスワード", "API", "キー", "トークン"],
- "phases": [3, 4, 6], # 実装 → 改善ループ → 公開
- "description": "セキュリティ関連変更",
- },
- "full": {
- "keywords": ["全体", "大幅", "リファクタ", "作り直し"],
- "phases": [3, 4, 5, 6], # 実装 → 改善ループ → 完成処理 → 公開
- "description": "大規模変更",
- },
- }
-
- def __init__(self, project_path: str = None):
- self.project_path = Path(project_path) if project_path else Path.cwd()
- self.state_manager = get_state_manager(str(self.project_path))
-
- def print_banner(self, title: str, char: str = "="):
- """バナーを表示"""
- width = 60
- print("\n" + char * width)
- print(f" {title}")
- print(char * width)
-
- def print_success(self, message: str):
- print(f" ✅ {message}")
-
- def print_warning(self, message: str):
- print(f" ⚠️ {message}")
-
- def print_error(self, message: str):
- print(f" ❌ {message}")
-
- def print_info(self, message: str):
- print(f" ℹ️ {message}")
-
- def analyze_feedback(self, feedback: str) -> Tuple[str, List[int]]:
- """
- フィードバックを分析し、修正タイプと再実行フェーズを決定
-
- Returns:
- (modification_type, phases_to_rerun)
- """
- feedback_lower = feedback.lower()
-
- # キーワードマッチングで修正タイプを判定
- matched_types = []
- for mod_type, config in self.MODIFICATION_TYPES.items():
- for keyword in config["keywords"]:
- if keyword.lower() in feedback_lower:
- matched_types.append(mod_type)
- break
-
- # マッチしたタイプから最も包括的なフェーズセットを選択
- if not matched_types:
- # デフォルトはUI変更として扱う
- return "ui", [3, 6]
-
- # 複数マッチした場合は、より多くのフェーズを含むものを選択
- best_type = max(matched_types, key=lambda t: len(self.MODIFICATION_TYPES[t]["phases"]))
- return best_type, self.MODIFICATION_TYPES[best_type]["phases"]
-
- def request_modification(self, feedback: str, phases: List[int] = None) -> bool:
- """
- 修正を依頼
-
- Args:
- feedback: 修正内容
- phases: 再実行するフェーズ(省略時は自動判定)
-
- Returns:
- success: 成功したかどうか
- """
- self.print_banner("📝 Phase 7: 修正ワークフロー")
-
- # 状態確認
- state = self.state_manager.state
- if state is None:
- self.print_error("ワークフロー状態が見つかりません")
- return False
-
- if state.status != WorkflowStatus.AWAITING_REVIEW.value:
- self.print_warning(f"現在の状態: {state.status}")
- self.print_warning("修正依頼はユーザーレビュー待ち状態でのみ受け付けます")
-
- # フィードバック分析
- if phases is None:
- mod_type, phases = self.analyze_feedback(feedback)
- self.print_info(f"修正タイプ: {self.MODIFICATION_TYPES[mod_type]['description']}")
- else:
- mod_type = "custom"
-
- print(f"\n 修正内容: {feedback}")
- print(f" 再実行フェーズ: {phases}")
-
- # 修正依頼を記録
- self.state_manager.request_modification(feedback, phases)
-
- self.print_success("修正依頼を記録しました")
-
- # 次のアクションを表示
- print("\n 【次のステップ】")
- print(" 以下のフェーズを再実行してください:")
- for phase in phases:
- phase_name = self.state_manager.PHASES.get(phase, f"Phase {phase}")
- print(f" - Phase {phase}: {phase_name}")
-
- print("\n 【実行方法】")
- print(f" python modification_workflow.py --execute")
-
- return True
-
- def execute_modification(
- self,
- skip_confirm: bool = False,
- dry_run: bool = False,
- ) -> Tuple[bool, str]:
- """
- 修正ワークフローを実行
-
- Returns:
- (success, message)
- """
- self.print_banner("🔧 Phase 7: 修正実行")
-
- # 保留中の修正を取得
- modification = self.state_manager.get_pending_modification()
- if modification is None:
- self.print_error("保留中の修正依頼がありません")
- return False, "No pending modification"
-
- feedback = modification.get("feedback", "")
- phases = modification.get("phases_to_rerun", [])
- iteration = modification.get("iteration", 1)
-
- print(f"\n イテレーション: #{iteration}")
- print(f" 修正内容: {feedback}")
- print(f" 再実行フェーズ: {phases}")
-
- # 修正開始
- self.state_manager.start_modification()
-
- # フェーズ再実行のガイダンスを表示
- self.print_banner("修正実行ガイダンス", "─")
-
- print("\n 以下の手順で修正を実行してください:\n")
-
- for i, phase in enumerate(phases, 1):
- phase_name = self.state_manager.PHASES.get(phase, f"Phase {phase}")
-
- if phase == 3:
- print(f" {i}. Phase {phase}({phase_name})")
- print(f" 修正内容: {feedback}")
- print(f" → 該当するコードを修正してください")
- print()
-
- elif phase == 4:
- print(f" {i}. Phase {phase}({phase_name})")
- print(f" → テストを実行し、問題があれば修正してください")
- print()
-
- elif phase == 5:
- print(f" {i}. Phase {phase}({phase_name})")
- print(f" → ドキュメントを更新してください(必要な場合)")
- print()
-
- elif phase == 6:
- print(f" {i}. Phase {phase}({phase_name})")
- print(f" → 以下のコマンドで再公開してください:")
- print(f" python publish_portfolio.py {self.project_path} --skip-agent-review")
- print()
-
- # Phase 6が含まれている場合、自動実行オプション
- if 6 in phases:
- print("\n 【自動実行オプション】")
- print(" 修正完了後、以下のコマンドで Phase 6 を自動実行できます:")
- print(f" python modification_workflow.py --republish")
-
- return True, "Modification guidance displayed"
-
- def republish(
- self,
- app_name: str = None,
- skip_confirm: bool = False,
- dry_run: bool = False,
- ) -> Tuple[bool, str]:
- """
- 修正後の再公開(Phase 6 再実行)
-
- Returns:
- (success, message)
- """
- self.print_banner("🔄 再公開(Phase 6 再実行)")
-
- state = self.state_manager.state
- if state is None:
- self.print_error("ワークフロー状態が見つかりません")
- return False, "No workflow state"
-
- # アプリ名を取得
- if app_name is None:
- portfolio = state.portfolio
- app_name = portfolio.get("app_name")
- if not app_name:
- app_name = state.project_name
-
- if not app_name:
- self.print_error("アプリ名が特定できません")
- return False, "App name not found"
-
- print(f"\n アプリ名: {app_name}")
- print(f" ソース: {self.project_path}")
-
- # Phase 6 を再実行
- publisher = PortfolioPublisher(project_path=str(self.project_path))
- success, message = publisher.publish(
- source_dir=str(self.project_path),
- app_name=app_name,
- dry_run=dry_run,
- skip_confirm=skip_confirm,
- skip_agent_review=True, # 修正時はエージェントレビューをスキップ
- )
-
- if success:
- # 修正完了を記録
- self.state_manager.complete_modification()
- self.print_success("再公開完了")
- else:
- self.print_error(f"再公開失敗: {message}")
-
- return success, message
-
- def complete_workflow(self) -> bool:
- """ワークフローを完了としてマーク"""
- self.print_banner("🎉 ワークフロー完了")
-
- state = self.state_manager.state
- if state is None:
- self.print_error("ワークフロー状態が見つかりません")
- return False
-
- self.state_manager.complete_workflow()
-
- print("\n ワークフローが正常に完了しました。")
- print(f"\n プロジェクト: {state.project_name}")
- print(f" 公開URL: {state.portfolio.get('app_url', '(未設定)')}")
-
- if state.modifications:
- print(f"\n 修正イテレーション: {len(state.modifications)} 回")
-
- return True
-
- def show_status(self):
- """現在の状態を表示"""
- self.state_manager.print_status_report()
- print(self.state_manager.get_next_action_prompt())
-
-
-def main():
- """メインエントリーポイント"""
- parser = argparse.ArgumentParser(
- description="修正ワークフロー(Phase 7)",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-使用例:
- # 修正を依頼
- python modification_workflow.py --request "ボタンの色を青から緑に変更"
-
- # 修正実行ガイダンスを表示
- python modification_workflow.py --execute
-
- # 再公開(Phase 6 再実行)
- python modification_workflow.py --republish
-
- # ワークフローを完了
- python modification_workflow.py --complete
-
- # 状態を確認
- python modification_workflow.py --status
-
- # 特定のフェーズを再実行
- python modification_workflow.py --request "大幅な修正" --phases 3,4,5,6
- """,
- )
-
- parser.add_argument(
- "--path",
- default=".",
- help="プロジェクトパス",
- )
- parser.add_argument(
- "--request",
- metavar="FEEDBACK",
- help="修正を依頼(フィードバック内容を指定)",
- )
- parser.add_argument(
- "--phases",
- help="再実行するフェーズ(カンマ区切り、例: 3,4,6)",
- )
- parser.add_argument(
- "--execute",
- action="store_true",
- help="修正実行ガイダンスを表示",
- )
- parser.add_argument(
- "--republish",
- action="store_true",
- help="再公開(Phase 6 再実行)",
- )
- parser.add_argument(
- "--complete",
- action="store_true",
- help="ワークフローを完了としてマーク",
- )
- parser.add_argument(
- "--status",
- action="store_true",
- help="現在の状態を表示",
- )
- parser.add_argument(
- "--app-name",
- help="アプリ名(再公開時に使用)",
- )
- parser.add_argument(
- "-y", "--yes",
- action="store_true",
- help="確認プロンプトをスキップ",
- )
- parser.add_argument(
- "--dry-run",
- action="store_true",
- help="ドライラン",
- )
-
- args = parser.parse_args()
-
- # プロジェクトパスの解決
- project_path = Path(args.path).resolve()
- workflow = ModificationWorkflow(str(project_path))
-
- # コマンド実行
- if args.status:
- workflow.show_status()
-
- elif args.request:
- phases = None
- if args.phases:
- phases = [int(p.strip()) for p in args.phases.split(",")]
- workflow.request_modification(args.request, phases)
-
- elif args.execute:
- success, message = workflow.execute_modification(
- skip_confirm=args.yes,
- dry_run=args.dry_run,
- )
- if not success:
- sys.exit(1)
-
- elif args.republish:
- success, message = workflow.republish(
- app_name=args.app_name,
- skip_confirm=args.yes,
- dry_run=args.dry_run,
- )
- if not success:
- sys.exit(1)
-
- elif args.complete:
- if not workflow.complete_workflow():
- sys.exit(1)
-
- else:
- # デフォルトは状態表示
- workflow.show_status()
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/path_validator.py b/src/path_validator.py
deleted file mode 100755
index 7740195..0000000
--- a/src/path_validator.py
+++ /dev/null
@@ -1,290 +0,0 @@
-#!/usr/bin/env python3
-"""
-GitHub Pages用パス検証・自動修正ツール
-
-目的:
-- HTML/CSS/JS内の絶対パスを検出
-- 相対パスに自動変換
-- GitHub Pages環境を模倣したローカルテスト
-
-使用方法:
- python3 src/path_validator.py project/public/
-"""
-
-import re
-import sys
-from pathlib import Path
-from typing import List, Dict, Tuple
-import http.server
-import socketserver
-import threading
-import time
-import subprocess
-
-
-class PathValidator:
- def __init__(self, public_dir: Path):
- self.public_dir = Path(public_dir)
- self.issues: List[Dict] = []
- self.fixes: List[Dict] = []
-
- def validate_and_fix(self) -> Tuple[List[Dict], List[Dict]]:
- """パス検証と自動修正を実行"""
- print("=" * 60)
- print("🔍 GitHub Pages用パス検証開始")
- print("=" * 60)
- print(f"対象ディレクトリ: {self.public_dir}")
- print()
-
- # HTML, CSS, JSファイルを検証
- html_files = list(self.public_dir.glob("**/*.html"))
- css_files = list(self.public_dir.glob("**/*.css"))
- js_files = list(self.public_dir.glob("**/*.js"))
-
- all_files = html_files + css_files + js_files
-
- print(f"📄 検証対象: {len(all_files)}ファイル")
- print(f" - HTML: {len(html_files)}")
- print(f" - CSS: {len(css_files)}")
- print(f" - JS: {len(js_files)}")
- print()
-
- for file_path in all_files:
- self._validate_file(file_path)
-
- # 結果サマリー
- self._print_summary()
-
- return self.issues, self.fixes
-
- def _validate_file(self, file_path: Path):
- """個別ファイルの検証と修正"""
- try:
- content = file_path.read_text(encoding='utf-8')
- original_content = content
- modified = False
-
- relative_path = file_path.relative_to(self.public_dir)
-
- # 1. 絶対パス検出(/ で始まる src/href)
- absolute_paths = re.findall(r'((?:src|href|content)=["\'])(/[^"\']+)', content)
- for attr, path in absolute_paths:
- if not path.startswith('//'): # プロトコル相対URLは除外
- # 絶対パスを相対パスに変換
- fixed_path = '.' + path
- content = content.replace(f'{attr}{path}', f'{attr}{fixed_path}')
- modified = True
-
- self.issues.append({
- 'file': str(relative_path),
- 'type': '絶対パス',
- 'original': path,
- 'fixed': fixed_path
- })
-
- # 2. file:// プロトコル検出
- file_protocols = re.findall(r'file://[^\s\'"]+', content)
- for protocol_url in file_protocols:
- self.issues.append({
- 'file': str(relative_path),
- 'type': 'file://プロトコル',
- 'original': protocol_url,
- 'fixed': '(要手動修正)'
- })
-
- # 3. ../ の過度な使用(警告のみ)
- parent_refs = content.count('../')
- if parent_refs > 3:
- self.issues.append({
- 'file': str(relative_path),
- 'type': '../の過剰使用',
- 'original': f'{parent_refs}回',
- 'fixed': '(確認推奨)'
- })
-
- # 4. ルート相対パス(CSSのurl()内)
- css_urls = re.findall(r'url\(["\']?(/[^)"\'"]+)', content)
- for url_path in css_urls:
- if not url_path.startswith('//'):
- fixed_path = '.' + url_path
- content = re.sub(
- rf'url\(["\']?{re.escape(url_path)}',
- f'url({fixed_path}',
- content
- )
- modified = True
-
- self.issues.append({
- 'file': str(relative_path),
- 'type': 'CSS絶対パス',
- 'original': url_path,
- 'fixed': fixed_path
- })
-
- # 修正内容を保存
- if modified:
- file_path.write_text(content, encoding='utf-8')
- self.fixes.append({
- 'file': str(relative_path),
- 'changes': len([i for i in self.issues if i['file'] == str(relative_path)])
- })
- print(f"✅ 修正: {relative_path} ({len([i for i in self.issues if i['file'] == str(relative_path)])}箇所)")
-
- except Exception as e:
- print(f"⚠️ エラー: {relative_path} - {e}")
-
- def _print_summary(self):
- """結果サマリーを出力"""
- print()
- print("=" * 60)
- print("📊 検証結果サマリー")
- print("=" * 60)
-
- if not self.issues:
- print("✅ 問題なし!すべてのパスが相対パスです。")
- return
-
- # 問題タイプ別集計
- issue_types = {}
- for issue in self.issues:
- issue_type = issue['type']
- issue_types[issue_type] = issue_types.get(issue_type, 0) + 1
-
- print(f"🔴 検出された問題: {len(self.issues)}件")
- for issue_type, count in issue_types.items():
- print(f" - {issue_type}: {count}件")
-
- print()
- print(f"✅ 自動修正完了: {len(self.fixes)}ファイル")
-
- # 詳細リスト(最大10件)
- if self.issues:
- print()
- print("📋 問題詳細(最大10件):")
- for i, issue in enumerate(self.issues[:10]):
- print(f" {i+1}. [{issue['type']}] {issue['file']}")
- print(f" 変更前: {issue['original']}")
- print(f" 変更後: {issue['fixed']}")
-
- if len(self.issues) > 10:
- print(f" ... 他 {len(self.issues) - 10}件")
-
- print()
-
-
-class LocalServerTester:
- """GitHub Pages環境を模倣したローカルサーバーテスト"""
-
- def __init__(self, public_dir: Path, app_name: str):
- self.public_dir = Path(public_dir)
- self.app_name = app_name
- self.port = 8000
-
- def test(self):
- """ローカルサーバーで動作確認"""
- print("=" * 60)
- print("🌐 ローカルサーバーテスト(GitHub Pages環境模倣)")
- print("=" * 60)
-
- # サーバー起動
- print(f"サーバー起動中: http://localhost:{self.port}/{self.app_name}/")
-
- # シンプルなHTTPサーバー起動
- handler = http.server.SimpleHTTPRequestHandler
-
- try:
- with socketserver.TCPServer(("", self.port), handler) as httpd:
- # バックグラウンドでサーバー起動
- server_thread = threading.Thread(target=httpd.serve_forever)
- server_thread.daemon = True
- server_thread.start()
-
- time.sleep(1) # サーバー起動待機
-
- # 重要ファイルのアクセステスト
- test_urls = [
- f"http://localhost:{self.port}/{self.app_name}/index.html",
- f"http://localhost:{self.port}/{self.app_name}/about.html",
- f"http://localhost:{self.port}/{self.app_name}/explanation.mp3",
- ]
-
- print("\n📄 アクセステスト:")
- results = []
- for url in test_urls:
- try:
- result = subprocess.run(
- ['curl', '-s', '-o', '/dev/null', '-w', '%{http_code}', url],
- capture_output=True,
- text=True,
- timeout=5
- )
- status = result.stdout.strip()
-
- if status == '200':
- print(f" ✅ {url} - OK")
- results.append(True)
- else:
- print(f" ❌ {url} - NG (HTTP {status})")
- results.append(False)
- except Exception as e:
- print(f" ⚠️ {url} - エラー: {e}")
- results.append(False)
-
- # サーバー停止
- httpd.shutdown()
-
- # 結果サマリー
- print()
- if all(results):
- print("✅ すべてのファイルにアクセス可能です!")
- else:
- print("⚠️ 一部のファイルにアクセスできませんでした。")
-
- print()
- print("💡 手動確認:")
- print(f" ブラウザで確認: http://localhost:{self.port}/{self.app_name}/index.html")
- print(f" about.html: http://localhost:{self.port}/{self.app_name}/about.html")
-
- except OSError as e:
- print(f"⚠️ サーバー起動失敗: {e}")
- print(f" ポート{self.port}が使用中の可能性があります")
-
-
-def main():
- """メイン処理"""
- if len(sys.argv) < 2:
- print("使用方法: python3 src/path_validator.py ")
- print("例: python3 src/path_validator.py project/public/")
- sys.exit(1)
-
- public_dir = Path(sys.argv[1])
-
- if not public_dir.exists():
- print(f"エラー: ディレクトリが存在しません: {public_dir}")
- sys.exit(1)
-
- # アプリ名を推定(ディレクトリ名から)
- app_name = public_dir.parent.name if public_dir.name == 'public' else public_dir.name
-
- # パス検証と自動修正
- validator = PathValidator(public_dir)
- issues, fixes = validator.validate_and_fix()
-
- # ローカルサーバーテスト(オプション)
- if '--test' in sys.argv:
- tester = LocalServerTester(public_dir.parent, app_name)
- tester.test()
-
- # 終了コード
- if any(issue['type'] in ['file://プロトコル'] for issue in issues):
- print()
- print("⚠️ 手動修正が必要な問題があります。")
- sys.exit(1)
- else:
- print()
- print("✅ パス検証・修正完了!GitHub Pagesで正常に動作するはずです。")
- sys.exit(0)
-
-
-if __name__ == '__main__':
- main()
diff --git a/src/pdf_converter.js b/src/pdf_converter.js
deleted file mode 100755
index 48ce02f..0000000
--- a/src/pdf_converter.js
+++ /dev/null
@@ -1,347 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * Markdown to PDF Converter
- * Uses puppeteer for high-quality PDF generation
- */
-
-const fs = require('fs').promises;
-const path = require('path');
-const { marked } = require('marked');
-const puppeteer = require('puppeteer');
-
-// PDF変換設定
-const PDF_OPTIONS = {
- format: 'A4',
- margin: {
- top: '20mm',
- right: '20mm',
- bottom: '20mm',
- left: '20mm'
- },
- printBackground: true,
- displayHeaderFooter: true,
- headerTemplate: '',
- footerTemplate: ' /
'
-};
-
-// CSSスタイル(日本語フォント対応)
-const CSS_STYLE = `
-
-`;
-
-/**
- * MarkdownファイルをPDFに変換
- */
-async function convertMarkdownToPDF(mdFilePath, pdfFilePath) {
- try {
- // Markdownファイルを読み込み
- const markdown = await fs.readFile(mdFilePath, 'utf-8');
-
- // MarkdownをHTMLに変換
- const htmlContent = marked(markdown);
-
- // 完全なHTMLドキュメントを作成
- const fullHtml = `
-
-
-
-
-
- ${path.basename(mdFilePath, '.md')}
- ${CSS_STYLE}
-
-
- ${htmlContent}
-
-
- `;
-
- // Puppeteerでブラウザを起動
- const browser = await puppeteer.launch({
- headless: 'new',
- args: ['--no-sandbox', '--disable-setuid-sandbox']
- });
-
- const page = await browser.newPage();
-
- // HTMLを設定
- await page.setContent(fullHtml, {
- waitUntil: 'networkidle0'
- });
-
- // PDFを生成
- await page.pdf({
- path: pdfFilePath,
- ...PDF_OPTIONS
- });
-
- await browser.close();
-
- console.log(`✅ 変換完了: ${path.basename(pdfFilePath)}`);
- return true;
- } catch (error) {
- console.error(`❌ 変換エラー (${path.basename(mdFilePath)}):`, error.message);
- return false;
- }
-}
-
-/**
- * ディレクトリ内のすべてのMarkdownファイルをPDFに変換
- */
-async function convertAllMarkdownFiles(directory) {
- try {
- const files = await fs.readdir(directory);
- const mdFiles = files.filter(file => file.endsWith('.md'));
-
- if (mdFiles.length === 0) {
- console.log('変換するMarkdownファイルが見つかりません。');
- return;
- }
-
- console.log(`\n📄 ${mdFiles.length}個のファイルをPDFに変換します...\n`);
-
- let successCount = 0;
- for (const mdFile of mdFiles) {
- const mdPath = path.join(directory, mdFile);
- const pdfPath = path.join(directory, mdFile.replace('.md', '.pdf'));
-
- const success = await convertMarkdownToPDF(mdPath, pdfPath);
- if (success) successCount++;
- }
-
- console.log(`\n✨ 変換完了: ${successCount}/${mdFiles.length} ファイル`);
-
- } catch (error) {
- console.error('エラー:', error);
- process.exit(1);
- }
-}
-
-/**
- * package.jsonの依存関係を確認
- */
-async function checkDependencies() {
- try {
- require('marked');
- require('puppeteer');
- return true;
- } catch (error) {
- console.log('\n⚠️ 必要なパッケージがインストールされていません。');
- console.log('以下のコマンドを実行してください:\n');
- console.log('npm install marked puppeteer');
- console.log('\nまたは:\n');
- console.log('npm install -g marked puppeteer\n');
- return false;
- }
-}
-
-/**
- * メイン処理
- */
-async function main() {
- // 依存関係の確認
- const depsOk = await checkDependencies();
- if (!depsOk) {
- process.exit(1);
- }
-
- // コマンドライン引数の処理
- const args = process.argv.slice(2);
-
- if (args.length === 0) {
- // デフォルト: deliverables/01_documents/
- const defaultDir = path.join(process.cwd(), 'deliverables', '01_documents');
-
- try {
- await fs.access(defaultDir);
- await convertAllMarkdownFiles(defaultDir);
- } catch {
- console.log('使用方法:');
- console.log(' node pdf_converter.js [ディレクトリ]');
- console.log(' node pdf_converter.js [入力.md] [出力.pdf]');
- console.log('\n例:');
- console.log(' node pdf_converter.js deliverables/01_documents/');
- console.log(' node pdf_converter.js README.md README.pdf');
- }
- } else if (args.length === 1) {
- // ディレクトリ指定
- const dir = args[0];
- const stats = await fs.stat(dir);
-
- if (stats.isDirectory()) {
- await convertAllMarkdownFiles(dir);
- } else if (dir.endsWith('.md')) {
- // 単一ファイル(出力名自動)
- const pdfPath = dir.replace('.md', '.pdf');
- await convertMarkdownToPDF(dir, pdfPath);
- }
- } else if (args.length === 2) {
- // 入力と出力を指定
- const [input, output] = args;
- await convertMarkdownToPDF(input, output);
- }
-}
-
-// スクリプト実行
-if (require.main === module) {
- main().catch(console.error);
-}
-
-module.exports = {
- convertMarkdownToPDF,
- convertAllMarkdownFiles
-};
\ No newline at end of file
diff --git a/src/playwright_e2e_tester.py b/src/playwright_e2e_tester.py
deleted file mode 100755
index 17ebbb8..0000000
--- a/src/playwright_e2e_tester.py
+++ /dev/null
@@ -1,478 +0,0 @@
-#!/usr/bin/env python3
-"""
-Playwright E2Eテスター - Playwright MCPを使用した自動E2Eテスト
-
-目的:
-- すべてのユーザーフローを実際のブラウザで検証
-- 全機能が正常に動作するまで繰り返しテスト
-- Playwright MCPを活用してテストを自動生成・実行
-
-使用方法:
- python3 src/playwright_e2e_tester.py [--scenarios ]
-
-例:
- python3 src/playwright_e2e_tester.py http://localhost:3000
- python3 src/playwright_e2e_tester.py http://localhost:8080 --scenarios e2e_scenarios.json
-"""
-
-import json
-import subprocess
-import sys
-import time
-from pathlib import Path
-from typing import Dict, List, Optional
-import logging
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-class PlaywrightE2ETester:
- """Playwright MCPを使用したE2Eテスター"""
-
- def __init__(self, app_url: str, project_path: Path = Path(".")):
- self.app_url = app_url
- self.project_path = Path(project_path)
- self.scenarios_file = self.project_path / "E2E_SCENARIOS.json"
- self.results_file = self.project_path / "E2E_TEST_RESULTS.json"
-
- def generate_scenarios(self, project_info: Dict) -> List[Dict]:
- """プロジェクト情報からE2Eテストシナリオを自動生成
-
- Args:
- project_info: PROJECT_INFO.yamlから読み込んだプロジェクト情報
-
- Returns:
- List[Dict]: テストシナリオのリスト
- """
- project_name = project_info.get('name', 'App')
- project_type = project_info.get('type', 'web')
-
- logger.info(f"🎯 Generating E2E scenarios for: {project_name} ({project_type})")
-
- scenarios = []
-
- # 基本シナリオ(全アプリ共通)
- scenarios.append({
- "name": "Basic Page Load",
- "description": "アプリケーションが正常に読み込まれる",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "wait_for_load_state", "state": "networkidle"},
- {"action": "assert_title_contains", "text": project_name}
- ]
- })
-
- # プロジェクトタイプ別シナリオ
- if 'todo' in project_name.lower() or 'task' in project_name.lower():
- scenarios.extend(self._generate_todo_scenarios())
- elif 'game' in project_type.lower() or 'ゲーム' in project_name.lower():
- scenarios.extend(self._generate_game_scenarios())
- elif 'chat' in project_name.lower():
- scenarios.extend(self._generate_chat_scenarios())
- elif 'calculator' in project_name.lower():
- scenarios.extend(self._generate_calculator_scenarios())
- else:
- scenarios.extend(self._generate_generic_web_scenarios())
-
- # シナリオをファイルに保存
- with open(self.scenarios_file, 'w', encoding='utf-8') as f:
- json.dump({"scenarios": scenarios}, f, indent=2, ensure_ascii=False)
-
- logger.info(f"✅ Generated {len(scenarios)} scenarios: {self.scenarios_file}")
- return scenarios
-
- def _generate_todo_scenarios(self) -> List[Dict]:
- """TODOアプリ用シナリオ"""
- return [
- {
- "name": "Add New Todo",
- "description": "新しいTODOを追加できる",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "fill", "selector": "input[type='text'], input[placeholder*='todo' i]", "text": "Test Task"},
- {"action": "click", "selector": "button:has-text('Add'), button:has-text('追加')"},
- {"action": "wait_for_selector", "selector": "text=Test Task"},
- {"action": "assert_text_visible", "text": "Test Task"}
- ]
- },
- {
- "name": "Complete Todo",
- "description": "TODOを完了にできる",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "fill", "selector": "input[type='text']", "text": "Complete Me"},
- {"action": "click", "selector": "button:has-text('Add')"},
- {"action": "click", "selector": "input[type='checkbox']:near(text='Complete Me')"},
- {"action": "assert_element_has_class", "selector": "text=Complete Me", "class": "completed"}
- ]
- },
- {
- "name": "Delete Todo",
- "description": "TODOを削除できる",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "fill", "selector": "input[type='text']", "text": "Delete Me"},
- {"action": "click", "selector": "button:has-text('Add')"},
- {"action": "click", "selector": "button:has-text('Delete'):near(text='Delete Me'), button:has-text('削除'):near(text='Delete Me')"},
- {"action": "assert_text_not_visible", "text": "Delete Me"}
- ]
- }
- ]
-
- def _generate_game_scenarios(self) -> List[Dict]:
- """ゲームアプリ用シナリオ"""
- return [
- {
- "name": "Game Start",
- "description": "ゲームを開始できる",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "click", "selector": "button:has-text('Start'), button:has-text('スタート'), button:has-text('開始')"},
- {"action": "wait_for_selector", "selector": "canvas, #game-canvas"},
- {"action": "assert_element_visible", "selector": "canvas, #game-canvas"}
- ]
- },
- {
- "name": "Player Controls",
- "description": "プレイヤー操作が動作する",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "click", "selector": "button:has-text('Start')"},
- {"action": "keyboard_press", "key": "ArrowRight"},
- {"action": "keyboard_press", "key": "ArrowLeft"},
- {"action": "keyboard_press", "key": "Space"},
- {"action": "wait", "ms": 1000}
- ]
- },
- {
- "name": "Game Over Flow",
- "description": "ゲームオーバーからリスタートできる",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "click", "selector": "button:has-text('Start')"},
- {"action": "wait_for_selector", "selector": "text=Game Over, text=ゲームオーバー", "timeout": 60000},
- {"action": "click", "selector": "button:has-text('Restart'), button:has-text('再開始')"},
- {"action": "assert_element_visible", "selector": "canvas"}
- ]
- }
- ]
-
- def _generate_chat_scenarios(self) -> List[Dict]:
- """チャットアプリ用シナリオ"""
- return [
- {
- "name": "Send Message",
- "description": "メッセージを送信できる",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "fill", "selector": "input[type='text'], textarea", "text": "Hello World"},
- {"action": "click", "selector": "button:has-text('Send'), button:has-text('送信')"},
- {"action": "wait_for_selector", "selector": "text=Hello World"},
- {"action": "assert_text_visible", "text": "Hello World"}
- ]
- }
- ]
-
- def _generate_calculator_scenarios(self) -> List[Dict]:
- """計算機アプリ用シナリオ"""
- return [
- {
- "name": "Basic Calculation",
- "description": "基本的な計算ができる",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "click", "selector": "button:has-text('2')"},
- {"action": "click", "selector": "button:has-text('+')"},
- {"action": "click", "selector": "button:has-text('3')"},
- {"action": "click", "selector": "button:has-text('=')"},
- {"action": "assert_text_visible", "text": "5"}
- ]
- }
- ]
-
- def _generate_generic_web_scenarios(self) -> List[Dict]:
- """一般的なWebアプリ用シナリオ"""
- return [
- {
- "name": "Navigation",
- "description": "ページ間のナビゲーションが動作する",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "click", "selector": "a:has-text('About'), a:has-text('について')"},
- {"action": "wait_for_url", "pattern": "**/about**"},
- {"action": "go_back"},
- {"action": "wait_for_url", "pattern": self.app_url}
- ]
- },
- {
- "name": "Form Submission",
- "description": "フォーム送信が動作する",
- "steps": [
- {"action": "goto", "url": self.app_url},
- {"action": "fill", "selector": "input[type='text']:first", "text": "Test Input"},
- {"action": "click", "selector": "button[type='submit'], input[type='submit']"},
- {"action": "wait_for_load_state", "state": "networkidle"}
- ]
- }
- ]
-
- def run_scenarios_with_playwright_mcp(self, scenarios: List[Dict]) -> Dict:
- """Playwright MCPを使用してシナリオを実行
-
- 注意: この関数はClaude Codeの環境で実行されることを想定
- Playwright MCPツールを直接呼び出します
- """
- logger.info("\n" + "=" * 60)
- logger.info("🎭 Running E2E tests with Playwright MCP")
- logger.info("=" * 60)
-
- results = {
- "total": len(scenarios),
- "passed": 0,
- "failed": 0,
- "errors": [],
- "details": []
- }
-
- for i, scenario in enumerate(scenarios):
- logger.info(f"\n📋 Scenario {i+1}/{len(scenarios)}: {scenario['name']}")
- logger.info(f" {scenario['description']}")
-
- try:
- # Claude Codeの環境でPlaywright MCPツールを使用
- # 注意: この部分はClaude Codeによって実行される必要があります
- result = self._execute_scenario_steps(scenario)
-
- if result['success']:
- results['passed'] += 1
- logger.info(f" ✅ PASSED")
- else:
- results['failed'] += 1
- results['errors'].append({
- "scenario": scenario['name'],
- "error": result.get('error', 'Unknown error')
- })
- logger.error(f" ❌ FAILED: {result.get('error')}")
-
- results['details'].append({
- "scenario": scenario['name'],
- "success": result['success'],
- "duration_ms": result.get('duration_ms', 0),
- "error": result.get('error')
- })
-
- except Exception as e:
- results['failed'] += 1
- results['errors'].append({
- "scenario": scenario['name'],
- "error": str(e)
- })
- logger.error(f" ❌ ERROR: {e}")
-
- # 結果を保存
- with open(self.results_file, 'w', encoding='utf-8') as f:
- json.dump(results, f, indent=2, ensure_ascii=False)
-
- logger.info("\n" + "=" * 60)
- logger.info(f"📊 E2E Test Results")
- logger.info("=" * 60)
- logger.info(f"Total: {results['total']}")
- logger.info(f"✅ Passed: {results['passed']}")
- logger.info(f"❌ Failed: {results['failed']}")
- logger.info(f"Pass Rate: {results['passed']/results['total']*100:.1f}%")
- logger.info("=" * 60)
-
- return results
-
- def _execute_scenario_steps(self, scenario: Dict) -> Dict:
- """シナリオのステップを実行
-
- 注意: この関数はClaude Codeによって呼び出され、
- Playwright MCPツールを使用してブラウザ操作を実行します
-
- Returns:
- Dict: {'success': bool, 'error': str, 'duration_ms': int}
- """
- # この関数はClaude Codeによってオーバーライドされることを想定
- # ここではダミー実装を提供
- logger.warning("⚠️ This function should be called by Claude Code with Playwright MCP access")
-
- return {
- "success": False,
- "error": "Playwright MCP is not available in standalone execution. Run via Claude Code.",
- "duration_ms": 0
- }
-
- def generate_test_report_html(self) -> Path:
- """E2Eテスト結果のHTML レポートを生成"""
- if not self.results_file.exists():
- logger.error("❌ No test results found")
- return None
-
- with open(self.results_file, 'r', encoding='utf-8') as f:
- results = json.load(f)
-
- report_path = self.project_path / "E2E_TEST_REPORT.html"
-
- pass_rate = results['passed'] / results['total'] * 100 if results['total'] > 0 else 0
- status_color = "#4CAF50" if pass_rate >= 100 else "#FF9800" if pass_rate >= 70 else "#F44336"
-
- html_content = f"""
-
-
-
-
- E2E Test Report
-
-
-
-
-
-
-
-
Total Tests
-
{results['total']}
-
-
-
✅ Passed
-
{results['passed']}
-
-
-
❌ Failed
-
{results['failed']}
-
-
-
Pass Rate
-
{pass_rate:.1f}%
-
-
-
-
-
Test Scenarios
-"""
-
- for detail in results.get('details', []):
- status_icon = "✅" if detail['success'] else "❌"
- status_class = "passed" if detail['success'] else "failed"
- error_info = f"
Error: {detail['error']}
" if detail.get('error') else ""
-
- html_content += f"""
-
-
- {status_icon} {detail['scenario']}
- {detail.get('duration_ms', 0)}ms
-
- {error_info}
-
-"""
-
- html_content += """
-
-
-
-"""
-
- with open(report_path, 'w', encoding='utf-8') as f:
- f.write(html_content)
-
- logger.info(f"✅ HTML report generated: {report_path}")
- return report_path
-
-
-def main():
- """CLI エントリーポイント"""
- if len(sys.argv) < 2:
- print("Usage: python3 src/playwright_e2e_tester.py [--scenarios ]")
- print("\nExample:")
- print(" python3 src/playwright_e2e_tester.py http://localhost:3000")
- sys.exit(1)
-
- app_url = sys.argv[1]
- scenarios_file = None
-
- if '--scenarios' in sys.argv:
- idx = sys.argv.index('--scenarios')
- scenarios_file = sys.argv[idx + 1] if len(sys.argv) > idx + 1 else None
-
- tester = PlaywrightE2ETester(app_url)
-
- # PROJECT_INFO.yamlから情報を読み込み
- project_info_path = Path("PROJECT_INFO.yaml")
- if project_info_path.exists():
- import yaml
- with open(project_info_path, 'r') as f:
- data = yaml.safe_load(f)
- project_info = data.get('project', {})
- else:
- project_info = {'name': 'App', 'type': 'web'}
-
- # シナリオ生成
- scenarios = tester.generate_scenarios(project_info)
-
- print("\n" + "=" * 60)
- print("🎭 Playwright E2E Tester")
- print("=" * 60)
- print(f"App URL: {app_url}")
- print(f"Scenarios: {len(scenarios)}")
- print("=" * 60)
- print("\n⚠️ This script requires Playwright MCP to run tests.")
- print("Please execute via Claude Code with Playwright MCP enabled.")
- print("\nScenarios have been generated:")
- print(f" {tester.scenarios_file}")
- print("\nTo run tests, use Claude Code and say:")
- print(" 'Run E2E tests using Playwright MCP with E2E_SCENARIOS.json'")
-
-
-if __name__ == '__main__':
- main()
diff --git a/src/portfolio_cleanup.py b/src/portfolio_cleanup.py
deleted file mode 100755
index 441059e..0000000
--- a/src/portfolio_cleanup.py
+++ /dev/null
@@ -1,292 +0,0 @@
-#!/usr/bin/env python3
-"""
-GitHubポートフォリオ クリーンアップスクリプト
-既存のリポジトリから不要なファイルを除去
-"""
-
-import os
-import sys
-import subprocess
-import shutil
-from pathlib import Path
-from typing import List, Set
-
-class PortfolioCleanup:
- """ポートフォリオクリーンアップクラス"""
-
- def __init__(self, portfolio_repo: str = None):
- """
- Args:
- portfolio_repo: ポートフォリオリポジトリのパス
- """
- self.portfolio_repo = portfolio_repo or os.path.expanduser("~/Desktop/GitHub/ai-agent-portfolio")
-
- # 削除すべきファイル/ディレクトリのパターン
- self.remove_patterns = {
- # ディレクトリ
- '__pycache__',
- 'node_modules',
- '.git',
- 'venv',
- 'env',
- '.vscode',
- '.idea',
-
- # エージェント関連ファイル(完全一致)
- 'claude_agent_executor.py',
- 'workflow_orchestrator.py',
- 'launcher_generator.py',
- 'portfolio_publisher.py',
- 'requirements_gatherer.py',
- 'documentation_generator.py',
- 'improvement_loop_controller.py',
- 'progress_reporter.py',
- 'tts_generator.py',
- 'tts_smart_generator.py',
- 'pdf_converter.js',
- 'client_document_generator.py',
- 'enhanced_client_document_generator.py',
- 'portfolio_doc_generator.py',
- 'error_handler.sh',
-
- # ビルドファイル
- '*.pyc',
- '*.pyo',
- '*.pyd',
- '.DS_Store',
- 'Thumbs.db',
-
- # 環境ファイル
- '.env',
- '.env.local',
- '*.env'
- }
-
- def scan_directory(self, path: Path) -> List[Path]:
- """ディレクトリをスキャンして削除対象を検出"""
- to_remove = []
-
- for item in path.rglob('*'):
- # ファイル名取得
- name = item.name
-
- # 完全一致チェック
- if name in self.remove_patterns:
- to_remove.append(item)
- continue
-
- # パターンマッチチェック
- for pattern in self.remove_patterns:
- if '*' in pattern:
- # ワイルドカードパターン
- import fnmatch
- if fnmatch.fnmatch(name, pattern):
- to_remove.append(item)
- break
- elif pattern in name.lower():
- # 部分一致(エージェント関連)
- if 'agent' in pattern or 'orchestrator' in pattern:
- to_remove.append(item)
- break
-
- return to_remove
-
- def cleanup_app(self, app_name: str, dry_run: bool = True):
- """特定アプリをクリーンアップ"""
- app_path = Path(self.portfolio_repo) / 'apps' / app_name
-
- if not app_path.exists():
- print(f"❌ アプリが見つかりません: {app_name}")
- return
-
- print(f"\n🧹 {app_name} のクリーンアップ開始...")
- print(f"📁 パス: {app_path}")
-
- # 削除対象をスキャン
- to_remove = self.scan_directory(app_path)
-
- if not to_remove:
- print("✅ クリーンアップ不要(削除対象なし)")
- return
-
- # 削除対象を表示
- print(f"\n📝 削除対象({len(to_remove)}件):")
- for item in sorted(to_remove):
- rel_path = item.relative_to(app_path)
- if item.is_dir():
- print(f" 📁 {rel_path}/")
- else:
- print(f" 📄 {rel_path}")
-
- if dry_run:
- print("\n⚠️ ドライランモード - 実際には削除されません")
- print("実行するには --execute オプションを追加してください")
- else:
- # 実際に削除
- confirm = input("\n本当に削除しますか? (yes/no): ")
- if confirm.lower() == 'yes':
- for item in to_remove:
- try:
- if item.is_dir():
- shutil.rmtree(item)
- print(f" ✅ 削除: {item.name}/")
- else:
- item.unlink()
- print(f" ✅ 削除: {item.name}")
- except Exception as e:
- print(f" ❌ 削除失敗: {item.name} - {e}")
-
- print("\n✅ クリーンアップ完了")
- else:
- print("❌ キャンセルしました")
-
- def cleanup_all(self, dry_run: bool = True):
- """全アプリをクリーンアップ"""
- apps_dir = Path(self.portfolio_repo) / 'apps'
-
- if not apps_dir.exists():
- print("❌ appsディレクトリが見つかりません")
- return
-
- # 全アプリをリスト
- apps = [d.name for d in apps_dir.iterdir() if d.is_dir()]
-
- if not apps:
- print("ℹ️ アプリが見つかりません")
- return
-
- print(f"\n📱 {len(apps)}個のアプリが見つかりました:")
- for app in apps:
- print(f" - {app}")
-
- print("\n" + "="*50)
-
- # 各アプリをクリーンアップ
- for app in apps:
- self.cleanup_app(app, dry_run)
- print("\n" + "="*50)
-
- def generate_gitignore(self):
- """適切な.gitignoreファイルを生成"""
- gitignore_content = """# Python
-__pycache__/
-*.py[cod]
-*$py.class
-*.so
-.Python
-env/
-venv/
-ENV/
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# Node.js
-node_modules/
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-.pnpm-debug.log*
-
-# Environment files
-.env
-.env.local
-.env.production.local
-.env.development.local
-.env.test.local
-*.env
-
-# IDE
-.vscode/
-.idea/
-*.swp
-*.swo
-*~
-.DS_Store
-Thumbs.db
-
-# Logs
-logs/
-*.log
-
-# Testing
-coverage/
-.coverage
-htmlcov/
-.pytest_cache/
-.tox/
-
-# Build outputs
-dist/
-build/
-*.min.js
-*.min.css
-
-# Agent/Development files (SHOULD NOT BE IN PORTFOLIO)
-*agent*.py
-*orchestrator*.py
-*launcher_generator*.py
-*portfolio_publisher*.py
-*requirements_gatherer*.py
-*documentation_generator*.py
-*improvement_loop*.py
-*progress_reporter*.py
-*tts_generator*.py
-*pdf_converter*.py
-*client_document*.py
-error_handler.sh
-workflow_*.py
-claude_*.py
-
-# Temporary files
-*.tmp
-*.temp
-.cache/
-"""
-
- gitignore_path = Path(self.portfolio_repo) / '.gitignore'
- with open(gitignore_path, 'w') as f:
- f.write(gitignore_content)
-
- print(f"✅ .gitignore を生成しました: {gitignore_path}")
-
-def main():
- """コマンドライン実行用"""
- import argparse
-
- parser = argparse.ArgumentParser(description='GitHubポートフォリオのクリーンアップ')
- parser.add_argument('app_name', nargs='?', help='クリーンアップするアプリ名(省略時は全アプリ)')
- parser.add_argument('--execute', action='store_true', help='実際に削除を実行')
- parser.add_argument('--repo', help='ポートフォリオリポジトリのパス')
- parser.add_argument('--gitignore', action='store_true', help='.gitignoreを生成')
-
- args = parser.parse_args()
-
- cleaner = PortfolioCleanup(args.repo)
-
- if args.gitignore:
- cleaner.generate_gitignore()
- return
-
- dry_run = not args.execute
-
- if args.app_name:
- cleaner.cleanup_app(args.app_name, dry_run)
- else:
- cleaner.cleanup_all(dry_run)
-
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/portfolio_config.py b/src/portfolio_config.py
deleted file mode 100755
index 3c721fe..0000000
--- a/src/portfolio_config.py
+++ /dev/null
@@ -1,320 +0,0 @@
-#!/usr/bin/env python3
-"""
-ポートフォリオ公開設定
-階層型設定システムと連携して、安全な公開を実現
-"""
-
-import os
-import sys
-from pathlib import Path
-from typing import Dict, List, Set
-
-# 階層型設定システムのパスを追加
-CONFIG_SCRIPTS_DIR = Path.home() / ".config" / "ai-agents" / "scripts"
-if CONFIG_SCRIPTS_DIR.exists():
- sys.path.insert(0, str(CONFIG_SCRIPTS_DIR))
- try:
- from config_resolver import load_profile, resolve_config
- except ImportError:
- load_profile = None
- resolve_config = None
-else:
- load_profile = None
- resolve_config = None
-
-
-class PortfolioConfig:
- """ポートフォリオ公開の設定管理"""
-
- def __init__(self, project_path: str = None):
- self.project_path = Path(project_path) if project_path else Path.cwd()
- self._load_config()
-
- def _load_config(self):
- """設定を読み込み"""
- # 階層型設定システムから読み込み
- if resolve_config:
- config = resolve_config(str(self.project_path))
- else:
- config = {}
-
- # GitHub設定
- self.github_username = config.get("GITHUB_USERNAME", os.environ.get("GITHUB_USERNAME", ""))
- self.github_repo = config.get("GITHUB_PORTFOLIO_REPO", os.environ.get("GITHUB_PORTFOLIO_REPO", "ai-agent-portfolio"))
- self.github_token = config.get("GITHUB_TOKEN", os.environ.get("GITHUB_TOKEN", ""))
-
- # リポジトリURL
- self.repo_url = f"https://github.com/{self.github_username}/{self.github_repo}"
- self.repo_clone_url = f"https://github.com/{self.github_username}/{self.github_repo}.git"
-
- # GitHub Pages URL
- self.pages_url = f"https://{self.github_username}.github.io/{self.github_repo}"
-
- # =========================================
- # 除外パターン(絶対に公開しないファイル)
- # =========================================
-
- EXCLUDE_PATTERNS: Set[str] = {
- # 環境設定・認証
- ".env",
- ".env.*",
- ".env.local",
- ".env.development",
- ".env.production",
- "*.env",
-
- # 認証情報
- "credentials/",
- "credentials",
- "*.key",
- "*.pem",
- "*.p12",
- "*.pfx",
- "*.crt",
- "*.cer",
- "secrets.*",
- "secret.*",
- "*secret*",
- "*credential*",
- "*password*",
- "service-account*.json",
- "gcp-*.json",
- "firebase-*.json",
-
- # Git・バージョン管理
- ".git/",
- ".git",
- ".gitignore",
- ".gitattributes",
-
- # 依存関係・ビルド
- "node_modules/",
- "node_modules",
- "__pycache__/",
- "__pycache__",
- "*.pyc",
- "*.pyo",
- ".cache/",
- "dist/",
- "build/",
- "*.egg-info/",
- ".eggs/",
- "venv/",
- ".venv/",
- "env/",
-
- # IDE・エディタ
- ".idea/",
- ".vscode/",
- "*.swp",
- "*.swo",
- "*~",
- ".DS_Store",
- "Thumbs.db",
-
- # テスト・カバレッジ
- "coverage/",
- ".coverage",
- "htmlcov/",
- ".pytest_cache/",
- ".nyc_output/",
-
- # ログ・一時ファイル
- "*.log",
- "logs/",
- "tmp/",
- "temp/",
-
- # ワークフロー固有
- "worktrees/",
- "ai-agents-config/",
- "CLAUDE.md",
- "*.command",
-
- # その他
- "package-lock.json",
- "yarn.lock",
- "pnpm-lock.yaml",
- }
-
- # =========================================
- # 公開必須ファイル
- # =========================================
-
- REQUIRED_FILES: List[str] = [
- "index.html",
- "README.md",
- ]
-
- RECOMMENDED_FILES: List[str] = [
- "about.html",
- "style.css",
- "app.js",
- "script.js",
- ]
-
- # =========================================
- # セキュリティチェック用パターン
- # =========================================
-
- # APIキーパターン(正規表現)
- API_KEY_PATTERNS: List[str] = [
- # 汎用
- r'["\']?[Aa][Pp][Ii][_-]?[Kk][Ee][Yy]["\']?\s*[:=]\s*["\'][A-Za-z0-9_\-]{20,}["\']',
- r'["\']?[Ss][Ee][Cc][Rr][Ee][Tt][_-]?[Kk][Ee][Yy]["\']?\s*[:=]\s*["\'][A-Za-z0-9_\-]{20,}["\']',
- r'["\']?[Aa][Cc][Cc][Ee][Ss][Ss][_-]?[Tt][Oo][Kk][Ee][Nn]["\']?\s*[:=]\s*["\'][A-Za-z0-9_\-]{20,}["\']',
-
- # AWS
- r'AKIA[0-9A-Z]{16}',
- r'["\']?aws[_-]?access[_-]?key[_-]?id["\']?\s*[:=]\s*["\'][A-Z0-9]{20}["\']',
- r'["\']?aws[_-]?secret[_-]?access[_-]?key["\']?\s*[:=]\s*["\'][A-Za-z0-9/+=]{40}["\']',
-
- # GCP
- r'"type"\s*:\s*"service_account"',
- r'"private_key"\s*:\s*"-----BEGIN',
- r'AIza[0-9A-Za-z_-]{35}', # Google API Key
-
- # Firebase
- r'["\']?apiKey["\']?\s*:\s*["\']AIza[A-Za-z0-9_-]{35}["\']',
-
- # GitHub
- r'gh[pousr]_[A-Za-z0-9]{36,}',
- r'github_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}',
-
- # Stripe
- r'sk_live_[A-Za-z0-9]{24,}',
- r'pk_live_[A-Za-z0-9]{24,}',
- r'sk_test_[A-Za-z0-9]{24,}',
-
- # OpenAI
- r'sk-[A-Za-z0-9]{48}',
-
- # Anthropic
- r'sk-ant-[A-Za-z0-9_-]{40,}',
-
- # Slack
- r'xox[baprs]-[0-9]{10,}-[A-Za-z0-9]{10,}',
-
- # 汎用パスワード
- r'["\']?password["\']?\s*[:=]\s*["\'][^"\']{8,}["\']',
- r'["\']?passwd["\']?\s*[:=]\s*["\'][^"\']{8,}["\']',
-
- # データベース接続
- r'mongodb(\+srv)?://[^\s]+',
- r'postgres(ql)?://[^\s]+',
- r'mysql://[^\s]+',
- r'redis://[^\s]+',
- ]
-
- # 危険なファイル内容パターン
- DANGEROUS_CONTENT_PATTERNS: List[str] = [
- r'-----BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----',
- r'-----BEGIN CERTIFICATE-----',
- r'"private_key_id"\s*:',
- r'"client_email"\s*:\s*"[^"]+@[^"]+\.iam\.gserviceaccount\.com"',
- ]
-
- # 内部パス・URL(漏洩リスク)
- INTERNAL_PATH_PATTERNS: List[str] = [
- r'/Users/[a-zA-Z0-9_-]+/',
- r'/home/[a-zA-Z0-9_-]+/',
- r'C:\\Users\\[a-zA-Z0-9_-]+\\',
- r'192\.168\.\d{1,3}\.\d{1,3}',
- r'10\.\d{1,3}\.\d{1,3}\.\d{1,3}',
- r'172\.(1[6-9]|2[0-9]|3[01])\.\d{1,3}\.\d{1,3}',
- r'localhost:\d{4,5}',
- ]
-
- # =========================================
- # 公開対象の拡張子
- # =========================================
-
- ALLOWED_EXTENSIONS: Set[str] = {
- # Web
- ".html", ".htm", ".css", ".js", ".jsx", ".ts", ".tsx",
- ".json", ".xml", ".svg", ".ico",
-
- # 画像
- ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif",
-
- # 音声・動画
- ".mp3", ".wav", ".ogg", ".mp4", ".webm",
-
- # フォント
- ".woff", ".woff2", ".ttf", ".otf", ".eot",
-
- # ドキュメント
- ".md", ".txt", ".pdf",
-
- # データ
- ".csv",
-
- # その他
- ".map", # Source maps(必要な場合のみ)
- }
-
- # =========================================
- # ヘルパーメソッド
- # =========================================
-
- def should_exclude(self, filepath: str) -> bool:
- """ファイルを除外すべきかチェック"""
- path = Path(filepath)
- name = path.name
-
- # パターンマッチ
- for pattern in self.EXCLUDE_PATTERNS:
- if pattern.endswith("/"):
- # ディレクトリパターン
- if pattern.rstrip("/") in str(path.parent).split(os.sep):
- return True
- if name == pattern.rstrip("/"):
- return True
- elif "*" in pattern:
- # ワイルドカードパターン
- import fnmatch
- if fnmatch.fnmatch(name, pattern):
- return True
- if fnmatch.fnmatch(str(path), pattern):
- return True
- else:
- # 完全一致
- if name == pattern:
- return True
-
- return False
-
- def is_allowed_extension(self, filepath: str) -> bool:
- """許可された拡張子かチェック"""
- ext = Path(filepath).suffix.lower()
- return ext in self.ALLOWED_EXTENSIONS or ext == "" # 拡張子なしも許可(README等)
-
- def get_app_url(self, app_name: str) -> str:
- """アプリのGitHub Pages URLを取得"""
- return f"{self.pages_url}/apps/{app_name}/"
-
- def get_repo_app_path(self, app_name: str) -> str:
- """リポジトリ内のアプリパスを取得"""
- return f"apps/{app_name}"
-
-
-# グローバル設定インスタンス
-_config = None
-
-def get_config(project_path: str = None) -> PortfolioConfig:
- """設定インスタンスを取得"""
- global _config
- if _config is None or project_path:
- _config = PortfolioConfig(project_path)
- return _config
-
-
-if __name__ == "__main__":
- # テスト実行
- config = get_config()
- print(f"GitHub User: {config.github_username}")
- print(f"GitHub Repo: {config.github_repo}")
- print(f"Repo URL: {config.repo_url}")
- print(f"Pages URL: {config.pages_url}")
- print(f"\nExclude patterns: {len(config.EXCLUDE_PATTERNS)}")
- print(f"API key patterns: {len(config.API_KEY_PATTERNS)}")
diff --git a/src/portfolio_doc_generator.py b/src/portfolio_doc_generator.py
deleted file mode 100755
index c5e4ddc..0000000
--- a/src/portfolio_doc_generator.py
+++ /dev/null
@@ -1,163 +0,0 @@
-#!/usr/bin/env python3
-"""
-Portfolio用プロフェッショナル文書生成
-技術アピール用にClient同等の正式文書を生成
-"""
-
-import os
-import sys
-import subprocess
-from pathlib import Path
-from datetime import datetime
-
-# enhanced_client_document_generatorを流用
-from enhanced_client_document_generator import ClientDocumentGenerator
-
-class PortfolioDocumentGenerator(ClientDocumentGenerator):
- """Portfolio用にカスタマイズされた文書生成器"""
-
- def __init__(self, project_path="."):
- super().__init__(project_path)
- self.output_dir = Path(project_path) / "professional-docs"
-
- def generate_portfolio_documents(self):
- """Portfolio用の文書セットを生成"""
- print("📄 Portfolio用プロフェッショナル文書を生成中...")
-
- # 出力ディレクトリ作成
- self.output_dir.mkdir(parents=True, exist_ok=True)
-
- # 各文書を生成(Markdown形式)
- documents = {
- "requirements-spec.md": self.generate_requirements_doc(),
- "design-spec.md": self.generate_basic_design(),
- "test-report.md": self.generate_test_report(),
- "user-manual.md": self.generate_user_manual()
- }
-
- # Markdownファイルを生成
- for filename, content in documents.items():
- filepath = self.output_dir / filename
- with open(filepath, 'w', encoding='utf-8') as f:
- # Portfolio用のヘッダーを追加
- header = f"""---
-title: {filename.replace('.md', '').replace('-', ' ').title()}
-project: {self.project_info.get('project', {}).get('name', 'Project')}
-generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
-ai_generated: true
----
-
-# 📋 AI自動生成ドキュメント
-
-> このドキュメントは、AIエージェントシステムにより自動生成されました。
-> 実際の開発プロジェクトと同等の品質の文書を、わずか数秒で生成できることを実証しています。
-
----
-
-"""
- f.write(header + content)
- print(f" ✅ {filename}")
-
- # PDF変換を試みる
- self.convert_to_pdf_if_possible()
-
- # READMEを生成
- self.generate_docs_readme()
-
- return self.output_dir
-
- def convert_to_pdf_if_possible(self):
- """可能であればPDFに変換"""
- # pdf_converter.jsが利用可能か確認
- pdf_converter = Path(__file__).parent / "pdf_converter.js"
-
- if pdf_converter.exists() and (self.project_path / "node_modules" / "puppeteer").exists():
- print(" 📄 PDFに変換中...")
- try:
- subprocess.run(
- ["node", str(pdf_converter), str(self.output_dir)],
- capture_output=True,
- text=True,
- timeout=30
- )
- print(" ✅ PDF変換完了")
- except Exception as e:
- print(f" ⚠️ PDF変換スキップ: {e}")
- else:
- print(" ℹ️ PDF変換は利用できません(Markdown形式で保存)")
-
- def generate_docs_readme(self):
- """professional-docs用のREADME生成"""
- readme_content = f"""# 📚 プロフェッショナルドキュメント
-
-## 概要
-
-このディレクトリには、**実際の業務で使用されるレベルの正式文書**が含まれています。
-すべての文書は**AIエージェントにより自動生成**されました。
-
-## 含まれる文書
-
-### 📋 要件定義書 (requirements-spec)
-- プロジェクト概要
-- 機能要件・非機能要件
-- 制約事項
-- 成果物定義
-
-### 📐 基本設計書 (design-spec)
-- システム構成
-- アーキテクチャ設計
-- データベース設計
-- API仕様
-
-### 🧪 テスト結果報告書 (test-report)
-- テスト実施結果
-- カバレッジ分析
-- パフォーマンス測定
-- 品質評価
-
-### 📖 操作マニュアル (user-manual)
-- インストール手順
-- 使用方法
-- トラブルシューティング
-- FAQ
-
-## 特徴
-
-- ✅ **完全自動生成**: コードから必要な情報を抽出し、自動で文書化
-- ✅ **プロフェッショナル品質**: 実際の業務で使用可能なレベル
-- ✅ **PDF対応**: 印刷・配布可能な形式
-- ✅ **日本語対応**: ビジネス文書として適切な日本語表現
-
-## 生成時間
-
-これらすべての文書は、AIエージェントにより**約1分**で生成されました。
-
-## 技術的意義
-
-このような包括的な文書セットを自動生成できることは、
-**開発効率の大幅な向上**と**品質の標準化**を実現します。
-
----
-
-Generated by AI Agent System @ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
-"""
-
- readme_path = self.output_dir / "README.md"
- with open(readme_path, 'w', encoding='utf-8') as f:
- f.write(readme_content)
- print(" ✅ README.md 生成完了")
-
-def main():
- """Portfolio文書生成のメイン処理"""
- generator = PortfolioDocumentGenerator()
- output_dir = generator.generate_portfolio_documents()
-
- print(f"\n✅ Portfolio用文書生成完了!")
- print(f"📁 場所: {output_dir}")
- print("\nこれらの文書は、あなたの以下の能力を証明します:")
- print(" - AIを活用した自動化能力")
- print(" - プロフェッショナルな文書作成能力")
- print(" - 効率的なプロジェクト管理能力")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/progress_reporter.py b/src/progress_reporter.py
deleted file mode 100755
index 0d30f48..0000000
--- a/src/progress_reporter.py
+++ /dev/null
@@ -1,100 +0,0 @@
-#!/usr/bin/env python3
-"""
-進捗レポート生成
-"""
-
-import time
-import json
-from datetime import datetime
-from pathlib import Path
-
-class ProgressReporter:
- def __init__(self):
- self.start_time = time.time()
- self.tasks = []
- self.current_task = None
-
- def start_task(self, name, total_steps=1):
- """タスクを開始"""
- task = {
- "name": name,
- "start": time.time(),
- "total_steps": total_steps,
- "current_step": 0,
- "status": "in_progress"
- }
- self.current_task = task
- self.tasks.append(task)
- print(f"📋 {name} 開始...")
-
- def update_progress(self, step, message=""):
- """進捗を更新"""
- if self.current_task:
- self.current_task["current_step"] = step
- percent = (step / self.current_task["total_steps"]) * 100
- bar_length = 30
- filled = int(bar_length * step / self.current_task["total_steps"])
- bar = "=" * filled + "-" * (bar_length - filled)
-
- print(f"\r[{bar}] {percent:.1f}% {message}", end="")
-
- if step >= self.current_task["total_steps"]:
- print() # 改行
- self.complete_task()
-
- def complete_task(self, status="completed"):
- """タスクを完了"""
- if self.current_task:
- self.current_task["end"] = time.time()
- self.current_task["duration"] = self.current_task["end"] - self.current_task["start"]
- self.current_task["status"] = status
- print(f"✅ {self.current_task['name']} 完了 ({self.current_task['duration']:.1f}秒)")
- self.current_task = None
-
- def generate_report(self):
- """最終レポートを生成"""
- total_duration = time.time() - self.start_time
-
- report = {
- "generated_at": datetime.now().isoformat(),
- "total_duration": total_duration,
- "tasks": self.tasks,
- "summary": {
- "total_tasks": len(self.tasks),
- "completed": len([t for t in self.tasks if t["status"] == "completed"]),
- "failed": len([t for t in self.tasks if t["status"] == "failed"])
- }
- }
-
- # レポートを保存
- report_path = Path("progress_report.json")
- with open(report_path, 'w') as f:
- json.dump(report, f, indent=2, default=str)
-
- # サマリーを表示
- print("\n" + "="*50)
- print("📊 実行完了レポート")
- print("="*50)
- print(f"総実行時間: {total_duration:.1f}秒")
- print(f"完了タスク: {report['summary']['completed']}/{report['summary']['total_tasks']}")
-
- return report
-
-# 使用例
-if __name__ == "__main__":
- reporter = ProgressReporter()
-
- # タスク1
- reporter.start_task("ファイル処理", 100)
- for i in range(100):
- time.sleep(0.01)
- reporter.update_progress(i + 1, f"ファイル {i+1}/100")
-
- # タスク2
- reporter.start_task("データベース更新", 50)
- for i in range(50):
- time.sleep(0.01)
- reporter.update_progress(i + 1)
-
- # レポート生成
- reporter.generate_report()
diff --git a/src/publish_portfolio.py b/src/publish_portfolio.py
deleted file mode 100755
index 6cbabad..0000000
--- a/src/publish_portfolio.py
+++ /dev/null
@@ -1,424 +0,0 @@
-#!/usr/bin/env python3
-"""
-ポートフォリオ公開メインスクリプト(Phase 6)
-ワークフロー状態管理と統合、エージェントによるハイブリッドセキュリティチェック対応
-
-フェーズ:
-1. DELIVERY準備(スクリプト)
-2. セキュリティチェック第1弾(スクリプト)
-3. エージェントセキュリティレビュー(ハイブリッド)
-4. Git操作(コミットまで)
-5. ユーザー確認後プッシュ
-6. 状態更新・レビュー待ち移行
-"""
-
-import os
-import sys
-import json
-import argparse
-from pathlib import Path
-from typing import Optional, Tuple, List
-from datetime import datetime
-
-# 同じディレクトリのモジュールをインポート
-sys.path.insert(0, str(Path(__file__).parent))
-
-from portfolio_config import get_config, PortfolioConfig
-from security_checker import SecurityChecker, print_report, Severity
-from delivery_organizer import DeliveryOrganizer
-from github_publisher import GitHubPublisher, PublishResult
-from workflow_state_manager import WorkflowStateManager, get_state_manager
-
-
-class PortfolioPublisher:
- """ポートフォリオ公開オーケストレーター(Phase 6)"""
-
- def __init__(self, config: PortfolioConfig = None, project_path: str = None):
- self.config = config or get_config()
- self.security_checker = SecurityChecker(self.config)
- self.delivery_organizer = DeliveryOrganizer(self.config)
- self.github_publisher = GitHubPublisher(self.config)
-
- # 状態管理
- self.project_path = Path(project_path) if project_path else Path.cwd()
- self.state_manager = get_state_manager(str(self.project_path))
-
- def print_banner(self, title: str, char: str = "="):
- """バナーを表示"""
- width = 60
- print("\n" + char * width)
- print(f" {title}")
- print(char * width)
-
- def print_phase(self, phase_num: int, title: str):
- """フェーズヘッダーを表示"""
- print(f"\n{'─' * 60}")
- print(f" 【Phase 6-{phase_num}】 {title}")
- print(f"{'─' * 60}")
-
- def print_success(self, message: str):
- print(f" ✅ {message}")
-
- def print_warning(self, message: str):
- print(f" ⚠️ {message}")
-
- def print_error(self, message: str):
- print(f" ❌ {message}")
-
- def print_info(self, message: str):
- print(f" ℹ️ {message}")
-
- def confirm_action(self, prompt: str, default: bool = False) -> bool:
- """ユーザー確認を取得"""
- suffix = " [Y/n]: " if default else " [y/N]: "
- try:
- response = input(f"\n {prompt}{suffix}").strip().lower()
- if not response:
- return default
- return response in ("y", "yes", "はい")
- except (EOFError, KeyboardInterrupt):
- print("\n 操作がキャンセルされました。")
- return False
-
- def generate_agent_review_prompt(self, files: List[str], delivery_path: str) -> str:
- """エージェントセキュリティレビュー用のプロンプトを生成"""
- files_list = "\n".join([f" - {f}" for f in files[:50]])
- if len(files) > 50:
- files_list += f"\n ... 他 {len(files) - 50} ファイル"
-
- return f"""
-## エージェントセキュリティレビュー(Phase 6-3)
-
-以下のファイルがGitHubに公開されます。セキュリティ観点でレビューしてください。
-
-### 公開対象ファイル
-{files_list}
-
-### チェック項目
-1. **APIキー・トークン**: 各種サービスのAPIキーが含まれていないか
-2. **認証情報**: パスワード、秘密鍵、証明書が含まれていないか
-3. **内部情報**: 社内URL、IPアドレス、ユーザー名パスが含まれていないか
-4. **個人情報**: メールアドレス、電話番号等が含まれていないか
-5. **デバッグ情報**: console.log、デバッグコード、テストデータが残っていないか
-
-### 判定
-- **SAFE**: 公開して問題なし
-- **UNSAFE**: 公開を中止すべき問題あり(理由を明記)
-- **REVIEW_NEEDED**: 人間の確認が必要(懸念点を明記)
-
-### 出力形式
-```
-判定: [SAFE/UNSAFE/REVIEW_NEEDED]
-理由: [判定理由]
-懸念点: [あれば列挙]
-```
-
-DELIVERYフォルダのパス: {delivery_path}
-"""
-
- def publish(
- self,
- source_dir: str,
- app_name: str,
- dry_run: bool = False,
- skip_confirm: bool = False,
- skip_agent_review: bool = False,
- verbose: bool = False,
- ) -> Tuple[bool, str]:
- """
- ポートフォリオを公開(Phase 6)
-
- Args:
- source_dir: ソースディレクトリ
- app_name: アプリ名
- dry_run: ドライランモード
- skip_confirm: 確認をスキップ
- skip_agent_review: エージェントレビューをスキップ
- verbose: 詳細表示
-
- Returns:
- (success, message): 結果
- """
- # ワークフロー状態を更新
- state = self.state_manager.get_or_create(app_name)
- self.state_manager.start_phase(6, agents=["portfolio_publisher"])
-
- self.print_banner("🚀 Phase 6: ポートフォリオ公開ワークフロー")
- print(f"\n ソース: {source_dir}")
- print(f" アプリ名: {app_name}")
- print(f" リポジトリ: {self.config.github_repo}")
- print(f" 公開URL: {self.config.get_app_url(app_name)}")
-
- if dry_run:
- print(f"\n 🔍 ドライランモード: 実際の公開は行いません")
-
- # ========================================
- # Phase 6-1: DELIVERY準備
- # ========================================
- self.print_phase(1, "DELIVERY準備")
-
- try:
- manifest = self.delivery_organizer.prepare_delivery(
- source_dir=source_dir,
- app_name=app_name,
- )
- except Exception as e:
- self.state_manager.fail_phase(6, str(e))
- self.print_error(f"DELIVERY準備に失敗しました: {e}")
- return False, str(e)
-
- if not manifest.files:
- self.state_manager.fail_phase(6, "No files to publish")
- self.print_error("公開対象のファイルがありません")
- return False, "No files to publish"
-
- delivery_path = Path(source_dir) / "DELIVERY"
- self.print_success(f"{len(manifest.files)} ファイルを収集しました")
-
- # ========================================
- # Phase 6-2: セキュリティチェック(スクリプト)
- # ========================================
- self.print_phase(2, "セキュリティチェック(スクリプト)")
-
- security_report = self.security_checker.scan_directory(str(delivery_path))
- print_report(security_report, verbose)
-
- if security_report.has_critical:
- self.state_manager.fail_phase(6, "Security check failed: CRITICAL issues found")
- self.print_error("CRITICAL(重大)な問題が検出されました")
- self.print_error("公開を中止します。問題を解決してから再実行してください。")
- return False, "Security check failed: CRITICAL issues found"
-
- if security_report.has_high:
- self.print_warning("HIGH(高リスク)な問題が検出されました")
- if not skip_confirm:
- if not self.confirm_action("続行しますか?(推奨: No)"):
- self.state_manager.fail_phase(6, "User cancelled due to HIGH security issues")
- return False, "User cancelled due to HIGH security issues"
-
- script_security_passed = security_report.is_safe
- if script_security_passed:
- self.print_success("スクリプトセキュリティチェック通過")
- else:
- self.print_warning(f"{len(security_report.issues)} 件の問題を検出(MEDIUM/LOW)")
-
- # ========================================
- # Phase 6-3: エージェントセキュリティレビュー
- # ========================================
- self.print_phase(3, "エージェントセキュリティレビュー")
-
- agent_review_passed = True
- if skip_agent_review:
- self.print_info("エージェントレビューをスキップしました")
- else:
- # エージェントレビュー用プロンプトを生成
- agent_prompt = self.generate_agent_review_prompt(manifest.files, str(delivery_path))
-
- print("\n 【エージェントレビュー指示】")
- print(" 以下の内容でエージェントにセキュリティレビューを依頼してください:")
- print(" " + "-" * 50)
- print(agent_prompt)
- print(" " + "-" * 50)
-
- # 自動実行の場合はスキップ、対話モードでは確認
- if not skip_confirm:
- print("\n エージェントによるレビューが完了したら、結果を入力してください。")
- result = input(" レビュー結果 (SAFE/UNSAFE/REVIEW_NEEDED): ").strip().upper()
-
- if result == "UNSAFE":
- self.state_manager.fail_phase(6, "Agent review: UNSAFE")
- self.print_error("エージェントがUNSAFEと判定しました。公開を中止します。")
- return False, "Agent review: UNSAFE"
- elif result == "REVIEW_NEEDED":
- self.print_warning("エージェントがREVIEW_NEEDEDと判定しました。")
- if not self.confirm_action("続行しますか?"):
- self.state_manager.fail_phase(6, "User cancelled after REVIEW_NEEDED")
- return False, "User cancelled after REVIEW_NEEDED"
- agent_review_passed = True
- else:
- agent_review_passed = True
- self.print_success("エージェントセキュリティレビュー通過")
- else:
- self.print_info("対話モードでない場合、エージェントレビューは手動で実施してください")
- agent_review_passed = True
-
- # ========================================
- # Phase 6-4: Git操作(コミットまで)
- # ========================================
- self.print_phase(4, "Git操作")
-
- publish_result = self.github_publisher.publish(
- delivery_path=str(delivery_path),
- app_name=app_name,
- dry_run=dry_run,
- skip_push=True, # Phase 6-5までプッシュしない
- )
-
- if not publish_result.success:
- self.state_manager.fail_phase(6, publish_result.message)
- self.print_error(f"Git操作に失敗しました: {publish_result.message}")
- return False, publish_result.message
-
- self.print_success(f"コミット作成完了: {publish_result.commit_hash}")
- self.print_info(f"追加: {publish_result.files_added}, 変更: {publish_result.files_modified}, 削除: {publish_result.files_deleted}")
-
- # ========================================
- # Phase 6-5: プッシュ
- # ========================================
- self.print_phase(5, "GitHub公開")
-
- if dry_run:
- self.print_info("ドライランモードのためプッシュをスキップします")
- self.state_manager.complete_phase(6, {"dry_run": True})
- return True, "Dry run completed successfully"
-
- if publish_result.commit_hash == "(no changes)":
- self.print_info("変更がないためプッシュは不要です")
- self.state_manager.complete_phase(6, {"no_changes": True})
- return True, "No changes to publish"
-
- if not skip_confirm:
- print("\n ⚠️ この操作は取り消せません。")
- print(f" リポジトリ '{self.config.github_repo}' に公開されます。")
- if not self.confirm_action("プッシュしてよろしいですか?"):
- self.print_info("プッシュをキャンセルしました")
- self.print_info("後でプッシュするには: git push origin main")
- return True, "Commit created but push cancelled"
-
- # プッシュ実行
- if not self.github_publisher.push_to_remote():
- self.state_manager.fail_phase(6, "Push failed")
- self.print_error("プッシュに失敗しました")
- return False, "Push failed"
-
- # ========================================
- # Phase 6-6: 状態更新・レビュー待ち
- # ========================================
- self.print_phase(6, "公開完了・レビュー待ち移行")
-
- # ポートフォリオ公開を記録
- self.state_manager.record_portfolio_publish(
- app_name=app_name,
- app_url=publish_result.app_url,
- commit_hash=publish_result.commit_hash,
- security_check_passed=script_security_passed,
- agent_review_passed=agent_review_passed,
- )
-
- # Phase 6 完了
- self.state_manager.complete_phase(6, {
- "app_name": app_name,
- "app_url": publish_result.app_url,
- "commit_hash": publish_result.commit_hash,
- })
-
- self.print_banner("📋 公開完了 - ユーザーレビュー待ち", "═")
-
- print(f"\n 🎉 GitHub公開成功!")
- print(f"\n リポジトリ: {self.config.repo_url}")
- print(f" アプリURL: {publish_result.app_url}")
- print(f" コミット: {publish_result.commit_hash}")
-
- print("\n 【次のステップ】")
- print(f" 1. 公開されたアプリを確認: {publish_result.app_url}")
- print(f" 2. 問題があれば修正を依頼(Phase 7が実行されます)")
- print(f" 3. 問題なければ「完了」と伝えてください")
-
- print("\n 【修正依頼の例】")
- print(' 「修正依頼: ボタンの色を青から緑に変更してください」')
-
- self.print_banner("Phase 6 完了", "═")
-
- # 状態レポートを表示
- self.state_manager.print_status_report()
-
- return True, "Published successfully - awaiting user review"
-
-
-def main():
- """メインエントリーポイント"""
- parser = argparse.ArgumentParser(
- description="ポートフォリオ公開ワークフロー(Phase 6)",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-使用例:
- # 基本的な使用
- python publish_portfolio.py /path/to/app my-app
-
- # ドライラン(実際の公開なし)
- python publish_portfolio.py /path/to/app my-app --dry-run
-
- # 確認なしで実行(CI/CD用)
- python publish_portfolio.py /path/to/app my-app --yes
-
- # エージェントレビューをスキップ
- python publish_portfolio.py /path/to/app my-app --skip-agent-review
-
- # 詳細表示
- python publish_portfolio.py /path/to/app my-app -v
- """,
- )
-
- parser.add_argument(
- "source",
- help="ソースディレクトリ(アプリのルート)",
- )
- parser.add_argument(
- "app_name",
- nargs="?",
- help="アプリ名(省略時はフォルダ名を使用)",
- )
- parser.add_argument(
- "--dry-run",
- action="store_true",
- help="ドライラン(実際の公開は行わない)",
- )
- parser.add_argument(
- "-y", "--yes",
- action="store_true",
- help="確認プロンプトをスキップ",
- )
- parser.add_argument(
- "--skip-agent-review",
- action="store_true",
- help="エージェントセキュリティレビューをスキップ",
- )
- parser.add_argument(
- "-v", "--verbose",
- action="store_true",
- help="詳細表示",
- )
-
- args = parser.parse_args()
-
- # ソースディレクトリの検証
- source_path = Path(args.source).resolve()
- if not source_path.exists():
- print(f"❌ ソースディレクトリが存在しません: {source_path}")
- sys.exit(1)
-
- # アプリ名の決定
- app_name = args.app_name or source_path.name
- app_name = app_name.lower().replace(" ", "-").replace("_", "-")
-
- # 公開実行
- publisher = PortfolioPublisher(project_path=str(source_path))
- success, message = publisher.publish(
- source_dir=str(source_path),
- app_name=app_name,
- dry_run=args.dry_run,
- skip_confirm=args.yes,
- skip_agent_review=args.skip_agent_review,
- verbose=args.verbose,
- )
-
- if success:
- print(f"\n✅ {message}")
- sys.exit(0)
- else:
- print(f"\n❌ {message}")
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/requirements_gatherer.py b/src/requirements_gatherer.py
deleted file mode 100755
index 56b65fb..0000000
--- a/src/requirements_gatherer.py
+++ /dev/null
@@ -1,308 +0,0 @@
-#!/usr/bin/env python3
-"""
-対話的要件定義システム
-プロジェクト開始前に、ユーザーと対話しながら要件を明確にする
-"""
-
-import json
-import os
-from typing import Dict, List, Any
-from datetime import datetime
-
-class RequirementsGatherer:
- """要件収集を対話的に行うクラス"""
-
- def __init__(self):
- self.requirements = {
- "project_info": {},
- "functional_requirements": {},
- "technical_requirements": {},
- "constraints": {},
- "deliverables": {},
- "gathered_at": None
- }
-
- def gather_game_requirements(self) -> Dict:
- """ゲーム開発用の要件収集"""
-
- questions = {
- "project_info": [
- {
- "key": "project_name",
- "question": "プロジェクト名を教えてください:",
- "type": "text",
- "required": True
- },
- {
- "key": "game_genre",
- "question": "どのようなジャンルのゲームをお考えですか?\n1. シューティング\n2. パズル\n3. アクション\n4. RPG\n5. シミュレーション\n6. その他",
- "type": "choice",
- "options": ["シューティング", "パズル", "アクション", "RPG", "シミュレーション", "その他"],
- "required": True
- },
- {
- "key": "target_audience",
- "question": "対象とするプレイヤー層を教えてください(例: カジュアル、ハードコア、全年齢、大人向け等):",
- "type": "text",
- "required": True
- },
- {
- "key": "play_time",
- "question": "想定するプレイ時間は?\n1. 5分以内(カジュアル)\n2. 15-30分(短時間)\n3. 1時間以上(じっくり)",
- "type": "choice",
- "options": ["5分以内", "15-30分", "1時間以上"],
- "required": False
- }
- ],
- "functional_requirements": [
- {
- "key": "core_mechanics",
- "question": "ゲームの核となるメカニクスを教えてください(例: 射撃、ジャンプ、パズル解き等):",
- "type": "text_list",
- "required": True
- },
- {
- "key": "must_have_features",
- "question": "必須機能を列挙してください(カンマ区切り):",
- "type": "text_list",
- "required": True
- },
- {
- "key": "nice_to_have_features",
- "question": "あったら良い機能を列挙してください(カンマ区切り):",
- "type": "text_list",
- "required": False
- },
- {
- "key": "reference_games",
- "question": "参考にしたいゲームがあれば教えてください:",
- "type": "text",
- "required": False
- }
- ],
- "technical_requirements": [
- {
- "key": "platform",
- "question": "対象プラットフォームは?\n1. ブラウザ(Web)\n2. デスクトップ\n3. モバイル\n4. 全プラットフォーム",
- "type": "choice",
- "options": ["ブラウザ", "デスクトップ", "モバイル", "全プラットフォーム"],
- "required": True
- },
- {
- "key": "tech_stack",
- "question": "使用したい技術スタックがあれば指定してください(例: Three.js, Phaser, Unity等):",
- "type": "text",
- "required": False,
- "default": "Three.js"
- },
- {
- "key": "performance",
- "question": "パフォーマンス要件は?\n1. 高性能(60FPS必須)\n2. 標準(30FPS以上)\n3. 軽量(低スペックでも動作)",
- "type": "choice",
- "options": ["高性能", "標準", "軽量"],
- "required": False,
- "default": "標準"
- },
- {
- "key": "graphics_style",
- "question": "グラフィックスタイルは?\n1. 3D\n2. 2D\n3. 2.5D(疑似3D)",
- "type": "choice",
- "options": ["3D", "2D", "2.5D"],
- "required": True
- }
- ],
- "constraints": [
- {
- "key": "timeline",
- "question": "開発期限はありますか?(例: 2週間、1ヶ月、期限なし):",
- "type": "text",
- "required": False,
- "default": "期限なし"
- },
- {
- "key": "budget_constraints",
- "question": "予算や技術的制約があれば教えてください:",
- "type": "text",
- "required": False
- }
- ]
- }
-
- return questions
-
- def gather_web_app_requirements(self) -> Dict:
- """Webアプリケーション用の要件収集"""
-
- questions = {
- "project_info": [
- {
- "key": "project_name",
- "question": "プロジェクト名を教えてください:",
- "type": "text",
- "required": True
- },
- {
- "key": "app_type",
- "question": "どのような種類のWebアプリケーションですか?\n1. SPA(シングルページ)\n2. MPA(マルチページ)\n3. PWA(プログレッシブ)\n4. 静的サイト",
- "type": "choice",
- "options": ["SPA", "MPA", "PWA", "静的サイト"],
- "required": True
- },
- {
- "key": "target_users",
- "question": "想定ユーザー数と利用シーンを教えてください:",
- "type": "text",
- "required": True
- }
- ],
- "functional_requirements": [
- {
- "key": "core_features",
- "question": "コア機能を列挙してください(カンマ区切り):",
- "type": "text_list",
- "required": True
- },
- {
- "key": "authentication",
- "question": "認証機能は必要ですか?\n1. 必要(ログイン/ログアウト)\n2. 不要\n3. OAuth連携",
- "type": "choice",
- "options": ["必要", "不要", "OAuth連携"],
- "required": True
- },
- {
- "key": "database",
- "question": "データベースは必要ですか?\n1. 必要(リレーショナル)\n2. 必要(NoSQL)\n3. 不要",
- "type": "choice",
- "options": ["リレーショナル", "NoSQL", "不要"],
- "required": True
- }
- ],
- "technical_requirements": [
- {
- "key": "frontend_framework",
- "question": "フロントエンドフレームワークの希望は?\n1. React\n2. Vue\n3. Angular\n4. Vanilla JS\n5. その他",
- "type": "choice",
- "options": ["React", "Vue", "Angular", "Vanilla JS", "その他"],
- "required": False,
- "default": "React"
- },
- {
- "key": "backend_framework",
- "question": "バックエンドフレームワークの希望は?\n1. Node.js/Express\n2. Python/Django\n3. Python/FastAPI\n4. 不要(フロントのみ)",
- "type": "choice",
- "options": ["Node.js/Express", "Python/Django", "Python/FastAPI", "不要"],
- "required": False,
- "default": "Node.js/Express"
- },
- {
- "key": "responsive",
- "question": "レスポンシブデザインは必要ですか?",
- "type": "boolean",
- "required": True,
- "default": True
- }
- ],
- "constraints": [
- {
- "key": "browser_support",
- "question": "対応ブラウザの要件を教えてください(例: Chrome最新版、IE11対応等):",
- "type": "text",
- "required": False,
- "default": "モダンブラウザ(Chrome, Firefox, Safari最新版)"
- }
- ]
- }
-
- return questions
-
- def generate_requirements_spec(self, answers: Dict) -> str:
- """収集した要件から仕様書を生成"""
-
- spec = f"""# 要件定義書
-
-## プロジェクト情報
-- **プロジェクト名**: {answers.get('project_info', {}).get('project_name', '未定')}
-- **作成日**: {datetime.now().strftime('%Y年%m月%d日')}
-- **タイプ**: {answers.get('project_info', {}).get('game_genre', answers.get('project_info', {}).get('app_type', '未定'))}
-
-## 機能要件
-
-### 必須機能
-"""
-
- must_haves = answers.get('functional_requirements', {}).get('must_have_features', [])
- if isinstance(must_haves, str):
- must_haves = [f.strip() for f in must_haves.split(',')]
-
- for feature in must_haves:
- spec += f"- {feature}\n"
-
- spec += """
-### 追加機能(Nice to Have)
-"""
- nice_to_haves = answers.get('functional_requirements', {}).get('nice_to_have_features', [])
- if isinstance(nice_to_haves, str):
- nice_to_haves = [f.strip() for f in nice_to_haves.split(',')]
-
- for feature in nice_to_haves:
- spec += f"- {feature}\n"
-
- spec += f"""
-## 技術要件
-- **プラットフォーム**: {answers.get('technical_requirements', {}).get('platform', '未定')}
-- **技術スタック**: {answers.get('technical_requirements', {}).get('tech_stack', '未定')}
-- **パフォーマンス**: {answers.get('technical_requirements', {}).get('performance', '標準')}
-
-## 制約事項
-- **開発期限**: {answers.get('constraints', {}).get('timeline', '期限なし')}
-- **その他制約**: {answers.get('constraints', {}).get('budget_constraints', 'なし')}
-
-## 承認
-この仕様書に基づいて開発を進めてよろしいですか?
-"""
-
- return spec
-
- def save_requirements(self, answers: Dict, filepath: str = "requirements.json"):
- """要件をJSONファイルに保存"""
-
- self.requirements.update(answers)
- self.requirements["gathered_at"] = datetime.now().isoformat()
-
- with open(filepath, 'w', encoding='utf-8') as f:
- json.dump(self.requirements, f, ensure_ascii=False, indent=2)
-
- return filepath
-
- def load_requirements(self, filepath: str = "requirements.json") -> Dict:
- """保存された要件を読み込み"""
-
- if os.path.exists(filepath):
- with open(filepath, 'r', encoding='utf-8') as f:
- return json.load(f)
- return None
-
- def create_interactive_prompt(self, project_type: str = "game") -> List[Dict]:
- """インタラクティブな質問プロンプトを生成"""
-
- if project_type == "game":
- questions = self.gather_game_requirements()
- elif project_type == "web_app":
- questions = self.gather_web_app_requirements()
- else:
- raise ValueError(f"Unknown project type: {project_type}")
-
- prompts = []
- for category, items in questions.items():
- for item in items:
- prompts.append({
- "category": category,
- "key": item["key"],
- "prompt": item["question"],
- "type": item["type"],
- "required": item.get("required", False),
- "default": item.get("default", None),
- "options": item.get("options", None)
- })
-
- return prompts
\ No newline at end of file
diff --git a/src/security_checker.py b/src/security_checker.py
deleted file mode 100755
index f08e865..0000000
--- a/src/security_checker.py
+++ /dev/null
@@ -1,337 +0,0 @@
-#!/usr/bin/env python3
-"""
-セキュリティチェッカー
-公開前にファイル内の秘密情報を検出
-"""
-
-import os
-import re
-import json
-from pathlib import Path
-from typing import Dict, List, Tuple, Optional
-from dataclasses import dataclass, field
-from enum import Enum
-
-from portfolio_config import get_config, PortfolioConfig
-
-
-class Severity(Enum):
- """問題の深刻度"""
- CRITICAL = "CRITICAL" # 絶対に公開禁止(APIキー、秘密鍵等)
- HIGH = "HIGH" # 高リスク(パスワード、内部URL等)
- MEDIUM = "MEDIUM" # 中リスク(内部パス、デバッグ情報等)
- LOW = "LOW" # 低リスク(確認推奨)
-
-
-@dataclass
-class SecurityIssue:
- """検出されたセキュリティ問題"""
- file_path: str
- line_number: int
- severity: Severity
- issue_type: str
- description: str
- matched_content: str # マスク済みの内容
- recommendation: str
-
-
-@dataclass
-class SecurityReport:
- """セキュリティチェックレポート"""
- scan_path: str
- total_files: int
- scanned_files: int
- skipped_files: int
- issues: List[SecurityIssue] = field(default_factory=list)
-
- @property
- def has_critical(self) -> bool:
- return any(i.severity == Severity.CRITICAL for i in self.issues)
-
- @property
- def has_high(self) -> bool:
- return any(i.severity == Severity.HIGH for i in self.issues)
-
- @property
- def is_safe(self) -> bool:
- return not self.has_critical and not self.has_high
-
- def get_summary(self) -> Dict:
- """サマリーを取得"""
- by_severity = {s.value: 0 for s in Severity}
- for issue in self.issues:
- by_severity[issue.severity.value] += 1
- return {
- "total_files": self.total_files,
- "scanned_files": self.scanned_files,
- "skipped_files": self.skipped_files,
- "issues_count": len(self.issues),
- "by_severity": by_severity,
- "is_safe": self.is_safe,
- }
-
-
-class SecurityChecker:
- """セキュリティチェッカー"""
-
- def __init__(self, config: PortfolioConfig = None):
- self.config = config or get_config()
-
- # バイナリ拡張子(スキャンしない)
- self.binary_extensions = {
- ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif",
- ".ico", ".mp3", ".wav", ".ogg", ".mp4", ".webm",
- ".woff", ".woff2", ".ttf", ".otf", ".eot",
- ".pdf", ".zip", ".tar", ".gz",
- }
-
- def scan_directory(self, directory: str) -> SecurityReport:
- """ディレクトリをスキャン"""
- dir_path = Path(directory)
- report = SecurityReport(
- scan_path=str(dir_path),
- total_files=0,
- scanned_files=0,
- skipped_files=0,
- )
-
- if not dir_path.exists():
- print(f" ディレクトリが存在しません: {directory}")
- return report
-
- # 全ファイルを列挙
- all_files = list(dir_path.rglob("*"))
- report.total_files = len([f for f in all_files if f.is_file()])
-
- for file_path in all_files:
- if not file_path.is_file():
- continue
-
- # バイナリファイルはスキップ
- if file_path.suffix.lower() in self.binary_extensions:
- report.skipped_files += 1
- continue
-
- # 除外パターンに一致するファイルはスキップ
- if self.config.should_exclude(str(file_path)):
- report.skipped_files += 1
- continue
-
- # ファイルをスキャン
- issues = self._scan_file(file_path)
- report.issues.extend(issues)
- report.scanned_files += 1
-
- return report
-
- def scan_file(self, file_path: str) -> List[SecurityIssue]:
- """単一ファイルをスキャン"""
- return self._scan_file(Path(file_path))
-
- def _scan_file(self, file_path: Path) -> List[SecurityIssue]:
- """ファイル内容をスキャン"""
- issues = []
-
- try:
- content = file_path.read_text(encoding="utf-8", errors="ignore")
- lines = content.split("\n")
- except Exception as e:
- return issues
-
- rel_path = str(file_path)
-
- # 各行をチェック
- for line_num, line in enumerate(lines, 1):
- # APIキーパターンチェック
- for pattern in self.config.API_KEY_PATTERNS:
- matches = re.finditer(pattern, line, re.IGNORECASE)
- for match in matches:
- issues.append(SecurityIssue(
- file_path=rel_path,
- line_number=line_num,
- severity=Severity.CRITICAL,
- issue_type="API_KEY",
- description="APIキーまたはトークンが検出されました",
- matched_content=self._mask_sensitive(match.group()),
- recommendation="このファイルを公開対象から除外するか、該当箇所を削除してください",
- ))
-
- # 危険なコンテンツパターンチェック
- for pattern in self.config.DANGEROUS_CONTENT_PATTERNS:
- if re.search(pattern, line, re.IGNORECASE):
- issues.append(SecurityIssue(
- file_path=rel_path,
- line_number=line_num,
- severity=Severity.CRITICAL,
- issue_type="SENSITIVE_DATA",
- description="秘密鍵または認証情報が検出されました",
- matched_content=self._mask_line(line),
- recommendation="このファイルは絶対に公開しないでください",
- ))
-
- # 内部パスパターンチェック
- for pattern in self.config.INTERNAL_PATH_PATTERNS:
- if re.search(pattern, line):
- issues.append(SecurityIssue(
- file_path=rel_path,
- line_number=line_num,
- severity=Severity.MEDIUM,
- issue_type="INTERNAL_PATH",
- description="内部パスまたはIPアドレスが検出されました",
- matched_content=self._mask_line(line),
- recommendation="ハードコードされたパスを相対パスまたは環境変数に置き換えてください",
- ))
-
- # ファイル名自体のチェック
- filename_issues = self._check_filename(file_path)
- issues.extend(filename_issues)
-
- return issues
-
- def _check_filename(self, file_path: Path) -> List[SecurityIssue]:
- """ファイル名の危険性をチェック"""
- issues = []
- name = file_path.name.lower()
-
- dangerous_names = {
- ".env": (Severity.CRITICAL, "環境変数ファイル"),
- "credentials": (Severity.CRITICAL, "認証情報ファイル"),
- "secret": (Severity.CRITICAL, "秘密情報ファイル"),
- "password": (Severity.HIGH, "パスワードファイル"),
- "private": (Severity.HIGH, "プライベートキーファイル"),
- "serviceaccount": (Severity.CRITICAL, "サービスアカウントファイル"),
- }
-
- for keyword, (severity, desc) in dangerous_names.items():
- if keyword in name:
- issues.append(SecurityIssue(
- file_path=str(file_path),
- line_number=0,
- severity=severity,
- issue_type="DANGEROUS_FILENAME",
- description=f"危険なファイル名: {desc}",
- matched_content=name,
- recommendation="このファイルを公開対象から除外してください",
- ))
-
- return issues
-
- def _mask_sensitive(self, text: str) -> str:
- """機密情報をマスク"""
- if len(text) <= 10:
- return "*" * len(text)
- return text[:4] + "*" * (len(text) - 8) + text[-4:]
-
- def _mask_line(self, line: str) -> str:
- """行をマスク(最大50文字、中間をマスク)"""
- line = line.strip()
- if len(line) <= 20:
- return line[:5] + "..." + line[-5:] if len(line) > 10 else line
- return line[:10] + "..." + line[-10:]
-
-
-def print_report(report: SecurityReport, verbose: bool = False):
- """レポートを表示"""
- print("\n" + "=" * 60)
- print(" セキュリティスキャンレポート")
- print("=" * 60)
-
- summary = report.get_summary()
-
- print(f"\n スキャン対象: {report.scan_path}")
- print(f" 総ファイル数: {summary['total_files']}")
- print(f" スキャン済み: {summary['scanned_files']}")
- print(f" スキップ: {summary['skipped_files']}")
-
- print(f"\n 検出された問題: {summary['issues_count']} 件")
- print(f" CRITICAL: {summary['by_severity']['CRITICAL']}")
- print(f" HIGH: {summary['by_severity']['HIGH']}")
- print(f" MEDIUM: {summary['by_severity']['MEDIUM']}")
- print(f" LOW: {summary['by_severity']['LOW']}")
-
- if report.is_safe:
- print("\n " + "=" * 56)
- print(" ✅ 重大な問題は検出されませんでした。公開可能です。")
- print(" " + "=" * 56)
- else:
- print("\n " + "=" * 56)
- print(" ❌ 重大な問題が検出されました。公開を中止してください。")
- print(" " + "=" * 56)
-
- # 詳細表示
- if report.issues and (verbose or not report.is_safe):
- print("\n 【検出された問題の詳細】")
- print("-" * 60)
-
- for i, issue in enumerate(report.issues, 1):
- severity_icon = {
- Severity.CRITICAL: "🚨",
- Severity.HIGH: "⚠️ ",
- Severity.MEDIUM: "📋",
- Severity.LOW: "ℹ️ ",
- }[issue.severity]
-
- print(f"\n [{i}] {severity_icon} {issue.severity.value}: {issue.issue_type}")
- print(f" ファイル: {issue.file_path}")
- if issue.line_number > 0:
- print(f" 行: {issue.line_number}")
- print(f" 説明: {issue.description}")
- print(f" 内容: {issue.matched_content}")
- print(f" 推奨: {issue.recommendation}")
-
- print("\n" + "=" * 60 + "\n")
-
-
-def check_directory(directory: str, verbose: bool = False) -> Tuple[bool, SecurityReport]:
- """
- ディレクトリをチェック
-
- Returns:
- (is_safe, report): 安全かどうかとレポート
- """
- checker = SecurityChecker()
- report = checker.scan_directory(directory)
- print_report(report, verbose)
- return report.is_safe, report
-
-
-def check_files(files: List[str], verbose: bool = False) -> Tuple[bool, SecurityReport]:
- """
- ファイルリストをチェック
-
- Returns:
- (is_safe, report): 安全かどうかとレポート
- """
- checker = SecurityChecker()
-
- report = SecurityReport(
- scan_path="(file list)",
- total_files=len(files),
- scanned_files=0,
- skipped_files=0,
- )
-
- for file_path in files:
- path = Path(file_path)
- if not path.exists():
- report.skipped_files += 1
- continue
-
- issues = checker.scan_file(file_path)
- report.issues.extend(issues)
- report.scanned_files += 1
-
- print_report(report, verbose)
- return report.is_safe, report
-
-
-if __name__ == "__main__":
- import argparse
-
- parser = argparse.ArgumentParser(description="セキュリティチェッカー")
- parser.add_argument("path", nargs="?", default=".", help="スキャン対象のパス")
- parser.add_argument("-v", "--verbose", action="store_true", help="詳細表示")
- args = parser.parse_args()
-
- is_safe, report = check_directory(args.path, args.verbose)
- exit(0 if is_safe else 1)
diff --git a/src/simplified_github_publisher.py b/src/simplified_github_publisher.py
deleted file mode 100755
index ece9294..0000000
--- a/src/simplified_github_publisher.py
+++ /dev/null
@@ -1,578 +0,0 @@
-#!/usr/bin/env python3
-"""
-🚀 シンプル化されたGitHub公開スクリプト v8.0
-project/public/ から直接GitHubにプッシュ(一時clone方式)
-"""
-
-import os
-import sys
-import subprocess
-import shutil
-import re
-import json
-import tempfile
-from pathlib import Path
-from typing import Optional
-
-class SimplifiedGitHubPublisher:
- """シンプル化されたGitHub公開クラス"""
-
- def __init__(self, project_path: str = None, auto_mode: bool = False):
- """
- Args:
- project_path: プロジェクトのパス(AI-Apps内のフォルダ)
- auto_mode: 対話なしで自動実行するか(デフォルト: False)
- """
- self.project_path = Path(project_path or os.getcwd())
- self.auto_mode = auto_mode
- self._load_env()
-
- self.app_name = self._get_app_name()
- self.public_path = self.project_path / "project" / "public"
- self.temp_dir = None
- self.github_username = self._get_github_username()
-
- def _load_env(self):
- """環境変数を読み込み"""
- env_file = self.project_path / ".env"
- if not env_file.exists():
- return
-
- try:
- from dotenv import load_dotenv
- load_dotenv(env_file)
- except ImportError:
- with open(env_file, 'r') as f:
- for line in f:
- line = line.strip()
- if line and not line.startswith('#') and '=' in line:
- key, value = line.split('=', 1)
- os.environ[key.strip()] = value.strip()
-
- def _get_app_name(self) -> Optional[str]:
- """PROJECT_INFO.yamlからアプリ名を取得"""
- project_info_path = self.project_path / "PROJECT_INFO.yaml"
- if not project_info_path.exists():
- return None
-
- try:
- with open(project_info_path, 'r', encoding='utf-8') as f:
- for line in f:
- if line.strip().startswith('name:'):
- app_name = line.split(':', 1)[1].strip()
- return app_name.strip('"').strip("'")
- except Exception as e:
- print(f"⚠️ PROJECT_INFO.yaml読み込みエラー: {e}")
- return None
-
- def _get_github_username(self) -> str:
- """GitHub usernameを取得(M4 Mac対応版)"""
- username = os.environ.get('GITHUB_USERNAME')
- if username:
- return username
-
- # M4 Macに対応したghパスを試行
- gh_paths = [
- os.path.expanduser('~/bin/gh'), # M4 Mac用(ARM64版)
- '/usr/local/bin/gh', # Intel Mac用
- 'gh' # PATH上のgh
- ]
-
- for gh_path in gh_paths:
- try:
- if os.path.exists(gh_path) or shutil.which(gh_path):
- result = subprocess.run(
- [gh_path, 'api', 'user', '--jq', '.login'],
- capture_output=True,
- text=True
- )
- if result.returncode == 0:
- return result.stdout.strip()
- except:
- continue
-
- return "username"
-
- def _run_command(self, cmd: str, cwd: Path = None) -> bool:
- """コマンドを実行"""
- result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True)
- if result.returncode != 0:
- print(f"⚠️ コマンド失敗: {cmd}")
- if result.stderr:
- print(f" エラー: {result.stderr}")
- return False
- return True
-
- def get_slug(self) -> str:
- """アプリ名からslugを生成"""
- if self.app_name:
- name = self.app_name
- else:
- name = self.project_path.name
- name = re.sub(r'^\d{8}-', '', name)
- name = re.sub(r'-agent$', '', name)
-
- slug = name.lower()
- slug = re.sub(r'[^a-z0-9]+', '-', slug)
- slug = re.sub(r'-+', '-', slug)
- slug = slug.strip('-')
-
- return slug
-
- def validate_public(self) -> bool:
- """project/public/ の検証"""
- if not self.public_path.exists():
- print(f"❌ project/public/ フォルダが見つかりません: {self.public_path}")
- return False
-
- required_files = ['index.html', 'about.html', 'README.md']
- missing_files = []
-
- for file in required_files:
- if not (self.public_path / file).exists():
- missing_files.append(file)
-
- if missing_files:
- print(f"⚠️ 必須ファイル不足: {', '.join(missing_files)}")
- print(f" 検証パス: {self.public_path}")
- return False
-
- print(f"✅ project/public/ フォルダ検証OK")
- return True
-
- def clean_public(self):
- """開発ツール・認証情報を自動除外(厳密化版)"""
- print("\n🧹 開発ツール・機密ファイルをクリーンアップ中(厳密モード)...")
-
- # ========================================
- # ドットファイル/フォルダの除外(最優先)
- # ========================================
- # ポートフォリオ公開では基本的にすべてのドットファイルを除外
- # 理由: コード閲覧がメインのため、開発用設定ファイルは不要
- # 迷ったら除外する方が安全
- print("\n 📁 ドットファイル/フォルダを除外中...")
-
- # 再帰的にドットファイル/フォルダを検索して削除
- dotfiles_removed = []
- for item in list(self.public_path.rglob('.*')):
- if item.exists():
- try:
- rel_path = item.relative_to(self.public_path)
- if item.is_dir():
- shutil.rmtree(item)
- dotfiles_removed.append(f"{rel_path}/")
- else:
- item.unlink()
- dotfiles_removed.append(str(rel_path))
- except Exception as e:
- print(f" ⚠️ 削除失敗: {item} ({e})")
-
- if dotfiles_removed:
- for removed in dotfiles_removed:
- print(f" ✅ 削除(ドットファイル): {removed}")
- else:
- print(" ✅ ドットファイルなし")
-
- # ========================================
- # 除外するディレクトリ(拡張版)
- # ========================================
- exclude_dirs = [
- 'tests', 'test', '__tests__', 'spec', 'specs', # テストフォルダ
- '__pycache__', 'node_modules', 'venv', 'env', # 依存関係
- 'credentials', 'secrets', 'private', # 認証情報
- 'docs', 'design', 'planning', 'documentation', # 内部ドキュメント
- 'backup', 'old', 'temp', 'tmp', 'cache', # バックアップ
- 'coverage', 'htmlcov', # カバレッジ
- 'logs', 'log' # ログ
- ]
-
- # ========================================
- # 除外するファイルパターン(拡張版)
- # ========================================
- exclude_patterns = [
- # テストファイル
- '*.test.js', '*.spec.ts', '*.test.ts', '*.spec.js', 'test_*.py',
- # 開発ツール
- '*agent*.py', '*_agent.py', 'documenter_agent.py',
- 'generate_*.js', 'generate_*.py', 'audio_generator*.py',
- # 認証ファイル
- '*.key.json', '*-key.json', '*.pem', '*.cert', '*.key', '*.pfx',
- 'env.*', '*.env',
- # 開発ドキュメント
- 'WBS*.json', 'DESIGN*.md', 'PROJECT_INFO.yaml', 'SPEC*.md',
- # 設定ファイル
- 'pytest.ini', 'jest.config.js', 'karma.conf.js',
- # OS生成ファイル
- 'Thumbs.db', 'desktop.ini',
- # エディタファイル
- '*~',
- # ログファイル
- '*.log', '*.out',
- # バックアップ
- '*.backup', '*.bak', '*.old',
- # ロックファイル
- 'package-lock.json', 'yarn.lock', 'Pipfile.lock',
- # 実行スクリプト
- 'launch_app.command', '*.command', '*.sh', '*.bat',
- # ソースマップ(オプション)
- '*.map'
- ]
-
- print("\n 📁 その他の不要ファイル/フォルダを除外中...")
-
- for dir_name in exclude_dirs:
- for dir_path in self.public_path.rglob(dir_name):
- if dir_path.is_dir():
- shutil.rmtree(dir_path)
- print(f" ✅ 削除: {dir_path.relative_to(self.public_path)}/")
-
- for pattern in exclude_patterns:
- for file in self.public_path.rglob(pattern):
- if file.is_file():
- file.unlink()
- print(f" ✅ 削除: {file.relative_to(self.public_path)}")
-
- def clone_portfolio_repo(self, slug: str) -> Path:
- """ai-agent-portfolioリポジトリを一時ディレクトリにclone(M4 Mac対応)"""
- print("\n📥 ai-agent-portfolioリポジトリをclone中...")
-
- self.temp_dir = Path(tempfile.mkdtemp(prefix="portfolio_"))
- repo_url = f"https://github.com/{self.github_username}/ai-agent-portfolio.git"
-
- # M4 Mac対応: /usr/bin/gitを優先使用
- git_cmd = '/usr/bin/git' if os.path.exists('/usr/bin/git') else 'git'
-
- clone_cmd = f"{git_cmd} clone --depth 1 {repo_url} {self.temp_dir}"
- result = subprocess.run(clone_cmd, shell=True, capture_output=True, text=True)
-
- if result.returncode != 0:
- print("📝 ai-agent-portfolioリポジトリが存在しません - 新規作成します")
- self.temp_dir.mkdir(parents=True, exist_ok=True)
- self._run_command(f"{git_cmd} init", cwd=self.temp_dir)
- self._run_command(f"{git_cmd} checkout -b main", cwd=self.temp_dir)
- self._run_command(f"{git_cmd} remote add origin {repo_url}", cwd=self.temp_dir)
-
- # 初回コミット用README作成
- readme_path = self.temp_dir / "README.md"
- with open(readme_path, 'w') as f:
- f.write(f"# AI Agent Portfolio\n\nAI-generated portfolio apps\n")
-
- self._run_command(f"{git_cmd} add .", cwd=self.temp_dir)
- self._run_command(f'{git_cmd} commit -m "Initial commit"', cwd=self.temp_dir)
- else:
- print(f"✅ Clone完了: {self.temp_dir}")
-
- return self.temp_dir
-
- def copy_to_temp_portfolio(self, slug: str):
- """project/public/ を一時リポジトリの{slug}/にコピー"""
- print(f"\n📦 {slug} をポートフォリオにコピー中...")
-
- target_path = self.temp_dir / slug
-
- # 既存フォルダがあれば削除
- if target_path.exists():
- print(f"🔄 既存の {slug} を更新します")
- shutil.rmtree(target_path)
-
- # コピー
- shutil.copytree(self.public_path, target_path)
- print(f"✅ コピー完了: {target_path}")
-
- def _setup_git_credential_helper(self, repo_path: Path):
- """Git認証ヘルパーを設定(M4 Mac対応)"""
- # 既存の認証ヘルパースクリプトを確認
- helper_paths = [
- Path.home() / 'bin' / 'gh-credential-helper.sh',
- Path(__file__).parent / 'gh-credential-helper.sh'
- ]
-
- helper_script = None
- for path in helper_paths:
- if path.exists():
- helper_script = str(path)
- break
-
- if not helper_script:
- # 認証ヘルパースクリプトを動的に作成
- temp_helper = repo_path / '.git' / 'credential-helper.sh'
- temp_helper.parent.mkdir(parents=True, exist_ok=True)
-
- with open(temp_helper, 'w') as f:
- f.write('#!/bin/bash\n')
- f.write('# GitHub CLI credential helper for M4 Mac\n')
- f.write('if [ -x "$HOME/bin/gh" ]; then\n')
- f.write(' exec "$HOME/bin/gh" auth git-credential "$@"\n')
- f.write('elif command -v gh &> /dev/null; then\n')
- f.write(' exec gh auth git-credential "$@"\n')
- f.write('else\n')
- f.write(' echo "Error: GitHub CLI not found" >&2\n')
- f.write(' exit 1\n')
- f.write('fi\n')
-
- os.chmod(temp_helper, 0o755)
- helper_script = str(temp_helper)
-
- # Git設定にcredential helperを設定
- subprocess.run(
- ['git', 'config', 'credential.helper', f'!{helper_script}'],
- cwd=repo_path,
- capture_output=True
- )
-
- def git_commit_and_push(self, slug: str) -> bool:
- """Git commit & push(M4 Mac対応版)"""
- print("\n📤 GitHubにプッシュ中...")
-
- # 認証ヘルパーを設定
- self._setup_git_credential_helper(self.temp_dir)
-
- # gitコマンドは/usr/bin/gitを使用(M4 Mac対応)
- git_cmd = '/usr/bin/git' if os.path.exists('/usr/bin/git') else 'git'
-
- commands = [
- f"{git_cmd} add {slug}/",
- f'{git_cmd} commit -m "feat: update {slug}"',
- f"{git_cmd} push origin main"
- ]
-
- for cmd in commands:
- if not self._run_command(cmd, cwd=self.temp_dir):
- if "git push" in cmd:
- print("📝 リポジトリ作成を試みます...")
- # ghコマンドもM4 Mac対応
- gh_cmd = os.path.expanduser('~/bin/gh') if os.path.exists(os.path.expanduser('~/bin/gh')) else 'gh'
- create_cmd = f'{gh_cmd} repo create ai-agent-portfolio --public -d "AI Agent Portfolio" --source . --push'
- if self._run_command(create_cmd, cwd=self.temp_dir):
- print("✅ リポジトリ作成・プッシュ成功")
- return True
- return False
-
- print("✅ mainブランチへのプッシュ完了")
- return True
-
- def sync_to_gh_pages(self, slug: str) -> bool:
- """mainブランチの内容をgh-pagesブランチに同期(GitHub Pages用)"""
- print("\n🔄 gh-pagesブランチに同期中...")
-
- git_cmd = '/usr/bin/git' if os.path.exists('/usr/bin/git') else 'git'
-
- # gh-pagesブランチが存在するか確認
- result = subprocess.run(
- f"{git_cmd} ls-remote --heads origin gh-pages",
- shell=True, cwd=self.temp_dir, capture_output=True, text=True
- )
-
- gh_pages_exists = bool(result.stdout.strip())
-
- if gh_pages_exists:
- # gh-pagesブランチをfetch
- self._run_command(f"{git_cmd} fetch origin gh-pages:gh-pages", cwd=self.temp_dir)
- # gh-pagesにチェックアウト
- self._run_command(f"{git_cmd} checkout gh-pages", cwd=self.temp_dir)
- else:
- # gh-pagesブランチを新規作成(orphanブランチとして)
- print("📝 gh-pagesブランチを新規作成します...")
- self._run_command(f"{git_cmd} checkout --orphan gh-pages", cwd=self.temp_dir)
- # 全ファイルを一度削除(orphanブランチなので)
- self._run_command(f"{git_cmd} rm -rf .", cwd=self.temp_dir)
-
- # mainブランチから該当slugフォルダをコピー
- self._run_command(f"{git_cmd} checkout main -- {slug}/", cwd=self.temp_dir)
-
- # コミット&プッシュ
- self._run_command(f"{git_cmd} add {slug}/", cwd=self.temp_dir)
-
- # 変更があるかチェック
- diff_result = subprocess.run(
- f"{git_cmd} diff --cached --quiet",
- shell=True, cwd=self.temp_dir, capture_output=True
- )
-
- if diff_result.returncode != 0:
- # 変更がある場合のみコミット
- self._run_command(
- f'{git_cmd} commit -m "sync: {slug} from main to gh-pages"',
- cwd=self.temp_dir
- )
- if not self._run_command(f"{git_cmd} push origin gh-pages", cwd=self.temp_dir):
- print("⚠️ gh-pagesへのプッシュに失敗しました")
- # mainに戻す
- self._run_command(f"{git_cmd} checkout main", cwd=self.temp_dir)
- return False
- print("✅ gh-pagesブランチへの同期完了")
- else:
- print("✅ gh-pagesは既に最新です(変更なし)")
-
- # mainブランチに戻る
- self._run_command(f"{git_cmd} checkout main", cwd=self.temp_dir)
-
- return True
-
- def cleanup_temp_dir(self):
- """一時ディレクトリを削除"""
- if self.temp_dir and self.temp_dir.exists():
- print(f"\n🧹 一時ディレクトリを削除: {self.temp_dir}")
- shutil.rmtree(self.temp_dir)
- self.temp_dir = None
-
- def display_completion(self, slug: str):
- """完了メッセージ表示"""
- pages_url = f"https://{self.github_username}.github.io/ai-agent-portfolio/{slug}/"
- repo_url = f"https://github.com/{self.github_username}/ai-agent-portfolio"
-
- print("\n" + "="*60)
- print("🎉 GitHub公開完了!")
- print("="*60)
- print(f"\n📦 リポジトリURL:")
- print(f" {repo_url}")
- print(f"\n📊 公開確認:")
- print(f" {repo_url}/tree/main/{slug}")
- print(f"\n🌐 GitHub Pages(有効化した場合):")
- print(f" {pages_url}")
- print(f" {pages_url}about.html")
- print("\n" + "="*60)
-
- def verify_before_publish(self) -> bool:
- """公開前の最終セキュリティチェック"""
- print("\n🔍 公開前セキュリティチェック...")
-
- issues_found = []
-
- # ========================================
- # ドットファイル/フォルダの検出(最優先チェック)
- # ========================================
- dotfiles = list(self.public_path.rglob('.*'))
- if dotfiles:
- for item in dotfiles:
- rel_path = item.relative_to(self.public_path)
- item_type = "フォルダ" if item.is_dir() else "ファイル"
- issues_found.append(f" ❌ ドット{item_type}: {rel_path}")
-
- # ========================================
- # その他の危険なパターン
- # ========================================
- dangerous_patterns = {
- '**/*.key.json': 'APIキーファイル',
- '**/*.pem': '証明書ファイル',
- '**/credentials/*': '認証情報フォルダ',
- '**/old/*': 'バックアップフォルダ',
- '**/backup/*': 'バックアップフォルダ',
- '**/test*/*': 'テストフォルダ',
- '**/*agent*.py': '開発ツール'
- }
-
- for pattern, description in dangerous_patterns.items():
- for file_path in self.public_path.glob(pattern):
- if file_path.exists():
- issues_found.append(f" ❌ {description}: {file_path.relative_to(self.public_path)}")
-
- if issues_found:
- print("\n⚠️ 以下の問題が検出されました:")
- for issue in issues_found:
- print(issue)
-
- # auto_modeの場合は自動でクリーンアップを実行
- if self.auto_mode:
- print("\n🤖 自動モード: クリーンアップを自動実行します")
- self.clean_public()
- return True
-
- print("\n対応を選択してください:")
- print("1. 自動クリーンアップを実行して続行")
- print("2. 処理を中止")
-
- try:
- choice = input("\n選択 (1/2): ").strip()
- if choice == "1":
- print("\n🧹 追加クリーンアップを実行中...")
- self.clean_public()
- return True
- else:
- print("\n❌ 処理を中止しました")
- return False
- except EOFError:
- # 標準入力がない場合(非対話環境)は自動クリーンアップ
- print("\n🤖 非対話環境検出: クリーンアップを自動実行します")
- self.clean_public()
- return True
- else:
- print(" ✅ セキュリティチェック: 問題なし")
- return True
-
- def publish(self) -> bool:
- """メイン実行関数"""
- print("\n" + "="*60)
- print("🚀 GitHub公開 v8.1(一時clone方式・gh-pages同期対応)")
- print("="*60)
-
- try:
- # 1. slug決定
- slug = self.get_slug()
- print(f"\n📝 公開slug: {slug}")
-
- # 2. project/public/ 検証
- if not self.validate_public():
- return False
-
- # 3. クリーニング
- self.clean_public()
-
- # 4. セキュリティチェック
- if not self.verify_before_publish():
- return False
-
- # 5. 一時ディレクトリにclone
- self.clone_portfolio_repo(slug)
-
- # 6. コピー
- self.copy_to_temp_portfolio(slug)
-
- # 7. mainブランチにGit push
- if not self.git_commit_and_push(slug):
- return False
-
- # 8. gh-pagesブランチに同期(GitHub Pages用)
- if not self.sync_to_gh_pages(slug):
- print("⚠️ gh-pages同期に失敗しましたが、mainへの公開は完了しています")
- # mainへの公開は成功しているので続行
-
- # 9. 完了メッセージ
- self.display_completion(slug)
-
- return True
-
- finally:
- # 10. 一時ディレクトリ削除(必ず実行)
- self.cleanup_temp_dir()
-
-
-def main():
- """コマンドライン実行用"""
- # オプション解析
- args = sys.argv[1:]
- auto_mode = '--auto' in args or '-a' in args
- non_options = [a for a in args if not a.startswith('-')]
-
- if non_options:
- project_path = non_options[0]
- else:
- project_path = os.getcwd()
-
- project_path = os.path.abspath(project_path)
-
- if not os.path.exists(project_path):
- print(f"❌ パスが見つかりません: {project_path}")
- sys.exit(1)
-
- if auto_mode:
- print("🤖 自動モード有効: 対話なしで実行します")
-
- publisher = SimplifiedGitHubPublisher(project_path, auto_mode=auto_mode)
- success = publisher.publish()
-
- sys.exit(0 if success else 1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/tts_generator.py b/src/tts_generator.py
deleted file mode 100755
index 9041e4c..0000000
--- a/src/tts_generator.py
+++ /dev/null
@@ -1,337 +0,0 @@
-#!/usr/bin/env python3
-"""
-Google Text-to-Speech統合
-解説台本を音声ファイルに変換
-"""
-
-import os
-import json
-import base64
-from typing import Dict, Optional
-from pathlib import Path
-
-# Google Cloud TTS をオプショナルにインポート
-try:
- from google.cloud import texttospeech
- GOOGLE_TTS_AVAILABLE = True
-except ImportError:
- GOOGLE_TTS_AVAILABLE = False
- print("Warning: google-cloud-texttospeech not installed. TTS features will be limited.")
-
-class TTSGenerator:
- """Text-to-Speech生成クラス"""
-
- def __init__(self, credentials_path: Optional[str] = None):
- """
- Args:
- credentials_path: Google Cloud認証JSONファイルのパス
- """
- self.credentials_path = credentials_path
- self.client = None
-
- if GOOGLE_TTS_AVAILABLE and credentials_path:
- self._initialize_client()
-
- def _initialize_client(self):
- """Google TTS クライアントを初期化"""
- if self.credentials_path and os.path.exists(self.credentials_path):
- os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = self.credentials_path
- self.client = texttospeech.TextToSpeechClient()
- print("Google TTS client initialized successfully")
- else:
- print(f"Warning: Credentials file not found at {self.credentials_path}")
-
- def generate_audio(self,
- text: str,
- output_path: str = "narration.mp3",
- voice_config: Optional[Dict] = None) -> Optional[str]:
- """
- テキストから音声ファイルを生成
-
- Args:
- text: 読み上げるテキスト
- output_path: 出力ファイルパス
- voice_config: 音声設定
-
- Returns:
- 生成された音声ファイルのパス、失敗時はNone
- """
-
- if not GOOGLE_TTS_AVAILABLE:
- print("Google TTS is not available. Generating placeholder file...")
- return self._generate_placeholder_audio(text, output_path)
-
- if not self.client:
- print("TTS client not initialized. Generating placeholder file...")
- return self._generate_placeholder_audio(text, output_path)
-
- try:
- # デフォルトの音声設定
- if voice_config is None:
- voice_config = self._get_default_voice_config()
-
- # SSML形式かプレーンテキストかを判定
- if text.strip().startswith(''):
- synthesis_input = texttospeech.SynthesisInput(ssml=text)
- else:
- synthesis_input = texttospeech.SynthesisInput(text=text)
-
- # 音声設定
- voice = texttospeech.VoiceSelectionParams(
- language_code=voice_config['voice']['languageCode'],
- name=voice_config['voice'].get('name'),
- ssml_gender=texttospeech.SsmlVoiceGender[voice_config['voice']['ssmlGender']]
- )
-
- # オーディオ設定
- audio_config = texttospeech.AudioConfig(
- audio_encoding=texttospeech.AudioEncoding.MP3,
- speaking_rate=voice_config['audioConfig'].get('speakingRate', 1.0),
- pitch=voice_config['audioConfig'].get('pitch', 0.0),
- volume_gain_db=voice_config['audioConfig'].get('volumeGainDb', 0.0),
- effects_profile_id=voice_config['audioConfig'].get('effectsProfileId', [])
- )
-
- # 音声合成リクエスト
- response = self.client.synthesize_speech(
- input=synthesis_input,
- voice=voice,
- audio_config=audio_config
- )
-
- # 音声ファイルを保存
- with open(output_path, 'wb') as out:
- out.write(response.audio_content)
-
- print(f"Audio file generated successfully: {output_path}")
- return output_path
-
- except Exception as e:
- print(f"Error generating audio: {e}")
- return self._generate_placeholder_audio(text, output_path)
-
- def _get_default_voice_config(self) -> Dict:
- """デフォルトの音声設定を取得"""
- return {
- "voice": {
- "languageCode": "ja-JP",
- "name": "ja-JP-Wavenet-B",
- "ssmlGender": "MALE"
- },
- "audioConfig": {
- "speakingRate": 1.0,
- "pitch": 0.0,
- "volumeGainDb": 0.0,
- "effectsProfileId": ["headphone-class-device"]
- }
- }
-
- def _generate_placeholder_audio(self, text: str, output_path: str) -> str:
- """
- プレースホルダー音声ファイルを生成
- (実際のTTSが利用できない場合の代替)
- """
-
- # メタデータファイルを作成
- metadata_path = output_path.replace('.mp3', '_metadata.json')
- metadata = {
- "type": "placeholder",
- "text_length": len(text),
- "estimated_duration": len(text) / 10, # 大まかな推定時間(秒)
- "message": "This is a placeholder audio file. Google TTS is not configured.",
- "setup_instructions": {
- "1": "Install google-cloud-texttospeech: pip install google-cloud-texttospeech",
- "2": "Get Google Cloud credentials from https://console.cloud.google.com",
- "3": "Enable Text-to-Speech API",
- "4": "Set credentials path in environment or config"
- }
- }
-
- with open(metadata_path, 'w', encoding='utf-8') as f:
- json.dump(metadata, f, ensure_ascii=False, indent=2)
-
- # 簡単な無音MP3を生成(ヘッダーのみ)
- # これは実際の音声ではなく、プレースホルダーとしての最小限のMP3ファイル
- mp3_header = b'\xff\xfb\x90\x00' # MP3ヘッダーの簡易版
- with open(output_path, 'wb') as f:
- f.write(mp3_header)
-
- print(f"Placeholder audio file created: {output_path}")
- print(f"Metadata saved to: {metadata_path}")
- return output_path
-
- def batch_generate(self,
- scripts: Dict[str, str],
- output_dir: str = "./audio",
- voice_config: Optional[Dict] = None) -> Dict[str, str]:
- """
- 複数の台本を一括で音声化
-
- Args:
- scripts: {filename: text} の辞書
- output_dir: 出力ディレクトリ
- voice_config: 音声設定
-
- Returns:
- {filename: audio_path} の辞書
- """
-
- os.makedirs(output_dir, exist_ok=True)
- results = {}
-
- for filename, text in scripts.items():
- output_path = os.path.join(output_dir, f"{filename}.mp3")
- audio_path = self.generate_audio(text, output_path, voice_config)
- if audio_path:
- results[filename] = audio_path
-
- return results
-
- def estimate_cost(self, text: str) -> Dict:
- """
- Google TTS APIの使用料金を推定
-
- Args:
- text: 読み上げるテキスト
-
- Returns:
- 料金推定情報
- """
-
- # 文字数をカウント
- char_count = len(text)
-
- # Google TTS の料金体系(2024年時点の目安)
- # WaveNet voices: $16.00 per 1 million characters
- # Standard voices: $4.00 per 1 million characters
- wavenet_rate = 16.00 / 1_000_000
- standard_rate = 4.00 / 1_000_000
-
- return {
- "character_count": char_count,
- "estimated_cost_wavenet": f"${char_count * wavenet_rate:.4f}",
- "estimated_cost_standard": f"${char_count * standard_rate:.4f}",
- "free_tier_remaining": max(0, 1_000_000 - char_count), # 月間無料枠
- "note": "First 1 million characters per month are free"
- }
-
-
-class TTSConfig:
- """TTS設定管理クラス"""
-
- @staticmethod
- def create_config_template() -> Dict:
- """設定テンプレートを作成"""
- return {
- "google_cloud": {
- "credentials_path": "${GOOGLE_APPLICATION_CREDENTIALS}",
- "project_id": "${GOOGLE_CLOUD_PROJECT}"
- },
- "default_voice": {
- "language": "ja-JP",
- "voice_name": "ja-JP-Wavenet-B",
- "gender": "MALE",
- "speaking_rate": 1.0,
- "pitch": 0.0
- },
- "audio_settings": {
- "format": "MP3",
- "sample_rate": 24000,
- "effects_profile": ["headphone-class-device"]
- },
- "batch_settings": {
- "max_concurrent": 5,
- "retry_count": 3,
- "retry_delay": 1000
- }
- }
-
- @staticmethod
- def save_template(filepath: str = "tts_config_template.json"):
- """設定テンプレートをファイルに保存"""
- template = TTSConfig.create_config_template()
- with open(filepath, 'w', encoding='utf-8') as f:
- json.dump(template, f, ensure_ascii=False, indent=2)
- return filepath
-
-
-def setup_tts_environment():
- """TTS環境のセットアップヘルパー"""
-
- setup_script = """#!/bin/bash
-# Google Cloud Text-to-Speech セットアップスクリプト
-
-echo "📢 Google Cloud Text-to-Speech セットアップを開始します..."
-
-# 1. パッケージのインストール
-echo "1. 必要なパッケージをインストールしています..."
-pip install google-cloud-texttospeech
-
-# 2. 認証情報の確認
-echo "2. Google Cloud認証情報を確認しています..."
-if [ -z "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
- echo "⚠️ GOOGLE_APPLICATION_CREDENTIALS が設定されていません"
- echo " 以下の手順で設定してください:"
- echo " 1. https://console.cloud.google.com にアクセス"
- echo " 2. プロジェクトを作成または選択"
- echo " 3. Text-to-Speech API を有効化"
- echo " 4. サービスアカウントキーを作成"
- echo " 5. export GOOGLE_APPLICATION_CREDENTIALS='path/to/key.json'"
-else
- echo "✅ 認証情報が設定されています: $GOOGLE_APPLICATION_CREDENTIALS"
-fi
-
-# 3. API有効化の確認
-echo "3. Text-to-Speech APIの有効化を確認しています..."
-echo " https://console.cloud.google.com/apis/library/texttospeech.googleapis.com"
-
-# 4. テンプレート設定ファイルの作成
-echo "4. 設定テンプレートを作成しています..."
-python -c "
-from tts_generator import TTSConfig
-TTSConfig.save_template('tts_config_template.json')
-print('✅ tts_config_template.json を作成しました')
-"
-
-echo ""
-echo "🎉 セットアップ完了!"
-echo "次のステップ:"
-echo "1. Google Cloud認証情報を設定"
-echo "2. tts_config_template.json を編集"
-echo "3. TTSGenerator クラスを使用して音声生成"
-"""
-
- # セットアップスクリプトを保存
- setup_path = "setup_tts.sh"
- with open(setup_path, 'w') as f:
- f.write(setup_script)
- os.chmod(setup_path, 0o755)
-
- print(f"Setup script created: {setup_path}")
- print("Run './setup_tts.sh' to configure Google TTS")
-
- return setup_path
-
-
-if __name__ == "__main__":
- # セットアップヘルパーを実行
- setup_tts_environment()
-
- # 設定テンプレートを作成
- TTSConfig.save_template()
-
- print("\n📝 使用例:")
- print("```python")
- print("from tts_generator import TTSGenerator")
- print("")
- print("# TTSジェネレーターを初期化")
- print("tts = TTSGenerator(credentials_path='path/to/credentials.json')")
- print("")
- print("# テキストを音声に変換")
- print("text = 'こんにちは、これはテストです。'")
- print("audio_path = tts.generate_audio(text, 'output.mp3')")
- print("")
- print("# 料金を推定")
- print("cost = tts.estimate_cost(text)")
- print("print(cost)")
- print("```")
\ No newline at end of file
diff --git a/src/tts_smart_generator.py b/src/tts_smart_generator.py
deleted file mode 100755
index dddf34c..0000000
--- a/src/tts_smart_generator.py
+++ /dev/null
@@ -1,498 +0,0 @@
-#!/usr/bin/env python3
-"""
-スマートTTS生成システム
-文脈を考慮した自動分割と結合で、長い台本も1つのMP3ファイルに
-"""
-
-import os
-import json
-import re
-import tempfile
-import subprocess
-from typing import Dict, List, Optional, Tuple
-from datetime import datetime
-from pathlib import Path
-
-# Google Cloud TTS をオプショナルにインポート
-try:
- from google.cloud import texttospeech
- GOOGLE_TTS_AVAILABLE = True
-except ImportError:
- GOOGLE_TTS_AVAILABLE = False
- print("Warning: google-cloud-texttospeech not installed.")
-
-
-class SmartTTSGenerator:
- """スマートなTTS生成クラス"""
-
- def __init__(self, credentials_path: Optional[str] = None):
- """
- Args:
- credentials_path: Google Cloud認証JSONファイルのパス
- """
- # 認証パスの優先順位:
- # 1. 引数で明示的に指定
- # 2. 環境変数 GOOGLE_APPLICATION_CREDENTIALS
- # 3. .env ファイルから読み込み
- # 4. テンプレート環境のデフォルトパス
-
- if credentials_path:
- self.credentials_path = credentials_path
- elif os.environ.get('GOOGLE_APPLICATION_CREDENTIALS'):
- self.credentials_path = os.environ['GOOGLE_APPLICATION_CREDENTIALS']
- else:
- # .env ファイルを探して読み込み
- self._load_env_file()
- self.credentials_path = os.environ.get(
- 'GOOGLE_APPLICATION_CREDENTIALS',
- os.path.expanduser("~/Desktop/git-worktree-agent/credentials/gcp-workflow-key.json")
- )
-
- self.client = None
- self.max_bytes = 4500 # 5000バイト制限より少し小さめ
-
- if GOOGLE_TTS_AVAILABLE and os.path.exists(self.credentials_path):
- self._initialize_client()
-
- def _load_env_file(self):
- """カレントディレクトリから .env ファイルを探して読み込み"""
- try:
- from dotenv import load_dotenv
-
- # カレントディレクトリの .env
- if os.path.exists('.env'):
- load_dotenv('.env')
- return
-
- # 親ディレクトリの .env
- if os.path.exists('../.env'):
- load_dotenv('../.env')
- return
-
- except ImportError:
- # dotenvがない場合は手動で読み込み
- env_paths = ['.env', '../.env']
- for env_path in env_paths:
- if os.path.exists(env_path):
- with open(env_path, 'r') as f:
- for line in f:
- line = line.strip()
- if line and not line.startswith('#') and '=' in line:
- key, value = line.split('=', 1)
- # ~ を展開
- value = os.path.expanduser(value.strip())
- os.environ[key.strip()] = value
- return
-
- def _initialize_client(self):
- """Google TTS クライアントを初期化"""
- os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = self.credentials_path
- self.client = texttospeech.TextToSpeechClient()
- print("✅ Google TTS client initialized")
-
- def split_text_by_context(self, text: str, is_ssml: bool = False) -> List[str]:
- """
- 文脈を考慮してテキストを分割
-
- Args:
- text: 分割するテキスト
- is_ssml: SSML形式かどうか
-
- Returns:
- 分割されたテキストのリスト
- """
- chunks = []
- current_chunk = ""
-
- if is_ssml:
- # SSMLタグを一時的に削除して分割処理
- text = re.sub(r'|', '', text)
-
- # 優先度順の区切り文字
- # 1. セクション区切り(breakタイムが長いもの)
- # 2. 段落区切り(改行2つ以上)
- # 3. 文の区切り(句点)
- # 4. 改行
-
- # まず大きなセクションで分割を試みる
- sections = re.split(r'', text)
-
- for section in sections:
- if not section.strip():
- continue
-
- # セクションが制限内なら追加
- if self._get_byte_size(current_chunk + section) <= self.max_bytes:
- current_chunk += section
- if section != sections[-1]: # 最後のセクション以外
- current_chunk += ''
- else:
- # セクションが大きすぎる場合は段落で分割
- if current_chunk:
- chunks.append(current_chunk.strip())
- current_chunk = ""
-
- # 段落で分割
- paragraphs = section.split('\n\n')
- for paragraph in paragraphs:
- if self._get_byte_size(current_chunk + paragraph) <= self.max_bytes:
- current_chunk += paragraph + '\n\n'
- else:
- # 段落も大きすぎる場合は文で分割
- if current_chunk:
- chunks.append(current_chunk.strip())
- current_chunk = ""
-
- sentences = self._split_by_sentence(paragraph)
- for sentence in sentences:
- if self._get_byte_size(current_chunk + sentence) <= self.max_bytes:
- current_chunk += sentence
- else:
- if current_chunk:
- chunks.append(current_chunk.strip())
- current_chunk = sentence
-
- # 最後のチャンクを追加
- if current_chunk.strip():
- chunks.append(current_chunk.strip())
-
- # SSMLの場合は各チャンクにタグを追加
- if is_ssml:
- chunks = [f'{chunk}' for chunk in chunks]
-
- return chunks
-
- def _split_by_sentence(self, text: str) -> List[str]:
- """文単位で分割"""
- # 日本語の句点で分割
- sentences = re.split(r'([。!?])', text)
-
- # 句読点を文に含める
- result = []
- for i in range(0, len(sentences), 2):
- if i + 1 < len(sentences):
- result.append(sentences[i] + sentences[i + 1])
- else:
- result.append(sentences[i])
-
- return [s for s in result if s.strip()]
-
- def _get_byte_size(self, text: str) -> int:
- """テキストのバイトサイズを取得"""
- return len(text.encode('utf-8'))
-
- def generate_audio_chunks(self,
- chunks: List[str],
- voice_config: Optional[Dict] = None,
- is_ssml: bool = False) -> List[str]:
- """
- テキストチャンクから音声ファイルを生成
-
- Args:
- chunks: テキストチャンクのリスト
- voice_config: 音声設定
- is_ssml: SSML形式かどうか
-
- Returns:
- 生成された音声ファイルパスのリスト
- """
- if not self.client:
- print("❌ TTS client not initialized")
- return []
-
- temp_files = []
-
- # デフォルトの音声設定
- if voice_config is None:
- voice_config = {
- "language_code": "ja-JP",
- "name": "ja-JP-Wavenet-B",
- "ssml_gender": "MALE"
- }
-
- print(f"📝 {len(chunks)}個のチャンクを処理中...")
-
- for i, chunk in enumerate(chunks):
- print(f" チャンク {i+1}/{len(chunks)} を生成中...")
-
- try:
- # 音声合成の入力
- if is_ssml:
- synthesis_input = texttospeech.SynthesisInput(ssml=chunk)
- else:
- synthesis_input = texttospeech.SynthesisInput(text=chunk)
-
- # 音声設定
- voice = texttospeech.VoiceSelectionParams(
- language_code=voice_config.get('language_code', 'ja-JP'),
- name=voice_config.get('name', 'ja-JP-Wavenet-B')
- )
-
- audio_config = texttospeech.AudioConfig(
- audio_encoding=texttospeech.AudioEncoding.MP3,
- speaking_rate=voice_config.get('speaking_rate', 1.0),
- pitch=voice_config.get('pitch', 0.0),
- volume_gain_db=voice_config.get('volume_gain_db', 0.0)
- )
-
- # API呼び出し
- response = self.client.synthesize_speech(
- input=synthesis_input,
- voice=voice,
- audio_config=audio_config
- )
-
- # 一時ファイルに保存
- temp_file = tempfile.NamedTemporaryFile(
- suffix=f'_chunk_{i}.mp3',
- delete=False,
- dir=tempfile.gettempdir()
- )
- temp_file.write(response.audio_content)
- temp_file.close()
- temp_files.append(temp_file.name)
-
- print(f" ✅ チャンク {i+1} 完了 ({len(response.audio_content) / 1024:.2f} KB)")
-
- except Exception as e:
- print(f" ❌ チャンク {i+1} エラー: {e}")
-
- return temp_files
-
- def merge_audio_files(self, audio_files: List[str], output_path: str) -> bool:
- """
- 複数の音声ファイルを結合
-
- Args:
- audio_files: 結合する音声ファイルのリスト
- output_path: 出力ファイルパス
-
- Returns:
- 成功したかどうか
- """
- if not audio_files:
- print("❌ 結合する音声ファイルがありません")
- return False
-
- try:
- # ffmpegが利用可能か確認
- result = subprocess.run(['which', 'ffmpeg'], capture_output=True, text=True)
- has_ffmpeg = result.returncode == 0
-
- if has_ffmpeg:
- # ffmpegを使用して結合
- print("🔧 ffmpegで音声ファイルを結合中...")
-
- # ファイルリストを作成
- list_file = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False)
- for audio_file in audio_files:
- list_file.write(f"file '{audio_file}'\n")
- list_file.close()
-
- # ffmpegで結合
- cmd = [
- 'ffmpeg', '-f', 'concat', '-safe', '0',
- '-i', list_file.name,
- '-c', 'copy',
- '-y', # 上書き確認なし
- output_path
- ]
-
- result = subprocess.run(cmd, capture_output=True, text=True)
- os.unlink(list_file.name)
-
- if result.returncode == 0:
- print(f"✅ 音声ファイルを結合しました: {output_path}")
- return True
- else:
- print(f"❌ ffmpeg結合エラー: {result.stderr}")
- # フォールバック
- return self._merge_with_python(audio_files, output_path)
-
- else:
- # ffmpegがない場合はPythonで結合
- return self._merge_with_python(audio_files, output_path)
-
- except Exception as e:
- print(f"❌ 結合エラー: {e}")
- return False
-
- def _merge_with_python(self, audio_files: List[str], output_path: str) -> bool:
- """Pythonで音声ファイルを結合(簡易版)"""
- print("🔧 Pythonで音声ファイルを結合中...")
-
- try:
- with open(output_path, 'wb') as output:
- for audio_file in audio_files:
- with open(audio_file, 'rb') as input_file:
- output.write(input_file.read())
-
- print(f"✅ 音声ファイルを結合しました: {output_path}")
- return True
-
- except Exception as e:
- print(f"❌ Python結合エラー: {e}")
- return False
-
- def generate_from_text(self,
- text: str,
- output_path: str,
- voice_config: Optional[Dict] = None,
- cleanup: bool = True) -> Tuple[bool, Dict]:
- """
- テキストから音声ファイルを生成(メインメソッド)
-
- Args:
- text: 入力テキスト(プレーンテキストまたはSSML)
- output_path: 出力MP3ファイルパス
- voice_config: 音声設定
- cleanup: 一時ファイルを削除するか
-
- Returns:
- (成功フラグ, 統計情報)
- """
- start_time = datetime.now()
- stats = {
- 'total_characters': len(text),
- 'total_bytes': self._get_byte_size(text),
- 'chunks': 0,
- 'temp_files': [],
- 'duration': 0
- }
-
- print("\n" + "="*60)
- print("🎙️ スマートTTS音声生成を開始")
- print("="*60)
-
- # SSML判定
- is_ssml = text.strip().startswith('') or ' Optional[float]:
- if self.start_time and self.end_time:
- return (self.end_time - self.start_time).total_seconds()
- return None
-
-@dataclass
-class WorkflowPhase:
- """ワークフローフェーズ"""
- name: str
- agents: List[AgentType]
- parallel: bool = False
- max_iterations: int = 1
- success_criteria: Optional[str] = None
- tasks: List[Task] = field(default_factory=list)
-
-class WorkflowOrchestrator:
- """
- ワークフロー実行オーケストレーター
- 自律的にエージェントを起動し、並列処理を管理
- """
-
- def __init__(self, config_path: str = "agent_config.yaml"):
- self.config_path = Path(config_path)
- self.config = self._load_config()
- self.task_queue = queue.Queue()
- self.results = {}
- self.executor = ThreadPoolExecutor(max_workers=3)
- self.current_worktree = None
-
- def _load_config(self) -> Dict:
- """設定ファイルを読み込み"""
- if not self.config_path.exists():
- raise FileNotFoundError(f"Config file not found: {self.config_path}")
-
- with open(self.config_path, 'r', encoding='utf-8') as f:
- return yaml.safe_load(f)
-
- def execute_workflow(self, workflow_name: str, project_name: str) -> Dict:
- """
- ワークフローを実行
-
- Args:
- workflow_name: 実行するワークフロー名
- project_name: プロジェクト名
-
- Returns:
- 実行結果のディクショナリ
- """
- logger.info(f"🚀 Starting workflow: {workflow_name} for project: {project_name}")
-
- # ワークフロー定義を取得
- workflow_def = self.config['workflows'].get(workflow_name)
- if not workflow_def:
- raise ValueError(f"Workflow not found: {workflow_name}")
-
- # Worktree作成
- self.current_worktree = self._create_worktree(project_name)
-
- try:
- # フェーズごとに実行
- results = {}
- phases = workflow_def.get('phases', [])
-
- for phase_def in phases:
- phase = WorkflowPhase(
- name=phase_def['phase'],
- agents=[AgentType(a) for a in phase_def['agents']],
- parallel=phase_def.get('parallel', False),
- max_iterations=phase_def.get('max_iterations', 1),
- success_criteria=phase_def.get('success_criteria')
- )
-
- logger.info(f"📋 Executing phase: {phase.name}")
- phase_result = self._execute_phase(phase)
- results[phase.name] = phase_result
-
- # 改善ループの場合、成功するまで繰り返し
- if phase.name == "改善ループ":
- iteration = 1
- while iteration < phase.max_iterations:
- if self._check_success_criteria(phase_result, phase.success_criteria):
- break
-
- logger.info(f"🔄 Improvement iteration {iteration + 1}/{phase.max_iterations}")
- phase_result = self._execute_phase(phase)
- results[f"{phase.name}_iteration_{iteration + 1}"] = phase_result
- iteration += 1
-
- # 成果物をマージ
- if not workflow_def.get('auto_merge', True):
- logger.info("⚠️ Auto-merge disabled. Manual merge required.")
- else:
- self._merge_results(project_name)
-
- return {
- 'status': 'success',
- 'workflow': workflow_name,
- 'project': project_name,
- 'worktree': str(self.current_worktree),
- 'phases': results,
- 'timestamp': datetime.now().isoformat()
- }
-
- except Exception as e:
- logger.error(f"❌ Workflow execution failed: {e}")
- return {
- 'status': 'failed',
- 'error': str(e),
- 'workflow': workflow_name,
- 'project': project_name,
- 'timestamp': datetime.now().isoformat()
- }
-
- def _create_worktree(self, project_name: str) -> Path:
- """Git Worktreeを作成"""
- worktree_path = Path(f"./worktrees/mission-{project_name}")
-
- if worktree_path.exists():
- logger.warning(f"Worktree already exists: {worktree_path}")
- return worktree_path
-
- # git worktree add -b feat/{project_name} ./worktrees/mission-{project_name} main
- cmd = [
- "git", "worktree", "add",
- "-b", f"feat/{project_name}",
- str(worktree_path),
- "main"
- ]
-
- result = subprocess.run(cmd, capture_output=True, text=True)
- if result.returncode != 0:
- raise RuntimeError(f"Failed to create worktree: {result.stderr}")
-
- logger.info(f"✅ Created worktree: {worktree_path}")
- return worktree_path
-
- def _execute_phase(self, phase: WorkflowPhase) -> Dict:
- """フェーズを実行(並列/直列処理対応)"""
- phase_start = datetime.now()
-
- # タスクを生成
- tasks = []
- for i, agent in enumerate(phase.agents):
- task = Task(
- id=f"{phase.name}_{agent.value}_{i}",
- name=f"{agent.value} task",
- agent=agent,
- description=f"Execute {agent.value} for {phase.name}"
- )
- tasks.append(task)
-
- # 実行(並列または直列)
- if phase.parallel:
- results = self._execute_parallel(tasks)
- else:
- results = self._execute_serial(tasks)
-
- phase_end = datetime.now()
-
- return {
- 'phase': phase.name,
- 'parallel': phase.parallel,
- 'tasks': [self._task_to_dict(t) for t in tasks],
- 'duration': (phase_end - phase_start).total_seconds(),
- 'success': all(t.status == TaskStatus.COMPLETED for t in tasks)
- }
-
- def _execute_parallel(self, tasks: List[Task]) -> Dict:
- """タスクを並列実行"""
- logger.info(f"⚡ Executing {len(tasks)} tasks in parallel")
-
- futures = {}
- for task in tasks:
- future = self.executor.submit(self._execute_task, task)
- futures[future] = task
-
- # 結果を収集
- results = {}
- for future in as_completed(futures):
- task = futures[future]
- try:
- result = future.result()
- task.status = TaskStatus.COMPLETED
- task.result = result
- results[task.id] = result
- logger.info(f"✅ Task completed: {task.name}")
- except Exception as e:
- task.status = TaskStatus.FAILED
- task.error = str(e)
- logger.error(f"❌ Task failed: {task.name} - {e}")
-
- return results
-
- def _execute_serial(self, tasks: List[Task]) -> Dict:
- """タスクを直列実行"""
- logger.info(f"📝 Executing {len(tasks)} tasks serially")
-
- results = {}
- for task in tasks:
- try:
- result = self._execute_task(task)
- task.status = TaskStatus.COMPLETED
- task.result = result
- results[task.id] = result
- logger.info(f"✅ Task completed: {task.name}")
- except Exception as e:
- task.status = TaskStatus.FAILED
- task.error = str(e)
- logger.error(f"❌ Task failed: {task.name} - {e}")
- break # 直列実行では失敗時に停止
-
- return results
-
- def _execute_task(self, task: Task) -> Dict:
- """個別タスクを実行(エージェント起動)"""
- task.start_time = datetime.now()
- task.status = TaskStatus.RUNNING
-
- logger.info(f"🤖 Launching agent: {task.agent.value}")
-
- # エージェント設定を取得
- agent_config = self.config['agents'].get(task.agent.value, {})
-
- # Claudeコードを使ってTaskツールを呼び出すコマンドを生成
- # 実際の実装では、Claude APIを直接呼び出すか、
- # サブプロセスでClaude CLIを実行
-
- # シミュレーション(実際にはClaude APIを呼ぶ)
- time.sleep(2) # エージェント実行のシミュレーション
-
- task.end_time = datetime.now()
-
- # 結果を返す(シミュレーション)
- return {
- 'agent': task.agent.value,
- 'status': 'completed',
- 'duration': task.duration,
- 'output': f"Output from {task.agent.value}",
- 'files_created': [],
- 'tests_passed': True if 'test' in task.agent.value else None
- }
-
- def _check_success_criteria(self, phase_result: Dict, criteria: Optional[str]) -> bool:
- """成功基準をチェック"""
- if not criteria:
- return True
-
- if criteria == "all_tests_pass":
- # すべてのテストが成功しているかチェック
- for task in phase_result.get('tasks', []):
- if task.get('tests_passed') is False:
- return False
- return True
-
- return True
-
- def _merge_results(self, project_name: str):
- """成果物をメインブランチにマージ"""
- logger.info("🔀 Merging results to main branch")
-
- # git merge feat/{project_name}
- cmd = ["git", "merge", f"feat/{project_name}"]
- result = subprocess.run(cmd, capture_output=True, text=True)
-
- if result.returncode != 0:
- logger.error(f"Merge failed: {result.stderr}")
- raise RuntimeError("Failed to merge results")
-
- logger.info("✅ Successfully merged to main branch")
-
- def _task_to_dict(self, task: Task) -> Dict:
- """タスクを辞書形式に変換"""
- return {
- 'id': task.id,
- 'name': task.name,
- 'agent': task.agent.value,
- 'status': task.status.value,
- 'duration': task.duration,
- 'error': task.error
- }
-
- def cleanup(self):
- """リソースのクリーンアップ"""
- self.executor.shutdown(wait=True)
-
- if self.current_worktree and self.current_worktree.exists():
- # git worktree remove
- cmd = ["git", "worktree", "remove", str(self.current_worktree)]
- subprocess.run(cmd, capture_output=True, text=True)
- logger.info(f"🧹 Cleaned up worktree: {self.current_worktree}")
-
-
-def main():
- """メインエントリーポイント"""
- import argparse
-
- parser = argparse.ArgumentParser(description='Workflow Orchestrator')
- parser.add_argument('workflow', help='Workflow name to execute')
- parser.add_argument('project', help='Project name')
- parser.add_argument('--config', default='agent_config.yaml', help='Config file path')
-
- args = parser.parse_args()
-
- # オーケストレーター実行
- orchestrator = WorkflowOrchestrator(args.config)
-
- try:
- result = orchestrator.execute_workflow(args.workflow, args.project)
-
- # 結果を表示
- print("\n" + "="*60)
- print("📊 WORKFLOW EXECUTION REPORT")
- print("="*60)
- print(json.dumps(result, indent=2, ensure_ascii=False))
-
- finally:
- orchestrator.cleanup()
-
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/workflow_state_manager.py b/src/workflow_state_manager.py
deleted file mode 100755
index 7f5b5c6..0000000
--- a/src/workflow_state_manager.py
+++ /dev/null
@@ -1,510 +0,0 @@
-#!/usr/bin/env python3
-"""
-ワークフロー状態管理
-
-エージェントの実行状態を追跡し、中断からの復旧と
-Phase 7(修正ワークフロー)の実行を支援する
-"""
-
-import os
-import json
-from pathlib import Path
-from typing import Dict, List, Optional, Any
-from dataclasses import dataclass, field, asdict
-from datetime import datetime
-from enum import Enum
-
-
-class PhaseStatus(Enum):
- """フェーズの状態"""
- PENDING = "pending"
- IN_PROGRESS = "in_progress"
- COMPLETED = "completed"
- FAILED = "failed"
- SKIPPED = "skipped"
-
-
-class WorkflowStatus(Enum):
- """ワークフロー全体の状態"""
- NOT_STARTED = "not_started"
- IN_PROGRESS = "in_progress"
- AWAITING_REVIEW = "awaiting_review" # Phase 6完了、ユーザーレビュー待ち
- MODIFICATION_REQUESTED = "modification_requested" # 修正依頼あり
- MODIFICATION_IN_PROGRESS = "modification_in_progress" # Phase 7実行中
- COMPLETED = "completed"
- FAILED = "failed"
-
-
-@dataclass
-class PhaseRecord:
- """フェーズの実行記録"""
- phase_number: int
- phase_name: str
- status: str
- started_at: Optional[str] = None
- completed_at: Optional[str] = None
- agents_used: List[str] = field(default_factory=list)
- outputs: Dict[str, Any] = field(default_factory=dict)
- error_message: Optional[str] = None
-
-
-@dataclass
-class ModificationRecord:
- """修正の記録"""
- iteration: int
- requested_at: str
- feedback: str
- phases_to_rerun: List[int]
- status: str # pending, in_progress, completed
- completed_at: Optional[str] = None
-
-
-@dataclass
-class PortfolioRecord:
- """ポートフォリオ公開の記録"""
- published: bool = False
- app_name: Optional[str] = None
- app_url: Optional[str] = None
- commit_hash: Optional[str] = None
- last_published_at: Optional[str] = None
- security_check_passed: bool = False
- agent_review_passed: bool = False
-
-
-@dataclass
-class WorkflowState:
- """ワークフロー全体の状態"""
- project_name: str
- project_path: str
- workflow_type: str # creative_webapp, tdd_webapp, etc.
- status: str
- created_at: str
- updated_at: str
- current_phase: int
- phases: List[Dict] = field(default_factory=list)
- portfolio: Dict = field(default_factory=dict)
- modifications: List[Dict] = field(default_factory=list)
- metadata: Dict = field(default_factory=dict)
-
- @classmethod
- def create_new(cls, project_name: str, project_path: str, workflow_type: str) -> "WorkflowState":
- """新規ワークフロー状態を作成"""
- now = datetime.now().isoformat()
- return cls(
- project_name=project_name,
- project_path=project_path,
- workflow_type=workflow_type,
- status=WorkflowStatus.NOT_STARTED.value,
- created_at=now,
- updated_at=now,
- current_phase=0,
- phases=[],
- portfolio=asdict(PortfolioRecord()),
- modifications=[],
- metadata={},
- )
-
-
-class WorkflowStateManager:
- """ワークフロー状態管理マネージャー"""
-
- STATE_FILENAME = ".workflow_state.json"
-
- # フェーズ定義
- PHASES = {
- 1: "計画",
- 2: "デザイン",
- 3: "実装",
- 4: "改善ループ",
- 5: "完成処理",
- 6: "ポートフォリオ公開",
- 7: "修正ワークフロー",
- }
-
- def __init__(self, project_path: str):
- self.project_path = Path(project_path).resolve()
- self.state_file = self.project_path / self.STATE_FILENAME
- self._state: Optional[WorkflowState] = None
-
- @property
- def state(self) -> Optional[WorkflowState]:
- """現在の状態を取得"""
- if self._state is None:
- self._state = self.load_state()
- return self._state
-
- def load_state(self) -> Optional[WorkflowState]:
- """状態ファイルを読み込み"""
- if not self.state_file.exists():
- return None
-
- try:
- with open(self.state_file, "r", encoding="utf-8") as f:
- data = json.load(f)
- return WorkflowState(**data)
- except Exception as e:
- print(f" ⚠️ 状態ファイル読み込みエラー: {e}")
- return None
-
- def save_state(self):
- """状態をファイルに保存"""
- if self._state is None:
- return
-
- self._state.updated_at = datetime.now().isoformat()
-
- with open(self.state_file, "w", encoding="utf-8") as f:
- json.dump(asdict(self._state), f, ensure_ascii=False, indent=2)
-
- def initialize(self, project_name: str, workflow_type: str = "creative_webapp") -> WorkflowState:
- """新規ワークフローを初期化"""
- self._state = WorkflowState.create_new(
- project_name=project_name,
- project_path=str(self.project_path),
- workflow_type=workflow_type,
- )
- self.save_state()
- return self._state
-
- def get_or_create(self, project_name: str, workflow_type: str = "creative_webapp") -> WorkflowState:
- """状態を取得、なければ作成"""
- if self.state is None:
- return self.initialize(project_name, workflow_type)
- return self.state
-
- # ===========================================
- # フェーズ管理
- # ===========================================
-
- def start_phase(self, phase_number: int, agents: List[str] = None):
- """フェーズを開始"""
- if self._state is None:
- raise ValueError("ワークフローが初期化されていません")
-
- phase_name = self.PHASES.get(phase_number, f"Phase {phase_number}")
-
- record = PhaseRecord(
- phase_number=phase_number,
- phase_name=phase_name,
- status=PhaseStatus.IN_PROGRESS.value,
- started_at=datetime.now().isoformat(),
- agents_used=agents or [],
- )
-
- # 既存のフェーズ記録を更新または追加
- existing_idx = None
- for i, p in enumerate(self._state.phases):
- if p.get("phase_number") == phase_number:
- existing_idx = i
- break
-
- if existing_idx is not None:
- self._state.phases[existing_idx] = asdict(record)
- else:
- self._state.phases.append(asdict(record))
-
- self._state.current_phase = phase_number
- self._state.status = WorkflowStatus.IN_PROGRESS.value
- self.save_state()
-
- print(f"\n {'─' * 50}")
- print(f" 【Phase {phase_number}】 {phase_name} 開始")
- print(f" {'─' * 50}")
-
- def complete_phase(self, phase_number: int, outputs: Dict = None):
- """フェーズを完了"""
- if self._state is None:
- return
-
- for p in self._state.phases:
- if p.get("phase_number") == phase_number:
- p["status"] = PhaseStatus.COMPLETED.value
- p["completed_at"] = datetime.now().isoformat()
- if outputs:
- p["outputs"] = outputs
- break
-
- self.save_state()
- print(f" ✅ Phase {phase_number} 完了")
-
- def fail_phase(self, phase_number: int, error_message: str):
- """フェーズを失敗として記録"""
- if self._state is None:
- return
-
- for p in self._state.phases:
- if p.get("phase_number") == phase_number:
- p["status"] = PhaseStatus.FAILED.value
- p["completed_at"] = datetime.now().isoformat()
- p["error_message"] = error_message
- break
-
- self._state.status = WorkflowStatus.FAILED.value
- self.save_state()
- print(f" ❌ Phase {phase_number} 失敗: {error_message}")
-
- def get_phase_status(self, phase_number: int) -> Optional[str]:
- """フェーズの状態を取得"""
- if self._state is None:
- return None
-
- for p in self._state.phases:
- if p.get("phase_number") == phase_number:
- return p.get("status")
- return None
-
- # ===========================================
- # Phase 6: ポートフォリオ公開
- # ===========================================
-
- def record_portfolio_publish(
- self,
- app_name: str,
- app_url: str,
- commit_hash: str,
- security_check_passed: bool,
- agent_review_passed: bool,
- ):
- """ポートフォリオ公開を記録"""
- if self._state is None:
- return
-
- self._state.portfolio = {
- "published": True,
- "app_name": app_name,
- "app_url": app_url,
- "commit_hash": commit_hash,
- "last_published_at": datetime.now().isoformat(),
- "security_check_passed": security_check_passed,
- "agent_review_passed": agent_review_passed,
- }
-
- self._state.status = WorkflowStatus.AWAITING_REVIEW.value
- self.save_state()
-
- # ===========================================
- # Phase 7: 修正ワークフロー
- # ===========================================
-
- def request_modification(self, feedback: str, phases_to_rerun: List[int] = None):
- """修正を依頼"""
- if self._state is None:
- return
-
- # デフォルトは Phase 3 (実装) から再実行
- if phases_to_rerun is None:
- phases_to_rerun = [3, 4, 5, 6]
-
- iteration = len(self._state.modifications) + 1
-
- record = ModificationRecord(
- iteration=iteration,
- requested_at=datetime.now().isoformat(),
- feedback=feedback,
- phases_to_rerun=phases_to_rerun,
- status="pending",
- )
-
- self._state.modifications.append(asdict(record))
- self._state.status = WorkflowStatus.MODIFICATION_REQUESTED.value
- self._state.current_phase = 7
- self.save_state()
-
- print(f"\n 📝 修正依頼 #{iteration} を記録しました")
- print(f" 再実行するフェーズ: {phases_to_rerun}")
-
- def start_modification(self):
- """修正ワークフローを開始"""
- if self._state is None or not self._state.modifications:
- return
-
- # 最新の修正依頼を取得
- latest = self._state.modifications[-1]
- latest["status"] = "in_progress"
- self._state.status = WorkflowStatus.MODIFICATION_IN_PROGRESS.value
- self.save_state()
-
- def complete_modification(self):
- """修正ワークフローを完了"""
- if self._state is None or not self._state.modifications:
- return
-
- latest = self._state.modifications[-1]
- latest["status"] = "completed"
- latest["completed_at"] = datetime.now().isoformat()
- self._state.status = WorkflowStatus.AWAITING_REVIEW.value
- self.save_state()
-
- print(f" ✅ 修正ワークフロー完了(イテレーション #{latest['iteration']})")
-
- def get_pending_modification(self) -> Optional[Dict]:
- """保留中の修正を取得"""
- if self._state is None:
- return None
-
- for mod in reversed(self._state.modifications):
- if mod.get("status") in ["pending", "in_progress"]:
- return mod
- return None
-
- # ===========================================
- # ワークフロー完了
- # ===========================================
-
- def complete_workflow(self):
- """ワークフロー全体を完了"""
- if self._state is None:
- return
-
- self._state.status = WorkflowStatus.COMPLETED.value
- self.save_state()
-
- print("\n" + "=" * 60)
- print(" 🎉 ワークフロー完了")
- print("=" * 60)
-
- # ===========================================
- # 状態レポート
- # ===========================================
-
- def print_status_report(self):
- """状態レポートを表示"""
- if self._state is None:
- print(" ワークフロー状態: 未初期化")
- return
-
- print("\n" + "=" * 60)
- print(" 📊 ワークフロー状態レポート")
- print("=" * 60)
-
- print(f"\n プロジェクト: {self._state.project_name}")
- print(f" ワークフロー: {self._state.workflow_type}")
- print(f" 状態: {self._state.status}")
- print(f" 現在のフェーズ: {self._state.current_phase}")
-
- print("\n 【フェーズ進捗】")
- for phase_num, phase_name in self.PHASES.items():
- status = self.get_phase_status(phase_num)
- if status == PhaseStatus.COMPLETED.value:
- icon = "✅"
- elif status == PhaseStatus.IN_PROGRESS.value:
- icon = "🔄"
- elif status == PhaseStatus.FAILED.value:
- icon = "❌"
- else:
- icon = "⬜"
- print(f" {icon} Phase {phase_num}: {phase_name}")
-
- if self._state.portfolio.get("published"):
- print("\n 【ポートフォリオ】")
- print(f" URL: {self._state.portfolio.get('app_url')}")
- print(f" コミット: {self._state.portfolio.get('commit_hash')}")
-
- if self._state.modifications:
- print(f"\n 【修正履歴】: {len(self._state.modifications)} 回")
- for mod in self._state.modifications:
- status_icon = "✅" if mod.get("status") == "completed" else "🔄"
- print(f" {status_icon} #{mod['iteration']}: {mod['feedback'][:30]}...")
-
- print("\n" + "=" * 60)
-
- def get_next_action_prompt(self) -> str:
- """次のアクションを示すプロンプトを生成"""
- if self._state is None:
- return "ワークフローを開始してください。"
-
- status = self._state.status
-
- if status == WorkflowStatus.NOT_STARTED.value:
- return "Phase 1(計画)を開始してください。"
-
- elif status == WorkflowStatus.IN_PROGRESS.value:
- phase = self._state.current_phase
- phase_name = self.PHASES.get(phase, f"Phase {phase}")
- return f"Phase {phase}({phase_name})を続行してください。"
-
- elif status == WorkflowStatus.AWAITING_REVIEW.value:
- return """
-ユーザーレビュー待ちです。
-
-【ユーザーへ】
-公開されたアプリを確認してください:
-- URL: {app_url}
-
-修正が必要な場合は、修正内容を教えてください。
-問題なければ「完了」と伝えてください。
-
-【修正が必要な場合のコマンド例】
-「修正依頼: ボタンの色を青から緑に変更してください」
-""".format(app_url=self._state.portfolio.get("app_url", "(未公開)"))
-
- elif status == WorkflowStatus.MODIFICATION_REQUESTED.value:
- mod = self.get_pending_modification()
- if mod:
- return f"""
-修正依頼があります:
-- フィードバック: {mod.get('feedback')}
-- 再実行するフェーズ: {mod.get('phases_to_rerun')}
-
-Phase 7(修正ワークフロー)を開始してください。
-"""
- return "修正ワークフローを開始してください。"
-
- elif status == WorkflowStatus.MODIFICATION_IN_PROGRESS.value:
- mod = self.get_pending_modification()
- phases = mod.get("phases_to_rerun", []) if mod else []
- return f"修正ワークフロー実行中です。再実行フェーズ: {phases}"
-
- elif status == WorkflowStatus.COMPLETED.value:
- return "ワークフローは完了しています。新しいプロジェクトを開始しますか?"
-
- elif status == WorkflowStatus.FAILED.value:
- return "ワークフローが失敗しています。エラーを確認して再実行してください。"
-
- return "状態を確認してください。"
-
-
-# ===========================================
-# 便利関数
-# ===========================================
-
-def get_state_manager(project_path: str = None) -> WorkflowStateManager:
- """状態マネージャーを取得"""
- if project_path is None:
- project_path = os.getcwd()
- return WorkflowStateManager(project_path)
-
-
-def print_workflow_status(project_path: str = None):
- """ワークフロー状態を表示"""
- manager = get_state_manager(project_path)
- manager.print_status_report()
-
-
-def get_next_action(project_path: str = None) -> str:
- """次のアクションを取得"""
- manager = get_state_manager(project_path)
- return manager.get_next_action_prompt()
-
-
-if __name__ == "__main__":
- import argparse
-
- parser = argparse.ArgumentParser(description="ワークフロー状態管理")
- parser.add_argument("--path", default=".", help="プロジェクトパス")
- parser.add_argument("--status", action="store_true", help="状態を表示")
- parser.add_argument("--next", action="store_true", help="次のアクションを表示")
- parser.add_argument("--init", help="新規ワークフローを初期化(プロジェクト名を指定)")
- args = parser.parse_args()
-
- manager = get_state_manager(args.path)
-
- if args.init:
- manager.initialize(args.init)
- print(f"✅ ワークフローを初期化しました: {args.init}")
-
- if args.status:
- manager.print_status_report()
-
- if args.next:
- print(manager.get_next_action_prompt())
diff --git a/update_and_publish.sh b/update_and_publish.sh
deleted file mode 100755
index b3c43cf..0000000
--- a/update_and_publish.sh
+++ /dev/null
@@ -1,100 +0,0 @@
-#!/bin/bash
-# コード修正後の自動公開スクリプト
-# DELIVERYフォルダのみをGitHubに公開
-
-set -e
-
-# カラー定義
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-RED='\033[0;31m'
-NC='\033[0m'
-
-echo -e "${BLUE}================================${NC}"
-echo -e "${BLUE}🔄 コード更新・公開プロセス${NC}"
-echo -e "${BLUE}================================${NC}"
-echo ""
-
-# 現在のディレクトリチェック
-CURRENT_DIR=$(pwd)
-if [[ ! "$CURRENT_DIR" == *"worktrees/mission"* ]]; then
- echo -e "${YELLOW}⚠️ worktreeディレクトリで実行してください${NC}"
- echo -e " 例: cd worktrees/mission-v1"
- exit 1
-fi
-
-# プロジェクトルート取得
-PROJECT_ROOT=$(dirname $(dirname $CURRENT_DIR))
-
-echo -e "${GREEN}📍 作業ディレクトリ: $CURRENT_DIR${NC}"
-echo -e "${GREEN}📍 プロジェクトルート: $PROJECT_ROOT${NC}"
-echo ""
-
-# 1. テスト実行
-echo -e "${YELLOW}1. テスト実行中...${NC}"
-if [ -f "package.json" ]; then
- if npm test; then
- echo -e "${GREEN}✅ テスト成功${NC}"
- else
- echo -e "${RED}❌ テスト失敗。修正してください${NC}"
- exit 1
- fi
-elif [ -f "test_app.py" ] || [ -f "tests.py" ]; then
- if python3 -m pytest; then
- echo -e "${GREEN}✅ テスト成功${NC}"
- else
- echo -e "${RED}❌ テスト失敗。修正してください${NC}"
- exit 1
- fi
-else
- echo -e "${YELLOW}⚠️ テストファイルが見つかりません。スキップします${NC}"
-fi
-echo ""
-
-# 2. DELIVERY作成
-echo -e "${YELLOW}2. DELIVERYフォルダ作成中...${NC}"
-if python3 $PROJECT_ROOT/src/delivery_organizer.py; then
- echo -e "${GREEN}✅ DELIVERY作成成功${NC}"
-else
- echo -e "${RED}❌ DELIVERY作成失敗${NC}"
- exit 1
-fi
-echo ""
-
-# 3. 公開タイプ選択
-echo -e "${YELLOW}3. 公開タイプを選択してください:${NC}"
-echo " [1] 新規公開(初回)"
-echo " [2] 更新公開(2回目以降)"
-read -p "選択 (1/2): " PUBLISH_TYPE
-
-if [ "$PUBLISH_TYPE" = "2" ]; then
- UPDATE_FLAG="--update"
- echo -e "${GREEN}✓ 更新として公開します${NC}"
-else
- UPDATE_FLAG=""
- echo -e "${GREEN}✓ 新規として公開します${NC}"
-fi
-echo ""
-
-# 4. GitHub公開
-echo -e "${YELLOW}4. GitHubに公開中...${NC}"
-echo -e "${BLUE}対象: https://github.com/sohei-t/ai-agent-portfolio${NC}"
-
-if python3 $PROJECT_ROOT/src/github_publisher_v8.py $UPDATE_FLAG; then
- echo -e "${GREEN}✅ GitHub公開成功${NC}"
-else
- echo -e "${RED}❌ GitHub公開失敗${NC}"
- exit 1
-fi
-
-echo ""
-echo -e "${GREEN}================================${NC}"
-echo -e "${GREEN}✅ 更新・公開完了!${NC}"
-echo -e "${GREEN}================================${NC}"
-echo ""
-echo -e "${BLUE}次のステップ:${NC}"
-echo "1. GitHub Pagesで動作確認"
-echo "2. READMEのリンク確認"
-echo "3. about.htmlの表示確認"
-echo ""
\ No newline at end of file
diff --git a/validate_before_merge.sh b/validate_before_merge.sh
deleted file mode 100755
index 6f55b82..0000000
--- a/validate_before_merge.sh
+++ /dev/null
@@ -1,174 +0,0 @@
-#!/bin/bash
-
-# validate_before_merge.sh
-# マージ前の自動検証スクリプト
-
-set -e # エラーが発生したら即座に終了
-
-# 色付きの出力用
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-NC='\033[0m' # No Color
-
-# ログ関数
-log_success() {
- echo -e "${GREEN}✅ $1${NC}"
-}
-
-log_error() {
- echo -e "${RED}❌ $1${NC}"
-}
-
-log_warning() {
- echo -e "${YELLOW}⚠️ $1${NC}"
-}
-
-# Pythonアプリの検証
-validate_python_app() {
- local app_file=$1
- local validation_passed=true
-
- echo "======================================"
- echo "🔍 マージ前検証開始: $app_file"
- echo "======================================"
-
- # 1. Pythonバージョン確認
- echo -n "Python環境チェック... "
- if python3 --version > /dev/null 2>&1; then
- log_success "OK ($(python3 --version))"
- else
- log_error "Python3が見つかりません"
- return 1
- fi
-
- # 2. 構文チェック
- echo -n "構文チェック... "
- if python3 -m py_compile "$app_file" 2>/dev/null; then
- log_success "OK"
- else
- log_error "構文エラーがあります"
- python3 -m py_compile "$app_file"
- return 1
- fi
-
- # 3. 必要なモジュールのチェック(GUIアプリの場合)
- if grep -q "tkinter" "$app_file"; then
- echo -n "tkinterモジュールチェック... "
- if python3 -c "import tkinter" 2>/dev/null; then
- log_success "OK"
- else
- log_error "tkinterが利用できません"
- log_warning "macOSの場合: brew install python-tk"
- log_warning "Ubuntuの場合: sudo apt-get install python3-tk"
- return 1
- fi
- fi
-
- # 4. インポートテスト
- echo -n "インポートテスト... "
- local module_name="${app_file%.py}"
- if python3 -c "import $module_name" 2>/dev/null; then
- log_success "OK"
- else
- log_warning "インポートテストをスキップ(メイン実行ファイル)"
- fi
-
- # 5. 起動テスト(GUIアプリ用)
- echo -n "起動テスト... "
-
- # ヘッドレス環境チェック
- if [ -z "$DISPLAY" ] && ! [ -e /tmp/.X11-unix ]; then
- log_warning "GUI環境なし - 起動テストをスキップ"
- else
- # タイムアウト付きで起動テスト
- timeout 2 python3 "$app_file" > /dev/null 2>&1 &
- local pid=$!
- sleep 1
-
- if ps -p $pid > /dev/null 2>&1; then
- log_success "OK (プロセス起動確認)"
- kill $pid 2>/dev/null || true
- elif [ $? -eq 124 ]; then
- log_success "OK (タイムアウトによる正常終了)"
- else
- log_warning "起動テスト不確定"
- fi
- fi
-
- # 6. requirements.txt チェック
- if [ -f "requirements.txt" ]; then
- echo -n "依存関係チェック... "
- local missing_deps=false
-
- while IFS= read -r package || [ -n "$package" ]; do
- # コメント行をスキップ
- [[ "$package" =~ ^#.*$ ]] && continue
- [[ -z "$package" ]] && continue
-
- # パッケージ名を抽出(バージョン指定を除く)
- pkg_name=$(echo "$package" | sed 's/[<>=!].*//')
-
- if ! python3 -c "import $pkg_name" 2>/dev/null; then
- log_error "Missing: $pkg_name"
- missing_deps=true
- fi
- done < requirements.txt
-
- if [ "$missing_deps" = false ]; then
- log_success "OK"
- else
- log_warning "pip install -r requirements.txt を実行してください"
- fi
- fi
-
- echo "======================================"
- if [ "$validation_passed" = true ]; then
- log_success "全検証完了 - マージ可能です!"
- return 0
- else
- log_error "検証失敗 - 修正が必要です"
- return 1
- fi
-}
-
-# メイン処理
-main() {
- if [ $# -eq 0 ]; then
- echo "Usage: $0 "
- echo "Example: $0 calculator.py"
- exit 1
- fi
-
- local file_to_validate=$1
-
- if [ ! -f "$file_to_validate" ]; then
- log_error "ファイルが見つかりません: $file_to_validate"
- exit 1
- fi
-
- # Pythonファイルの検証
- if [[ "$file_to_validate" == *.py ]]; then
- validate_python_app "$file_to_validate"
- exit_code=$?
- else
- log_warning "現在はPythonファイルのみサポートしています"
- exit_code=1
- fi
-
- # 検証結果に応じた終了コード
- if [ $exit_code -eq 0 ]; then
- echo ""
- echo "🎉 マージの準備ができました!"
- echo "次のコマンドでマージできます:"
- echo " git merge "
- else
- echo ""
- echo "⚠️ マージ前に問題を修正してください"
- fi
-
- exit $exit_code
-}
-
-# スクリプト実行
-main "$@"
\ No newline at end of file
diff --git a/verify_completion.sh b/verify_completion.sh
deleted file mode 100755
index b42ad75..0000000
--- a/verify_completion.sh
+++ /dev/null
@@ -1,171 +0,0 @@
-#!/bin/bash
-
-# 完成物検証スクリプト
-# フェーズ5の成果物が正しく生成されているか確認
-
-set -e
-
-# カラー定義
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-CYAN='\033[0;36m'
-NC='\033[0m' # No Color
-
-echo -e "${BLUE}================================${NC}"
-echo -e "${BLUE}📋 完成物検証${NC}"
-echo -e "${BLUE}================================${NC}"
-
-# 引数チェック
-if [ $# -eq 0 ]; then
- echo -e "${YELLOW}使用方法: $0 ${NC}"
- echo -e "${YELLOW}例: $0 ./worktrees/mission-todo-app${NC}"
- exit 1
-fi
-
-WORKTREE_PATH="$1"
-
-if [ ! -d "$WORKTREE_PATH" ]; then
- echo -e "${RED}❌ ディレクトリが存在しません: $WORKTREE_PATH${NC}"
- exit 1
-fi
-
-cd "$WORKTREE_PATH"
-
-echo -e "\n${CYAN}プロジェクトディレクトリ: $(pwd)${NC}"
-
-# 検証結果を記録
-PASS_COUNT=0
-FAIL_COUNT=0
-TOTAL_COUNT=0
-
-# 検証関数
-check_file() {
- local file="$1"
- local description="$2"
- local optional="$3"
-
- ((TOTAL_COUNT++))
-
- if [ -f "$file" ]; then
- echo -e "${GREEN}✅ $description${NC}"
- echo -e " ファイル: $file"
- echo -e " サイズ: $(ls -lh "$file" | awk '{print $5}')"
- ((PASS_COUNT++))
- else
- if [ "$optional" = "optional" ]; then
- echo -e "${YELLOW}⚠️ $description (オプション)${NC}"
- ((PASS_COUNT++))
- else
- echo -e "${RED}❌ $description${NC}"
- echo -e " 期待されるファイル: $file"
- ((FAIL_COUNT++))
- fi
- fi
-}
-
-# フェーズ1: 要件定義・計画
-echo -e "\n${CYAN}=== フェーズ1: 要件定義・計画 ===${NC}"
-check_file "REQUIREMENTS.md" "要件定義書"
-check_file "WBS.json" "WBS(作業分解構造)"
-
-# フェーズ2: テスト設計
-echo -e "\n${CYAN}=== フェーズ2: テスト設計 ===${NC}"
-if [ -d "tests" ]; then
- TEST_FILES=$(find tests -name "*.test.js" -o -name "*.test.ts" -o -name "*.py" 2>/dev/null | head -5)
- if [ -n "$TEST_FILES" ]; then
- echo -e "${GREEN}✅ テストファイル${NC}"
- echo "$TEST_FILES" | while read -r file; do
- echo -e " - $file"
- done
- ((PASS_COUNT++))
- else
- echo -e "${RED}❌ テストファイルが見つかりません${NC}"
- ((FAIL_COUNT++))
- fi
- ((TOTAL_COUNT++))
-else
- echo -e "${YELLOW}⚠️ tests ディレクトリが存在しません${NC}"
-fi
-
-# フェーズ3: 実装
-echo -e "\n${CYAN}=== フェーズ3: 実装 ===${NC}"
-check_file "package.json" "Node.js プロジェクト設定" "optional"
-check_file "requirements.txt" "Python プロジェクト設定" "optional"
-
-if [ -d "src" ] || [ -d "app" ] || [ -d "public" ]; then
- echo -e "${GREEN}✅ ソースコードディレクトリ${NC}"
- ((PASS_COUNT++))
-else
- echo -e "${RED}❌ ソースコードディレクトリが見つかりません${NC}"
- ((FAIL_COUNT++))
-fi
-((TOTAL_COUNT++))
-
-# フェーズ5: 完成処理(最重要)
-echo -e "\n${CYAN}=== フェーズ5: 完成処理(最重要)===${NC}"
-check_file "README.md" "README"
-check_file "about.html" "プロジェクト解説ページ"
-check_file "audio_script.txt" "音声スクリプト"
-check_file "generate_audio_gcp.js" "音声生成スクリプト"
-check_file "explanation.mp3" "解説音声" "optional"
-check_file "launch_app.command" "起動スクリプト"
-
-# package.json に音声生成の依存関係があるかチェック
-if [ -f "package.json" ]; then
- if grep -q "@google-cloud/text-to-speech" package.json; then
- echo -e "${GREEN}✅ 音声生成依存関係${NC}"
- ((PASS_COUNT++))
- else
- echo -e "${YELLOW}⚠️ @google-cloud/text-to-speech が package.json にありません${NC}"
- fi
- ((TOTAL_COUNT++))
-fi
-
-# launch_app.command の実行権限チェック
-if [ -f "launch_app.command" ]; then
- if [ -x "launch_app.command" ]; then
- echo -e "${GREEN}✅ launch_app.command 実行権限${NC}"
- ((PASS_COUNT++))
- else
- echo -e "${YELLOW}⚠️ launch_app.command に実行権限がありません${NC}"
- echo -e " 修正: chmod +x launch_app.command"
- fi
- ((TOTAL_COUNT++))
-fi
-
-# 結果サマリー
-echo -e "\n${BLUE}================================${NC}"
-echo -e "${BLUE}📊 検証結果サマリー${NC}"
-echo -e "${BLUE}================================${NC}"
-
-echo -e "\n検証項目: ${TOTAL_COUNT}個"
-echo -e "${GREEN}成功: ${PASS_COUNT}個${NC}"
-echo -e "${RED}失敗: ${FAIL_COUNT}個${NC}"
-
-if [ $FAIL_COUNT -eq 0 ]; then
- echo -e "\n${GREEN}🎉 すべての検証に合格しました!${NC}"
-else
- echo -e "\n${YELLOW}⚠️ 未生成のファイルがあります${NC}"
- echo -e "\n${CYAN}修正方法:${NC}"
-
- if [ ! -f "about.html" ] || [ ! -f "audio_script.txt" ] || [ ! -f "generate_audio_gcp.js" ]; then
- echo -e "1. Documenterエージェントを実行:"
- echo -e " ${YELLOW}python3 ~/Desktop/git-worktree-agent/src/documenter_agent.py${NC}"
- fi
-
- if [ ! -f "explanation.mp3" ] && [ -f "generate_audio_gcp.js" ]; then
- echo -e "\n2. 音声を生成:"
- echo -e " ${YELLOW}export GOOGLE_APPLICATION_CREDENTIALS=\"\$HOME/Desktop/git-worktree-agent/credentials/gcp-workflow-key.json\"${NC}"
- echo -e " ${YELLOW}npm install @google-cloud/text-to-speech${NC}"
- echo -e " ${YELLOW}node generate_audio_gcp.js${NC}"
- fi
-
- if [ ! -f "launch_app.command" ]; then
- echo -e "\n3. 起動スクリプトを生成:"
- echo -e " ${YELLOW}python3 ~/Desktop/git-worktree-agent/src/launcher_generator.py${NC}"
- fi
-fi
-
-echo -e "\n${GREEN}検証完了${NC}"
\ No newline at end of file
diff --git a/worktrees/.gitkeep b/worktrees/.gitkeep
deleted file mode 100755
index e69de29..0000000