@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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate-phase.ts の単体テスト
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
|
+
import { validatePhase } from '../validate-phase.js';
|
|
8
|
+
|
|
9
|
+
// fsモジュールのモック
|
|
10
|
+
vi.mock('fs', () => ({
|
|
11
|
+
existsSync: vi.fn(),
|
|
12
|
+
readFileSync: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// project-metaのモック
|
|
16
|
+
vi.mock('../utils/project-meta.js', () => ({
|
|
17
|
+
loadProjectMeta: vi.fn(() => ({
|
|
18
|
+
projectId: 'test-project',
|
|
19
|
+
projectName: 'テストプロジェクト',
|
|
20
|
+
jiraProjectKey: 'TEST'
|
|
21
|
+
}))
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe('validatePhase', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('基本動作', () => {
|
|
30
|
+
it('すべての必須項目が揃っている場合、validationが成功する', () => {
|
|
31
|
+
// Arrange: requirementsフェーズを代表例としてテスト
|
|
32
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
33
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
|
|
34
|
+
confluence: {
|
|
35
|
+
spaceKey: 'TEST',
|
|
36
|
+
requirementsPageId: '12345'
|
|
37
|
+
},
|
|
38
|
+
milestones: {
|
|
39
|
+
requirements: {
|
|
40
|
+
completed: true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
const result = validatePhase('test-feature', 'requirements');
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(result.valid).toBe(true);
|
|
50
|
+
expect(result.errors).toHaveLength(0);
|
|
51
|
+
expect(result.phase).toBe('requirements');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('Confluenceページが作成されていない場合、エラーを返す', () => {
|
|
55
|
+
// Arrange
|
|
56
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
57
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
|
|
58
|
+
confluence: {
|
|
59
|
+
spaceKey: 'TEST'
|
|
60
|
+
// requirementsPageId が存在しない
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
const result = validatePhase('test-feature', 'requirements');
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(result.valid).toBe(false);
|
|
69
|
+
expect(result.errors).toContain('❌ Confluenceページ(要件定義)が作成されていません');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('フェーズ固有の検証', () => {
|
|
74
|
+
it('designフェーズ: 前提条件(requirements完了)をチェックする', () => {
|
|
75
|
+
// Arrange
|
|
76
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
77
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
|
|
78
|
+
confluence: {
|
|
79
|
+
designPageId: '67890'
|
|
80
|
+
},
|
|
81
|
+
milestones: {
|
|
82
|
+
requirements: {
|
|
83
|
+
completed: false // 前提条件が満たされていない
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
// Act
|
|
89
|
+
const result = validatePhase('test-feature', 'design');
|
|
90
|
+
|
|
91
|
+
// Assert
|
|
92
|
+
expect(result.valid).toBe(false);
|
|
93
|
+
expect(result.errors).toContain('❌ 要件定義が完了していません(前提条件)');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('tasksフェーズ: JIRA Epic/Story作成をチェックする', () => {
|
|
97
|
+
// Arrange
|
|
98
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
99
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
100
|
+
if (String(path).includes('tasks.md')) {
|
|
101
|
+
return '11/06(木)Day 1';
|
|
102
|
+
}
|
|
103
|
+
return JSON.stringify({
|
|
104
|
+
milestones: {
|
|
105
|
+
design: {
|
|
106
|
+
completed: true
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
jira: {
|
|
110
|
+
// epicKey が存在しない(重要なチェック)
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Act
|
|
116
|
+
const result = validatePhase('test-feature', 'tasks');
|
|
117
|
+
|
|
118
|
+
// Assert
|
|
119
|
+
expect(result.valid).toBe(false);
|
|
120
|
+
expect(result.errors).toContain('❌ JIRA Epicが作成されていません');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('tasksフェーズ: 営業日表記をチェックする(重要な独自機能)', () => {
|
|
124
|
+
// Arrange
|
|
125
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
126
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
127
|
+
if (String(path).includes('tasks.md')) {
|
|
128
|
+
return 'タスク一覧(曜日表記なし)'; // 営業日表記がない
|
|
129
|
+
}
|
|
130
|
+
return JSON.stringify({
|
|
131
|
+
milestones: {
|
|
132
|
+
design: {
|
|
133
|
+
completed: true
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
jira: {
|
|
137
|
+
epicKey: 'TEST-1',
|
|
138
|
+
stories: {
|
|
139
|
+
created: 5,
|
|
140
|
+
total: 5
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Act
|
|
147
|
+
const result = validatePhase('test-feature', 'tasks');
|
|
148
|
+
|
|
149
|
+
// Assert
|
|
150
|
+
expect(result.valid).toBe(true); // 警告だけなのでvalid
|
|
151
|
+
expect(result.warnings).toContain('⚠️ tasks.mdに曜日表記(月、火、水...)が含まれていません');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('エッジケース', () => {
|
|
156
|
+
it('spec.jsonが存在しない場合、エラーを返す', () => {
|
|
157
|
+
// Arrange
|
|
158
|
+
// requirements.mdは存在するが、spec.jsonは存在しない
|
|
159
|
+
vi.mocked(existsSync).mockImplementation((path: string | Buffer | URL) => {
|
|
160
|
+
const pathStr = typeof path === 'string' ? path : path instanceof URL ? path.pathname : path.toString();
|
|
161
|
+
if (pathStr.includes('requirements.md')) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (pathStr.includes('spec.json')) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Act
|
|
171
|
+
const result = validatePhase('test-feature', 'requirements');
|
|
172
|
+
|
|
173
|
+
// Assert: エラーをスローせず、errors配列にエラーを含めて返す
|
|
174
|
+
expect(result.valid).toBe(false);
|
|
175
|
+
expect(result.errors).toContainEqual(expect.stringContaining('spec.json読み込みエラー'));
|
|
176
|
+
expect(result.errors).toContainEqual(expect.stringContaining('spec.json not found'));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('不正なフェーズ名の場合、エラーをスローする', () => {
|
|
180
|
+
// Act & Assert
|
|
181
|
+
expect(() => validatePhase('test-feature', 'invalid' as any)).toThrow('Unknown phase: invalid');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 設定スキーマ定義
|
|
3
|
+
* Zodを使用して設定ファイルの型安全性を保証
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Confluence階層構造のモード
|
|
10
|
+
*/
|
|
11
|
+
export const ConfluenceHierarchyModeSchema = z.enum(['simple', 'nested']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Confluenceページ作成粒度
|
|
15
|
+
*/
|
|
16
|
+
export const ConfluencePageCreationGranularitySchema = z.enum([
|
|
17
|
+
'single',
|
|
18
|
+
'by-section',
|
|
19
|
+
'by-hierarchy',
|
|
20
|
+
'manual'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Confluence階層構造設定(by-hierarchyまたはmanualの場合)
|
|
25
|
+
*/
|
|
26
|
+
export const ConfluenceHierarchyStructureSchema = z.object({
|
|
27
|
+
mode: ConfluenceHierarchyModeSchema.optional(),
|
|
28
|
+
parentPageTitle: z.string().optional(),
|
|
29
|
+
createDocTypeParents: z.boolean().optional(),
|
|
30
|
+
structure: z.record(
|
|
31
|
+
z.string(),
|
|
32
|
+
z.object({
|
|
33
|
+
parent: z.string().optional(),
|
|
34
|
+
title: z.string().optional(),
|
|
35
|
+
children: z.array(
|
|
36
|
+
z.object({
|
|
37
|
+
section: z.string(),
|
|
38
|
+
title: z.string()
|
|
39
|
+
})
|
|
40
|
+
).optional(),
|
|
41
|
+
pages: z.array(
|
|
42
|
+
z.object({
|
|
43
|
+
title: z.string(),
|
|
44
|
+
sections: z.array(z.string()),
|
|
45
|
+
labels: z.array(z.string()).optional()
|
|
46
|
+
})
|
|
47
|
+
).optional()
|
|
48
|
+
})
|
|
49
|
+
).optional()
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Confluence設定スキーマ
|
|
54
|
+
*/
|
|
55
|
+
export const ConfluenceConfigSchema = z.object({
|
|
56
|
+
pageCreationGranularity: ConfluencePageCreationGranularitySchema.default('single'),
|
|
57
|
+
pageTitleFormat: z.string().default('[{projectName}] {featureName} {docTypeLabel}'),
|
|
58
|
+
autoLabels: z.array(z.string()).default(['{projectLabel}', '{docType}', '{featureName}', 'github-sync']),
|
|
59
|
+
spaces: z.object({
|
|
60
|
+
requirements: z.string().optional(),
|
|
61
|
+
design: z.string().optional(),
|
|
62
|
+
tasks: z.string().optional()
|
|
63
|
+
}).optional(),
|
|
64
|
+
hierarchy: ConfluenceHierarchyStructureSchema.optional()
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* JIRA Story作成粒度
|
|
69
|
+
*/
|
|
70
|
+
export const JiraStoryCreationGranularitySchema = z.enum([
|
|
71
|
+
'all',
|
|
72
|
+
'by-phase',
|
|
73
|
+
'selected-phases'
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* JIRA Story Points設定
|
|
78
|
+
*/
|
|
79
|
+
export const JiraStoryPointsSchema = z.enum(['auto', 'manual', 'disabled']);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* JIRA設定スキーマ
|
|
83
|
+
*/
|
|
84
|
+
export const JiraConfigSchema = z.object({
|
|
85
|
+
storyCreationGranularity: JiraStoryCreationGranularitySchema.default('all'),
|
|
86
|
+
createEpic: z.boolean().default(true),
|
|
87
|
+
storyPoints: JiraStoryPointsSchema.default('auto'),
|
|
88
|
+
autoLabels: z.array(z.string()).default(['{projectLabel}', '{featureName}', '{phaseLabel}']),
|
|
89
|
+
issueTypes: z.object({
|
|
90
|
+
epic: z.string().default('Epic'),
|
|
91
|
+
story: z.string().nullish().default(null), // null | undefined | string
|
|
92
|
+
subtask: z.string().nullish().default(null) // null | undefined | string
|
|
93
|
+
}).optional(),
|
|
94
|
+
selectedPhases: z.array(z.string()).optional()
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* ワークフロー設定スキーマ
|
|
99
|
+
*/
|
|
100
|
+
export const WorkflowConfigSchema = z.object({
|
|
101
|
+
enabledPhases: z.array(z.string()).default(['requirements', 'design', 'tasks']),
|
|
102
|
+
approvalGates: z.object({
|
|
103
|
+
requirements: z.array(z.string()).optional(),
|
|
104
|
+
design: z.array(z.string()).optional(),
|
|
105
|
+
release: z.array(z.string()).optional()
|
|
106
|
+
}).optional()
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 全体設定スキーマ
|
|
111
|
+
*/
|
|
112
|
+
export const AppConfigSchema = z.object({
|
|
113
|
+
confluence: ConfluenceConfigSchema.optional(),
|
|
114
|
+
jira: JiraConfigSchema.optional(),
|
|
115
|
+
workflow: WorkflowConfigSchema.optional()
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 設定の型定義
|
|
120
|
+
*/
|
|
121
|
+
export type ConfluenceHierarchyMode = z.infer<typeof ConfluenceHierarchyModeSchema>;
|
|
122
|
+
export type ConfluencePageCreationGranularity = z.infer<typeof ConfluencePageCreationGranularitySchema>;
|
|
123
|
+
export type ConfluenceHierarchyStructure = z.infer<typeof ConfluenceHierarchyStructureSchema>;
|
|
124
|
+
export type ConfluenceConfig = z.infer<typeof ConfluenceConfigSchema>;
|
|
125
|
+
export type JiraStoryCreationGranularity = z.infer<typeof JiraStoryCreationGranularitySchema>;
|
|
126
|
+
export type JiraStoryPoints = z.infer<typeof JiraStoryPointsSchema>;
|
|
127
|
+
export type JiraConfig = z.infer<typeof JiraConfigSchema>;
|
|
128
|
+
export type WorkflowConfig = z.infer<typeof WorkflowConfigSchema>;
|
|
129
|
+
export type AppConfig = z.infer<typeof AppConfigSchema>;
|
|
130
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"confluence": {
|
|
3
|
+
"pageCreationGranularity": "single",
|
|
4
|
+
"pageTitleFormat": "[{projectName}] {featureName} {docTypeLabel}",
|
|
5
|
+
"autoLabels": [
|
|
6
|
+
"{projectLabel}",
|
|
7
|
+
"{docType}",
|
|
8
|
+
"{featureName}",
|
|
9
|
+
"github-sync"
|
|
10
|
+
],
|
|
11
|
+
"spaces": {
|
|
12
|
+
"requirements": "${CONFLUENCE_PRD_SPACE}",
|
|
13
|
+
"design": "${CONFLUENCE_PRD_SPACE}",
|
|
14
|
+
"tasks": "${CONFLUENCE_PRD_SPACE}"
|
|
15
|
+
},
|
|
16
|
+
"hierarchy": {
|
|
17
|
+
"enabled": false
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"jira": {
|
|
21
|
+
"storyCreationGranularity": "all",
|
|
22
|
+
"createEpic": true,
|
|
23
|
+
"storyPoints": "auto",
|
|
24
|
+
"autoLabels": [
|
|
25
|
+
"{projectLabel}",
|
|
26
|
+
"{featureName}",
|
|
27
|
+
"{phaseLabel}"
|
|
28
|
+
],
|
|
29
|
+
"issueTypes": {
|
|
30
|
+
"epic": "Epic",
|
|
31
|
+
"story": null,
|
|
32
|
+
"subtask": null
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"workflow": {
|
|
36
|
+
"enabledPhases": [
|
|
37
|
+
"requirements",
|
|
38
|
+
"design",
|
|
39
|
+
"tasks"
|
|
40
|
+
],
|
|
41
|
+
"approvalGates": {
|
|
42
|
+
"requirements": [
|
|
43
|
+
"pm",
|
|
44
|
+
"director"
|
|
45
|
+
],
|
|
46
|
+
"design": [
|
|
47
|
+
"architect",
|
|
48
|
+
"director"
|
|
49
|
+
],
|
|
50
|
+
"release": [
|
|
51
|
+
"sm",
|
|
52
|
+
"director"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|