backlog-exporter 0.0.6 → 0.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.
@@ -0,0 +1,23 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Update 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
+ issuesOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ wikisOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<void>;
17
+ private confirmUpdate;
18
+ private determineUpdateTargets;
19
+ private findAndUpdateSettings;
20
+ private updateDirectory;
21
+ private updateIssues;
22
+ private updateWikis;
23
+ }
@@ -0,0 +1,237 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import * as dotenv from 'dotenv';
3
+ import { access, readdir } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { downloadIssues, downloadWikis } from '../../utils/backlog-api.js';
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';
9
+ // .envファイルを読み込む
10
+ dotenv.config();
11
+ export default class Update extends Command {
12
+ static args = {
13
+ directory: Args.string({
14
+ description: '更新対象のディレクトリ(設定ファイルが保存されている場所)',
15
+ required: false,
16
+ }),
17
+ };
18
+ static description = 'Backlogから最新データを取得して更新する';
19
+ static examples = [
20
+ `<%= config.bin %> <%= command.id %>
21
+ カレントディレクトリの設定を使用して更新する
22
+ `,
23
+ `<%= config.bin %> <%= command.id %> --force
24
+ 確認プロンプトをスキップする
25
+ `,
26
+ `<%= config.bin %> <%= command.id %> --apiKey YOUR_API_KEY --domain example.backlog.jp --projectIdOrKey PROJECT_KEY
27
+ 指定したパラメータで更新する(設定ファイルが存在する場合は上書きされます)
28
+ `,
29
+ `<%= config.bin %> <%= command.id %> ./my-project
30
+ 指定したディレクトリの設定を使用して更新する
31
+ `,
32
+ ];
33
+ static flags = {
34
+ apiKey: Flags.string({
35
+ description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)',
36
+ required: false,
37
+ }),
38
+ domain: Flags.string({
39
+ description: 'Backlog domain (e.g. example.backlog.jp)',
40
+ required: false,
41
+ }),
42
+ force: Flags.boolean({
43
+ char: 'f',
44
+ description: '確認プロンプトをスキップする',
45
+ required: false,
46
+ }),
47
+ issuesOnly: Flags.boolean({
48
+ description: '課題のみを更新する',
49
+ required: false,
50
+ }),
51
+ projectIdOrKey: Flags.string({
52
+ description: 'Backlog project ID or key',
53
+ required: false,
54
+ }),
55
+ wikisOnly: Flags.boolean({
56
+ description: 'Wikiのみを更新する',
57
+ required: false,
58
+ }),
59
+ };
60
+ async run() {
61
+ const { args, flags } = await this.parse(Update);
62
+ // 更新対象のディレクトリを決定(指定がなければカレントディレクトリ)
63
+ const targetDir = args.directory || process.cwd();
64
+ try {
65
+ // 設定ファイルを探索して更新を実行
66
+ await this.findAndUpdateSettings(targetDir, flags);
67
+ }
68
+ catch (error) {
69
+ const errorMessage = error instanceof Error ? error.message : String(error);
70
+ this.error(`更新に失敗しました: ${errorMessage}`);
71
+ }
72
+ }
73
+ // 確認プロンプトの表示
74
+ async confirmUpdate(options) {
75
+ if (options.force)
76
+ return true;
77
+ this.log(`以下の設定で更新を実行します:`);
78
+ this.log(`- ディレクトリ: ${options.targetDir}`);
79
+ this.log(`- ドメイン: ${options.domain}`);
80
+ this.log(`- プロジェクト: ${options.projectIdOrKey}`);
81
+ if (options.folderType) {
82
+ this.log(`- フォルダタイプ: ${options.folderType}`);
83
+ }
84
+ this.log(`- 更新対象: ${options.updateIssues ? '課題' : ''}${options.updateIssues && options.updateWikis ? 'と' : ''}${options.updateWikis ? 'Wiki' : ''}`);
85
+ // 確認プロンプトを表示
86
+ this.log('更新を実行しますか? (y/n)');
87
+ process.stdin.resume();
88
+ process.stdin.setEncoding('utf8');
89
+ const response = await new Promise((resolve) => {
90
+ process.stdin.once('data', (data) => {
91
+ const input = data.toString().trim().toLowerCase();
92
+ resolve(input === 'y' || input === 'yes');
93
+ process.stdin.pause();
94
+ });
95
+ });
96
+ if (!response) {
97
+ this.log('更新をキャンセルしました');
98
+ }
99
+ return response;
100
+ }
101
+ // 更新対象の決定
102
+ determineUpdateTargets(folderType, issuesOnly, wikisOnly) {
103
+ let updateIssues = !wikisOnly;
104
+ let updateWikis = !issuesOnly;
105
+ // フォルダタイプがISSUEの場合は課題のみ更新
106
+ if (folderType === FolderType.ISSUE) {
107
+ updateIssues = true;
108
+ updateWikis = false;
109
+ }
110
+ // フォルダタイプがWIKIの場合はWikiのみ更新
111
+ else if (folderType === FolderType.WIKI) {
112
+ updateIssues = false;
113
+ updateWikis = true;
114
+ }
115
+ return { updateIssues, updateWikis };
116
+ }
117
+ // 設定ファイルを探索して更新を実行
118
+ async findAndUpdateSettings(targetDir, flags) {
119
+ // 現在のディレクトリに設定ファイルがあるか確認
120
+ const settingsPath = getSettingsFilePath(targetDir);
121
+ let hasSettings = false;
122
+ try {
123
+ await access(settingsPath);
124
+ hasSettings = true;
125
+ }
126
+ catch {
127
+ // 設定ファイルが存在しない場合は何もしない
128
+ }
129
+ if (hasSettings) {
130
+ // 設定ファイルが存在する場合は更新を実行
131
+ await this.updateDirectory(targetDir, flags);
132
+ }
133
+ // サブディレクトリを探索
134
+ try {
135
+ const entries = await readdir(targetDir, { withFileTypes: true });
136
+ for (const entry of entries) {
137
+ if (entry.isDirectory()) {
138
+ const subDir = join(targetDir, entry.name);
139
+ // eslint-disable-next-line no-await-in-loop
140
+ await this.findAndUpdateSettings(subDir, flags);
141
+ }
142
+ }
143
+ }
144
+ catch {
145
+ this.warn(`ディレクトリの読み取りに失敗しました: ${targetDir}`);
146
+ }
147
+ }
148
+ // 指定されたディレクトリの更新を実行
149
+ async updateDirectory(targetDir, flags) {
150
+ // 設定ファイルを読み込む
151
+ const settings = await loadSettings(targetDir);
152
+ // コマンドライン引数と設定ファイルを組み合わせて使用する値を決定
153
+ const apiKey = flags.apiKey || settings.apiKey || getApiKey(this);
154
+ const domain = flags.domain || settings.domain;
155
+ const projectIdOrKey = flags.projectIdOrKey || settings.projectIdOrKey;
156
+ const { folderType } = settings;
157
+ const { force, issuesOnly, wikisOnly } = flags;
158
+ // 必須パラメータの検証
159
+ if (!domain) {
160
+ this.warn(`${targetDir}: ドメインが指定されていません。スキップします。`);
161
+ return;
162
+ }
163
+ if (!projectIdOrKey) {
164
+ this.warn(`${targetDir}: プロジェクトIDまたはキーが指定されていません。スキップします。`);
165
+ return;
166
+ }
167
+ // 更新対象の決定
168
+ const { updateIssues, updateWikis } = this.determineUpdateTargets(folderType, issuesOnly, wikisOnly);
169
+ // 更新前の確認
170
+ const confirmed = await this.confirmUpdate({
171
+ domain,
172
+ folderType,
173
+ force: force || false,
174
+ projectIdOrKey,
175
+ targetDir,
176
+ updateIssues,
177
+ updateWikis,
178
+ });
179
+ if (!confirmed)
180
+ return;
181
+ // 出力ディレクトリの作成
182
+ await createOutputDirectory(targetDir);
183
+ // プロジェクトキーからプロジェクトIDを取得
184
+ const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey);
185
+ this.log(`プロジェクトID: ${projectId} を使用します`);
186
+ // 課題の更新
187
+ if (updateIssues) {
188
+ await this.updateIssues({
189
+ apiKey,
190
+ domain,
191
+ projectId,
192
+ projectIdOrKey,
193
+ targetDir,
194
+ });
195
+ }
196
+ // Wikiの更新
197
+ if (updateWikis) {
198
+ await this.updateWikis({
199
+ apiKey,
200
+ domain,
201
+ projectIdOrKey,
202
+ targetDir,
203
+ });
204
+ }
205
+ this.log(`${targetDir} の更新が完了しました!`);
206
+ }
207
+ // 課題の更新
208
+ async updateIssues(options) {
209
+ this.log('課題の更新を開始します...');
210
+ await downloadIssues(this, options.domain, options.projectId, options.apiKey, options.targetDir, 100);
211
+ // 設定ファイルを更新
212
+ await updateSettings(options.targetDir, {
213
+ apiKey: options.apiKey,
214
+ domain: options.domain,
215
+ folderType: FolderType.ISSUE,
216
+ lastUpdated: new Date().toISOString(),
217
+ outputDir: options.targetDir,
218
+ projectIdOrKey: options.projectIdOrKey,
219
+ });
220
+ this.log('課題の更新が完了しました');
221
+ }
222
+ // Wikiの更新
223
+ async updateWikis(options) {
224
+ this.log('Wikiの更新を開始します...');
225
+ await downloadWikis(this, options.domain, options.projectIdOrKey, options.apiKey, options.targetDir);
226
+ // 設定ファイルを更新
227
+ await updateSettings(options.targetDir, {
228
+ apiKey: options.apiKey,
229
+ domain: options.domain,
230
+ folderType: FolderType.WIKI,
231
+ lastUpdated: new Date().toISOString(),
232
+ outputDir: options.targetDir,
233
+ projectIdOrKey: options.projectIdOrKey,
234
+ });
235
+ this.log('Wikiの更新が完了しました');
236
+ }
237
+ }
@@ -1,14 +1,11 @@
1
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
- };
2
+ export default class Wiki extends Command {
6
3
  static description: string;
7
4
  static examples: string[];
8
5
  static flags: {
9
6
  apiKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
7
  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>;
8
+ output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
9
  projectIdOrKey: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
10
  };
14
11
  run(): Promise<void>;
@@ -1,17 +1,18 @@
1
- import { Args, Command, Flags } from '@oclif/core';
1
+ import { Command, Flags } from '@oclif/core';
2
2
  import * as dotenv from 'dotenv';
3
3
  import { downloadWikis } from '../../utils/backlog-api.js';
4
4
  import { createOutputDirectory, getApiKey } from '../../utils/common.js';
5
+ import { FolderType, updateSettings } from '../../utils/settings.js';
5
6
  // .envファイルを読み込む
6
7
  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コンテンツをダウンロードする';
8
+ export default class Wiki extends Command {
9
+ static description = 'Backlogから Wiki を取得してMarkdownファイルとして保存する';
12
10
  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コンテンツをダウンロードする
11
+ `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY
12
+ Wikiをダウンロードする
13
+ `,
14
+ `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-project
15
+ 指定したディレクトリにWikiを保存する
15
16
  `,
16
17
  ];
17
18
  static flags = {
@@ -19,24 +20,44 @@ BacklogからAPIキーを使用してWikiコンテンツをダウンロードす
19
20
  description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)',
20
21
  required: false,
21
22
  }),
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 }),
23
+ domain: Flags.string({
24
+ description: 'Backlog domain (e.g. example.backlog.jp)',
25
+ required: true,
26
+ }),
27
+ output: Flags.string({
28
+ char: 'o',
29
+ description: '出力ディレクトリパス',
30
+ required: false,
31
+ }),
32
+ projectIdOrKey: Flags.string({
33
+ description: 'Backlog project ID or key',
34
+ required: true,
35
+ }),
25
36
  };
