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.
Files changed (121) hide show
  1. package/README.md +109 -1
  2. package/bin/dev.js +6 -2
  3. package/dist/commands/all/index.d.ts +3 -0
  4. package/dist/commands/all/index.js +69 -127
  5. package/dist/commands/document/index.d.ts +1 -0
  6. package/dist/commands/document/index.js +26 -12
  7. package/dist/commands/issue/index.d.ts +1 -0
  8. package/dist/commands/issue/index.js +29 -12
  9. package/dist/commands/prune/index.d.ts +16 -0
  10. package/dist/commands/prune/index.js +86 -0
  11. package/dist/commands/update/index.d.ts +4 -7
  12. package/dist/commands/update/index.js +33 -260
  13. package/dist/commands/wiki/index.d.ts +1 -0
  14. package/dist/commands/wiki/index.js +25 -10
  15. package/dist/composition/backlog-repositories.d.ts +16 -0
  16. package/dist/composition/backlog-repositories.js +15 -0
  17. package/dist/modules/all/domain/content-selection.d.ts +4 -0
  18. package/dist/modules/all/domain/content-selection.js +16 -0
  19. package/dist/modules/all/use-case/export-all.d.ts +25 -0
  20. package/dist/modules/all/use-case/export-all.js +84 -0
  21. package/dist/modules/document/domain/document-markdown.d.ts +2 -0
  22. package/dist/modules/document/domain/document-markdown.js +40 -0
  23. package/dist/modules/document/domain/document-path.d.ts +23 -0
  24. package/dist/modules/document/domain/document-path.js +57 -0
  25. package/dist/modules/document/domain/document-repository.d.ts +7 -0
  26. package/dist/modules/document/domain/document-repository.js +1 -0
  27. package/dist/modules/document/domain/document-save-plan.d.ts +9 -0
  28. package/dist/modules/document/domain/document-save-plan.js +15 -0
  29. package/dist/modules/document/domain/document.d.ts +50 -0
  30. package/dist/modules/document/domain/document.js +1 -0
  31. package/dist/modules/document/repository/backlog-document-repository.d.ts +3 -0
  32. package/dist/modules/document/repository/backlog-document-repository.js +35 -0
  33. package/dist/modules/document/use-case/export-documents.d.ts +17 -0
  34. package/dist/modules/document/use-case/export-documents.js +107 -0
  35. package/dist/modules/issue/domain/issue-filter.d.ts +2 -0
  36. package/dist/modules/issue/domain/issue-filter.js +7 -0
  37. package/dist/modules/issue/domain/issue-markdown.d.ts +5 -0
  38. package/dist/modules/issue/domain/issue-markdown.js +127 -0
  39. package/dist/modules/issue/domain/issue-path.d.ts +34 -0
  40. package/dist/modules/issue/domain/issue-path.js +42 -0
  41. package/dist/modules/issue/domain/issue-repository.d.ts +12 -0
  42. package/dist/modules/issue/domain/issue-repository.js +1 -0
  43. package/dist/modules/issue/domain/issue.d.ts +53 -0
  44. package/dist/modules/issue/domain/issue.js +1 -0
  45. package/dist/modules/issue/repository/backlog-issue-repository.d.ts +3 -0
  46. package/dist/modules/issue/repository/backlog-issue-repository.js +41 -0
  47. package/dist/modules/issue/use-case/export-issues.d.ts +19 -0
  48. package/dist/modules/issue/use-case/export-issues.js +113 -0
  49. package/dist/modules/project/domain/project-repository.d.ts +3 -0
  50. package/dist/modules/project/domain/project-repository.js +1 -0
  51. package/dist/modules/project/repository/backlog-project-repository.d.ts +3 -0
  52. package/dist/modules/project/repository/backlog-project-repository.js +17 -0
  53. package/dist/modules/prune/domain/expected-paths.d.ts +4 -0
  54. package/dist/modules/prune/domain/expected-paths.js +1 -0
  55. package/dist/modules/prune/domain/prune-target.d.ts +3 -0
  56. package/dist/modules/prune/domain/prune-target.js +21 -0
  57. package/dist/modules/prune/repository/prune-walker.d.ts +7 -0
  58. package/dist/modules/prune/repository/prune-walker.js +44 -0
  59. package/dist/modules/prune/use-case/prune-directories.d.ts +28 -0
  60. package/dist/modules/prune/use-case/prune-directories.js +74 -0
  61. package/dist/modules/prune/use-case/prune-exports.d.ts +27 -0
  62. package/dist/modules/prune/use-case/prune-exports.js +74 -0
  63. package/dist/modules/settings/domain/settings.d.ts +16 -0
  64. package/dist/modules/settings/domain/settings.js +6 -0
  65. package/dist/modules/settings/repository/settings-store.d.ts +6 -0
  66. package/dist/modules/settings/repository/settings-store.js +56 -0
  67. package/dist/modules/update/domain/update-plan.d.ts +38 -0
  68. package/dist/modules/update/domain/update-plan.js +70 -0
  69. package/dist/modules/update/use-case/update-exports.d.ts +21 -0
  70. package/dist/modules/update/use-case/update-exports.js +139 -0
  71. package/dist/modules/wiki/domain/wiki-filter.d.ts +9 -0
  72. package/dist/modules/wiki/domain/wiki-filter.js +11 -0
  73. package/dist/modules/wiki/domain/wiki-markdown.d.ts +6 -0
  74. package/dist/modules/wiki/domain/wiki-markdown.js +19 -0
  75. package/dist/modules/wiki/domain/wiki-path.d.ts +7 -0
  76. package/dist/modules/wiki/domain/wiki-path.js +37 -0
  77. package/dist/modules/wiki/domain/wiki-repository.d.ts +6 -0
  78. package/dist/modules/wiki/domain/wiki-repository.js +1 -0
  79. package/dist/modules/wiki/domain/wiki.d.ts +16 -0
  80. package/dist/modules/wiki/domain/wiki.js +1 -0
  81. package/dist/modules/wiki/repository/backlog-wiki-repository.d.ts +3 -0
  82. package/dist/modules/wiki/repository/backlog-wiki-repository.js +13 -0
  83. package/dist/modules/wiki/use-case/export-wikis.d.ts +15 -0
  84. package/dist/modules/wiki/use-case/export-wikis.js +71 -0
  85. package/dist/shared/attachment.d.ts +8 -0
  86. package/dist/shared/attachment.js +37 -0
  87. package/dist/shared/backlog/http-client.d.ts +20 -0
  88. package/dist/shared/backlog/http-client.js +93 -0
  89. package/dist/shared/backlog/sleep.d.ts +1 -0
  90. package/dist/shared/backlog/sleep.js +3 -0
  91. package/dist/shared/backlog-url.d.ts +1 -0
  92. package/dist/shared/backlog-url.js +5 -0
  93. package/dist/shared/config/env.d.ts +3 -0
  94. package/dist/shared/config/env.js +21 -0
  95. package/dist/shared/console/progress.d.ts +1 -0
  96. package/dist/shared/console/progress.js +11 -0
  97. package/dist/shared/console/prompt.d.ts +3 -0
  98. package/dist/shared/console/prompt.js +19 -0
  99. package/dist/shared/file-name.d.ts +3 -0
  100. package/dist/shared/file-name.js +26 -0
  101. package/dist/shared/markdown/body-marker.d.ts +3 -0
  102. package/dist/shared/markdown/body-marker.js +5 -0
  103. package/dist/shared/ports.d.ts +4 -0
  104. package/dist/shared/ports.js +1 -0
  105. package/dist/shared/storage/markdown-store.d.ts +7 -0
  106. package/dist/shared/storage/markdown-store.js +32 -0
  107. package/dist/shared/storage/update-log.d.ts +1 -0
  108. package/dist/{utils/log.js → shared/storage/update-log.js} +1 -5
  109. package/oclif.manifest.json +160 -27
  110. package/package.json +15 -19
  111. package/dist/utils/backlog-api.d.ts +0 -74
  112. package/dist/utils/backlog-api.js +0 -453
  113. package/dist/utils/backlog.d.ts +0 -16
  114. package/dist/utils/backlog.js +0 -34
  115. package/dist/utils/common.d.ts +0 -25
  116. package/dist/utils/common.js +0 -54
  117. package/dist/utils/log.d.ts +0 -6
  118. package/dist/utils/settings.d.ts +0 -45
  119. package/dist/utils/settings.js +0 -63
  120. package/dist/utils/sleep.d.ts +0 -28
  121. 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.3 linux-x64 node-v25.8.1
