backlog-exporter 0.0.6

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.
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Hello extends Command {
3
+ static args: {
4
+ person: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ from: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,19 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ export default class Hello extends Command {
3
+ static args = {
4
+ person: Args.string({ description: 'Person to say hello to', required: true }),
5
+ };
6
+ static description = 'Say hello';
7
+ static examples = [
8
+ `<%= config.bin %> <%= command.id %> friend --from oclif
9
+ hello friend from oclif! (./src/commands/hello/index.ts)
10
+ `,
11
+ ];
12
+ static flags = {
13
+ from: Flags.string({ char: 'f', description: 'Who is saying hello', required: true }),
14
+ };
15
+ async run() {
16
+ const { args, flags } = await this.parse(Hello);
17
+ this.log(`hello ${args.person} from ${flags.from}! (./src/commands/hello/index.ts)`);
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class World extends Command {
3
+ static args: {};
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {};
7
+ run(): Promise<void>;
8
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class World extends Command {
3
+ static args = {};
4
+ static description = 'Say hello world';
5
+ static examples = [
6
+ `<%= config.bin %> <%= command.id %>
7
+ hello world! (./src/commands/hello/world.ts)
8
+ `,
9
+ ];
10
+ static flags = {};
11
+ async run() {
12
+ this.log('hello world! (./src/commands/hello/world.ts)');
13
+ }
14
+ }
@@ -0,0 +1,17 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class DownloadIssue extends Command {
3
+ static args: {
4
+ url: 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
+ count: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
+ domain: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
+ statusId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,53 @@
1
+ import { Args, 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
+ // .envファイルを読み込む
7
+ dotenv.config();
8
+ export default class DownloadIssue extends Command {
9
+ static args = {
10
+ url: Args.string({ description: 'URL to download from', required: false }),
11
+ };
12
+ static description = 'Backlogから課題をダウンロードする';
13
+ static examples = [
14
+ `<%= config.bin %> <%= command.id %> --domain cm1.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./issue-data
15
+ BacklogからAPIキーを使用して課題をダウンロードする
16
+ `,
17
+ ];
18
+ static flags = {
19
+ apiKey: Flags.string({
20
+ description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)',
21
+ required: false,
22
+ }),
23
+ count: Flags.integer({ char: 'c', default: 100, description: '一度に取得する課題数', required: false }),
24
+ domain: Flags.string({ description: 'Backlog domain (e.g. example.backlog.jp)', required: true }),
25
+ output: Flags.string({
26
+ char: 'o',
27
+ default: './backlog-issues',
28
+ description: '出力ディレクトリパス',
29
+ required: false,
30
+ }),
31
+ projectIdOrKey: Flags.string({ description: 'Backlog project ID or key', required: true }),
32
+ statusId: Flags.string({ description: 'Filter issues by status ID', required: false }),
33
+ };
34
+ async run() {
35
+ const { flags } = await this.parse(DownloadIssue);
36
+ const { count, domain, output: outputDir, projectIdOrKey, statusId } = flags;
37
+ const apiKey = getApiKey(this, flags.apiKey);
38
+ this.log(`Backlogから ${domain} のプロジェクト ${projectIdOrKey} の課題を取得しています...`);
39
+ try {
40
+ // プロジェクトキーからプロジェクトIDを取得
41
+ const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey);
42
+ this.log(`プロジェクトID: ${projectId} を使用します`);
43
+ // 出力ディレクトリの作成
44
+ await createOutputDirectory(outputDir);
45
+ // 課題のダウンロード
46
+ await downloadIssues(this, domain, projectId, apiKey, outputDir, count, statusId);
47
+ this.log('ダウンロードが完了しました!');
48
+ }
49
+ catch (error) {
50
+ this.error(`ダウンロードに失敗しました: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class DownloadWiki extends Command {
3
+ static args: {
4
+ url: 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, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,42 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import * as dotenv from 'dotenv';
3
+ import { downloadWikis } from '../../utils/backlog-api.js';
4
+ import { createOutputDirectory, getApiKey } from '../../utils/common.js';
5
+ // .envファイルを読み込む
6
+ dotenv.config();
7
+ export default class DownloadWiki extends Command {
8
+ static args = {
9
+ url: Args.string({ description: 'URL to download from', required: false }),
10
+ };
11
+ static description = 'BacklogからWikiコンテンツをダウンロードする';
12
+ static examples = [
13
+ `<%= config.bin %> <%= command.id %> --domain cm1.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./wiki-data
14
+ BacklogからAPIキーを使用してWikiコンテンツをダウンロードする
15
+ `,
16
+ ];
17
+ static flags = {
18
+ apiKey: Flags.string({
19
+ description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)',
20
+ required: false,
21
+ }),
22
+ domain: Flags.string({ description: 'Backlog domain (e.g. example.backlog.jp)', required: true }),
23
+ output: Flags.string({ char: 'o', default: './backlog-wiki', description: '出力ディレクトリパス', required: false }),
24
+ projectIdOrKey: Flags.string({ description: 'Backlog project ID or key', required: true }),
25
+ };
26
+ async run() {
27
+ const { flags } = await this.parse(DownloadWiki);
28
+ const { domain, output: outputDir, projectIdOrKey } = flags;
29
+ const apiKey = getApiKey(this, flags.apiKey);
30
+ this.log(`Backlogから ${domain} のプロジェクト ${projectIdOrKey} のWikiを取得しています...`);
31
+ try {
32
+ // 出力ディレクトリの作成
33
+ await createOutputDirectory(outputDir);
34
+ // Wikiのダウンロード
35
+ await downloadWikis(this, domain, projectIdOrKey, apiKey, outputDir);
36
+ this.log('ダウンロードが完了しました!');
37
+ }
38
+ catch (error) {
39
+ this.error(`ダウンロードに失敗しました: ${error instanceof Error ? error.message : String(error)}`);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
@@ -0,0 +1,21 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * Backlogから課題をダウンロードする
4
+ * @param command コマンドインスタンス
5
+ * @param domain Backlogドメイン
6
+ * @param projectId プロジェクトID
7
+ * @param apiKey APIキー
8
+ * @param outputDir 出力ディレクトリ
9
+ * @param count 一度に取得する課題数
10
+ * @param statusId ステータスID(オプション)
11
+ */
12
+ export declare function downloadIssues(command: Command, domain: string, projectId: number, apiKey: string, outputDir: string, count: number, statusId?: string): Promise<void>;
13
+ /**
14
+ * BacklogからWikiをダウンロードする
15
+ * @param command コマンドインスタンス
16
+ * @param domain Backlogドメイン
17
+ * @param projectIdOrKey プロジェクトIDまたはキー
18
+ * @param apiKey APIキー
19
+ * @param outputDir 出力ディレクトリ
20
+ */
21
+ export declare function downloadWikis(command: Command, domain: string, projectIdOrKey: string, apiKey: string, outputDir: string): Promise<void>;
@@ -0,0 +1,141 @@
1
+ import ky from 'ky';
2
+ import * as fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { sanitizeFileName, sanitizeWikiFileName } from './common.js';
5
+ /**
6
+ * Backlogから課題をダウンロードする
7
+ * @param command コマンドインスタンス
8
+ * @param domain Backlogドメイン
9
+ * @param projectId プロジェクトID
10
+ * @param apiKey APIキー
11
+ * @param outputDir 出力ディレクトリ
12
+ * @param count 一度に取得する課題数
13
+ * @param statusId ステータスID(オプション)
14
+ */
15
+ export async function downloadIssues(command, domain, projectId, apiKey, outputDir, count, statusId) {
16
+ const baseUrl = `https://${domain}/api/v2`;
17
+ command.log('課題の取得を開始します...');
18
+ // APIパラメータの構築
19
+ const params = new URLSearchParams({
20
+ apiKey,
21
+ count: count.toString(),
22
+ 'projectId[]': projectId.toString(),
23
+ });
24
+ // ステータスIDが指定されている場合は追加
25
+ if (statusId) {
26
+ params.append('statusId[]', statusId);
27
+ }
28
+ const issues = await ky.get(`${baseUrl}/issues?${params.toString()}`).json();
29
+ command.log(`${issues.length}件の課題が見つかりました。`);
30
+ // 各課題の詳細情報を取得して保存
31
+ command.log('課題を保存しています...');
32
+ // Promise.allを使用して並列処理
33
+ const issuePromises = issues.map(async (issue) => {
34
+ try {
35
+ // 課題の詳細情報をJSONファイルとして保存
36
+ const issueFileName = `${sanitizeFileName(issue.summary)}.md`;
37
+ const issueFilePath = path.join(outputDir, issueFileName);
38
+ // BacklogのIssueへのリンクを作成
39
+ const backlogIssueUrl = `https://${domain}/view/${issue.issueKey}`;
40
+ // コメント一覧を取得
41
+ command.log(`課題 ${issue.issueKey} のコメントを取得しています...`);
42
+ const commentsParams = new URLSearchParams({
43
+ apiKey,
44
+ count: '100', // 最大100件のコメントを取得
45
+ order: 'asc', // 古い順に取得
46
+ });
47
+ const comments = await ky.get(`${baseUrl}/issues/${issue.issueKey}/comments?${commentsParams.toString()}`).json();
48
+ // コメントセクションを作成
49
+ let commentsSection = '';
50
+ if (comments.length > 0) {
51
+ commentsSection = '\n\n## コメント\n';
52
+ let commentIndex = 1;
53
+ for (const comment of comments) {
54
+ const commentDate = new Date(comment.created).toLocaleString('ja-JP');
55
+ commentsSection += `\n### コメント ${commentIndex}\n- **投稿者**: ${comment.createdUser.name}\n- **日時**: ${commentDate}\n\n${comment.content || '(内容なし)'}\n\n---\n`;
56
+ commentIndex++;
57
+ }
58
+ // 最後の区切り線を削除
59
+ commentsSection = commentsSection.slice(0, -5);
60
+ }
61
+ // Markdownファイルに書き込む
62
+ const assigneeName = issue.assignee ? issue.assignee.name : '未割り当て';
63
+ const markdownContent = `# ${issue.summary}
64
+
65
+ ## 基本情報
66
+ - 課題キー: ${issue.issueKey}
67
+ - ステータス: ${issue.status.name}
68
+ - 優先度: ${issue.priority.name}
69
+ - 担当者: ${assigneeName}
70
+ - 作成日時: ${new Date(issue.created).toLocaleString('ja-JP')}
71
+ - 更新日時: ${new Date(issue.updated).toLocaleString('ja-JP')}
72
+ - [Backlog Issue Link](${backlogIssueUrl})
73
+
74
+ ## 詳細
75
+ ${issue.description || '詳細情報なし'}${commentsSection}`;
76
+ await fs.writeFile(issueFilePath, markdownContent);
77
+ command.log(`課題 "${issue.issueKey}: ${issue.summary}" を ${issueFilePath} に保存しました`);
78
+ }
79
+ catch (error) {
80
+ command.warn(`課題 ${issue.issueKey} の保存に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
81
+ }
82
+ });
83
+ await Promise.all(issuePromises);
84
+ command.log('課題のダウンロードが完了しました!');
85
+ }
86
+ /**
87
+ * BacklogからWikiをダウンロードする
88
+ * @param command コマンドインスタンス
89
+ * @param domain Backlogドメイン
90
+ * @param projectIdOrKey プロジェクトIDまたはキー
91
+ * @param apiKey APIキー
92
+ * @param outputDir 出力ディレクトリ
93
+ */
94
+ export async function downloadWikis(command, domain, projectIdOrKey, apiKey, outputDir) {
95
+ const baseUrl = `https://${domain}/api/v2`;
96
+ command.log('Wikiの取得を開始します...');
97
+ // Wiki一覧の取得
98
+ command.log('Wiki一覧を取得しています...');
99
+ const wikis = await ky
100
+ .get(`${baseUrl}/wikis?apiKey=${apiKey}&projectIdOrKey=${projectIdOrKey}`)
101
+ .json();
102
+ command.log(`${wikis.length}件のWikiが見つかりました。`);
103
+ // 各Wikiの詳細情報を取得
104
+ command.log('Wiki詳細を取得しています...');
105
+ // Promise.allを使用して並列処理
106
+ const wikiPromises = wikis.map(async (wiki) => {
107
+ const wikiId = wiki.id;
108
+ command.log(`Wiki: ${wiki.name} (ID: ${wikiId}) を取得しています`);
109
+ try {
110
+ const wikiDetail = await ky
111
+ .get(`${baseUrl}/wikis/${wikiId}?projectIdOrKey=${projectIdOrKey}&apiKey=${apiKey}`)
112
+ .json();
113
+ // Wikiの名前をファイルパスとして使用
114
+ const wikiName = wiki.name;
115
+ // ファイル名のサニタイズ
116
+ const sanitizedName = sanitizeWikiFileName(wikiName);
117
+ // ファイル名の拡張子を追加
118
+ const wikiFileName = `${sanitizedName}.md`;
119
+ // ディレクトリ構造を作成(必要な場合)
120
+ const dirPath = path.dirname(wikiFileName);
121
+ if (dirPath !== '.') {
122
+ await fs.mkdir(path.join(outputDir, dirPath), { recursive: true });
123
+ }
124
+ // Markdownファイルを保存
125
+ const wikiFilePath = path.join(outputDir, wikiFileName);
126
+ // JSONからcontentフィールドを取得
127
+ const content = wikiDetail.content || '';
128
+ // BacklogのWikiへのリンクを作成
129
+ const backlogWikiUrl = `https://${domain}/alias/wiki/${wikiId}`;
130
+ // Markdownファイルに書き込む(タイトルとBacklogリンクを追加)
131
+ const markdownContent = `# ${wiki.name}\n\n[Backlog Wiki Link](${backlogWikiUrl})\n\n${content}`;
132
+ await fs.writeFile(wikiFilePath, markdownContent);
133
+ command.log(`Wiki "${wiki.name}" を ${wikiFilePath} に保存しました`);
134
+ }
135
+ catch (error) {
136
+ command.warn(`Wiki ID ${wikiId} の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
137
+ }
138
+ });
139
+ await Promise.all(wikiPromises);
140
+ command.log('Wikiのダウンロードが完了しました!');
141
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * プロジェクトキーからプロジェクトIDを取得する
3
+ * @param domain Backlogドメイン (例: example.backlog.jp)
4
+ * @param projectKey プロジェクトキー
5
+ * @param apiKey BacklogのAPIキー
6
+ * @returns プロジェクトID
7
+ */
8
+ export declare function getProjectIdFromKey(domain: string, projectKey: string, apiKey: string): Promise<number>;
9
+ /**
10
+ * プロジェクトIDまたはキーを検証し、必要に応じてキーからIDを取得する
11
+ * @param domain Backlogドメイン
12
+ * @param projectIdOrKey プロジェクトIDまたはキー
13
+ * @param apiKey BacklogのAPIキー
14
+ * @returns プロジェクトID
15
+ */
16
+ export declare function validateAndGetProjectId(domain: string, projectIdOrKey: string, apiKey: string): Promise<number>;
@@ -0,0 +1,34 @@
1
+ import ky from 'ky';
2
+ /**
3
+ * プロジェクトキーからプロジェクトIDを取得する
4
+ * @param domain Backlogドメイン (例: example.backlog.jp)
5
+ * @param projectKey プロジェクトキー
6
+ * @param apiKey BacklogのAPIキー
7
+ * @returns プロジェクトID
8
+ */
9
+ export async function getProjectIdFromKey(domain, projectKey, apiKey) {
10
+ try {
11
+ const baseUrl = `https://${domain}/api/v2`;
12
+ const projectUrl = `${baseUrl}/projects/${projectKey}?apiKey=${apiKey}`;
13
+ const projectData = await ky.get(projectUrl).json();
14
+ return projectData.id;
15
+ }
16
+ catch (error) {
17
+ throw new Error(`プロジェクトキー "${projectKey}" からプロジェクトIDの取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
18
+ }
19
+ }
20
+ /**
21
+ * プロジェクトIDまたはキーを検証し、必要に応じてキーからIDを取得する
22
+ * @param domain Backlogドメイン
23
+ * @param projectIdOrKey プロジェクトIDまたはキー
24
+ * @param apiKey BacklogのAPIキー
25
+ * @returns プロジェクトID
26
+ */
27
+ export async function validateAndGetProjectId(domain, projectIdOrKey, apiKey) {
28
+ // 数値の場合はそのままプロジェクトIDとして返す
29
+ if (!Number.isNaN(Number(projectIdOrKey))) {
30
+ return Number(projectIdOrKey);
31
+ }
32
+ // 文字列の場合はプロジェクトキーとして扱い、IDを取得する
33
+ return getProjectIdFromKey(domain, projectIdOrKey, apiKey);
34
+ }
@@ -0,0 +1,25 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * APIキーを取得する(優先順位: コマンドライン引数 > 環境変数)
4
+ * @param command コマンドインスタンス
5
+ * @param providedApiKey コマンドライン引数から提供されたAPIキー
6
+ * @returns APIキー
7
+ */
8
+ export declare function getApiKey(command: Command, providedApiKey?: string): string;
9
+ /**
10
+ * ファイル名に使用できない文字を置換する関数
11
+ * @param name 元のファイル名
12
+ * @returns サニタイズされたファイル名
13
+ */
14
+ export declare function sanitizeFileName(name: string): string;
15
+ /**
16
+ * Wikiファイル名のサニタイズ(スラッシュはディレクトリ区切りとして使用するため残す)
17
+ * @param name 元のWiki名
18
+ * @returns サニタイズされたWikiファイル名
19
+ */
20
+ export declare function sanitizeWikiFileName(name: string): string;
21
+ /**
22
+ * 出力ディレクトリを作成する
23
+ * @param outputDir 出力ディレクトリパス
24
+ */
25
+ export declare function createOutputDirectory(outputDir: string): Promise<void>;
@@ -0,0 +1,54 @@
1
+ import * as fs from 'node:fs/promises';
2
+ /**
3
+ * APIキーを取得する(優先順位: コマンドライン引数 > 環境変数)
4
+ * @param command コマンドインスタンス
5
+ * @param providedApiKey コマンドライン引数から提供されたAPIキー
6
+ * @returns APIキー
7
+ */
8
+ export function getApiKey(command, providedApiKey) {
9
+ // コマンドライン引数からのAPIキー
10
+ if (providedApiKey) {
11
+ return providedApiKey;
12
+ }
13
+ // 環境変数からのAPIキー
14
+ const envApiKey = process.env.BACKLOG_API_KEY;
15
+ if (envApiKey) {
16
+ command.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します');
17
+ return envApiKey;
18
+ }
19
+ // APIキーが見つからない場合はエラー
20
+ command.error('APIキーが見つかりません。--apiKey フラグまたは BACKLOG_API_KEY 環境変数で提供してください');
21
+ return ''; // この行は実行されないが、TypeScriptのエラーを回避するために必要
22
+ }
23
+ /**
24
+ * ファイル名に使用できない文字を置換する関数
25
+ * @param name 元のファイル名
26
+ * @returns サニタイズされたファイル名
27
+ */
28
+ export function sanitizeFileName(name) {
29
+ return name
30
+ .replaceAll(/[\\/:*?"<>|]/g, '_') // Windowsで使用できない文字を置換
31
+ .replaceAll(/\s+/g, '_') // スペースをアンダースコアに置換
32
+ .replaceAll('.', '_') // ドットを置換
33
+ .slice(0, 200); // 長すぎるファイル名を防ぐために200文字に制限
34
+ }
35
+ /**
36
+ * Wikiファイル名のサニタイズ(スラッシュはディレクトリ区切りとして使用するため残す)
37
+ * @param name 元のWiki名
38
+ * @returns サニタイズされたWikiファイル名
39
+ */
40
+ export function sanitizeWikiFileName(name) {
41
+ const invalidChars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
42
+ let sanitizedName = name;
43
+ for (const char of invalidChars) {
44
+ sanitizedName = sanitizedName.replaceAll(char, '_');
45
+ }
46
+ return sanitizedName;
47
+ }
48
+ /**
49
+ * 出力ディレクトリを作成する
50
+ * @param outputDir 出力ディレクトリパス
51
+ */
52
+ export async function createOutputDirectory(outputDir) {
53
+ await fs.mkdir(outputDir, { recursive: true });
54
+ }