@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +24 -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/src/__tests__/cli.test.d.ts +5 -0
  16. package/dist/src/__tests__/cli.test.d.ts.map +1 -0
  17. package/dist/src/__tests__/cli.test.js +58 -0
  18. package/dist/src/__tests__/cli.test.js.map +1 -0
  19. package/docs/setup.md +1 -1
  20. package/package.json +5 -3
  21. package/scripts/__tests__/README.md +101 -0
  22. package/scripts/__tests__/validate-phase.test.ts +185 -0
  23. package/scripts/config/config-schema.ts +130 -0
  24. package/scripts/config/default-config.json +57 -0
  25. package/scripts/config-interactive.ts +494 -0
  26. package/scripts/confluence-sync.ts +503 -0
  27. package/scripts/create-project.ts +293 -0
  28. package/scripts/jira-sync.ts +644 -0
  29. package/scripts/list-projects.ts +85 -0
  30. package/scripts/markdown-to-confluence.ts +161 -0
  31. package/scripts/multi-project-estimate.ts +255 -0
  32. package/scripts/phase-runner.ts +303 -0
  33. package/scripts/pr-automation.ts +67 -0
  34. package/scripts/pre-flight-check.ts +285 -0
  35. package/scripts/resource-dashboard.ts +124 -0
  36. package/scripts/setup-env.sh +52 -0
  37. package/scripts/setup-existing-project.ts +381 -0
  38. package/scripts/setup-existing.sh +145 -0
  39. package/scripts/utils/__tests__/config-validator.test.ts +302 -0
  40. package/scripts/utils/__tests__/feature-name-validator.test.ts +129 -0
  41. package/scripts/utils/config-loader.ts +326 -0
  42. package/scripts/utils/config-validator.ts +347 -0
  43. package/scripts/utils/confluence-hierarchy.ts +854 -0
  44. package/scripts/utils/feature-name-validator.ts +135 -0
  45. package/scripts/utils/project-meta.ts +69 -0
  46. package/scripts/validate-phase.ts +279 -0
  47. package/scripts/workflow-orchestrator.ts +178 -0
