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.
Files changed (116) hide show
  1. package/README.md +87 -1
  2. package/bin/dev.js +6 -2
  3. package/dist/commands/all/index.d.ts +2 -0
  4. package/dist/commands/all/index.js +62 -126
  5. package/dist/commands/document/index.js +18 -11
  6. package/dist/commands/issue/index.js +18 -11
  7. package/dist/commands/prune/index.d.ts +16 -0
  8. package/dist/commands/prune/index.js +86 -0
  9. package/dist/commands/update/index.d.ts +3 -7
  10. package/dist/commands/update/index.js +28 -260
  11. package/dist/commands/wiki/index.js +17 -9
  12. package/dist/composition/backlog-repositories.d.ts +16 -0
  13. package/dist/composition/backlog-repositories.js +15 -0
  14. package/dist/modules/all/domain/content-selection.d.ts +4 -0
  15. package/dist/modules/all/domain/content-selection.js +16 -0
  16. package/dist/modules/all/use-case/export-all.d.ts +24 -0
  17. package/dist/modules/all/use-case/export-all.js +78 -0
  18. package/dist/modules/document/domain/document-markdown.d.ts +2 -0
  19. package/dist/modules/document/domain/document-markdown.js +37 -0
  20. package/dist/modules/document/domain/document-path.d.ts +15 -0
  21. package/dist/modules/document/domain/document-path.js +48 -0
  22. package/dist/modules/document/domain/document-repository.d.ts +6 -0
  23. package/dist/modules/document/domain/document-repository.js +1 -0
  24. package/dist/modules/document/domain/document-save-plan.d.ts +9 -0
  25. package/dist/modules/document/domain/document-save-plan.js +15 -0
  26. package/dist/modules/document/domain/document.d.ts +50 -0
  27. package/dist/modules/document/domain/document.js +1 -0
  28. package/dist/modules/document/repository/backlog-document-repository.d.ts +3 -0
  29. package/dist/modules/document/repository/backlog-document-repository.js +31 -0
  30. package/dist/modules/document/use-case/export-documents.d.ts +16 -0
  31. package/dist/modules/document/use-case/export-documents.js +82 -0
  32. package/dist/modules/issue/domain/issue-filter.d.ts +2 -0
  33. package/dist/modules/issue/domain/issue-filter.js +7 -0
  34. package/dist/modules/issue/domain/issue-markdown.d.ts +4 -0
  35. package/dist/modules/issue/domain/issue-markdown.js +112 -0
  36. package/dist/modules/issue/domain/issue-path.d.ts +23 -0
  37. package/dist/modules/issue/domain/issue-path.js +30 -0
  38. package/dist/modules/issue/domain/issue-repository.d.ts +11 -0
  39. package/dist/modules/issue/domain/issue-repository.js +1 -0
  40. package/dist/modules/issue/domain/issue.d.ts +47 -0
  41. package/dist/modules/issue/domain/issue.js +1 -0
  42. package/dist/modules/issue/repository/backlog-issue-repository.d.ts +3 -0
  43. package/dist/modules/issue/repository/backlog-issue-repository.js +38 -0
  44. package/dist/modules/issue/use-case/export-issues.d.ts +18 -0
  45. package/dist/modules/issue/use-case/export-issues.js +87 -0
  46. package/dist/modules/project/domain/project-repository.d.ts +3 -0
  47. package/dist/modules/project/domain/project-repository.js +1 -0
  48. package/dist/modules/project/repository/backlog-project-repository.d.ts +3 -0
  49. package/dist/modules/project/repository/backlog-project-repository.js +17 -0
  50. package/dist/modules/prune/domain/expected-paths.d.ts +4 -0
  51. package/dist/modules/prune/domain/expected-paths.js +1 -0
  52. package/dist/modules/prune/domain/prune-target.d.ts +3 -0
  53. package/dist/modules/prune/domain/prune-target.js +21 -0
  54. package/dist/modules/prune/repository/prune-walker.d.ts +7 -0
  55. package/dist/modules/prune/repository/prune-walker.js +39 -0
  56. package/dist/modules/prune/use-case/prune-directories.d.ts +28 -0
  57. package/dist/modules/prune/use-case/prune-directories.js +74 -0
  58. package/dist/modules/prune/use-case/prune-exports.d.ts +27 -0
  59. package/dist/modules/prune/use-case/prune-exports.js +74 -0
  60. package/dist/modules/settings/domain/settings.d.ts +15 -0
  61. package/dist/modules/settings/domain/settings.js +6 -0
  62. package/dist/modules/settings/repository/settings-store.d.ts +6 -0
  63. package/dist/modules/settings/repository/settings-store.js +56 -0
  64. package/dist/modules/update/domain/update-plan.d.ts +36 -0
  65. package/dist/modules/update/domain/update-plan.js +69 -0
  66. package/dist/modules/update/use-case/update-exports.d.ts +21 -0
  67. package/dist/modules/update/use-case/update-exports.js +136 -0
  68. package/dist/modules/wiki/domain/wiki-filter.d.ts +9 -0
  69. package/dist/modules/wiki/domain/wiki-filter.js +11 -0
  70. package/dist/modules/wiki/domain/wiki-markdown.d.ts +1 -0
  71. package/dist/modules/wiki/domain/wiki-markdown.js +4 -0
  72. package/dist/modules/wiki/domain/wiki-path.d.ts +4 -0
  73. package/dist/modules/wiki/domain/wiki-path.js +25 -0
  74. package/dist/modules/wiki/domain/wiki-repository.d.ts +5 -0
  75. package/dist/modules/wiki/domain/wiki-repository.js +1 -0
  76. package/dist/modules/wiki/domain/wiki.d.ts +10 -0
  77. package/dist/modules/wiki/domain/wiki.js +1 -0
  78. package/dist/modules/wiki/repository/backlog-wiki-repository.d.ts +3 -0
  79. package/dist/modules/wiki/repository/backlog-wiki-repository.js +10 -0
  80. package/dist/modules/wiki/use-case/export-wikis.d.ts +14 -0
  81. package/dist/modules/wiki/use-case/export-wikis.js +43 -0
  82. package/dist/shared/backlog/http-client.d.ts +17 -0
  83. package/dist/shared/backlog/http-client.js +62 -0
  84. package/dist/shared/backlog/rate-limiter.d.ts +9 -0
  85. package/dist/shared/backlog/rate-limiter.js +25 -0
  86. package/dist/shared/backlog-url.d.ts +1 -0
  87. package/dist/shared/backlog-url.js +5 -0
  88. package/dist/shared/config/env.d.ts +3 -0
  89. package/dist/shared/config/env.js +21 -0
  90. package/dist/shared/console/progress.d.ts +1 -0
  91. package/dist/shared/console/progress.js +4 -0
  92. package/dist/shared/console/prompt.d.ts +3 -0
  93. package/dist/shared/console/prompt.js +19 -0
  94. package/dist/shared/file-name.d.ts +2 -0
  95. package/dist/shared/file-name.js +15 -0
  96. package/dist/shared/markdown/body-marker.d.ts +3 -0
  97. package/dist/shared/markdown/body-marker.js +5 -0
  98. package/dist/shared/ports.d.ts +4 -0
  99. package/dist/shared/ports.js +1 -0
  100. package/dist/shared/storage/markdown-store.d.ts +5 -0
  101. package/dist/shared/storage/markdown-store.js +21 -0
  102. package/dist/shared/storage/update-log.d.ts +1 -0
  103. package/dist/{utils/log.js → shared/storage/update-log.js} +1 -5
  104. package/oclif.manifest.json +118 -26
  105. package/package.json +15 -19
  106. package/dist/utils/backlog-api.d.ts +0 -74
  107. package/dist/utils/backlog-api.js +0 -448
  108. package/dist/utils/backlog.d.ts +0 -16
  109. package/dist/utils/backlog.js +0 -34
  110. package/dist/utils/common.d.ts +0 -25
  111. package/dist/utils/common.js +0 -54
  112. package/dist/utils/log.d.ts +0 -6
  113. package/dist/utils/settings.d.ts +0 -45
  114. package/dist/utils/settings.js +0 -63
  115. package/dist/utils/sleep.d.ts +0 -28
  116. package/dist/utils/sleep.js +0 -42
