atl-fetch 1.0.0

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/dist/cli/cli.d.ts +61 -0
  4. package/dist/cli/cli.js +131 -0
  5. package/dist/cli/index.d.ts +5 -0
  6. package/dist/cli/index.js +4 -0
  7. package/dist/index.d.ts +8 -0
  8. package/dist/index.js +13 -0
  9. package/dist/ports/file/file-port.d.ts +89 -0
  10. package/dist/ports/file/file-port.js +155 -0
  11. package/dist/ports/file/index.d.ts +1 -0
  12. package/dist/ports/file/index.js +1 -0
  13. package/dist/ports/http/http-port.d.ts +107 -0
  14. package/dist/ports/http/http-port.js +238 -0
  15. package/dist/ports/http/index.d.ts +1 -0
  16. package/dist/ports/http/index.js +1 -0
  17. package/dist/services/auth/auth-service.d.ts +79 -0
  18. package/dist/services/auth/auth-service.js +158 -0
  19. package/dist/services/auth/index.d.ts +1 -0
  20. package/dist/services/auth/index.js +1 -0
  21. package/dist/services/confluence/confluence-service.d.ts +152 -0
  22. package/dist/services/confluence/confluence-service.js +510 -0
  23. package/dist/services/confluence/index.d.ts +1 -0
  24. package/dist/services/confluence/index.js +1 -0
  25. package/dist/services/diff/diff-service.d.ts +84 -0
  26. package/dist/services/diff/diff-service.js +881 -0
  27. package/dist/services/diff/index.d.ts +1 -0
  28. package/dist/services/diff/index.js +1 -0
  29. package/dist/services/fetch/fetch-service.d.ts +112 -0
  30. package/dist/services/fetch/fetch-service.js +302 -0
  31. package/dist/services/fetch/index.d.ts +1 -0
  32. package/dist/services/fetch/index.js +1 -0
  33. package/dist/services/jira/index.d.ts +1 -0
  34. package/dist/services/jira/index.js +1 -0
  35. package/dist/services/jira/jira-service.d.ts +100 -0
  36. package/dist/services/jira/jira-service.js +354 -0
  37. package/dist/services/output/index.d.ts +4 -0
  38. package/dist/services/output/index.js +4 -0
  39. package/dist/services/output/output-service.d.ts +67 -0
  40. package/dist/services/output/output-service.js +228 -0
  41. package/dist/services/storage/index.d.ts +6 -0
  42. package/dist/services/storage/index.js +6 -0
  43. package/dist/services/storage/storage-service.d.ts +77 -0
  44. package/dist/services/storage/storage-service.js +738 -0
  45. package/dist/services/text-converter/index.d.ts +1 -0
  46. package/dist/services/text-converter/index.js +1 -0
  47. package/dist/services/text-converter/text-converter.d.ts +35 -0
  48. package/dist/services/text-converter/text-converter.js +681 -0
  49. package/dist/services/url-parser/index.d.ts +1 -0
  50. package/dist/services/url-parser/index.js +1 -0
  51. package/dist/services/url-parser/url-parser.d.ts +43 -0
  52. package/dist/services/url-parser/url-parser.js +283 -0
  53. package/dist/types/auth.d.ts +25 -0
  54. package/dist/types/auth.js +1 -0
  55. package/dist/types/confluence.d.ts +68 -0
  56. package/dist/types/confluence.js +1 -0
  57. package/dist/types/diff.d.ts +77 -0
  58. package/dist/types/diff.js +7 -0
  59. package/dist/types/fetch.d.ts +65 -0
  60. package/dist/types/fetch.js +1 -0
  61. package/dist/types/file.d.ts +22 -0
  62. package/dist/types/file.js +1 -0
  63. package/dist/types/http.d.ts +45 -0
  64. package/dist/types/http.js +1 -0
  65. package/dist/types/jira.d.ts +90 -0
  66. package/dist/types/jira.js +1 -0
  67. package/dist/types/output.d.ts +55 -0
  68. package/dist/types/output.js +7 -0
  69. package/dist/types/result.d.ts +104 -0
  70. package/dist/types/result.js +119 -0
  71. package/dist/types/storage.d.ts +209 -0
  72. package/dist/types/storage.js +6 -0
  73. package/dist/types/url-parser.d.ts +46 -0
  74. package/dist/types/url-parser.js +1 -0
  75. package/package.json +106 -0
