@axis-ds/cli 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.
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/chunk-CGFESSCW.js +24 -0
- package/dist/chunk-D26GW2HO.js +873 -0
- package/dist/chunk-DYIVW3T5.js +873 -0
- package/dist/chunk-GCOWKKXG.js +202 -0
- package/dist/chunk-WRDOGNSG.js +173 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2501 -0
- package/dist/monet.d.ts +90 -0
- package/dist/monet.js +12 -0
- package/dist/registry-client-TPWFZ4HE.js +8 -0
- package/dist/sync/index.d.ts +339 -0
- package/dist/sync/index.js +24 -0
- package/dist/v0.d.ts +26 -0
- package/dist/v0.js +10 -0
- package/package.json +54 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
// src/sync/index.ts
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import path2 from "path";
|
|
5
|
+
import fs3 from "fs-extra";
|
|
6
|
+
|
|
7
|
+
// src/sync/parser.ts
|
|
8
|
+
import fs from "fs-extra";
|
|
9
|
+
var IN_PROGRESS_ROW_REGEX = /\|\s*(\d+)\s*\|\s*([^|]+)\|\s*\[?(WI-\d{4})\]?(?:\([^)]*\))?\s*\|\s*([^|]*)\|\s*([^|]*)\|\s*(✅|🔄|⏳)\s*\|/;
|
|
10
|
+
var COMPLETED_ROW_REGEX = /\|\s*(\d+)\s*\|\s*([^|]+)\|\s*\[?(WI-\d{4})\]?(?:\([^)]*\))?\s*\|\s*([^|]*)\|\s*([^|]*)\|/;
|
|
11
|
+
var SECTION_HEADER_REGEX = /^###\s+(진행 중|완료)/;
|
|
12
|
+
async function parseProjectTodo(filePath) {
|
|
13
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
14
|
+
return parseContent(content);
|
|
15
|
+
}
|
|
16
|
+
function parseContent(content) {
|
|
17
|
+
const lines = content.split("\n");
|
|
18
|
+
const inProgress = [];
|
|
19
|
+
const completed = [];
|
|
20
|
+
const errors = [];
|
|
21
|
+
let currentSection = "none";
|
|
22
|
+
let inTable = false;
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i];
|
|
25
|
+
const lineNumber = i + 1;
|
|
26
|
+
const sectionMatch = line.match(SECTION_HEADER_REGEX);
|
|
27
|
+
if (sectionMatch) {
|
|
28
|
+
currentSection = sectionMatch[1] === "\uC9C4\uD589 \uC911" ? "inProgress" : "completed";
|
|
29
|
+
inTable = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (line.trim().startsWith("| #") || line.trim().startsWith("|---")) {
|
|
33
|
+
inTable = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (line.trim() === "" || !line.trim().startsWith("|")) {
|
|
37
|
+
inTable = false;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (inTable && currentSection !== "none") {
|
|
41
|
+
try {
|
|
42
|
+
const workItem = parseTableRow(line, currentSection);
|
|
43
|
+
if (workItem) {
|
|
44
|
+
if (currentSection === "inProgress") {
|
|
45
|
+
inProgress.push(workItem);
|
|
46
|
+
} else {
|
|
47
|
+
completed.push(workItem);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (line.includes("WI-")) {
|
|
52
|
+
errors.push({
|
|
53
|
+
line: lineNumber,
|
|
54
|
+
content: line,
|
|
55
|
+
message: error.message
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { inProgress, completed, errors };
|
|
62
|
+
}
|
|
63
|
+
function parseTableRow(line, section) {
|
|
64
|
+
if (section === "inProgress") {
|
|
65
|
+
const match = line.match(IN_PROGRESS_ROW_REGEX);
|
|
66
|
+
if (!match) {
|
|
67
|
+
if (!line.includes("WI-")) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
throw new Error("\uC9C4\uD589 \uC911 \uD14C\uC774\uBE14 \uD589 \uD30C\uC2F1 \uC2E4\uD328");
|
|
71
|
+
}
|
|
72
|
+
const [, index, title, wiId, phase, priority, status] = match;
|
|
73
|
+
const linkMatch = line.match(/\[WI-\d{4}\]\(([^)]+)\)/);
|
|
74
|
+
return {
|
|
75
|
+
index: parseInt(index, 10),
|
|
76
|
+
title: title.trim(),
|
|
77
|
+
wiId: wiId.trim(),
|
|
78
|
+
phase: phase.trim(),
|
|
79
|
+
priority: priority.trim(),
|
|
80
|
+
status: status.trim(),
|
|
81
|
+
link: linkMatch ? linkMatch[1] : void 0
|
|
82
|
+
};
|
|
83
|
+
} else {
|
|
84
|
+
const match = line.match(COMPLETED_ROW_REGEX);
|
|
85
|
+
if (!match) {
|
|
86
|
+
if (!line.includes("WI-")) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
throw new Error("\uC644\uB8CC \uD14C\uC774\uBE14 \uD589 \uD30C\uC2F1 \uC2E4\uD328");
|
|
90
|
+
}
|
|
91
|
+
const [, index, title, wiId, phase] = match;
|
|
92
|
+
const linkMatch = line.match(/\[WI-\d{4}\]\(([^)]+)\)/);
|
|
93
|
+
return {
|
|
94
|
+
index: parseInt(index, 10),
|
|
95
|
+
title: title.trim(),
|
|
96
|
+
wiId: wiId.trim(),
|
|
97
|
+
phase: phase.trim(),
|
|
98
|
+
priority: "",
|
|
99
|
+
status: "\u2705",
|
|
100
|
+
link: linkMatch ? linkMatch[1] : void 0
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function extractAllWorkItems(result) {
|
|
105
|
+
const allItems = [...result.inProgress, ...result.completed];
|
|
106
|
+
const seen = /* @__PURE__ */ new Set();
|
|
107
|
+
return allItems.filter((item) => {
|
|
108
|
+
if (seen.has(item.wiId)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
seen.add(item.wiId);
|
|
112
|
+
return true;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function filterByWiIds(items, wiIds) {
|
|
116
|
+
const targetSet = new Set(wiIds.map((id) => id.toUpperCase()));
|
|
117
|
+
return items.filter((item) => targetSet.has(item.wiId.toUpperCase()));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/sync/github-client.ts
|
|
121
|
+
import { execSync } from "child_process";
|
|
122
|
+
var GITHUB_GRAPHQL_URL = "https://api.github.com/graphql";
|
|
123
|
+
var GitHubClient = class {
|
|
124
|
+
token;
|
|
125
|
+
owner;
|
|
126
|
+
repo;
|
|
127
|
+
constructor(owner, repo, token) {
|
|
128
|
+
this.owner = owner;
|
|
129
|
+
this.repo = repo;
|
|
130
|
+
this.token = token || this.getTokenFromGhCli();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* gh CLI에서 토큰 가져오기
|
|
134
|
+
*/
|
|
135
|
+
getTokenFromGhCli() {
|
|
136
|
+
try {
|
|
137
|
+
const token = execSync("gh auth token", {
|
|
138
|
+
encoding: "utf-8",
|
|
139
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
140
|
+
}).trim();
|
|
141
|
+
if (!token) {
|
|
142
|
+
throw new Error("\uD1A0\uD070\uC774 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4.");
|
|
143
|
+
}
|
|
144
|
+
return token;
|
|
145
|
+
} catch {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"GitHub \uD1A0\uD070\uC744 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. gh CLI \uC124\uCE58 \uBC0F \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.\n \uC124\uCE58: https://cli.github.com\n \uC778\uC99D: gh auth login"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* GraphQL 요청 실행
|
|
153
|
+
*/
|
|
154
|
+
async graphql(query, variables) {
|
|
155
|
+
const response = await fetch(GITHUB_GRAPHQL_URL, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
"Authorization": `Bearer ${this.token}`,
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
"Accept": "application/json"
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify({ query, variables })
|
|
163
|
+
});
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
throw new Error(`GitHub API \uC624\uB958: ${response.status} ${response.statusText}`);
|
|
166
|
+
}
|
|
167
|
+
const result = await response.json();
|
|
168
|
+
if (result.errors && result.errors.length > 0) {
|
|
169
|
+
throw new Error(`GraphQL \uC624\uB958: ${result.errors.map((e) => e.message).join(", ")}`);
|
|
170
|
+
}
|
|
171
|
+
return result.data;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Project V2 정보 조회
|
|
175
|
+
*/
|
|
176
|
+
async getProjectInfo(projectNumber) {
|
|
177
|
+
const query = `
|
|
178
|
+
query GetProject($owner: String!, $repo: String!, $number: Int!) {
|
|
179
|
+
repository(owner: $owner, name: $repo) {
|
|
180
|
+
projectV2(number: $number) {
|
|
181
|
+
id
|
|
182
|
+
title
|
|
183
|
+
url
|
|
184
|
+
field(name: "Status") {
|
|
185
|
+
... on ProjectV2SingleSelectField {
|
|
186
|
+
id
|
|
187
|
+
options {
|
|
188
|
+
id
|
|
189
|
+
name
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
const data = await this.graphql(query, {
|
|
198
|
+
owner: this.owner,
|
|
199
|
+
repo: this.repo,
|
|
200
|
+
number: projectNumber
|
|
201
|
+
});
|
|
202
|
+
const project = data.repository.projectV2;
|
|
203
|
+
const statusField = project.field;
|
|
204
|
+
const options = {
|
|
205
|
+
todo: "",
|
|
206
|
+
inProgress: "",
|
|
207
|
+
done: ""
|
|
208
|
+
};
|
|
209
|
+
for (const opt of statusField.options) {
|
|
210
|
+
const nameLower = opt.name.toLowerCase();
|
|
211
|
+
if (nameLower === "todo" || nameLower === "to do") {
|
|
212
|
+
options.todo = opt.id;
|
|
213
|
+
} else if (nameLower === "in progress" || nameLower === "in-progress") {
|
|
214
|
+
options.inProgress = opt.id;
|
|
215
|
+
} else if (nameLower === "done" || nameLower === "completed") {
|
|
216
|
+
options.done = opt.id;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
id: project.id,
|
|
221
|
+
title: project.title,
|
|
222
|
+
url: project.url,
|
|
223
|
+
statusField: {
|
|
224
|
+
id: statusField.id,
|
|
225
|
+
options
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 프로젝트의 모든 아이템 조회
|
|
231
|
+
*/
|
|
232
|
+
async getProjectItems(projectId) {
|
|
233
|
+
const query = `
|
|
234
|
+
query GetProjectItems($projectId: ID!, $cursor: String) {
|
|
235
|
+
node(id: $projectId) {
|
|
236
|
+
... on ProjectV2 {
|
|
237
|
+
items(first: 100, after: $cursor) {
|
|
238
|
+
pageInfo {
|
|
239
|
+
hasNextPage
|
|
240
|
+
endCursor
|
|
241
|
+
}
|
|
242
|
+
nodes {
|
|
243
|
+
id
|
|
244
|
+
content {
|
|
245
|
+
... on Issue {
|
|
246
|
+
id
|
|
247
|
+
number
|
|
248
|
+
title
|
|
249
|
+
url
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
fieldValueByName(name: "Status") {
|
|
253
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
254
|
+
name
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
`;
|
|
263
|
+
const items = [];
|
|
264
|
+
let cursor = null;
|
|
265
|
+
let hasMore = true;
|
|
266
|
+
while (hasMore) {
|
|
267
|
+
const result = await this.graphql(query, {
|
|
268
|
+
projectId,
|
|
269
|
+
cursor
|
|
270
|
+
});
|
|
271
|
+
const itemsPage = result.node.items;
|
|
272
|
+
for (const node of itemsPage.nodes) {
|
|
273
|
+
items.push({
|
|
274
|
+
id: node.id,
|
|
275
|
+
content: node.content,
|
|
276
|
+
fieldValues: {
|
|
277
|
+
status: node.fieldValueByName?.name
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
hasMore = itemsPage.pageInfo.hasNextPage;
|
|
282
|
+
cursor = itemsPage.pageInfo.endCursor;
|
|
283
|
+
}
|
|
284
|
+
return items;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Issue 생성
|
|
288
|
+
*/
|
|
289
|
+
async createIssue(title, body, labels) {
|
|
290
|
+
const query = `
|
|
291
|
+
mutation CreateIssue($repositoryId: ID!, $title: String!, $body: String, $labelIds: [ID!]) {
|
|
292
|
+
createIssue(input: {
|
|
293
|
+
repositoryId: $repositoryId
|
|
294
|
+
title: $title
|
|
295
|
+
body: $body
|
|
296
|
+
labelIds: $labelIds
|
|
297
|
+
}) {
|
|
298
|
+
issue {
|
|
299
|
+
id
|
|
300
|
+
number
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
`;
|
|
305
|
+
const repoQuery = `
|
|
306
|
+
query GetRepoId($owner: String!, $repo: String!) {
|
|
307
|
+
repository(owner: $owner, name: $repo) {
|
|
308
|
+
id
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
`;
|
|
312
|
+
const repoData = await this.graphql(repoQuery, {
|
|
313
|
+
owner: this.owner,
|
|
314
|
+
repo: this.repo
|
|
315
|
+
});
|
|
316
|
+
const data = await this.graphql(query, {
|
|
317
|
+
repositoryId: repoData.repository.id,
|
|
318
|
+
title,
|
|
319
|
+
body,
|
|
320
|
+
labelIds: labels
|
|
321
|
+
});
|
|
322
|
+
return data.createIssue.issue;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Issue를 Project에 추가
|
|
326
|
+
*/
|
|
327
|
+
async addIssueToProject(projectId, issueId) {
|
|
328
|
+
const query = `
|
|
329
|
+
mutation AddToProject($projectId: ID!, $contentId: ID!) {
|
|
330
|
+
addProjectV2ItemById(input: {
|
|
331
|
+
projectId: $projectId
|
|
332
|
+
contentId: $contentId
|
|
333
|
+
}) {
|
|
334
|
+
item {
|
|
335
|
+
id
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
`;
|
|
340
|
+
const data = await this.graphql(query, {
|
|
341
|
+
projectId,
|
|
342
|
+
contentId: issueId
|
|
343
|
+
});
|
|
344
|
+
return data.addProjectV2ItemById.item.id;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Project Item의 Status 업데이트
|
|
348
|
+
*/
|
|
349
|
+
async updateItemStatus(projectId, itemId, fieldId, optionId) {
|
|
350
|
+
const query = `
|
|
351
|
+
mutation UpdateStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
352
|
+
updateProjectV2ItemFieldValue(input: {
|
|
353
|
+
projectId: $projectId
|
|
354
|
+
itemId: $itemId
|
|
355
|
+
fieldId: $fieldId
|
|
356
|
+
value: { singleSelectOptionId: $optionId }
|
|
357
|
+
}) {
|
|
358
|
+
projectV2Item {
|
|
359
|
+
id
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
`;
|
|
364
|
+
await this.graphql(query, {
|
|
365
|
+
projectId,
|
|
366
|
+
itemId,
|
|
367
|
+
fieldId,
|
|
368
|
+
optionId
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Issue 번호로 Issue 정보 조회
|
|
373
|
+
*/
|
|
374
|
+
async getIssue(issueNumber) {
|
|
375
|
+
const query = `
|
|
376
|
+
query GetIssue($owner: String!, $repo: String!, $number: Int!) {
|
|
377
|
+
repository(owner: $owner, name: $repo) {
|
|
378
|
+
issue(number: $number) {
|
|
379
|
+
id
|
|
380
|
+
title
|
|
381
|
+
state
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
`;
|
|
386
|
+
try {
|
|
387
|
+
const data = await this.graphql(query, {
|
|
388
|
+
owner: this.owner,
|
|
389
|
+
repo: this.repo,
|
|
390
|
+
number: issueNumber
|
|
391
|
+
});
|
|
392
|
+
return data.repository.issue;
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/sync/mapper.ts
|
|
400
|
+
import fs2 from "fs-extra";
|
|
401
|
+
import path from "path";
|
|
402
|
+
var DEFAULT_MAPPING = {
|
|
403
|
+
version: "1.0.0",
|
|
404
|
+
repository: {
|
|
405
|
+
owner: "thoughtandaction",
|
|
406
|
+
repo: "axis"
|
|
407
|
+
},
|
|
408
|
+
project: {
|
|
409
|
+
number: 4
|
|
410
|
+
},
|
|
411
|
+
mappings: [],
|
|
412
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
413
|
+
};
|
|
414
|
+
var WiMapper = class {
|
|
415
|
+
mappingPath;
|
|
416
|
+
mapping;
|
|
417
|
+
constructor(mappingPath) {
|
|
418
|
+
this.mappingPath = mappingPath;
|
|
419
|
+
this.mapping = { ...DEFAULT_MAPPING };
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* 매핑 파일 로드
|
|
423
|
+
*/
|
|
424
|
+
async load() {
|
|
425
|
+
if (await fs2.pathExists(this.mappingPath)) {
|
|
426
|
+
try {
|
|
427
|
+
this.mapping = await fs2.readJSON(this.mappingPath);
|
|
428
|
+
} catch {
|
|
429
|
+
console.warn("\uB9E4\uD551 \uD30C\uC77C \uB85C\uB4DC \uC2E4\uD328, \uAE30\uBCF8\uAC12 \uC0AC\uC6A9");
|
|
430
|
+
this.mapping = { ...DEFAULT_MAPPING };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* 매핑 파일 저장
|
|
436
|
+
*/
|
|
437
|
+
async save() {
|
|
438
|
+
this.mapping.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
439
|
+
await fs2.ensureDir(path.dirname(this.mappingPath));
|
|
440
|
+
await fs2.writeJSON(this.mappingPath, this.mapping, { spaces: 2 });
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 저장소 정보 반환
|
|
444
|
+
*/
|
|
445
|
+
getRepository() {
|
|
446
|
+
return this.mapping.repository;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* 프로젝트 번호 반환
|
|
450
|
+
*/
|
|
451
|
+
getProjectNumber() {
|
|
452
|
+
return this.mapping.project.number;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* 프로젝트 ID 설정
|
|
456
|
+
*/
|
|
457
|
+
setProjectId(id) {
|
|
458
|
+
this.mapping.project.id = id;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* WI ID로 매핑 찾기
|
|
462
|
+
*/
|
|
463
|
+
findByWiId(wiId) {
|
|
464
|
+
return this.mapping.mappings.find(
|
|
465
|
+
(m) => m.wiId.toUpperCase() === wiId.toUpperCase()
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Issue 번호로 매핑 찾기
|
|
470
|
+
*/
|
|
471
|
+
findByIssueNumber(issueNumber) {
|
|
472
|
+
return this.mapping.mappings.find((m) => m.issueNumber === issueNumber);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* 매핑 추가 또는 업데이트
|
|
476
|
+
*/
|
|
477
|
+
upsertMapping(wiId, issueNumber, projectItemId, status) {
|
|
478
|
+
const existing = this.findByWiId(wiId);
|
|
479
|
+
if (existing) {
|
|
480
|
+
existing.issueNumber = issueNumber;
|
|
481
|
+
if (projectItemId) {
|
|
482
|
+
existing.projectItemId = projectItemId;
|
|
483
|
+
}
|
|
484
|
+
existing.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
485
|
+
if (status) {
|
|
486
|
+
existing.lastStatus = status;
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
this.mapping.mappings.push({
|
|
490
|
+
wiId: wiId.toUpperCase(),
|
|
491
|
+
issueNumber,
|
|
492
|
+
projectItemId,
|
|
493
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
494
|
+
lastStatus: status
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* 모든 매핑 반환
|
|
500
|
+
*/
|
|
501
|
+
getAllMappings() {
|
|
502
|
+
return [...this.mapping.mappings];
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* GitHub 프로젝트 아이템에서 WI ID 추출
|
|
506
|
+
*/
|
|
507
|
+
extractWiIdFromItem(item) {
|
|
508
|
+
const title = item.content?.title || "";
|
|
509
|
+
const match = title.match(/\[?(WI-\d{4})\]?/i);
|
|
510
|
+
return match ? match[1].toUpperCase() : null;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* 프로젝트 아이템으로 매핑 동기화
|
|
514
|
+
*/
|
|
515
|
+
syncFromProjectItems(items) {
|
|
516
|
+
for (const item of items) {
|
|
517
|
+
if (!item.content) continue;
|
|
518
|
+
const wiId = this.extractWiIdFromItem(item);
|
|
519
|
+
if (wiId) {
|
|
520
|
+
const existing = this.findByWiId(wiId);
|
|
521
|
+
if (existing) {
|
|
522
|
+
existing.projectItemId = item.id;
|
|
523
|
+
} else {
|
|
524
|
+
this.mapping.mappings.push({
|
|
525
|
+
wiId,
|
|
526
|
+
issueNumber: item.content.number,
|
|
527
|
+
projectItemId: item.id,
|
|
528
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
function generateIssueTitle(workItem) {
|
|
536
|
+
return `[${workItem.wiId}] ${workItem.title}`;
|
|
537
|
+
}
|
|
538
|
+
function generateIssueBody(workItem) {
|
|
539
|
+
const lines = [
|
|
540
|
+
`## Work Item: ${workItem.wiId}`,
|
|
541
|
+
"",
|
|
542
|
+
`**\uC81C\uBAA9**: ${workItem.title}`,
|
|
543
|
+
`**Phase**: ${workItem.phase}`,
|
|
544
|
+
`**\uC6B0\uC120\uC21C\uC704**: ${workItem.priority}`,
|
|
545
|
+
""
|
|
546
|
+
];
|
|
547
|
+
if (workItem.link) {
|
|
548
|
+
lines.push(`**WI \uD3F4\uB354**: [\uBC14\uB85C\uAC00\uAE30](${workItem.link})`);
|
|
549
|
+
lines.push("");
|
|
550
|
+
}
|
|
551
|
+
lines.push("---");
|
|
552
|
+
lines.push("");
|
|
553
|
+
lines.push("> \uC774 Issue\uB294 `project-todo.md`\uC640 \uC790\uB3D9 \uB3D9\uAE30\uD654\uB429\uB2C8\uB2E4.");
|
|
554
|
+
return lines.join("\n");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/sync/types.ts
|
|
558
|
+
var STATUS_MAP = {
|
|
559
|
+
"\u2705": "Done",
|
|
560
|
+
"\u{1F504}": "In Progress",
|
|
561
|
+
"\u23F3": "Todo"
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// src/sync/index.ts
|
|
565
|
+
var statusMap = {
|
|
566
|
+
"\u2705": "Done",
|
|
567
|
+
"\u{1F504}": "In Progress",
|
|
568
|
+
"\u23F3": "Todo"
|
|
569
|
+
};
|
|
570
|
+
var DEFAULT_TODO_PATH = "project-todo.md";
|
|
571
|
+
var DEFAULT_MAPPING_PATH = ".github/wi-mapping.json";
|
|
572
|
+
function registerSyncCommand(program) {
|
|
573
|
+
const syncCmd = program.command("sync").description("GitHub Project\uC640 project-todo.md \uB3D9\uAE30\uD654");
|
|
574
|
+
syncCmd.option("--dry-run", "\uBBF8\uB9AC\uBCF4\uAE30 \uBAA8\uB4DC (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C)").option("--wi <ids...>", "\uD2B9\uC815 WI\uB9CC \uB3D9\uAE30\uD654 (\uC608: WI-0001 WI-0002)").option("-v, --verbose", "\uC0C1\uC138 \uCD9C\uB825").option("--token <token>", "GitHub \uD1A0\uD070 (\uAE30\uBCF8: gh auth token \uC0AC\uC6A9)").action(async (options) => {
|
|
575
|
+
await runSync({
|
|
576
|
+
dryRun: options.dryRun,
|
|
577
|
+
wiIds: options.wi,
|
|
578
|
+
verbose: options.verbose,
|
|
579
|
+
token: options.token
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
syncCmd.command("status").description("\uD604\uC7AC \uB3D9\uAE30\uD654 \uC0C1\uD0DC \uD655\uC778").action(async () => {
|
|
583
|
+
await showSyncStatus();
|
|
584
|
+
});
|
|
585
|
+
syncCmd.command("init").description("\uB3D9\uAE30\uD654 \uC124\uC815 \uCD08\uAE30\uD654").option("--owner <owner>", "GitHub \uC800\uC7A5\uC18C \uC18C\uC720\uC790", "thoughtandaction").option("--repo <repo>", "GitHub \uC800\uC7A5\uC18C \uC774\uB984", "axis").option("--project <number>", "GitHub Project \uBC88\uD638", "4").action(async (options) => {
|
|
586
|
+
await initSync(options.owner, options.repo, parseInt(options.project, 10));
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
async function runSync(options) {
|
|
590
|
+
const spinner = ora();
|
|
591
|
+
const result = {
|
|
592
|
+
success: true,
|
|
593
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
594
|
+
summary: { total: 0, created: 0, updated: 0, skipped: 0, failed: 0 },
|
|
595
|
+
items: [],
|
|
596
|
+
errors: []
|
|
597
|
+
};
|
|
598
|
+
try {
|
|
599
|
+
spinner.start("project-todo.md \uD30C\uC2F1 \uC911...");
|
|
600
|
+
const todoPath = path2.resolve(DEFAULT_TODO_PATH);
|
|
601
|
+
if (!await fs3.pathExists(todoPath)) {
|
|
602
|
+
throw new Error(`project-todo.md\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${todoPath}`);
|
|
603
|
+
}
|
|
604
|
+
const parseResult = await parseProjectTodo(todoPath);
|
|
605
|
+
let workItems = extractAllWorkItems(parseResult);
|
|
606
|
+
if (options.wiIds && options.wiIds.length > 0) {
|
|
607
|
+
workItems = filterByWiIds(workItems, options.wiIds);
|
|
608
|
+
}
|
|
609
|
+
spinner.succeed(`${workItems.length}\uAC1C Work Item \uD30C\uC2F1 \uC644\uB8CC`);
|
|
610
|
+
if (workItems.length === 0) {
|
|
611
|
+
console.log(chalk.yellow("\n\uB3D9\uAE30\uD654\uD560 Work Item\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
spinner.start("\uB9E4\uD551 \uD30C\uC77C \uB85C\uB4DC \uC911...");
|
|
615
|
+
const mapper = new WiMapper(path2.resolve(DEFAULT_MAPPING_PATH));
|
|
616
|
+
await mapper.load();
|
|
617
|
+
spinner.succeed("\uB9E4\uD551 \uD30C\uC77C \uB85C\uB4DC \uC644\uB8CC");
|
|
618
|
+
spinner.start("GitHub \uC5F0\uACB0 \uC911...");
|
|
619
|
+
const { owner, repo } = mapper.getRepository();
|
|
620
|
+
const client = new GitHubClient(owner, repo, options.token);
|
|
621
|
+
spinner.succeed("GitHub \uC5F0\uACB0 \uC644\uB8CC");
|
|
622
|
+
spinner.start("GitHub Project \uC815\uBCF4 \uC870\uD68C \uC911...");
|
|
623
|
+
const projectNumber = mapper.getProjectNumber();
|
|
624
|
+
const projectInfo = await client.getProjectInfo(projectNumber);
|
|
625
|
+
mapper.setProjectId(projectInfo.id);
|
|
626
|
+
spinner.succeed(`Project: ${projectInfo.title}`);
|
|
627
|
+
spinner.start("\uD504\uB85C\uC81D\uD2B8 \uC544\uC774\uD15C \uC870\uD68C \uC911...");
|
|
628
|
+
const projectItems = await client.getProjectItems(projectInfo.id);
|
|
629
|
+
mapper.syncFromProjectItems(projectItems);
|
|
630
|
+
spinner.succeed(`${projectItems.length}\uAC1C \uD504\uB85C\uC81D\uD2B8 \uC544\uC774\uD15C \uC870\uD68C \uC644\uB8CC`);
|
|
631
|
+
console.log(chalk.blue("\n\u{1F4E6} \uB3D9\uAE30\uD654 \uC2DC\uC791\n"));
|
|
632
|
+
if (options.dryRun) {
|
|
633
|
+
console.log(chalk.yellow("\u{1F50D} Dry Run \uBAA8\uB4DC - \uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C\n"));
|
|
634
|
+
}
|
|
635
|
+
result.summary.total = workItems.length;
|
|
636
|
+
for (const workItem of workItems) {
|
|
637
|
+
const itemResult = await syncWorkItem(
|
|
638
|
+
workItem,
|
|
639
|
+
mapper,
|
|
640
|
+
client,
|
|
641
|
+
projectInfo,
|
|
642
|
+
options
|
|
643
|
+
);
|
|
644
|
+
result.items.push(itemResult);
|
|
645
|
+
switch (itemResult.action) {
|
|
646
|
+
case "create":
|
|
647
|
+
result.summary.created++;
|
|
648
|
+
break;
|
|
649
|
+
case "update":
|
|
650
|
+
result.summary.updated++;
|
|
651
|
+
break;
|
|
652
|
+
case "skip":
|
|
653
|
+
result.summary.skipped++;
|
|
654
|
+
break;
|
|
655
|
+
case "error":
|
|
656
|
+
result.summary.failed++;
|
|
657
|
+
result.errors.push(itemResult.error || "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958");
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
printItemResult(itemResult, options.verbose);
|
|
661
|
+
}
|
|
662
|
+
if (!options.dryRun) {
|
|
663
|
+
spinner.start("\uB9E4\uD551 \uD30C\uC77C \uC800\uC7A5 \uC911...");
|
|
664
|
+
await mapper.save();
|
|
665
|
+
spinner.succeed("\uB9E4\uD551 \uD30C\uC77C \uC800\uC7A5 \uC644\uB8CC");
|
|
666
|
+
}
|
|
667
|
+
printSummary(result);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
spinner.fail(`\uC624\uB958 \uBC1C\uC0DD: ${error.message}`);
|
|
670
|
+
result.success = false;
|
|
671
|
+
result.errors.push(error.message);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
async function syncWorkItem(workItem, mapper, client, projectInfo, options) {
|
|
675
|
+
const mapping = mapper.findByWiId(workItem.wiId);
|
|
676
|
+
const targetStatus = statusMap[workItem.status];
|
|
677
|
+
const statusOptionId = targetStatus === "Done" ? projectInfo.statusField.options.done : targetStatus === "In Progress" ? projectInfo.statusField.options.inProgress : projectInfo.statusField.options.todo;
|
|
678
|
+
try {
|
|
679
|
+
if (!mapping) {
|
|
680
|
+
if (options.dryRun) {
|
|
681
|
+
return {
|
|
682
|
+
wiId: workItem.wiId,
|
|
683
|
+
action: "create",
|
|
684
|
+
newStatus: workItem.status
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
const issue = await client.createIssue(
|
|
688
|
+
generateIssueTitle(workItem),
|
|
689
|
+
generateIssueBody(workItem)
|
|
690
|
+
);
|
|
691
|
+
const itemId = await client.addIssueToProject(projectInfo.id, issue.id);
|
|
692
|
+
await client.updateItemStatus(
|
|
693
|
+
projectInfo.id,
|
|
694
|
+
itemId,
|
|
695
|
+
projectInfo.statusField.id,
|
|
696
|
+
statusOptionId
|
|
697
|
+
);
|
|
698
|
+
mapper.upsertMapping(workItem.wiId, issue.number, itemId, workItem.status);
|
|
699
|
+
return {
|
|
700
|
+
wiId: workItem.wiId,
|
|
701
|
+
action: "create",
|
|
702
|
+
newStatus: workItem.status,
|
|
703
|
+
issueNumber: issue.number,
|
|
704
|
+
projectItemId: itemId
|
|
705
|
+
};
|
|
706
|
+
} else {
|
|
707
|
+
const previousStatus = mapping.lastStatus;
|
|
708
|
+
if (previousStatus === workItem.status) {
|
|
709
|
+
return {
|
|
710
|
+
wiId: workItem.wiId,
|
|
711
|
+
action: "skip",
|
|
712
|
+
previousStatus,
|
|
713
|
+
newStatus: workItem.status,
|
|
714
|
+
issueNumber: mapping.issueNumber,
|
|
715
|
+
projectItemId: mapping.projectItemId
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
if (options.dryRun) {
|
|
719
|
+
return {
|
|
720
|
+
wiId: workItem.wiId,
|
|
721
|
+
action: "update",
|
|
722
|
+
previousStatus,
|
|
723
|
+
newStatus: workItem.status,
|
|
724
|
+
issueNumber: mapping.issueNumber
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
let itemId = mapping.projectItemId;
|
|
728
|
+
if (!itemId) {
|
|
729
|
+
const issue = await client.getIssue(mapping.issueNumber);
|
|
730
|
+
if (issue) {
|
|
731
|
+
itemId = await client.addIssueToProject(projectInfo.id, issue.id);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (itemId) {
|
|
735
|
+
await client.updateItemStatus(
|
|
736
|
+
projectInfo.id,
|
|
737
|
+
itemId,
|
|
738
|
+
projectInfo.statusField.id,
|
|
739
|
+
statusOptionId
|
|
740
|
+
);
|
|
741
|
+
mapper.upsertMapping(workItem.wiId, mapping.issueNumber, itemId, workItem.status);
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
wiId: workItem.wiId,
|
|
745
|
+
action: "update",
|
|
746
|
+
previousStatus,
|
|
747
|
+
newStatus: workItem.status,
|
|
748
|
+
issueNumber: mapping.issueNumber,
|
|
749
|
+
projectItemId: itemId
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
} catch (error) {
|
|
753
|
+
return {
|
|
754
|
+
wiId: workItem.wiId,
|
|
755
|
+
action: "error",
|
|
756
|
+
newStatus: workItem.status,
|
|
757
|
+
error: error.message
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function printItemResult(result, verbose) {
|
|
762
|
+
const statusIcons = {
|
|
763
|
+
create: chalk.green("+"),
|
|
764
|
+
update: chalk.yellow("~"),
|
|
765
|
+
skip: chalk.gray("-"),
|
|
766
|
+
error: chalk.red("\u2717")
|
|
767
|
+
};
|
|
768
|
+
const icon = statusIcons[result.action];
|
|
769
|
+
const wiId = chalk.cyan(result.wiId);
|
|
770
|
+
const status = result.newStatus;
|
|
771
|
+
let message = `${icon} ${wiId} ${status}`;
|
|
772
|
+
if (result.action === "update" && result.previousStatus) {
|
|
773
|
+
message += chalk.gray(` (${result.previousStatus} \u2192 ${result.newStatus})`);
|
|
774
|
+
}
|
|
775
|
+
if (result.action === "create" && result.issueNumber) {
|
|
776
|
+
message += chalk.gray(` \u2192 Issue #${result.issueNumber}`);
|
|
777
|
+
}
|
|
778
|
+
if (result.action === "error") {
|
|
779
|
+
message += chalk.red(` - ${result.error}`);
|
|
780
|
+
}
|
|
781
|
+
console.log(` ${message}`);
|
|
782
|
+
}
|
|
783
|
+
function printSummary(result) {
|
|
784
|
+
console.log(chalk.blue("\n\u{1F4CA} \uB3D9\uAE30\uD654 \uACB0\uACFC\n"));
|
|
785
|
+
console.log(` \uC804\uCCB4: ${result.summary.total}`);
|
|
786
|
+
console.log(chalk.green(` \uC0DD\uC131: ${result.summary.created}`));
|
|
787
|
+
console.log(chalk.yellow(` \uC5C5\uB370\uC774\uD2B8: ${result.summary.updated}`));
|
|
788
|
+
console.log(chalk.gray(` \uC2A4\uD0B5: ${result.summary.skipped}`));
|
|
789
|
+
if (result.summary.failed > 0) {
|
|
790
|
+
console.log(chalk.red(` \uC2E4\uD328: ${result.summary.failed}`));
|
|
791
|
+
}
|
|
792
|
+
console.log();
|
|
793
|
+
}
|
|
794
|
+
async function showSyncStatus() {
|
|
795
|
+
const spinner = ora();
|
|
796
|
+
try {
|
|
797
|
+
spinner.start("\uC0C1\uD0DC \uD655\uC778 \uC911...");
|
|
798
|
+
const mapper = new WiMapper(path2.resolve(DEFAULT_MAPPING_PATH));
|
|
799
|
+
await mapper.load();
|
|
800
|
+
const todoPath = path2.resolve(DEFAULT_TODO_PATH);
|
|
801
|
+
if (!await fs3.pathExists(todoPath)) {
|
|
802
|
+
throw new Error(`project-todo.md\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${todoPath}`);
|
|
803
|
+
}
|
|
804
|
+
const parseResult = await parseProjectTodo(todoPath);
|
|
805
|
+
const workItems = extractAllWorkItems(parseResult);
|
|
806
|
+
spinner.succeed("\uC0C1\uD0DC \uD655\uC778 \uC644\uB8CC");
|
|
807
|
+
console.log(chalk.blue("\n\u{1F4CB} \uB3D9\uAE30\uD654 \uC0C1\uD0DC\n"));
|
|
808
|
+
const { owner, repo } = mapper.getRepository();
|
|
809
|
+
const projectNumber = mapper.getProjectNumber();
|
|
810
|
+
console.log(chalk.gray(`\uC800\uC7A5\uC18C: ${owner}/${repo}`));
|
|
811
|
+
console.log(chalk.gray(`\uD504\uB85C\uC81D\uD2B8: #${projectNumber}`));
|
|
812
|
+
console.log();
|
|
813
|
+
console.log(chalk.bold("Work Items:"));
|
|
814
|
+
let synced = 0;
|
|
815
|
+
let unsynced = 0;
|
|
816
|
+
for (const wi of workItems) {
|
|
817
|
+
const mapping = mapper.findByWiId(wi.wiId);
|
|
818
|
+
const icon = mapping ? chalk.green("\u2713") : chalk.yellow("\u25CB");
|
|
819
|
+
const issueInfo = mapping ? chalk.gray(` \u2192 #${mapping.issueNumber}`) : chalk.yellow(" (\uBBF8\uB3D9\uAE30\uD654)");
|
|
820
|
+
console.log(` ${icon} ${chalk.cyan(wi.wiId)} ${wi.status} ${wi.title}${issueInfo}`);
|
|
821
|
+
if (mapping) {
|
|
822
|
+
synced++;
|
|
823
|
+
} else {
|
|
824
|
+
unsynced++;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
console.log();
|
|
828
|
+
console.log(chalk.gray(`\uB3D9\uAE30\uD654\uB428: ${synced}, \uBBF8\uB3D9\uAE30\uD654: ${unsynced}`));
|
|
829
|
+
console.log();
|
|
830
|
+
} catch (error) {
|
|
831
|
+
spinner.fail(`\uC624\uB958: ${error.message}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async function initSync(owner, repo, projectNumber) {
|
|
835
|
+
const spinner = ora();
|
|
836
|
+
try {
|
|
837
|
+
spinner.start("\uC124\uC815 \uCD08\uAE30\uD654 \uC911...");
|
|
838
|
+
const mappingPath = path2.resolve(DEFAULT_MAPPING_PATH);
|
|
839
|
+
const mapping = {
|
|
840
|
+
version: "1.0.0",
|
|
841
|
+
repository: { owner, repo },
|
|
842
|
+
project: { number: projectNumber },
|
|
843
|
+
mappings: [],
|
|
844
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
845
|
+
};
|
|
846
|
+
await fs3.ensureDir(path2.dirname(mappingPath));
|
|
847
|
+
await fs3.writeJSON(mappingPath, mapping, { spaces: 2 });
|
|
848
|
+
spinner.succeed("\uC124\uC815 \uCD08\uAE30\uD654 \uC644\uB8CC");
|
|
849
|
+
console.log(chalk.blue("\n\u2713 \uB3D9\uAE30\uD654 \uC124\uC815 \uC644\uB8CC\n"));
|
|
850
|
+
console.log(chalk.gray(` \uB9E4\uD551 \uD30C\uC77C: ${DEFAULT_MAPPING_PATH}`));
|
|
851
|
+
console.log(chalk.gray(` \uC800\uC7A5\uC18C: ${owner}/${repo}`));
|
|
852
|
+
console.log(chalk.gray(` \uD504\uB85C\uC81D\uD2B8: #${projectNumber}`));
|
|
853
|
+
console.log();
|
|
854
|
+
console.log(chalk.cyan("\uB2E4\uC74C \uBA85\uB839\uC5B4\uB85C \uB3D9\uAE30\uD654\uB97C \uC2DC\uC791\uD558\uC138\uC694:"));
|
|
855
|
+
console.log(chalk.cyan(" npx axis-cli sync"));
|
|
856
|
+
console.log();
|
|
857
|
+
} catch (error) {
|
|
858
|
+
spinner.fail(`\uC624\uB958: ${error.message}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
export {
|
|
863
|
+
parseProjectTodo,
|
|
864
|
+
parseContent,
|
|
865
|
+
extractAllWorkItems,
|
|
866
|
+
filterByWiIds,
|
|
867
|
+
GitHubClient,
|
|
868
|
+
WiMapper,
|
|
869
|
+
generateIssueTitle,
|
|
870
|
+
generateIssueBody,
|
|
871
|
+
STATUS_MAP,
|
|
872
|
+
registerSyncCommand
|
|
873
|
+
};
|