52
+ backlog-exporter/1.1.0 linux-x64 node-v26.5.0
47
53
  $ backlog-exporter --help [COMMAND]
48
54
  USAGE
49
55
  $ backlog-exporter COMMAND
@@ -153,8 +159,30 @@ $ backlog-exporter issue --domain example.backlog.jp --projectIdOrKey PROJECT_KE
153
159
  $ backlog-exporter issue --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFileName --issueKeyFolder
154
160
  ```
155
161
 
162
+ **課題の添付ファイルもダウンロードする**
163
+
164
+ ```sh
165
+ $ backlog-exporter issue --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --downloadAttachments
166
+ ```
167
+
156
168
  エクスポートされた課題は、指定したディレクトリ内に Markdown ファイルとして保存されます。ファイル名は課題のキーに基づいて自動的に生成されます。
157
169
 
170
+ ## 添付ファイルのダウンロード
171
+
172
+ `--downloadAttachments`(短縮形: `-d`)フラグを指定すると、課題・Wiki・ドキュメントの添付ファイルもダウンロードされます(`all` / `issue` / `wiki` / `document` / `update` コマンドで使用可能)。
173
+
174
+ - 保存先:
175
+ - 課題: デフォルトは `{年}/attachments/{課題キー}/`、`--issueKeyFolder` 指定時は課題フォルダ直下の `attachments/`
176
+ - Wiki: Markdownと同じディレクトリの `attachments/{Wiki名}/`(階層Wikiは末尾のページ名)
177
+ - ドキュメント: Markdownと同じディレクトリの `attachments/{ドキュメント名}/`
178
+ - ファイル名は同名の衝突を避けるため `{添付ID}_{ファイル名}` になります
179
+ - Markdown の `## 添付ファイル` セクションにローカルファイルへの相対リンクが記載されます(フラグ未指定時はファイル名とサイズのみ記載)
180
+ - 課題の本文・コメント内の添付画像のインライン記法(`![image][ファイル名]` / `#image(ファイル名)`)は、ダウンロード済みファイルへの画像リンクに変換され、Markdownビューアでそのまま表示できます
181
+ - Wiki・ドキュメントの本文はBacklogの原文のまま維持されます(添付参照記法の書き換えは行いません)。添付ファイルへは `## 添付ファイル` セクションのリンクからアクセスできます
182
+ - ダウンロード済みのファイルは再ダウンロードされないため、`update` コマンドでの差分更新でも効率的に動作します
183
+ - 設定は `backlog-settings.json` に保存され、以降の `update` コマンドで自動的に引き継がれます(`update` コマンド自体でフラグを指定した場合はその実行のみ有効です)
184
+ - Backlog側で削除された添付ファイルは、誤削除防止のため `prune` コマンドでも削除されず残ります
185
+
158
186
  ## カスタム属性の対応