26
37
  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を取得しています...`);
38
+ const { flags } = await this.parse(Wiki);
31
39
  try {
40
+ const { domain, projectIdOrKey } = flags;
41
+ const apiKey = flags.apiKey || getApiKey(this);
42
+ const outputDir = flags.output || './wiki';
32
43
  // 出力ディレクトリの作成
33
44
  await createOutputDirectory(outputDir);
34
- // Wikiのダウンロード
45
+ // Wikiの取得と保存
35
46
  await downloadWikis(this, domain, projectIdOrKey, apiKey, outputDir);
36
- this.log('ダウンロードが完了しました!');
47
+ // 設定ファイルを保存
48
+ await updateSettings(outputDir, {
49
+ apiKey,
50
+ domain,
51
+ folderType: FolderType.WIKI,
52
+ lastUpdated: new Date().toISOString(),
53
+ outputDir,
54
+ projectIdOrKey,
55
+ });
56
+ this.log('Wikiの取得が完了しました!');
37
57
  }
38
58
  catch (error) {
39
- this.error(`ダウンロードに失敗しました: ${error instanceof Error ? error.message : String(error)}`);
59
+ const errorMessage = error instanceof Error ? error.message : String(error);
60
+ this.error(`Wikiの取得に失敗しました: ${errorMessage}`);
40
61
  }
41
62
  }
42
63
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * フォルダタイプの定義
3
+ */
4
+ export declare enum FolderType {
5
+ ISSUE = "issue",
6
+ WIKI = "wiki"
7
+ }
8
+ /**
9
+ * 設定ファイルの型定義
10
+ */
11
+ export interface Settings {
12
+ apiKey?: string;
13
+ domain?: string;
14
+ folderType?: FolderType;
15
+ lastUpdated?: string;
16
+ outputDir?: string;
17
+ projectIdOrKey?: string;
18
+ }
19
+ /**
20
+ * 設定ファイルのパスを取得する
21
+ * @param directory 設定ファイルを保存するディレクトリ
22
+ * @returns 設定ファイルのパス
23
+ */
24
+ export declare function getSettingsFilePath(directory: string): string;
25
+ /**
26
+ * 設定ファイルを読み込む
27
+ * @param directory 設定ファイルが保存されているディレクトリ
28
+ * @returns 設定情報
29
+ */
30
+ export declare function loadSettings(directory: string): Promise<Settings>;
31
+ /**
32
+ * 設定ファイルを保存する
33
+ * @param directory 設定ファイルを保存するディレクトリ
34
+ * @param settings 保存する設定情報
35
+ */
36
+ export declare function saveSettings(directory: string, settings: Settings): Promise<void>;
37
+ /**
38
+ * 設定ファイルを更新する
39
+ * @param directory 設定ファイルを保存するディレクトリ
40
+ * @param newSettings 更新する設定情報
41
+ */
42
+ export declare function updateSettings(directory: string, newSettings: Partial<Settings>): Promise<Settings>;
@@ -0,0 +1,60 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * フォルダタイプの定義
5
+ */
6
+ export var FolderType;
7
+ (function (FolderType) {
8
+ FolderType["ISSUE"] = "issue";
9
+ FolderType["WIKI"] = "wiki";
10
+ })(FolderType || (FolderType = {}));
11
+ /**
12
+ * 設定ファイルのパスを取得する
13
+ * @param directory 設定ファイルを保存するディレクトリ
14
+ * @returns 設定ファイルのパス
15
+ */
16
+ export function getSettingsFilePath(directory) {
17
+ return join(directory, 'backlog-settings.json');
18
+ }
19
+ /**
20
+ * 設定ファイルを読み込む
21
+ * @param directory 設定ファイルが保存されているディレクトリ
22
+ * @returns 設定情報
23
+ */
24
+ export async function loadSettings(directory) {
25
+ const settingsPath = getSettingsFilePath(directory);
26
+ try {
27
+ const data = await fs.readFile(settingsPath, 'utf8');
28
+ return JSON.parse(data);
29
+ }
30
+ catch {
31
+ // ファイルが存在しない場合や読み込みエラーの場合は空のオブジェクトを返す
32
+ return {};
33
+ }
34
+ }
35
+ /**
36
+ * 設定ファイルを保存する
37
+ * @param directory 設定ファイルを保存するディレクトリ
38
+ * @param settings 保存する設定情報
39
+ */
40
+ export async function saveSettings(directory, settings) {
41
+ const settingsPath = getSettingsFilePath(directory);
42
+ // ディレクトリが存在しない場合は作成
43
+ await fs.mkdir(directory, { recursive: true });
44
+ // 設定ファイルを保存
45
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
46
+ }
47
+ /**
48
+ * 設定ファイルを更新する
49
+ * @param directory 設定ファイルを保存するディレクトリ
50
+ * @param newSettings 更新する設定情報
51
+ */
52
+ export async function updateSettings(directory, newSettings) {
53
+ // 既存の設定を読み込む
54
+ const currentSettings = await loadSettings(directory);
55
+ // 新しい設定で更新
56
+ const updatedSettings = { ...currentSettings, ...newSettings };
57
+ // 更新した設定を保存
58
+ await saveSettings(directory, updatedSettings);
59
+ return updatedSettings;
60
+ }