backlog-exporter 0.7.3 → 1.1.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 +109 -1
- package/bin/dev.js +6 -2
- package/dist/commands/all/index.d.ts +3 -0
- package/dist/commands/all/index.js +69 -127
- package/dist/commands/document/index.d.ts +1 -0
- package/dist/commands/document/index.js +26 -12
- package/dist/commands/issue/index.d.ts +1 -0
- package/dist/commands/issue/index.js +29 -12
- package/dist/commands/prune/index.d.ts +16 -0
- package/dist/commands/prune/index.js +86 -0
- package/dist/commands/update/index.d.ts +4 -7
- package/dist/commands/update/index.js +33 -260
- package/dist/commands/wiki/index.d.ts +1 -0
- package/dist/commands/wiki/index.js +25 -10
- 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 +25 -0
- package/dist/modules/all/use-case/export-all.js +84 -0
- package/dist/modules/document/domain/document-markdown.d.ts +2 -0
- package/dist/modules/document/domain/document-markdown.js +40 -0
- package/dist/modules/document/domain/document-path.d.ts +23 -0
- package/dist/modules/document/domain/document-path.js +57 -0
- package/dist/modules/document/domain/document-repository.d.ts +7 -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 +35 -0
- package/dist/modules/document/use-case/export-documents.d.ts +17 -0
- package/dist/modules/document/use-case/export-documents.js +107 -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 +5 -0
- package/dist/modules/issue/domain/issue-markdown.js +127 -0
- package/dist/modules/issue/domain/issue-path.d.ts +34 -0
- package/dist/modules/issue/domain/issue-path.js +42 -0
- package/dist/modules/issue/domain/issue-repository.d.ts +12 -0
- package/dist/modules/issue/domain/issue-repository.js +1 -0
- package/dist/modules/issue/domain/issue.d.ts +53 -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 +41 -0
- package/dist/modules/issue/use-case/export-issues.d.ts +19 -0
- package/dist/modules/issue/use-case/export-issues.js +113 -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 +44 -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 +16 -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 +38 -0
- package/dist/modules/update/domain/update-plan.js +70 -0
- package/dist/modules/update/use-case/update-exports.d.ts +21 -0
- package/dist/modules/update/use-case/update-exports.js +139 -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 +6 -0
- package/dist/modules/wiki/domain/wiki-markdown.js +19 -0
- package/dist/modules/wiki/domain/wiki-path.d.ts +7 -0
- package/dist/modules/wiki/domain/wiki-path.js +37 -0
- package/dist/modules/wiki/domain/wiki-repository.d.ts +6 -0
- package/dist/modules/wiki/domain/wiki-repository.js +1 -0
- package/dist/modules/wiki/domain/wiki.d.ts +16 -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 +13 -0
- package/dist/modules/wiki/use-case/export-wikis.d.ts +15 -0
- package/dist/modules/wiki/use-case/export-wikis.js +71 -0
- package/dist/shared/attachment.d.ts +8 -0
- package/dist/shared/attachment.js +37 -0
- package/dist/shared/backlog/http-client.d.ts +20 -0
- package/dist/shared/backlog/http-client.js +93 -0
- package/dist/shared/backlog/sleep.d.ts +1 -0
- package/dist/shared/backlog/sleep.js +3 -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 +11 -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 +3 -0
- package/dist/shared/file-name.js +26 -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 +7 -0
- package/dist/shared/storage/markdown-store.js +32 -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 +160 -27
- package/package.json +15 -19
- package/dist/utils/backlog-api.d.ts +0 -74
- package/dist/utils/backlog-api.js +0 -453
- 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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { createBacklogRepositories } from '../../composition/backlog-repositories.js';
|
|
4
|
+
import { pruneDirectories } from '../../modules/prune/use-case/prune-directories.js';
|
|
5
|
+
import { loadDotenv } from '../../shared/config/env.js';
|
|
6
|
+
import { isInteractiveStdin, readYesNo } from '../../shared/console/prompt.js';
|
|
7
|
+
// .envファイルを読み込む
|
|
8
|
+
loadDotenv();
|
|
9
|
+
export default class Prune extends Command {
|
|
10
|
+
static args = {
|
|
11
|
+
directory: Args.string({
|
|
12
|
+
description: '対象ディレクトリ(設定ファイルが保存されている場所)',
|
|
13
|
+
required: false,
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
static description = 'Backlog上で削除・移動された課題・ドキュメント・Wikiのローカルファイルを削除し、Backlogと同じ状態に揃える';
|
|
17
|
+
static examples = [
|
|
18
|
+
`<%= config.bin %> <%= command.id %>
|
|
19
|
+
カレントディレクトリ配下の設定ファイルを探索し、Backlog上に存在しないドキュメントファイルを削除する
|
|
20
|
+
`,
|
|
21
|
+
`<%= config.bin %> <%= command.id %> ./backlog-documents
|
|
22
|
+
指定したディレクトリを対象にする
|
|
23
|
+
`,
|
|
24
|
+
`<%= config.bin %> <%= command.id %> --force
|
|
25
|
+
確認プロンプトをスキップして削除する
|
|
26
|
+
`,
|
|
27
|
+
];
|
|
28
|
+
static flags = {
|
|
29
|
+
apiKey: Flags.string({
|
|
30
|
+
description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)',
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
domain: Flags.string({
|
|
34
|
+
description: 'Backlog domain (e.g. example.backlog.jp)',
|
|
35
|
+
required: false,
|
|
36
|
+
}),
|
|
37
|
+
force: Flags.boolean({
|
|
38
|
+
char: 'f',
|
|
39
|
+
description: '確認プロンプトをスキップする',
|
|
40
|
+
required: false,
|
|
41
|
+
}),
|
|
42
|
+
projectIdOrKey: Flags.string({
|
|
43
|
+
description: 'Backlog project ID or key',
|
|
44
|
+
required: false,
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
async run() {
|
|
48
|
+
const { args, flags } = await this.parse(Prune);
|
|
49
|
+
const targetDir = args.directory || process.cwd();
|
|
50
|
+
const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
|
|
51
|
+
try {
|
|
52
|
+
await pruneDirectories({ createRepositories: createBacklogRepositories, logger }, {
|
|
53
|
+
confirmDirectory: (directory) => this.confirmPrune(directory, flags.force ?? false),
|
|
54
|
+
flags: {
|
|
55
|
+
apiKey: flags.apiKey,
|
|
56
|
+
domain: flags.domain,
|
|
57
|
+
projectIdOrKey: flags.projectIdOrKey,
|
|
58
|
+
},
|
|
59
|
+
rootDir: targetDir,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
64
|
+
this.error(`pruneに失敗しました: ${errorMessage}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 削除実行前の確認プロンプト(--force 時はスキップ)
|
|
68
|
+
async confirmPrune(targetDir, force) {
|
|
69
|
+
if (force) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
// 非対話環境(CI・パイプ入力など)では 'data' イベントが発火せず永久に待機してしまうため、
|
|
73
|
+
// プロンプトを出さずにエラーとして終了する
|
|
74
|
+
if (!isInteractiveStdin()) {
|
|
75
|
+
this.error('対話的な確認ができない環境のため中止しました(標準入力が端末ではありません)。--force フラグを付けると確認をスキップして実行できます。');
|
|
76
|
+
}
|
|
77
|
+
this.log('以下のディレクトリで、Backlog上に存在しない課題・ドキュメント・Wiki(.mdファイル)を削除します:');
|
|
78
|
+
this.log(`- ディレクトリ: ${targetDir}`);
|
|
79
|
+
this.log('削除を実行しますか? (y/n)');
|
|
80
|
+
const response = await readYesNo();
|
|
81
|
+
if (!response) {
|
|
82
|
+
this.log('pruneをキャンセルしました');
|
|
83
|
+
}
|
|
84
|
+
return response;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -7,21 +7,18 @@ export default class Update extends Command {
|
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
9
|
apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
documentId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
documentsOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
downloadAttachments: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
14
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
issueIdOrKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
16
|
issueKeyFileName: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
17
|
issueKeyFolder: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
18
|
issuesOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
19
|
projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
wikiId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
21
|
wikisOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
22
|
};
|
|
19
23
|
run(): Promise<void>;
|
|
20
|
-
private confirmUpdate;
|
|
21
|
-
private determineUpdateTargets;
|
|
22
|
-
private findAndUpdateSettings;
|
|
23
|
-
private updateDirectory;
|
|
24
|
-
private updateDocuments;
|
|
25
|
-
private updateIssues;
|
|
26
|
-
private updateWikis;
|
|
27
24
|
}
|
|
@@ -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,
|
|
@@ -52,11 +62,20 @@ export default class Update extends Command {
|
|
|
52
62
|
description: 'Backlog domain (e.g. example.backlog.jp)',
|
|
53
63
|
required: false,
|
|
54
64
|
}),
|
|
65
|
+
downloadAttachments: Flags.boolean({
|
|
66
|
+
char: 'd',
|
|
67
|
+
description: '課題の添付ファイルもダウンロードする(未指定時は設定ファイルの値を使用)',
|
|
68
|
+
required: false,
|
|
69
|
+
}),
|
|
55
70
|
force: Flags.boolean({
|
|
56
71
|
char: 'f',
|
|
57
72
|
description: '確認プロンプトをスキップする',
|
|
58
73
|
required: false,
|
|
59
74
|
}),
|
|
75
|
+
issueIdOrKey: Flags.string({
|
|
76
|
+
description: '指定した課題(IDまたはキー)のみを再取得する(カンマ区切りで複数指定可能)',
|
|
77
|
+
required: false,
|
|
78
|
+
}),
|
|
60
79
|
issueKeyFileName: Flags.boolean({
|
|
61
80
|
description: 'ファイル名を課題キーにする',
|
|
62
81
|
required: false,
|
|
@@ -73,6 +92,10 @@ export default class Update extends Command {
|
|
|
73
92
|
description: 'Backlog project ID or key',
|
|
74
93
|
required: false,
|
|
75
94
|
}),
|
|
95
|
+
wikiId: Flags.string({
|
|
96
|
+
description: '指定したWiki(ID)のみを再取得する(カンマ区切りで複数指定可能)',
|
|
97
|
+
required: false,
|
|
98
|
+
}),
|
|
76
99
|
wikisOnly: Flags.boolean({
|
|
77
100
|
description: 'Wikiのみを更新する',
|
|
78
101
|
required: false,
|
|
@@ -82,263 +105,13 @@ export default class Update extends Command {
|
|
|
82
105
|
const { args, flags } = await this.parse(Update);
|
|
83
106
|
// 更新対象のディレクトリを決定(指定がなければカレントディレクトリ)
|
|
84
107
|
const targetDir = args.directory || process.cwd();
|
|
108
|
+
const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
|
|
85
109
|
try {
|
|
86
|
-
|
|
87
|
-
await this.findAndUpdateSettings(targetDir, flags);
|
|
110
|
+
await updateExports({ createRepositories: createBacklogRepositories, logger }, targetDir, flags);
|
|
88
111
|
}
|
|
89
112
|
catch (error) {
|
|
90
113
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
91
114
|
this.error(`更新に失敗しました: ${errorMessage}`);
|
|
92
115
|
}
|
|
93
116
|
}
|
|
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
117
|
}
|
|
@@ -5,6 +5,7 @@ export default class Wiki 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
|
+
downloadAttachments: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
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
|
};
|
|
@@ -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 = [
|
|
@@ -24,6 +26,11 @@ Wikiをダウンロードする
|
|
|
24
26
|
description: 'Backlog domain (e.g. example.backlog.jp)',
|
|
25
27
|
required: true,
|
|
26
28
|
}),
|
|
29
|
+
downloadAttachments: Flags.boolean({
|
|
30
|
+
char: 'd',
|
|
31
|
+
description: '添付ファイルもダウンロードする',
|
|
32
|
+
required: false,
|
|
33
|
+
}),
|
|
27
34
|
output: Flags.string({
|
|
28
35
|
char: 'o',
|
|
29
36
|
description: '出力ディレクトリパス',
|
|
@@ -37,23 +44,31 @@ Wikiをダウンロードする
|
|
|
37
44
|
async run() {
|
|
38
45
|
const { flags } = await this.parse(Wiki);
|
|
39
46
|
try {
|
|
40
|
-
const { domain, projectIdOrKey } = flags;
|
|
41
|
-
const apiKey = flags.apiKey
|
|
47
|
+
const { domain, downloadAttachments, projectIdOrKey } = flags;
|
|
48
|
+
const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
|
|
49
|
+
this.error(API_KEY_NOT_FOUND_MESSAGE);
|
|
42
50
|
const outputDir = flags.output || './wiki';
|
|
51
|
+
const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
|
|
52
|
+
const { wikiRepository } = createBacklogRepositories({
|
|
53
|
+
apiKey,
|
|
54
|
+
domain,
|
|
55
|
+
onRateLimitExceeded: (waitSeconds) => this.log(`レート制限の上限に達しました。${waitSeconds}秒待機します...`),
|
|
56
|
+
});
|
|
43
57
|
// 出力ディレクトリの作成
|
|
44
|
-
await
|
|
58
|
+
await ensureDirectory(outputDir);
|
|
45
59
|
// 設定ファイルを保存
|
|
46
60
|
await updateSettings(outputDir, {
|
|
47
61
|
apiKey,
|
|
48
62
|
domain,
|
|
63
|
+
downloadAttachments,
|
|
49
64
|
folderType: FolderType.WIKI,
|
|
50
65
|
outputDir,
|
|
51
66
|
projectIdOrKey,
|
|
52
67
|
});
|
|
53
68
|
// Wikiの取得と保存
|
|
54
|
-
await
|
|
55
|
-
apiKey,
|
|
69
|
+
await exportWikis({ logger, wikiRepository }, {
|
|
56
70
|
domain,
|
|
71
|
+
downloadAttachments,
|
|
57
72
|
outputDir,
|
|
58
73
|
projectIdOrKey,
|
|
59
74
|
});
|
|
@@ -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
|
+
onRateLimitExceeded?: (waitSeconds: number) => 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,25 @@
|
|
|
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
|
+
downloadAttachments?: boolean;
|
|
18
|
+
issueKeyFileName?: boolean;
|
|
19
|
+
issueKeyFolder?: boolean;
|
|
20
|
+
maxCount: number;
|
|
21
|
+
outputRoot: string;
|
|
22
|
+
projectIdOrKey: string;
|
|
23
|
+
targets: ExportTarget[];
|
|
24
|
+
}
|
|
25
|
+
export declare function exportAll(deps: ExportAllDeps, options: ExportAllOptions): Promise<void>;
|