159
187
 
160
188
  課題のエクスポートでは、Backlogのカスタム属性も含めて出力されます:
@@ -227,12 +255,14 @@ $ backlog-exporter document --domain example.backlog.jp --projectIdOrKey PROJECT
227
255
 
228
256
  - Backlogのドキュメントツリーの階層構造がそのままローカルディレクトリ構造として再現されます
229
257
  - フォルダはディレクトリとして作成され、ドキュメントはMarkdownファイルとして保存されます
258
+ - 子を持つ親ドキュメント自身が本文を持つ場合は、フォルダ内の `00_index.md` として保存されます(本文が空の親はファイルを作成しません)
230
259
 
231
260
  ### 出力例
232
261
 
233
262
  ```
234
263
  documents/
235
264
  ├── プロジェクト概要/
265
+ │ ├── 00_index.md ← 親ドキュメント「プロジェクト概要」自身の本文
236
266
  │ ├── 要件定義書.md
237
267
  │ └── 仕様書.md
238
268
  ├── 設計書/
@@ -248,6 +278,7 @@ documents/
248
278
  ### 特徴
249
279
 
250
280
  - **階層構造の完全再現**: Backlogのフォルダ階層がそのまま保持されます
281
+ - **親ドキュメント本文の保存**: 子を持つ親ドキュメントの本文もフォルダ内の `00_index.md` として保存されます
251
282
  - **重複処理の防止**: 同じドキュメントが複数回処理されることを防ぎます
252
283
  - **ファイル名の自動サニタイズ**: 不正な文字を自動的に除去して安全なファイル名を生成します
253
284
  - **メタデータの保持**: 作成者、更新者、タグ、添付ファイル情報なども含めて保存されます
@@ -286,6 +317,15 @@ $ backlog-exporter all --domain example.backlog.jp --projectIdOrKey PROJECT_KEY
286
317
 
287
318
  ## エクスポート対象の制御
288
319
 
320
+ `--only` / `--exclude` を指定せずに対話端末で実行した場合は、Wiki・ドキュメントのどちらを取得するかを選択できます(BacklogはWikiからドキュメントへの移行を予定しています):
321
+
322
+ ```
323
+ BacklogのWikiとドキュメントのどちらを取得しますか?(BacklogはWikiからドキュメントへの移行を予定しています)
324
+ [1] 両方(デフォルト) [2] Wikiのみ [3] ドキュメントのみ
325
+ ```
326
+
327
+ 非対話環境(CI・パイプ実行)では従来どおり両方を取得します。
328
+
289
329
  `all`コマンドでは、以下のフラグを使用してエクスポート対象を制御できます:
290
330
 
291
331
  ### --only フラグ
@@ -368,8 +408,51 @@ $ backlog-exporter update --documentsOnly
368
408
  $ backlog-exporter update --apiKey YOUR_API_KEY
369
409
  ```