@@ -0,0 +1,354 @@
1
+ import { err, ok } from 'neverthrow';
2
+ import { z } from 'zod';
3
+ import { httpDownload, httpRequest } from '../../ports/http/http-port.js';
4
+ import { getAuthHeader } from '../auth/auth-service.js';
5
+ /**
6
+ * Jira Cloud API のベース URL を構築する
7
+ *
8
+ * @param organization - 組織名(.atlassian.net のサブドメイン)
9
+ * @returns Jira Cloud API のベース URL
10
+ */
11
+ function getJiraApiBaseUrl(organization) {
12
+ return `https://${organization}.atlassian.net/rest/api/3`;
13
+ }
14
+ /**
15
+ * ADF (Atlassian Document Format) からプレーンテキストを抽出する
16
+ *
17
+ * Jira Cloud API v3 は説明やコメントを ADF 形式で返す。
18
+ * この関数は ADF ドキュメントからテキストコンテンツを抽出する。
19
+ *
20
+ * @param adf - ADF ドキュメント
21
+ * @returns プレーンテキスト
22
+ *
23
+ * @internal テスト用に export
24
+ */
25
+ export function extractTextFromAdf(adf) {
26
+ if (!adf || typeof adf !== 'object') {
27
+ return '';
28
+ }
29
+ const doc = adf;
30
+ if (doc.type === 'text' && typeof doc.text === 'string') {
31
+ return doc.text;
32
+ }
33
+ if (Array.isArray(doc.content)) {
34
+ return doc.content.map((child) => extractTextFromAdf(child)).join('');
35
+ }
36
+ return '';
37
+ }
38
+ /**
39
+ * Jira API レスポンスのコメントスキーマ
40
+ */
41
+ const jiraApiCommentSchema = z.object({
42
+ author: z.object({
43
+ displayName: z.string(),
44
+ }),
45
+ body: z.unknown(),
46
+ created: z.string(),
47
+ id: z.string(),
48
+ updated: z.string(),
49
+ });
50
+ /**
51
+ * Jira API レスポンスの添付ファイルスキーマ
52
+ */
53
+ const jiraApiAttachmentSchema = z.object({
54
+ content: z.string(),
55
+ filename: z.string(),
56
+ id: z.string(),
57
+ mimeType: z.string(),
58
+ size: z.number(),
59
+ });
60
+ /**
61
+ * Jira API レスポンスの変更履歴アイテムスキーマ
62
+ */
63
+ const jiraApiChangelogItemSchema = z.object({
64
+ field: z.string(),
65
+ fromString: z.string().nullable(),
66
+ toString: z.string().nullable(),
67
+ });
68
+ /**
69
+ * Jira API レスポンスの変更履歴エントリスキーマ
70
+ */
71
+ const jiraApiChangelogEntrySchema = z.object({
72
+ author: z.object({
73
+ displayName: z.string(),
74
+ }),
75
+ created: z.string(),
76
+ id: z.string(),
77
+ items: z.array(jiraApiChangelogItemSchema),
78
+ });
79
+ /**
80
+ * Jira API レスポンスのスキーマ
81
+ */
82
+ const jiraApiResponseSchema = z.object({
83
+ changelog: z
84
+ .object({
85
+ histories: z.array(jiraApiChangelogEntrySchema),
86
+ })
87
+ .optional(),
88
+ fields: z.object({
89
+ attachment: z.array(jiraApiAttachmentSchema).optional(),
90
+ comment: z
91
+ .object({
92
+ comments: z.array(jiraApiCommentSchema),
93
+ total: z.number(),
94
+ })
95
+ .optional(),
96
+ description: z.unknown().nullable(),
97
+ summary: z.string(),
98
+ }),
99
+ key: z.string(),
100
+ });
101
+ /**
102
+ * API レスポンスを JiraComment に変換する
103
+ *
104
+ * @param apiComment - API レスポンスのコメント
105
+ * @returns JiraComment
106
+ */
107
+ function mapApiCommentToJiraComment(apiComment) {
108
+ return {
109
+ author: apiComment.author.displayName,
110
+ body: extractTextFromAdf(apiComment.body),
111
+ created: apiComment.created,
112
+ id: apiComment.id,
113
+ updated: apiComment.updated,
114
+ };
115
+ }
116
+ /**
117
+ * API レスポンスを JiraAttachment に変換する
118
+ *
119
+ * @param apiAttachment - API レスポンスの添付ファイル
120
+ * @returns JiraAttachment
121
+ */
122
+ function mapApiAttachmentToJiraAttachment(apiAttachment) {
123
+ return {
124
+ contentUrl: apiAttachment.content,
125
+ filename: apiAttachment.filename,
126
+ id: apiAttachment.id,
127
+ mimeType: apiAttachment.mimeType,
128
+ size: apiAttachment.size,
129
+ };
130
+ }
131
+ /**
132
+ * API レスポンスを JiraChangelogEntry に変換する
133
+ *
134
+ * @param apiEntry - API レスポンスの変更履歴エントリ
135
+ * @returns JiraChangelogEntry
136
+ */
137
+ function mapApiChangelogEntryToJiraChangelogEntry(apiEntry) {
138
+ return {
139
+ author: apiEntry.author.displayName,
140
+ created: apiEntry.created,
141
+ id: apiEntry.id,
142
+ items: apiEntry.items.map((item) => ({
143
+ field: item.field,
144
+ fromString: item.fromString,
145
+ toString: item.toString,
146
+ })),
147
+ };
148
+ }
149
+ /**
150
+ * HTTP エラーを JiraError に変換する
151
+ *
152
+ * @param status - HTTP ステータスコード
153
+ * @param message - エラーメッセージ
154
+ * @returns JiraError
155
+ *
156
+ * @internal テスト用に export
157
+ */
158
+ export function mapHttpStatusToJiraError(status, message) {
159
+ switch (status) {
160
+ case 401:
161
+ return {
162
+ kind: 'AUTH_FAILED',
163
+ message: '認証に失敗しました。API トークンとメールアドレスを確認してください。',
164
+ };
165
+ case 403:
166
+ return {
167
+ kind: 'FORBIDDEN',
168
+ message: 'この Issue へのアクセス権限がありません。権限を確認してください。',
169
+ };
170
+ case 404:
171
+ return {
172
+ kind: 'NOT_FOUND',
173
+ message: '指定された Issue が見つかりません。Issue キーを確認してください。',
174
+ };
175
+ default:
176
+ return {
177
+ kind: 'NETWORK_ERROR',
178
+ message: `API リクエストに失敗しました: ${message}`,
179
+ };
180
+ }
181
+ }
182
+ /**
183
+ * Jira Issue を取得する
184
+ *
185
+ * Jira Cloud API を使用して Issue の基本情報(タイトル、説明、コメント、
186
+ * 変更履歴、添付ファイル)を取得する。
187
+ *
188
+ * @param organization - 組織名(.atlassian.net のサブドメイン)
189
+ * @param issueKey - Issue キー(例: PROJECT-123)
190
+ * @returns 成功時は {@link JiraIssue} を含む Ok、失敗時は {@link JiraError} を含む Err
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * // 基本的な使用例
195
+ * const result = await fetchJiraIssue('my-company', 'PROJECT-123');
196
+ * if (result.isOk()) {
197
+ * console.log(result.value.summary);
198
+ * console.log(result.value.description);
199
+ * }
200
+ * ```
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * // エラーハンドリング
205
+ * const result = await fetchJiraIssue('my-company', 'NOTEXIST-999');
206
+ * if (result.isErr()) {
207
+ * if (result.error.kind === 'NOT_FOUND') {
208
+ * console.log('Issue が見つかりません');
209
+ * }
210
+ * }
211
+ * ```
212
+ */
213
+ export async function fetchJiraIssue(organization, issueKey) {
214
+ // 認証ヘッダーを取得
215
+ const authHeaderResult = getAuthHeader();
216
+ if (authHeaderResult.isErr()) {
217
+ return err({
218
+ kind: 'AUTH_FAILED',
219
+ message: authHeaderResult.error.message,
220
+ });
221
+ }
222
+ const authHeader = authHeaderResult.value;
223
+ const baseUrl = getJiraApiBaseUrl(organization);
224
+ const url = `${baseUrl}/issue/${issueKey}?expand=changelog`;
225
+ // API リクエストを実行
226
+ const response = await httpRequest(url, {
227
+ headers: {
228
+ Accept: 'application/json',
229
+ Authorization: authHeader,
230
+ },
231
+ });
232
+ if (response.isErr()) {
233
+ const error = response.error;
234
+ if (error.kind === 'HTTP_ERROR') {
235
+ return err(mapHttpStatusToJiraError(error.status, error.message));
236
+ }
237
+ return err({
238
+ kind: 'NETWORK_ERROR',
239
+ message: error.message,
240
+ });
241
+ }
242
+ // レスポンスをパース
243
+ const parseResult = jiraApiResponseSchema.safeParse(response.value.data);
244
+ if (!parseResult.success) {
245
+ return err({
246
+ kind: 'PARSE_ERROR',
247
+ message: `API レスポンスのパースに失敗しました: ${parseResult.error.message}`,
248
+ });
249
+ }
250
+ const apiResponse = parseResult.data;
251
+ // JiraIssue に変換
252
+ const issue = {
253
+ attachments: (apiResponse.fields.attachment ?? []).map(mapApiAttachmentToJiraAttachment),
254
+ changelog: (apiResponse.changelog?.histories ?? []).map(mapApiChangelogEntryToJiraChangelogEntry),
255
+ comments: (apiResponse.fields.comment?.comments ?? []).map(mapApiCommentToJiraComment),
256
+ description: apiResponse.fields.description ? extractTextFromAdf(apiResponse.fields.description) : null,
257
+ key: apiResponse.key,
258
+ summary: apiResponse.fields.summary,
259
+ };
260
+ return ok(issue);
261
+ }
262
+ /**
263
+ * HTTP エラーステータスをダウンロード用の JiraError に変換する
264
+ *
265
+ * @param status - HTTP ステータスコード
266
+ * @returns JiraError
267
+ *
268
+ * @internal テスト用に export
269
+ */
270
+ export function mapHttpStatusToDownloadError(status) {
271
+ switch (status) {
272
+ case 401:
273
+ return {
274
+ kind: 'AUTH_FAILED',
275
+ message: '認証に失敗しました。API トークンとメールアドレスを確認してください。',
276
+ };
277
+ case 403:
278
+ return {
279
+ kind: 'FORBIDDEN',
280
+ message: 'この添付ファイルへのアクセス権限がありません。権限を確認してください。',
281
+ };
282
+ case 404:
283
+ return {
284
+ kind: 'NOT_FOUND',
285
+ message: '指定された添付ファイルが見つかりません。',
286
+ };
287
+ default:
288
+ return {
289
+ kind: 'NETWORK_ERROR',
290
+ message: `添付ファイルのダウンロードに失敗しました(HTTP ${status})`,
291
+ };
292
+ }
293
+ }
294
+ /**
295
+ * Jira 添付ファイルをダウンロードする
296
+ *
297
+ * Jira Cloud API を使用して添付ファイルを指定パスにダウンロードする。
298
+ *
299
+ * @param attachment - ダウンロード対象の添付ファイル情報
300
+ * @param destPath - 保存先ファイルパス
301
+ * @param onProgress - 進捗コールバック関数(オプション)
302
+ * @returns 成功時は void を含む Ok、失敗時は {@link JiraError} を含む Err
303
+ *
304
+ * @example
305
+ * ```typescript
306
+ * // 基本的な使用例
307
+ * const attachment = issue.attachments[0];
308
+ * const result = await downloadJiraAttachment(attachment, '/path/to/save/file.png');
309
+ * if (result.isOk()) {
310
+ * console.log('ダウンロード完了');
311
+ * }
312
+ * ```
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * // 進捗表示付き
317
+ * const result = await downloadJiraAttachment(
318
+ * attachment,
319
+ * '/path/to/save/file.png',
320
+ * (transferred, total) => {
321
+ * if (total) {
322
+ * console.log(`Progress: ${(transferred / total * 100).toFixed(1)}%`);
323
+ * }
324
+ * }
325
+ * );
326
+ * ```
327
+ */
328
+ export async function downloadJiraAttachment(attachment, destPath, onProgress) {
329
+ // 認証ヘッダーを取得
330
+ const authHeaderResult = getAuthHeader();
331
+ if (authHeaderResult.isErr()) {
332
+ return err({
333
+ kind: 'AUTH_FAILED',
334
+ message: authHeaderResult.error.message,
335
+ });
336
+ }
337
+ const authHeader = authHeaderResult.value;
338
+ // ファイルをダウンロード
339
+ const downloadResult = await httpDownload(attachment.contentUrl, destPath, {
340
+ Accept: '*/*',
341
+ Authorization: authHeader,
342
+ }, onProgress);
343
+ if (downloadResult.isErr()) {
344
+ const error = downloadResult.error;
345
+ if (error.kind === 'HTTP_ERROR') {
346
+ return err(mapHttpStatusToDownloadError(error.status));
347
+ }
348
+ return err({
349
+ kind: 'NETWORK_ERROR',
350
+ message: error.message,
351
+ });
352
+ }
353
+ return ok(undefined);
354
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 出力サービスのエントリーポイント
3
+ */
4
+ export { formatConfluencePage, formatJiraIssue, showProgress, writeToFile } from './output-service.js';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 出力サービスのエントリーポイント
3
+ */
4
+ export { formatConfluencePage, formatJiraIssue, showProgress, writeToFile } from './output-service.js';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 出力サービス
3
+ *
4
+ * 取得したデータを指定形式(JSON/Markdown/YAML)で出力する。
5
+ */
6
+ import { type Result } from 'neverthrow';
7
+ import type { ConfluencePage } from '../../types/confluence.js';
8
+ import type { JiraIssue } from '../../types/jira.js';
9
+ import type { OutputError, OutputFormat } from '../../types/output.js';
10
+ /**
11
+ * Jira Issue を指定形式でフォーマットする
12
+ *
13
+ * @param issue Jira Issue
14
+ * @param options 出力オプション
15
+ * @returns フォーマット済み文字列
16
+ */
17
+ export declare const formatJiraIssue: (issue: JiraIssue, options: {
18
+ format: OutputFormat;
19
+ }) => Result<string, OutputError>;
20
+ /**
21
+ * Confluence ページを指定形式でフォーマットする
22
+ *
23
+ * @param page Confluence ページ
24
+ * @param options 出力オプション
25
+ * @returns フォーマット済み文字列
26
+ */
27
+ export declare const formatConfluencePage: (page: ConfluencePage, options: {
28
+ format: OutputFormat;
29
+ }) => Result<string, OutputError>;
30
+ /**
31
+ * ダウンロード進捗を標準エラー出力に表示する
32
+ *
33
+ * 添付ファイルダウンロード時に進捗を表示する。
34
+ * 同じ行で更新するためにキャリッジリターン(\r)を使用し、
35
+ * 完了時(current === total)には改行を追加する。
36
+ *
37
+ * @param message 進捗メッセージ
38
+ * @param current 現在の進捗位置
39
+ * @param total 合計数
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * // ダウンロード進捗を表示
44
+ * showProgress('Downloading', 1, 3); // "\rDownloading: 1/3 (33%)"
45
+ * showProgress('Downloading', 2, 3); // "\rDownloading: 2/3 (67%)"
46
+ * showProgress('Downloading', 3, 3); // "\rDownloading: 3/3 (100%)\n"
47
+ * ```
48
+ */
49
+ export declare const showProgress: (message: string, current: number, total: number) => void;
50
+ /**
51
+ * コンテンツをファイルに書き込む
52
+ *
53
+ * 親ディレクトリを作成してから、指定されたパスにコンテンツを書き込む。
54
+ *
55
+ * @param content 書き込むコンテンツ
56
+ * @param outputPath 出力先ファイルパス
57
+ * @returns 成功時は Ok(void)、失敗時は Err(OutputError)
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * const result = await writeToFile('{"key": "value"}', '/path/to/output.json');
62
+ * if (result.isOk()) {
63
+ * console.log('ファイルを書き込みました');
64
+ * }
65
+ * ```
66
+ */
67
+ export declare const writeToFile: (content: string, outputPath: string) => Promise<Result<void, OutputError>>;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * 出力サービス
3
+ *
4
+ * 取得したデータを指定形式(JSON/Markdown/YAML)で出力する。
5
+ */
6
+ import { dirname } from 'node:path';
7
+ import { err, ok } from 'neverthrow';
8
+ import { stringify as yamlStringify } from 'yaml';
9
+ import { ensureDir, writeFileContent } from '../../ports/file/file-port.js';
10
+ /**
11
+ * Jira Issue を Markdown 形式にフォーマットする
12
+ *
13
+ * @param issue Jira Issue
14
+ * @returns Markdown 文字列
15
+ */
16
+ const formatJiraIssueAsMarkdown = (issue) => {
17
+ const lines = [];
18
+ // Title
19
+ lines.push(`# ${issue.key}: ${issue.summary}`);
20
+ lines.push('');
21
+ // Description
22
+ lines.push('## Description');
23
+ lines.push('');
24
+ lines.push(issue.description ?? 'N/A');
25
+ lines.push('');
26
+ // Comments
27
+ lines.push('## Comments');
28
+ lines.push('');
29
+ if (issue.comments.length === 0) {
30
+ lines.push('No comments');
31
+ }
32
+ else {
33
+ for (const comment of issue.comments) {
34
+ lines.push(`### ${comment.author} (${comment.created})`);
35
+ lines.push('');
36
+ lines.push(comment.body);
37
+ lines.push('');
38
+ }
39
+ }
40
+ // Changelog
41
+ lines.push('## Changelog');
42
+ lines.push('');
43
+ if (issue.changelog.length === 0) {
44
+ lines.push('No changes');
45
+ }
46
+ else {
47
+ for (const entry of issue.changelog) {
48
+ lines.push(`### ${entry.author} (${entry.created})`);
49
+ lines.push('');
50
+ for (const item of entry.items) {
51
+ const from = item.fromString ?? '(none)';
52
+ const to = item.toString ?? '(none)';
53
+ lines.push(`- **${item.field}**: ${from} → ${to}`);
54
+ }
55
+ lines.push('');
56
+ }
57
+ }
58
+ // Attachments
59
+ lines.push('## Attachments');
60
+ lines.push('');
61
+ if (issue.attachments.length === 0) {
62
+ lines.push('No attachments');
63
+ }
64
+ else {
65
+ for (const attachment of issue.attachments) {
66
+ const sizeKB = (attachment.size / 1024).toFixed(1);
67
+ lines.push(`- **${attachment.filename}** (${attachment.mimeType}, ${sizeKB} KB)`);
68
+ }
69
+ }
70
+ return lines.join('\n');
71
+ };
72
+ /**
73
+ * Jira Issue を指定形式でフォーマットする
74
+ *
75
+ * @param issue Jira Issue
76
+ * @param options 出力オプション
77
+ * @returns フォーマット済み文字列
78
+ */
79
+ export const formatJiraIssue = (issue, options) => {
80
+ if (options.format === 'json') {
81
+ return ok(JSON.stringify(issue, null, 2));
82
+ }
83
+ if (options.format === 'markdown') {
84
+ return ok(formatJiraIssueAsMarkdown(issue));
85
+ }
86
+ // YAML 形式
87
+ return ok(yamlStringify(issue));
88
+ };
89
+ /**
90
+ * HTML タグを除去してプレーンテキストを抽出する
91
+ *
92
+ * @param html HTML 文字列
93
+ * @returns プレーンテキスト
94
+ */
95
+ const stripHtmlTags = (html) => {
96
+ return html.replaceAll(/<[^>]*>/g, '');
97
+ };
98
+ /**
99
+ * Confluence ページを Markdown 形式にフォーマットする
100
+ *
101
+ * @param page Confluence ページ
102
+ * @returns Markdown 文字列
103
+ */
104
+ const formatConfluencePageAsMarkdown = (page) => {
105
+ const lines = [];
106
+ // Title
107
+ lines.push(`# ${page.title}`);
108
+ lines.push('');
109
+ // Metadata
110
+ lines.push(`**Page ID**: ${page.id}`);
111
+ lines.push(`**Space**: ${page.spaceKey}`);
112
+ lines.push(`**Version**: ${String(page.currentVersion)}`);
113
+ lines.push('');
114
+ // Content
115
+ lines.push('## Content');
116
+ lines.push('');
117
+ lines.push(stripHtmlTags(page.body));
118
+ lines.push('');
119
+ // Version History
120
+ lines.push('## Version History');
121
+ lines.push('');
122
+ if (page.versions.length === 0) {
123
+ lines.push('No version history');
124
+ }
125
+ else {
126
+ for (const version of page.versions) {
127
+ const message = version.message !== null ? `: ${version.message}` : '';
128
+ lines.push(`### Version ${String(version.number)} by ${version.by} (${version.when})${message}`);
129
+ lines.push('');
130
+ }
131
+ }
132
+ // Attachments
133
+ lines.push('## Attachments');
134
+ lines.push('');
135
+ if (page.attachments.length === 0) {
136
+ lines.push('No attachments');
137
+ }
138
+ else {
139
+ for (const attachment of page.attachments) {
140
+ const sizeKB = (attachment.fileSize / 1024).toFixed(1);
141
+ lines.push(`- **${attachment.title}** (${attachment.mediaType}, ${sizeKB} KB)`);
142
+ }
143
+ }
144
+ return lines.join('\n');
145
+ };
146
+ /**
147
+ * Confluence ページを指定形式でフォーマットする
148
+ *
149
+ * @param page Confluence ページ
150
+ * @param options 出力オプション
151
+ * @returns フォーマット済み文字列
152
+ */
153
+ export const formatConfluencePage = (page, options) => {
154
+ if (options.format === 'json') {
155
+ return ok(JSON.stringify(page, null, 2));
156
+ }
157
+ if (options.format === 'markdown') {
158
+ return ok(formatConfluencePageAsMarkdown(page));
159
+ }
160
+ // YAML 形式
161
+ return ok(yamlStringify(page));
162
+ };
163
+ /**
164
+ * ダウンロード進捗を標準エラー出力に表示する
165
+ *
166
+ * 添付ファイルダウンロード時に進捗を表示する。
167
+ * 同じ行で更新するためにキャリッジリターン(\r)を使用し、
168
+ * 完了時(current === total)には改行を追加する。
169
+ *
170
+ * @param message 進捗メッセージ
171
+ * @param current 現在の進捗位置
172
+ * @param total 合計数
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * // ダウンロード進捗を表示
177
+ * showProgress('Downloading', 1, 3); // "\rDownloading: 1/3 (33%)"
178
+ * showProgress('Downloading', 2, 3); // "\rDownloading: 2/3 (67%)"
179
+ * showProgress('Downloading', 3, 3); // "\rDownloading: 3/3 (100%)\n"
180
+ * ```
181
+ */
182
+ export const showProgress = (message, current, total) => {
183
+ // パーセンテージを計算(total が 0 の場合は 0%)
184
+ const percentage = total === 0 ? 0 : Math.min(100, Math.floor((current / total) * 100));
185
+ // 進捗メッセージを構築
186
+ const progressMessage = `\r${message}: ${String(current)}/${String(total)} (${String(percentage)}%)`;
187
+ // 完了時は改行を追加
188
+ const output = current >= total && total > 0 ? `${progressMessage}\n` : progressMessage;
189
+ // 標準エラー出力に書き込む
190
+ process.stderr.write(output);
191
+ };
192
+ /**
193
+ * コンテンツをファイルに書き込む
194
+ *
195
+ * 親ディレクトリを作成してから、指定されたパスにコンテンツを書き込む。
196
+ *
197
+ * @param content 書き込むコンテンツ
198
+ * @param outputPath 出力先ファイルパス
199
+ * @returns 成功時は Ok(void)、失敗時は Err(OutputError)
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const result = await writeToFile('{"key": "value"}', '/path/to/output.json');
204
+ * if (result.isOk()) {
205
+ * console.log('ファイルを書き込みました');
206
+ * }
207
+ * ```
208
+ */
209
+ export const writeToFile = async (content, outputPath) => {
210
+ const parentDir = dirname(outputPath);
211
+ // 親ディレクトリを作成
212
+ const ensureDirResult = await ensureDir(parentDir);
213
+ if (ensureDirResult.isErr()) {
214
+ return err({
215
+ kind: 'WRITE_FAILED',
216
+ message: `ディレクトリの作成に失敗しました: ${parentDir} - ${ensureDirResult.error.message}`,
217
+ });
218
+ }
219
+ // ファイルに書き込む
220
+ const writeResult = await writeFileContent(outputPath, content);
221
+ if (writeResult.isErr()) {
222
+ return err({
223
+ kind: 'WRITE_FAILED',
224
+ message: `ファイルの書き込みに失敗しました: ${outputPath} - ${writeResult.error.message}`,
225
+ });
226
+ }
227
+ return ok(undefined);
228
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * ストレージサービス
3
+ *
4
+ * Jira Issue / Confluence ページをディレクトリ構造で保存する。
5
+ */
6
+ export { saveConfluencePage, saveConfluenceVersions, saveJiraIssue } from './storage-service.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * ストレージサービス
3
+ *
4
+ * Jira Issue / Confluence ページをディレクトリ構造で保存する。
5
+ */
6
+ export { saveConfluencePage, saveConfluenceVersions, saveJiraIssue } from './storage-service.js';