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
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { validateAndGetProjectId } from '../../utils/backlog.js';
|
|
7
|
-
import { createOutputDirectory, getApiKey } from '../../utils/common.js';
|
|
8
|
-
import { FolderType, getSettingsFilePath, loadSettings, updateSettings } from '../../utils/settings.js';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { createBacklogRepositories } from '../../composition/backlog-repositories.js';
|
|
4
|
+
import { updateExports } from '../../modules/update/use-case/update-exports.js';
|
|
5
|
+
import { loadDotenv } from '../../shared/config/env.js';
|
|
9
6
|
// .envファイルを読み込む
|
|
10
|
-
|
|
7
|
+
loadDotenv();
|
|
11
8
|
export default class Update extends Command {
|
|
12
9
|
static args = {
|
|
13
10
|
directory: Args.string({
|
|
@@ -37,6 +34,15 @@ export default class Update extends Command {
|
|
|
37
34
|
`,
|
|
38
35
|
`<%= config.bin %> <%= command.id %> --issueKeyFileName --issueKeyFolder
|
|
39
36
|
課題キーでフォルダを作成し、ファイル名も課題キーにする
|
|
37
|
+
`,
|
|
38
|
+
`<%= config.bin %> <%= command.id %> --issueIdOrKey PROJECT-1,PROJECT-2
|
|
39
|
+
指定した課題(IDまたはキー)のみを再取得する(全件差分更新は行わない)
|
|
40
|
+
`,
|
|
41
|
+
`<%= config.bin %> <%= command.id %> --wikiId 12345,12346
|
|
42
|
+
指定したWiki(ID)のみを再取得する(全件差分更新は行わない)
|
|
43
|
+
`,
|
|
44
|
+
`<%= config.bin %> <%= command.id %> --documentId abc123,def456
|
|
45
|
+
指定したドキュメント(ID)のみを再取得する(全件差分更新は行わない)
|
|
40
46
|
`,
|
|
41
47
|
];
|
|
42
48
|
static flags = {
|
|
@@ -44,6 +50,10 @@ export default class Update extends Command {
|
|
|
44
50
|
description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)',
|
|
45
51
|
required: false,
|
|
46
52
|
}),
|
|
53
|
+
documentId: Flags.string({
|
|
54
|
+
description: '指定したドキュメント(ID)のみを再取得する(カンマ区切りで複数指定可能)',
|
|
55
|
+
required: false,
|
|
56
|
+
}),
|
|
47
57
|
documentsOnly: Flags.boolean({
|
|
48
58
|
description: 'ドキュメントのみを更新する',
|
|
49
59
|
required: false,
|
|
@@ -57,6 +67,10 @@ export default class Update extends Command {
|
|
|
57
67
|
description: '確認プロンプトをスキップする',
|
|
58
68
|
required: false,
|
|
59
69
|
}),
|
|
70
|
+
issueIdOrKey: Flags.string({
|
|
71
|
+
description: '指定した課題(IDまたはキー)のみを再取得する(カンマ区切りで複数指定可能)',
|
|
72
|
+
required: false,
|
|
73
|
+
}),
|
|
60
74
|
issueKeyFileName: Flags.boolean({
|
|
61
75
|
description: 'ファイル名を課題キーにする',
|
|
62
76
|
required: false,
|
|
@@ -73,6 +87,10 @@ export default class Update extends Command {
|
|
|
73
87
|
description: 'Backlog project ID or key',
|
|
74
88
|
required: false,
|
|
75
89
|
}),
|
|
90
|
+
wikiId: Flags.string({
|
|
91
|
+
description: '指定したWiki(ID)のみを再取得する(カンマ区切りで複数指定可能)',
|
|
92
|
+
required: false,
|
|
93
|
+
}),
|
|
76
94
|
wikisOnly: Flags.boolean({
|
|
77
95
|
description: 'Wikiのみを更新する',
|
|
78
96
|
required: false,
|
|
@@ -82,263 +100,13 @@ export default class Update extends Command {
|
|
|
82
100
|
const { args, flags } = await this.parse(Update);
|
|
83
101
|
// 更新対象のディレクトリを決定(指定がなければカレントディレクトリ)
|
|
84
102
|
const targetDir = args.directory || process.cwd();
|
|
103
|
+
const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
|
|
85
104
|
try {
|
|
86
|
-
|
|
87
|
-
await this.findAndUpdateSettings(targetDir, flags);
|
|
105
|
+
await updateExports({ createRepositories: createBacklogRepositories, logger }, targetDir, flags);
|
|
88
106
|
}
|
|
89
107
|
catch (error) {
|
|
90
108
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
91
109
|
this.error(`更新に失敗しました: ${errorMessage}`);
|
|
92
110
|
}
|
|
93
111
|
}
|
|
94
|
-
// 確認プロンプトの表示
|
|
95
|
-
async confirmUpdate(options) {
|
|
96
|
-
if (options.force)
|
|
97
|
-
return true;
|
|
98
|
-
this.log(`以下の設定で更新を実行します:`);
|
|
99
|
-
this.log(`- ディレクトリ: ${options.targetDir}`);
|
|
100
|
-
this.log(`- ドメイン: ${options.domain}`);
|
|
101
|
-
this.log(`- プロジェクト: ${options.projectIdOrKey}`);
|
|
102
|
-
if (options.folderType) {
|
|
103
|
-
this.log(`- フォルダタイプ: ${options.folderType}`);
|
|
104
|
-
}
|
|
105
|
-
const updateTargets = [];
|
|
106
|
-
if (options.updateIssues)
|
|
107
|
-
updateTargets.push('課題');
|
|
108
|
-
if (options.updateWikis)
|
|
109
|
-
updateTargets.push('Wiki');
|
|
110
|
-
if (options.updateDocuments)
|
|
111
|
-
updateTargets.push('ドキュメント');
|
|
112
|
-
this.log(`- 更新対象: ${updateTargets.join('・')}`);
|
|
113
|
-
// 確認プロンプトを表示
|
|
114
|
-
this.log('更新を実行しますか? (y/n)');
|
|
115
|
-
process.stdin.resume();
|
|
116
|
-
process.stdin.setEncoding('utf8');
|
|
117
|
-
const response = await new Promise((resolve) => {
|
|
118
|
-
process.stdin.once('data', (data) => {
|
|
119
|
-
const input = data.toString().trim().toLowerCase();
|
|
120
|
-
resolve(input === 'y' || input === 'yes');
|
|
121
|
-
process.stdin.pause();
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
if (!response) {
|
|
125
|
-
this.log('更新をキャンセルしました');
|
|
126
|
-
}
|
|
127
|
-
return response;
|
|
128
|
-
}
|
|
129
|
-
// 更新対象の決定
|
|
130
|
-
determineUpdateTargets(folderType, documentsOnly, issuesOnly, wikisOnly) {
|
|
131
|
-
let updateIssues = !wikisOnly && !documentsOnly;
|
|
132
|
-
let updateWikis = !issuesOnly && !documentsOnly;
|
|
133
|
-
let updateDocuments = !issuesOnly && !wikisOnly;
|
|
134
|
-
// フォルダタイプに応じて更新対象を決定
|
|
135
|
-
switch (folderType) {
|
|
136
|
-
case FolderType.DOCUMENT: {
|
|
137
|
-
updateIssues = false;
|
|
138
|
-
updateWikis = false;
|
|
139
|
-
updateDocuments = true;
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
case FolderType.ISSUE: {
|
|
143
|
-
updateIssues = true;
|
|
144
|
-
updateWikis = false;
|
|
145
|
-
updateDocuments = false;
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
case FolderType.WIKI: {
|
|
149
|
-
updateIssues = false;
|
|
150
|
-
updateWikis = true;
|
|
151
|
-
updateDocuments = false;
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return { updateDocuments, updateIssues, updateWikis };
|
|
156
|
-
}
|
|
157
|
-
// 設定ファイルを探索して更新を実行
|
|
158
|
-
async findAndUpdateSettings(targetDir, flags) {
|
|
159
|
-
// 現在のディレクトリに設定ファイルがあるか確認
|
|
160
|
-
const settingsPath = getSettingsFilePath(targetDir);
|
|
161
|
-
let hasSettings = false;
|
|
162
|
-
try {
|
|
163
|
-
await access(settingsPath);
|
|
164
|
-
hasSettings = true;
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
// 設定ファイルが存在しない場合は何もしない
|
|
168
|
-
}
|
|
169
|
-
if (hasSettings) {
|
|
170
|
-
// 設定ファイルが存在する場合は更新を実行
|
|
171
|
-
await this.updateDirectory(targetDir, flags);
|
|
172
|
-
}
|
|
173
|
-
// サブディレクトリを探索
|
|
174
|
-
try {
|
|
175
|
-
const entries = await readdir(targetDir, { withFileTypes: true });
|
|
176
|
-
for (const entry of entries) {
|
|
177
|
-
if (entry.isDirectory()) {
|
|
178
|
-
const subDir = join(targetDir, entry.name);
|
|
179
|
-
// eslint-disable-next-line no-await-in-loop
|
|
180
|
-
await this.findAndUpdateSettings(subDir, flags);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
this.warn(`ディレクトリの読み取りに失敗しました: ${targetDir}`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// 指定されたディレクトリの更新を実行
|
|
189
|
-
async updateDirectory(targetDir, flags) {
|
|
190
|
-
// 設定ファイルを読み込む
|
|
191
|
-
const settings = await loadSettings(targetDir);
|
|
192
|
-
// コマンドライン引数と設定ファイルを組み合わせて使用する値を決定
|
|
193
|
-
const domain = flags.domain || settings.domain;
|
|
194
|
-
const projectIdOrKey = flags.projectIdOrKey || settings.projectIdOrKey;
|
|
195
|
-
const { folderType } = settings;
|
|
196
|
-
const { documentsOnly, force, issuesOnly, wikisOnly } = flags;
|
|
197
|
-
// 設定ファイルからオプションを読み込み、コマンドライン引数で上書き
|
|
198
|
-
const issueKeyFileName = flags.issueKeyFileName ?? settings.issueKeyFileName ?? false;
|
|
199
|
-
const issueKeyFolder = flags.issueKeyFolder ?? settings.issueKeyFolder ?? false;
|
|
200
|
-
// 必須パラメータの検証
|
|
201
|
-
if (!domain) {
|
|
202
|
-
this.warn(`${targetDir}: ドメインが指定されていません。スキップします。`);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (!projectIdOrKey) {
|
|
206
|
-
this.warn(`${targetDir}: プロジェクトIDまたはキーが指定されていません。スキップします。`);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
// APIキーの検証
|
|
210
|
-
let apiKey;
|
|
211
|
-
try {
|
|
212
|
-
apiKey = flags.apiKey || settings.apiKey || getApiKey(this);
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
this.warn(`${targetDir}: APIキーが指定されていません。--apiKey フラグまたは BACKLOG_API_KEY 環境変数で設定してください。`);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
// 更新対象の決定
|
|
219
|
-
const { updateDocuments, updateIssues, updateWikis } = this.determineUpdateTargets(folderType, documentsOnly, issuesOnly, wikisOnly);
|
|
220
|
-
// 更新前の確認
|
|
221
|
-
const confirmed = await this.confirmUpdate({
|
|
222
|
-
domain,
|
|
223
|
-
folderType,
|
|
224
|
-
force: force || false,
|
|
225
|
-
projectIdOrKey,
|
|
226
|
-
targetDir,
|
|
227
|
-
updateDocuments,
|
|
228
|
-
updateIssues,
|
|
229
|
-
updateWikis,
|
|
230
|
-
});
|
|
231
|
-
if (!confirmed)
|
|
232
|
-
return;
|
|
233
|
-
// 出力ディレクトリの作成
|
|
234
|
-
await createOutputDirectory(targetDir);
|
|
235
|
-
// プロジェクトキーからプロジェクトIDを取得
|
|
236
|
-
const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey);
|
|
237
|
-
this.log(`プロジェクトID: ${projectId} を使用します`);
|
|
238
|
-
// 課題の更新
|
|
239
|
-
if (updateIssues) {
|
|
240
|
-
await this.updateIssues({
|
|
241
|
-
apiKey,
|
|
242
|
-
domain,
|
|
243
|
-
issueKeyFileName,
|
|
244
|
-
issueKeyFolder,
|
|
245
|
-
projectId,
|
|
246
|
-
projectIdOrKey,
|
|
247
|
-
targetDir,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
// Wikiの更新
|
|
251
|
-
if (updateWikis) {
|
|
252
|
-
await this.updateWikis({
|
|
253
|
-
apiKey,
|
|
254
|
-
domain,
|
|
255
|
-
projectIdOrKey,
|
|
256
|
-
targetDir,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
// ドキュメントの更新
|
|
260
|
-
if (updateDocuments) {
|
|
261
|
-
await this.updateDocuments({
|
|
262
|
-
apiKey,
|
|
263
|
-
domain,
|
|
264
|
-
projectId,
|
|
265
|
-
projectIdOrKey,
|
|
266
|
-
targetDir,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
this.log(`${targetDir} の更新が完了しました!`);
|
|
270
|
-
}
|
|
271
|
-
// ドキュメントの更新
|
|
272
|
-
async updateDocuments(options) {
|
|
273
|
-
this.log('ドキュメントの更新を開始します...');
|
|
274
|
-
// 設定ファイルから前回の更新日時を取得
|
|
275
|
-
const { lastUpdated } = await loadSettings(options.targetDir);
|
|
276
|
-
await downloadDocuments(this, {
|
|
277
|
-
apiKey: options.apiKey,
|
|
278
|
-
domain: options.domain,
|
|
279
|
-
lastUpdated,
|
|
280
|
-
outputDir: options.targetDir,
|
|
281
|
-
projectId: options.projectId,
|
|
282
|
-
projectIdOrKey: options.projectIdOrKey,
|
|
283
|
-
});
|
|
284
|
-
// 設定ファイルを更新
|
|
285
|
-
await updateSettings(options.targetDir, {
|
|
286
|
-
apiKey: options.apiKey,
|
|
287
|
-
domain: options.domain,
|
|
288
|
-
folderType: FolderType.DOCUMENT,
|
|
289
|
-
lastUpdated: new Date().toISOString(),
|
|
290
|
-
outputDir: options.targetDir,
|
|
291
|
-
projectIdOrKey: options.projectIdOrKey,
|
|
292
|
-
});
|
|
293
|
-
this.log('ドキュメントの更新が完了しました');
|
|
294
|
-
}
|
|
295
|
-
// 課題の更新
|
|
296
|
-
async updateIssues(options) {
|
|
297
|
-
this.log('課題の更新を開始します...');
|
|
298
|
-
// 設定ファイルから前回の更新日時を取得
|
|
299
|
-
const { lastUpdated } = await loadSettings(options.targetDir);
|
|
300
|
-
await downloadIssues(this, {
|
|
301
|
-
apiKey: options.apiKey,
|
|
302
|
-
count: 100,
|
|
303
|
-
domain: options.domain,
|
|
304
|
-
issueKeyFileName: options.issueKeyFileName,
|
|
305
|
-
issueKeyFolder: options.issueKeyFolder,
|
|
306
|
-
lastUpdated,
|
|
307
|
-
outputDir: options.targetDir,
|
|
308
|
-
projectId: options.projectId,
|
|
309
|
-
});
|
|
310
|
-
// 設定ファイルを更新
|
|
311
|
-
await updateSettings(options.targetDir, {
|
|
312
|
-
apiKey: options.apiKey,
|
|
313
|
-
domain: options.domain,
|
|
314
|
-
folderType: FolderType.ISSUE,
|
|
315
|
-
lastUpdated: new Date().toISOString(),
|
|
316
|
-
outputDir: options.targetDir,
|
|
317
|
-
projectIdOrKey: options.projectIdOrKey,
|
|
318
|
-
});
|
|
319
|
-
this.log('課題の更新が完了しました');
|
|
320
|
-
}
|
|
321
|
-
// Wikiの更新
|
|
322
|
-
async updateWikis(options) {
|
|
323
|
-
this.log('Wikiの更新を開始します...');
|
|
324
|
-
// 設定ファイルから前回の更新日時を取得
|
|
325
|
-
const { lastUpdated } = await loadSettings(options.targetDir);
|
|
326
|
-
await downloadWikis(this, {
|
|
327
|
-
apiKey: options.apiKey,
|
|
328
|
-
domain: options.domain,
|
|
329
|
-
lastUpdated,
|
|
330
|
-
outputDir: options.targetDir,
|
|
331
|
-
projectIdOrKey: options.projectIdOrKey,
|
|
332
|
-
});
|
|
333
|
-
// 設定ファイルを更新
|
|
334
|
-
await updateSettings(options.targetDir, {
|
|
335
|
-
apiKey: options.apiKey,
|
|
336
|
-
domain: options.domain,
|
|
337
|
-
folderType: FolderType.WIKI,
|
|
338
|
-
lastUpdated: new Date().toISOString(),
|
|
339
|
-
outputDir: options.targetDir,
|
|
340
|
-
projectIdOrKey: options.projectIdOrKey,
|
|
341
|
-
});
|
|
342
|
-
this.log('Wikiの更新が完了しました');
|
|
343
|
-
}
|
|
344
112
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { createBacklogRepositories } from '../../composition/backlog-repositories.js';
|
|
3
|
+
import { FolderType } from '../../modules/settings/domain/settings.js';
|
|
4
|
+
import { updateSettings } from '../../modules/settings/repository/settings-store.js';
|
|
5
|
+
import { exportWikis } from '../../modules/wiki/use-case/export-wikis.js';
|
|
6
|
+
import { API_KEY_NOT_FOUND_MESSAGE, loadDotenv, resolveApiKey } from '../../shared/config/env.js';
|
|
7
|
+
import { ensureDirectory } from '../../shared/storage/markdown-store.js';
|
|
6
8
|
// .envファイルを読み込む
|
|
7
|
-
|
|
9
|
+
loadDotenv();
|
|
8
10
|
export default class Wiki extends Command {
|
|
9
11
|
static description = 'Backlogから Wiki を取得してMarkdownファイルとして保存する';
|
|
10
12
|
static examples = [
|
|
@@ -38,10 +40,17 @@ Wikiをダウンロードする
|
|
|
38
40
|
const { flags } = await this.parse(Wiki);
|
|
39
41
|
try {
|
|
40
42
|
const { domain, projectIdOrKey } = flags;
|
|
41
|
-
const apiKey = flags.apiKey
|
|
43
|
+
const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
|
|
44
|
+
this.error(API_KEY_NOT_FOUND_MESSAGE);
|
|
42
45
|
const outputDir = flags.output || './wiki';
|
|
46
|
+
const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
|
|
47
|
+
const { wikiRepository } = createBacklogRepositories({
|
|
48
|
+
apiKey,
|
|
49
|
+
domain,
|
|
50
|
+
onRateLimitWait: () => this.log('レート制限を回避するため15秒間待機します...'),
|
|
51
|
+
});
|
|
43
52
|
// 出力ディレクトリの作成
|
|
44
|
-
await
|
|
53
|
+
await ensureDirectory(outputDir);
|
|
45
54
|
// 設定ファイルを保存
|
|
46
55
|
await updateSettings(outputDir, {
|
|
47
56
|
apiKey,
|
|
@@ -51,8 +60,7 @@ Wikiをダウンロードする
|
|
|
51
60
|
projectIdOrKey,
|
|
52
61
|
});
|
|
53
62
|
// Wikiの取得と保存
|
|
54
|
-
await
|
|
55
|
-
apiKey,
|
|
63
|
+
await exportWikis({ logger, wikiRepository }, {
|
|
56
64
|
domain,
|
|
57
65
|
outputDir,
|
|
58
66
|
projectIdOrKey,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DocumentRepository } from '../modules/document/domain/document-repository.js';
|
|
2
|
+
import { IssueRepository } from '../modules/issue/domain/issue-repository.js';
|
|
3
|
+
import { ProjectRepository } from '../modules/project/domain/project-repository.js';
|
|
4
|
+
import { WikiRepository } from '../modules/wiki/domain/wiki-repository.js';
|
|
5
|
+
export interface BacklogConnection {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
domain: string;
|
|
8
|
+
onRateLimitWait?: () => void;
|
|
9
|
+
}
|
|
10
|
+
export interface BacklogRepositories {
|
|
11
|
+
documentRepository: DocumentRepository;
|
|
12
|
+
issueRepository: IssueRepository;
|
|
13
|
+
projectRepository: ProjectRepository;
|
|
14
|
+
wikiRepository: WikiRepository;
|
|
15
|
+
}
|
|
16
|
+
export declare function createBacklogRepositories(connection: BacklogConnection): BacklogRepositories;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { newBacklogDocumentRepository } from '../modules/document/repository/backlog-document-repository.js';
|
|
2
|
+
import { newBacklogIssueRepository } from '../modules/issue/repository/backlog-issue-repository.js';
|
|
3
|
+
import { newBacklogProjectRepository } from '../modules/project/repository/backlog-project-repository.js';
|
|
4
|
+
import { newBacklogWikiRepository } from '../modules/wiki/repository/backlog-wiki-repository.js';
|
|
5
|
+
import { BacklogHttpClient } from '../shared/backlog/http-client.js';
|
|
6
|
+
// 1つのHTTPクライアント(=レート制限カウンタ)を全repositoryで共有する
|
|
7
|
+
export function createBacklogRepositories(connection) {
|
|
8
|
+
const client = new BacklogHttpClient(connection);
|
|
9
|
+
return {
|
|
10
|
+
documentRepository: newBacklogDocumentRepository(client),
|
|
11
|
+
issueRepository: newBacklogIssueRepository(client),
|
|
12
|
+
projectRepository: newBacklogProjectRepository(client),
|
|
13
|
+
wikiRepository: newBacklogWikiRepository(client),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ExportTarget } from '../use-case/export-all.js';
|
|
2
|
+
export type ContentSelection = 'both' | 'documents' | 'wiki';
|
|
3
|
+
export declare function parseContentSelection(input: string): ContentSelection;
|
|
4
|
+
export declare function applyContentSelection(targets: ExportTarget[], selection: ContentSelection): ExportTarget[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// 空入力・不正入力は「両方」として扱う(従来動作がデフォルト)
|
|
2
|
+
export function parseContentSelection(input) {
|
|
3
|
+
const normalized = input.trim().toLowerCase();
|
|
4
|
+
if (normalized === '2' || normalized === 'wiki')
|
|
5
|
+
return 'wiki';
|
|
6
|
+
if (normalized === '3' || normalized === 'document' || normalized === 'documents')
|
|
7
|
+
return 'documents';
|
|
8
|
+
return 'both';
|
|
9
|
+
}
|
|
10
|
+
export function applyContentSelection(targets, selection) {
|
|
11
|
+
if (selection === 'wiki')
|
|
12
|
+
return targets.filter((target) => target !== 'documents');
|
|
13
|
+
if (selection === 'documents')
|
|
14
|
+
return targets.filter((target) => target !== 'wiki');
|
|
15
|
+
return targets;
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Logger } from '../../../shared/ports.js';
|
|
2
|
+
import { DocumentRepository } from '../../document/domain/document-repository.js';
|
|
3
|
+
import { IssueRepository } from '../../issue/domain/issue-repository.js';
|
|
4
|
+
import { ProjectRepository } from '../../project/domain/project-repository.js';
|
|
5
|
+
import { WikiRepository } from '../../wiki/domain/wiki-repository.js';
|
|
6
|
+
export interface ExportAllDeps {
|
|
7
|
+
documentRepository: DocumentRepository;
|
|
8
|
+
issueRepository: IssueRepository;
|
|
9
|
+
logger: Logger;
|
|
10
|
+
projectRepository: ProjectRepository;
|
|
11
|
+
wikiRepository: WikiRepository;
|
|
12
|
+
}
|
|
13
|
+
export type ExportTarget = 'documents' | 'issues' | 'wiki';
|
|
14
|
+
export interface ExportAllOptions {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
domain: string;
|
|
17
|
+
issueKeyFileName?: boolean;
|
|
18
|
+
issueKeyFolder?: boolean;
|
|
19
|
+
maxCount: number;
|
|
20
|
+
outputRoot: string;
|
|
21
|
+
projectIdOrKey: string;
|
|
22
|
+
targets: ExportTarget[];
|
|
23
|
+
}
|
|
24
|
+
export declare function exportAll(deps: ExportAllDeps, options: ExportAllOptions): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ensureDirectory } from '../../../shared/storage/markdown-store.js';
|
|
3
|
+
import { exportDocuments } from '../../document/use-case/export-documents.js';
|
|
4
|
+
import { exportIssues } from '../../issue/use-case/export-issues.js';
|
|
5
|
+
import { FolderType } from '../../settings/domain/settings.js';
|
|
6
|
+
import { updateSettings } from '../../settings/repository/settings-store.js';
|
|
7
|
+
import { exportWikis } from '../../wiki/use-case/export-wikis.js';
|
|
8
|
+
export async function exportAll(deps, options) {
|
|
9
|
+
const { documentRepository, issueRepository, logger, projectRepository, wikiRepository } = deps;
|
|
10
|
+
const { apiKey, domain, issueKeyFileName, issueKeyFolder, maxCount, outputRoot, projectIdOrKey, targets } = options;
|
|
11
|
+
await ensureDirectory(outputRoot);
|
|
12
|
+
const projectId = await projectRepository.resolveProjectId(projectIdOrKey);
|
|
13
|
+
logger.log(`プロジェクトID: ${projectId} を使用します`);
|
|
14
|
+
if (targets.includes('issues')) {
|
|
15
|
+
const issueOutput = path.join(outputRoot, 'issues');
|
|
16
|
+
await ensureDirectory(issueOutput);
|
|
17
|
+
await updateSettings(issueOutput, {
|
|
18
|
+
apiKey,
|
|
19
|
+
domain,
|
|
20
|
+
folderType: FolderType.ISSUE,
|
|
21
|
+
issueKeyFileName,
|
|
22
|
+
issueKeyFolder,
|
|
23
|
+
outputDir: issueOutput,
|
|
24
|
+
projectIdOrKey,
|
|
25
|
+
});
|
|
26
|
+
logger.log('課題の取得を開始します...');
|
|
27
|
+
await exportIssues({ issueRepository, logger }, {
|
|
28
|
+
count: maxCount,
|
|
29
|
+
domain,
|
|
30
|
+
issueKeyFileName,
|
|
31
|
+
issueKeyFolder,
|
|
32
|
+
outputDir: issueOutput,
|
|
33
|
+
projectId,
|
|
34
|
+
});
|
|
35
|
+
await updateSettings(issueOutput, { lastUpdated: new Date().toISOString() });
|
|
36
|
+
logger.log('課題の取得が完了しました');
|
|
37
|
+
}
|
|
38
|
+
if (targets.includes('wiki')) {
|
|
39
|
+
const wikiOutput = path.join(outputRoot, 'wiki');
|
|
40
|
+
await ensureDirectory(wikiOutput);
|
|
41
|
+
await updateSettings(wikiOutput, {
|
|
42
|
+
apiKey,
|
|
43
|
+
domain,
|
|
44
|
+
folderType: FolderType.WIKI,
|
|
45
|
+
outputDir: wikiOutput,
|
|
46
|
+
projectIdOrKey,
|
|
47
|
+
});
|
|
48
|
+
logger.log('Wikiの取得を開始します...');
|
|
49
|
+
await exportWikis({ logger, wikiRepository }, {
|
|
50
|
+
domain,
|
|
51
|
+
outputDir: wikiOutput,
|
|
52
|
+
projectIdOrKey,
|
|
53
|
+
});
|
|
54
|
+
await updateSettings(wikiOutput, { lastUpdated: new Date().toISOString() });
|
|
55
|
+
logger.log('Wikiの取得が完了しました');
|
|
56
|
+
}
|
|
57
|
+
if (targets.includes('documents')) {
|
|
58
|
+
const documentOutput = path.join(outputRoot, 'documents');
|
|
59
|
+
await ensureDirectory(documentOutput);
|
|
60
|
+
await updateSettings(documentOutput, {
|
|
61
|
+
apiKey,
|
|
62
|
+
domain,
|
|
63
|
+
folderType: FolderType.DOCUMENT,
|
|
64
|
+
outputDir: documentOutput,
|
|
65
|
+
projectIdOrKey,
|
|
66
|
+
});
|
|
67
|
+
logger.log('ドキュメントの取得を開始します...');
|
|
68
|
+
await exportDocuments({ documentRepository, logger }, {
|
|
69
|
+
domain,
|
|
70
|
+
outputDir: documentOutput,
|
|
71
|
+
projectId,
|
|
72
|
+
projectIdOrKey,
|
|
73
|
+
});
|
|
74
|
+
await updateSettings(documentOutput, { lastUpdated: new Date().toISOString() });
|
|
75
|
+
logger.log('ドキュメントの取得が完了しました');
|
|
76
|
+
}
|
|
77
|
+
logger.log('すべてのデータの取得が完了しました!');
|
|
78
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { wrapBody } from '../../../shared/markdown/body-marker.js';
|
|
2
|
+
export function buildDocumentMarkdown(documentDetail, backlogDocumentUrl) {
|
|
3
|
+
// 添付ファイルリストの作成
|
|
4
|
+
let attachmentsSection = '';
|
|
5
|
+
if (documentDetail.attachments && documentDetail.attachments.length > 0) {
|
|
6
|
+
attachmentsSection = '\n\n## 添付ファイル\n';
|
|
7
|
+
for (const attachment of documentDetail.attachments) {
|
|
8
|
+
const attachmentDate = new Date(attachment.created).toLocaleString('ja-JP');
|
|
9
|
+
const fileSize = (attachment.size / 1024).toFixed(1);
|
|
10
|
+
attachmentsSection += `- **${attachment.name}** (${fileSize} KB) - 作成者: ${attachment.createdUser.name}, 作成日時: ${attachmentDate}\n`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// タグリストの作成
|
|
14
|
+
let tagsSection = '';
|
|
15
|
+
if (documentDetail.tags && documentDetail.tags.length > 0) {
|
|
16
|
+
tagsSection = '\n\n## タグ\n';
|
|
17
|
+
for (const tag of documentDetail.tags) {
|
|
18
|
+
tagsSection += `- ${tag.name}\n`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// 作成者・更新者情報
|
|
22
|
+
const createdDate = new Date(documentDetail.created).toLocaleString('ja-JP');
|
|
23
|
+
const updatedDate = new Date(documentDetail.updated).toLocaleString('ja-JP');
|
|
24
|
+
return `# ${documentDetail.title}
|
|
25
|
+
|
|
26
|
+
[Backlog Document Link](${backlogDocumentUrl})
|
|
27
|
+
|
|
28
|
+
**ステータス**: ${documentDetail.statusId}${documentDetail.emoji ? ` ${documentDetail.emoji}` : ''}
|
|
29
|
+
**作成者**: ${documentDetail.createdUser.name}
|
|
30
|
+
**作成日時**: ${createdDate}
|
|
31
|
+
**更新者**: ${documentDetail.updatedUser.name}
|
|
32
|
+
**更新日時**: ${updatedDate}
|
|
33
|
+
|
|
34
|
+
## 内容
|
|
35
|
+
|
|
36
|
+
${wrapBody(documentDetail.plain || '(内容なし)')}${attachmentsSection}${tagsSection}`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ExpectedPaths } from '../../prune/domain/expected-paths.js';
|
|
2
|
+
import { DocumentNode } from './document.js';
|
|
3
|
+
export declare const PARENT_DOCUMENT_INDEX_FILENAME = "00_index.md";
|
|
4
|
+
export declare function documentFolderPath(currentPath: string, folderName: string): string;
|
|
5
|
+
export declare function documentFileName(title: string, asParentIndex: boolean): string;
|
|
6
|
+
export declare function documentUrl(domain: string, projectIdOrKey: string, documentId: string): string;
|
|
7
|
+
export interface DocumentTreePaths extends ExpectedPaths {
|
|
8
|
+
leafNodes: Array<{
|
|
9
|
+
currentPath: string;
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export declare function collectDocumentTreePaths(rootNodes: DocumentNode[]): DocumentTreePaths;
|
|
15
|
+
export declare function resolveDocumentLeafPaths(paths: DocumentTreePaths, titlesById: Map<string, string>): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { backlogOrigin } from '../../../shared/backlog-url.js';
|
|
3
|
+
import { sanitizeFileName } from '../../../shared/file-name.js';
|
|
4
|
+
// 子を持つ親ドキュメント自身の本文の保存先。ダウンロード側とprune側でレイアウト定義を共有する
|
|
5
|
+
export const PARENT_DOCUMENT_INDEX_FILENAME = '00_index.md';
|
|
6
|
+
export function documentFolderPath(currentPath, folderName) {
|
|
7
|
+
return path.join(currentPath, sanitizeFileName(folderName));
|
|
8
|
+
}
|
|
9
|
+
export function documentFileName(title, asParentIndex) {
|
|
10
|
+
return asParentIndex ? PARENT_DOCUMENT_INDEX_FILENAME : `${sanitizeFileName(title)}.md`;
|
|
11
|
+
}
|
|
12
|
+
export function documentUrl(domain, projectIdOrKey, documentId) {
|
|
13
|
+
return `${backlogOrigin(domain)}/document/${projectIdOrKey}/${documentId}`;
|
|
14
|
+
}
|
|
15
|
+
export function collectDocumentTreePaths(rootNodes) {
|
|
16
|
+
const expectedFiles = new Set();
|
|
17
|
+
const expectedDirs = new Set();
|
|
18
|
+
const leafNodes = [];
|
|
19
|
+
const walk = (node, currentPath) => {
|
|
20
|
+
if (node.children && node.children.length > 0) {
|
|
21
|
+
const dirPath = documentFolderPath(currentPath, node.name).normalize('NFC');
|
|
22
|
+
expectedDirs.add(dirPath);
|
|
23
|
+
expectedFiles.add(path.join(dirPath, PARENT_DOCUMENT_INDEX_FILENAME).normalize('NFC'));
|
|
24
|
+
for (const child of node.children) {
|
|
25
|
+
walk(child, dirPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
leafNodes.push({ currentPath, id: node.id, name: node.name });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
for (const rootNode of rootNodes) {
|
|
33
|
+
walk(rootNode, '');
|
|
34
|
+
}
|
|
35
|
+
return { expectedDirs, expectedFiles, leafNodes };
|
|
36
|
+
}
|
|
37
|
+
// 一覧に現れないリーフは空フォルダの可能性があるため、ディレクトリとname由来ファイルの両方を保護する(誤削除より安全側)
|
|
38
|
+
export function resolveDocumentLeafPaths(paths, titlesById) {
|
|
39
|
+
for (const leaf of paths.leafNodes) {
|
|
40
|
+
const title = titlesById.get(leaf.id);
|
|
41
|
+
if (title === undefined) {
|
|
42
|
+
paths.expectedDirs.add(path.join(leaf.currentPath, sanitizeFileName(leaf.name)).normalize('NFC'));
|
|
43
|
+
paths.expectedFiles.add(path.join(leaf.currentPath, `${sanitizeFileName(leaf.name)}.md`).normalize('NFC'));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
paths.expectedFiles.add(path.join(leaf.currentPath, `${sanitizeFileName(title)}.md`).normalize('NFC'));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DocumentDetail, DocumentTree } from './document.js';
|
|
2
|
+
export interface DocumentRepository {
|
|
3
|
+
fetchAllTitles(projectId: number): Promise<Map<string, string>>;
|
|
4
|
+
fetchDetail(documentId: string): Promise<DocumentDetail>;
|
|
5
|
+
fetchTree(projectId: number): Promise<DocumentTree>;
|
|
6
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|