@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,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confluence同期スクリプト
|
|
3
|
+
* GitHub の Markdown ファイルを Confluence に同期
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from 'fs';
|
|
7
|
+
import { resolve, join } from 'path';
|
|
8
|
+
import axios from 'axios';
|
|
9
|
+
import { config } from 'dotenv';
|
|
10
|
+
import { loadProjectMeta, type ProjectMetadata } from './utils/project-meta.js';
|
|
11
|
+
import { convertMarkdownToConfluence, createConfluencePage } from './markdown-to-confluence.js';
|
|
12
|
+
import { validateFeatureNameOrThrow } from './utils/feature-name-validator.js';
|
|
13
|
+
import { getConfig } from './utils/config-loader.js';
|
|
14
|
+
import { createPagesByGranularity } from './utils/confluence-hierarchy.js';
|
|
15
|
+
import { validateForConfluenceSync } from './utils/config-validator.js';
|
|
16
|
+
|
|
17
|
+
// 環境変数読み込み
|
|
18
|
+
config();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* リクエスト間のスリープ処理(レートリミット対策)
|
|
22
|
+
*/
|
|
23
|
+
function sleep(ms: number): Promise<void> {
|
|
24
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* リクエスト間の待機時間(ミリ秒)
|
|
29
|
+
* 環境変数 ATLASSIAN_REQUEST_DELAY で調整可能(デフォルト: 500ms)
|
|
30
|
+
*/
|
|
31
|
+
function getRequestDelay(): number {
|
|
32
|
+
return parseInt(process.env.ATLASSIAN_REQUEST_DELAY || '500', 10);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ConfluenceConfig {
|
|
36
|
+
url: string;
|
|
37
|
+
email: string;
|
|
38
|
+
apiToken: string;
|
|
39
|
+
space: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Confluence設定を環境変数から取得
|
|
44
|
+
*/
|
|
45
|
+
function getConfluenceConfig(): ConfluenceConfig {
|
|
46
|
+
const url = process.env.ATLASSIAN_URL;
|
|
47
|
+
const email = process.env.ATLASSIAN_EMAIL;
|
|
48
|
+
const apiToken = process.env.ATLASSIAN_API_TOKEN;
|
|
49
|
+
const space = process.env.CONFLUENCE_PRD_SPACE || 'PRD';
|
|
50
|
+
|
|
51
|
+
if (!url || !email || !apiToken) {
|
|
52
|
+
throw new Error('Missing Confluence credentials in .env file');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { url, email, apiToken, space };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Confluence REST API クライアント
|
|
60
|
+
*/
|
|
61
|
+
class ConfluenceClient {
|
|
62
|
+
private baseUrl: string;
|
|
63
|
+
private auth: string;
|
|
64
|
+
private requestDelay: number;
|
|
65
|
+
|
|
66
|
+
constructor(config: ConfluenceConfig) {
|
|
67
|
+
this.baseUrl = `${config.url}/wiki/rest/api`;
|
|
68
|
+
this.auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
|
|
69
|
+
this.requestDelay = getRequestDelay();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* ページを検索
|
|
74
|
+
* @param spaceKey スペースキー
|
|
75
|
+
* @param title ページタイトル
|
|
76
|
+
* @param parentId 親ページID(オプション)。指定された場合、その親ページの子ページのみを検索
|
|
77
|
+
*/
|
|
78
|
+
async searchPage(spaceKey: string, title: string, parentId?: string): Promise<any | null> {
|
|
79
|
+
// レートリミット対策: リクエスト前に待機
|
|
80
|
+
await sleep(this.requestDelay);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// 親ページIDが指定されている場合、CQLクエリを使用して親ページの子ページのみを検索
|
|
84
|
+
if (parentId) {
|
|
85
|
+
// CQLクエリ: スペース、タイトル、親ページIDで検索
|
|
86
|
+
// タイトル内の特殊文字をエスケープ
|
|
87
|
+
const escapedTitle = title.replace(/"/g, '\\"');
|
|
88
|
+
// ancestorの代わりにparentを使用(Confluence CQLの正しい構文)
|
|
89
|
+
const cql = `space = ${spaceKey} AND title = "${escapedTitle}" AND parent = ${parentId}`;
|
|
90
|
+
console.log(` CQL Query: ${cql}`);
|
|
91
|
+
|
|
92
|
+
const response = await axios.get(`${this.baseUrl}/content/search`, {
|
|
93
|
+
params: {
|
|
94
|
+
cql,
|
|
95
|
+
expand: 'version'
|
|
96
|
+
},
|
|
97
|
+
headers: {
|
|
98
|
+
'Authorization': `Basic ${this.auth}`,
|
|
99
|
+
'Content-Type': 'application/json'
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
console.log(` CQL Search results: ${response.data.results?.length || 0} pages found`);
|
|
104
|
+
|
|
105
|
+
if (response.data.results && response.data.results.length > 0) {
|
|
106
|
+
return response.data.results[0];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// CQLクエリで見つからない場合、従来の方法で検索(親ページIDでフィルタリング)
|
|
110
|
+
console.log(` Falling back to standard search (may find pages in different parent)`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 親ページIDが指定されていない場合、従来の方法で検索
|
|
115
|
+
const response = await axios.get(`${this.baseUrl}/content`, {
|
|
116
|
+
params: {
|
|
117
|
+
spaceKey,
|
|
118
|
+
title,
|
|
119
|
+
expand: 'version'
|
|
120
|
+
},
|
|
121
|
+
headers: {
|
|
122
|
+
'Authorization': `Basic ${this.auth}`,
|
|
123
|
+
'Content-Type': 'application/json'
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (response.data.results && response.data.results.length > 0) {
|
|
128
|
+
return response.data.results[0];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
} catch (error: any) {
|
|
133
|
+
// 404エラーは既存ページがないことを意味するので、nullを返す
|
|
134
|
+
if (error.response?.status === 404) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// その他のエラーは詳細をログ出力
|
|
139
|
+
console.error('Error searching page:', error.message);
|
|
140
|
+
if (error.response) {
|
|
141
|
+
console.error(' Status:', error.response.status);
|
|
142
|
+
console.error(' Data:', JSON.stringify(error.response.data, null, 2));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 404以外のエラーは再スロー(認証、権限、ネットワーク、サーバーエラーなど)
|
|
146
|
+
// エラーの詳細情報を含めて再スロー
|
|
147
|
+
if (error.response) {
|
|
148
|
+
// HTTPレスポンスがある場合(4xx/5xxエラー)
|
|
149
|
+
const enhancedError = new Error(
|
|
150
|
+
`Confluence API error: ${error.message} (status: ${error.response.status})`
|
|
151
|
+
);
|
|
152
|
+
(enhancedError as any).response = error.response;
|
|
153
|
+
throw enhancedError;
|
|
154
|
+
} else {
|
|
155
|
+
// ネットワークエラーなど、レスポンスがない場合
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* ページを作成
|
|
163
|
+
*/
|
|
164
|
+
async createPage(spaceKey: string, title: string, content: string, labels: string[] = [], parentId?: string): Promise<any> {
|
|
165
|
+
// レートリミット対策: リクエスト前に待機
|
|
166
|
+
await sleep(this.requestDelay);
|
|
167
|
+
|
|
168
|
+
const payload: any = {
|
|
169
|
+
type: 'page',
|
|
170
|
+
title,
|
|
171
|
+
space: { key: spaceKey },
|
|
172
|
+
body: {
|
|
173
|
+
storage: {
|
|
174
|
+
value: content,
|
|
175
|
+
representation: 'storage'
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
metadata: {
|
|
179
|
+
labels: labels.map(label => ({ name: label }))
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// 親ページが指定されている場合、ancestorsを追加
|
|
184
|
+
if (parentId) {
|
|
185
|
+
payload.ancestors = [{ id: parentId }];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const response = await axios.post(`${this.baseUrl}/content`, payload, {
|
|
189
|
+
headers: {
|
|
190
|
+
'Authorization': `Basic ${this.auth}`,
|
|
191
|
+
'Content-Type': 'application/json'
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return response.data;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 親ページの下に子ページを作成
|
|
200
|
+
*/
|
|
201
|
+
async createPageUnderParent(
|
|
202
|
+
spaceKey: string,
|
|
203
|
+
title: string,
|
|
204
|
+
content: string,
|
|
205
|
+
labels: string[] = [],
|
|
206
|
+
parentId: string
|
|
207
|
+
): Promise<any> {
|
|
208
|
+
return this.createPage(spaceKey, title, content, labels, parentId);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* ページを更新
|
|
213
|
+
*/
|
|
214
|
+
async updatePage(pageId: string, title: string, content: string, version: number): Promise<any> {
|
|
215
|
+
// レートリミット対策: リクエスト前に待機
|
|
216
|
+
await sleep(this.requestDelay);
|
|
217
|
+
|
|
218
|
+
const payload = {
|
|
219
|
+
version: { number: version + 1 },
|
|
220
|
+
title,
|
|
221
|
+
type: 'page',
|
|
222
|
+
body: {
|
|
223
|
+
storage: {
|
|
224
|
+
value: content,
|
|
225
|
+
representation: 'storage'
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const response = await axios.put(`${this.baseUrl}/content/${pageId}`, payload, {
|
|
231
|
+
headers: {
|
|
232
|
+
'Authorization': `Basic ${this.auth}`,
|
|
233
|
+
'Content-Type': 'application/json'
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return response.data;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* ページの親情報を取得
|
|
242
|
+
* @param pageId ページID
|
|
243
|
+
* @returns 親ページID(ルートページの場合はnull)
|
|
244
|
+
*/
|
|
245
|
+
async getPageParentId(pageId: string): Promise<string | null> {
|
|
246
|
+
// レートリミット対策: リクエスト前に待機
|
|
247
|
+
await sleep(this.requestDelay);
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const response = await axios.get(`${this.baseUrl}/content/${pageId}`, {
|
|
251
|
+
params: {
|
|
252
|
+
expand: 'ancestors'
|
|
253
|
+
},
|
|
254
|
+
headers: {
|
|
255
|
+
'Authorization': `Basic ${this.auth}`,
|
|
256
|
+
'Content-Type': 'application/json'
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ancestors配列の最後の要素が直接の親ページ
|
|
261
|
+
const ancestors = response.data.ancestors;
|
|
262
|
+
if (ancestors && ancestors.length > 0) {
|
|
263
|
+
return ancestors[ancestors.length - 1].id;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return null; // ルートページ
|
|
267
|
+
} catch (error: any) {
|
|
268
|
+
// 404エラーはページが存在しないことを意味する
|
|
269
|
+
if (error.response?.status === 404) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// その他のエラーは詳細をログ出力
|
|
274
|
+
console.error('Error getting page parent:', error.message);
|
|
275
|
+
if (error.response) {
|
|
276
|
+
console.error(' Status:', error.response.status);
|
|
277
|
+
console.error(' Data:', JSON.stringify(error.response.data, null, 2));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 404以外のエラーは再スロー
|
|
281
|
+
if (error.response) {
|
|
282
|
+
const enhancedError = new Error(
|
|
283
|
+
`Confluence API error: ${error.message} (status: ${error.response.status})`
|
|
284
|
+
);
|
|
285
|
+
(enhancedError as any).response = error.response;
|
|
286
|
+
throw enhancedError;
|
|
287
|
+
} else {
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* ページのラベルを追加
|
|
295
|
+
*/
|
|
296
|
+
async addLabels(pageId: string, labels: string[]): Promise<void> {
|
|
297
|
+
for (const label of labels) {
|
|
298
|
+
// レートリミット対策: リクエスト前に待機
|
|
299
|
+
await sleep(this.requestDelay);
|
|
300
|
+
|
|
301
|
+
await axios.post(
|
|
302
|
+
`${this.baseUrl}/content/${pageId}/label`,
|
|
303
|
+
[{ name: label }],
|
|
304
|
+
{
|
|
305
|
+
headers: {
|
|
306
|
+
'Authorization': `Basic ${this.auth}`,
|
|
307
|
+
'Content-Type': 'application/json'
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* spec.jsonを読み込む
|
|
317
|
+
* @param featureName 機能名
|
|
318
|
+
* @param projectRoot プロジェクトルート(デフォルト: process.cwd())
|
|
319
|
+
* @returns spec.jsonの内容、存在しない場合はnull
|
|
320
|
+
*/
|
|
321
|
+
function loadSpecJson(featureName: string, projectRoot: string = process.cwd()): any | null {
|
|
322
|
+
const specPath = resolve(projectRoot, `.kiro/specs/${featureName}/spec.json`);
|
|
323
|
+
|
|
324
|
+
if (!existsSync(specPath)) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const content = readFileSync(specPath, 'utf-8');
|
|
330
|
+
return JSON.parse(content);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.warn(`⚠️ Failed to load spec.json from ${specPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Markdownファイルを Confluence に同期
|
|
339
|
+
*/
|
|
340
|
+
async function syncToConfluence(
|
|
341
|
+
featureName: string,
|
|
342
|
+
docType: 'requirements' | 'design' | 'tasks' = 'requirements'
|
|
343
|
+
): Promise<string> {
|
|
344
|
+
console.log(`Syncing ${docType} for feature: ${featureName}`);
|
|
345
|
+
|
|
346
|
+
// feature名のバリデーション(必須)
|
|
347
|
+
validateFeatureNameOrThrow(featureName);
|
|
348
|
+
|
|
349
|
+
// 実行前の必須設定値チェック
|
|
350
|
+
const validation = validateForConfluenceSync(docType);
|
|
351
|
+
|
|
352
|
+
if (validation.info.length > 0) {
|
|
353
|
+
validation.info.forEach(msg => console.log(`ℹ️ ${msg}`));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (validation.warnings.length > 0) {
|
|
357
|
+
console.warn('⚠️ Warnings:');
|
|
358
|
+
validation.warnings.forEach(warning => console.warn(` ${warning}`));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (validation.errors.length > 0) {
|
|
362
|
+
console.error('❌ Configuration errors:');
|
|
363
|
+
validation.errors.forEach(error => console.error(` ${error}`));
|
|
364
|
+
const configPath = resolve('.kiro/config.json');
|
|
365
|
+
console.error(`\n設定ファイル: ${configPath}`);
|
|
366
|
+
throw new Error('Confluence同期に必要な設定値が不足しています。上記のエラーを確認して設定を修正してください。');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
console.log(`⏳ Request delay: ${getRequestDelay()}ms (set ATLASSIAN_REQUEST_DELAY to adjust)`);
|
|
370
|
+
|
|
371
|
+
// プロジェクトメタデータ読み込み
|
|
372
|
+
const projectMeta = loadProjectMeta();
|
|
373
|
+
console.log(`Project: ${projectMeta.projectName} (${projectMeta.projectId})`);
|
|
374
|
+
|
|
375
|
+
// 設定を読み込み
|
|
376
|
+
const appConfig = getConfig();
|
|
377
|
+
const confluenceConfig = appConfig.confluence || {
|
|
378
|
+
pageCreationGranularity: 'single',
|
|
379
|
+
pageTitleFormat: '[{projectName}] {featureName} {docTypeLabel}',
|
|
380
|
+
autoLabels: ['{projectLabel}', '{docType}', '{featureName}', 'github-sync']
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
console.log(`📋 Page creation granularity: ${confluenceConfig.pageCreationGranularity}`);
|
|
384
|
+
|
|
385
|
+
// 設定ソースのログ出力
|
|
386
|
+
if (confluenceConfig.spaces?.[docType]) {
|
|
387
|
+
console.log(`📝 Config source: config.json (spaces.${docType} = ${confluenceConfig.spaces[docType]})`);
|
|
388
|
+
} else if (process.env.CONFLUENCE_PRD_SPACE) {
|
|
389
|
+
console.log(`📝 Config source: environment variable (CONFLUENCE_PRD_SPACE = ${process.env.CONFLUENCE_PRD_SPACE})`);
|
|
390
|
+
} else {
|
|
391
|
+
console.log(`📝 Config source: default config`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Markdownファイル読み込み
|
|
395
|
+
const markdownPath = resolve(`.kiro/specs/${featureName}/${docType}.md`);
|
|
396
|
+
const markdown = readFileSync(markdownPath, 'utf-8');
|
|
397
|
+
|
|
398
|
+
// GitHub URL生成
|
|
399
|
+
const githubUrl = `${projectMeta.repository}/blob/main/.kiro/specs/${featureName}/${docType}.md`;
|
|
400
|
+
|
|
401
|
+
// Confluence設定を取得
|
|
402
|
+
const confluenceApiConfig = getConfluenceConfig();
|
|
403
|
+
|
|
404
|
+
// spec.jsonを読み込み
|
|
405
|
+
const specJson = loadSpecJson(featureName);
|
|
406
|
+
|
|
407
|
+
// スペースキーを決定(優先順位: spec.json → config.json → 環境変数/デフォルト)
|
|
408
|
+
let spaceKey: string;
|
|
409
|
+
let spaceKeySource: string;
|
|
410
|
+
|
|
411
|
+
if (specJson?.confluence?.spaceKey) {
|
|
412
|
+
spaceKey = specJson.confluence.spaceKey;
|
|
413
|
+
spaceKeySource = 'spec.json';
|
|
414
|
+
} else if (confluenceConfig.spaces?.[docType]) {
|
|
415
|
+
spaceKey = confluenceConfig.spaces[docType];
|
|
416
|
+
spaceKeySource = 'config.json';
|
|
417
|
+
} else {
|
|
418
|
+
// confluenceApiConfig.space は常に存在(getConfluenceConfig()で 'PRD' がデフォルト)
|
|
419
|
+
spaceKey = confluenceApiConfig.space;
|
|
420
|
+
spaceKeySource = process.env.CONFLUENCE_PRD_SPACE ? 'environment variable' : 'default from config';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
console.log(`📌 Using Confluence space: ${spaceKey} (source: ${spaceKeySource})`);
|
|
424
|
+
|
|
425
|
+
// Confluenceクライアント初期化
|
|
426
|
+
const client = new ConfluenceClient(confluenceApiConfig);
|
|
427
|
+
|
|
428
|
+
// 階層構造に応じてページを作成
|
|
429
|
+
const result = await createPagesByGranularity(
|
|
430
|
+
client,
|
|
431
|
+
spaceKey,
|
|
432
|
+
markdown,
|
|
433
|
+
confluenceConfig,
|
|
434
|
+
projectMeta,
|
|
435
|
+
featureName,
|
|
436
|
+
docType,
|
|
437
|
+
githubUrl
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
// 最初のページのURLを返す(後方互換性のため)
|
|
441
|
+
if (result.pages.length === 0) {
|
|
442
|
+
throw new Error('No pages were created');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const firstPageUrl = result.pages[0].url;
|
|
446
|
+
console.log(`✅ Sync completed: ${result.pages.length} page(s) created/updated`);
|
|
447
|
+
|
|
448
|
+
if (result.pages.length > 1) {
|
|
449
|
+
console.log(`📄 Created pages:`);
|
|
450
|
+
result.pages.forEach((page, index) => {
|
|
451
|
+
console.log(` ${index + 1}. ${page.title} - ${page.url}`);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return firstPageUrl;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* ドキュメントタイプのラベルを取得
|
|
460
|
+
*/
|
|
461
|
+
function getDocTypeLabel(docType: string): string {
|
|
462
|
+
const labels: Record<string, string> = {
|
|
463
|
+
requirements: '要件定義',
|
|
464
|
+
design: '設計',
|
|
465
|
+
tasks: 'タスク分割'
|
|
466
|
+
};
|
|
467
|
+
return labels[docType] || docType;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// CLI実行
|
|
471
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
472
|
+
const args = process.argv.slice(2);
|
|
473
|
+
|
|
474
|
+
if (args.length === 0) {
|
|
475
|
+
console.error('Usage: npm run confluence:sync <feature-name> [doc-type]');
|
|
476
|
+
console.error(' doc-type: requirements (default) | design | tasks');
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const featureName = args[0];
|
|
481
|
+
const docType = (args[1] as any) || 'requirements';
|
|
482
|
+
|
|
483
|
+
syncToConfluence(featureName, docType)
|
|
484
|
+
.then(() => {
|
|
485
|
+
console.log('✅ Sync completed');
|
|
486
|
+
process.exit(0);
|
|
487
|
+
})
|
|
488
|
+
.catch((error) => {
|
|
489
|
+
console.error('❌ Sync failed:', error.message);
|
|
490
|
+
if (error.response) {
|
|
491
|
+
console.error('Response status:', error.response.status);
|
|
492
|
+
console.error('Response data:', JSON.stringify(error.response.data, null, 2));
|
|
493
|
+
}
|
|
494
|
+
if (error.config) {
|
|
495
|
+
console.error('Request URL:', error.config.url);
|
|
496
|
+
console.error('Request params:', JSON.stringify(error.config.params, null, 2));
|
|
497
|
+
}
|
|
498
|
+
process.exit(1);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export { syncToConfluence, ConfluenceClient };
|
|
503
|
+
|