backlog-exporter 0.7.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +87 -1
  2. package/bin/dev.js +6 -2
  3. package/dist/commands/all/index.d.ts +2 -0
  4. package/dist/commands/all/index.js +62 -126
  5. package/dist/commands/document/index.js +18 -11
  6. package/dist/commands/issue/index.js +18 -11
  7. package/dist/commands/prune/index.d.ts +16 -0
  8. package/dist/commands/prune/index.js +86 -0
  9. package/dist/commands/update/index.d.ts +3 -7
  10. package/dist/commands/update/index.js +28 -260
  11. package/dist/commands/wiki/index.js +17 -9
  12. package/dist/composition/backlog-repositories.d.ts +16 -0
  13. package/dist/composition/backlog-repositories.js +15 -0
  14. package/dist/modules/all/domain/content-selection.d.ts +4 -0
  15. package/dist/modules/all/domain/content-selection.js +16 -0
  16. package/dist/modules/all/use-case/export-all.d.ts +24 -0
  17. package/dist/modules/all/use-case/export-all.js +78 -0
  18. package/dist/modules/document/domain/document-markdown.d.ts +2 -0
  19. package/dist/modules/document/domain/document-markdown.js +37 -0
  20. package/dist/modules/document/domain/document-path.d.ts +15 -0
  21. package/dist/modules/document/domain/document-path.js +48 -0
  22. package/dist/modules/document/domain/document-repository.d.ts +6 -0
  23. package/dist/modules/document/domain/document-repository.js +1 -0
  24. package/dist/modules/document/domain/document-save-plan.d.ts +9 -0
  25. package/dist/modules/document/domain/document-save-plan.js +15 -0
  26. package/dist/modules/document/domain/document.d.ts +50 -0
  27. package/dist/modules/document/domain/document.js +1 -0
  28. package/dist/modules/document/repository/backlog-document-repository.d.ts +3 -0
  29. package/dist/modules/document/repository/backlog-document-repository.js +31 -0
  30. package/dist/modules/document/use-case/export-documents.d.ts +16 -0
  31. package/dist/modules/document/use-case/export-documents.js +82 -0
  32. package/dist/modules/issue/domain/issue-filter.d.ts +2 -0
  33. package/dist/modules/issue/domain/issue-filter.js +7 -0
  34. package/dist/modules/issue/domain/issue-markdown.d.ts +4 -0
  35. package/dist/modules/issue/domain/issue-markdown.js +112 -0
  36. package/dist/modules/issue/domain/issue-path.d.ts +23 -0
  37. package/dist/modules/issue/domain/issue-path.js +30 -0
  38. package/dist/modules/issue/domain/issue-repository.d.ts +11 -0
  39. package/dist/modules/issue/domain/issue-repository.js +1 -0
  40. package/dist/modules/issue/domain/issue.d.ts +47 -0
  41. package/dist/modules/issue/domain/issue.js +1 -0
  42. package/dist/modules/issue/repository/backlog-issue-repository.d.ts +3 -0
  43. package/dist/modules/issue/repository/backlog-issue-repository.js +38 -0
  44. package/dist/modules/issue/use-case/export-issues.d.ts +18 -0
  45. package/dist/modules/issue/use-case/export-issues.js +87 -0
  46. package/dist/modules/project/domain/project-repository.d.ts +3 -0
  47. package/dist/modules/project/domain/project-repository.js +1 -0
  48. package/dist/modules/project/repository/backlog-project-repository.d.ts +3 -0
  49. package/dist/modules/project/repository/backlog-project-repository.js +17 -0
  50. package/dist/modules/prune/domain/expected-paths.d.ts +4 -0
  51. package/dist/modules/prune/domain/expected-paths.js +1 -0
  52. package/dist/modules/prune/domain/prune-target.d.ts +3 -0
  53. package/dist/modules/prune/domain/prune-target.js +21 -0
  54. package/dist/modules/prune/repository/prune-walker.d.ts +7 -0
  55. package/dist/modules/prune/repository/prune-walker.js +39 -0
  56. package/dist/modules/prune/use-case/prune-directories.d.ts +28 -0
  57. package/dist/modules/prune/use-case/prune-directories.js +74 -0
  58. package/dist/modules/prune/use-case/prune-exports.d.ts +27 -0
  59. package/dist/modules/prune/use-case/prune-exports.js +74 -0
  60. package/dist/modules/settings/domain/settings.d.ts +15 -0
  61. package/dist/modules/settings/domain/settings.js +6 -0
  62. package/dist/modules/settings/repository/settings-store.d.ts +6 -0
  63. package/dist/modules/settings/repository/settings-store.js +56 -0
  64. package/dist/modules/update/domain/update-plan.d.ts +36 -0
  65. package/dist/modules/update/domain/update-plan.js +69 -0
  66. package/dist/modules/update/use-case/update-exports.d.ts +21 -0
  67. package/dist/modules/update/use-case/update-exports.js +136 -0
  68. package/dist/modules/wiki/domain/wiki-filter.d.ts +9 -0
  69. package/dist/modules/wiki/domain/wiki-filter.js +11 -0
  70. package/dist/modules/wiki/domain/wiki-markdown.d.ts +1 -0
  71. package/dist/modules/wiki/domain/wiki-markdown.js +4 -0
  72. package/dist/modules/wiki/domain/wiki-path.d.ts +4 -0
  73. package/dist/modules/wiki/domain/wiki-path.js +25 -0
  74. package/dist/modules/wiki/domain/wiki-repository.d.ts +5 -0
  75. package/dist/modules/wiki/domain/wiki-repository.js +1 -0
  76. package/dist/modules/wiki/domain/wiki.d.ts +10 -0
  77. package/dist/modules/wiki/domain/wiki.js +1 -0
  78. package/dist/modules/wiki/repository/backlog-wiki-repository.d.ts +3 -0
  79. package/dist/modules/wiki/repository/backlog-wiki-repository.js +10 -0
  80. package/dist/modules/wiki/use-case/export-wikis.d.ts +14 -0
  81. package/dist/modules/wiki/use-case/export-wikis.js +43 -0
  82. package/dist/shared/backlog/http-client.d.ts +17 -0
  83. package/dist/shared/backlog/http-client.js +62 -0
  84. package/dist/shared/backlog/rate-limiter.d.ts +9 -0
  85. package/dist/shared/backlog/rate-limiter.js +25 -0
  86. package/dist/shared/backlog-url.d.ts +1 -0
  87. package/dist/shared/backlog-url.js +5 -0
  88. package/dist/shared/config/env.d.ts +3 -0
  89. package/dist/shared/config/env.js +21 -0
  90. package/dist/shared/console/progress.d.ts +1 -0
  91. package/dist/shared/console/progress.js +4 -0
  92. package/dist/shared/console/prompt.d.ts +3 -0
  93. package/dist/shared/console/prompt.js +19 -0
  94. package/dist/shared/file-name.d.ts +2 -0
  95. package/dist/shared/file-name.js +15 -0
  96. package/dist/shared/markdown/body-marker.d.ts +3 -0
  97. package/dist/shared/markdown/body-marker.js +5 -0
  98. package/dist/shared/ports.d.ts +4 -0
  99. package/dist/shared/ports.js +1 -0
  100. package/dist/shared/storage/markdown-store.d.ts +5 -0
  101. package/dist/shared/storage/markdown-store.js +21 -0
  102. package/dist/shared/storage/update-log.d.ts +1 -0
  103. package/dist/{utils/log.js → shared/storage/update-log.js} +1 -5
  104. package/oclif.manifest.json +118 -26
  105. package/package.json +15 -19
  106. package/dist/utils/backlog-api.d.ts +0 -74
  107. package/dist/utils/backlog-api.js +0 -453
  108. package/dist/utils/backlog.d.ts +0 -16
  109. package/dist/utils/backlog.js +0 -34
  110. package/dist/utils/common.d.ts +0 -25
  111. package/dist/utils/common.js +0 -54
  112. package/dist/utils/log.d.ts +0 -6
  113. package/dist/utils/settings.d.ts +0 -45
  114. package/dist/utils/settings.js +0 -63
  115. package/dist/utils/sleep.d.ts +0 -28
  116. package/dist/utils/sleep.js +0 -42
