backlog-exporter 1.0.0 → 1.1.1

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 (70) hide show
  1. package/README.md +143 -636
  2. package/dist/commands/all/index.d.ts +1 -0
  3. package/dist/commands/all/index.js +8 -2
  4. package/dist/commands/document/index.d.ts +1 -0
  5. package/dist/commands/document/index.js +9 -2
  6. package/dist/commands/issue/index.d.ts +1 -0
  7. package/dist/commands/issue/index.js +12 -2
  8. package/dist/commands/update/index.d.ts +1 -0
  9. package/dist/commands/update/index.js +5 -0
  10. package/dist/commands/wiki/index.d.ts +1 -0
  11. package/dist/commands/wiki/index.js +9 -2
  12. package/dist/composition/backlog-repositories.d.ts +1 -1
  13. package/dist/composition/backlog-repositories.js +1 -1
  14. package/dist/modules/all/use-case/export-all.d.ts +1 -0
  15. package/dist/modules/all/use-case/export-all.js +7 -1
  16. package/dist/modules/document/domain/document-markdown.d.ts +1 -1
  17. package/dist/modules/document/domain/document-markdown.js +6 -3
  18. package/dist/modules/document/domain/document-path.d.ts +11 -1
  19. package/dist/modules/document/domain/document-path.js +17 -0
  20. package/dist/modules/document/domain/document-repository.d.ts +1 -0
  21. package/dist/modules/document/domain/document-save-plan.d.ts +4 -3
  22. package/dist/modules/document/domain/document-save-plan.js +11 -5
  23. package/dist/modules/document/domain/document-tree-gap.d.ts +2 -0
  24. package/dist/modules/document/domain/document-tree-gap.js +21 -0
  25. package/dist/modules/document/domain/document.d.ts +4 -0
  26. package/dist/modules/document/repository/backlog-document-repository.js +4 -0
  27. package/dist/modules/document/use-case/export-documents.d.ts +1 -0
  28. package/dist/modules/document/use-case/export-documents.js +72 -9
  29. package/dist/modules/issue/domain/issue-markdown.d.ts +4 -3
  30. package/dist/modules/issue/domain/issue-markdown.js +23 -8
  31. package/dist/modules/issue/domain/issue-path.d.ts +12 -1
  32. package/dist/modules/issue/domain/issue-path.js +12 -0
  33. package/dist/modules/issue/domain/issue-repository.d.ts +1 -0
  34. package/dist/modules/issue/domain/issue.d.ts +6 -0
  35. package/dist/modules/issue/repository/backlog-issue-repository.js +3 -0
  36. package/dist/modules/issue/use-case/export-issues.d.ts +1 -0
  37. package/dist/modules/issue/use-case/export-issues.js +29 -3
  38. package/dist/modules/prune/repository/prune-walker.js +5 -0
  39. package/dist/modules/prune/use-case/prune-directories.d.ts +1 -1
  40. package/dist/modules/prune/use-case/prune-directories.js +1 -1
  41. package/dist/modules/prune/use-case/prune-exports.js +15 -12
  42. package/dist/modules/settings/domain/settings.d.ts +1 -0
  43. package/dist/modules/update/domain/update-plan.d.ts +2 -0
  44. package/dist/modules/update/domain/update-plan.js +1 -0
  45. package/dist/modules/update/use-case/update-exports.d.ts +1 -1
  46. package/dist/modules/update/use-case/update-exports.js +4 -1
  47. package/dist/modules/wiki/domain/wiki-markdown.d.ts +6 -1
  48. package/dist/modules/wiki/domain/wiki-markdown.js +17 -2
  49. package/dist/modules/wiki/domain/wiki-path.d.ts +3 -0
  50. package/dist/modules/wiki/domain/wiki-path.js +12 -0
  51. package/dist/modules/wiki/domain/wiki-repository.d.ts +1 -0
  52. package/dist/modules/wiki/domain/wiki.d.ts +6 -0
  53. package/dist/modules/wiki/repository/backlog-wiki-repository.js +3 -0
  54. package/dist/modules/wiki/use-case/export-wikis.d.ts +1 -0
  55. package/dist/modules/wiki/use-case/export-wikis.js +31 -3
  56. package/dist/shared/attachment.d.ts +8 -0
  57. package/dist/shared/attachment.js +37 -0
  58. package/dist/shared/backlog/http-client.d.ts +5 -2
  59. package/dist/shared/backlog/http-client.js +41 -10
  60. package/dist/shared/backlog/sleep.d.ts +1 -0
  61. package/dist/shared/backlog/sleep.js +3 -0
  62. package/dist/shared/console/progress.js +8 -1
  63. package/dist/shared/file-name.d.ts +1 -0
  64. package/dist/shared/file-name.js +11 -0
  65. package/dist/shared/storage/markdown-store.d.ts +2 -0
  66. package/dist/shared/storage/markdown-store.js +11 -0
  67. package/oclif.manifest.json +43 -2
  68. package/package.json +1 -1
  69. package/dist/shared/backlog/rate-limiter.d.ts +0 -9
  70. package/dist/shared/backlog/rate-limiter.js +0 -25
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import { writeProgress } from '../../../shared/console/progress.js';
3
- import { writeMarkdownFile } from '../../../shared/storage/markdown-store.js';
3
+ import { fileSize, writeBinaryFile, writeMarkdownFile } from '../../../shared/storage/markdown-store.js';
4
4
  import { appendLog } from '../../../shared/storage/update-log.js';