370
410
 
411
+ **指定した項目(課題・Wiki・ドキュメント)のみを再取得する**
412
+
413
+ ```sh
414
+ # 特定の課題だけを再取得(課題キーまたは課題ID)
415
+ $ backlog-exporter update --issueIdOrKey PROJECT-1,PROJECT-2
416
+
417
+ # 特定のWikiだけを再取得(Wiki ID)
418
+ $ backlog-exporter update --wikiId 12345,12346
419
+
420
+ # 特定のドキュメントだけを再取得(ドキュメントID)
421
+ $ backlog-exporter update --documentId abc123,def456
422
+ ```
423
+
424
+ `update` は通常、設定ファイルの最終更新日時(`lastUpdated`)以降に更新された項目を差分取得します。一方で「特定の項目だけを取り直したい」場合は、上記のID指定フラグを使います。
425
+
426
+ - `--issueIdOrKey`: 課題キー(`PROJECT-1`)または課題ID(数値)。Wiki・ドキュメントは数値IDのみのため `--wikiId` / `--documentId` を使います
427
+ - いずれもカンマ区切りで複数指定できます
428
+ - 指定したフラグに対応する項目のみを再取得し、それ以外の種別の更新は行いません(例: `--wikiId` のみ指定時は課題・ドキュメントを更新しません)
429
+ - 指定した項目以外のローカルファイルには影響しません
430
+
431
+ > **Note**: これらのID指定フラグは全件差分更新ではないため、設定ファイルの最終更新日時(`lastUpdated`)は更新されません。そのため、次回の通常の差分更新に影響を与えません。
432
+
371
433
  更新コマンドは、各ディレクトリの設定ファイルに基づいて、課題・Wiki・ドキュメントを自動的に更新します。設定ファイルが見つかったディレクトリでは、そのディレクトリ内のファイルが直接更新されます(サブフォルダは作成されません)。