@@ -0,0 +1,4 @@
1
+ import { wrapBody } from '../../../shared/markdown/body-marker.js';
2
+ export function buildWikiMarkdown(wikiName, backlogWikiUrl, content) {
3
+ return `# ${wikiName}\n\n[Backlog Wiki Link](${backlogWikiUrl})\n\n${wrapBody(content || '(内容なし)')}`;
4
+ }
@@ -0,0 +1,4 @@
1
+ import { ExpectedPaths } from '../../prune/domain/expected-paths.js';
2
+ export declare function wikiRelativePath(wikiName: string): string;
3
+ export declare function wikiUrl(domain: string, wikiId: string): string;
4
+ export declare function buildWikiExpectedPaths(wikiNames: string[]): ExpectedPaths;
@@ -0,0 +1,25 @@
1
+ import path from 'node:path';
2
+ import { backlogOrigin } from '../../../shared/backlog-url.js';
3
+ import { sanitizeWikiFileName } from '../../../shared/file-name.js';
4
+ export function wikiRelativePath(wikiName) {
5
+ return `${sanitizeWikiFileName(wikiName)}.md`;
6
+ }
7
+ export function wikiUrl(domain, wikiId) {
8
+ return `${backlogOrigin(domain)}/alias/wiki/${wikiId}`;
9
+ }
10
+ // 比較相手のpath.relativeはOS標準の区切り文字を返すため、path.normalizeで揃える(Windows対策)
11
+ export function buildWikiExpectedPaths(wikiNames) {
12
+ const expectedFiles = new Set();
13
+ const expectedDirs = new Set();
14
+ for (const name of wikiNames) {
15
+ const relativePath = path.normalize(wikiRelativePath(name)).normalize('NFC');
16
+ expectedFiles.add(relativePath);
17
+ // "親/子.md" のような名前では親ディレクトリも「あるべきディレクトリ」として登録する
18
+ let dir = path.dirname(relativePath);
19
+ while (dir && dir !== '.') {
20
+ expectedDirs.add(dir.normalize('NFC'));
21
+ dir = path.dirname(dir);
22
+ }
23
+ }
24
+ return { expectedDirs, expectedFiles };
25
+ }
@@ -0,0 +1,5 @@
1
+ import { WikiDetail, WikiSummary } from './wiki.js';
2
+ export interface WikiRepository {
3
+ fetchDetail(wikiId: string, projectIdOrKey: string): Promise<WikiDetail>;
4
+ fetchWikis(projectIdOrKey: string): Promise<WikiSummary[]>;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ export interface WikiSummary {
2
+ id: string;
3
+ name: string;
4
+ updated: string;
5
+ }
6
+ export interface WikiDetail {
7
+ content?: string;
8
+ id: string;
9
+ name: string;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { BacklogHttpClient } from '../../../shared/backlog/http-client.js';
2
+ import { WikiRepository } from '../domain/wiki-repository.js';
3
+ export declare function newBacklogWikiRepository(client: BacklogHttpClient): WikiRepository;
@@ -0,0 +1,10 @@
1
+ export function newBacklogWikiRepository(client) {
2
+ return {
3
+ async fetchDetail(wikiId, projectIdOrKey) {
4
+ return client.getJson(`/wikis/${wikiId}`, { projectIdOrKey });
5
+ },
6
+ async fetchWikis(projectIdOrKey) {
7
+ return client.getJson('/wikis', { projectIdOrKey });
8
+ },
9
+ };
10
+ }
@@ -0,0 +1,14 @@
1
+ import { Logger } from '../../../shared/ports.js';
2
+ import { WikiRepository } from '../domain/wiki-repository.js';
3
+ export interface ExportWikisDeps {
4
+ logger: Logger;
5
+ wikiRepository: WikiRepository;
6
+ }
7
+ export interface ExportWikisOptions {
8
+ domain: string;
9
+ lastUpdated?: string;
10
+ outputDir: string;
11
+ projectIdOrKey: string;
12
+ wikiIds?: string[];
13
+ }
14
+ export declare function exportWikis(deps: ExportWikisDeps, options: ExportWikisOptions): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import path from 'node:path';
2
+ import { writeProgress } from '../../../shared/console/progress.js';
3
+ import { writeMarkdownFile } from '../../../shared/storage/markdown-store.js';
4
+ import { appendLog } from '../../../shared/storage/update-log.js';
5
+ import { selectWikisToExport } from '../domain/wiki-filter.js';
6
+ import { buildWikiMarkdown } from '../domain/wiki-markdown.js';
7
+ import { wikiRelativePath, wikiUrl } from '../domain/wiki-path.js';
8
+ export async function exportWikis(deps, options) {
9
+ const { logger, wikiRepository } = deps;
10
+ logger.log('Wikiの取得を開始します...');
11
+ logger.log('Wiki一覧を取得しています...');
12
+ const allWikis = await wikiRepository.fetchWikis(options.projectIdOrKey);
13
+ logger.log(`${allWikis.length}件のWikiが見つかりました。`);
14
+ const { reason, wikis } = selectWikisToExport(allWikis, options);
15
+ if (reason === 'ids') {
16
+ logger.log(`指定された${wikis.length}件のWikiを処理します。`);
17
+ }
18
+ else if (reason === 'updatedSince') {
19
+ logger.log(`前回の更新日時(${options.lastUpdated})以降に更新された${wikis.length}件のWikiを処理します。`);
20
+ }
21
+ if (wikis.length === 0) {
22
+ logger.log('更新が必要なWikiはありません。');
23
+ return;
24
+ }
25
+ logger.log('Wiki詳細を取得しています...');
26
+ /* eslint-disable no-await-in-loop */
27
+ for (const [index, wiki] of wikis.entries()) {
28
+ try {
29
+ writeProgress(`Wikiを取得中... (${index + 1}/${wikis.length}件)`);
30
+ const wikiDetail = await wikiRepository.fetchDetail(wiki.id, options.projectIdOrKey);
31
+ const filePath = path.join(options.outputDir, wikiRelativePath(wiki.name));
32
+ const backlogWikiUrl = wikiUrl(options.domain, wiki.id);
33
+ await writeMarkdownFile(filePath, buildWikiMarkdown(wiki.name, backlogWikiUrl, wikiDetail.content || ''));
34
+ await appendLog(options.outputDir, `Wiki「${wiki.name}」を更新しました: ${backlogWikiUrl}`);
35
+ writeProgress(`Wikiを保存中... (${index + 1}/${wikis.length}件)`);
36
+ }
37
+ catch (error) {
38
+ logger.warn(`Wiki ${wiki.name} の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
39
+ }
40
+ }
41
+ /* eslint-enable no-await-in-loop */
42
+ logger.log('\nWikiのダウンロードが完了しました!');
43
+ }
@@ -0,0 +1,17 @@
1
+ export declare class HttpError extends Error {
2
+ readonly status: number;
3
+ constructor(status: number, statusText: string, url: string);
4
+ }
5
+ export declare class BacklogHttpClient {
6
+ private readonly apiKey;
7
+ private readonly baseUrl;
8
+ private readonly rateLimiter;
9
+ constructor(options: {
10
+ apiKey: string;
11
+ domain: string;
12
+ onRateLimitWait?: () => void;
13
+ });
14
+ getJson<T>(pathname: string, params?: Record<string, string>): Promise<T>;
15
+ private fetchWithRetry;
16
+ private maskApiKey;
17
+ }
@@ -0,0 +1,62 @@
1
+ import { backlogOrigin } from '../backlog-url.js';
2
+ import { RateLimiter, sleep } from './rate-limiter.js';
3
+ // fetchは4xx/5xxでthrowしないため、response.okでない場合にこれを投げる
4
+ export class HttpError extends Error {
5
+ status;
6
+ constructor(status, statusText, url) {
7
+ super(`HTTP ${status} ${statusText}: GET ${url}`);
8
+ this.status = status;
9
+ this.name = 'HttpError';
10
+ }
11
+ }
12
+ const RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
13
+ const MAX_RETRIES = 2;
14
+ const RETRY_BASE_DELAY_MS = 300;
15
+ // apiKey付与・レート制限(100req/15s)・一時エラーのリトライを担うBacklog APIクライアント
16
+ export class BacklogHttpClient {
17
+ apiKey;
18
+ baseUrl;
19
+ rateLimiter;
20
+ constructor(options) {
21
+ this.apiKey = options.apiKey;
22
+ this.baseUrl = `${backlogOrigin(options.domain)}/api/v2`;
23
+ this.rateLimiter = new RateLimiter(options.onRateLimitWait);
24
+ }
25
+ 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);
30
+ return (await response.json());
31
+ }
32
+ // リトライのため、ループ内のawaitは意図したもの
33
+ /* eslint-disable no-await-in-loop */
34
+ async fetchWithRetry(url) {
35
+ for (let attempt = 0;; attempt++) {
36
+ let response;
37
+ try {
38
+ response = await fetch(url);
39
+ }
40
+ catch (error) {
41
+ // ネットワークエラー
42
+ if (attempt < MAX_RETRIES) {
43
+ await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
44
+ continue;
45
+ }
46
+ throw error;
47
+ }
48
+ if (response.ok) {
49
+ return response;
50
+ }
51
+ if (attempt < MAX_RETRIES && RETRYABLE_STATUSES.has(response.status)) {
52
+ await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
53
+ continue;
54
+ }
55
+ throw new HttpError(response.status, response.statusText, this.maskApiKey(url));
56
+ }
57
+ }
58
+ /* eslint-enable no-await-in-loop */
59
+ maskApiKey(url) {
60
+ return url.replace(/apiKey=[^&]*/, 'apiKey=***');
61
+ }
62
+ }
@@ -0,0 +1,9 @@
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
+ }
@@ -0,0 +1,25 @@
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
+ }
@@ -0,0 +1 @@
1
+ export declare function backlogOrigin(domain: string): string;
@@ -0,0 +1,5 @@
1
+ // 通常はBacklogドメイン(example.backlog.jp)を https:// として扱う。
2
+ // テストのモックサーバやセルフホスト向けにプロトコル付き(http://127.0.0.1:8080 等)も許可する
3
+ export function backlogOrigin(domain) {
4
+ return /^https?:\/\//.test(domain) ? domain : `https://${domain}`;
5
+ }
@@ -0,0 +1,3 @@
1
+ export declare function loadDotenv(): void;
2
+ export declare function resolveApiKey(provided?: string, onEnvUsed?: () => void): string | undefined;
3
+ export declare const API_KEY_NOT_FOUND_MESSAGE = "API\u30AD\u30FC\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002--apiKey \u30D5\u30E9\u30B0\u307E\u305F\u306F BACKLOG_API_KEY \u74B0\u5883\u5909\u6570\u3067\u63D0\u4F9B\u3057\u3066\u304F\u3060\u3055\u3044";
@@ -0,0 +1,21 @@
1
+ import process from 'node:process';
2
+ export function loadDotenv() {
3
+ try {
4
+ process.loadEnvFile();
5
+ }
6
+ catch {
7
+ // .env が存在しない場合は何もしない
8
+ }
9
+ }
10
+ export function resolveApiKey(provided, onEnvUsed) {
11
+ if (provided) {
12
+ return provided;
13
+ }
14
+ const envApiKey = process.env.BACKLOG_API_KEY;
15
+ if (envApiKey) {
16
+ onEnvUsed?.();
17
+ return envApiKey;
18
+ }
19
+ return undefined;
20
+ }
21
+ export const API_KEY_NOT_FOUND_MESSAGE = 'APIキーが見つかりません。--apiKey フラグまたは BACKLOG_API_KEY 環境変数で提供してください';
@@ -0,0 +1 @@
1
+ export declare function writeProgress(message: string): void;
@@ -0,0 +1,4 @@
1
+ import process from 'node:process';
2
+ export function writeProgress(message) {
3
+ process.stdout.write(`\r${message}`);
4
+ }
@@ -0,0 +1,3 @@
1
+ export declare function isInteractiveStdin(): boolean;
2
+ export declare function readLine(): Promise<string>;
3
+ export declare function readYesNo(): Promise<boolean>;
@@ -0,0 +1,19 @@
1
+ import process from 'node:process';
2
+ // 非対話環境では'data'イベントが発火せず確認プロンプトが永久に待機するため、表示前に必ず確認する
3
+ export function isInteractiveStdin() {
4
+ return Boolean(process.stdin.isTTY);
5
+ }
6
+ export async function readLine() {
7
+ process.stdin.resume();
8
+ process.stdin.setEncoding('utf8');
9
+ return new Promise((resolve) => {
10
+ process.stdin.once('data', (data) => {
11
+ resolve(data.toString().trim());
12
+ process.stdin.pause();
13
+ });
14
+ });
15
+ }
16
+ export async function readYesNo() {
17
+ const input = (await readLine()).toLowerCase();
18
+ return input === 'y' || input === 'yes';
19
+ }
@@ -0,0 +1,2 @@
1
+ export declare function sanitizeFileName(name: string): string;
2
+ export declare function sanitizeWikiFileName(name: string): string;
@@ -0,0 +1,15 @@
1
+ export function sanitizeFileName(name) {
2
+ return name
3
+ .replaceAll(/[\\/:*?"<>|]/g, '_') // Windowsで使用できない文字を置換
4
+ .replaceAll(/\s+/g, '_') // スペースをアンダースコアに置換
5
+ .replaceAll('.', '_') // ドットを置換
6
+ .slice(0, 200); // 長すぎるファイル名を防ぐために200文字に制限
7
+ }
8
+ export function sanitizeWikiFileName(name) {
9
+ const invalidChars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
10
+ let sanitizedName = name;
11
+ for (const char of invalidChars) {
12
+ sanitizedName = sanitizedName.replaceAll(char, '_');
13
+ }
14
+ return sanitizedName;
15
+ }
@@ -0,0 +1,3 @@
1
+ export declare const BODY_START_MARKER = "<!-- backlog-exporter:body:start -->";
2
+ export declare const BODY_END_MARKER = "<!-- backlog-exporter:body:end -->";
3
+ export declare function wrapBody(body: string): string;
@@ -0,0 +1,5 @@
1
+ export const BODY_START_MARKER = '<!-- backlog-exporter:body:start -->';
2
+ export const BODY_END_MARKER = '<!-- backlog-exporter:body:end -->';
3
+ export function wrapBody(body) {
4
+ return `${BODY_START_MARKER}\n${body}\n${BODY_END_MARKER}`;
5
+ }
@@ -0,0 +1,4 @@
1
+ export interface Logger {
2
+ log(message: string): void;
3
+ warn(message: string): void;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare function ensureDirectory(directory: string): Promise<void>;
2
+ export declare function fileExists(filePath: string): Promise<boolean>;
3
+ export declare function writeMarkdownFile(filePath: string, content: string): Promise<void>;
4
+ export declare function deleteFile(filePath: string): Promise<void>;
5
+ export declare function assertDirectoryExists(directory: string): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export async function ensureDirectory(directory) {
4
+ await fs.mkdir(directory, { recursive: true });
5
+ }
6
+ export async function fileExists(filePath) {
7
+ return fs.access(filePath).then(() => true, () => false);
8
+ }
9
+ export async function writeMarkdownFile(filePath, content) {
10
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
11
+ await fs.writeFile(filePath, content);
12
+ }
13
+ export async function deleteFile(filePath) {
14
+ await fs.unlink(filePath);
15
+ }
16
+ export async function assertDirectoryExists(directory) {
17
+ const stats = await fs.stat(directory).catch(() => { });
18
+ if (!stats?.isDirectory()) {
19
+ throw new Error(`指定されたディレクトリが存在しません: ${directory}`);
20
+ }
21
+ }
@@ -0,0 +1 @@
1
+ export declare function appendLog(outputDir: string, message: string): Promise<void>;
@@ -1,10 +1,6 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- /**
4
- * 更新ログを記録する
5
- * @param outputDir 出力ディレクトリ
6
- * @param message ログメッセージ
7
- */
3
+ // backlog-update.logは監査用途のため、実行環境のロケールに依存せず日本語固定とする
8
4
  export async function appendLog(outputDir, message) {
9
5
  const logPath = path.join(outputDir, 'backlog-update.log');
10
6
  const timestamp = new Date().toLocaleString('ja-JP');
@@ -105,6 +105,74 @@
105
105
  "index.js"
106
106
  ]
107
107
  },
108
+ "document": {
109
+ "aliases": [],
110
+ "args": {},
111
+ "description": "Backlogからドキュメントを取得してMarkdownファイルとして保存する",
112
+ "examples": [
113
+ "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY\nドキュメントをMarkdownファイルとして保存する\n",
114
+ "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-documents\n指定したディレクトリにドキュメントを保存する\n",
115
+ "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --keyword 仕様書\nキーワード「仕様書」を含むドキュメントのみを取得する\n"
116
+ ],
117
+ "flags": {
118
+ "apiKey": {
119
+ "description": "Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)",
120
+ "name": "apiKey",
121
+ "required": false,
122
+ "hasDynamicHelp": false,
123
+ "multiple": false,
124
+ "type": "option"
125
+ },
126
+ "domain": {
127
+ "description": "Backlog domain (e.g. example.backlog.jp)",
128
+ "name": "domain",
129
+ "required": true,
130
+ "hasDynamicHelp": false,
131
+ "multiple": false,
132
+ "type": "option"
133
+ },
134
+ "keyword": {
135
+ "description": "検索キーワード",
136
+ "name": "keyword",
137
+ "required": false,
138
+ "hasDynamicHelp": false,
139
+ "multiple": false,
140
+ "type": "option"
141
+ },
142
+ "output": {
143
+ "char": "o",
144
+ "description": "出力ディレクトリパス",
145
+ "name": "output",
146
+ "required": false,
147
+ "hasDynamicHelp": false,
148
+ "multiple": false,
149
+ "type": "option"
150
+ },
151
+ "projectIdOrKey": {
152
+ "description": "Backlog project ID or key",
153
+ "name": "projectIdOrKey",
154
+ "required": true,
155
+ "hasDynamicHelp": false,
156
+ "multiple": false,
157
+ "type": "option"
158
+ }
159
+ },
160
+ "hasDynamicHelp": false,
161
+ "hiddenAliases": [],
162
+ "id": "document",
163
+ "pluginAlias": "backlog-exporter",
164
+ "pluginName": "backlog-exporter",
165
+ "pluginType": "core",
166
+ "strict": true,
167
+ "enableJsonFlag": false,
168
+ "isESM": true,
169
+ "relativePath": [
170
+ "dist",
171
+ "commands",
172
+ "document",
173
+ "index.js"
174
+ ]
175
+ },
108
176
  "issue": {
109
177
  "aliases": [],
110
178
  "args": {},
@@ -201,14 +269,20 @@
201
269
  "index.js"
202
270
  ]
203
271
  },
204
- "document": {
272
+ "prune": {
205
273
  "aliases": [],
206
- "args": {},
207
- "description": "Backlogからドキュメントを取得してMarkdownファイルとして保存する",
274
+ "args": {
275
+ "directory": {
276
+ "description": "対象ディレクトリ(設定ファイルが保存されている場所)",
277
+ "name": "directory",
278
+ "required": false
279
+ }
280
+ },
281
+ "description": "Backlog上で削除・移動された課題・ドキュメント・Wikiのローカルファイルを削除し、Backlogと同じ状態に揃える",
208
282
  "examples": [
209
- "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY\nドキュメントをMarkdownファイルとして保存する\n",
210
- "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-documents\n指定したディレクトリにドキュメントを保存する\n",
211
- "<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --keyword 仕様書\nキーワード「仕様書」を含むドキュメントのみを取得する\n"
283
+ "<%= config.bin %> <%= command.id %>\nカレントディレクトリ配下の設定ファイルを探索し、Backlog上に存在しないドキュメントファイルを削除する\n",
284
+ "<%= config.bin %> <%= command.id %> ./backlog-documents\n指定したディレクトリを対象にする\n",
285
+ "<%= config.bin %> <%= command.id %> --force\n確認プロンプトをスキップして削除する\n"
212
286
  ],
213
287
  "flags": {
214
288
  "apiKey": {
@@ -222,32 +296,23 @@
222
296
  "domain": {
223
297
  "description": "Backlog domain (e.g. example.backlog.jp)",
224
298
  "name": "domain",
225
- "required": true,
226
- "hasDynamicHelp": false,
227
- "multiple": false,
228
- "type": "option"
229
- },
230
- "keyword": {
231
- "description": "検索キーワード",
232
- "name": "keyword",
233
299
  "required": false,
234
300
  "hasDynamicHelp": false,
235
301
  "multiple": false,
236
302
  "type": "option"
237
303
  },
238
- "output": {
239
- "char": "o",
240
- "description": "出力ディレクトリパス",
241
- "name": "output",
304
+ "force": {
305
+ "char": "f",
306
+ "description": "確認プロンプトをスキップする",
307
+ "name": "force",
242
308
  "required": false,
243
- "hasDynamicHelp": false,
244
- "multiple": false,
245
- "type": "option"
309
+ "allowNo": false,
310
+ "type": "boolean"
246
311
  },
247
312
  "projectIdOrKey": {
248
313
  "description": "Backlog project ID or key",
249
314
  "name": "projectIdOrKey",
250
- "required": true,
315
+ "required": false,
251
316
  "hasDynamicHelp": false,
252
317
  "multiple": false,
253
318
  "type": "option"
@@ -255,7 +320,7 @@
255
320
  },
256
321
  "hasDynamicHelp": false,
257
322
  "hiddenAliases": [],
258
- "id": "document",
323
+ "id": "prune",
259
324
  "pluginAlias": "backlog-exporter",
260
325
  "pluginName": "backlog-exporter",
261
326
  "pluginType": "core",
@@ -265,7 +330,7 @@
265
330
  "relativePath": [
266
331
  "dist",
267
332
  "commands",
268
- "document",
333
+ "prune",
269
334
  "index.js"
270
335
  ]
271
336
  },
@@ -286,7 +351,10 @@
286
351
  "<%= config.bin %> <%= command.id %> ./my-project\n指定したディレクトリの設定を使用して更新する\n",
287
352
  "<%= config.bin %> <%= command.id %> --issueKeyFileName\nファイル名を課題キーにする\n",
288
353
  "<%= config.bin %> <%= command.id %> --issueKeyFolder\n課題キーでフォルダを作成する\n",
289
- "<%= config.bin %> <%= command.id %> --issueKeyFileName --issueKeyFolder\n課題キーでフォルダを作成し、ファイル名も課題キーにする\n"
354
+ "<%= config.bin %> <%= command.id %> --issueKeyFileName --issueKeyFolder\n課題キーでフォルダを作成し、ファイル名も課題キーにする\n",
355
+ "<%= config.bin %> <%= command.id %> --issueIdOrKey PROJECT-1,PROJECT-2\n指定した課題(IDまたはキー)のみを再取得する(全件差分更新は行わない)\n",
356
+ "<%= config.bin %> <%= command.id %> --wikiId 12345,12346\n指定したWiki(ID)のみを再取得する(全件差分更新は行わない)\n",
357
+ "<%= config.bin %> <%= command.id %> --documentId abc123,def456\n指定したドキュメント(ID)のみを再取得する(全件差分更新は行わない)\n"
290
358
  ],
291
359
  "flags": {
292
360
  "apiKey": {
@@ -297,6 +365,14 @@
297
365
  "multiple": false,
298
366
  "type": "option"
299
367
  },
368
+ "documentId": {
369
+ "description": "指定したドキュメント(ID)のみを再取得する(カンマ区切りで複数指定可能)",
370
+ "name": "documentId",
371
+ "required": false,
372
+ "hasDynamicHelp": false,
373
+ "multiple": false,
374
+ "type": "option"
375
+ },
300
376
  "documentsOnly": {
301
377
  "description": "ドキュメントのみを更新する",
302
378
  "name": "documentsOnly",
@@ -320,6 +396,14 @@
320
396
  "allowNo": false,
321
397
  "type": "boolean"
322
398
  },
399
+ "issueIdOrKey": {
400
+ "description": "指定した課題(IDまたはキー)のみを再取得する(カンマ区切りで複数指定可能)",
401
+ "name": "issueIdOrKey",
402
+ "required": false,
403
+ "hasDynamicHelp": false,
404
+ "multiple": false,
405
+ "type": "option"
406
+ },
323
407
  "issueKeyFileName": {
324
408
  "description": "ファイル名を課題キーにする",
325
409
  "name": "issueKeyFileName",
@@ -349,6 +433,14 @@
349
433
  "multiple": false,
350
434
  "type": "option"
351
435
  },
436
+ "wikiId": {
437
+ "description": "指定したWiki(ID)のみを再取得する(カンマ区切りで複数指定可能)",
438
+ "name": "wikiId",
439
+ "required": false,
440
+ "hasDynamicHelp": false,
441
+ "multiple": false,
442
+ "type": "option"
443
+ },
352
444
  "wikisOnly": {
353
445
  "description": "Wikiのみを更新する",
354
446
  "name": "wikisOnly",
@@ -433,5 +525,5 @@
433
525
  ]
434
526
  }
435
527
  },
436
- "version": "0.7.3"
528
+ "version": "1.0.0"
437
529
  }