package/README.md CHANGED
@@ -16,6 +16,12 @@ Backlog のデータをエクスポートするためのコマンドラインツ
16
16
  * [ドキュメント のエクスポート](#ドキュメント-のエクスポート)
17
17
  * [課題・Wiki・ドキュメント の一括エクスポート](#課題wikiドキュメント-の一括エクスポート)
18
18
  * [データの更新](#データの更新)
19
+ * [特定の課題だけを再取得(課題キーまたは課題ID)](#特定の課題だけを再取得課題キーまたは課題id)
20
+ * [特定のWikiだけを再取得(Wiki ID)](#特定のwikiだけを再取得wiki-id)
21
+ * [特定のドキュメントだけを再取得(ドキュメントID)](#特定のドキュメントだけを再取得ドキュメントid)
22
+ * [カレントディレクトリ配下の設定ファイルを探索して実行](#カレントディレクトリ配下の設定ファイルを探索して実行)
23
+ * [対象ディレクトリを指定](#対象ディレクトリを指定)
24
+ * [確認プロンプトをスキップ](#確認プロンプトをスキップ)
19
25
  * [コマンド](#コマンド)
20
26
  * [出力形式](#出力形式)
21
27
  * [課題のタイトル](#課題のタイトル)
@@ -43,7 +49,7 @@ $ npm install -g backlog-exporter
43
49
  $ backlog-exporter COMMAND
44
50
  running command...
45
51
  $ backlog-exporter (--version)
46
- backlog-exporter/0.7.2 linux-x64 node-v25.2.1
52
+ backlog-exporter/1.0.0 linux-x64 node-v26.4.0
47
53
  $ backlog-exporter --help [COMMAND]
48
54
  USAGE
49
55
  $ backlog-exporter COMMAND
@@ -227,12 +233,14 @@ $ backlog-exporter document --domain example.backlog.jp --projectIdOrKey PROJECT
227
233
 
228
234
  - Backlogのドキュメントツリーの階層構造がそのままローカルディレクトリ構造として再現されます
229
235
  - フォルダはディレクトリとして作成され、ドキュメントはMarkdownファイルとして保存されます
236
+ - 子を持つ親ドキュメント自身が本文を持つ場合は、フォルダ内の `00_index.md` として保存されます(本文が空の親はファイルを作成しません)
230
237
 
231
238
  ### 出力例
232
239
 
233
240
  ```
234
241
  documents/
235
242
  ├── プロジェクト概要/
243
+ │ ├── 00_index.md ← 親ドキュメント「プロジェクト概要」自身の本文
236
244
  │ ├── 要件定義書.md
237
245
  │ └── 仕様書.md
238
246
  ├── 設計書/
@@ -248,6 +256,7 @@ documents/
248
256
  ### 特徴
249
257
 
250
258
  - **階層構造の完全再現**: Backlogのフォルダ階層がそのまま保持されます
259
+ - **親ドキュメント本文の保存**: 子を持つ親ドキュメントの本文もフォルダ内の `00_index.md` として保存されます
251
260
  - **重複処理の防止**: 同じドキュメントが複数回処理されることを防ぎます
252
261
  - **ファイル名の自動サニタイズ**: 不正な文字を自動的に除去して安全なファイル名を生成します
253
262
  - **メタデータの保持**: 作成者、更新者、タグ、添付ファイル情報なども含めて保存されます
@@ -286,6 +295,15 @@ $ backlog-exporter all --domain example.backlog.jp --projectIdOrKey PROJECT_KEY
286
295
 
287
296
  ## エクスポート対象の制御
288
297
 
298
+ `--only` / `--exclude` を指定せずに対話端末で実行した場合は、Wiki・ドキュメントのどちらを取得するかを選択できます(BacklogはWikiからドキュメントへの移行を予定しています):
299
+
300
+ ```
301
+ BacklogのWikiとドキュメントのどちらを取得しますか?(BacklogはWikiからドキュメントへの移行を予定しています)
302
+ [1] 両方(デフォルト) [2] Wikiのみ [3] ドキュメントのみ
303
+ ```
304
+
305
+ 非対話環境(CI・パイプ実行)では従来どおり両方を取得します。
306
+
289
307
  `all`コマンドでは、以下のフラグを使用してエクスポート対象を制御できます:
290
308
 
291
309
  ### --only フラグ
@@ -368,8 +386,51 @@ $ backlog-exporter update --documentsOnly
368
386
  $ backlog-exporter update --apiKey YOUR_API_KEY
369
387
  ```
370
388
 
389
+ **指定した項目(課題・Wiki・ドキュメント)のみを再取得する**
390
+
391
+ ```sh
392
+ # 特定の課題だけを再取得(課題キーまたは課題ID)
393
+ $ backlog-exporter update --issueIdOrKey PROJECT-1,PROJECT-2
394
+
395
+ # 特定のWikiだけを再取得(Wiki ID)
396
+ $ backlog-exporter update --wikiId 12345,12346
397
+
398
+ # 特定のドキュメントだけを再取得(ドキュメントID)
399
+ $ backlog-exporter update --documentId abc123,def456
400
+ ```
401
+
402
+ `update` は通常、設定ファイルの最終更新日時(`lastUpdated`)以降に更新された項目を差分取得します。一方で「特定の項目だけを取り直したい」場合は、上記のID指定フラグを使います。
403
+
404
+ - `--issueIdOrKey`: 課題キー(`PROJECT-1`)または課題ID(数値)。Wiki・ドキュメントは数値IDのみのため `--wikiId` / `--documentId` を使います
405
+ - いずれもカンマ区切りで複数指定できます
406
+ - 指定したフラグに対応する項目のみを再取得し、それ以外の種別の更新は行いません(例: `--wikiId` のみ指定時は課題・ドキュメントを更新しません)
407
+ - 指定した項目以外のローカルファイルには影響しません
408
+
409
+ > **Note**: これらのID指定フラグは全件差分更新ではないため、設定ファイルの最終更新日時(`lastUpdated`)は更新されません。そのため、次回の通常の差分更新に影響を与えません。
410
+
371
411
  更新コマンドは、各ディレクトリの設定ファイルに基づいて、課題・Wiki・ドキュメントを自動的に更新します。設定ファイルが見つかったディレクトリでは、そのディレクトリ内のファイルが直接更新されます(サブフォルダは作成されません)。
372
412
 
413
+ **Backlog上で削除・移動された課題・ドキュメント・Wikiのローカルファイルを削除する(prune)**
414
+
415
+ ```sh
416
+ # カレントディレクトリ配下の設定ファイルを探索して実行
417
+ $ backlog-exporter prune
418
+
419
+ # 対象ディレクトリを指定
420
+ $ backlog-exporter prune ./backlog-documents
421
+
422
+ # 確認プロンプトをスキップ
423
+ $ backlog-exporter prune --force
424
+ ```
425
+
426
+ `update`(および各取得コマンド)は増分更新(追加・上書き)のため、Backlog上で削除された課題・ドキュメント・Wikiや別の場所へ移動されたものは、ローカルにファイルが残り続けます。`prune` コマンドは、Backlogに存在しないローカルの `.md` ファイルと、空になったディレクトリを削除してBacklogと同じ状態に揃えます。
427
+
428
+ - 対象は**課題・ドキュメント・Wikiフォルダ**です(設定ファイルの `folderType` で判定します)
429
+ - ファイルを削除する破壊的な操作のため、`update` とは独立したコマンドとして分離しています(実行時に確認プロンプトを表示。`--force` でスキップ可能)
430
+ - 削除対象は `.md` ファイルのみ。`backlog-settings.json`・`backlog-update.log`・`.md` 以外のファイルには触れません。ただし、対象フォルダ内にユーザーが独自に置いた `.md` ファイルや空のディレクトリは、Backlog上に存在しないものとして削除されるため注意してください
431
+ - ファイル名は保存時と同じロジックで比較します(ドキュメントは一覧APIの `title` 基準、課題は設定ファイルの `issueKeyFileName`/`issueKeyFolder` を反映)。サニタイズ差異やツリー名との差異による誤削除は起きません。ドキュメント情報の取得に失敗した場合は、誤削除を防ぐため何も削除せずに中止します
432
+ - 削除したファイルは `backlog-update.log` に記録されます
433
+
373
434
  # コマンド
374
435
 
375
436
  ## `backlog-exporter help [COMMAND]`
@@ -686,6 +747,25 @@ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/
686
747
 
687
748
  # 出力形式
688
749
 
750
+ ## 本文マーカー
751
+
752
+ 課題の詳細・Wikiの本文・ドキュメントの内容といった「本文」部分は、種別を問わず以下のHTMLコメントマーカーで囲まれて出力されます。
753
+
754
+ ```markdown
755
+ <!-- backlog-exporter:body:start -->
756
+ ここが本文(## 見出しなどを含んでもよい)
757
+ <!-- backlog-exporter:body:end -->
758
+ ```
759
+
760
+ - マーカーはMarkdownのレンダリング時には表示されません(HTMLコメント)
761
+ - 本文自体が `##` 見出しなどを含んでいても、開始・終了マーカー間を本文として機械的に抽出・差し替えできます
762
+ - 本文をBacklogへ書き戻す(API反映する)ツールなどが、どこからどこまでが本文かを一意に判定するために使えます
763
+ - 子を持つ親ドキュメントの本文(`00_index.md`)にも同じマーカーが付きます
764
+
765
+ **抽出時の規約**: 本文自体がマーカーと同じ文字列を含む可能性があるため(例: このツールの使い方をBacklog上にメモした場合)、抽出ツールは「ファイル内で**最初に現れる開始マーカー**から**最後に現れる終了マーカー**まで」を本文とみなしてください。
766
+
767
+ 以降の出力例では、この本文マーカーを含めた形を示します。
768
+
689
769
  ## 課題の出力形式
690
770
 
691
771
  課題は以下の形式で Markdown ファイルとして保存されます:
@@ -714,7 +794,9 @@ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/
714
794
 
715
795
  ## 詳細
716
796
 
797
+ <!-- backlog-exporter:body:start -->
717
798
  ここに課題の詳細説明が入ります。
799
+ <!-- backlog-exporter:body:end -->
718
800
 
719
801
  ## コメント
720
802
 
@@ -744,8 +826,10 @@ Wiki は以下の形式で Markdown ファイルとして保存されます:
744
826
 
745
827
  [Backlog Wiki Link](https://example.backlog.jp/alias/wiki/12345)
746
828
 
829
+ <!-- backlog-exporter:body:start -->
747
830
  ここに Wiki の本文内容が入ります。
748
831
  Backlog の書式がそのまま保持されます。
832
+ <!-- backlog-exporter:body:end -->
749
833
  ```
750
834
 
751
835
  ## ドキュメント の出力形式
@@ -765,8 +849,10 @@ Backlog の書式がそのまま保持されます。
765
849
 
766
850
  ## 内容
767
851
 
852
+ <!-- backlog-exporter:body:start -->
768
853
  ここにドキュメントの本文内容が入ります。
769
854
  Backlog の書式がそのまま保持されます。
855
+ <!-- backlog-exporter:body:end -->
770
856
 
771
857
  ## 添付ファイル
772
858
 
package/bin/dev.js CHANGED
@@ -1,5 +1,9 @@
1
- #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
1
+ #!/usr/bin/env -S node --import tsx --disable-warning=ExperimentalWarning
2
2
 
3
3
  import {execute} from '@oclif/core'
4
4
 
5
- await execute({development: true, dir: import.meta.url})
5
+ // execute({development: true}) はエラー時のスタックトレース表示も有効化してしまうため、
6
+ // srcのTS解決に必要な NODE_ENV のみを設定する
7
+ process.env.NODE_ENV = 'development'
8
+
9
+ await execute({dir: import.meta.url})
@@ -14,4 +14,6 @@ export default class All extends Command {
14
14
  projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
15
  };
16
16
  run(): Promise<void>;
17
+ private determineTargets;
18
+ private promptContentSelection;
17
19
  }
@@ -1,12 +1,12 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import * as dotenv from 'dotenv';
3
- import path from 'node:path';
4
- import { downloadDocuments, downloadIssues, downloadWikis } from '../../utils/backlog-api.js';
5
- import { validateAndGetProjectId } from '../../utils/backlog.js';
6
- import { createOutputDirectory, getApiKey } from '../../utils/common.js';
7
- import { FolderType, updateSettings } from '../../utils/settings.js';
2
+ import { createBacklogRepositories } from '../../composition/backlog-repositories.js';
3
+ import { applyContentSelection, parseContentSelection } from '../../modules/all/domain/content-selection.js';
4
+ import { exportAll } from '../../modules/all/use-case/export-all.js';
5
+ import { API_KEY_NOT_FOUND_MESSAGE, loadDotenv, resolveApiKey } from '../../shared/config/env.js';
6
+ import { isInteractiveStdin, readLine } from '../../shared/console/prompt.js';
8
7
  // .envファイルを読み込む
9
- dotenv.config();
8
+ loadDotenv();
9
+ const VALID_TARGETS = ['issues', 'wiki', 'documents'];
10
10
  export default class All extends Command {
11
11
  static description = 'Backlogから課題・Wiki・ドキュメントを取得してMarkdownファイルとして保存する';
12
12
  static examples = [
@@ -80,131 +80,67 @@ export default class All extends Command {
80
80
  const { flags } = await this.parse(All);
81
81
  try {
82
82
  const { domain, exclude, issueKeyFileName, issueKeyFolder, maxCount, only, projectIdOrKey } = flags;
83
- const apiKey = flags.apiKey || getApiKey(this);
83
+ const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
84
+ this.error(API_KEY_NOT_FOUND_MESSAGE);
84
85
  const outputRoot = flags.output || './backlog-data';
85
- // Check for conflicting flags
86
- if (only && exclude) {
87
- this.error('Cannot use both --only and --exclude flags together. Please use only one.');
86
+ let targets = this.determineTargets(only, exclude);
87
+ // --only / --exclude 未指定の対話実行では、Wiki・ドキュメントのどちらを取得するか選択できるようにする
88
+ if (!only && !exclude && isInteractiveStdin()) {
89
+ targets = applyContentSelection(targets, await this.promptContentSelection());
88
90
  }
89
- // Determine targets based on flags
90
- let targets;
91
- if (only) {
92
- targets = only.split(',');
93
- }
94
- else if (exclude) {
95
- const excludeTargets = exclude.split(',');
96
- const allTargets = ['issues', 'wiki', 'documents'];
97
- targets = allTargets.filter((target) => !excludeTargets.includes(target));
98
- }
99
- else {
100
- targets = ['issues', 'wiki', 'documents'];
101
- }
102
- // Validate targets
103
- const validTargets = ['issues', 'wiki', 'documents'];
104
- const inputTargets = only ? only.split(',') : exclude ? exclude.split(',') : [];
105
- for (const target of inputTargets) {
106
- if (!validTargets.includes(target)) {
107
- this.error(`Invalid target '${target}'. Available targets are: ${validTargets.join(', ')}`);
108
- }
109
- }
110
- // Check if any targets remain after exclusion
111
- if (targets.length === 0) {
112
- this.error('No targets remaining after exclusion. Please specify valid targets to export.');
113
- }
114
- // 出力ディレクトリの作成
115
- await createOutputDirectory(outputRoot);
116
- // プロジェクトキーからプロジェクトIDを取得
117
- const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey);
118
- this.log(`プロジェクトID: ${projectId} を使用します`);
119
- if (targets.includes('issues')) {
120
- // 課題の出力ディレクトリ
121
- const issueOutput = path.join(outputRoot, 'issues');
122
- await createOutputDirectory(issueOutput);
123
- // 課題フォルダに設定ファイルを保存
124
- await updateSettings(issueOutput, {
125
- apiKey,
126
- domain,
127
- folderType: FolderType.ISSUE,
128
- issueKeyFileName,
129
- issueKeyFolder,
130
- outputDir: issueOutput,
131
- projectIdOrKey,
132
- });
133
- // 課題の取得と保存
134
- this.log('課題の取得を開始します...');
135
- await downloadIssues(this, {
136
- apiKey,
137
- count: maxCount,
138
- domain,
139
- issueKeyFileName,
140
- issueKeyFolder,
141
- outputDir: issueOutput,
142
- projectId,
143
- });
144
- // 課題フォルダの最終更新日時を更新
145
- await updateSettings(issueOutput, {
146
- lastUpdated: new Date().toISOString(),
147
- });
148
- this.log('課題の取得が完了しました');
149
- }
150
- if (targets.includes('wiki')) {
151
- // Wikiの出力ディレクトリ
152
- const wikiOutput = path.join(outputRoot, 'wiki');
153
- await createOutputDirectory(wikiOutput);
154
- // Wikiフォルダに設定ファイルを保存
155
- await updateSettings(wikiOutput, {
156
- apiKey,
157
- domain,
158
- folderType: FolderType.WIKI,
159
- outputDir: wikiOutput,
160
- projectIdOrKey,
161
- });
162
- // Wikiの取得と保存
163
- this.log('Wikiの取得を開始します...');
164
- await downloadWikis(this, {
165
- apiKey,
166
- domain,
167
- outputDir: wikiOutput,
168
- projectIdOrKey,
169
- });
170
- // Wikiフォルダの最終更新日時を更新
171
- await updateSettings(wikiOutput, {
172
- lastUpdated: new Date().toISOString(),
173
- });
174
- this.log('Wikiの取得が完了しました');
175
- }
176
- if (targets.includes('documents')) {
177
- // ドキュメントの出力ディレクトリ
178
- const documentOutput = path.join(outputRoot, 'documents');
179
- await createOutputDirectory(documentOutput);
180
- // ドキュメントフォルダに設定ファイルを保存
181
- await updateSettings(documentOutput, {
182
- apiKey,
183
- domain,
184
- folderType: FolderType.DOCUMENT,
185
- outputDir: documentOutput,
186
- projectIdOrKey,
187
- });
188
- // ドキュメントの取得と保存
189
- this.log('ドキュメントの取得を開始します...');
190
- await downloadDocuments(this, {
191
- apiKey,
192
- domain,
193
- outputDir: documentOutput,
194
- projectId,
195
- projectIdOrKey,
196
- });
197
- // ドキュメントフォルダの最終更新日時を更新
198
- await updateSettings(documentOutput, {
199
- lastUpdated: new Date().toISOString(),
200
- });
201
- this.log('ドキュメントの取得が完了しました');
202
- }
203
- this.log('すべてのデータの取得が完了しました!');
91
+ this.log(`取得対象: ${targets.join(', ')}`);
92
+ const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
93
+ const repositories = createBacklogRepositories({
94
+ apiKey,
95
+ domain,
96
+ onRateLimitWait: () => this.log('レート制限を回避するため15秒間待機します...'),
97
+ });
98
+ await exportAll({ ...repositories, logger }, {
99
+ apiKey,
100
+ domain,
101
+ issueKeyFileName,
102
+ issueKeyFolder,
103
+ maxCount,
104
+ outputRoot,
105
+ projectIdOrKey,
106
+ targets,
107
+ });
204
108
  }
205
109
  catch (error) {
206
110
  const errorMessage = error instanceof Error ? error.message : String(error);
207
111
  this.error(`データの取得に失敗しました: ${errorMessage}`);
208
112
  }
209
113
  }
114
+ determineTargets(only, exclude) {
115
+ if (only && exclude) {
116
+ this.error('Cannot use both --only and --exclude flags together. Please use only one.');
117
+ }
118
+ // 指定された値の検証
119
+ const inputTargets = only ? only.split(',') : exclude ? exclude.split(',') : [];
120
+ for (const target of inputTargets) {
121
+ if (!VALID_TARGETS.includes(target)) {
122
+ this.error(`Invalid target '${target}'. Available targets are: ${VALID_TARGETS.join(', ')}`);
123
+ }
124
+ }
125
+ let targets;
126
+ if (only) {
127
+ targets = only.split(',');
128
+ }
129
+ else if (exclude) {
130
+ const excludeTargets = exclude.split(',');
131
+ targets = VALID_TARGETS.filter((target) => !excludeTargets.includes(target));
132
+ }
133
+ else {
134
+ targets = [...VALID_TARGETS];
135
+ }
136
+ if (targets.length === 0) {
137
+ this.error('No targets remaining after exclusion. Please specify valid targets to export.');
138
+ }
139
+ return targets;
140
+ }
141
+ async promptContentSelection() {
142
+ this.log('BacklogのWikiとドキュメントのどちらを取得しますか?(BacklogはWikiからドキュメントへの移行を予定しています)');
143
+ this.log(' [1] 両方(デフォルト) [2] Wikiのみ [3] ドキュメントのみ');
144
+ return parseContentSelection(await readLine());
145
+ }
210
146
  }
@@ -1,11 +1,12 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import * as dotenv from 'dotenv';
3
- import { downloadDocuments } from '../../utils/backlog-api.js';
4
- import { validateAndGetProjectId } from '../../utils/backlog.js';
5
- import { createOutputDirectory, getApiKey } from '../../utils/common.js';
6
- import { FolderType, updateSettings } from '../../utils/settings.js';
2
+ import { createBacklogRepositories } from '../../composition/backlog-repositories.js';
3
+ import { exportDocuments } from '../../modules/document/use-case/export-documents.js';
4
+ import { FolderType } from '../../modules/settings/domain/settings.js';
5
+ import { updateSettings } from '../../modules/settings/repository/settings-store.js';
6
+ import { API_KEY_NOT_FOUND_MESSAGE, loadDotenv, resolveApiKey } from '../../shared/config/env.js';
7
+ import { ensureDirectory } from '../../shared/storage/markdown-store.js';
7
8
  // .envファイルを読み込む
8
- dotenv.config();
9
+ loadDotenv();
9
10
  export default class Document extends Command {
10
11
  static description = 'Backlogからドキュメントを取得してMarkdownファイルとして保存する';
11
12
  static examples = [
@@ -46,12 +47,19 @@ export default class Document extends Command {
46
47
  const { flags } = await this.parse(Document);
47
48
  try {
48
49
  const { domain, keyword, projectIdOrKey } = flags;
49
- const apiKey = flags.apiKey || getApiKey(this);
50
+ const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
51
+ this.error(API_KEY_NOT_FOUND_MESSAGE);
50
52
  const outputDir = flags.output || './backlog-documents';
53
+ const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
54
+ const { documentRepository, projectRepository } = createBacklogRepositories({
55
+ apiKey,
56
+ domain,
57
+ onRateLimitWait: () => this.log('レート制限を回避するため15秒間待機します...'),
58
+ });
51
59
  // 出力ディレクトリの作成
52
- await createOutputDirectory(outputDir);
60
+ await ensureDirectory(outputDir);
53
61
  // プロジェクトキーからプロジェクトIDを取得
54
- const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey);
62
+ const projectId = await projectRepository.resolveProjectId(projectIdOrKey);
55
63
  this.log(`プロジェクトID: ${projectId} を使用します`);
56
64
  // 設定ファイルを保存
57
65
  await updateSettings(outputDir, {
@@ -62,8 +70,7 @@ export default class Document extends Command {
62
70
  projectIdOrKey,
63
71
  });
64
72
  // ドキュメントの取得と保存
65
- await downloadDocuments(this, {
66
- apiKey,
73
+ await exportDocuments({ documentRepository, logger }, {
67
74
  domain,
68
75
  keyword,
69
76
  outputDir,
@@ -1,11 +1,12 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import * as dotenv from 'dotenv';
3
- import { downloadIssues } from '../../utils/backlog-api.js';
4
- import { validateAndGetProjectId } from '../../utils/backlog.js';
5
- import { createOutputDirectory, getApiKey } from '../../utils/common.js';
6
- import { FolderType, updateSettings } from '../../utils/settings.js';
2
+ import { createBacklogRepositories } from '../../composition/backlog-repositories.js';
3
+ import { exportIssues } from '../../modules/issue/use-case/export-issues.js';
4
+ import { FolderType } from '../../modules/settings/domain/settings.js';
5
+ import { updateSettings } from '../../modules/settings/repository/settings-store.js';
6
+ import { API_KEY_NOT_FOUND_MESSAGE, loadDotenv, resolveApiKey } from '../../shared/config/env.js';
7
+ import { ensureDirectory } from '../../shared/storage/markdown-store.js';
7
8
  // .envファイルを読み込む
8
- dotenv.config();
9
+ loadDotenv();
9
10
  export default class Issue extends Command {
10
11
  static description = 'Backlogから課題を取得してMarkdownファイルとして保存する';
11
12
  static examples = [
@@ -72,12 +73,19 @@ export default class Issue extends Command {
72
73
  const { flags } = await this.parse(Issue);
73
74
  try {
74
75
  const { domain, issueKeyFileName, issueKeyFolder, maxCount, projectIdOrKey, statusId } = flags;
75
- const apiKey = flags.apiKey || getApiKey(this);
76
+ const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
77
+ this.error(API_KEY_NOT_FOUND_MESSAGE);
76
78
  const outputDir = flags.output || './backlog-issues';
79
+ const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
80
+ const { issueRepository, projectRepository } = createBacklogRepositories({
81
+ apiKey,
82
+ domain,
83
+ onRateLimitWait: () => this.log('レート制限を回避するため15秒間待機します...'),
84
+ });
77
85
  // 出力ディレクトリの作成
78
- await createOutputDirectory(outputDir);
86
+ await ensureDirectory(outputDir);
79
87
  // プロジェクトキーからプロジェクトIDを取得
80
- const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey);
88
+ const projectId = await projectRepository.resolveProjectId(projectIdOrKey);
81
89
  this.log(`プロジェクトID: ${projectId} を使用します`);
82
90
  // 設定ファイルを保存
83
91
  await updateSettings(outputDir, {
@@ -90,8 +98,7 @@ export default class Issue extends Command {
90
98
  projectIdOrKey,
91
99
  });
92
100
  // 課題の取得と保存
93
- await downloadIssues(this, {
94
- apiKey,
101
+ await exportIssues({ issueRepository, logger }, {
95
102
  count: maxCount,
96
103
  domain,
97
104
  issueKeyFileName,
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Prune extends Command {
3
+ static args: {
4
+ directory: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<void>;
15
+ private confirmPrune;
16
+ }
@@ -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,17 @@ 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>;
12
13
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ issueIdOrKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
15
  issueKeyFileName: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
16
  issueKeyFolder: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
17
  issuesOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
18
  projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ wikiId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
20
  wikisOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
21
  };
19
22
  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
23
  }