backlog-exporter 0.7.2 → 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/README.md +87 -1
- package/bin/dev.js +6 -2
- package/dist/commands/all/index.d.ts +2 -0
- package/dist/commands/all/index.js +62 -126
- package/dist/commands/document/index.js +18 -11
- package/dist/commands/issue/index.js +18 -11
- package/dist/commands/prune/index.d.ts +16 -0
- package/dist/commands/prune/index.js +86 -0
- package/dist/commands/update/index.d.ts +3 -7
- package/dist/commands/update/index.js +28 -260
- package/dist/commands/wiki/index.js +17 -9
- package/dist/composition/backlog-repositories.d.ts +16 -0
- package/dist/composition/backlog-repositories.js +15 -0
- package/dist/modules/all/domain/content-selection.d.ts +4 -0
- package/dist/modules/all/domain/content-selection.js +16 -0
- package/dist/modules/all/use-case/export-all.d.ts +24 -0
- package/dist/modules/all/use-case/export-all.js +78 -0
- package/dist/modules/document/domain/document-markdown.d.ts +2 -0
- package/dist/modules/document/domain/document-markdown.js +37 -0
- package/dist/modules/document/domain/document-path.d.ts +15 -0
- package/dist/modules/document/domain/document-path.js +48 -0
- package/dist/modules/document/domain/document-repository.d.ts +6 -0
- package/dist/modules/document/domain/document-repository.js +1 -0
- package/dist/modules/document/domain/document-save-plan.d.ts +9 -0
- package/dist/modules/document/domain/document-save-plan.js +15 -0
- package/dist/modules/document/domain/document.d.ts +50 -0
- package/dist/modules/document/domain/document.js +1 -0
- package/dist/modules/document/repository/backlog-document-repository.d.ts +3 -0
- package/dist/modules/document/repository/backlog-document-repository.js +31 -0
- package/dist/modules/document/use-case/export-documents.d.ts +16 -0
- package/dist/modules/document/use-case/export-documents.js +82 -0
- package/dist/modules/issue/domain/issue-filter.d.ts +2 -0
- package/dist/modules/issue/domain/issue-filter.js +7 -0
- package/dist/modules/issue/domain/issue-markdown.d.ts +4 -0
- package/dist/modules/issue/domain/issue-markdown.js +112 -0
- package/dist/modules/issue/domain/issue-path.d.ts +23 -0
- package/dist/modules/issue/domain/issue-path.js +30 -0
- package/dist/modules/issue/domain/issue-repository.d.ts +11 -0
- package/dist/modules/issue/domain/issue-repository.js +1 -0
- package/dist/modules/issue/domain/issue.d.ts +47 -0
- package/dist/modules/issue/domain/issue.js +1 -0
- package/dist/modules/issue/repository/backlog-issue-repository.d.ts +3 -0
- package/dist/modules/issue/repository/backlog-issue-repository.js +38 -0
- package/dist/modules/issue/use-case/export-issues.d.ts +18 -0
- package/dist/modules/issue/use-case/export-issues.js +87 -0
- package/dist/modules/project/domain/project-repository.d.ts +3 -0
- package/dist/modules/project/domain/project-repository.js +1 -0
- package/dist/modules/project/repository/backlog-project-repository.d.ts +3 -0
- package/dist/modules/project/repository/backlog-project-repository.js +17 -0
- package/dist/modules/prune/domain/expected-paths.d.ts +4 -0
- package/dist/modules/prune/domain/expected-paths.js +1 -0
- package/dist/modules/prune/domain/prune-target.d.ts +3 -0
- package/dist/modules/prune/domain/prune-target.js +21 -0
- package/dist/modules/prune/repository/prune-walker.d.ts +7 -0
- package/dist/modules/prune/repository/prune-walker.js +39 -0
- package/dist/modules/prune/use-case/prune-directories.d.ts +28 -0
- package/dist/modules/prune/use-case/prune-directories.js +74 -0
- package/dist/modules/prune/use-case/prune-exports.d.ts +27 -0
- package/dist/modules/prune/use-case/prune-exports.js +74 -0
- package/dist/modules/settings/domain/settings.d.ts +15 -0
- package/dist/modules/settings/domain/settings.js +6 -0
- package/dist/modules/settings/repository/settings-store.d.ts +6 -0
- package/dist/modules/settings/repository/settings-store.js +56 -0
- package/dist/modules/update/domain/update-plan.d.ts +36 -0
- package/dist/modules/update/domain/update-plan.js +69 -0
- package/dist/modules/update/use-case/update-exports.d.ts +21 -0
- package/dist/modules/update/use-case/update-exports.js +136 -0
- package/dist/modules/wiki/domain/wiki-filter.d.ts +9 -0
- package/dist/modules/wiki/domain/wiki-filter.js +11 -0
- package/dist/modules/wiki/domain/wiki-markdown.d.ts +1 -0
- package/dist/modules/wiki/domain/wiki-markdown.js +4 -0
- package/dist/modules/wiki/domain/wiki-path.d.ts +4 -0
- package/dist/modules/wiki/domain/wiki-path.js +25 -0
- package/dist/modules/wiki/domain/wiki-repository.d.ts +5 -0
- package/dist/modules/wiki/domain/wiki-repository.js +1 -0
- package/dist/modules/wiki/domain/wiki.d.ts +10 -0
- package/dist/modules/wiki/domain/wiki.js +1 -0
- package/dist/modules/wiki/repository/backlog-wiki-repository.d.ts +3 -0
- package/dist/modules/wiki/repository/backlog-wiki-repository.js +10 -0
- package/dist/modules/wiki/use-case/export-wikis.d.ts +14 -0
- package/dist/modules/wiki/use-case/export-wikis.js +43 -0
- package/dist/shared/backlog/http-client.d.ts +17 -0
- package/dist/shared/backlog/http-client.js +62 -0
- package/dist/shared/backlog/rate-limiter.d.ts +9 -0
- package/dist/shared/backlog/rate-limiter.js +25 -0
- package/dist/shared/backlog-url.d.ts +1 -0
- package/dist/shared/backlog-url.js +5 -0
- package/dist/shared/config/env.d.ts +3 -0
- package/dist/shared/config/env.js +21 -0
- package/dist/shared/console/progress.d.ts +1 -0
- package/dist/shared/console/progress.js +4 -0
- package/dist/shared/console/prompt.d.ts +3 -0
- package/dist/shared/console/prompt.js +19 -0
- package/dist/shared/file-name.d.ts +2 -0
- package/dist/shared/file-name.js +15 -0
- package/dist/shared/markdown/body-marker.d.ts +3 -0
- package/dist/shared/markdown/body-marker.js +5 -0
- package/dist/shared/ports.d.ts +4 -0
- package/dist/shared/ports.js +1 -0
- package/dist/shared/storage/markdown-store.d.ts +5 -0
- package/dist/shared/storage/markdown-store.js +21 -0
- package/dist/shared/storage/update-log.d.ts +1 -0
- package/dist/{utils/log.js → shared/storage/update-log.js} +1 -5
- package/oclif.manifest.json +118 -26
- package/package.json +15 -19
- package/dist/utils/backlog-api.d.ts +0 -74
- package/dist/utils/backlog-api.js +0 -448
- package/dist/utils/backlog.d.ts +0 -16
- package/dist/utils/backlog.js +0 -34
- package/dist/utils/common.d.ts +0 -25
- package/dist/utils/common.js +0 -54
- package/dist/utils/log.d.ts +0 -6
- package/dist/utils/settings.d.ts +0 -45
- package/dist/utils/settings.js +0 -63
- package/dist/utils/sleep.d.ts +0 -28
- package/dist/utils/sleep.js +0 -42
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backlog-exporter",
|
|
3
3
|
"description": "A new CLI generated with oclif",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"author": "ShuntaToda",
|
|
6
6
|
"bin": {
|
|
7
7
|
"backlog-exporter": "bin/run.js"
|
|
@@ -10,31 +10,25 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@oclif/core": "^4",
|
|
12
12
|
"@oclif/plugin-help": "^6",
|
|
13
|
-
"@oclif/plugin-
|
|
14
|
-
"
|
|
15
|
-
"ky": "^1.7.5"
|
|
13
|
+
"@oclif/plugin-not-found": "^3.2.88",
|
|
14
|
+
"@oclif/plugin-plugins": "^5"
|
|
16
15
|
},
|
|
17
16
|
"devDependencies": {
|
|
18
17
|
"@eslint/compat": "^1",
|
|
19
18
|
"@oclif/prettier-config": "^0.2.1",
|
|
20
19
|
"@oclif/test": "^4",
|
|
21
|
-
"@types/
|
|
22
|
-
"@types/mocha": "^10",
|
|
23
|
-
"@types/node": "^18",
|
|
24
|
-
"chai": "^4",
|
|
20
|
+
"@types/node": "^22.20.0",
|
|
25
21
|
"eslint": "^9",
|
|
26
22
|
"eslint-config-oclif": "^6",
|
|
27
23
|
"eslint-config-prettier": "^10",
|
|
28
|
-
"mocha": "^10",
|
|
29
|
-
"nock": "^14.0.1",
|
|
30
24
|
"oclif": "^4",
|
|
31
25
|
"prettier": "^3.6.2",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
26
|
+
"tsx": "^4.23.0",
|
|
27
|
+
"typescript": "^5",
|
|
28
|
+
"vitest": "^4.1.10"
|
|
35
29
|
},
|
|
36
30
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
31
|
+
"node": ">=22.0.0"
|
|
38
32
|
},
|
|
39
33
|
"files": [
|
|
40
34
|
"./bin",
|
|
@@ -54,7 +48,8 @@
|
|
|
54
48
|
"commands": "./dist/commands",
|
|
55
49
|
"plugins": [
|
|
56
50
|
"@oclif/plugin-help",
|
|
57
|
-
"@oclif/plugin-plugins"
|
|
51
|
+
"@oclif/plugin-plugins",
|
|
52
|
+
"@oclif/plugin-not-found"
|
|
58
53
|
],
|
|
59
54
|
"topicSeparator": " ",
|
|
60
55
|
"topics": {
|
|
@@ -68,15 +63,16 @@
|
|
|
68
63
|
"url": "git+https://github.com/ShuntaToda/backlog-exporter.git"
|
|
69
64
|
},
|
|
70
65
|
"scripts": {
|
|
71
|
-
"build": "
|
|
66
|
+
"build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
72
67
|
"format": "prettier --write .",
|
|
73
68
|
"format:check": "prettier --check .",
|
|
74
69
|
"lint": "eslint",
|
|
75
|
-
"postpack": "
|
|
70
|
+
"postpack": "node -e \"fs.rmSync('oclif.manifest.json',{force:true})\"",
|
|
76
71
|
"posttest": "npm run lint",
|
|
77
72
|
"prepack": "oclif manifest && oclif readme",
|
|
78
|
-
"test": "
|
|
79
|
-
"version": "oclif readme && git add README.md"
|
|
73
|
+
"test": "vitest run",
|
|
74
|
+
"version": "oclif readme && git add README.md",
|
|
75
|
+
"test:e2e": "npm run build && vitest run --config vitest.e2e.config.ts"
|
|
80
76
|
},
|
|
81
77
|
"types": "dist/index.d.ts"
|
|
82
78
|
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
|
-
/**
|
|
3
|
-
* カスタム属性セクションを作成する
|
|
4
|
-
* @param customFields カスタム属性の配列
|
|
5
|
-
* @returns Markdownテーブル形式のカスタム属性セクション
|
|
6
|
-
*/
|
|
7
|
-
export declare function createCustomFieldsSection(customFields?: Array<{
|
|
8
|
-
id: number;
|
|
9
|
-
name: string;
|
|
10
|
-
value: unknown;
|
|
11
|
-
}>): string;
|
|
12
|
-
/**
|
|
13
|
-
* Backlogから課題をダウンロードする
|
|
14
|
-
* @param command コマンドインスタンス
|
|
15
|
-
* @param options 課題ダウンロードのオプション
|
|
16
|
-
* @param options.apiKey Backlog API key
|
|
17
|
-
* @param options.count 取得する課題の最大数
|
|
18
|
-
* @param options.domain Backlogのドメイン
|
|
19
|
-
* @param options.lastUpdated 最終更新日時
|
|
20
|
-
* @param options.outputDir 出力ディレクトリ
|
|
21
|
-
* @param options.projectId プロジェクトID
|
|
22
|
-
* @param options.statusId ステータスID
|
|
23
|
-
* @param options.issueKeyFileName ファイル名を課題キーにするかどうか
|
|
24
|
-
* @param options.issueKeyFolder 課題キーでフォルダを作成するかどうか
|
|
25
|
-
*/
|
|
26
|
-
export declare function downloadIssues(command: Command, options: {
|
|
27
|
-
apiKey: string;
|
|
28
|
-
count?: number;
|
|
29
|
-
domain: string;
|
|
30
|
-
issueKeyFileName?: boolean;
|
|
31
|
-
issueKeyFolder?: boolean;
|
|
32
|
-
lastUpdated?: string;
|
|
33
|
-
outputDir: string;
|
|
34
|
-
projectId: number;
|
|
35
|
-
statusId?: string;
|
|
36
|
-
}): Promise<void>;
|
|
37
|
-
/**
|
|
38
|
-
* BacklogからWikiをダウンロードする
|
|
39
|
-
* @param command コマンドインスタンス
|
|
40
|
-
* @param options Wikiダウンロードのオプション
|
|
41
|
-
* @param options.apiKey Backlog API key
|
|
42
|
-
* @param options.domain Backlogのドメイン
|
|
43
|
-
* @param options.lastUpdated 最終更新日時
|
|
44
|
-
* @param options.outputDir 出力ディレクトリ
|
|
45
|
-
* @param options.projectIdOrKey プロジェクトIDまたはキー
|
|
46
|
-
*/
|
|
47
|
-
export declare function downloadWikis(command: Command, options: {
|
|
48
|
-
apiKey: string;
|
|
49
|
-
domain: string;
|
|
50
|
-
lastUpdated?: string;
|
|
51
|
-
outputDir: string;
|
|
52
|
-
projectIdOrKey: string;
|
|
53
|
-
}): Promise<void>;
|
|
54
|
-
/**
|
|
55
|
-
* Backlogからドキュメントをダウンロードする
|
|
56
|
-
* @param command コマンドインスタンス
|
|
57
|
-
* @param options ドキュメントダウンロードのオプション
|
|
58
|
-
* @param options.apiKey Backlog API key
|
|
59
|
-
* @param options.domain Backlogのドメイン
|
|
60
|
-
* @param options.keyword キーワードフィルター
|
|
61
|
-
* @param options.lastUpdated 最終更新日時
|
|
62
|
-
* @param options.outputDir 出力ディレクトリ
|
|
63
|
-
* @param options.projectId プロジェクトID
|
|
64
|
-
* @param options.projectIdOrKey プロジェクトIDまたはキー
|
|
65
|
-
*/
|
|
66
|
-
export declare function downloadDocuments(command: Command, options: {
|
|
67
|
-
apiKey: string;
|
|
68
|
-
domain: string;
|
|
69
|
-
keyword?: string;
|
|
70
|
-
lastUpdated?: string;
|
|
71
|
-
outputDir: string;
|
|
72
|
-
projectId: number;
|
|
73
|
-
projectIdOrKey: string;
|
|
74
|
-
}): Promise<void>;
|
|
@@ -1,448 +0,0 @@
|
|
|
1
|
-
import ky from 'ky';
|
|
2
|
-
import * as fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import process from 'node:process';
|
|
5
|
-
import { sanitizeFileName, sanitizeWikiFileName } from './common.js';
|
|
6
|
-
import { appendLog } from './log.js';
|
|
7
|
-
import { RateLimiter } from './sleep.js';
|
|
8
|
-
/**
|
|
9
|
-
* カスタム属性セクションを作成する
|
|
10
|
-
* @param customFields カスタム属性の配列
|
|
11
|
-
* @returns Markdownテーブル形式のカスタム属性セクション
|
|
12
|
-
*/
|
|
13
|
-
export function createCustomFieldsSection(customFields) {
|
|
14
|
-
if (!customFields || customFields.length === 0) {
|
|
15
|
-
return '';
|
|
16
|
-
}
|
|
17
|
-
let customFieldsSection = '\n\n## カスタム属性\n\n| 属性名 | 値 |\n|--------|----|\n';
|
|
18
|
-
for (const customField of customFields) {
|
|
19
|
-
let fieldValue = 'なし';
|
|
20
|
-
if (customField.value !== null && customField.value !== undefined) {
|
|
21
|
-
if (Array.isArray(customField.value)) {
|
|
22
|
-
// 配列の場合(複数選択など)
|
|
23
|
-
fieldValue = customField.value
|
|
24
|
-
.map((item) => item.name || item.value || String(item))
|
|
25
|
-
.join(', ');
|
|
26
|
-
}
|
|
27
|
-
else if (typeof customField.value === 'object' && customField.value !== null) {
|
|
28
|
-
// オブジェクトの場合(単一選択など)
|
|
29
|
-
const valueObj = customField.value;
|
|
30
|
-
fieldValue = valueObj.name || valueObj.value || String(customField.value);
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
// プリミティブ値の場合
|
|
34
|
-
fieldValue = String(customField.value);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
// テーブル内では改行をHTMLの<br>タグに変換し、パイプ文字をエスケープ
|
|
38
|
-
const escapedFieldValue = fieldValue.replaceAll('|', String.raw `\|`).replaceAll('\n', '<br>');
|
|
39
|
-
customFieldsSection += `| ${customField.name} | ${escapedFieldValue} |\n`;
|
|
40
|
-
}
|
|
41
|
-
return customFieldsSection;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Backlogから課題をダウンロードする
|
|
45
|
-
* @param command コマンドインスタンス
|
|
46
|
-
* @param options 課題ダウンロードのオプション
|
|
47
|
-
* @param options.apiKey Backlog API key
|
|
48
|
-
* @param options.count 取得する課題の最大数
|
|
49
|
-
* @param options.domain Backlogのドメイン
|
|
50
|
-
* @param options.lastUpdated 最終更新日時
|
|
51
|
-
* @param options.outputDir 出力ディレクトリ
|
|
52
|
-
* @param options.projectId プロジェクトID
|
|
53
|
-
* @param options.statusId ステータスID
|
|
54
|
-
* @param options.issueKeyFileName ファイル名を課題キーにするかどうか
|
|
55
|
-
* @param options.issueKeyFolder 課題キーでフォルダを作成するかどうか
|
|
56
|
-
*/
|
|
57
|
-
export async function downloadIssues(command, options) {
|
|
58
|
-
const baseUrl = `https://${options.domain}/api/v2`;
|
|
59
|
-
command.log('課題の取得を開始します...');
|
|
60
|
-
// 全ての課題を格納する配列
|
|
61
|
-
let allIssues = [];
|
|
62
|
-
// countのデフォルト値を5000に設定
|
|
63
|
-
const count = options.count ?? 5000;
|
|
64
|
-
const maxCount = Math.min(count, 100); // APIの制限は100件
|
|
65
|
-
// APIリクエスト数をカウントするためのRateLimiterを作成
|
|
66
|
-
const rateLimiter = new RateLimiter(command);
|
|
67
|
-
// 課題を取得する関数
|
|
68
|
-
const fetchIssues = async (offset) => {
|
|
69
|
-
// APIリクエスト数をインクリメント
|
|
70
|
-
await rateLimiter.increment();
|
|
71
|
-
// APIパラメータの構築
|
|
72
|
-
const params = new URLSearchParams({
|
|
73
|
-
apiKey: options.apiKey,
|
|
74
|
-
count: maxCount.toString(),
|
|
75
|
-
offset: offset.toString(),
|
|
76
|
-
'projectId[]': options.projectId.toString(),
|
|
77
|
-
});
|
|
78
|
-
// ステータスIDが指定されている場合は追加
|
|
79
|
-
if (options.statusId) {
|
|
80
|
-
params.append('statusId[]', options.statusId);
|
|
81
|
-
}
|
|
82
|
-
// 進捗状況を一行で更新
|
|
83
|
-
process.stdout.write(`\r課題を取得中... (${allIssues.length}件取得済み)`);
|
|
84
|
-
return ky.get(`${baseUrl}/issues?${params.toString()}`).json();
|
|
85
|
-
};
|
|
86
|
-
// 再帰的に全ての課題を取得
|
|
87
|
-
const fetchAllIssues = async (offset) => {
|
|
88
|
-
try {
|
|
89
|
-
const issues = await fetchIssues(offset);
|
|
90
|
-
// 取得した課題を追加
|
|
91
|
-
allIssues = [...allIssues, ...issues];
|
|
92
|
-
// 次のページがあるかどうかを確認
|
|
93
|
-
if (issues.length === maxCount) {
|
|
94
|
-
// 次のページを取得
|
|
95
|
-
await fetchAllIssues(offset + maxCount);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
command.error(`課題の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
// 課題取得開始
|
|
103
|
-
await fetchAllIssues(0);
|
|
104
|
-
command.log(`\n合計 ${allIssues.length}件の課題が見つかりました。`);
|
|
105
|
-
// 前回の更新日時より新しい課題のみをフィルタリング
|
|
106
|
-
let filteredIssues = allIssues;
|
|
107
|
-
if (options.lastUpdated) {
|
|
108
|
-
const lastUpdatedDate = new Date(options.lastUpdated);
|
|
109
|
-
filteredIssues = allIssues.filter((issue) => {
|
|
110
|
-
const issueUpdatedDate = new Date(issue.updated);
|
|
111
|
-
return issueUpdatedDate > lastUpdatedDate;
|
|
112
|
-
});
|
|
113
|
-
command.log(`前回の更新日時(${options.lastUpdated})以降に更新された${filteredIssues.length}件の課題を処理します。`);
|
|
114
|
-
}
|
|
115
|
-
if (filteredIssues.length === 0) {
|
|
116
|
-
command.log('更新が必要な課題はありません。');
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
// 各課題の詳細情報を取得して保存
|
|
120
|
-
command.log('課題を保存しています...');
|
|
121
|
-
// 並列処理ではなく順次処理に変更
|
|
122
|
-
for (const issue of filteredIssues) {
|
|
123
|
-
try {
|
|
124
|
-
// 進捗状況を一行で更新
|
|
125
|
-
const currentIndex = filteredIssues.indexOf(issue) + 1;
|
|
126
|
-
process.stdout.write(`\r課題を保存中... (${currentIndex}/${filteredIssues.length}件)`);
|
|
127
|
-
// BacklogのIssueへのリンクを作成
|
|
128
|
-
const backlogIssueUrl = `https://${options.domain}/view/${issue.issueKey}`;
|
|
129
|
-
// コメントを取得する関数を呼び出し
|
|
130
|
-
// eslint-disable-next-line no-await-in-loop
|
|
131
|
-
const { comments: allComments } = await fetchAllCommentsForIssue({
|
|
132
|
-
apiKey: options.apiKey,
|
|
133
|
-
baseUrl,
|
|
134
|
-
command,
|
|
135
|
-
issueKey: issue.issueKey,
|
|
136
|
-
rateLimiter,
|
|
137
|
-
});
|
|
138
|
-
// コメントセクションを作成
|
|
139
|
-
let commentsSection = '';
|
|
140
|
-
if (allComments.length > 0) {
|
|
141
|
-
commentsSection = '\n\n## コメント\n';
|
|
142
|
-
let commentIndex = 1;
|
|
143
|
-
for (const comment of allComments) {
|
|
144
|
-
const commentDate = new Date(comment.created).toLocaleString('ja-JP');
|
|
145
|
-
commentsSection += `\n### コメント ${commentIndex}\n- **投稿者**: ${comment.createdUser.name}\n- **日時**: ${commentDate}\n\n${comment.content || '(内容なし)'}\n\n---\n`;
|
|
146
|
-
commentIndex++;
|
|
147
|
-
}
|
|
148
|
-
// 最後の区切り線を削除
|
|
149
|
-
commentsSection = commentsSection.slice(0, -5);
|
|
150
|
-
}
|
|
151
|
-
// 課題の作成年を取得
|
|
152
|
-
const createdYear = new Date(issue.created).getFullYear();
|
|
153
|
-
// 年ごとのフォルダパスを作成
|
|
154
|
-
const yearDirPath = path.join(options.outputDir, createdYear.toString());
|
|
155
|
-
// 年ごとのフォルダを作成
|
|
156
|
-
// eslint-disable-next-line no-await-in-loop
|
|
157
|
-
await fs.mkdir(yearDirPath, { recursive: true });
|
|
158
|
-
let issueFilePath;
|
|
159
|
-
let issueFileName;
|
|
160
|
-
if (options.issueKeyFolder) {
|
|
161
|
-
// 年ごとのフォルダ内に、課題キーでフォルダを作成
|
|
162
|
-
const issueKeyDirPath = path.join(yearDirPath, issue.issueKey);
|
|
163
|
-
// eslint-disable-next-line no-await-in-loop
|
|
164
|
-
await fs.mkdir(issueKeyDirPath, { recursive: true });
|
|
165
|
-
// ファイル名を課題名(標準)にするか課題キーにするかを決定
|
|
166
|
-
issueFileName = options.issueKeyFileName ? `${issue.issueKey}.md` : `${sanitizeFileName(issue.summary)}.md`;
|
|
167
|
-
issueFilePath = path.join(issueKeyDirPath, issueFileName);
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
// 年ごとのフォルダ内に、Markdownファイルを作成
|
|
171
|
-
issueFileName = options.issueKeyFileName ? `${issue.issueKey}.md` : `${sanitizeFileName(issue.summary)}.md`;
|
|
172
|
-
issueFilePath = path.join(yearDirPath, issueFileName);
|
|
173
|
-
}
|
|
174
|
-
// カスタム属性セクションを作成
|
|
175
|
-
const customFieldsSection = createCustomFieldsSection(issue.customFields);
|
|
176
|
-
// Markdownファイルに書き込む
|
|
177
|
-
const assigneeName = issue.assignee ? issue.assignee.name : '未割り当て';
|
|
178
|
-
const markdownContent = `# ${issue.summary}
|
|
179
|
-
|
|
180
|
-
## 基本情報
|
|
181
|
-
- 課題キー: ${issue.issueKey}
|
|
182
|
-
- ステータス: ${issue.status.name}
|
|
183
|
-
- 優先度: ${issue.priority.name}
|
|
184
|
-
- 担当者: ${assigneeName}
|
|
185
|
-
- 作成日時: ${new Date(issue.created).toLocaleString('ja-JP')}
|
|
186
|
-
- 更新日時: ${new Date(issue.updated).toLocaleString('ja-JP')}
|
|
187
|
-
- [Backlog Issue Link](${backlogIssueUrl})${customFieldsSection}
|
|
188
|
-
|
|
189
|
-
## 詳細
|
|
190
|
-
${issue.description || '詳細情報なし'}${commentsSection}`;
|
|
191
|
-
// eslint-disable-next-line no-await-in-loop
|
|
192
|
-
await fs.writeFile(issueFilePath, markdownContent);
|
|
193
|
-
// ログに記録
|
|
194
|
-
// eslint-disable-next-line no-await-in-loop
|
|
195
|
-
await appendLog(options.outputDir, `課題「${issue.summary}」を更新しました: ${backlogIssueUrl}`);
|
|
196
|
-
}
|
|
197
|
-
catch (error) {
|
|
198
|
-
command.warn(`課題 ${issue.issueKey} の保存に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
command.log('\n課題のダウンロードが完了しました!');
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* 課題のコメントを全て取得する
|
|
205
|
-
*/
|
|
206
|
-
async function fetchAllCommentsForIssue({ apiKey, baseUrl, command, issueKey, rateLimiter, }) {
|
|
207
|
-
const allComments = [];
|
|
208
|
-
const fetchComments = async (minId) => {
|
|
209
|
-
// APIリクエスト数をインクリメント
|
|
210
|
-
await rateLimiter.increment();
|
|
211
|
-
let url = `${baseUrl}/issues/${issueKey}/comments?apiKey=${apiKey}&count=100`;
|
|
212
|
-
if (minId) {
|
|
213
|
-
url += `&minId=${minId}`;
|
|
214
|
-
}
|
|
215
|
-
const comments = await ky.get(url).json();
|
|
216
|
-
allComments.push(...comments);
|
|
217
|
-
if (comments.length === 100) {
|
|
218
|
-
// 取得したコメントの最後のIDを次のリクエストのminIdとして使用
|
|
219
|
-
const lastCommentId = comments.at(-1).id;
|
|
220
|
-
await fetchComments(lastCommentId + 1);
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
try {
|
|
224
|
-
await fetchComments();
|
|
225
|
-
// コメントを古い順(昇順)に並び替える
|
|
226
|
-
allComments.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
|
|
227
|
-
return { comments: allComments };
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
command.warn(`課題 ${issueKey} のコメント取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
|
|
231
|
-
return { comments: allComments };
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* BacklogからWikiをダウンロードする
|
|
236
|
-
* @param command コマンドインスタンス
|
|
237
|
-
* @param options Wikiダウンロードのオプション
|
|
238
|
-
* @param options.apiKey Backlog API key
|
|
239
|
-
* @param options.domain Backlogのドメイン
|
|
240
|
-
* @param options.lastUpdated 最終更新日時
|
|
241
|
-
* @param options.outputDir 出力ディレクトリ
|
|
242
|
-
* @param options.projectIdOrKey プロジェクトIDまたはキー
|
|
243
|
-
*/
|
|
244
|
-
export async function downloadWikis(command, options) {
|
|
245
|
-
const baseUrl = `https://${options.domain}/api/v2`;
|
|
246
|
-
command.log('Wikiの取得を開始します...');
|
|
247
|
-
// APIリクエスト数をカウントするためのRateLimiterを作成
|
|
248
|
-
const rateLimiter = new RateLimiter(command);
|
|
249
|
-
// Wiki一覧の取得
|
|
250
|
-
command.log('Wiki一覧を取得しています...');
|
|
251
|
-
// APIリクエスト数をインクリメント
|
|
252
|
-
await rateLimiter.increment();
|
|
253
|
-
const wikis = await ky
|
|
254
|
-
.get(`${baseUrl}/wikis?apiKey=${options.apiKey}&projectIdOrKey=${options.projectIdOrKey}`)
|
|
255
|
-
.json();
|
|
256
|
-
command.log(`${wikis.length}件のWikiが見つかりました。`);
|
|
257
|
-
// 前回の更新日時より新しいWikiのみをフィルタリング
|
|
258
|
-
let filteredWikis = wikis;
|
|
259
|
-
if (options.lastUpdated) {
|
|
260
|
-
const lastUpdatedDate = new Date(options.lastUpdated);
|
|
261
|
-
filteredWikis = wikis.filter((wiki) => {
|
|
262
|
-
const wikiUpdatedDate = new Date(wiki.updated);
|
|
263
|
-
return wikiUpdatedDate > lastUpdatedDate;
|
|
264
|
-
});
|
|
265
|
-
command.log(`前回の更新日時(${options.lastUpdated})以降に更新された${filteredWikis.length}件のWikiを処理します。`);
|
|
266
|
-
}
|
|
267
|
-
if (filteredWikis.length === 0) {
|
|
268
|
-
command.log('更新が必要なWikiはありません。');
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
// 各Wikiの詳細情報を取得
|
|
272
|
-
command.log('Wiki詳細を取得しています...');
|
|
273
|
-
// 並列処理ではなく順次処理に変更
|
|
274
|
-
for (const wiki of filteredWikis) {
|
|
275
|
-
const wikiId = wiki.id;
|
|
276
|
-
try {
|
|
277
|
-
// APIリクエスト数をインクリメント
|
|
278
|
-
// eslint-disable-next-line no-await-in-loop
|
|
279
|
-
await rateLimiter.increment();
|
|
280
|
-
// 進捗状況を一行で更新
|
|
281
|
-
const currentIndex = filteredWikis.indexOf(wiki) + 1;
|
|
282
|
-
process.stdout.write(`\rWikiを取得中... (${currentIndex}/${filteredWikis.length}件)`);
|
|
283
|
-
// eslint-disable-next-line no-await-in-loop
|
|
284
|
-
const wikiDetail = await ky
|
|
285
|
-
.get(`${baseUrl}/wikis/${wikiId}?projectIdOrKey=${options.projectIdOrKey}&apiKey=${options.apiKey}`)
|
|
286
|
-
.json();
|
|
287
|
-
// Wikiの名前をファイルパスとして使用
|
|
288
|
-
const wikiName = wiki.name;
|
|
289
|
-
// ファイル名のサニタイズ
|
|
290
|
-
const sanitizedName = sanitizeWikiFileName(wikiName);
|
|
291
|
-
// ファイル名の拡張子を追加
|
|
292
|
-
const wikiFileName = `${sanitizedName}.md`;
|
|
293
|
-
// ディレクトリ構造を作成(必要な場合)
|
|
294
|
-
const dirPath = path.dirname(wikiFileName);
|
|
295
|
-
if (dirPath !== '.') {
|
|
296
|
-
// eslint-disable-next-line no-await-in-loop
|
|
297
|
-
await fs.mkdir(path.join(options.outputDir, dirPath), { recursive: true });
|
|
298
|
-
}
|
|
299
|
-
// Markdownファイルを保存
|
|
300
|
-
const wikiFilePath = path.join(options.outputDir, wikiFileName);
|
|
301
|
-
// JSONからcontentフィールドを取得
|
|
302
|
-
const content = wikiDetail.content || '';
|
|
303
|
-
// BacklogのWikiへのリンクを作成
|
|
304
|
-
const backlogWikiUrl = `https://${options.domain}/alias/wiki/${wikiId}`;
|
|
305
|
-
// Markdownファイルに書き込む(タイトルとBacklogリンクを追加)
|
|
306
|
-
const markdownContent = `# ${wiki.name}\n\n[Backlog Wiki Link](${backlogWikiUrl})\n\n${content}`;
|
|
307
|
-
// eslint-disable-next-line no-await-in-loop
|
|
308
|
-
await fs.writeFile(wikiFilePath, markdownContent);
|
|
309
|
-
// ログに記録
|
|
310
|
-
// eslint-disable-next-line no-await-in-loop
|
|
311
|
-
await appendLog(options.outputDir, `Wiki「${wiki.name}」を更新しました: ${backlogWikiUrl}`);
|
|
312
|
-
// 進捗状況を一行で更新
|
|
313
|
-
const wikiIndex = filteredWikis.indexOf(wiki) + 1;
|
|
314
|
-
process.stdout.write(`\rWikiを保存中... (${wikiIndex}/${filteredWikis.length}件)`);
|
|
315
|
-
}
|
|
316
|
-
catch (error) {
|
|
317
|
-
command.warn(`Wiki ${wiki.name} の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
command.log('\nWikiのダウンロードが完了しました!');
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Backlogからドキュメントをダウンロードする
|
|
324
|
-
* @param command コマンドインスタンス
|
|
325
|
-
* @param options ドキュメントダウンロードのオプション
|
|
326
|
-
* @param options.apiKey Backlog API key
|
|
327
|
-
* @param options.domain Backlogのドメイン
|
|
328
|
-
* @param options.keyword キーワードフィルター
|
|
329
|
-
* @param options.lastUpdated 最終更新日時
|
|
330
|
-
* @param options.outputDir 出力ディレクトリ
|
|
331
|
-
* @param options.projectId プロジェクトID
|
|
332
|
-
* @param options.projectIdOrKey プロジェクトIDまたはキー
|
|
333
|
-
*/
|
|
334
|
-
export async function downloadDocuments(command, options) {
|
|
335
|
-
const baseUrl = `https://${options.domain}/api/v2`;
|
|
336
|
-
command.log('ドキュメントの取得を開始します...');
|
|
337
|
-
// APIリクエスト数をカウントするためのRateLimiterを作成
|
|
338
|
-
const rateLimiter = new RateLimiter(command);
|
|
339
|
-
// ドキュメントツリーの取得
|
|
340
|
-
command.log('ドキュメントツリーを取得しています...');
|
|
341
|
-
// APIリクエスト数をインクリメント
|
|
342
|
-
await rateLimiter.increment();
|
|
343
|
-
const documentTree = await ky
|
|
344
|
-
.get(`${baseUrl}/documents/tree?projectIdOrKey=${options.projectId}&apiKey=${options.apiKey}`)
|
|
345
|
-
.json();
|
|
346
|
-
command.log('アクティブなドキュメントツリーを処理します...');
|
|
347
|
-
// ツリー構造をトラバースして、各ドキュメントの詳細を取得・保存
|
|
348
|
-
const processedDocuments = [];
|
|
349
|
-
/**
|
|
350
|
-
* ドキュメントノードを再帰的に処理する
|
|
351
|
-
*/
|
|
352
|
-
const processDocumentNode = async (node, currentPath) => {
|
|
353
|
-
// フォルダの場合
|
|
354
|
-
if (node.children && node.children.length > 0) {
|
|
355
|
-
// フォルダを作成
|
|
356
|
-
const folderPath = path.join(options.outputDir, currentPath, sanitizeFileName(node.name));
|
|
357
|
-
await fs.mkdir(folderPath, { recursive: true });
|
|
358
|
-
// 子ノードを処理
|
|
359
|
-
for (const child of node.children) {
|
|
360
|
-
// eslint-disable-next-line no-await-in-loop
|
|
361
|
-
await processDocumentNode(child, path.join(currentPath, sanitizeFileName(node.name)));
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
// ドキュメントファイルの場合
|
|
366
|
-
try {
|
|
367
|
-
// 既に処理済みのドキュメントはスキップ
|
|
368
|
-
if (processedDocuments.includes(node.id)) {
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
processedDocuments.push(node.id);
|
|
372
|
-
// APIリクエスト数をインクリメント
|
|
373
|
-
await rateLimiter.increment();
|
|
374
|
-
// 進捗状況を表示
|
|
375
|
-
process.stdout.write(`\rドキュメント「${node.name}」を処理中...`);
|
|
376
|
-
// ドキュメント詳細を取得
|
|
377
|
-
const documentDetail = await ky.get(`${baseUrl}/documents/${node.id}?apiKey=${options.apiKey}`).json();
|
|
378
|
-
// 前回の更新日時チェック
|
|
379
|
-
if (options.lastUpdated) {
|
|
380
|
-
const lastUpdatedDate = new Date(options.lastUpdated);
|
|
381
|
-
const documentUpdatedDate = new Date(documentDetail.updated);
|
|
382
|
-
if (documentUpdatedDate <= lastUpdatedDate) {
|
|
383
|
-
return; // 更新が必要ない場合はスキップ
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
// ファイルパスを構築
|
|
387
|
-
const sanitizedTitle = sanitizeFileName(documentDetail.title);
|
|
388
|
-
const documentFileName = `${sanitizedTitle}.md`;
|
|
389
|
-
const documentFilePath = path.join(options.outputDir, currentPath, documentFileName);
|
|
390
|
-
// ディレクトリを作成(必要に応じて)
|
|
391
|
-
const dirPath = path.dirname(documentFilePath);
|
|
392
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
393
|
-
// Backlogのドキュメントへのリンクを作成
|
|
394
|
-
const backlogDocumentUrl = `https://${options.domain}/document/${options.projectIdOrKey}/${node.id}`;
|
|
395
|
-
// 添付ファイルリストの作成
|
|
396
|
-
let attachmentsSection = '';
|
|
397
|
-
if (documentDetail.attachments && documentDetail.attachments.length > 0) {
|
|
398
|
-
attachmentsSection = '\n\n## 添付ファイル\n';
|
|
399
|
-
for (const attachment of documentDetail.attachments) {
|
|
400
|
-
const attachmentDate = new Date(attachment.created).toLocaleString('ja-JP');
|
|
401
|
-
const fileSize = (attachment.size / 1024).toFixed(1);
|
|
402
|
-
attachmentsSection += `- **${attachment.name}** (${fileSize} KB) - 作成者: ${attachment.createdUser.name}, 作成日時: ${attachmentDate}\n`;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
// タグリストの作成
|
|
406
|
-
let tagsSection = '';
|
|
407
|
-
if (documentDetail.tags && documentDetail.tags.length > 0) {
|
|
408
|
-
tagsSection = '\n\n## タグ\n';
|
|
409
|
-
for (const tag of documentDetail.tags) {
|
|
410
|
-
tagsSection += `- ${tag.name}\n`;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
// 作成者・更新者情報
|
|
414
|
-
const createdDate = new Date(documentDetail.created).toLocaleString('ja-JP');
|
|
415
|
-
const updatedDate = new Date(documentDetail.updated).toLocaleString('ja-JP');
|
|
416
|
-
// Markdownファイルに書き込む
|
|
417
|
-
const markdownContent = `# ${documentDetail.title}
|
|
418
|
-
|
|
419
|
-
[Backlog Document Link](${backlogDocumentUrl})
|
|
420
|
-
|
|
421
|
-
**ステータス**: ${documentDetail.statusId}${documentDetail.emoji ? ` ${documentDetail.emoji}` : ''}
|
|
422
|
-
**作成者**: ${documentDetail.createdUser.name}
|
|
423
|
-
**作成日時**: ${createdDate}
|
|
424
|
-
**更新者**: ${documentDetail.updatedUser.name}
|
|
425
|
-
**更新日時**: ${updatedDate}
|
|
426
|
-
|
|
427
|
-
## 内容
|
|
428
|
-
|
|
429
|
-
${documentDetail.plain || '(内容なし)'}${attachmentsSection}${tagsSection}`;
|
|
430
|
-
await fs.writeFile(documentFilePath, markdownContent);
|
|
431
|
-
// ログに記録
|
|
432
|
-
await appendLog(options.outputDir, `ドキュメント「${documentDetail.title}」を更新しました: ${backlogDocumentUrl}`);
|
|
433
|
-
}
|
|
434
|
-
catch (error) {
|
|
435
|
-
command.warn(`ドキュメント ${node.name} の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
// アクティブツリーのルートから処理開始
|
|
440
|
-
if (documentTree.activeTree.children && documentTree.activeTree.children.length > 0) {
|
|
441
|
-
for (const rootNode of documentTree.activeTree.children) {
|
|
442
|
-
// eslint-disable-next-line no-await-in-loop
|
|
443
|
-
await processDocumentNode(rootNode, '');
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
command.log(`\n合計 ${processedDocuments.length}件のドキュメントが処理されました。`);
|
|
447
|
-
command.log('ドキュメントのダウンロードが完了しました!');
|
|
448
|
-
}
|
package/dist/utils/backlog.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* プロジェクトキーからプロジェクトIDを取得する
|
|
3
|
-
* @param domain Backlogドメイン (例: example.backlog.jp)
|
|
4
|
-
* @param projectKey プロジェクトキー
|
|
5
|
-
* @param apiKey BacklogのAPIキー
|
|
6
|
-
* @returns プロジェクトID
|
|
7
|
-
*/
|
|
8
|
-
export declare function getProjectIdFromKey(domain: string, projectKey: string, apiKey: string): Promise<number>;
|
|
9
|
-
/**
|
|
10
|
-
* プロジェクトIDまたはキーを検証し、必要に応じてキーからIDを取得する
|
|
11
|
-
* @param domain Backlogドメイン
|
|
12
|
-
* @param projectIdOrKey プロジェクトIDまたはキー
|
|
13
|
-
* @param apiKey BacklogのAPIキー
|
|
14
|
-
* @returns プロジェクトID
|
|
15
|
-
*/
|
|
16
|
-
export declare function validateAndGetProjectId(domain: string, projectIdOrKey: string, apiKey: string): Promise<number>;
|
package/dist/utils/backlog.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import ky from 'ky';
|
|
2
|
-
/**
|
|
3
|
-
* プロジェクトキーからプロジェクトIDを取得する
|
|
4
|
-
* @param domain Backlogドメイン (例: example.backlog.jp)
|
|
5
|
-
* @param projectKey プロジェクトキー
|
|
6
|
-
* @param apiKey BacklogのAPIキー
|
|
7
|
-
* @returns プロジェクトID
|
|
8
|
-
*/
|
|
9
|
-
export async function getProjectIdFromKey(domain, projectKey, apiKey) {
|
|
10
|
-
try {
|
|
11
|
-
const baseUrl = `https://${domain}/api/v2`;
|
|
12
|
-
const projectUrl = `${baseUrl}/projects/${projectKey}?apiKey=${apiKey}`;
|
|
13
|
-
const projectData = await ky.get(projectUrl).json();
|
|
14
|
-
return projectData.id;
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
throw new Error(`プロジェクトキー "${projectKey}" からプロジェクトIDの取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* プロジェクトIDまたはキーを検証し、必要に応じてキーからIDを取得する
|
|
22
|
-
* @param domain Backlogドメイン
|
|
23
|
-
* @param projectIdOrKey プロジェクトIDまたはキー
|
|
24
|
-
* @param apiKey BacklogのAPIキー
|
|
25
|
-
* @returns プロジェクトID
|
|
26
|
-
*/
|
|
27
|
-
export async function validateAndGetProjectId(domain, projectIdOrKey, apiKey) {
|
|
28
|
-
// 数値の場合はそのままプロジェクトIDとして返す
|
|
29
|
-
if (!Number.isNaN(Number(projectIdOrKey))) {
|
|
30
|
-
return Number(projectIdOrKey);
|
|
31
|
-
}
|
|
32
|
-
// 文字列の場合はプロジェクトキーとして扱い、IDを取得する
|
|
33
|
-
return getProjectIdFromKey(domain, projectIdOrKey, apiKey);
|
|
34
|
-
}
|