@@ -0,0 +1,854 @@
1
+ /**
2
+ * Confluence階層構造作成ロジック
3
+ * 各パターン(single, by-section, by-hierarchy, manual)に対応
4
+ */
5
+
6
+ import { readFileSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ import type { ConfluenceClient } from '../confluence-sync.js';
9
+ import { convertMarkdownToConfluence, createConfluencePage } from '../markdown-to-confluence.js';
10
+ import type { ProjectMetadata } from './project-meta.js';
11
+ import type { ConfluenceConfig, ConfluencePageCreationGranularity } from '../config/config-schema.js';
12
+
13
+ /**
14
+ * ページ作成結果
15
+ */
16
+ export interface PageCreationResult {
17
+ url: string;
18
+ pageId: string;
19
+ title: string;
20
+ }
21
+
22
+ /**
23
+ * 階層構造作成結果
24
+ */
25
+ export interface HierarchyCreationResult {
26
+ pages: PageCreationResult[];
27
+ parentPageId?: string;
28
+ }
29
+
30
+ /**
31
+ * タイトルに変数を展開
32
+ */
33
+ function expandTitleTemplate(
34
+ template: string,
35
+ projectMeta: ProjectMetadata,
36
+ featureName: string,
37
+ docType: string,
38
+ sectionTitle?: string
39
+ ): string {
40
+ const docTypeLabels: Record<string, string> = {
41
+ requirements: '要件定義',
42
+ design: '設計',
43
+ tasks: 'タスク分割'
44
+ };
45
+
46
+ return template
47
+ .replace(/{projectName}/g, projectMeta.projectName)
48
+ .replace(/{featureName}/g, featureName)
49
+ .replace(/{docTypeLabel}/g, docTypeLabels[docType] || docType)
50
+ .replace(/{sectionTitle}/g, sectionTitle || '')
51
+ .trim();
52
+ }
53
+
54
+ /**
55
+ * ラベルに変数を展開
56
+ */
57
+ function expandLabels(
58
+ labels: string[],
59
+ projectMeta: ProjectMetadata,
60
+ featureName: string,
61
+ docType: string
62
+ ): string[] {
63
+ return labels.map(label =>
64
+ label
65
+ .replace(/{projectLabel}/g, projectMeta.confluenceLabels[0] || '')
66
+ .replace(/{docType}/g, docType)
67
+ .replace(/{featureName}/g, featureName)
68
+ ).filter(label => label.length > 0);
69
+ }
70
+
71
+ /**
72
+ * 指定されたセクションパターンに一致するセクションを抽出
73
+ */
74
+ function extractSectionsFromMarkdown(
75
+ markdown: string,
76
+ sectionPatterns: string[]
77
+ ): string {
78
+ const lines = markdown.split('\n');
79
+ const extractedSections: string[] = [];
80
+ let inSection = false;
81
+ let currentSection = '';
82
+ let matchedPattern: string | null = null;
83
+
84
+ for (const line of lines) {
85
+ // セクション開始をチェック
86
+ if (!inSection) {
87
+ for (const pattern of sectionPatterns) {
88
+ const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
89
+ const regex = new RegExp(`^##+\\s+${escapedPattern}`);
90
+ if (regex.test(line)) {
91
+ inSection = true;
92
+ matchedPattern = pattern;
93
+ currentSection = line + '\n';
94
+ break;
95
+ }
96
+ }
97
+ } else {
98
+ // セクション内の処理
99
+ currentSection += line + '\n';
100
+
101
+ // 次のセクション(同じレベル以上)が見つかったら終了
102
+ const nextSectionMatch = line.match(/^(##+)\s+/);
103
+ if (nextSectionMatch && matchedPattern) {
104
+ const currentLevel = nextSectionMatch[1].length;
105
+ // 現在のセクションのレベルを確認
106
+ const firstLineMatch = currentSection.match(/^(##+)\s+/);
107
+ if (firstLineMatch) {
108
+ const firstLevel = firstLineMatch[1].length;
109
+ // 同じレベル以上のセクションが見つかったら終了
110
+ if (currentLevel <= firstLevel) {
111
+ // 現在のセクションを保存(次のセクションの行を除く)
112
+ // currentSectionから最後に追加した行(line)を除く
113
+ const lines = currentSection.split('\n');
114
+ lines.pop(); // 最後の空行を削除
115
+ if (lines.length > 0 && lines[lines.length - 1] === line) {
116
+ lines.pop(); // 次のセクションの行を削除
117
+ }
118
+ const sectionContent = lines.join('\n');
119
+ if (sectionContent.trim()) {
120
+ extractedSections.push(sectionContent.trim());
121
+ }
122
+ inSection = false;
123
+ matchedPattern = null;
124
+ currentSection = '';
125
+
126
+ // 新しいセクションがパターンに一致するかチェック
127
+ for (const pattern of sectionPatterns) {
128
+ const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
129
+ const regex = new RegExp(`^##+\\s+${escapedPattern}`);
130
+ if (regex.test(line)) {
131
+ inSection = true;
132
+ matchedPattern = pattern;
133
+ currentSection = line + '\n';
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ // 最後のセクションを追加
144
+ if (inSection && currentSection.trim()) {
145
+ extractedSections.push(currentSection.trim());
146
+ }
147
+
148
+ return extractedSections.join('\n\n');
149
+ }
150
+
151
+ /**
152
+ * Markdownをセクションごとに分割
153
+ * 空のセクション(タイトルのみで内容がない)は除外
154
+ */
155
+ function splitMarkdownBySections(markdown: string): Array<{ title: string; content: string }> {
156
+ const lines = markdown.split('\n');
157
+ const sections: Array<{ title: string; content: string }> = [];
158
+ let currentSection: { title: string; content: string } | null = null;
159
+
160
+ for (const line of lines) {
161
+ // ## で始まる行をセクションタイトルとして認識
162
+ const sectionMatch = line.match(/^##\s+(.+)$/);
163
+ if (sectionMatch) {
164
+ // 前のセクションを保存(空でない場合のみ)
165
+ if (currentSection) {
166
+ // タイトル行を除いた内容をチェック
167
+ const contentWithoutTitle = currentSection.content.replace(/^##+\s+.*\n/, '').trim();
168
+ if (contentWithoutTitle.length > 0) {
169
+ sections.push(currentSection);
170
+ }
171
+ }
172
+ // 新しいセクションを開始
173
+ currentSection = {
174
+ title: sectionMatch[1].trim(),
175
+ content: line + '\n'
176
+ };
177
+ } else if (currentSection) {
178
+ currentSection.content += line + '\n';
179
+ }
180
+ }
181
+
182
+ // 最後のセクションを保存(空でない場合のみ)
183
+ if (currentSection) {
184
+ const contentWithoutTitle = currentSection.content.replace(/^##+\s+.*\n/, '').trim();
185
+ if (contentWithoutTitle.length > 0) {
186
+ sections.push(currentSection);
187
+ }
188
+ }
189
+
190
+ return sections;
191
+ }
192
+
193
+ /**
194
+ * 親ページを作成または取得
195
+ */
196
+ async function getOrCreateParentPage(
197
+ client: ConfluenceClient,
198
+ spaceKey: string,
199
+ parentTitle: string,
200
+ projectMeta: ProjectMetadata,
201
+ featureName: string,
202
+ githubUrl: string
203
+ ): Promise<string> {
204
+ // 既存の親ページを検索
205
+ const existingParent = await client.searchPage(spaceKey, parentTitle);
206
+ if (existingParent) {
207
+ return existingParent.id;
208
+ }
209
+
210
+ // 親ページを作成
211
+ const parentContent = createConfluencePage({
212
+ title: parentTitle,
213
+ githubUrl: `${projectMeta.repository}/tree/main/.kiro/specs/${featureName}`,
214
+ content: `<p>機能: <strong>${featureName}</strong></p><p>このページの下に、要件定義・設計・タスク分割のページが配置されます。</p>`,
215
+ approvers: projectMeta.stakeholders,
216
+ projectName: projectMeta.projectName
217
+ });
218
+
219
+ const parentLabels = expandLabels(
220
+ ['{projectLabel}', '{featureName}', 'github-sync'],
221
+ projectMeta,
222
+ featureName,
223
+ ''
224
+ );
225
+
226
+ const created = await client.createPage(spaceKey, parentTitle, parentContent, parentLabels);
227
+ console.log(`✅ Parent page created: ${parentTitle}`);
228
+
229
+ return created.id;
230
+ }
231
+
232
+ /**
233
+ * パターン1: single(1ドキュメント = 1ページ)
234
+ */
235
+ export async function createSinglePage(
236
+ client: ConfluenceClient,
237
+ spaceKey: string,
238
+ markdown: string,
239
+ config: ConfluenceConfig,
240
+ projectMeta: ProjectMetadata,
241
+ featureName: string,
242
+ docType: 'requirements' | 'design' | 'tasks',
243
+ githubUrl: string
244
+ ): Promise<HierarchyCreationResult> {
245
+ const pageTitleFormat = config.pageTitleFormat || '[{projectName}] {featureName} {docTypeLabel}';
246
+ const pageTitle = expandTitleTemplate(
247
+ pageTitleFormat,
248
+ projectMeta,
249
+ featureName,
250
+ docType
251
+ );
252
+
253
+ const confluenceContent = convertMarkdownToConfluence(markdown);
254
+ const fullContent = createConfluencePage({
255
+ title: pageTitle,
256
+ githubUrl,
257
+ content: confluenceContent,
258
+ approvers: projectMeta.stakeholders,
259
+ projectName: projectMeta.projectName
260
+ });
261
+
262
+ const labels = expandLabels(config.autoLabels, projectMeta, featureName, docType);
263
+
264
+ // 既存ページを検索
265
+ const existingPage = await client.searchPage(spaceKey, pageTitle);
266
+
267
+ let page: any;
268
+ if (existingPage) {
269
+ page = await client.updatePage(
270
+ existingPage.id,
271
+ pageTitle,
272
+ fullContent,
273
+ existingPage.version.number
274
+ );
275
+ console.log(`✅ Page updated: ${pageTitle}`);
276
+ } else {
277
+ page = await client.createPage(spaceKey, pageTitle, fullContent, labels);
278
+ console.log(`✅ Page created: ${pageTitle}`);
279
+ }
280
+
281
+ const baseUrl = process.env.ATLASSIAN_URL || '';
282
+ return {
283
+ pages: [{
284
+ url: `${baseUrl}/wiki${page._links.webui}`,
285
+ pageId: page.id,
286
+ title: pageTitle
287
+ }]
288
+ };
289
+ }
290
+
291
+ /**
292
+ * パターン2: by-section(セクションごとにページ分割)
293
+ */
294
+ export async function createBySectionPages(
295
+ client: ConfluenceClient,
296
+ spaceKey: string,
297
+ markdown: string,
298
+ config: ConfluenceConfig,
299
+ projectMeta: ProjectMetadata,
300
+ featureName: string,
301
+ docType: 'requirements' | 'design' | 'tasks',
302
+ githubUrl: string
303
+ ): Promise<HierarchyCreationResult> {
304
+ const sections = splitMarkdownBySections(markdown);
305
+ const pages: PageCreationResult[] = [];
306
+
307
+ const pageTitleFormat = config.pageTitleFormat || '[{projectName}] {featureName} {docTypeLabel}';
308
+
309
+ for (const section of sections) {
310
+ // sectionTitleを含む一意のタイトルを生成
311
+ // pageTitleFormatに{sectionTitle}が含まれていない場合は追加
312
+ const titleTemplate = pageTitleFormat.includes('{sectionTitle}')
313
+ ? pageTitleFormat
314
+ : `${pageTitleFormat} - {sectionTitle}`;
315
+
316
+ const pageTitle = expandTitleTemplate(
317
+ titleTemplate,
318
+ projectMeta,
319
+ featureName,
320
+ docType,
321
+ section.title
322
+ );
323
+
324
+ const confluenceContent = convertMarkdownToConfluence(section.content);
325
+ const fullContent = createConfluencePage({
326
+ title: pageTitle,
327
+ githubUrl,
328
+ content: confluenceContent,
329
+ approvers: projectMeta.stakeholders,
330
+ projectName: projectMeta.projectName
331
+ });
332
+
333
+ const labels = expandLabels(config.autoLabels, projectMeta, featureName, docType);
334
+
335
+ // 既存ページを検索
336
+ const existingPage = await client.searchPage(spaceKey, pageTitle);
337
+
338
+ let page: any;
339
+ if (existingPage) {
340
+ page = await client.updatePage(
341
+ existingPage.id,
342
+ pageTitle,
343
+ fullContent,
344
+ existingPage.version.number
345
+ );
346
+ console.log(`✅ Page updated: ${pageTitle}`);
347
+ } else {
348
+ page = await client.createPage(spaceKey, pageTitle, fullContent, labels);
349
+ console.log(`✅ Page created: ${pageTitle}`);
350
+ }
351
+
352
+ const baseUrl = process.env.ATLASSIAN_URL || '';
353
+ pages.push({
354
+ url: `${baseUrl}/wiki${page._links.webui}`,
355
+ pageId: page.id,
356
+ title: pageTitle
357
+ });
358
+ }
359
+
360
+ return { pages };
361
+ }
362
+
363
+ /**
364
+ * パターン3: by-hierarchy simple(親ページ + ドキュメントタイプ子ページ)
365
+ */
366
+ export async function createByHierarchySimplePages(
367
+ client: ConfluenceClient,
368
+ spaceKey: string,
369
+ markdown: string,
370
+ config: ConfluenceConfig,
371
+ projectMeta: ProjectMetadata,
372
+ featureName: string,
373
+ docType: 'requirements' | 'design' | 'tasks',
374
+ githubUrl: string
375
+ ): Promise<HierarchyCreationResult> {
376
+ // 親ページを作成または取得
377
+ const parentTitle = expandTitleTemplate(
378
+ config.hierarchy?.parentPageTitle || '[{projectName}] {featureName}',
379
+ projectMeta,
380
+ featureName,
381
+ ''
382
+ );
383
+
384
+ const parentPageId = await getOrCreateParentPage(
385
+ client,
386
+ spaceKey,
387
+ parentTitle,
388
+ projectMeta,
389
+ featureName,
390
+ githubUrl
391
+ );
392
+
393
+ // ドキュメントタイプの子ページを作成(featureNameを含む一意のタイトル)
394
+ const pageTitleFormat = config.pageTitleFormat || '[{projectName}] {featureName} {docTypeLabel}';
395
+ let childPageTitle = expandTitleTemplate(
396
+ pageTitleFormat,
397
+ projectMeta,
398
+ featureName,
399
+ docType
400
+ );
401
+
402
+ // タイトルに機能名が含まれていない場合、自動的に追加(重複を避けるため)
403
+ if (!childPageTitle.includes(featureName)) {
404
+ console.warn(`⚠️ Warning: pageTitleFormat does not include {featureName}. Adding feature name to ensure uniqueness.`);
405
+ childPageTitle = `[${featureName}] ${childPageTitle}`;
406
+ }
407
+
408
+ const confluenceContent = convertMarkdownToConfluence(markdown);
409
+ const fullContent = createConfluencePage({
410
+ title: childPageTitle,
411
+ githubUrl,
412
+ content: confluenceContent,
413
+ approvers: projectMeta.stakeholders,
414
+ projectName: projectMeta.projectName
415
+ });
416
+
417
+ const labels = expandLabels(config.autoLabels, projectMeta, featureName, docType);
418
+
419
+ // 既存の子ページを検索(親ページIDで絞り込んで検索)
420
+ // これにより、同じタイトルでも別機能のページがヒットすることを防ぐ
421
+ console.log(`🔍 Searching for existing child page: "${childPageTitle}" under parent ${parentPageId}`);
422
+ let existingChild = await client.searchPage(spaceKey, childPageTitle, parentPageId);
423
+
424
+ // CQLクエリで見つからない場合、親ページIDなしで検索(既存ページが別の親の下にある可能性)
425
+ if (!existingChild) {
426
+ console.log(` CQL search found nothing, trying search without parent filter`);
427
+ existingChild = await client.searchPage(spaceKey, childPageTitle);
428
+ if (existingChild) {
429
+ console.log(` ⚠️ Found page with same title: ${existingChild.id}, verifying parent page ID`);
430
+
431
+ // 見つかったページの実際の親ページIDを取得して検証
432
+ const actualParentId = await client.getPageParentId(existingChild.id);
433
+
434
+ if (actualParentId === parentPageId) {
435
+ // 親ページIDが一致する場合、更新を続行
436
+ console.log(` ✅ Parent page ID matches (${parentPageId}), proceeding with update`);
437
+ } else {
438
+ // 親ページIDが一致しない場合、エラーをスロー
439
+ console.error(` ❌ Parent page ID mismatch!`);
440
+ console.error(` Expected parent: ${parentPageId}`);
441
+ console.error(` Actual parent: ${actualParentId || 'root (no parent)'}`);
442
+ console.error(` Page ID: ${existingChild.id}`);
443
+ throw new Error(
444
+ `Page conflict: Found page "${childPageTitle}" (ID: ${existingChild.id}) ` +
445
+ `under different parent (expected: ${parentPageId}, actual: ${actualParentId || 'root'}). ` +
446
+ `Cannot update foreign page. Please rename or delete the conflicting page.`
447
+ );
448
+ }
449
+ }
450
+ }
451
+
452
+ let childPage: any;
453
+ if (existingChild) {
454
+ console.log(`📄 Found existing child page: ${existingChild.id} (version ${existingChild.version.number})`);
455
+ childPage = await client.updatePage(
456
+ existingChild.id,
457
+ childPageTitle,
458
+ fullContent,
459
+ existingChild.version.number
460
+ );
461
+ console.log(`✅ Child page updated: ${childPageTitle}`);
462
+ } else {
463
+ // 親ページのIDを指定して子ページを作成
464
+ childPage = await client.createPageUnderParent(
465
+ spaceKey,
466
+ childPageTitle,
467
+ fullContent,
468
+ labels,
469
+ parentPageId
470
+ );
471
+ console.log(`✅ Child page created: ${childPageTitle} (under ${parentTitle})`);
472
+ }
473
+
474
+ const baseUrl = process.env.ATLASSIAN_URL || '';
475
+ return {
476
+ pages: [{
477
+ url: `${baseUrl}/wiki${childPage._links.webui}`,
478
+ pageId: childPage.id,
479
+ title: childPageTitle
480
+ }],
481
+ parentPageId
482
+ };
483
+ }
484
+
485
+ /**
486
+ * パターン4: by-hierarchy nested(3階層構造)
487
+ */
488
+ export async function createByHierarchyNestedPages(
489
+ client: ConfluenceClient,
490
+ spaceKey: string,
491
+ markdown: string,
492
+ config: ConfluenceConfig,
493
+ projectMeta: ProjectMetadata,
494
+ featureName: string,
495
+ docType: 'requirements' | 'design' | 'tasks',
496
+ githubUrl: string
497
+ ): Promise<HierarchyCreationResult> {
498
+ // 親ページを作成または取得
499
+ const parentTitle = expandTitleTemplate(
500
+ config.hierarchy?.parentPageTitle || '[{projectName}] {featureName}',
501
+ projectMeta,
502
+ featureName,
503
+ ''
504
+ );
505
+
506
+ const parentPageId = await getOrCreateParentPage(
507
+ client,
508
+ spaceKey,
509
+ parentTitle,
510
+ projectMeta,
511
+ featureName,
512
+ githubUrl
513
+ );
514
+
515
+ // ドキュメントタイプの親ページを作成または取得(featureNameを含む一意のタイトル)
516
+ const pageTitleFormat = config.pageTitleFormat || '[{projectName}] {featureName} {docTypeLabel}';
517
+ let docTypeParentTitle = expandTitleTemplate(
518
+ pageTitleFormat,
519
+ projectMeta,
520
+ featureName,
521
+ docType
522
+ );
523
+
524
+ // タイトルに機能名が含まれていない場合、自動的に追加(重複を避けるため)
525
+ if (!docTypeParentTitle.includes(featureName)) {
526
+ console.warn(`⚠️ Warning: pageTitleFormat does not include {featureName}. Adding feature name to ensure uniqueness.`);
527
+ docTypeParentTitle = `[${featureName}] ${docTypeParentTitle}`;
528
+ }
529
+
530
+ // 親ページIDで絞り込んで検索(同じタイトルでも別機能のページがヒットすることを防ぐ)
531
+ const existingDocTypeParent = await client.searchPage(spaceKey, docTypeParentTitle, parentPageId);
532
+
533
+ let docTypeParentId: string;
534
+ if (existingDocTypeParent) {
535
+ docTypeParentId = existingDocTypeParent.id;
536
+ } else {
537
+ const docTypeParentContent = createConfluencePage({
538
+ title: docTypeParentTitle,
539
+ githubUrl,
540
+ content: `<p>${docTypeParentTitle}の詳細ページがこの下に配置されます。</p>`,
541
+ approvers: projectMeta.stakeholders,
542
+ projectName: projectMeta.projectName
543
+ });
544
+
545
+ const docTypeParent = await client.createPageUnderParent(
546
+ spaceKey,
547
+ docTypeParentTitle,
548
+ docTypeParentContent,
549
+ expandLabels(config.autoLabels, projectMeta, featureName, docType),
550
+ parentPageId
551
+ );
552
+ docTypeParentId = docTypeParent.id;
553
+ console.log(`✅ DocType parent page created: ${docTypeParentTitle}`);
554
+ }
555
+
556
+ // セクションごとに子ページを作成(featureNameとsectionTitleを含む一意のタイトル)
557
+ const sections = splitMarkdownBySections(markdown);
558
+ const pages: PageCreationResult[] = [];
559
+
560
+ for (const section of sections) {
561
+ // セクションページのタイトルにfeatureNameとsectionTitleを含める
562
+ let sectionPageTitle = expandTitleTemplate(
563
+ pageTitleFormat,
564
+ projectMeta,
565
+ featureName,
566
+ docType,
567
+ section.title
568
+ );
569
+
570
+ // タイトルに機能名が含まれていない場合、自動的に追加(重複を避けるため)
571
+ if (!sectionPageTitle.includes(featureName)) {
572
+ console.warn(`⚠️ Warning: pageTitleFormat does not include {featureName}. Adding feature name to ensure uniqueness.`);
573
+ sectionPageTitle = `[${featureName}] ${sectionPageTitle}`;
574
+ }
575
+ const confluenceContent = convertMarkdownToConfluence(section.content);
576
+ const fullContent = createConfluencePage({
577
+ title: sectionPageTitle,
578
+ githubUrl,
579
+ content: confluenceContent,
580
+ approvers: projectMeta.stakeholders,
581
+ projectName: projectMeta.projectName
582
+ });
583
+
584
+ const labels = expandLabels(config.autoLabels, projectMeta, featureName, docType);
585
+
586
+ // 既存のセクションページを検索(docTypeParentIdで絞り込んで検索)
587
+ // これにより、同じタイトルでも別機能のページがヒットすることを防ぐ
588
+ const existingSectionPage = await client.searchPage(spaceKey, sectionPageTitle, docTypeParentId);
589
+
590
+ let sectionPage: any;
591
+ if (existingSectionPage) {
592
+ sectionPage = await client.updatePage(
593
+ existingSectionPage.id,
594
+ sectionPageTitle,
595
+ fullContent,
596
+ existingSectionPage.version.number
597
+ );
598
+ console.log(`✅ Section page updated: ${sectionPageTitle}`);
599
+ } else {
600
+ sectionPage = await client.createPageUnderParent(
601
+ spaceKey,
602
+ sectionPageTitle,
603
+ fullContent,
604
+ labels,
605
+ docTypeParentId
606
+ );
607
+ console.log(`✅ Section page created: ${sectionPageTitle} (under ${docTypeParentTitle})`);
608
+ }
609
+
610
+ const baseUrl = process.env.ATLASSIAN_URL || '';
611
+ pages.push({
612
+ url: `${baseUrl}/wiki${sectionPage._links.webui}`,
613
+ pageId: sectionPage.id,
614
+ title: sectionPageTitle
615
+ });
616
+ }
617
+
618
+ return {
619
+ pages,
620
+ parentPageId
621
+ };
622
+ }
623
+
624
+ /**
625
+ * パターン5: manual(設定ファイルベースの手動指定)
626
+ */
627
+ export async function createManualPages(
628
+ client: ConfluenceClient,
629
+ spaceKey: string,
630
+ markdown: string,
631
+ config: ConfluenceConfig,
632
+ projectMeta: ProjectMetadata,
633
+ featureName: string,
634
+ docType: 'requirements' | 'design' | 'tasks',
635
+ githubUrl: string
636
+ ): Promise<HierarchyCreationResult> {
637
+ if (!config.hierarchy?.structure || !config.hierarchy.structure[docType]) {
638
+ throw new Error(`Manual structure not defined for docType: ${docType}`);
639
+ }
640
+
641
+ const structure = config.hierarchy.structure[docType];
642
+
643
+ // 親ページを作成または取得
644
+ let parentPageId: string | undefined;
645
+ if (config.hierarchy.parentPageTitle) {
646
+ const parentTitle = expandTitleTemplate(
647
+ config.hierarchy.parentPageTitle,
648
+ projectMeta,
649
+ featureName,
650
+ ''
651
+ );
652
+ parentPageId = await getOrCreateParentPage(
653
+ client,
654
+ spaceKey,
655
+ parentTitle,
656
+ projectMeta,
657
+ featureName,
658
+ githubUrl
659
+ );
660
+ }
661
+
662
+ const pages: PageCreationResult[] = [];
663
+
664
+ // 設定ファイルで指定されたページを作成
665
+ if (structure.pages) {
666
+ for (const pageConfig of structure.pages) {
667
+ // セクションが指定されている場合、最初のセクション名を取得
668
+ let sectionTitleForTitle: string | undefined;
669
+ if (pageConfig.sections && pageConfig.sections.length > 0) {
670
+ // マークダウンからセクション名を抽出
671
+ const lines = markdown.split('\n');
672
+ for (const line of lines) {
673
+ for (const sectionPattern of pageConfig.sections) {
674
+ const escapedPattern = sectionPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
675
+ const regex = new RegExp(`^##+\\s+${escapedPattern}`);
676
+ if (regex.test(line)) {
677
+ // セクションタイトルを抽出(## を除く)
678
+ const match = line.match(/^##+\s+(.+)$/);
679
+ if (match) {
680
+ sectionTitleForTitle = match[1].trim();
681
+ break;
682
+ }
683
+ }
684
+ }
685
+ if (sectionTitleForTitle) break;
686
+ }
687
+ }
688
+
689
+ // featureNameとsectionTitleを含む一意のタイトルを生成
690
+ let pageTitle = expandTitleTemplate(
691
+ pageConfig.title,
692
+ projectMeta,
693
+ featureName,
694
+ docType,
695
+ sectionTitleForTitle
696
+ );
697
+
698
+ // タイトルに機能名が含まれていない場合、自動的に追加(重複を避けるため)
699
+ if (!pageTitle.includes(featureName)) {
700
+ console.warn(`⚠️ Warning: Manual page title does not include {featureName}. Adding feature name to ensure uniqueness.`);
701
+ pageTitle = `[${featureName}] ${pageTitle}`;
702
+ }
703
+
704
+ // 指定されたセクションを抽出
705
+ let pageContent = '';
706
+ if (pageConfig.sections && pageConfig.sections.length > 0) {
707
+ pageContent = extractSectionsFromMarkdown(markdown, pageConfig.sections);
708
+ // セクションが見つからない場合は全体を使用
709
+ if (!pageContent.trim()) {
710
+ pageContent = markdown;
711
+ }
712
+ } else {
713
+ pageContent = markdown;
714
+ }
715
+
716
+ const confluenceContent = convertMarkdownToConfluence(pageContent);
717
+ const fullContent = createConfluencePage({
718
+ title: pageTitle,
719
+ githubUrl,
720
+ content: confluenceContent,
721
+ approvers: projectMeta.stakeholders,
722
+ projectName: projectMeta.projectName
723
+ });
724
+
725
+ const labels = pageConfig.labels
726
+ ? expandLabels(pageConfig.labels, projectMeta, featureName, docType)
727
+ : expandLabels(config.autoLabels, projectMeta, featureName, docType);
728
+
729
+ // 既存ページを検索(親ページIDが指定されている場合は絞り込んで検索)
730
+ const existingPage = await client.searchPage(spaceKey, pageTitle, parentPageId);
731
+
732
+ let page: any;
733
+ if (existingPage) {
734
+ page = await client.updatePage(
735
+ existingPage.id,
736
+ pageTitle,
737
+ fullContent,
738
+ existingPage.version.number
739
+ );
740
+ console.log(`✅ Page updated: ${pageTitle}`);
741
+ } else {
742
+ if (parentPageId) {
743
+ page = await client.createPageUnderParent(
744
+ spaceKey,
745
+ pageTitle,
746
+ fullContent,
747
+ labels,
748
+ parentPageId
749
+ );
750
+ console.log(`✅ Page created: ${pageTitle} (under parent)`);
751
+ } else {
752
+ page = await client.createPage(spaceKey, pageTitle, fullContent, labels);
753
+ console.log(`✅ Page created: ${pageTitle}`);
754
+ }
755
+ }
756
+
757
+ const baseUrl = process.env.ATLASSIAN_URL || '';
758
+ pages.push({
759
+ url: `${baseUrl}/wiki${page._links.webui}`,
760
+ pageId: page.id,
761
+ title: pageTitle
762
+ });
763
+ }
764
+ }
765
+
766
+ return {
767
+ pages,
768
+ parentPageId
769
+ };
770
+ }
771
+
772
+ /**
773
+ * 階層構造に応じてページを作成
774
+ */
775
+ export async function createPagesByGranularity(
776
+ client: ConfluenceClient,
777
+ spaceKey: string,
778
+ markdown: string,
779
+ config: ConfluenceConfig,
780
+ projectMeta: ProjectMetadata,
781
+ featureName: string,
782
+ docType: 'requirements' | 'design' | 'tasks',
783
+ githubUrl: string
784
+ ): Promise<HierarchyCreationResult> {
785
+ const granularity = config.pageCreationGranularity || 'single';
786
+
787
+ switch (granularity) {
788
+ case 'single':
789
+ return await createSinglePage(
790
+ client,
791
+ spaceKey,
792
+ markdown,
793
+ config,
794
+ projectMeta,
795
+ featureName,
796
+ docType,
797
+ githubUrl
798
+ );
799
+
800
+ case 'by-section':
801
+ return await createBySectionPages(
802
+ client,
803
+ spaceKey,
804
+ markdown,
805
+ config,
806
+ projectMeta,
807
+ featureName,
808
+ docType,
809
+ githubUrl
810
+ );
811
+
812
+ case 'by-hierarchy':
813
+ const hierarchyMode = config.hierarchy?.mode || 'simple';
814
+ if (hierarchyMode === 'nested') {
815
+ return await createByHierarchyNestedPages(
816
+ client,
817
+ spaceKey,
818
+ markdown,
819
+ config,
820
+ projectMeta,
821
+ featureName,
822
+ docType,
823
+ githubUrl
824
+ );
825
+ } else {
826
+ return await createByHierarchySimplePages(
827
+ client,
828
+ spaceKey,
829
+ markdown,
830
+ config,
831
+ projectMeta,
832
+ featureName,
833
+ docType,
834
+ githubUrl
835
+ );
836
+ }
837
+
838
+ case 'manual':
839
+ return await createManualPages(
840
+ client,
841
+ spaceKey,
842
+ markdown,
843
+ config,
844
+ projectMeta,
845
+ featureName,
846
+ docType,
847
+ githubUrl
848
+ );
849
+
850
+ default:
851
+ throw new Error(`Unknown page creation granularity: ${granularity}`);
852
+ }
853
+ }
854
+