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.
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/cli/cli.d.ts +61 -0
- package/dist/cli/cli.js +131 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +4 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/ports/file/file-port.d.ts +89 -0
- package/dist/ports/file/file-port.js +155 -0
- package/dist/ports/file/index.d.ts +1 -0
- package/dist/ports/file/index.js +1 -0
- package/dist/ports/http/http-port.d.ts +107 -0
- package/dist/ports/http/http-port.js +238 -0
- package/dist/ports/http/index.d.ts +1 -0
- package/dist/ports/http/index.js +1 -0
- package/dist/services/auth/auth-service.d.ts +79 -0
- package/dist/services/auth/auth-service.js +158 -0
- package/dist/services/auth/index.d.ts +1 -0
- package/dist/services/auth/index.js +1 -0
- package/dist/services/confluence/confluence-service.d.ts +152 -0
- package/dist/services/confluence/confluence-service.js +510 -0
- package/dist/services/confluence/index.d.ts +1 -0
- package/dist/services/confluence/index.js +1 -0
- package/dist/services/diff/diff-service.d.ts +84 -0
- package/dist/services/diff/diff-service.js +881 -0
- package/dist/services/diff/index.d.ts +1 -0
- package/dist/services/diff/index.js +1 -0
- package/dist/services/fetch/fetch-service.d.ts +112 -0
- package/dist/services/fetch/fetch-service.js +302 -0
- package/dist/services/fetch/index.d.ts +1 -0
- package/dist/services/fetch/index.js +1 -0
- package/dist/services/jira/index.d.ts +1 -0
- package/dist/services/jira/index.js +1 -0
- package/dist/services/jira/jira-service.d.ts +100 -0
- package/dist/services/jira/jira-service.js +354 -0
- package/dist/services/output/index.d.ts +4 -0
- package/dist/services/output/index.js +4 -0
- package/dist/services/output/output-service.d.ts +67 -0
- package/dist/services/output/output-service.js +228 -0
- package/dist/services/storage/index.d.ts +6 -0
- package/dist/services/storage/index.js +6 -0
- package/dist/services/storage/storage-service.d.ts +77 -0
- package/dist/services/storage/storage-service.js +738 -0
- package/dist/services/text-converter/index.d.ts +1 -0
- package/dist/services/text-converter/index.js +1 -0
- package/dist/services/text-converter/text-converter.d.ts +35 -0
- package/dist/services/text-converter/text-converter.js +681 -0
- package/dist/services/url-parser/index.d.ts +1 -0
- package/dist/services/url-parser/index.js +1 -0
- package/dist/services/url-parser/url-parser.d.ts +43 -0
- package/dist/services/url-parser/url-parser.js +283 -0
- package/dist/types/auth.d.ts +25 -0
- package/dist/types/auth.js +1 -0
- package/dist/types/confluence.d.ts +68 -0
- package/dist/types/confluence.js +1 -0
- package/dist/types/diff.d.ts +77 -0
- package/dist/types/diff.js +7 -0
- package/dist/types/fetch.d.ts +65 -0
- package/dist/types/fetch.js +1 -0
- package/dist/types/file.d.ts +22 -0
- package/dist/types/file.js +1 -0
- package/dist/types/http.d.ts +45 -0
- package/dist/types/http.js +1 -0
- package/dist/types/jira.d.ts +90 -0
- package/dist/types/jira.js +1 -0
- package/dist/types/output.d.ts +55 -0
- package/dist/types/output.js +7 -0
- package/dist/types/result.d.ts +104 -0
- package/dist/types/result.js +119 -0
- package/dist/types/storage.d.ts +209 -0
- package/dist/types/storage.js +6 -0
- package/dist/types/url-parser.d.ts +46 -0
- package/dist/types/url-parser.js +1 -0
- 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,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
|
+
};
|