@sk8metal/michi-cli 0.0.1 → 0.0.2
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 +24 -0
- package/README.md +24 -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/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/docs/setup.md +1 -1
- package/package.json +5 -3
- 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 +854 -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,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 機能名(feature)のバリデーション
|
|
3
|
+
* kebab-case形式を強制
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ValidationResult {
|
|
7
|
+
valid: boolean;
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* kebab-case形式の正規表現
|
|
13
|
+
* - 小文字の英数字で始まる
|
|
14
|
+
* - 小文字の英数字とハイフンのみ
|
|
15
|
+
* - 連続したハイフン不可
|
|
16
|
+
* - 先頭・末尾のハイフン不可
|
|
17
|
+
*/
|
|
18
|
+
const KEBAB_CASE_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* feature名がkebab-case形式か検証
|
|
22
|
+
*/
|
|
23
|
+
export function validateFeatureName(featureName: string): ValidationResult {
|
|
24
|
+
const errors: string[] = [];
|
|
25
|
+
|
|
26
|
+
// 空文字チェック
|
|
27
|
+
if (!featureName || featureName.trim().length === 0) {
|
|
28
|
+
errors.push('❌ feature名が空です');
|
|
29
|
+
return { valid: false, errors };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmed = featureName.trim();
|
|
33
|
+
|
|
34
|
+
// kebab-case形式チェック
|
|
35
|
+
if (!KEBAB_CASE_PATTERN.test(trimmed)) {
|
|
36
|
+
errors.push(`❌ feature名が無効な形式です: "${trimmed}"`);
|
|
37
|
+
errors.push(' 必須形式: 英語、kebab-case(ハイフン区切り)、小文字のみ');
|
|
38
|
+
|
|
39
|
+
// 具体的な問題を特定
|
|
40
|
+
if (/[A-Z]/.test(trimmed)) {
|
|
41
|
+
errors.push(' → 大文字が含まれています(小文字に変換してください)');
|
|
42
|
+
}
|
|
43
|
+
if (/[ぁ-んァ-ヶー一-龯]/.test(trimmed)) {
|
|
44
|
+
errors.push(' → 日本語が含まれています(英語に変換してください)');
|
|
45
|
+
}
|
|
46
|
+
if (/_/.test(trimmed)) {
|
|
47
|
+
errors.push(' → アンダースコア(_)が含まれています(ハイフン(-)を使用してください)');
|
|
48
|
+
}
|
|
49
|
+
if (/\s/.test(trimmed)) {
|
|
50
|
+
errors.push(' → スペースが含まれています(ハイフン(-)を使用してください)');
|
|
51
|
+
}
|
|
52
|
+
if (trimmed.startsWith('-') || trimmed.endsWith('-')) {
|
|
53
|
+
errors.push(' → 先頭または末尾にハイフンがあります(削除してください)');
|
|
54
|
+
}
|
|
55
|
+
if (/--/.test(trimmed)) {
|
|
56
|
+
errors.push(' → 連続したハイフンがあります(1つに減らしてください)');
|
|
57
|
+
}
|
|
58
|
+
if (/[^a-z0-9-]/.test(trimmed)) {
|
|
59
|
+
const invalidChars = trimmed.match(/[^a-z0-9-]/g);
|
|
60
|
+
errors.push(` → 使用できない文字が含まれています: ${[...new Set(invalidChars)].join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 修正案を提示
|
|
64
|
+
const suggestion = suggestFeatureName(trimmed);
|
|
65
|
+
if (suggestion && suggestion !== trimmed) {
|
|
66
|
+
errors.push(` 💡 修正案: "${suggestion}"`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 長さチェック(推奨)
|
|
71
|
+
if (trimmed.length > 50) {
|
|
72
|
+
errors.push(`⚠️ feature名が長すぎます(${trimmed.length}文字)。50文字以内を推奨`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 単語数チェック(推奨)
|
|
76
|
+
const wordCount = trimmed.split('-').length;
|
|
77
|
+
if (wordCount > 5) {
|
|
78
|
+
errors.push(`⚠️ 単語数が多すぎます(${wordCount}単語)。2-4単語を推奨`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
valid: errors.length === 0,
|
|
83
|
+
errors
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 不正なfeature名を修正案に変換
|
|
89
|
+
*/
|
|
90
|
+
export function suggestFeatureName(input: string): string {
|
|
91
|
+
return input
|
|
92
|
+
.toLowerCase() // 小文字に変換
|
|
93
|
+
.replace(/[ぁ-んァ-ヶー一-龯]/g, '') // 日本語削除
|
|
94
|
+
.replace(/\s+/g, '-') // スペース→ハイフン
|
|
95
|
+
.replace(/_+/g, '-') // アンダースコア→ハイフン
|
|
96
|
+
.replace(/[^a-z0-9-]/g, '') // 無効文字削除
|
|
97
|
+
.replace(/--+/g, '-') // 連続ハイフン→1つ
|
|
98
|
+
.replace(/^-+|-+$/g, ''); // 先頭・末尾ハイフン削除
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* feature名をバリデートし、エラー時は例外をスロー
|
|
103
|
+
*/
|
|
104
|
+
export function validateFeatureNameOrThrow(featureName: string): void {
|
|
105
|
+
const result = validateFeatureName(featureName);
|
|
106
|
+
|
|
107
|
+
if (!result.valid) {
|
|
108
|
+
const errorMessage = [
|
|
109
|
+
`Invalid feature name: "${featureName}"`,
|
|
110
|
+
'',
|
|
111
|
+
...result.errors,
|
|
112
|
+
'',
|
|
113
|
+
'ヘルプ: README.md#機能名(feature)の命名規則 を参照してください'
|
|
114
|
+
].join('\n');
|
|
115
|
+
|
|
116
|
+
throw new Error(errorMessage);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* feature名をバリデートし、警告を表示(続行可能)
|
|
122
|
+
*/
|
|
123
|
+
export function validateFeatureNameWithWarning(featureName: string): boolean {
|
|
124
|
+
const result = validateFeatureName(featureName);
|
|
125
|
+
|
|
126
|
+
if (!result.valid) {
|
|
127
|
+
console.error('\n⚠️ Feature name validation failed:');
|
|
128
|
+
result.errors.forEach(err => console.error(err));
|
|
129
|
+
console.error('\nヘルプ: README.md#機能名(feature)の命名規則 を参照してください\n');
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* プロジェクトメタデータ読み込みユーティリティ
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, existsSync } from 'fs';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
|
|
8
|
+
export interface ProjectMetadata {
|
|
9
|
+
projectId: string;
|
|
10
|
+
projectName: string;
|
|
11
|
+
jiraProjectKey: string;
|
|
12
|
+
confluenceLabels: string[];
|
|
13
|
+
status: 'active' | 'inactive' | 'completed';
|
|
14
|
+
team: string[];
|
|
15
|
+
stakeholders: string[];
|
|
16
|
+
repository: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* .kiro/project.json を読み込む
|
|
22
|
+
*/
|
|
23
|
+
export function loadProjectMeta(projectRoot: string = process.cwd()): ProjectMetadata {
|
|
24
|
+
const projectJsonPath = resolve(projectRoot, '.kiro/project.json');
|
|
25
|
+
|
|
26
|
+
if (!existsSync(projectJsonPath)) {
|
|
27
|
+
throw new Error(`Project metadata not found: ${projectJsonPath}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(projectJsonPath, 'utf-8');
|
|
32
|
+
const meta = JSON.parse(content) as ProjectMetadata;
|
|
33
|
+
|
|
34
|
+
// 必須フィールドのバリデーション
|
|
35
|
+
const requiredFields: (keyof ProjectMetadata)[] = [
|
|
36
|
+
'projectId',
|
|
37
|
+
'projectName',
|
|
38
|
+
'jiraProjectKey',
|
|
39
|
+
'confluenceLabels'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const field of requiredFields) {
|
|
43
|
+
if (!meta[field]) {
|
|
44
|
+
throw new Error(`Required field missing in project.json: ${field}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return meta;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof SyntaxError) {
|
|
51
|
+
throw new Error(`Invalid JSON in ${projectJsonPath}: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* プロジェクトメタデータを表示用にフォーマット
|
|
59
|
+
*/
|
|
60
|
+
export function formatProjectInfo(meta: ProjectMetadata): string {
|
|
61
|
+
return `
|
|
62
|
+
Project: ${meta.projectName} (${meta.projectId})
|
|
63
|
+
JIRA: ${meta.jiraProjectKey}
|
|
64
|
+
Labels: ${meta.confluenceLabels.join(', ')}
|
|
65
|
+
Status: ${meta.status}
|
|
66
|
+
Team: ${meta.team.join(', ')}
|
|
67
|
+
`.trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* フェーズ完了バリデーションスクリプト
|
|
3
|
+
* 各フェーズで必須項目が完了しているかチェック
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { loadProjectMeta } from './utils/project-meta.js';
|
|
9
|
+
import { validateFeatureName } from './utils/feature-name-validator.js';
|
|
10
|
+
|
|
11
|
+
type Phase = 'requirements' | 'design' | 'tasks';
|
|
12
|
+
|
|
13
|
+
interface ValidationResult {
|
|
14
|
+
phase: Phase;
|
|
15
|
+
valid: boolean;
|
|
16
|
+
errors: string[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* spec.jsonを読み込み
|
|
22
|
+
*/
|
|
23
|
+
function loadSpecJson(feature: string): any {
|
|
24
|
+
const specPath = join(process.cwd(), '.kiro', 'specs', feature, 'spec.json');
|
|
25
|
+
|
|
26
|
+
if (!existsSync(specPath)) {
|
|
27
|
+
throw new Error(`spec.json not found: ${specPath}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return JSON.parse(readFileSync(specPath, 'utf-8'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 要件定義フェーズのバリデーション
|
|
35
|
+
*/
|
|
36
|
+
function validateRequirements(feature: string): ValidationResult {
|
|
37
|
+
const errors: string[] = [];
|
|
38
|
+
const warnings: string[] = [];
|
|
39
|
+
|
|
40
|
+
// 0. feature名のバリデーション
|
|
41
|
+
const nameValidation = validateFeatureName(feature);
|
|
42
|
+
if (!nameValidation.valid) {
|
|
43
|
+
errors.push(...nameValidation.errors);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1. requirements.md存在チェック
|
|
47
|
+
const requirementsPath = join(process.cwd(), '.kiro', 'specs', feature, 'requirements.md');
|
|
48
|
+
if (!existsSync(requirementsPath)) {
|
|
49
|
+
errors.push('❌ requirements.md が作成されていません');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. spec.json読み込み
|
|
53
|
+
let spec: any;
|
|
54
|
+
try {
|
|
55
|
+
spec = loadSpecJson(feature);
|
|
56
|
+
} catch (error: any) {
|
|
57
|
+
errors.push(`❌ spec.json読み込みエラー: ${error.message}`);
|
|
58
|
+
return { phase: 'requirements', valid: false, errors, warnings };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Confluenceページ作成チェック(必須)
|
|
62
|
+
if (!spec.confluence?.requirementsPageId) {
|
|
63
|
+
errors.push('❌ Confluenceページ(要件定義)が作成されていません');
|
|
64
|
+
errors.push(' → 実行: npm run confluence:sync <feature> requirements');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. spec.jsonのconfluence情報チェック
|
|
68
|
+
if (!spec.confluence?.spaceKey) {
|
|
69
|
+
errors.push('❌ spec.jsonにconfluence.spaceKeyが記録されていません');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 5. マイルストーン更新チェック
|
|
73
|
+
if (!spec.milestones?.requirements?.completed) {
|
|
74
|
+
warnings.push('⚠️ spec.jsonのmilestones.requirements.completedがfalseです');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
phase: 'requirements',
|
|
79
|
+
valid: errors.length === 0,
|
|
80
|
+
errors,
|
|
81
|
+
warnings
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 設計フェーズのバリデーション
|
|
87
|
+
*/
|
|
88
|
+
function validateDesign(feature: string): ValidationResult {
|
|
89
|
+
const errors: string[] = [];
|
|
90
|
+
const warnings: string[] = [];
|
|
91
|
+
|
|
92
|
+
// 0. feature名のバリデーション
|
|
93
|
+
const nameValidation = validateFeatureName(feature);
|
|
94
|
+
if (!nameValidation.valid) {
|
|
95
|
+
errors.push(...nameValidation.errors);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 1. design.md存在チェック
|
|
99
|
+
const designPath = join(process.cwd(), '.kiro', 'specs', feature, 'design.md');
|
|
100
|
+
if (!existsSync(designPath)) {
|
|
101
|
+
errors.push('❌ design.md が作成されていません');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 2. spec.json読み込み
|
|
105
|
+
let spec: any;
|
|
106
|
+
try {
|
|
107
|
+
spec = loadSpecJson(feature);
|
|
108
|
+
} catch (error: any) {
|
|
109
|
+
errors.push(`❌ spec.json読み込みエラー: ${error.message}`);
|
|
110
|
+
return { phase: 'design', valid: false, errors, warnings };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 3. 前提: 要件定義完了チェック
|
|
114
|
+
if (!spec.milestones?.requirements?.completed) {
|
|
115
|
+
errors.push('❌ 要件定義が完了していません(前提条件)');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 4. Confluenceページ作成チェック(必須)
|
|
119
|
+
if (!spec.confluence?.designPageId) {
|
|
120
|
+
errors.push('❌ Confluenceページ(設計書)が作成されていません');
|
|
121
|
+
errors.push(' → 実行: npm run confluence:sync <feature> design');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 5. マイルストーン更新チェック
|
|
125
|
+
if (!spec.milestones?.design?.completed) {
|
|
126
|
+
warnings.push('⚠️ spec.jsonのmilestones.design.completedがfalseです');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
phase: 'design',
|
|
131
|
+
valid: errors.length === 0,
|
|
132
|
+
errors,
|
|
133
|
+
warnings
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* タスク分割フェーズのバリデーション
|
|
139
|
+
*/
|
|
140
|
+
function validateTasks(feature: string): ValidationResult {
|
|
141
|
+
const errors: string[] = [];
|
|
142
|
+
const warnings: string[] = [];
|
|
143
|
+
|
|
144
|
+
// 0. feature名のバリデーション
|
|
145
|
+
const nameValidation = validateFeatureName(feature);
|
|
146
|
+
if (!nameValidation.valid) {
|
|
147
|
+
errors.push(...nameValidation.errors);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 1. tasks.md存在チェック
|
|
151
|
+
const tasksPath = join(process.cwd(), '.kiro', 'specs', feature, 'tasks.md');
|
|
152
|
+
if (!existsSync(tasksPath)) {
|
|
153
|
+
errors.push('❌ tasks.md が作成されていません');
|
|
154
|
+
} else {
|
|
155
|
+
// 営業日表記チェック
|
|
156
|
+
const tasksContent = readFileSync(tasksPath, 'utf-8');
|
|
157
|
+
if (!tasksContent.includes('(月)') && !tasksContent.includes('(火)')) {
|
|
158
|
+
warnings.push('⚠️ tasks.mdに曜日表記(月、火、水...)が含まれていません');
|
|
159
|
+
}
|
|
160
|
+
if (!tasksContent.includes('Day 1') && !tasksContent.includes('Day1')) {
|
|
161
|
+
warnings.push('⚠️ tasks.mdに営業日カウント(Day 1, Day 2...)が含まれていません');
|
|
162
|
+
}
|
|
163
|
+
if (!tasksContent.includes('土日')) {
|
|
164
|
+
warnings.push('⚠️ tasks.mdに土日休みの明記がありません');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. spec.json読み込み
|
|
169
|
+
let spec: any;
|
|
170
|
+
try {
|
|
171
|
+
spec = loadSpecJson(feature);
|
|
172
|
+
} catch (error: any) {
|
|
173
|
+
errors.push(`❌ spec.json読み込みエラー: ${error.message}`);
|
|
174
|
+
return { phase: 'tasks', valid: false, errors, warnings };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 3. 前提: 設計完了チェック
|
|
178
|
+
if (!spec.milestones?.design?.completed) {
|
|
179
|
+
errors.push('❌ 設計が完了していません(前提条件)');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 4. JIRA Epic作成チェック(必須)
|
|
183
|
+
if (!spec.jira?.epicKey) {
|
|
184
|
+
errors.push('❌ JIRA Epicが作成されていません');
|
|
185
|
+
errors.push(' → 実行: npm run jira:sync <feature>');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 5. JIRA Story作成チェック(必須)
|
|
189
|
+
if (!spec.jira?.stories || spec.jira.stories.created === 0) {
|
|
190
|
+
errors.push('❌ JIRA Storyが1つも作成されていません');
|
|
191
|
+
errors.push(' → 実行: npm run jira:sync <feature>');
|
|
192
|
+
} else if (spec.jira.stories.created < spec.jira.stories.total) {
|
|
193
|
+
warnings.push(`⚠️ JIRA Storyが一部未作成: ${spec.jira.stories.created}/${spec.jira.stories.total}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 6. マイルストーン更新チェック
|
|
197
|
+
if (!spec.milestones?.tasks?.completed) {
|
|
198
|
+
warnings.push('⚠️ spec.jsonのmilestones.tasks.completedがfalseです');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
phase: 'tasks',
|
|
203
|
+
valid: errors.length === 0,
|
|
204
|
+
errors,
|
|
205
|
+
warnings
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* フェーズをバリデート
|
|
211
|
+
*/
|
|
212
|
+
export function validatePhase(feature: string, phase: Phase): ValidationResult {
|
|
213
|
+
console.log(`\n🔍 Validating phase: ${phase} for feature: ${feature}`);
|
|
214
|
+
|
|
215
|
+
let result: ValidationResult;
|
|
216
|
+
|
|
217
|
+
switch (phase) {
|
|
218
|
+
case 'requirements':
|
|
219
|
+
result = validateRequirements(feature);
|
|
220
|
+
break;
|
|
221
|
+
case 'design':
|
|
222
|
+
result = validateDesign(feature);
|
|
223
|
+
break;
|
|
224
|
+
case 'tasks':
|
|
225
|
+
result = validateTasks(feature);
|
|
226
|
+
break;
|
|
227
|
+
default:
|
|
228
|
+
throw new Error(`Unknown phase: ${phase}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 結果表示
|
|
232
|
+
console.log('\n📊 Validation Result:');
|
|
233
|
+
|
|
234
|
+
if (result.errors.length > 0) {
|
|
235
|
+
console.log('\n❌ エラー:');
|
|
236
|
+
result.errors.forEach(err => console.log(` ${err}`));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (result.warnings.length > 0) {
|
|
240
|
+
console.log('\n⚠️ 警告:');
|
|
241
|
+
result.warnings.forEach(warn => console.log(` ${warn}`));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (result.valid) {
|
|
245
|
+
console.log('\n✅ バリデーション成功: すべての必須項目が完了しています');
|
|
246
|
+
} else {
|
|
247
|
+
console.log('\n❌ バリデーション失敗: 上記のエラーを修正してください');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// CLI実行
|
|
254
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
255
|
+
const args = process.argv.slice(2);
|
|
256
|
+
|
|
257
|
+
if (args.length < 2) {
|
|
258
|
+
console.error('Usage: npm run validate:phase <feature> <phase>');
|
|
259
|
+
console.error('Example: npm run validate:phase calculator-app requirements');
|
|
260
|
+
console.error('Phases: requirements, design, tasks');
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const [feature, phase] = args;
|
|
265
|
+
|
|
266
|
+
if (!['requirements', 'design', 'tasks'].includes(phase)) {
|
|
267
|
+
console.error('Invalid phase. Must be: requirements, design, or tasks');
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const result = validatePhase(feature, phase as Phase);
|
|
273
|
+
process.exit(result.valid ? 0 : 1);
|
|
274
|
+
} catch (error: any) {
|
|
275
|
+
console.error(`\n❌ Validation error: ${error.message}`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ワークフローオーケストレーター
|
|
3
|
+
* AI開発フロー全体を統合実行
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { config } from 'dotenv';
|
|
7
|
+
import { loadProjectMeta } from './utils/project-meta.js';
|
|
8
|
+
import { syncToConfluence } from './confluence-sync.js';
|
|
9
|
+
import { syncTasksToJIRA } from './jira-sync.js';
|
|
10
|
+
import { createPR } from './pr-automation.js';
|
|
11
|
+
|
|
12
|
+
config();
|
|
13
|
+
|
|
14
|
+
export interface WorkflowConfig {
|
|
15
|
+
feature: string;
|
|
16
|
+
stages: WorkflowStage[];
|
|
17
|
+
approvalGates?: {
|
|
18
|
+
requirements?: string[];
|
|
19
|
+
design?: string[];
|
|
20
|
+
release?: string[];
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type WorkflowStage =
|
|
25
|
+
| 'requirements'
|
|
26
|
+
| 'design'
|
|
27
|
+
| 'tasks'
|
|
28
|
+
| 'implement'
|
|
29
|
+
| 'test'
|
|
30
|
+
| 'release';
|
|
31
|
+
|
|
32
|
+
export class WorkflowOrchestrator {
|
|
33
|
+
private config: WorkflowConfig;
|
|
34
|
+
|
|
35
|
+
constructor(config: WorkflowConfig) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* ワークフロー全体を実行
|
|
41
|
+
*/
|
|
42
|
+
async run(): Promise<void> {
|
|
43
|
+
console.log(`🚀 Starting workflow for: ${this.config.feature}`);
|
|
44
|
+
console.log(`Stages: ${this.config.stages.join(' → ')}`);
|
|
45
|
+
|
|
46
|
+
const projectMeta = loadProjectMeta();
|
|
47
|
+
console.log(`Project: ${projectMeta.projectName}`);
|
|
48
|
+
|
|
49
|
+
for (const stage of this.config.stages) {
|
|
50
|
+
console.log(`\n📋 Stage: ${stage}`);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await this.executeStage(stage);
|
|
54
|
+
|
|
55
|
+
// 承認ゲートチェック
|
|
56
|
+
if (this.hasApprovalGate(stage)) {
|
|
57
|
+
await this.waitForApproval(stage);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`✅ Stage completed: ${stage}`);
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
console.error(`❌ Stage failed: ${stage}`, error.message);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('\n🎉 Workflow completed successfully!');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 各ステージを実行
|
|
72
|
+
*/
|
|
73
|
+
private async executeStage(stage: WorkflowStage): Promise<void> {
|
|
74
|
+
switch (stage) {
|
|
75
|
+
case 'requirements':
|
|
76
|
+
console.log(' Syncing requirements to Confluence...');
|
|
77
|
+
await syncToConfluence(this.config.feature, 'requirements');
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case 'design':
|
|
81
|
+
console.log(' Syncing design to Confluence...');
|
|
82
|
+
await syncToConfluence(this.config.feature, 'design');
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case 'tasks':
|
|
86
|
+
console.log(' Creating JIRA tasks...');
|
|
87
|
+
await syncTasksToJIRA(this.config.feature);
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case 'implement':
|
|
91
|
+
console.log(' Implementation phase - manual step');
|
|
92
|
+
console.log(' Use: /kiro:spec-impl <feature> <tasks>');
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
case 'test':
|
|
96
|
+
console.log(' Test phase - execute tests');
|
|
97
|
+
// TODO: テスト実行とレポート生成
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case 'release':
|
|
101
|
+
console.log(' Release preparation');
|
|
102
|
+
// TODO: リリースノート生成とJIRA Release作成
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 承認ゲートがあるかチェック
|
|
109
|
+
*/
|
|
110
|
+
private hasApprovalGate(stage: WorkflowStage): boolean {
|
|
111
|
+
const gates = this.config.approvalGates;
|
|
112
|
+
if (!gates) return false;
|
|
113
|
+
|
|
114
|
+
const gateList =
|
|
115
|
+
stage === 'requirements' ? gates.requirements :
|
|
116
|
+
stage === 'design' ? gates.design :
|
|
117
|
+
stage === 'release' ? gates.release :
|
|
118
|
+
undefined;
|
|
119
|
+
|
|
120
|
+
return Array.isArray(gateList) && gateList.length > 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 承認を待つ
|
|
125
|
+
*/
|
|
126
|
+
private async waitForApproval(stage: WorkflowStage): Promise<void> {
|
|
127
|
+
console.log(`\n⏸️ Approval required for: ${stage}`);
|
|
128
|
+
|
|
129
|
+
const approvers = this.config.approvalGates?.[stage as keyof typeof this.config.approvalGates];
|
|
130
|
+
if (approvers) {
|
|
131
|
+
console.log(` Approvers: ${approvers.join(', ')}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(' ✅ Confluence で承認してください');
|
|
135
|
+
console.log(' ⏳ 承認完了後、次のステージに進みます');
|
|
136
|
+
|
|
137
|
+
// TODO: Confluence APIで承認状態をポーリング
|
|
138
|
+
// 現在は手動確認
|
|
139
|
+
console.log(' (手動で承認を確認してください)');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// CLI実行
|
|
144
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
145
|
+
const args = process.argv.slice(2);
|
|
146
|
+
|
|
147
|
+
if (args.length === 0) {
|
|
148
|
+
console.error('Usage: npm run workflow:run -- --feature <feature_name>');
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const featureIndex = args.indexOf('--feature');
|
|
153
|
+
const feature = featureIndex >= 0 ? args[featureIndex + 1] : undefined;
|
|
154
|
+
|
|
155
|
+
if (featureIndex === -1 || !feature) {
|
|
156
|
+
console.error('Usage: npm run workflow:run -- --feature <feature_name>');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const workflowConfig: WorkflowConfig = {
|
|
161
|
+
feature,
|
|
162
|
+
stages: ['requirements', 'design', 'tasks', 'implement', 'test', 'release'],
|
|
163
|
+
approvalGates: {
|
|
164
|
+
requirements: ['企画', '部長'],
|
|
165
|
+
design: ['アーキテクト', '部長'],
|
|
166
|
+
release: ['SM', '部長']
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const orchestrator = new WorkflowOrchestrator(workflowConfig);
|
|
171
|
+
|
|
172
|
+
orchestrator.run()
|
|
173
|
+
.then(() => process.exit(0))
|
|
174
|
+
.catch((error) => {
|
|
175
|
+
console.error('❌ Workflow failed:', error.message);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
|
178
|
+
}
|