372
434
 
435
+ **Backlog上で削除・移動された課題・ドキュメント・Wikiのローカルファイルを削除する(prune)**
436
+
437
+ ```sh
438
+ # カレントディレクトリ配下の設定ファイルを探索して実行
439
+ $ backlog-exporter prune
440
+
441
+ # 対象ディレクトリを指定
442
+ $ backlog-exporter prune ./backlog-documents
443
+
444
+ # 確認プロンプトをスキップ
445
+ $ backlog-exporter prune --force
446
+ ```
447
+
448
+ `update`(および各取得コマンド)は増分更新(追加・上書き)のため、Backlog上で削除された課題・ドキュメント・Wikiや別の場所へ移動されたものは、ローカルにファイルが残り続けます。`prune` コマンドは、Backlogに存在しないローカルの `.md` ファイルと、空になったディレクトリを削除してBacklogと同じ状態に揃えます。
449
+
450
+ - 対象は**課題・ドキュメント・Wikiフォルダ**です(設定ファイルの `folderType` で判定します)
451
+ - ファイルを削除する破壊的な操作のため、`update` とは独立したコマンドとして分離しています(実行時に確認プロンプトを表示。`--force` でスキップ可能)
452
+ - 削除対象は `.md` ファイルのみ。`backlog-settings.json`・`backlog-update.log`・`.md` 以外のファイルには触れません。ただし、対象フォルダ内にユーザーが独自に置いた `.md` ファイルや空のディレクトリは、Backlog上に存在しないものとして削除されるため注意してください
453
+ - ファイル名は保存時と同じロジックで比較します(ドキュメントは一覧APIの `title` 基準、課題は設定ファイルの `issueKeyFileName`/`issueKeyFolder` を反映)。サニタイズ差異やツリー名との差異による誤削除は起きません。ドキュメント情報の取得に失敗した場合は、誤削除を防ぐため何も削除せずに中止します
454
+ - 削除したファイルは `backlog-update.log` に記録されます
455
+
373
456
  # コマンド
374
457
 
375
458
  ## `backlog-exporter help [COMMAND]`
@@ -686,6 +769,25 @@ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/
686
769
 
687
770
  # 出力形式
688
771
 
772
+ ## 本文マーカー
773
+
774
+ 課題の詳細・Wikiの本文・ドキュメントの内容といった「本文」部分は、種別を問わず以下のHTMLコメントマーカーで囲まれて出力されます。
775
+
776
+ ```markdown
777
+ <!-- backlog-exporter:body:start -->
778
+ ここが本文(## 見出しなどを含んでもよい)
779
+ <!-- backlog-exporter:body:end -->
780
+ ```
781
+
782
+ - マーカーはMarkdownのレンダリング時には表示されません(HTMLコメント)
783
+ - 本文自体が `##` 見出しなどを含んでいても、開始・終了マーカー間を本文として機械的に抽出・差し替えできます
784
+ - 本文をBacklogへ書き戻す(API反映する)ツールなどが、どこからどこまでが本文かを一意に判定するために使えます
785
+ - 子を持つ親ドキュメントの本文(`00_index.md`)にも同じマーカーが付きます
786
+
787
+ **抽出時の規約**: 本文自体がマーカーと同じ文字列を含む可能性があるため(例: このツールの使い方をBacklog上にメモした場合)、抽出ツールは「ファイル内で**最初に現れる開始マーカー**から**最後に現れる終了マーカー**まで」を本文とみなしてください。
788
+
789
+ 以降の出力例では、この本文マーカーを含めた形を示します。
790
+
689
791
  ## 課題の出力形式
