@sk8metal/michi-cli 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +60 -24
- package/dist/scripts/__tests__/validate-phase.test.d.ts +5 -0
- package/dist/scripts/__tests__/validate-phase.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/validate-phase.test.js +162 -0
- package/dist/scripts/__tests__/validate-phase.test.js.map +1 -0
- package/dist/scripts/utils/__tests__/config-validator.test.d.ts +5 -0
- package/dist/scripts/utils/__tests__/config-validator.test.d.ts.map +1 -0
- package/dist/scripts/utils/__tests__/config-validator.test.js +247 -0
- package/dist/scripts/utils/__tests__/config-validator.test.js.map +1 -0
- package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts +5 -0
- package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts.map +1 -0
- package/dist/scripts/utils/__tests__/feature-name-validator.test.js +106 -0
- package/dist/scripts/utils/__tests__/feature-name-validator.test.js.map +1 -0
- package/dist/scripts/utils/config-loader.js +1 -1
- package/dist/scripts/utils/config-loader.js.map +1 -1
- package/dist/scripts/utils/confluence-hierarchy.d.ts.map +1 -1
- package/dist/scripts/utils/confluence-hierarchy.js +2 -1
- package/dist/scripts/utils/confluence-hierarchy.js.map +1 -1
- package/dist/src/__tests__/cli.test.d.ts +5 -0
- package/dist/src/__tests__/cli.test.d.ts.map +1 -0
- package/dist/src/__tests__/cli.test.js +58 -0
- package/dist/src/__tests__/cli.test.js.map +1 -0
- package/dist/src/cli.js +0 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +29 -0
- package/dist/vitest.config.js.map +1 -0
- package/docs/setup.md +1 -1
- package/package.json +8 -4
- package/scripts/__tests__/README.md +101 -0
- package/scripts/__tests__/validate-phase.test.ts +185 -0
- package/scripts/config/config-schema.ts +130 -0
- package/scripts/config/default-config.json +57 -0
- package/scripts/config-interactive.ts +494 -0
- package/scripts/confluence-sync.ts +503 -0
- package/scripts/create-project.ts +293 -0
- package/scripts/jira-sync.ts +644 -0
- package/scripts/list-projects.ts +85 -0
- package/scripts/markdown-to-confluence.ts +161 -0
- package/scripts/multi-project-estimate.ts +255 -0
- package/scripts/phase-runner.ts +303 -0
- package/scripts/pr-automation.ts +67 -0
- package/scripts/pre-flight-check.ts +285 -0
- package/scripts/resource-dashboard.ts +124 -0
- package/scripts/setup-env.sh +52 -0
- package/scripts/setup-existing-project.ts +381 -0
- package/scripts/setup-existing.sh +145 -0
- package/scripts/utils/__tests__/config-validator.test.ts +302 -0
- package/scripts/utils/__tests__/feature-name-validator.test.ts +129 -0
- package/scripts/utils/config-loader.ts +326 -0
- package/scripts/utils/config-validator.ts +347 -0
- package/scripts/utils/confluence-hierarchy.ts +855 -0
- package/scripts/utils/feature-name-validator.ts +135 -0
- package/scripts/utils/project-meta.ts +69 -0
- package/scripts/validate-phase.ts +279 -0
- package/scripts/workflow-orchestrator.ts +178 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* プロジェクト一覧ツール
|
|
3
|
+
* 全リポジトリのプロジェクト情報を表示
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Octokit } from '@octokit/rest';
|
|
7
|
+
import { config } from 'dotenv';
|
|
8
|
+
import axios from 'axios';
|
|
9
|
+
|
|
10
|
+
config();
|
|
11
|
+
|
|
12
|
+
interface ProjectInfo {
|
|
13
|
+
name: string;
|
|
14
|
+
projectId: string;
|
|
15
|
+
status: string;
|
|
16
|
+
jiraKey: string;
|
|
17
|
+
team: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function listProjects(): Promise<void> {
|
|
21
|
+
const token = process.env.GITHUB_TOKEN;
|
|
22
|
+
const org = process.env.GITHUB_ORG;
|
|
23
|
+
|
|
24
|
+
if (!token || !org) {
|
|
25
|
+
throw new Error('Missing GitHub credentials');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const octokit = new Octokit({ auth: token });
|
|
29
|
+
|
|
30
|
+
console.log(`Fetching projects for organization: ${org}`);
|
|
31
|
+
|
|
32
|
+
const { data: repos } = await octokit.repos.listForOrg({ org });
|
|
33
|
+
|
|
34
|
+
const projects: ProjectInfo[] = [];
|
|
35
|
+
|
|
36
|
+
for (const repo of repos) {
|
|
37
|
+
try {
|
|
38
|
+
// .kiro/project.json を取得
|
|
39
|
+
const { data } = await octokit.repos.getContent({
|
|
40
|
+
owner: org,
|
|
41
|
+
repo: repo.name,
|
|
42
|
+
path: '.kiro/project.json'
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if ('content' in data) {
|
|
46
|
+
const content = Buffer.from(data.content, 'base64').toString('utf-8');
|
|
47
|
+
const projectMeta = JSON.parse(content);
|
|
48
|
+
|
|
49
|
+
projects.push({
|
|
50
|
+
name: projectMeta.projectName,
|
|
51
|
+
projectId: projectMeta.projectId,
|
|
52
|
+
status: projectMeta.status,
|
|
53
|
+
jiraKey: projectMeta.jiraProjectKey,
|
|
54
|
+
team: projectMeta.team
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// .kiro/project.json が存在しない場合はスキップ
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('\n📋 プロジェクト一覧:\n');
|
|
64
|
+
console.log('| プロジェクト | ID | ステータス | JIRA | チーム |');
|
|
65
|
+
console.log('|------------|-------|----------|------|--------|');
|
|
66
|
+
|
|
67
|
+
for (const project of projects) {
|
|
68
|
+
console.log(`| ${project.name} | ${project.projectId} | ${project.status} | ${project.jiraKey} | ${project.team.join(', ')} |`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`\n合計: ${projects.length} プロジェクト`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// CLI実行
|
|
75
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
76
|
+
listProjects()
|
|
77
|
+
.then(() => process.exit(0))
|
|
78
|
+
.catch((error) => {
|
|
79
|
+
console.error('❌ Failed:', error.message);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { listProjects };
|
|
85
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → Confluence Storage Format 変換
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import MarkdownIt from 'markdown-it';
|
|
6
|
+
|
|
7
|
+
const md = new MarkdownIt({
|
|
8
|
+
html: true,
|
|
9
|
+
breaks: true,
|
|
10
|
+
linkify: true
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Markdown を Confluence Storage Format (HTML) に変換
|
|
15
|
+
*/
|
|
16
|
+
export function convertMarkdownToConfluence(markdown: string): string {
|
|
17
|
+
// MarkdownIt でHTMLに変換
|
|
18
|
+
let html = md.render(markdown);
|
|
19
|
+
|
|
20
|
+
// Confluence固有の変換
|
|
21
|
+
html = convertCodeBlocks(html);
|
|
22
|
+
html = convertTables(html);
|
|
23
|
+
html = convertInfoBoxes(html);
|
|
24
|
+
|
|
25
|
+
return html;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* コードブロックをConfluenceマクロに変換
|
|
30
|
+
*/
|
|
31
|
+
function convertCodeBlocks(html: string): string {
|
|
32
|
+
// <pre><code class="language-xxx">...</code></pre>
|
|
33
|
+
// → <ac:structured-macro ac:name="code"><ac:parameter ac:name="language">xxx</ac:parameter>...</ac:structured-macro>
|
|
34
|
+
|
|
35
|
+
return html.replace(
|
|
36
|
+
/<pre><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g,
|
|
37
|
+
(match, lang, code) => {
|
|
38
|
+
const decodedCode = decodeHtmlEntities(code);
|
|
39
|
+
return `<ac:structured-macro ac:name="code">
|
|
40
|
+
<ac:parameter ac:name="language">${lang}</ac:parameter>
|
|
41
|
+
<ac:plain-text-body><![CDATA[${decodedCode}]]></ac:plain-text-body>
|
|
42
|
+
</ac:structured-macro>`;
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* テーブルをConfluence形式に変換(そのままHTMLでOK)
|
|
49
|
+
*/
|
|
50
|
+
function convertTables(html: string): string {
|
|
51
|
+
// HTMLテーブルはConfluenceでもサポートされているのでそのまま
|
|
52
|
+
return html;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 特殊なブロック(> で始まる引用など)をConfluence infoマクロに変換
|
|
57
|
+
*/
|
|
58
|
+
function convertInfoBoxes(html: string): string {
|
|
59
|
+
// <blockquote>...</blockquote> → <ac:structured-macro ac:name="info">
|
|
60
|
+
let transformed = html.replace(
|
|
61
|
+
/<blockquote>\s*<p><strong>(.*?)<\/strong>:\s*([\s\S]*?)<\/p>\s*<\/blockquote>/g,
|
|
62
|
+
(match, title, content) => {
|
|
63
|
+
return `<ac:structured-macro ac:name="info">
|
|
64
|
+
<ac:parameter ac:name="title">${title}</ac:parameter>
|
|
65
|
+
<ac:rich-text-body>
|
|
66
|
+
<p>${content}</p>
|
|
67
|
+
</ac:rich-text-body>
|
|
68
|
+
</ac:structured-macro>`;
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// 通常のblockquoteもinfoマクロに
|
|
73
|
+
transformed = transformed.replace(
|
|
74
|
+
/<blockquote>([\s\S]*?)<\/blockquote>/g,
|
|
75
|
+
(match, content) => {
|
|
76
|
+
return `<ac:structured-macro ac:name="info">
|
|
77
|
+
<ac:rich-text-body>
|
|
78
|
+
${content}
|
|
79
|
+
</ac:rich-text-body>
|
|
80
|
+
</ac:structured-macro>`;
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return transformed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* HTMLエンティティをデコード
|
|
89
|
+
*/
|
|
90
|
+
function decodeHtmlEntities(text: string): string {
|
|
91
|
+
const entities: Record<string, string> = {
|
|
92
|
+
'<': '<',
|
|
93
|
+
'>': '>',
|
|
94
|
+
'&': '&',
|
|
95
|
+
'"': '"',
|
|
96
|
+
''': "'",
|
|
97
|
+
' ': ' '
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return text.replace(/&[a-z]+;|&#\d+;/g, (entity) => {
|
|
101
|
+
return entities[entity] || entity;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Confluenceページテンプレートを生成
|
|
107
|
+
*/
|
|
108
|
+
export interface ConfluencePageOptions {
|
|
109
|
+
title: string;
|
|
110
|
+
githubUrl: string;
|
|
111
|
+
content: string;
|
|
112
|
+
approvers?: string[];
|
|
113
|
+
projectName?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createConfluencePage(options: ConfluencePageOptions): string {
|
|
117
|
+
const { title, githubUrl, content, approvers = ['企画', '部長'], projectName } = options;
|
|
118
|
+
|
|
119
|
+
const approversList = approvers.map(a => a.startsWith('@') ? a : `@${a}`).join(',');
|
|
120
|
+
|
|
121
|
+
return `
|
|
122
|
+
<ac:structured-macro ac:name="info">
|
|
123
|
+
<ac:parameter ac:name="title">GitHub連携</ac:parameter>
|
|
124
|
+
<ac:rich-text-body>
|
|
125
|
+
<p>📄 最新版は <a href="${githubUrl}">GitHub</a> で管理</p>
|
|
126
|
+
<p>編集はGitHubで行い、自動同期されます</p>
|
|
127
|
+
${projectName ? `<p><strong>プロジェクト</strong>: ${projectName}</p>` : ''}
|
|
128
|
+
</ac:rich-text-body>
|
|
129
|
+
</ac:structured-macro>
|
|
130
|
+
|
|
131
|
+
<hr/>
|
|
132
|
+
|
|
133
|
+
${content}
|
|
134
|
+
|
|
135
|
+
<hr/>
|
|
136
|
+
|
|
137
|
+
<ac:structured-macro ac:name="page-properties">
|
|
138
|
+
<ac:parameter ac:name="approval">${approversList}</ac:parameter>
|
|
139
|
+
<ac:parameter ac:name="status">レビュー待ち</ac:parameter>
|
|
140
|
+
</ac:structured-macro>
|
|
141
|
+
`.trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// CLI実行用
|
|
145
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
146
|
+
const { readFileSync } = await import('fs');
|
|
147
|
+
const { resolve } = await import('path');
|
|
148
|
+
|
|
149
|
+
const args = process.argv.slice(2);
|
|
150
|
+
if (args.length === 0) {
|
|
151
|
+
console.error('Usage: tsx markdown-to-confluence.ts <markdown-file>');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const markdownFile = resolve(args[0]);
|
|
156
|
+
const markdown = readFileSync(markdownFile, 'utf-8');
|
|
157
|
+
const confluenceHtml = convertMarkdownToConfluence(markdown);
|
|
158
|
+
|
|
159
|
+
console.log(confluenceHtml);
|
|
160
|
+
}
|
|
161
|
+
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* マルチプロジェクト見積もり集計
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Octokit } from '@octokit/rest';
|
|
6
|
+
import { config } from 'dotenv';
|
|
7
|
+
import ExcelJS from 'exceljs';
|
|
8
|
+
import { resolve, join, dirname } from 'path';
|
|
9
|
+
import { writeFileSync, mkdirSync, unlinkSync, readFileSync } from 'fs';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
import { mkdir } from 'fs/promises';
|
|
12
|
+
|
|
13
|
+
config();
|
|
14
|
+
|
|
15
|
+
// EstimateData型定義(estimate-generator.tsから統合)
|
|
16
|
+
export interface EstimateData {
|
|
17
|
+
feature: string;
|
|
18
|
+
tasks: TaskEstimate[];
|
|
19
|
+
totalDays: number;
|
|
20
|
+
totalPoints: number;
|
|
21
|
+
risks: RiskEstimate[];
|
|
22
|
+
optimistic: number;
|
|
23
|
+
standard: number;
|
|
24
|
+
pessimistic: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface TaskEstimate {
|
|
28
|
+
name: string;
|
|
29
|
+
days: number;
|
|
30
|
+
assignee: string;
|
|
31
|
+
notes?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface RiskEstimate {
|
|
35
|
+
risk: string;
|
|
36
|
+
impact: number;
|
|
37
|
+
mitigation: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* design.mdから見積もりを抽出(estimate-generator.tsから統合)
|
|
42
|
+
*/
|
|
43
|
+
function parseEstimateFromDesign(designPath: string): EstimateData {
|
|
44
|
+
const content = readFileSync(designPath, 'utf-8');
|
|
45
|
+
|
|
46
|
+
const tasks: TaskEstimate[] = [];
|
|
47
|
+
let totalDays = 0;
|
|
48
|
+
|
|
49
|
+
// 見積もりテーブルを正規表現で抽出
|
|
50
|
+
const tableRegex = /\|\s*([^|]+)\s*\|\s*(\d+(?:\.\d+)?)\s*\|\s*([^|]+)\s*\|/g;
|
|
51
|
+
let match;
|
|
52
|
+
|
|
53
|
+
while ((match = tableRegex.exec(content)) !== null) {
|
|
54
|
+
const [, name, daysStr, assignee] = match;
|
|
55
|
+
const days = parseFloat(daysStr);
|
|
56
|
+
|
|
57
|
+
if (!isNaN(days) && name.trim() !== 'タスク' && name.trim() !== '**合計**') {
|
|
58
|
+
tasks.push({
|
|
59
|
+
name: name.trim(),
|
|
60
|
+
days,
|
|
61
|
+
assignee: assignee.trim()
|
|
62
|
+
});
|
|
63
|
+
totalDays += days;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const risks: RiskEstimate[] = [
|
|
68
|
+
{ risk: '技術的課題', impact: 5, mitigation: 'プロトタイプ検証' },
|
|
69
|
+
{ risk: '要件変更', impact: 3, mitigation: 'バッファ確保' }
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const riskTotal = risks.reduce((sum, r) => sum + r.impact, 0);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
feature: 'Unknown',
|
|
76
|
+
tasks,
|
|
77
|
+
totalDays,
|
|
78
|
+
totalPoints: Math.ceil(totalDays / 0.5),
|
|
79
|
+
risks,
|
|
80
|
+
optimistic: totalDays,
|
|
81
|
+
standard: totalDays + riskTotal,
|
|
82
|
+
pessimistic: Math.ceil(totalDays * 1.5)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Excel出力(excel-sync.tsから統合)
|
|
88
|
+
*/
|
|
89
|
+
async function exportToExcel(
|
|
90
|
+
estimates: EstimateData[],
|
|
91
|
+
outputPath: string
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
const workbook = new ExcelJS.Workbook();
|
|
94
|
+
const worksheet = workbook.addWorksheet('見積もりサマリー');
|
|
95
|
+
|
|
96
|
+
worksheet.columns = [
|
|
97
|
+
{ header: 'プロジェクト/機能', key: 'feature', width: 30 },
|
|
98
|
+
{ header: '楽観的(人日)', key: 'optimistic', width: 15 },
|
|
99
|
+
{ header: '標準的(人日)', key: 'standard', width: 15 },
|
|
100
|
+
{ header: '悲観的(人日)', key: 'pessimistic', width: 15 },
|
|
101
|
+
{ header: 'ストーリーポイント', key: 'points', width: 18 },
|
|
102
|
+
{ header: 'タスク数', key: 'taskCount', width: 12 }
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
for (const estimate of estimates) {
|
|
106
|
+
worksheet.addRow({
|
|
107
|
+
feature: estimate.feature,
|
|
108
|
+
optimistic: estimate.optimistic,
|
|
109
|
+
standard: estimate.standard,
|
|
110
|
+
pessimistic: estimate.pessimistic,
|
|
111
|
+
points: estimate.totalPoints,
|
|
112
|
+
taskCount: estimate.tasks.length
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
worksheet.getRow(1).font = { bold: true };
|
|
117
|
+
worksheet.getRow(1).fill = {
|
|
118
|
+
type: 'pattern',
|
|
119
|
+
pattern: 'solid',
|
|
120
|
+
fgColor: { argb: 'FFE0E0E0' }
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const totalRow = worksheet.addRow({
|
|
124
|
+
feature: '合計',
|
|
125
|
+
optimistic: estimates.reduce((sum, e) => sum + e.optimistic, 0),
|
|
126
|
+
standard: estimates.reduce((sum, e) => sum + e.standard, 0),
|
|
127
|
+
pessimistic: estimates.reduce((sum, e) => sum + e.pessimistic, 0),
|
|
128
|
+
points: estimates.reduce((sum, e) => sum + e.totalPoints, 0),
|
|
129
|
+
taskCount: estimates.reduce((sum, e) => sum + e.tasks.length, 0)
|
|
130
|
+
});
|
|
131
|
+
totalRow.font = { bold: true };
|
|
132
|
+
|
|
133
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
134
|
+
await workbook.xlsx.writeFile(outputPath);
|
|
135
|
+
console.log(`✅ Excel file saved: ${outputPath}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* content文字列から見積もりを抽出(一時ファイル経由)
|
|
140
|
+
*/
|
|
141
|
+
function parseEstimateFromContent(content: string, featureName: string): EstimateData | null {
|
|
142
|
+
let tempFile: string | null = null;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// 一時ファイルに書き出してからパース
|
|
146
|
+
const tempDir = join(tmpdir(), 'michi-estimate');
|
|
147
|
+
mkdirSync(tempDir, { recursive: true });
|
|
148
|
+
tempFile = join(tempDir, `design-${Date.now()}.md`);
|
|
149
|
+
writeFileSync(tempFile, content);
|
|
150
|
+
|
|
151
|
+
const estimate = parseEstimateFromDesign(tempFile);
|
|
152
|
+
estimate.feature = featureName;
|
|
153
|
+
|
|
154
|
+
return estimate;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.warn(` ⚠️ Failed to parse estimate:`, error instanceof Error ? error.message : error);
|
|
157
|
+
return null;
|
|
158
|
+
} finally {
|
|
159
|
+
// 一時ファイルをクリーンアップ
|
|
160
|
+
if (tempFile) {
|
|
161
|
+
try {
|
|
162
|
+
unlinkSync(tempFile);
|
|
163
|
+
} catch {
|
|
164
|
+
// 削除失敗は無視
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function aggregateEstimates(): Promise<void> {
|
|
171
|
+
const token = process.env.GITHUB_TOKEN;
|
|
172
|
+
const org = process.env.GITHUB_ORG;
|
|
173
|
+
|
|
174
|
+
if (!token || !org) {
|
|
175
|
+
throw new Error('Missing GitHub credentials');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const octokit = new Octokit({ auth: token });
|
|
179
|
+
const estimates: EstimateData[] = [];
|
|
180
|
+
|
|
181
|
+
console.log('Aggregating estimates from all projects...');
|
|
182
|
+
|
|
183
|
+
const { data: repos } = await octokit.repos.listForOrg({ org });
|
|
184
|
+
|
|
185
|
+
for (const repo of repos) {
|
|
186
|
+
try {
|
|
187
|
+
// .kiro/specs/ ディレクトリを取得
|
|
188
|
+
const { data: specs } = await octokit.repos.getContent({
|
|
189
|
+
owner: org,
|
|
190
|
+
repo: repo.name,
|
|
191
|
+
path: '.kiro/specs'
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (Array.isArray(specs)) {
|
|
195
|
+
for (const spec of specs) {
|
|
196
|
+
if (spec.type === 'dir') {
|
|
197
|
+
// design.md を取得
|
|
198
|
+
try {
|
|
199
|
+
const { data: designFile } = await octokit.repos.getContent({
|
|
200
|
+
owner: org,
|
|
201
|
+
repo: repo.name,
|
|
202
|
+
path: `.kiro/specs/${spec.name}/design.md`
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if ('content' in designFile) {
|
|
206
|
+
const content = Buffer.from(designFile.content, 'base64').toString('utf-8');
|
|
207
|
+
|
|
208
|
+
// コンテンツから見積もりを抽出
|
|
209
|
+
try {
|
|
210
|
+
const estimateData = parseEstimateFromContent(content, `${repo.name}/${spec.name}`);
|
|
211
|
+
if (estimateData) {
|
|
212
|
+
estimates.push(estimateData);
|
|
213
|
+
console.log(` ✅ Parsed: ${repo.name}/${spec.name} (${estimateData.totalDays}日)`);
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.warn(` ⚠️ Failed to parse ${repo.name}/${spec.name}:`, error instanceof Error ? error.message : error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Excel出力
|
|
231
|
+
if (estimates.length > 0) {
|
|
232
|
+
const outputDir = resolve('./estimates');
|
|
233
|
+
mkdirSync(outputDir, { recursive: true });
|
|
234
|
+
const outputPath = join(outputDir, 'multi-project-estimates.xlsx');
|
|
235
|
+
await exportToExcel(estimates, outputPath);
|
|
236
|
+
} else {
|
|
237
|
+
console.log('No estimates found');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// CLI実行
|
|
242
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
243
|
+
aggregateEstimates()
|
|
244
|
+
.then(() => {
|
|
245
|
+
console.log('✅ Aggregation completed');
|
|
246
|
+
process.exit(0);
|
|
247
|
+
})
|
|
248
|
+
.catch((error) => {
|
|
249
|
+
console.error('❌ Failed:', error.message);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export { aggregateEstimates };
|
|
255
|
+
|