backlog-exporter 0.2.0 → 0.2.1
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 +22 -8
- package/dist/commands/all/index.d.ts +1 -0
- package/dist/commands/all/index.js +29 -14
- package/dist/commands/issue/index.d.ts +2 -0
- package/dist/commands/issue/index.js +30 -10
- package/dist/commands/wiki/index.js +9 -6
- package/dist/utils/backlog-api.d.ts +1 -1
- package/dist/utils/backlog-api.js +126 -39
- package/dist/utils/sleep.d.ts +28 -0
- package/dist/utils/sleep.js +42 -0
- package/oclif.manifest.json +34 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ $ npm install -g backlog-exporter
|
|
|
59
59
|
$ backlog-exporter COMMAND
|
|
60
60
|
running command...
|
|
61
61
|
$ backlog-exporter (--version)
|
|
62
|
-
backlog-exporter/0.2.
|
|
62
|
+
backlog-exporter/0.2.1 linux-x64 node-v23.9.0
|
|
63
63
|
$ backlog-exporter --help [COMMAND]
|
|
64
64
|
USAGE
|
|
65
65
|
$ backlog-exporter COMMAND
|
|
@@ -196,9 +196,11 @@ Backlogから課題とWikiを取得してMarkdownファイルとして保存す
|
|
|
196
196
|
|
|
197
197
|
```
|
|
198
198
|
USAGE
|
|
199
|
-
$ backlog-exporter all --domain <value> --projectIdOrKey <value> [--apiKey <value>] [-
|
|
199
|
+
$ backlog-exporter all --domain <value> --projectIdOrKey <value> [--apiKey <value>] [-m <value>] [-o
|
|
200
|
+
<value>]
|
|
200
201
|
|
|
201
202
|
FLAGS
|
|
203
|
+
-m, --maxCount=<value> [default: 5000] 一度に取得する課題の最大数(デフォルト: 5000)
|
|
202
204
|
-o, --output=<value> 出力ディレクトリパス
|
|
203
205
|
--apiKey=<value> Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)
|
|
204
206
|
--domain=<value> (required) Backlog domain (e.g. example.backlog.jp)
|
|
@@ -213,9 +215,12 @@ EXAMPLES
|
|
|
213
215
|
|
|
214
216
|
$ backlog-exporter all --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-project
|
|
215
217
|
指定したディレクトリに課題とWikiを保存する
|
|
218
|
+
|
|
219
|
+
$ backlog-exporter all --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --maxCount 1000
|
|
220
|
+
最大1000件の課題を取得する(デフォルトは100件)
|
|
216
221
|
```
|
|
217
222
|
|
|
218
|
-
_See code: [src/commands/all/index.ts](https://github.com/ShuntaToda/backlog-exporter/blob/v0.2.
|
|
223
|
+
_See code: [src/commands/all/index.ts](https://github.com/ShuntaToda/backlog-exporter/blob/v0.2.1/src/commands/all/index.ts)_
|
|
219
224
|
|
|
220
225
|
## `backlog-exporter help [COMMAND]`
|
|
221
226
|
|
|
@@ -243,13 +248,16 @@ Backlogから課題を取得してMarkdownファイルとして保存する
|
|
|
243
248
|
|
|
244
249
|
```
|
|
245
250
|
USAGE
|
|
246
|
-
$ backlog-exporter issue --domain <value> --projectIdOrKey <value> [--apiKey <value>] [-
|
|
251
|
+
$ backlog-exporter issue --domain <value> --projectIdOrKey <value> [--apiKey <value>] [-m <value>] [-o
|
|
252
|
+
<value>] [--statusId <value>]
|
|
247
253
|
|
|
248
254
|
FLAGS
|
|
255
|
+
-m, --maxCount=<value> [default: 5000] 一度に取得する課題の最大数(デフォルト: 5000)
|
|
249
256
|
-o, --output=<value> 出力ディレクトリパス
|
|
250
257
|
--apiKey=<value> Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)
|
|
251
258
|
--domain=<value> (required) Backlog domain (e.g. example.backlog.jp)
|
|
252
259
|
--projectIdOrKey=<value> (required) Backlog project ID or key
|
|
260
|
+
--statusId=<value> ステータスID(カンマ区切りで複数指定可能)
|
|
253
261
|
|
|
254
262
|
DESCRIPTION
|
|
255
263
|
Backlogから課題を取得してMarkdownファイルとして保存する
|
|
@@ -258,11 +266,17 @@ EXAMPLES
|
|
|
258
266
|
$ backlog-exporter issue --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY
|
|
259
267
|
課題をMarkdownファイルとして保存する
|
|
260
268
|
|
|
261
|
-
$ backlog-exporter issue --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-
|
|
269
|
+
$ backlog-exporter issue --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-issues
|
|
262
270
|
指定したディレクトリに課題を保存する
|
|
271
|
+
|
|
272
|
+
$ backlog-exporter issue --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --statusId 1,2,3
|
|
273
|
+
指定したステータスIDの課題のみを取得する
|
|
274
|
+
|
|
275
|
+
$ backlog-exporter issue --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --maxCount 10000
|
|
276
|
+
最大10000件の課題を取得する(デフォルトは5000件)
|
|
263
277
|
```
|
|
264
278
|
|
|
265
|
-
_See code: [src/commands/issue/index.ts](https://github.com/ShuntaToda/backlog-exporter/blob/v0.2.
|
|
279
|
+
_See code: [src/commands/issue/index.ts](https://github.com/ShuntaToda/backlog-exporter/blob/v0.2.1/src/commands/issue/index.ts)_
|
|
266
280
|
|
|
267
281
|
## `backlog-exporter plugins`
|
|
268
282
|
|
|
@@ -591,7 +605,7 @@ EXAMPLES
|
|
|
591
605
|
指定したディレクトリの設定を使用して更新する
|
|
592
606
|
```
|
|
593
607
|
|
|
594
|
-
_See code: [src/commands/update/index.ts](https://github.com/ShuntaToda/backlog-exporter/blob/v0.2.
|
|
608
|
+
_See code: [src/commands/update/index.ts](https://github.com/ShuntaToda/backlog-exporter/blob/v0.2.1/src/commands/update/index.ts)_
|
|
595
609
|
|
|
596
610
|
## `backlog-exporter wiki`
|
|
597
611
|
|
|
@@ -618,7 +632,7 @@ EXAMPLES
|
|
|
618
632
|
指定したディレクトリにWikiを保存する
|
|
619
633
|
```
|
|
620
634
|
|
|
621
|
-
_See code: [src/commands/wiki/index.ts](https://github.com/ShuntaToda/backlog-exporter/blob/v0.2.
|
|
635
|
+
_See code: [src/commands/wiki/index.ts](https://github.com/ShuntaToda/backlog-exporter/blob/v0.2.1/src/commands/wiki/index.ts)_
|
|
622
636
|
<!-- commandsstop -->
|
|
623
637
|
|
|
624
638
|
# 出力形式
|
|
@@ -5,6 +5,7 @@ export default class All extends Command {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
domain: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
maxCount: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
};
|
|
@@ -15,6 +15,9 @@ export default class All extends Command {
|
|
|
15
15
|
`,
|
|
16
16
|
`<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-project
|
|
17
17
|
指定したディレクトリに課題とWikiを保存する
|
|
18
|
+
`,
|
|
19
|
+
`<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --maxCount 1000
|
|
20
|
+
最大1000件の課題を取得する(デフォルトは100件)
|
|
18
21
|
`,
|
|
19
22
|
];
|
|
20
23
|
static flags = {
|
|
@@ -26,6 +29,12 @@ export default class All extends Command {
|
|
|
26
29
|
description: 'Backlog domain (e.g. example.backlog.jp)',
|
|
27
30
|
required: true,
|
|
28
31
|
}),
|
|
32
|
+
maxCount: Flags.integer({
|
|
33
|
+
char: 'm',
|
|
34
|
+
default: 5000,
|
|
35
|
+
description: '一度に取得する課題の最大数(デフォルト: 5000)',
|
|
36
|
+
required: false,
|
|
37
|
+
}),
|
|
29
38
|
output: Flags.string({
|
|
30
39
|
char: 'o',
|
|
31
40
|
description: '出力ディレクトリパス',
|
|
@@ -39,7 +48,7 @@ export default class All extends Command {
|
|
|
39
48
|
async run() {
|
|
40
49
|
const { flags } = await this.parse(All);
|
|
41
50
|
try {
|
|
42
|
-
const { domain, projectIdOrKey } = flags;
|
|
51
|
+
const { domain, maxCount, projectIdOrKey } = flags;
|
|
43
52
|
const apiKey = flags.apiKey || getApiKey(this);
|
|
44
53
|
const outputRoot = flags.output || './backlog-data';
|
|
45
54
|
// 出力ディレクトリの作成
|
|
@@ -53,23 +62,34 @@ export default class All extends Command {
|
|
|
53
62
|
// Wikiの出力ディレクトリ
|
|
54
63
|
const wikiOutput = path.join(outputRoot, 'wiki');
|
|
55
64
|
await createOutputDirectory(wikiOutput);
|
|
65
|
+
// 課題フォルダに設定ファイルを保存
|
|
66
|
+
await updateSettings(issueOutput, {
|
|
67
|
+
apiKey,
|
|
68
|
+
domain,
|
|
69
|
+
folderType: FolderType.ISSUE,
|
|
70
|
+
outputDir: issueOutput,
|
|
71
|
+
projectIdOrKey,
|
|
72
|
+
});
|
|
73
|
+
// Wikiフォルダに設定ファイルを保存
|
|
74
|
+
await updateSettings(wikiOutput, {
|
|
75
|
+
apiKey,
|
|
76
|
+
domain,
|
|
77
|
+
folderType: FolderType.WIKI,
|
|
78
|
+
outputDir: wikiOutput,
|
|
79
|
+
projectIdOrKey,
|
|
80
|
+
});
|
|
56
81
|
// 課題の取得と保存
|
|
57
82
|
this.log('課題の取得を開始します...');
|
|
58
83
|
await downloadIssues(this, {
|
|
59
84
|
apiKey,
|
|
60
|
-
count:
|
|
85
|
+
count: maxCount,
|
|
61
86
|
domain,
|
|
62
87
|
outputDir: issueOutput,
|
|
63
88
|
projectId,
|
|
64
89
|
});
|
|
65
|
-
//
|
|
90
|
+
// 課題フォルダの最終更新日時を更新
|
|
66
91
|
await updateSettings(issueOutput, {
|
|
67
|
-
apiKey,
|
|
68
|
-
domain,
|
|
69
|
-
folderType: FolderType.ISSUE,
|
|
70
92
|
lastUpdated: new Date().toISOString(),
|
|
71
|
-
outputDir: issueOutput,
|
|
72
|
-
projectIdOrKey,
|
|
73
93
|
});
|
|
74
94
|
this.log('課題の取得が完了しました');
|
|
75
95
|
// Wikiの取得と保存
|
|
@@ -80,14 +100,9 @@ export default class All extends Command {
|
|
|
80
100
|
outputDir: wikiOutput,
|
|
81
101
|
projectIdOrKey,
|
|
82
102
|
});
|
|
83
|
-
// Wiki
|
|
103
|
+
// Wikiフォルダの最終更新日時を更新
|
|
84
104
|
await updateSettings(wikiOutput, {
|
|
85
|
-
apiKey,
|
|
86
|
-
domain,
|
|
87
|
-
folderType: FolderType.WIKI,
|
|
88
105
|
lastUpdated: new Date().toISOString(),
|
|
89
|
-
outputDir: wikiOutput,
|
|
90
|
-
projectIdOrKey,
|
|
91
106
|
});
|
|
92
107
|
this.log('Wikiの取得が完了しました');
|
|
93
108
|
this.log('すべてのデータの取得が完了しました!');
|
|
@@ -5,8 +5,10 @@ export default class Issue extends Command {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
domain: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
maxCount: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
statusId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
};
|
|
11
13
|
run(): Promise<void>;
|
|
12
14
|
}
|
|
@@ -12,8 +12,14 @@ export default class Issue extends Command {
|
|
|
12
12
|
`<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY
|
|
13
13
|
課題をMarkdownファイルとして保存する
|
|
14
14
|
`,
|
|
15
|
-
`<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-
|
|
15
|
+
`<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-issues
|
|
16
16
|
指定したディレクトリに課題を保存する
|
|
17
|
+
`,
|
|
18
|
+
`<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --statusId 1,2,3
|
|
19
|
+
指定したステータスIDの課題のみを取得する
|
|
20
|
+
`,
|
|
21
|
+
`<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --maxCount 10000
|
|
22
|
+
最大10000件の課題を取得する(デフォルトは5000件)
|
|
17
23
|
`,
|
|
18
24
|
];
|
|
19
25
|
static flags = {
|
|
@@ -25,6 +31,12 @@ export default class Issue extends Command {
|
|
|
25
31
|
description: 'Backlog domain (e.g. example.backlog.jp)',
|
|
26
32
|
required: true,
|
|
27
33
|
}),
|
|
34
|
+
maxCount: Flags.integer({
|
|
35
|
+
char: 'm',
|
|
36
|
+
default: 5000,
|
|
37
|
+
description: '一度に取得する課題の最大数(デフォルト: 5000)',
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
28
40
|
output: Flags.string({
|
|
29
41
|
char: 'o',
|
|
30
42
|
description: '出力ディレクトリパス',
|
|
@@ -34,34 +46,42 @@ export default class Issue extends Command {
|
|
|
34
46
|
description: 'Backlog project ID or key',
|
|
35
47
|
required: true,
|
|
36
48
|
}),
|
|
49
|
+
statusId: Flags.string({
|
|
50
|
+
description: 'ステータスID(カンマ区切りで複数指定可能)',
|
|
51
|
+
required: false,
|
|
52
|
+
}),
|
|
37
53
|
};
|
|
38
54
|
async run() {
|
|
39
55
|
const { flags } = await this.parse(Issue);
|
|
40
56
|
try {
|
|
41
|
-
const { domain, projectIdOrKey } = flags;
|
|
57
|
+
const { domain, maxCount, projectIdOrKey, statusId } = flags;
|
|
42
58
|
const apiKey = flags.apiKey || getApiKey(this);
|
|
43
|
-
const outputDir = flags.output || './issues';
|
|
59
|
+
const outputDir = flags.output || './backlog-issues';
|
|
44
60
|
// 出力ディレクトリの作成
|
|
45
61
|
await createOutputDirectory(outputDir);
|
|
46
62
|
// プロジェクトキーからプロジェクトIDを取得
|
|
47
63
|
const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey);
|
|
48
64
|
this.log(`プロジェクトID: ${projectId} を使用します`);
|
|
65
|
+
// 設定ファイルを保存
|
|
66
|
+
await updateSettings(outputDir, {
|
|
67
|
+
apiKey,
|
|
68
|
+
domain,
|
|
69
|
+
folderType: FolderType.ISSUE,
|
|
70
|
+
outputDir,
|
|
71
|
+
projectIdOrKey,
|
|
72
|
+
});
|
|
49
73
|
// 課題の取得と保存
|
|
50
74
|
await downloadIssues(this, {
|
|
51
75
|
apiKey,
|
|
52
|
-
count:
|
|
76
|
+
count: maxCount,
|
|
53
77
|
domain,
|
|
54
78
|
outputDir,
|
|
55
79
|
projectId,
|
|
80
|
+
statusId,
|
|
56
81
|
});
|
|
57
|
-
//
|
|
82
|
+
// 最終更新日時を更新
|
|
58
83
|
await updateSettings(outputDir, {
|
|
59
|
-
apiKey,
|
|
60
|
-
domain,
|
|
61
|
-
folderType: FolderType.ISSUE,
|
|
62
84
|
lastUpdated: new Date().toISOString(),
|
|
63
|
-
outputDir,
|
|
64
|
-
projectIdOrKey,
|
|
65
85
|
});
|
|
66
86
|
this.log('課題の取得が完了しました!');
|
|
67
87
|
}
|
|
@@ -42,22 +42,25 @@ Wikiをダウンロードする
|
|
|
42
42
|
const outputDir = flags.output || './wiki';
|
|
43
43
|
// 出力ディレクトリの作成
|
|
44
44
|
await createOutputDirectory(outputDir);
|
|
45
|
-
//
|
|
46
|
-
await
|
|
45
|
+
// 設定ファイルを保存
|
|
46
|
+
await updateSettings(outputDir, {
|
|
47
47
|
apiKey,
|
|
48
48
|
domain,
|
|
49
|
+
folderType: FolderType.WIKI,
|
|
49
50
|
outputDir,
|
|
50
51
|
projectIdOrKey,
|
|
51
52
|
});
|
|
52
|
-
//
|
|
53
|
-
await
|
|
53
|
+
// Wikiの取得と保存
|
|
54
|
+
await downloadWikis(this, {
|
|
54
55
|
apiKey,
|
|
55
56
|
domain,
|
|
56
|
-
folderType: FolderType.WIKI,
|
|
57
|
-
lastUpdated: new Date().toISOString(),
|
|
58
57
|
outputDir,
|
|
59
58
|
projectIdOrKey,
|
|
60
59
|
});
|
|
60
|
+
// 最終更新日時を更新
|
|
61
|
+
await updateSettings(outputDir, {
|
|
62
|
+
lastUpdated: new Date().toISOString(),
|
|
63
|
+
});
|
|
61
64
|
this.log('Wikiの取得が完了しました!');
|
|
62
65
|
}
|
|
63
66
|
catch (error) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import ky from 'ky';
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
4
5
|
import { sanitizeFileName, sanitizeWikiFileName } from './common.js';
|
|
6
|
+
import { RateLimiter } from './sleep.js';
|
|
5
7
|
/**
|
|
6
8
|
* Backlogから課題をダウンロードする
|
|
7
9
|
* @param command コマンドインスタンス
|
|
@@ -10,23 +12,56 @@ import { sanitizeFileName, sanitizeWikiFileName } from './common.js';
|
|
|
10
12
|
export async function downloadIssues(command, options) {
|
|
11
13
|
const baseUrl = `https://${options.domain}/api/v2`;
|
|
12
14
|
command.log('課題の取得を開始します...');
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
// 全ての課題を格納する配列
|
|
16
|
+
let allIssues = [];
|
|
17
|
+
// countのデフォルト値を5000に設定
|
|
18
|
+
const count = options.count ?? 5000;
|
|
19
|
+
const maxCount = Math.min(count, 100); // APIの制限は100件
|
|
20
|
+
// APIリクエスト数をカウントするためのRateLimiterを作成
|
|
21
|
+
const rateLimiter = new RateLimiter(command);
|
|
22
|
+
// 課題を取得する関数
|
|
23
|
+
const fetchIssues = async (offset) => {
|
|
24
|
+
// APIリクエスト数をインクリメント
|
|
25
|
+
await rateLimiter.increment();
|
|
26
|
+
// APIパラメータの構築
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
apiKey: options.apiKey,
|
|
29
|
+
count: maxCount.toString(),
|
|
30
|
+
offset: offset.toString(),
|
|
31
|
+
'projectId[]': options.projectId.toString(),
|
|
32
|
+
});
|
|
33
|
+
// ステータスIDが指定されている場合は追加
|
|
34
|
+
if (options.statusId) {
|
|
35
|
+
params.append('statusId[]', options.statusId);
|
|
36
|
+
}
|
|
37
|
+
// 進捗状況を一行で更新
|
|
38
|
+
process.stdout.write(`\r課題を取得中... (${allIssues.length}件取得済み)`);
|
|
39
|
+
return ky.get(`${baseUrl}/issues?${params.toString()}`).json();
|
|
40
|
+
};
|
|
41
|
+
// 再帰的に全ての課題を取得
|
|
42
|
+
const fetchAllIssues = async (offset) => {
|
|
43
|
+
try {
|
|
44
|
+
const issues = await fetchIssues(offset);
|
|
45
|
+
// 取得した課題を追加
|
|
46
|
+
allIssues = [...allIssues, ...issues];
|
|
47
|
+
// 次のページがあるかどうかを確認
|
|
48
|
+
if (issues.length === maxCount) {
|
|
49
|
+
// 次のページを取得
|
|
50
|
+
await fetchAllIssues(offset + maxCount);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
command.error(`課題の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
// 課題取得開始
|
|
58
|
+
await fetchAllIssues(0);
|
|
59
|
+
command.log(`\n合計 ${allIssues.length}件の課題が見つかりました。`);
|
|
25
60
|
// 前回の更新日時より新しい課題のみをフィルタリング
|
|
26
|
-
let filteredIssues =
|
|
61
|
+
let filteredIssues = allIssues;
|
|
27
62
|
if (options.lastUpdated) {
|
|
28
63
|
const lastUpdatedDate = new Date(options.lastUpdated);
|
|
29
|
-
filteredIssues =
|
|
64
|
+
filteredIssues = allIssues.filter((issue) => {
|
|
30
65
|
const issueUpdatedDate = new Date(issue.updated);
|
|
31
66
|
return issueUpdatedDate > lastUpdatedDate;
|
|
32
67
|
});
|
|
@@ -38,28 +73,29 @@ export async function downloadIssues(command, options) {
|
|
|
38
73
|
}
|
|
39
74
|
// 各課題の詳細情報を取得して保存
|
|
40
75
|
command.log('課題を保存しています...');
|
|
41
|
-
//
|
|
42
|
-
const
|
|
76
|
+
// 並列処理ではなく順次処理に変更
|
|
77
|
+
for (const issue of filteredIssues) {
|
|
43
78
|
try {
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
-
|
|
79
|
+
// 進捗状況を一行で更新
|
|
80
|
+
const currentIndex = filteredIssues.indexOf(issue) + 1;
|
|
81
|
+
process.stdout.write(`\r課題を保存中... (${currentIndex}/${filteredIssues.length}件)`);
|
|
47
82
|
// BacklogのIssueへのリンクを作成
|
|
48
83
|
const backlogIssueUrl = `https://${options.domain}/view/${issue.issueKey}`;
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
const
|
|
84
|
+
// コメントを取得する関数を呼び出し
|
|
85
|
+
// eslint-disable-next-line no-await-in-loop
|
|
86
|
+
const { comments: allComments } = await fetchAllCommentsForIssue({
|
|
52
87
|
apiKey: options.apiKey,
|
|
53
|
-
|
|
54
|
-
|
|
88
|
+
baseUrl,
|
|
89
|
+
command,
|
|
90
|
+
issueKey: issue.issueKey,
|
|
91
|
+
rateLimiter,
|
|
55
92
|
});
|
|
56
|
-
const comments = await ky.get(`${baseUrl}/issues/${issue.issueKey}/comments?${commentsParams.toString()}`).json();
|
|
57
93
|
// コメントセクションを作成
|
|
58
94
|
let commentsSection = '';
|
|
59
|
-
if (
|
|
95
|
+
if (allComments.length > 0) {
|
|
60
96
|
commentsSection = '\n\n## コメント\n';
|
|
61
97
|
let commentIndex = 1;
|
|
62
|
-
for (const comment of
|
|
98
|
+
for (const comment of allComments) {
|
|
63
99
|
const commentDate = new Date(comment.created).toLocaleString('ja-JP');
|
|
64
100
|
commentsSection += `\n### コメント ${commentIndex}\n- **投稿者**: ${comment.createdUser.name}\n- **日時**: ${commentDate}\n\n${comment.content || '(内容なし)'}\n\n---\n`;
|
|
65
101
|
commentIndex++;
|
|
@@ -67,6 +103,16 @@ export async function downloadIssues(command, options) {
|
|
|
67
103
|
// 最後の区切り線を削除
|
|
68
104
|
commentsSection = commentsSection.slice(0, -5);
|
|
69
105
|
}
|
|
106
|
+
// 課題の作成年を取得
|
|
107
|
+
const createdYear = new Date(issue.created).getFullYear();
|
|
108
|
+
// 年ごとのフォルダパスを作成
|
|
109
|
+
const yearDirPath = path.join(options.outputDir, createdYear.toString());
|
|
110
|
+
// 年ごとのフォルダを作成
|
|
111
|
+
// eslint-disable-next-line no-await-in-loop
|
|
112
|
+
await fs.mkdir(yearDirPath, { recursive: true });
|
|
113
|
+
// 年ごとのフォルダ内にMarkdownファイルを保存
|
|
114
|
+
const issueFileName = `${sanitizeFileName(issue.summary)}.md`;
|
|
115
|
+
const issueFilePath = path.join(yearDirPath, issueFileName);
|
|
70
116
|
// Markdownファイルに書き込む
|
|
71
117
|
const assigneeName = issue.assignee ? issue.assignee.name : '未割り当て';
|
|
72
118
|
const markdownContent = `# ${issue.summary}
|
|
@@ -82,15 +128,43 @@ export async function downloadIssues(command, options) {
|
|
|
82
128
|
|
|
83
129
|
## 詳細
|
|
84
130
|
${issue.description || '詳細情報なし'}${commentsSection}`;
|
|
131
|
+
// eslint-disable-next-line no-await-in-loop
|
|
85
132
|
await fs.writeFile(issueFilePath, markdownContent);
|
|
86
|
-
command.log(`課題 "${issue.issueKey}: ${issue.summary}" を ${issueFilePath} に保存しました`);
|
|
87
133
|
}
|
|
88
134
|
catch (error) {
|
|
89
135
|
command.warn(`課題 ${issue.issueKey} の保存に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
136
|
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
137
|
+
}
|
|
138
|
+
command.log('\n課題のダウンロードが完了しました!');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 課題のコメントを全て取得する
|
|
142
|
+
*/
|
|
143
|
+
async function fetchAllCommentsForIssue({ apiKey, baseUrl, command, issueKey, rateLimiter, }) {
|
|
144
|
+
const allComments = [];
|
|
145
|
+
const fetchComments = async (minId) => {
|
|
146
|
+
// APIリクエスト数をインクリメント
|
|
147
|
+
await rateLimiter.increment();
|
|
148
|
+
let url = `${baseUrl}/issues/${issueKey}/comments?apiKey=${apiKey}&count=100`;
|
|
149
|
+
if (minId) {
|
|
150
|
+
url += `&minId=${minId}`;
|
|
151
|
+
}
|
|
152
|
+
const comments = await ky.get(url).json();
|
|
153
|
+
allComments.push(...comments);
|
|
154
|
+
if (comments.length === 100) {
|
|
155
|
+
// 取得したコメントの最後のIDを次のリクエストのminIdとして使用
|
|
156
|
+
const lastCommentId = comments.at(-1).id;
|
|
157
|
+
await fetchComments(lastCommentId + 1);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
try {
|
|
161
|
+
await fetchComments();
|
|
162
|
+
return { comments: allComments };
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
command.warn(`課題 ${issueKey} のコメント取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
|
|
166
|
+
return { comments: allComments };
|
|
167
|
+
}
|
|
94
168
|
}
|
|
95
169
|
/**
|
|
96
170
|
* BacklogからWikiをダウンロードする
|
|
@@ -100,8 +174,12 @@ ${issue.description || '詳細情報なし'}${commentsSection}`;
|
|
|
100
174
|
export async function downloadWikis(command, options) {
|
|
101
175
|
const baseUrl = `https://${options.domain}/api/v2`;
|
|
102
176
|
command.log('Wikiの取得を開始します...');
|
|
177
|
+
// APIリクエスト数をカウントするためのRateLimiterを作成
|
|
178
|
+
const rateLimiter = new RateLimiter(command);
|
|
103
179
|
// Wiki一覧の取得
|
|
104
180
|
command.log('Wiki一覧を取得しています...');
|
|
181
|
+
// APIリクエスト数をインクリメント
|
|
182
|
+
await rateLimiter.increment();
|
|
105
183
|
const wikis = await ky
|
|
106
184
|
.get(`${baseUrl}/wikis?apiKey=${options.apiKey}&projectIdOrKey=${options.projectIdOrKey}`)
|
|
107
185
|
.json();
|
|
@@ -122,11 +200,17 @@ export async function downloadWikis(command, options) {
|
|
|
122
200
|
}
|
|
123
201
|
// 各Wikiの詳細情報を取得
|
|
124
202
|
command.log('Wiki詳細を取得しています...');
|
|
125
|
-
//
|
|
126
|
-
const
|
|
203
|
+
// 並列処理ではなく順次処理に変更
|
|
204
|
+
for (const wiki of filteredWikis) {
|
|
127
205
|
const wikiId = wiki.id;
|
|
128
|
-
command.log(`Wiki: ${wiki.name} (ID: ${wikiId}) を取得しています`);
|
|
129
206
|
try {
|
|
207
|
+
// APIリクエスト数をインクリメント
|
|
208
|
+
// eslint-disable-next-line no-await-in-loop
|
|
209
|
+
await rateLimiter.increment();
|
|
210
|
+
// 進捗状況を一行で更新
|
|
211
|
+
const currentIndex = filteredWikis.indexOf(wiki) + 1;
|
|
212
|
+
process.stdout.write(`\rWikiを取得中... (${currentIndex}/${filteredWikis.length}件)`);
|
|
213
|
+
// eslint-disable-next-line no-await-in-loop
|
|
130
214
|
const wikiDetail = await ky
|
|
131
215
|
.get(`${baseUrl}/wikis/${wikiId}?projectIdOrKey=${options.projectIdOrKey}&apiKey=${options.apiKey}`)
|
|
132
216
|
.json();
|
|
@@ -139,6 +223,7 @@ export async function downloadWikis(command, options) {
|
|
|
139
223
|
// ディレクトリ構造を作成(必要な場合)
|
|
140
224
|
const dirPath = path.dirname(wikiFileName);
|
|
141
225
|
if (dirPath !== '.') {
|
|
226
|
+
// eslint-disable-next-line no-await-in-loop
|
|
142
227
|
await fs.mkdir(path.join(options.outputDir, dirPath), { recursive: true });
|
|
143
228
|
}
|
|
144
229
|
// Markdownファイルを保存
|
|
@@ -149,13 +234,15 @@ export async function downloadWikis(command, options) {
|
|
|
149
234
|
const backlogWikiUrl = `https://${options.domain}/alias/wiki/${wikiId}`;
|
|
150
235
|
// Markdownファイルに書き込む(タイトルとBacklogリンクを追加)
|
|
151
236
|
const markdownContent = `# ${wiki.name}\n\n[Backlog Wiki Link](${backlogWikiUrl})\n\n${content}`;
|
|
237
|
+
// eslint-disable-next-line no-await-in-loop
|
|
152
238
|
await fs.writeFile(wikiFilePath, markdownContent);
|
|
153
|
-
|
|
239
|
+
// 進捗状況を一行で更新
|
|
240
|
+
const wikiIndex = filteredWikis.indexOf(wiki) + 1;
|
|
241
|
+
process.stdout.write(`\rWikiを保存中... (${wikiIndex}/${filteredWikis.length}件)`);
|
|
154
242
|
}
|
|
155
243
|
catch (error) {
|
|
156
|
-
command.warn(`Wiki
|
|
244
|
+
command.warn(`Wiki ${wiki.name} の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
|
|
157
245
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
command.log('Wikiのダウンロードが完了しました!');
|
|
246
|
+
}
|
|
247
|
+
command.log('\nWikiのダウンロードが完了しました!');
|
|
161
248
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
/**
|
|
3
|
+
* 指定したミリ秒だけ待機する
|
|
4
|
+
* @param ms 待機時間(ミリ秒)
|
|
5
|
+
*/
|
|
6
|
+
export declare const sleep: (ms: number) => Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* APIリクエストのレート制限対策のためのユーティリティ
|
|
9
|
+
* 100リクエストごとに15秒間待機する
|
|
10
|
+
*/
|
|
11
|
+
export declare class RateLimiter {
|
|
12
|
+
private readonly command;
|
|
13
|
+
private requestCount;
|
|
14
|
+
constructor(command: Command);
|
|
15
|
+
/**
|
|
16
|
+
* 現在のリクエスト数を取得する
|
|
17
|
+
*/
|
|
18
|
+
getCount(): number;
|
|
19
|
+
/**
|
|
20
|
+
* APIリクエストをカウントし、必要に応じて待機する
|
|
21
|
+
* @returns 現在のリクエスト数
|
|
22
|
+
*/
|
|
23
|
+
increment(): Promise<number>;
|
|
24
|
+
/**
|
|
25
|
+
* リクエスト数を設定する
|
|
26
|
+
*/
|
|
27
|
+
setCount(count: number): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 指定したミリ秒だけ待機する
|
|
3
|
+
* @param ms 待機時間(ミリ秒)
|
|
4
|
+
*/
|
|
5
|
+
export const sleep = (ms) => new Promise((resolve) => {
|
|
6
|
+
setTimeout(resolve, ms);
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* APIリクエストのレート制限対策のためのユーティリティ
|
|
10
|
+
* 100リクエストごとに15秒間待機する
|
|
11
|
+
*/
|
|
12
|
+
export class RateLimiter {
|
|
13
|
+
command;
|
|
14
|
+
requestCount = 0;
|
|
15
|
+
constructor(command) {
|
|
16
|
+
this.command = command;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 現在のリクエスト数を取得する
|
|
20
|
+
*/
|
|
21
|
+
getCount() {
|
|
22
|
+
return this.requestCount;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* APIリクエストをカウントし、必要に応じて待機する
|
|
26
|
+
* @returns 現在のリクエスト数
|
|
27
|
+
*/
|
|
28
|
+
async increment() {
|
|
29
|
+
this.requestCount++;
|
|
30
|
+
if (this.requestCount > 1 && this.requestCount % 100 === 0) {
|
|
31
|
+
this.command.log('レート制限を回避するため15秒間待機します...');
|
|
32
|
+
await sleep(15_000);
|
|
33
|
+
}
|
|
34
|
+
return this.requestCount;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* リクエスト数を設定する
|
|
38
|
+
*/
|
|
39
|
+
setCount(count) {
|
|
40
|
+
this.requestCount = count;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
"description": "Backlogから課題とWikiを取得してMarkdownファイルとして保存する",
|
|
7
7
|
"examples": [
|
|
8
8
|
"<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY\n課題とWikiをMarkdownファイルとして保存する\n",
|
|
9
|
-
"<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-project\n指定したディレクトリに課題とWikiを保存する\n"
|
|
9
|
+
"<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-project\n指定したディレクトリに課題とWikiを保存する\n",
|
|
10
|
+
"<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --maxCount 1000\n最大1000件の課題を取得する(デフォルトは100件)\n"
|
|
10
11
|
],
|
|
11
12
|
"flags": {
|
|
12
13
|
"apiKey": {
|
|
@@ -25,6 +26,16 @@
|
|
|
25
26
|
"multiple": false,
|
|
26
27
|
"type": "option"
|
|
27
28
|
},
|
|
29
|
+
"maxCount": {
|
|
30
|
+
"char": "m",
|
|
31
|
+
"description": "一度に取得する課題の最大数(デフォルト: 5000)",
|
|
32
|
+
"name": "maxCount",
|
|
33
|
+
"required": false,
|
|
34
|
+
"default": 5000,
|
|
35
|
+
"hasDynamicHelp": false,
|
|
36
|
+
"multiple": false,
|
|
37
|
+
"type": "option"
|
|
38
|
+
},
|
|
28
39
|
"output": {
|
|
29
40
|
"char": "o",
|
|
30
41
|
"description": "出力ディレクトリパス",
|
|
@@ -65,7 +76,9 @@
|
|
|
65
76
|
"description": "Backlogから課題を取得してMarkdownファイルとして保存する",
|
|
66
77
|
"examples": [
|
|
67
78
|
"<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY\n課題をMarkdownファイルとして保存する\n",
|
|
68
|
-
"<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-
|
|
79
|
+
"<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-issues\n指定したディレクトリに課題を保存する\n",
|
|
80
|
+
"<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --statusId 1,2,3\n指定したステータスIDの課題のみを取得する\n",
|
|
81
|
+
"<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --maxCount 10000\n最大10000件の課題を取得する(デフォルトは5000件)\n"
|
|
69
82
|
],
|
|
70
83
|
"flags": {
|
|
71
84
|
"apiKey": {
|
|
@@ -84,6 +97,16 @@
|
|
|
84
97
|
"multiple": false,
|
|
85
98
|
"type": "option"
|
|
86
99
|
},
|
|
100
|
+
"maxCount": {
|
|
101
|
+
"char": "m",
|
|
102
|
+
"description": "一度に取得する課題の最大数(デフォルト: 5000)",
|
|
103
|
+
"name": "maxCount",
|
|
104
|
+
"required": false,
|
|
105
|
+
"default": 5000,
|
|
106
|
+
"hasDynamicHelp": false,
|
|
107
|
+
"multiple": false,
|
|
108
|
+
"type": "option"
|
|
109
|
+
},
|
|
87
110
|
"output": {
|
|
88
111
|
"char": "o",
|
|
89
112
|
"description": "出力ディレクトリパス",
|
|
@@ -100,6 +123,14 @@
|
|
|
100
123
|
"hasDynamicHelp": false,
|
|
101
124
|
"multiple": false,
|
|
102
125
|
"type": "option"
|
|
126
|
+
},
|
|
127
|
+
"statusId": {
|
|
128
|
+
"description": "ステータスID(カンマ区切りで複数指定可能)",
|
|
129
|
+
"name": "statusId",
|
|
130
|
+
"required": false,
|
|
131
|
+
"hasDynamicHelp": false,
|
|
132
|
+
"multiple": false,
|
|
133
|
+
"type": "option"
|
|
103
134
|
}
|
|
104
135
|
},
|
|
105
136
|
"hasDynamicHelp": false,
|
|
@@ -258,5 +289,5 @@
|
|
|
258
289
|
]
|
|
259
290
|
}
|
|
260
291
|
},
|
|
261
|
-
"version": "0.2.
|
|
292
|
+
"version": "0.2.1"
|
|
262
293
|
}
|