690
792
 
691
793
  課題は以下の形式で Markdown ファイルとして保存されます:
@@ -714,7 +816,9 @@ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/
714
816
 
715
817
  ## 詳細
716
818
 
819
+ <!-- backlog-exporter:body:start -->
717
820
  ここに課題の詳細説明が入ります。
821
+ <!-- backlog-exporter:body:end -->
718
822
 
719
823
  ## コメント
720
824
 
@@ -744,8 +848,10 @@ Wiki は以下の形式で Markdown ファイルとして保存されます:
744
848
 
745
849
  [Backlog Wiki Link](https://example.backlog.jp/alias/wiki/12345)
746
850
 
851
+ <!-- backlog-exporter:body:start -->
747
852
  ここに Wiki の本文内容が入ります。
748
853
  Backlog の書式がそのまま保持されます。
854
+ <!-- backlog-exporter:body:end -->
749
855
  ```
750
856
 
751
857
  ## ドキュメント の出力形式
@@ -765,8 +871,10 @@ Backlog の書式がそのまま保持されます。
765
871
 
766
872
  ## 内容
767
873
 
874
+ <!-- backlog-exporter:body:start -->
768
875
  ここにドキュメントの本文内容が入ります。
769
876
  Backlog の書式がそのまま保持されます。
877
+ <!-- backlog-exporter:body:end -->
770
878
 
771
879
  ## 添付ファイル
772
880
 
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})
@@ -5,6 +5,7 @@ export default class All extends Command {
5
5
  static flags: {
6
6
  apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  domain: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ downloadAttachments: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
9
  exclude: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  issueKeyFileName: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  issueKeyFolder: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -14,4 +15,6 @@ export default class All extends Command {
14
15
  projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
16
  };
16
17
  run(): Promise<void>;
18
+ private determineTargets;
19
+ private promptContentSelection;
17
20
  }
@@ -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 = [
@@ -44,6 +44,11 @@ export default class All extends Command {
44
44
  description: 'Backlog domain (e.g. example.backlog.jp)',
45
45
  required: true,
46
46
  }),
47
+ downloadAttachments: Flags.boolean({
48
+ char: 'd',
49
+ description: '課題の添付ファイルもダウンロードする',
50
+ required: false,
51
+ }),
47
52
  exclude: Flags.string({
48
53
  description: "Exclude the specified types, separated by commas (e.g., 'documents,wiki')",
49
54
  required: false,
@@ -79,132 +84,69 @@ export default class All extends Command {
79
84
  async run() {
80
85
  const { flags } = await this.parse(All);
81
86
  try {
82
- const { domain, exclude, issueKeyFileName, issueKeyFolder, maxCount, only, projectIdOrKey } = flags;
83
- const apiKey = flags.apiKey || getApiKey(this);
87
+ const { domain, downloadAttachments, exclude, issueKeyFileName, issueKeyFolder, maxCount, only, projectIdOrKey } = flags;
88
+ const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
89
+ this.error(API_KEY_NOT_FOUND_MESSAGE);
84
90
  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.');
88
- }
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('ドキュメントの取得が完了しました');
91
+ let targets = this.determineTargets(only, exclude);
92
+ // --only / --exclude 未指定の対話実行では、Wiki・ドキュメントのどちらを取得するか選択できるようにする
93
+ if (!only && !exclude && isInteractiveStdin()) {
94
+ targets = applyContentSelection(targets, await this.promptContentSelection());
202
95
  }
203
- this.log('すべてのデータの取得が完了しました!');
96
+ this.log(`取得対象: ${targets.join(', ')}`);
97
+ const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
98
+ const repositories = createBacklogRepositories({
99
+ apiKey,
100
+ domain,
101
+ onRateLimitExceeded: (waitSeconds) => this.log(`レート制限の上限に達しました。${waitSeconds}秒待機します...`),
102
+ });
103
+ await exportAll({ ...repositories, logger }, {
104
+ apiKey,
105
+ domain,
106
+ downloadAttachments,
107
+ issueKeyFileName,
108
+ issueKeyFolder,
109
+ maxCount,
110
+ outputRoot,
111
+ projectIdOrKey,
112
+ targets,
113
+ });
204
114
  }
205
115
  catch (error) {
206
116
  const errorMessage = error instanceof Error ? error.message : String(error);
207
117
  this.error(`データの取得に失敗しました: ${errorMessage}`);
208
118
  }
209
119
  }
120
+ determineTargets(only, exclude) {
121
+ if (only && exclude) {
122
+ this.error('Cannot use both --only and --exclude flags together. Please use only one.');
123
+ }
124
+ // 指定された値の検証
125
+ const inputTargets = only ? only.split(',') : exclude ? exclude.split(',') : [];
126
+ for (const target of inputTargets) {
127
+ if (!VALID_TARGETS.includes(target)) {
128
+ this.error(`Invalid target '${target}'. Available targets are: ${VALID_TARGETS.join(', ')}`);
129
+ }
130
+ }
131
+ let targets;
132
+ if (only) {
133
+ targets = only.split(',');
134
+ }
135
+ else if (exclude) {
136
+ const excludeTargets = exclude.split(',');
137
+ targets = VALID_TARGETS.filter((target) => !excludeTargets.includes(target));
138
+ }
139
+ else {
140
+ targets = [...VALID_TARGETS];
141
+ }
142
+ if (targets.length === 0) {
143
+ this.error('No targets remaining after exclusion. Please specify valid targets to export.');
144
+ }
145
+ return targets;
146
+ }
147
+ async promptContentSelection() {
148
+ this.log('BacklogのWikiとドキュメントのどちらを取得しますか?(BacklogはWikiからドキュメントへの移行を予定しています)');
149
+ this.log(' [1] 両方(デフォルト) [2] Wikiのみ [3] ドキュメントのみ');
150
+ return parseContentSelection(await readLine());
151
+ }
210
152
  }
@@ -5,6 +5,7 @@ export default class Document 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
  keyword: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -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 = [
@@ -28,6 +29,11 @@ export default class Document extends Command {
28
29
  description: 'Backlog domain (e.g. example.backlog.jp)',
29
30
  required: true,
30
31
  }),
32
+ downloadAttachments: Flags.boolean({
33
+ char: 'd',
34
+ description: '添付ファイルもダウンロードする',
35
+ required: false,
36
+ }),
31
37
  keyword: Flags.string({
32
38
  description: '検索キーワード',
33
39
  required: false,
@@ -45,26 +51,34 @@ export default class Document extends Command {
45
51
  async run() {
46
52
  const { flags } = await this.parse(Document);
47
53
  try {
48
- const { domain, keyword, projectIdOrKey } = flags;
49
- const apiKey = flags.apiKey || getApiKey(this);
54
+ const { domain, downloadAttachments, keyword, projectIdOrKey } = flags;
55
+ const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
56
+ this.error(API_KEY_NOT_FOUND_MESSAGE);
50
57
  const outputDir = flags.output || './backlog-documents';
58
+ const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
59
+ const { documentRepository, projectRepository } = createBacklogRepositories({
60
+ apiKey,
61
+ domain,
62
+ onRateLimitExceeded: (waitSeconds) => this.log(`レート制限の上限に達しました。${waitSeconds}秒待機します...`),
63
+ });
51
64
  // 出力ディレクトリの作成
52
- await createOutputDirectory(outputDir);
65
+ await ensureDirectory(outputDir);
53
66
  // プロジェクトキーからプロジェクトIDを取得
54
- const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey);
67
+ const projectId = await projectRepository.resolveProjectId(projectIdOrKey);
55
68
  this.log(`プロジェクトID: ${projectId} を使用します`);
56
69
  // 設定ファイルを保存
57
70
  await updateSettings(outputDir, {
58
71
  apiKey,
59
72
  domain,
73
+ downloadAttachments,
60
74
  folderType: FolderType.DOCUMENT,
61
75
  outputDir,
62
76
  projectIdOrKey,
63
77
  });
64
78
  // ドキュメントの取得と保存
65
- await downloadDocuments(this, {
66
- apiKey,
79
+ await exportDocuments({ documentRepository, logger }, {
67
80
  domain,
81
+ downloadAttachments,
68
82
  keyword,
69
83
  outputDir,
70
84
  projectId,
@@ -5,6 +5,7 @@ export default class Issue extends Command {
5
5
  static flags: {
6
6
  apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  domain: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ downloadAttachments: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
9
  issueKeyFileName: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
10
  issueKeyFolder: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  maxCount: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
@@ -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 = [
@@ -29,6 +30,9 @@ export default class Issue extends Command {
29
30
  `,
30
31
  `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFileName --issueKeyFolder
31
32
  課題キーでフォルダを作成し、ファイル名も課題キーにする
33
+ `,
34
+ `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --downloadAttachments
35
+ 課題の添付ファイルもダウンロードする
32
36
  `,
33
37
  ];
