@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +60 -24
  3. package/dist/scripts/__tests__/validate-phase.test.d.ts +5 -0
  4. package/dist/scripts/__tests__/validate-phase.test.d.ts.map +1 -0
  5. package/dist/scripts/__tests__/validate-phase.test.js +162 -0
  6. package/dist/scripts/__tests__/validate-phase.test.js.map +1 -0
  7. package/dist/scripts/utils/__tests__/config-validator.test.d.ts +5 -0
  8. package/dist/scripts/utils/__tests__/config-validator.test.d.ts.map +1 -0
  9. package/dist/scripts/utils/__tests__/config-validator.test.js +247 -0
  10. package/dist/scripts/utils/__tests__/config-validator.test.js.map +1 -0
  11. package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts +5 -0
  12. package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts.map +1 -0
  13. package/dist/scripts/utils/__tests__/feature-name-validator.test.js +106 -0
  14. package/dist/scripts/utils/__tests__/feature-name-validator.test.js.map +1 -0
  15. package/dist/scripts/utils/config-loader.js +1 -1
  16. package/dist/scripts/utils/config-loader.js.map +1 -1
  17. package/dist/scripts/utils/confluence-hierarchy.d.ts.map +1 -1
  18. package/dist/scripts/utils/confluence-hierarchy.js +2 -1
  19. package/dist/scripts/utils/confluence-hierarchy.js.map +1 -1
  20. package/dist/src/__tests__/cli.test.d.ts +5 -0
  21. package/dist/src/__tests__/cli.test.d.ts.map +1 -0
  22. package/dist/src/__tests__/cli.test.js +58 -0
  23. package/dist/src/__tests__/cli.test.js.map +1 -0
  24. package/dist/src/cli.js +0 -0
  25. package/dist/vitest.config.d.ts +3 -0
  26. package/dist/vitest.config.d.ts.map +1 -0
  27. package/dist/vitest.config.js +29 -0
  28. package/dist/vitest.config.js.map +1 -0
  29. package/docs/setup.md +1 -1
  30. package/package.json +8 -4
  31. package/scripts/__tests__/README.md +101 -0
  32. package/scripts/__tests__/validate-phase.test.ts +185 -0
  33. package/scripts/config/config-schema.ts +130 -0
  34. package/scripts/config/default-config.json +57 -0
  35. package/scripts/config-interactive.ts +494 -0
  36. package/scripts/confluence-sync.ts +503 -0
  37. package/scripts/create-project.ts +293 -0
  38. package/scripts/jira-sync.ts +644 -0
  39. package/scripts/list-projects.ts +85 -0
  40. package/scripts/markdown-to-confluence.ts +161 -0
  41. package/scripts/multi-project-estimate.ts +255 -0
  42. package/scripts/phase-runner.ts +303 -0
  43. package/scripts/pr-automation.ts +67 -0
  44. package/scripts/pre-flight-check.ts +285 -0
  45. package/scripts/resource-dashboard.ts +124 -0
  46. package/scripts/setup-env.sh +52 -0
  47. package/scripts/setup-existing-project.ts +381 -0
  48. package/scripts/setup-existing.sh +145 -0
  49. package/scripts/utils/__tests__/config-validator.test.ts +302 -0
  50. package/scripts/utils/__tests__/feature-name-validator.test.ts +129 -0
  51. package/scripts/utils/config-loader.ts +326 -0
  52. package/scripts/utils/config-validator.ts +347 -0
  53. package/scripts/utils/confluence-hierarchy.ts +855 -0
  54. package/scripts/utils/feature-name-validator.ts +135 -0
  55. package/scripts/utils/project-meta.ts +69 -0
  56. package/scripts/validate-phase.ts +279 -0
  57. 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
+ '&lt;': '<',
93
+ '&gt;': '>',
94
+ '&amp;': '&',
95
+ '&quot;': '"',
96
+ '&#39;': "'",
97
+ '&nbsp;': ' '
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
+