5
5
  import { selectWikisToExport } from '../domain/wiki-filter.js';
6
6
  import { buildWikiMarkdown } from '../domain/wiki-markdown.js';
7
- import { wikiRelativePath, wikiUrl } from '../domain/wiki-path.js';
7
+ import { wikiAttachmentMarkdownLink, wikiAttachmentRelativePath, wikiRelativePath, wikiUrl } from '../domain/wiki-path.js';
8
8
  export async function exportWikis(deps, options) {
9
9
  const { logger, wikiRepository } = deps;
10
10
  logger.log('Wikiの取得を開始します...');
@@ -30,7 +30,13 @@ export async function exportWikis(deps, options) {
30
30
  const wikiDetail = await wikiRepository.fetchDetail(wiki.id, options.projectIdOrKey);
31
31
  const filePath = path.join(options.outputDir, wikiRelativePath(wiki.name));
32
32
  const backlogWikiUrl = wikiUrl(options.domain, wiki.id);
33
- await writeMarkdownFile(filePath, buildWikiMarkdown(wiki.name, backlogWikiUrl, wikiDetail.content || ''));
33
+ const attachmentLinks = options.downloadAttachments
34
+ ? await downloadWikiAttachments(deps, wiki, wikiDetail.attachments ?? [], options.outputDir)
35
+ : undefined;
36
+ await writeMarkdownFile(filePath, buildWikiMarkdown(wiki.name, backlogWikiUrl, wikiDetail.content || '', {
37
+ items: wikiDetail.attachments,
38
+ localLinks: attachmentLinks,
39
+ }));
34
40
  await appendLog(options.outputDir, `Wiki「${wiki.name}」を更新しました: ${backlogWikiUrl}`);
35
41
  writeProgress(`Wikiを保存中... (${index + 1}/${wikis.length}件)`);
36
42
  }
@@ -41,3 +47,25 @@ export async function exportWikis(deps, options) {
41
47
  /* eslint-enable no-await-in-loop */
42
48
  logger.log('\nWikiのダウンロードが完了しました!');
43
49
  }
50
+ // 保存できた添付のみリンク化する。個々の失敗は警告に留め、Wiki本体の保存は続行する
51
+ async function downloadWikiAttachments(deps, wiki, attachments, outputDir) {
52
+ const links = new Map();
53
+ for (const attachment of attachments) {
54
+ const absolutePath = path.join(outputDir, wikiAttachmentRelativePath(wiki.name, attachment));
55
+ try {
56
+ // 添付IDは不変のため、サイズの一致するファイルが既にあれば再ダウンロードしない
57
+ // eslint-disable-next-line no-await-in-loop
58
+ if ((await fileSize(absolutePath)) !== attachment.size) {
59
+ // eslint-disable-next-line no-await-in-loop
60
+ const data = await deps.wikiRepository.downloadAttachment(wiki.id, attachment.id);
61
+ // eslint-disable-next-line no-await-in-loop
62
+ await writeBinaryFile(absolutePath, data);
63
+ }
64
+ links.set(attachment.id, wikiAttachmentMarkdownLink(wiki.name, attachment));
65
+ }
66
+ catch (error) {
67
+ deps.logger.warn(`Wiki「${wiki.name}」の添付ファイル「${attachment.name}」の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
68
+ }
69
+ }
70
+ return links;
71
+ }
@@ -0,0 +1,8 @@
1
+ export interface AttachmentRef {
2
+ id: number;
3
+ name: string;
4
+ }
5
+ export declare function attachmentFileName(attachment: AttachmentRef): string;
6
+ export declare function encodeLinkDestination(linkPath: string): string;
7
+ export declare function escapeLinkText(name: string): string;
8
+ export declare function rewriteInlineImages(text: string, attachments: AttachmentRef[] | undefined, localLinks?: Map<number, string>): string;
@@ -0,0 +1,37 @@
1
+ import { sanitizeAttachmentFileName } from './file-name.js';
2
+ // 添付IDを前置して同名添付の衝突を防ぎ、存在チェックだけでDL済み判定できるようにする
3
+ export function attachmentFileName(attachment) {
4
+ return `${attachment.id}_${sanitizeAttachmentFileName(attachment.name)}`;
5
+ }
6
+ // Markdownリンク先の丸括弧はインラインリンクを壊すためエンコードする
7
+ export function encodeLinkDestination(linkPath) {
8
+ return linkPath.replaceAll('(', '%28').replaceAll(')', '%29');
9
+ }
10
+ // ファイル名中の角括弧はリンク構文を壊すためエスケープする
11
+ export function escapeLinkText(name) {
12
+ return name.replaceAll('[', String.raw `\[`).replaceAll(']', String.raw `\]`);
13
+ }
14
+ // Backlogの添付画像インライン記法(Markdown拡張の ![alt][ファイル名] とBacklog記法の #image(ファイル名))を
15
+ // ダウンロード済みファイルへのローカルリンクに変換する。未ダウンロードの参照は壊さずそのまま残す
16
+ export function rewriteInlineImages(text, attachments, localLinks) {
17
+ if (!text || !attachments || attachments.length === 0 || !localLinks || localLinks.size === 0) {
18
+ return text;
19
+ }
20
+ // 同名添付が複数ある場合は記法から特定できないため先勝ちで解決する。
21
+ // 記法内のファイル名はNFD(macOSからのD&D等)で入ることがあるため、照合はNFC正規化で行う
22
+ const linkByName = new Map();
23
+ for (const attachment of attachments) {
24
+ const link = localLinks.get(attachment.id);
25
+ const name = attachment.name.normalize('NFC');
26
+ if (link && !linkByName.has(name)) {
27
+ linkByName.set(name, link);
28
+ }
29
+ }
30
+ const toLocalImage = (match, name) => {
31
+ const link = linkByName.get(name.normalize('NFC'));
32
+ return link ? `![${escapeLinkText(name)}](${link})` : match;
33
+ };
34
+ return text
35
+ .replaceAll(/!\[[^\]]*\]\[([^\]]+)\]/g, toLocalImage)
36
+ .replaceAll(/#image\(([^)]+)\)/g, toLocalImage);
37
+ }
@@ -2,16 +2,19 @@ export declare class HttpError extends Error {
2
2
  readonly status: number;
3
3
  constructor(status: number, statusText: string, url: string);
4
4
  }
5
+ export declare function rateLimitWaitMs(resetHeader: null | string, nowMs: number): number;
5
6
  export declare class BacklogHttpClient {
6
7
  private readonly apiKey;
7
8
  private readonly baseUrl;
8
- private readonly rateLimiter;
9
+ private readonly onRateLimitExceeded?;
9
10
  constructor(options: {
10
11
  apiKey: string;
11
12
  domain: string;
12
- onRateLimitWait?: () => void;
13
+ onRateLimitExceeded?: (waitSeconds: number) => void;
13
14
  });
15
+ getBinary(pathname: string, params?: Record<string, string>): Promise<ArrayBuffer>;
14
16
  getJson<T>(pathname: string, params?: Record<string, string>): Promise<T>;
15
17
  private fetchWithRetry;
16
18
  private maskApiKey;
19
+ private requestUrl;
17
20
  }
@@ -1,5 +1,5 @@
1
1
  import { backlogOrigin } from '../backlog-url.js';
2
- import { RateLimiter, sleep } from './rate-limiter.js';
2
+ import { sleep } from './sleep.js';
3
3
  // fetchは4xx/5xxでthrowしないため、response.okでない場合にこれを投げる
4
4
  export class HttpError extends Error {
5
5
  status;
@@ -9,30 +9,47 @@ export class HttpError extends Error {
9
9
  this.name = 'HttpError';
10
10
  }
11
11
  }
12
- const RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
12
+ const RETRYABLE_STATUSES = new Set([408, 500, 502, 503, 504]);
13
13
  const MAX_RETRIES = 2;
14
+ // 429待機は1回ごとにレート制限ウィンドウ(1分)が更新されるため、通常リトライとは別枠で上限を設ける
15
+ const MAX_RATE_LIMIT_WAITS = 5;
14
16
  const RETRY_BASE_DELAY_MS = 300;
15
- // apiKey付与・レート制限(100req/15s)・一時エラーのリトライを担うBacklog APIクライアント
17
+ const RATE_LIMIT_MIN_WAIT_MS = 1000;
18
+ const RATE_LIMIT_FALLBACK_WAIT_MS = 60_000;
19
+ const RATE_LIMIT_MAX_WAIT_MS = 120_000;
20
+ // レート制限(429)はウィンドウが1分単位のため、短いbackoffではなく
21
+ // X-RateLimit-Reset(UNIX秒)まで待つ。ヘッダーが無い/不正な場合は1分待つ
22
+ export function rateLimitWaitMs(resetHeader, nowMs) {
23
+ const resetSeconds = Number(resetHeader);
24
+ if (!resetHeader || !Number.isFinite(resetSeconds))
25
+ return RATE_LIMIT_FALLBACK_WAIT_MS;
26
+ // リセット直後の境界ずれで再度429にならないよう1秒の余裕を持たせる
27
+ const waitMs = resetSeconds * 1000 - nowMs + 1000;
28
+ return Math.min(Math.max(waitMs, RATE_LIMIT_MIN_WAIT_MS), RATE_LIMIT_MAX_WAIT_MS);
29
+ }
30
+ // apiKey付与・429時のX-RateLimit-Resetまでの待機・一時エラーのリトライを担うBacklog APIクライアント
16
31
  export class BacklogHttpClient {
17
32
  apiKey;
18
33
  baseUrl;
19
- rateLimiter;
34
+ onRateLimitExceeded;
20
35
  constructor(options) {
21
36
  this.apiKey = options.apiKey;
22
37
  this.baseUrl = `${backlogOrigin(options.domain)}/api/v2`;
23
- this.rateLimiter = new RateLimiter(options.onRateLimitWait);
38
+ this.onRateLimitExceeded = options.onRateLimitExceeded;
39
+ }
40
+ async getBinary(pathname, params = {}) {
41
+ const response = await this.fetchWithRetry(this.requestUrl(pathname, params));
42
+ return response.arrayBuffer();
24
43
  }
25
44
  async getJson(pathname, params = {}) {
26
- await this.rateLimiter.increment();
27
- const searchParams = new URLSearchParams({ apiKey: this.apiKey, ...params });
28
- const url = `${this.baseUrl}${pathname}?${searchParams.toString()}`;
29
- const response = await this.fetchWithRetry(url);
45
+ const response = await this.fetchWithRetry(this.requestUrl(pathname, params));
30
46
  return (await response.json());
31
47
  }
32
48
  // リトライのため、ループ内のawaitは意図したもの
33
49
  /* eslint-disable no-await-in-loop */
34
50
  async fetchWithRetry(url) {
35
- for (let attempt = 0;; attempt++) {
51
+ let rateLimitWaits = 0;
52
+ for (let attempt = 0;;) {
36
53
  let response;
37
54
  try {
38
55
  response = await fetch(url);
@@ -41,6 +58,7 @@ export class BacklogHttpClient {
41
58
  // ネットワークエラー
42
59
  if (attempt < MAX_RETRIES) {
43
60
  await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
61
+ attempt++;
44
62
  continue;
45
63
  }
46
64
  throw error;
@@ -48,8 +66,17 @@ export class BacklogHttpClient {
48
66
  if (response.ok) {
49
67
  return response;
50
68
  }
69
+ // 429は待機すれば解消する見込みが高いため、通常のリトライ回数を消費しない
70
+ if (response.status === 429 && rateLimitWaits < MAX_RATE_LIMIT_WAITS) {
71
+ const waitMs = rateLimitWaitMs(response.headers.get('x-ratelimit-reset'), Date.now());
72
+ this.onRateLimitExceeded?.(Math.ceil(waitMs / 1000));
73
+ await sleep(waitMs);
74
+ rateLimitWaits++;
75
+ continue;
76
+ }
51
77
  if (attempt < MAX_RETRIES && RETRYABLE_STATUSES.has(response.status)) {
52
78
  await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
79
+ attempt++;
53
80
  continue;
54
81
  }
55
82
  throw new HttpError(response.status, response.statusText, this.maskApiKey(url));
@@ -59,4 +86,8 @@ export class BacklogHttpClient {
59
86
  maskApiKey(url) {
60
87
  return url.replace(/apiKey=[^&]*/, 'apiKey=***');
61
88
  }
89
+ requestUrl(pathname, params) {
90
+ const searchParams = new URLSearchParams({ apiKey: this.apiKey, ...params });
91
+ return `${this.baseUrl}${pathname}?${searchParams.toString()}`;
92
+ }
62
93
  }
@@ -0,0 +1 @@
1
+ export declare const sleep: (ms: number) => Promise<void>;
@@ -0,0 +1,3 @@
1
+ export const sleep = (ms) => new Promise((resolve) => {
2
+ setTimeout(resolve, ms);
3
+ });
@@ -1,4 +1,11 @@
1
1
  import process from 'node:process';
2
2
  export function writeProgress(message) {
3
- process.stdout.write(`\r${message}`);
3
+ if (process.stdout.isTTY) {
4
+ // ESC[K でカーソル位置から行末まで消去し、前の長いメッセージの残骸を防ぐ
5
+ process.stdout.write(`\r\u001B[K${message}`);
6
+ }
7
+ else {
8
+ // 非TTY(CIログ・パイプ)では \r が効かず1行に連結されるため改行して出力する
9
+ process.stdout.write(`${message}\n`);
10
+ }
4
11
  }
@@ -1,2 +1,3 @@
1
1
  export declare function sanitizeFileName(name: string): string;
2
+ export declare function sanitizeAttachmentFileName(name: string): string;
2
3
  export declare function sanitizeWikiFileName(name: string): string;
@@ -5,6 +5,17 @@ export function sanitizeFileName(name) {
5
5
  .replaceAll('.', '_') // ドットを置換
6
6
  .slice(0, 200); // 長すぎるファイル名を防ぐために200文字に制限
7
7
  }
8
+ // 添付ファイル用: sanitizeFileNameと異なり、拡張子が消えないようドットを保持する
9
+ export function sanitizeAttachmentFileName(name) {
10
+ const sanitized = name.replaceAll(/[\\/:*?"<>|]/g, '_').replaceAll(/\s+/g, '_');
11
+ if (sanitized.length <= 200)
12
+ return sanitized;
13
+ const dotIndex = sanitized.lastIndexOf('.');
14
+ const extension = dotIndex > 0 ? sanitized.slice(dotIndex) : '';
15
+ if (extension.length === 0 || extension.length >= 200)
16
+ return sanitized.slice(0, 200);
17
+ return sanitized.slice(0, 200 - extension.length) + extension;
18
+ }
8
19
  export function sanitizeWikiFileName(name) {
9
20
  const invalidChars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
10
21
  let sanitizedName = name;
@@ -1,5 +1,7 @@
1
1
  export declare function ensureDirectory(directory: string): Promise<void>;
2
2
  export declare function fileExists(filePath: string): Promise<boolean>;
3
3
  export declare function writeMarkdownFile(filePath: string, content: string): Promise<void>;
4
+ export declare function writeBinaryFile(filePath: string, data: ArrayBuffer): Promise<void>;
5
+ export declare function fileSize(filePath: string): Promise<null | number>;
4
6
  export declare function deleteFile(filePath: string): Promise<void>;
5
7
  export declare function assertDirectoryExists(directory: string): Promise<void>;
@@ -10,6 +10,17 @@ export async function writeMarkdownFile(filePath, content) {
10
10
  await fs.mkdir(path.dirname(filePath), { recursive: true });
11
11
  await fs.writeFile(filePath, content);
12
12
  }
13
+ // 中断で壊れたファイルが完成品として残らないよう、一時ファイルに書いてからrenameする
14
+ export async function writeBinaryFile(filePath, data) {
15
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
16
+ const tempPath = `${filePath}.tmp`;
17
+ await fs.writeFile(tempPath, new Uint8Array(data));
18
+ await fs.rename(tempPath, filePath);
19
+ }
20
+ export async function fileSize(filePath) {
21
+ const stats = await fs.stat(filePath).catch(() => null);
22
+ return stats?.size ?? null;
23
+ }
13
24
  export async function deleteFile(filePath) {
14
25
  await fs.unlink(filePath);
15
26
  }
@@ -31,6 +31,14 @@
31
31
  "multiple": false,
32
32
  "type": "option"
33
33
  },
34
+ "downloadAttachments": {
35
+ "char": "d",
36
+ "description": "課題の添付ファイルもダウンロードする",
37
+ "name": "downloadAttachments",
38
+ "required": false,
39
+ "allowNo": false,
40
+ "type": "boolean"
41
+ },
34
42
  "exclude": {
35
43
  "description": "Exclude the specified types, separated by commas (e.g., 'documents,wiki')",
36
44
  "name": "exclude",
@@ -131,6 +139,14 @@
131
139
  "multiple": false,
132
140
  "type": "option"
133
141
  },
142
+ "downloadAttachments": {
143
+ "char": "d",
144
+ "description": "添付ファイルもダウンロードする",
145
+ "name": "downloadAttachments",
146
+ "required": false,
147
+ "allowNo": false,
148
+ "type": "boolean"
149
+ },
134
150
  "keyword": {
135
151
  "description": "検索キーワード",
136
152
  "name": "keyword",
@@ -184,7 +200,8 @@
184
200
  "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --maxCount 10000\n最大10000件の課題を取得する(デフォルトは5000件)\n",
185
201
  "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFileName\nファイル名を課題キーにする\n",
186
202
  "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFolder\n課題キーでフォルダを作成する\n",
187
- "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFileName --issueKeyFolder\n課題キーでフォルダを作成し、ファイル名も課題キーにする\n"
203
+ "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFileName --issueKeyFolder\n課題キーでフォルダを作成し、ファイル名も課題キーにする\n",
204
+ "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --downloadAttachments\n課題の添付ファイルもダウンロードする\n"
188
205
  ],
189
206
  "flags": {
190
207
  "apiKey": {
@@ -203,6 +220,14 @@
203
220
  "multiple": false,
204
221
  "type": "option"
205
222
  },
223
+ "downloadAttachments": {
224
+ "char": "d",
225
+ "description": "課題の添付ファイルもダウンロードする",
226
+ "name": "downloadAttachments",
227
+ "required": false,
228
+ "allowNo": false,
229
+ "type": "boolean"
230
+ },
206
231
  "issueKeyFileName": {
207
232
  "description": "ファイル名を課題キーにする",
208
233
  "name": "issueKeyFileName",
@@ -388,6 +413,14 @@
388
413
  "multiple": false,
389
414
  "type": "option"
390
415
  },
416
+ "downloadAttachments": {
417
+ "char": "d",
418
+ "description": "課題の添付ファイルもダウンロードする(未指定時は設定ファイルの値を使用)",
419
+ "name": "downloadAttachments",
420
+ "required": false,
421
+ "allowNo": false,
422
+ "type": "boolean"
423
+ },
391
424
  "force": {
392
425
  "char": "f",
393
426
  "description": "確認プロンプトをスキップする",
@@ -490,6 +523,14 @@
490
523
  "multiple": false,
491
524
  "type": "option"
492
525
  },
526
+ "downloadAttachments": {
527
+ "char": "d",
528
+ "description": "添付ファイルもダウンロードする",
529
+ "name": "downloadAttachments",
530
+ "required": false,
531
+ "allowNo": false,
532
+ "type": "boolean"
533
+ },
493
534
  "output": {
494
535
  "char": "o",
495
536
  "description": "出力ディレクトリパス",
@@ -525,5 +566,5 @@
525
566
  ]
526
567
  }
527
568
  },
528
- "version": "1.0.0"
569
+ "version": "1.1.1"
529
570
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "backlog-exporter",
3
3
  "description": "A new CLI generated with oclif",
4
- "version": "1.0.0",
4
+ "version": "1.1.1",
5
5
  "author": "ShuntaToda",
6
6
  "bin": {
7
7
  "backlog-exporter": "bin/run.js"
@@ -1,9 +0,0 @@
1
- export declare const sleep: (ms: number) => Promise<void>;
2
- export declare class RateLimiter {
3
- private readonly onWait?;
4
- private requestCount;
5
- constructor(onWait?: () => void);
6
- getCount(): number;
7
- increment(): Promise<number>;
8
- setCount(count: number): void;
9
- }
@@ -1,25 +0,0 @@
1
- export const sleep = (ms) => new Promise((resolve) => {
2
- setTimeout(resolve, ms);
3
- });
4
- // Backlogのレート制限(100req/15s)対策: 100リクエストごとに15秒待機する
5
- export class RateLimiter {
6
- onWait;
7
- requestCount = 0;
8
- constructor(onWait) {
9
- this.onWait = onWait;
10
- }
11
- getCount() {
12
- return this.requestCount;
13
- }
14
- async increment() {
15
- this.requestCount++;
16
- if (this.requestCount > 1 && this.requestCount % 100 === 0) {
17
- this.onWait?.();
18
- await sleep(15_000);
19
- }
20
- return this.requestCount;
21
- }
22
- setCount(count) {
23
- this.requestCount = count;
24
- }
25
- }