34
38
  static flags = {
@@ -40,6 +44,11 @@ export default class Issue extends Command {
40
44
  description: 'Backlog domain (e.g. example.backlog.jp)',
41
45
  required: true,
42
46
  }),
47
+ downloadAttachments: Flags.boolean({
48
+ char: 'd',
49
+ description: '課題の添付ファイルもダウンロードする',
50
+ required: false,
51
+ }),
43
52
  issueKeyFileName: Flags.boolean({
44
53
  description: 'ファイル名を課題キーにする',
45
54
  required: false,
@@ -71,18 +80,26 @@ export default class Issue extends Command {
71
80
  async run() {
72
81
  const { flags } = await this.parse(Issue);
73
82
  try {
74
- const { domain, issueKeyFileName, issueKeyFolder, maxCount, projectIdOrKey, statusId } = flags;
75
- const apiKey = flags.apiKey || getApiKey(this);
83
+ const { domain, downloadAttachments, issueKeyFileName, issueKeyFolder, maxCount, projectIdOrKey, statusId } = flags;
84
+ const apiKey = resolveApiKey(flags.apiKey, () => this.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します')) ??
85
+ this.error(API_KEY_NOT_FOUND_MESSAGE);
76
86
  const outputDir = flags.output || './backlog-issues';
87
+ const logger = { log: (message) => this.log(message), warn: (message) => this.warn(message) };
88
+ const { issueRepository, projectRepository } = createBacklogRepositories({
89
+ apiKey,
90
+ domain,
91
+ onRateLimitExceeded: (waitSeconds) => this.log(`レート制限の上限に達しました。${waitSeconds}秒待機します...`),
92
+ });
77
93
  // 出力ディレクトリの作成
78
- await createOutputDirectory(outputDir);
94
+ await ensureDirectory(outputDir);
79
95
  // プロジェクトキーからプロジェクトIDを取得
80
- const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey);
96
+ const projectId = await projectRepository.resolveProjectId(projectIdOrKey);
81
97
  this.log(`プロジェクトID: ${projectId} を使用します`);
82
98
  // 設定ファイルを保存
83
99
  await updateSettings(outputDir, {
84
100
  apiKey,
85
101
  domain,
102
+ downloadAttachments,
86
103
  folderType: FolderType.ISSUE,
87
104
  issueKeyFileName,
88
105
  issueKeyFolder,
@@ -90,10 +107,10 @@ export default class Issue extends Command {
90
107
  projectIdOrKey,
91
108
  });
92
109
  // 課題の取得と保存
93
- await downloadIssues(this, {
94
- apiKey,
110
+ await exportIssues({ issueRepository, logger }, {
95
111
  count: maxCount,
96
112
  domain,
113
+ downloadAttachments,
97
114
  issueKeyFileName,
98
115
  issueKeyFolder,
99
116
  outputDir,
@@ -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
+ }