@bifos/dooray-cli 0.4.0 → 0.5.2
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/README.md +16 -6
- package/dist/index.js +281 -173
- package/package.json +2 -1
- package/skills/dooray-cli/SKILL.md +224 -0
package/README.md
CHANGED
|
@@ -34,8 +34,9 @@ dooray doctor
|
|
|
34
34
|
### 프로젝트
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
dooray project list # 프로젝트 목록
|
|
37
|
+
dooray project list # 프로젝트 목록 (기본: public)
|
|
38
38
|
dooray project list --search ocr # 코드로 검색
|
|
39
|
+
dooray project list --type private # 개인 프로젝트 목록
|
|
39
40
|
dooray project members tc-ocr # 멤버 목록
|
|
40
41
|
dooray project workflows tc-ocr # 워크플로우 목록
|
|
41
42
|
```
|
|
@@ -53,10 +54,13 @@ dooray post get tc-ocr 42 --json # JSON 출력
|
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
56
|
dooray post create tc-ocr \
|
|
56
|
-
--
|
|
57
|
+
--title "업무 제목" \
|
|
57
58
|
--body "본문 마크다운" \
|
|
58
59
|
--to "담당자이름" \
|
|
59
60
|
--priority normal
|
|
61
|
+
|
|
62
|
+
# 본문을 파일에서 읽기 (--body와 --body-file은 동시 사용 불가)
|
|
63
|
+
dooray post create tc-ocr --title "업무 제목" --body-file ./content.md
|
|
60
64
|
```
|
|
61
65
|
|
|
62
66
|
### 업무 수정
|
|
@@ -66,7 +70,10 @@ dooray post create tc-ocr \
|
|
|
66
70
|
dooray post edit tc-ocr 42
|
|
67
71
|
|
|
68
72
|
# 비대화형 (AI 에이전트 친화)
|
|
69
|
-
dooray post edit tc-ocr 42 --
|
|
73
|
+
dooray post edit tc-ocr 42 --title "새 제목" --body "새 본문"
|
|
74
|
+
|
|
75
|
+
# 본문을 파일에서 읽기
|
|
76
|
+
dooray post edit tc-ocr 42 --body-file ./updated.md
|
|
70
77
|
```
|
|
71
78
|
|
|
72
79
|
### 댓글
|
|
@@ -74,6 +81,7 @@ dooray post edit tc-ocr 42 --subject "새 제목" --body "새 본문"
|
|
|
74
81
|
```bash
|
|
75
82
|
dooray post comment list tc-ocr 42
|
|
76
83
|
dooray post comment add tc-ocr 42 --body "댓글 내용"
|
|
84
|
+
dooray post comment add tc-ocr 42 --body-file ./comment.md
|
|
77
85
|
```
|
|
78
86
|
|
|
79
87
|
### 상태 변경
|
|
@@ -89,8 +97,10 @@ dooray post workflow tc-ocr 42 "진행 중" # 워크플로우 변경
|
|
|
89
97
|
dooray wiki list # 위키 목록
|
|
90
98
|
dooray wiki pages tc-ocr # 페이지 목록
|
|
91
99
|
dooray wiki page get tc-ocr <page-id> # 페이지 상세
|
|
92
|
-
dooray wiki page create tc-ocr
|
|
93
|
-
dooray wiki page edit tc-ocr <page-id>
|
|
100
|
+
dooray wiki page create tc-ocr --title "..." [--parent <page-id>] [--body "..." | --body-file <path>]
|
|
101
|
+
dooray wiki page edit tc-ocr <page-id> --title "새 제목" # 제목만 (비대화형)
|
|
102
|
+
dooray wiki page edit tc-ocr <page-id> --body "..." | --body-file <path> # 본문만 (비대화형)
|
|
103
|
+
dooray wiki page edit tc-ocr <page-id> # $EDITOR (플래그 없을 때)
|
|
94
104
|
```
|
|
95
105
|
|
|
96
106
|
### 메일
|
|
@@ -166,7 +176,7 @@ cp -r skills/dooray-cli ~/.claude/skills/
|
|
|
166
176
|
|
|
167
177
|
## 캐시
|
|
168
178
|
|
|
169
|
-
프로젝트, 멤버,
|
|
179
|
+
프로젝트, 멤버, 워크플로우, 위키 정보는 `~/.dooray/cache/`에 캐시됩니다.
|
|
170
180
|
|
|
171
181
|
```bash
|
|
172
182
|
dooray cache clear # 캐시 삭제
|
package/dist/index.js
CHANGED
|
@@ -195,23 +195,25 @@ var import_node_os2 = require("os");
|
|
|
195
195
|
var CACHE_DIR = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".dooray", "cache");
|
|
196
196
|
var ME_PATH = (0, import_node_path2.join)(CACHE_DIR, "me.json");
|
|
197
197
|
var PROJECTS_PATH = (0, import_node_path2.join)(CACHE_DIR, "projects.json");
|
|
198
|
+
var PROJECTS_PRIVATE_PATH = (0, import_node_path2.join)(CACHE_DIR, "projects-private.json");
|
|
198
199
|
var MEMBERS_DIR = (0, import_node_path2.join)(CACHE_DIR, "members");
|
|
199
200
|
var WORKFLOWS_DIR = (0, import_node_path2.join)(CACHE_DIR, "workflows");
|
|
201
|
+
var WIKIS_PATH = (0, import_node_path2.join)(CACHE_DIR, "wikis.json");
|
|
200
202
|
async function ensureDir2(dir) {
|
|
201
203
|
await (0, import_promises2.mkdir)(dir, { recursive: true });
|
|
202
204
|
}
|
|
203
|
-
async function readJson(
|
|
205
|
+
async function readJson(path3) {
|
|
204
206
|
try {
|
|
205
|
-
const raw = await (0, import_promises2.readFile)(
|
|
207
|
+
const raw = await (0, import_promises2.readFile)(path3, "utf-8");
|
|
206
208
|
return JSON.parse(raw);
|
|
207
209
|
} catch {
|
|
208
210
|
return null;
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
|
-
async function writeJson(
|
|
212
|
-
const dir =
|
|
213
|
+
async function writeJson(path3, data) {
|
|
214
|
+
const dir = path3.substring(0, path3.lastIndexOf("/"));
|
|
213
215
|
await ensureDir2(dir);
|
|
214
|
-
await (0, import_promises2.writeFile)(
|
|
216
|
+
await (0, import_promises2.writeFile)(path3, JSON.stringify(data, null, 2) + "\n");
|
|
215
217
|
}
|
|
216
218
|
function isExpired(updatedAt, ttlMs) {
|
|
217
219
|
if (!updatedAt) return true;
|
|
@@ -232,6 +234,12 @@ async function getProjects() {
|
|
|
232
234
|
async function setProjects(items) {
|
|
233
235
|
await writeJson(PROJECTS_PATH, { updatedAt: now(), data: items });
|
|
234
236
|
}
|
|
237
|
+
async function getPrivateProjects() {
|
|
238
|
+
return readJson(PROJECTS_PRIVATE_PATH);
|
|
239
|
+
}
|
|
240
|
+
async function setPrivateProjects(items) {
|
|
241
|
+
await writeJson(PROJECTS_PRIVATE_PATH, { updatedAt: now(), data: items });
|
|
242
|
+
}
|
|
235
243
|
function membersPath(projectId) {
|
|
236
244
|
return (0, import_node_path2.join)(MEMBERS_DIR, `${projectId}.json`);
|
|
237
245
|
}
|
|
@@ -250,6 +258,12 @@ async function getWorkflows(projectId) {
|
|
|
250
258
|
async function setWorkflows(projectId, items) {
|
|
251
259
|
await writeJson(workflowsPath(projectId), { updatedAt: now(), data: items });
|
|
252
260
|
}
|
|
261
|
+
async function getWikis() {
|
|
262
|
+
return readJson(WIKIS_PATH);
|
|
263
|
+
}
|
|
264
|
+
async function setWikis(items) {
|
|
265
|
+
await writeJson(WIKIS_PATH, { updatedAt: now(), data: items });
|
|
266
|
+
}
|
|
253
267
|
async function clearCache() {
|
|
254
268
|
try {
|
|
255
269
|
await (0, import_promises2.rm)(CACHE_DIR, { recursive: true, force: true });
|
|
@@ -289,11 +303,24 @@ cacheCommand.command("refresh").description("\uCE90\uC2DC \uAC31\uC2E0 (API \uD0
|
|
|
289
303
|
// src/commands/doctor.ts
|
|
290
304
|
var import_commander3 = require("commander");
|
|
291
305
|
var import_chalk3 = __toESM(require("chalk"));
|
|
306
|
+
var import_path = __toESM(require("path"));
|
|
307
|
+
var import_promises4 = __toESM(require("fs/promises"));
|
|
292
308
|
|
|
293
309
|
// src/api/client.ts
|
|
294
310
|
var import_ky = __toESM(require("ky"));
|
|
295
311
|
var import_promises3 = require("fs/promises");
|
|
296
312
|
var import_node_path3 = require("path");
|
|
313
|
+
|
|
314
|
+
// src/utils/dooray-message.ts
|
|
315
|
+
function normalizeDoorayMessage(raw) {
|
|
316
|
+
try {
|
|
317
|
+
return decodeURIComponent(raw.replace(/\+/g, " "));
|
|
318
|
+
} catch {
|
|
319
|
+
return raw;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/api/client.ts
|
|
297
324
|
function joinIds(ids) {
|
|
298
325
|
return ids && ids.length > 0 ? ids.join(",") : void 0;
|
|
299
326
|
}
|
|
@@ -304,7 +331,7 @@ async function toDoorayCliError(error) {
|
|
|
304
331
|
try {
|
|
305
332
|
const body = await error.response.json();
|
|
306
333
|
throw new DoorayCliError(
|
|
307
|
-
`API \uD638\uCD9C \uC2E4\uD328: ${body.header.resultMessage}`,
|
|
334
|
+
`API \uD638\uCD9C \uC2E4\uD328: ${normalizeDoorayMessage(body.header.resultMessage)}`,
|
|
308
335
|
exitCode
|
|
309
336
|
);
|
|
310
337
|
} catch (e) {
|
|
@@ -538,6 +565,20 @@ var DoorayApiClient = class {
|
|
|
538
565
|
return toDoorayCliError(e);
|
|
539
566
|
}
|
|
540
567
|
}
|
|
568
|
+
async updateWikiPageTitle(wikiId, pageId, body) {
|
|
569
|
+
try {
|
|
570
|
+
return await this.api.put(`wiki/v1/wikis/${wikiId}/pages/${pageId}/title`, { json: body }).json();
|
|
571
|
+
} catch (e) {
|
|
572
|
+
return toDoorayCliError(e);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async updateWikiPageContent(wikiId, pageId, body) {
|
|
576
|
+
try {
|
|
577
|
+
return await this.api.put(`wiki/v1/wikis/${wikiId}/pages/${pageId}/content`, { json: body }).json();
|
|
578
|
+
} catch (e) {
|
|
579
|
+
return toDoorayCliError(e);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
541
582
|
// ─── Post Files ─────────────────────────────────────
|
|
542
583
|
async getPostFiles(projectId, postId) {
|
|
543
584
|
try {
|
|
@@ -636,6 +677,7 @@ var PROJECTS_TTL_MS = 36e5;
|
|
|
636
677
|
var MEMBERS_TTL_MS = 36e5;
|
|
637
678
|
var WORKFLOWS_TTL_MS = 864e5;
|
|
638
679
|
var ME_TTL_MS = 864e5;
|
|
680
|
+
var WIKIS_TTL_MS = 864e5;
|
|
639
681
|
|
|
640
682
|
// src/resolvers/me.ts
|
|
641
683
|
async function ensureMe(client) {
|
|
@@ -675,6 +717,35 @@ var doctorCommand = new import_commander3.Command("doctor").description("\uC124\
|
|
|
675
717
|
console.log(` \uD504\uB85C\uC81D\uD2B8: ${stats.projectCount}\uAC1C`);
|
|
676
718
|
console.log(` \uBA64\uBC84: ${stats.memberProjectCount}\uAC1C \uD504\uB85C\uC81D\uD2B8`);
|
|
677
719
|
console.log(` \uC6CC\uD06C\uD50C\uB85C\uC6B0: ${stats.workflowProjectCount}\uAC1C \uD504\uB85C\uC81D\uD2B8`);
|
|
720
|
+
const claudeDir = import_path.default.join(
|
|
721
|
+
process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
722
|
+
".claude"
|
|
723
|
+
);
|
|
724
|
+
const claudeDirExists = await import_promises4.default.access(claudeDir).then(() => true).catch(() => false);
|
|
725
|
+
if (claudeDirExists) {
|
|
726
|
+
console.log(import_chalk3.default.bold("\n\u{1F527} Claude Code \uC2A4\uD0AC\n"));
|
|
727
|
+
const skillDst = import_path.default.join(claudeDir, "skills", "dooray-cli");
|
|
728
|
+
try {
|
|
729
|
+
const stat2 = await import_promises4.default.lstat(skillDst);
|
|
730
|
+
if (stat2.isSymbolicLink()) {
|
|
731
|
+
const target = await import_promises4.default.readlink(skillDst);
|
|
732
|
+
const targetExists = await import_promises4.default.access(target).then(() => true).catch(() => false);
|
|
733
|
+
if (targetExists) {
|
|
734
|
+
console.log(` dooray-cli: ${import_chalk3.default.green("\u2705 \uC124\uCE58\uB428 (\uC2EC\uBCFC\uB9AD \uB9C1\uD06C)")}`);
|
|
735
|
+
} else {
|
|
736
|
+
console.log(
|
|
737
|
+
` dooray-cli: ${import_chalk3.default.yellow("\u26A0\uFE0F \uB9C1\uD06C \uAE68\uC9D0 \u2014 dooray setup\uC73C\uB85C \uC7AC\uC124\uCE58")}`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
console.log(` dooray-cli: ${import_chalk3.default.green("\u2705 \uC124\uCE58\uB428 (\uBCF5\uC0AC\uBCF8)")}`);
|
|
742
|
+
}
|
|
743
|
+
} catch {
|
|
744
|
+
console.log(
|
|
745
|
+
` dooray-cli: ${import_chalk3.default.red("\u274C \uBBF8\uC124\uCE58 \u2014 dooray setup\uC73C\uB85C \uC124\uCE58")}`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
678
749
|
console.log();
|
|
679
750
|
if (apiKeyOk && baseUrlOk) {
|
|
680
751
|
console.log(import_chalk3.default.green("\u2713 \uAE30\uBCF8 \uC124\uC815\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
@@ -688,6 +759,8 @@ var doctorCommand = new import_commander3.Command("doctor").description("\uC124\
|
|
|
688
759
|
// src/commands/setup.ts
|
|
689
760
|
var import_commander4 = require("commander");
|
|
690
761
|
var import_chalk4 = __toESM(require("chalk"));
|
|
762
|
+
var import_path2 = __toESM(require("path"));
|
|
763
|
+
var import_promises5 = __toESM(require("fs/promises"));
|
|
691
764
|
var setupCommand = new import_commander4.Command("setup").description("\uB300\uD654\uD615 \uCD08\uAE30 \uC124\uC815 \uB9C8\uBC95\uC0AC").action(async () => {
|
|
692
765
|
const { input, select, password, confirm } = await import("@inquirer/prompts");
|
|
693
766
|
const existing = await getConfig();
|
|
@@ -760,6 +833,45 @@ var setupCommand = new import_commander4.Command("setup").description("\uB300\uD
|
|
|
760
833
|
theme: { prefix: "\u{1F512}" }
|
|
761
834
|
});
|
|
762
835
|
}
|
|
836
|
+
const claudeDir = import_path2.default.join(
|
|
837
|
+
process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
838
|
+
".claude"
|
|
839
|
+
);
|
|
840
|
+
const claudeDirExists = await import_promises5.default.access(claudeDir).then(() => true).catch(() => false);
|
|
841
|
+
if (claudeDirExists) {
|
|
842
|
+
const isNpx = /_npx[/\\]/.test(__dirname) || /\.npm[/\\]_npx/.test(__dirname) || /npx-/.test(__dirname);
|
|
843
|
+
if (isNpx) {
|
|
844
|
+
console.log(
|
|
845
|
+
import_chalk4.default.yellow(
|
|
846
|
+
" \u26A0 npx \uD658\uACBD\uC5D0\uC11C\uB294 \uC2A4\uD0AC \uC124\uCE58\uAC00 \uBD88\uAC00\uD569\uB2C8\uB2E4. npm i -g @bifos/dooray-cli \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694."
|
|
847
|
+
)
|
|
848
|
+
);
|
|
849
|
+
} else {
|
|
850
|
+
const installSkill = await confirm({
|
|
851
|
+
message: "Claude Code \uC2A4\uD0AC\uC744 \uC124\uCE58\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
|
|
852
|
+
default: true,
|
|
853
|
+
theme: { prefix: "\u{1F527}" }
|
|
854
|
+
});
|
|
855
|
+
if (installSkill) {
|
|
856
|
+
try {
|
|
857
|
+
const skillSrc = import_path2.default.resolve(__dirname, "../skills/dooray-cli");
|
|
858
|
+
const skillsDir = import_path2.default.join(claudeDir, "skills");
|
|
859
|
+
const skillDst = import_path2.default.join(skillsDir, "dooray-cli");
|
|
860
|
+
await import_promises5.default.mkdir(skillsDir, { recursive: true });
|
|
861
|
+
await import_promises5.default.lstat(skillDst).then(() => import_promises5.default.rm(skillDst, { recursive: true, force: true })).catch(() => {
|
|
862
|
+
});
|
|
863
|
+
await import_promises5.default.symlink(skillSrc, skillDst);
|
|
864
|
+
console.log(import_chalk4.default.green(" \u2713 Claude Code \uC2A4\uD0AC \uC124\uCE58 \uC644\uB8CC"));
|
|
865
|
+
} catch (err) {
|
|
866
|
+
console.log(
|
|
867
|
+
import_chalk4.default.yellow(
|
|
868
|
+
` \u26A0 \uC2A4\uD0AC \uC124\uCE58 \uC2E4\uD328: ${err instanceof Error ? err.message : String(err)}`
|
|
869
|
+
)
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
763
875
|
const config = {
|
|
764
876
|
version: 1,
|
|
765
877
|
apiKey,
|
|
@@ -791,12 +903,12 @@ var setupCommand = new import_commander4.Command("setup").description("\uB300\uD
|
|
|
791
903
|
var import_commander5 = require("commander");
|
|
792
904
|
|
|
793
905
|
// src/resolvers/project.ts
|
|
794
|
-
async function fetchAllProjects(client) {
|
|
906
|
+
async function fetchAllProjects(client, options) {
|
|
795
907
|
const all = [];
|
|
796
908
|
let page = 0;
|
|
797
909
|
const size = 100;
|
|
798
910
|
while (true) {
|
|
799
|
-
const res = await client.getProjects({ page, size });
|
|
911
|
+
const res = await client.getProjects({ page, size, type: options?.type });
|
|
800
912
|
for (const p of res.result) {
|
|
801
913
|
all.push({
|
|
802
914
|
id: p.id,
|
|
@@ -818,12 +930,27 @@ async function ensureProjects(client) {
|
|
|
818
930
|
await setProjects(items);
|
|
819
931
|
return items;
|
|
820
932
|
}
|
|
933
|
+
async function ensurePrivateProjects(client) {
|
|
934
|
+
const entry = await getPrivateProjects();
|
|
935
|
+
if (entry && !isExpired(entry.updatedAt, PROJECTS_TTL_MS)) {
|
|
936
|
+
return entry.data;
|
|
937
|
+
}
|
|
938
|
+
const items = await fetchAllProjects(client, { type: "private" });
|
|
939
|
+
await setPrivateProjects(items);
|
|
940
|
+
return items;
|
|
941
|
+
}
|
|
821
942
|
async function resolveProject(client, input) {
|
|
822
943
|
const projects = await ensureProjects(client);
|
|
823
944
|
const match = projects.find((p) => p.code === input || p.id === input);
|
|
824
945
|
if (match) return match.id;
|
|
946
|
+
const privateCached = await getPrivateProjects();
|
|
947
|
+
if (privateCached && !isExpired(privateCached.updatedAt, PROJECTS_TTL_MS)) {
|
|
948
|
+
const privateMatch = privateCached.data.find((p) => p.code === input || p.id === input);
|
|
949
|
+
if (privateMatch) return privateMatch.id;
|
|
950
|
+
}
|
|
825
951
|
throw new DoorayCliError(
|
|
826
|
-
`\uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input}
|
|
952
|
+
`\uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input}
|
|
953
|
+
\uAC1C\uC778 \uD504\uB85C\uC81D\uD2B8\uB77C\uBA74: dooray project list --type private \uB85C \uCE90\uC2DC\uB97C \uAC31\uC2E0\uD558\uC138\uC694`,
|
|
827
954
|
EXIT_PARAM_ERROR
|
|
828
955
|
);
|
|
829
956
|
}
|
|
@@ -871,13 +998,16 @@ function stopSpinner(success, text) {
|
|
|
871
998
|
}
|
|
872
999
|
|
|
873
1000
|
// src/commands/project/list.ts
|
|
874
|
-
var projectListCommand = new import_commander5.Command("list").description("\uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uC870\uD68C").option("-s, --search <keyword>", "code \uD544\uD130\uB9C1").
|
|
1001
|
+
var projectListCommand = new import_commander5.Command("list").description("\uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uC870\uD68C").option("-s, --search <keyword>", "code \uD544\uD130\uB9C1").addOption(
|
|
1002
|
+
new import_commander5.Option("-t, --type <type>", "\uD504\uB85C\uC81D\uD2B8 \uD0C0\uC785 \uD544\uD130").choices(["public", "private"]).default("public")
|
|
1003
|
+
).action(async (opts) => {
|
|
875
1004
|
const globalOpts = projectListCommand.optsWithGlobals();
|
|
876
1005
|
const config = await getConfigOrThrow();
|
|
877
1006
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1007
|
+
const isPrivate = opts.type === "private";
|
|
1008
|
+
startSpinner(isPrivate ? "\uAC1C\uC778 \uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uC870\uD68C \uC911..." : "\uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
1009
|
+
const projects = isPrivate ? await ensurePrivateProjects(client) : await ensureProjects(client);
|
|
1010
|
+
stopSpinner(true, isPrivate ? "\uAC1C\uC778 \uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uC870\uD68C \uC644\uB8CC" : "\uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uC870\uD68C \uC644\uB8CC");
|
|
881
1011
|
let filtered = projects;
|
|
882
1012
|
if (opts.search) {
|
|
883
1013
|
const keyword = opts.search.toLowerCase();
|
|
@@ -1154,11 +1284,10 @@ var postGetCommand = new import_commander10.Command("get").description("\uC5C5\u
|
|
|
1154
1284
|
|
|
1155
1285
|
// src/commands/post/edit.ts
|
|
1156
1286
|
var import_commander11 = require("commander");
|
|
1157
|
-
var import_promises5 = __toESM(require("fs/promises"));
|
|
1158
1287
|
|
|
1159
1288
|
// src/editor/index.ts
|
|
1160
1289
|
var import_node_child_process = require("child_process");
|
|
1161
|
-
var
|
|
1290
|
+
var import_promises6 = __toESM(require("fs/promises"));
|
|
1162
1291
|
var import_tmp = __toESM(require("tmp"));
|
|
1163
1292
|
var import_js_yaml = __toESM(require("js-yaml"));
|
|
1164
1293
|
function openInEditor(content) {
|
|
@@ -1172,7 +1301,7 @@ function openInEditor(content) {
|
|
|
1172
1301
|
const tmpFile = import_tmp.default.fileSync({ prefix: "dooray-", postfix: ".md" });
|
|
1173
1302
|
return new Promise(async (resolve, reject) => {
|
|
1174
1303
|
try {
|
|
1175
|
-
await
|
|
1304
|
+
await import_promises6.default.writeFile(tmpFile.name, content, "utf-8");
|
|
1176
1305
|
const child = (0, import_node_child_process.spawn)(editor, [tmpFile.name], {
|
|
1177
1306
|
stdio: "inherit"
|
|
1178
1307
|
});
|
|
@@ -1188,7 +1317,7 @@ function openInEditor(content) {
|
|
|
1188
1317
|
EXIT_PARAM_ERROR
|
|
1189
1318
|
);
|
|
1190
1319
|
}
|
|
1191
|
-
const result = await
|
|
1320
|
+
const result = await import_promises6.default.readFile(tmpFile.name, "utf-8");
|
|
1192
1321
|
resolve(result);
|
|
1193
1322
|
} catch (e) {
|
|
1194
1323
|
reject(e);
|
|
@@ -1281,14 +1410,25 @@ function parseWikiFrontmatter(content) {
|
|
|
1281
1410
|
};
|
|
1282
1411
|
}
|
|
1283
1412
|
|
|
1284
|
-
// src/
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1413
|
+
// src/utils/body-input.ts
|
|
1414
|
+
var import_promises7 = require("fs/promises");
|
|
1415
|
+
async function readBodyInput(opts) {
|
|
1416
|
+
if (opts.body != null && opts.bodyFile != null) {
|
|
1417
|
+
throw new DoorayCliError(
|
|
1418
|
+
"--body\uC640 --body-file\uC740 \uD568\uAED8 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
1419
|
+
EXIT_PARAM_ERROR
|
|
1420
|
+
);
|
|
1290
1421
|
}
|
|
1291
|
-
|
|
1422
|
+
if (opts.bodyFile) {
|
|
1423
|
+
if (opts.bodyFile === "-") return readStdin();
|
|
1424
|
+
return (0, import_promises7.readFile)(opts.bodyFile, "utf-8");
|
|
1425
|
+
}
|
|
1426
|
+
if (opts.body === "-") return readStdin();
|
|
1427
|
+
return opts.body ?? "";
|
|
1428
|
+
}
|
|
1429
|
+
async function readBodyInputOrNull(opts) {
|
|
1430
|
+
if (opts.body == null && opts.bodyFile == null) return null;
|
|
1431
|
+
return readBodyInput(opts);
|
|
1292
1432
|
}
|
|
1293
1433
|
async function readStdin() {
|
|
1294
1434
|
if (process.stdin.isTTY) {
|
|
@@ -1303,18 +1443,17 @@ async function readStdin() {
|
|
|
1303
1443
|
}
|
|
1304
1444
|
return Buffer.concat(chunks).toString("utf-8");
|
|
1305
1445
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
return import_promises5.default.readFile(opts.bodyFile, "utf-8");
|
|
1446
|
+
|
|
1447
|
+
// src/commands/post/edit.ts
|
|
1448
|
+
async function resolveUsers(client, projectId, emails) {
|
|
1449
|
+
const users = [];
|
|
1450
|
+
for (const email of emails) {
|
|
1451
|
+
const memberId = await resolveMember(client, projectId, email);
|
|
1452
|
+
users.push({ type: "member", member: { organizationMemberId: memberId } });
|
|
1314
1453
|
}
|
|
1315
|
-
return
|
|
1454
|
+
return users;
|
|
1316
1455
|
}
|
|
1317
|
-
var postEditCommand = new import_commander11.Command("edit").description("\uC5C5\uBB34 \uC218\uC815 ($EDITOR \uB610\uB294 --
|
|
1456
|
+
var postEditCommand = new import_commander11.Command("edit").description("\uC5C5\uBB34 \uC218\uC815 ($EDITOR \uB610\uB294 --title/--body \uC635\uC158)").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").argument("<post-number>", "\uC5C5\uBB34 \uBC88\uD638").option("--title <title>", "\uC81C\uBAA9 \uBCC0\uACBD (non-interactive)").option("--subject <subject>", "--title\uC758 deprecated alias").option("--body <text>", "\uBCF8\uBB38 \uBCC0\uACBD (- \uC785\uB825 \uC2DC stdin, non-interactive)").option("--body-file <path>", "\uBCF8\uBB38 \uD30C\uC77C \uACBD\uB85C (- \uC785\uB825 \uC2DC stdin, non-interactive)").action(async (project, postNumberStr, opts) => {
|
|
1318
1457
|
const config = await getConfigOrThrow();
|
|
1319
1458
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
1320
1459
|
startSpinner("\uC5C5\uBB34 \uC870\uD68C \uC911...");
|
|
@@ -1324,9 +1463,15 @@ var postEditCommand = new import_commander11.Command("edit").description("\uC5C5
|
|
|
1324
1463
|
const post = res.result;
|
|
1325
1464
|
const members = await ensureMembers(client, projectId);
|
|
1326
1465
|
stopSpinner(true, "\uC5C5\uBB34 \uC870\uD68C \uC644\uB8CC");
|
|
1327
|
-
const
|
|
1466
|
+
const title = opts.title ?? opts.subject;
|
|
1467
|
+
if (opts.subject && !opts.title) {
|
|
1468
|
+
process.stderr.write(
|
|
1469
|
+
"\u26A0 --subject\uB294 deprecated\uC785\uB2C8\uB2E4. \uB300\uC2E0 --title\uC744 \uC0AC\uC6A9\uD574\uC8FC\uC138\uC694.\n"
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
const nonInteractive = title || opts.body || opts.bodyFile;
|
|
1328
1473
|
if (nonInteractive) {
|
|
1329
|
-
const newBody = await
|
|
1474
|
+
const newBody = await readBodyInputOrNull(opts);
|
|
1330
1475
|
startSpinner("\uC5C5\uBB34 \uC218\uC815 \uC911...");
|
|
1331
1476
|
const toUsers = post.users.to.map((u) => ({
|
|
1332
1477
|
type: u.type,
|
|
@@ -1341,7 +1486,7 @@ var postEditCommand = new import_commander11.Command("edit").description("\uC5C5
|
|
|
1341
1486
|
group: u.group
|
|
1342
1487
|
}));
|
|
1343
1488
|
await client.updatePost(projectId, postId, {
|
|
1344
|
-
subject:
|
|
1489
|
+
subject: title ?? post.subject,
|
|
1345
1490
|
body: {
|
|
1346
1491
|
mimeType: "text/x-markdown",
|
|
1347
1492
|
content: newBody ?? post.body.content
|
|
@@ -1379,32 +1524,6 @@ var postEditCommand = new import_commander11.Command("edit").description("\uC5C5
|
|
|
1379
1524
|
|
|
1380
1525
|
// src/commands/post/create.ts
|
|
1381
1526
|
var import_commander12 = require("commander");
|
|
1382
|
-
var import_promises6 = __toESM(require("fs/promises"));
|
|
1383
|
-
async function readBody(opts) {
|
|
1384
|
-
if (opts.bodyFile) {
|
|
1385
|
-
if (opts.bodyFile === "-") {
|
|
1386
|
-
return readStdin2();
|
|
1387
|
-
}
|
|
1388
|
-
return import_promises6.default.readFile(opts.bodyFile, "utf-8");
|
|
1389
|
-
}
|
|
1390
|
-
if (opts.body === "-") {
|
|
1391
|
-
return readStdin2();
|
|
1392
|
-
}
|
|
1393
|
-
return "";
|
|
1394
|
-
}
|
|
1395
|
-
async function readStdin2() {
|
|
1396
|
-
if (process.stdin.isTTY) {
|
|
1397
|
-
throw new DoorayCliError(
|
|
1398
|
-
"stdin\uC5D0\uC11C \uC77D\uC73C\uB824\uBA74 \uD30C\uC774\uD504\uB85C \uB370\uC774\uD130\uB97C \uC804\uB2EC\uD574\uC8FC\uC138\uC694.",
|
|
1399
|
-
EXIT_PARAM_ERROR
|
|
1400
|
-
);
|
|
1401
|
-
}
|
|
1402
|
-
const chunks = [];
|
|
1403
|
-
for await (const chunk of process.stdin) {
|
|
1404
|
-
chunks.push(chunk);
|
|
1405
|
-
}
|
|
1406
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
1407
|
-
}
|
|
1408
1527
|
async function resolveUsers2(client, projectId, inputs) {
|
|
1409
1528
|
const users = [];
|
|
1410
1529
|
for (const input of inputs) {
|
|
@@ -1413,17 +1532,29 @@ async function resolveUsers2(client, projectId, inputs) {
|
|
|
1413
1532
|
}
|
|
1414
1533
|
return users;
|
|
1415
1534
|
}
|
|
1416
|
-
var postCreateCommand = new import_commander12.Command("create").description("\uC5C5\uBB34 \uC0DD\uC131").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").
|
|
1535
|
+
var postCreateCommand = new import_commander12.Command("create").description("\uC5C5\uBB34 \uC0DD\uC131").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").option("--title <title>", "\uC5C5\uBB34 \uC81C\uBAA9").option("--subject <subject>", "--title\uC758 deprecated alias").option("--to <members...>", "\uB2F4\uB2F9\uC790 (\uC774\uB984 \uB610\uB294 \uC774\uBA54\uC77C, \uC5EC\uB7EC \uBA85 \uAC00\uB2A5)").option("--cc <members...>", "\uCC38\uC870\uC790 (\uC774\uB984 \uB610\uB294 \uC774\uBA54\uC77C, \uC5EC\uB7EC \uBA85 \uAC00\uB2A5)").option("--body <text>", "\uBCF8\uBB38 \uD14D\uC2A4\uD2B8 (- \uC785\uB825 \uC2DC stdin\uC5D0\uC11C \uC77D\uAE30)").option("--body-file <path>", "\uBCF8\uBB38 \uD30C\uC77C \uACBD\uB85C (- \uC785\uB825 \uC2DC stdin\uC5D0\uC11C \uC77D\uAE30)").option("--priority <level>", "\uC6B0\uC120\uC21C\uC704 (highest, high, normal, low, lowest)", "normal").option("--due-date <date>", "\uB9C8\uAC10\uC77C (ISO 8601 \uD615\uC2DD)").action(async (project, opts) => {
|
|
1417
1536
|
const globalOpts = postCreateCommand.optsWithGlobals();
|
|
1418
1537
|
const config = await getConfigOrThrow();
|
|
1419
1538
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
1420
|
-
const
|
|
1539
|
+
const subject = opts.title ?? opts.subject;
|
|
1540
|
+
if (!subject) {
|
|
1541
|
+
throw new DoorayCliError(
|
|
1542
|
+
"--title\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.",
|
|
1543
|
+
EXIT_PARAM_ERROR
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
if (opts.subject && !opts.title) {
|
|
1547
|
+
process.stderr.write(
|
|
1548
|
+
"\u26A0 --subject\uB294 deprecated\uC785\uB2C8\uB2E4. \uB300\uC2E0 --title\uC744 \uC0AC\uC6A9\uD574\uC8FC\uC138\uC694.\n"
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
const bodyContent = await readBodyInput(opts);
|
|
1421
1552
|
startSpinner("\uC5C5\uBB34 \uC0DD\uC131 \uC911...");
|
|
1422
1553
|
const projectId = await resolveProject(client, project);
|
|
1423
1554
|
const toUsers = opts.to ? await resolveUsers2(client, projectId, opts.to) : [];
|
|
1424
1555
|
const ccUsers = opts.cc ? await resolveUsers2(client, projectId, opts.cc) : [];
|
|
1425
1556
|
const res = await client.createPost(projectId, {
|
|
1426
|
-
subject
|
|
1557
|
+
subject,
|
|
1427
1558
|
body: { mimeType: "text/x-markdown", content: bodyContent },
|
|
1428
1559
|
users: { to: toUsers, cc: ccUsers },
|
|
1429
1560
|
priority: opts.priority,
|
|
@@ -1488,36 +1619,11 @@ var commentListCommand = new import_commander15.Command("list").description("\uB
|
|
|
1488
1619
|
|
|
1489
1620
|
// src/commands/post/comment/add.ts
|
|
1490
1621
|
var import_commander16 = require("commander");
|
|
1491
|
-
var import_promises7 = __toESM(require("fs/promises"));
|
|
1492
|
-
async function readStdin3() {
|
|
1493
|
-
if (process.stdin.isTTY) {
|
|
1494
|
-
throw new DoorayCliError(
|
|
1495
|
-
"stdin\uC5D0\uC11C \uC77D\uC73C\uB824\uBA74 \uD30C\uC774\uD504\uB85C \uB370\uC774\uD130\uB97C \uC804\uB2EC\uD574\uC8FC\uC138\uC694.",
|
|
1496
|
-
EXIT_PARAM_ERROR
|
|
1497
|
-
);
|
|
1498
|
-
}
|
|
1499
|
-
const chunks = [];
|
|
1500
|
-
for await (const chunk of process.stdin) {
|
|
1501
|
-
chunks.push(chunk);
|
|
1502
|
-
}
|
|
1503
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
1504
|
-
}
|
|
1505
|
-
async function resolveBody2(opts) {
|
|
1506
|
-
if (opts.body) {
|
|
1507
|
-
if (opts.body === "-") return readStdin3();
|
|
1508
|
-
return opts.body;
|
|
1509
|
-
}
|
|
1510
|
-
if (opts.bodyFile) {
|
|
1511
|
-
if (opts.bodyFile === "-") return readStdin3();
|
|
1512
|
-
return import_promises7.default.readFile(opts.bodyFile, "utf-8");
|
|
1513
|
-
}
|
|
1514
|
-
return null;
|
|
1515
|
-
}
|
|
1516
1622
|
var commentAddCommand = new import_commander16.Command("add").description("\uB313\uAE00 \uCD94\uAC00").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").argument("<post-number>", "\uC5C5\uBB34 \uBC88\uD638").option("--body <text>", "\uB313\uAE00 \uBCF8\uBB38 (- \uC785\uB825 \uC2DC stdin\uC5D0\uC11C \uC77D\uAE30)").option("--body-file <path>", "\uBCF8\uBB38 \uD30C\uC77C \uACBD\uB85C (- \uC785\uB825 \uC2DC stdin\uC5D0\uC11C \uC77D\uAE30)").action(async (project, postNumberStr, opts) => {
|
|
1517
1623
|
const globalOpts = commentAddCommand.optsWithGlobals();
|
|
1518
1624
|
const config = await getConfigOrThrow();
|
|
1519
1625
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
1520
|
-
let bodyContent = await
|
|
1626
|
+
let bodyContent = await readBodyInputOrNull(opts);
|
|
1521
1627
|
if (bodyContent == null) {
|
|
1522
1628
|
bodyContent = await openInEditor("");
|
|
1523
1629
|
if (!bodyContent.trim()) {
|
|
@@ -1544,31 +1650,6 @@ var commentAddCommand = new import_commander16.Command("add").description("\uB31
|
|
|
1544
1650
|
|
|
1545
1651
|
// src/commands/post/comment/edit.ts
|
|
1546
1652
|
var import_commander17 = require("commander");
|
|
1547
|
-
var import_promises8 = __toESM(require("fs/promises"));
|
|
1548
|
-
async function readStdin4() {
|
|
1549
|
-
if (process.stdin.isTTY) {
|
|
1550
|
-
throw new DoorayCliError(
|
|
1551
|
-
"stdin\uC5D0\uC11C \uC77D\uC73C\uB824\uBA74 \uD30C\uC774\uD504\uB85C \uB370\uC774\uD130\uB97C \uC804\uB2EC\uD574\uC8FC\uC138\uC694.",
|
|
1552
|
-
EXIT_PARAM_ERROR
|
|
1553
|
-
);
|
|
1554
|
-
}
|
|
1555
|
-
const chunks = [];
|
|
1556
|
-
for await (const chunk of process.stdin) {
|
|
1557
|
-
chunks.push(chunk);
|
|
1558
|
-
}
|
|
1559
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
1560
|
-
}
|
|
1561
|
-
async function resolveBody3(opts) {
|
|
1562
|
-
if (opts.body) {
|
|
1563
|
-
if (opts.body === "-") return readStdin4();
|
|
1564
|
-
return opts.body;
|
|
1565
|
-
}
|
|
1566
|
-
if (opts.bodyFile) {
|
|
1567
|
-
if (opts.bodyFile === "-") return readStdin4();
|
|
1568
|
-
return import_promises8.default.readFile(opts.bodyFile, "utf-8");
|
|
1569
|
-
}
|
|
1570
|
-
return null;
|
|
1571
|
-
}
|
|
1572
1653
|
var commentEditCommand = new import_commander17.Command("edit").description("\uB313\uAE00 \uC218\uC815 ($EDITOR \uB610\uB294 --body \uC635\uC158)").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").argument("<post-number>", "\uC5C5\uBB34 \uBC88\uD638").argument("<comment-id>", "\uB313\uAE00 ID").option("--body <text>", "\uB313\uAE00 \uBCF8\uBB38 \uBCC0\uACBD (- \uC785\uB825 \uC2DC stdin, non-interactive)").option("--body-file <path>", "\uBCF8\uBB38 \uD30C\uC77C \uACBD\uB85C (- \uC785\uB825 \uC2DC stdin, non-interactive)").action(async (project, postNumberStr, commentId, opts) => {
|
|
1573
1654
|
const config = await getConfigOrThrow();
|
|
1574
1655
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
@@ -1583,7 +1664,7 @@ var commentEditCommand = new import_commander17.Command("edit").description("\uB
|
|
|
1583
1664
|
`);
|
|
1584
1665
|
process.exit(1);
|
|
1585
1666
|
}
|
|
1586
|
-
let edited = await
|
|
1667
|
+
let edited = await readBodyInputOrNull(opts);
|
|
1587
1668
|
if (edited == null) {
|
|
1588
1669
|
const original = comment.body.content;
|
|
1589
1670
|
edited = await openInEditor(original);
|
|
@@ -1647,7 +1728,7 @@ var fileListCommand = new import_commander19.Command("list").description("\uC5C5
|
|
|
1647
1728
|
|
|
1648
1729
|
// src/commands/post/file/download.ts
|
|
1649
1730
|
var import_commander20 = require("commander");
|
|
1650
|
-
var
|
|
1731
|
+
var import_promises8 = require("fs/promises");
|
|
1651
1732
|
var import_node_path4 = require("path");
|
|
1652
1733
|
var fileDownloadCommand = new import_commander20.Command("download").description("\uCCA8\uBD80\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").argument("<post-number>", "\uC5C5\uBB34 \uBC88\uD638").argument("<file-id>", "\uD30C\uC77C ID").option("-o, --output <dir>", "\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC", ".").action(async (project, postNumberStr, fileId, opts) => {
|
|
1653
1734
|
const config = await getConfigOrThrow();
|
|
@@ -1657,7 +1738,7 @@ var fileDownloadCommand = new import_commander20.Command("download").description
|
|
|
1657
1738
|
const postId = await resolvePost(client, projectId, Number(postNumberStr));
|
|
1658
1739
|
const { buffer, fileName } = await client.downloadPostFile(projectId, postId, fileId);
|
|
1659
1740
|
const outputPath = (0, import_node_path4.join)(opts.output, fileName);
|
|
1660
|
-
await (0,
|
|
1741
|
+
await (0, import_promises8.writeFile)(outputPath, Buffer.from(buffer));
|
|
1661
1742
|
stopSpinner(true, "\uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC");
|
|
1662
1743
|
process.stdout.write(`${outputPath}
|
|
1663
1744
|
`);
|
|
@@ -1665,7 +1746,7 @@ var fileDownloadCommand = new import_commander20.Command("download").description
|
|
|
1665
1746
|
|
|
1666
1747
|
// src/commands/post/file/download-all.ts
|
|
1667
1748
|
var import_commander21 = require("commander");
|
|
1668
|
-
var
|
|
1749
|
+
var import_promises9 = require("fs/promises");
|
|
1669
1750
|
var import_node_path5 = require("path");
|
|
1670
1751
|
var fileDownloadAllCommand = new import_commander21.Command("download-all").description("\uC5C5\uBB34\uC758 \uBAA8\uB4E0 \uCCA8\uBD80\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").argument("<post-number>", "\uC5C5\uBB34 \uBC88\uD638").option("-o, --output <dir>", "\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC", ".").action(async (project, postNumberStr, opts) => {
|
|
1671
1752
|
const config = await getConfigOrThrow();
|
|
@@ -1679,18 +1760,18 @@ var fileDownloadAllCommand = new import_commander21.Command("download-all").desc
|
|
|
1679
1760
|
process.stdout.write("\uCCA8\uBD80\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
|
|
1680
1761
|
return;
|
|
1681
1762
|
}
|
|
1682
|
-
await (0,
|
|
1763
|
+
await (0, import_promises9.mkdir)(opts.output, { recursive: true });
|
|
1683
1764
|
const downloaded = [];
|
|
1684
1765
|
for (const file of res.result) {
|
|
1685
1766
|
spinner.text = `\uB2E4\uC6B4\uB85C\uB4DC \uC911: ${file.name} (${downloaded.length + 1}/${res.result.length})`;
|
|
1686
1767
|
const { buffer, fileName } = await client.downloadPostFile(projectId, postId, file.id);
|
|
1687
1768
|
const outputPath = (0, import_node_path5.join)(opts.output, fileName);
|
|
1688
|
-
await (0,
|
|
1769
|
+
await (0, import_promises9.writeFile)(outputPath, Buffer.from(buffer));
|
|
1689
1770
|
downloaded.push(outputPath);
|
|
1690
1771
|
}
|
|
1691
1772
|
stopSpinner(true, `${downloaded.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC`);
|
|
1692
|
-
for (const
|
|
1693
|
-
process.stdout.write(`${
|
|
1773
|
+
for (const path3 of downloaded) {
|
|
1774
|
+
process.stdout.write(`${path3}
|
|
1694
1775
|
`);
|
|
1695
1776
|
}
|
|
1696
1777
|
});
|
|
@@ -1807,6 +1888,31 @@ async function resolveWiki(client, projectCode) {
|
|
|
1807
1888
|
}
|
|
1808
1889
|
return project.wikiId;
|
|
1809
1890
|
}
|
|
1891
|
+
async function resolveWikiHomePageId(client, wikiId) {
|
|
1892
|
+
const cached = await getWikis();
|
|
1893
|
+
const fresh = cached && !isExpired(cached.updatedAt, WIKIS_TTL_MS);
|
|
1894
|
+
let wikis;
|
|
1895
|
+
if (fresh) {
|
|
1896
|
+
wikis = cached.data;
|
|
1897
|
+
} else {
|
|
1898
|
+
const res = await client.getWikis({ size: 100 });
|
|
1899
|
+
wikis = res.result.map((w) => ({
|
|
1900
|
+
id: w.id,
|
|
1901
|
+
projectId: w.project.id,
|
|
1902
|
+
name: w.name,
|
|
1903
|
+
homePageId: w.home.pageId
|
|
1904
|
+
}));
|
|
1905
|
+
await setWikis(wikis);
|
|
1906
|
+
}
|
|
1907
|
+
const wiki = wikis.find((w) => w.id === wikiId);
|
|
1908
|
+
if (!wiki?.homePageId) {
|
|
1909
|
+
throw new DoorayCliError(
|
|
1910
|
+
`\uC704\uD0A4\uC758 home \uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (wikiId: ${wikiId})`,
|
|
1911
|
+
EXIT_API_ERROR
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
return wiki.homePageId;
|
|
1915
|
+
}
|
|
1810
1916
|
|
|
1811
1917
|
// src/commands/wiki/pages.ts
|
|
1812
1918
|
var wikiPagesCommand = new import_commander25.Command("pages").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uBAA9\uB85D \uC870\uD68C").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").option("--parent <page-id>", "\uBD80\uBAA8 \uD398\uC774\uC9C0 ID").action(async (project, opts) => {
|
|
@@ -1835,43 +1941,18 @@ var wikiPageGetCommand = new import_commander26.Command("get").description("\uC7
|
|
|
1835
1941
|
|
|
1836
1942
|
// src/commands/wiki/page-create.ts
|
|
1837
1943
|
var import_commander27 = require("commander");
|
|
1838
|
-
var import_promises11 = __toESM(require("fs/promises"));
|
|
1839
|
-
async function readBody2(opts) {
|
|
1840
|
-
if (opts.bodyFile) {
|
|
1841
|
-
if (opts.bodyFile === "-") {
|
|
1842
|
-
return readStdin5();
|
|
1843
|
-
}
|
|
1844
|
-
return import_promises11.default.readFile(opts.bodyFile, "utf-8");
|
|
1845
|
-
}
|
|
1846
|
-
if (opts.body === "-") {
|
|
1847
|
-
return readStdin5();
|
|
1848
|
-
}
|
|
1849
|
-
return "";
|
|
1850
|
-
}
|
|
1851
|
-
async function readStdin5() {
|
|
1852
|
-
if (process.stdin.isTTY) {
|
|
1853
|
-
throw new DoorayCliError(
|
|
1854
|
-
"stdin\uC5D0\uC11C \uC77D\uC73C\uB824\uBA74 \uD30C\uC774\uD504\uB85C \uB370\uC774\uD130\uB97C \uC804\uB2EC\uD574\uC8FC\uC138\uC694.",
|
|
1855
|
-
EXIT_PARAM_ERROR
|
|
1856
|
-
);
|
|
1857
|
-
}
|
|
1858
|
-
const chunks = [];
|
|
1859
|
-
for await (const chunk of process.stdin) {
|
|
1860
|
-
chunks.push(chunk);
|
|
1861
|
-
}
|
|
1862
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
1863
|
-
}
|
|
1864
1944
|
var wikiPageCreateCommand = new import_commander27.Command("create").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uC0DD\uC131").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").requiredOption("--title <title>", "\uD398\uC774\uC9C0 \uC81C\uBAA9").option("--parent <page-id>", "\uBD80\uBAA8 \uD398\uC774\uC9C0 ID").option("--body <text>", "\uBCF8\uBB38 \uD14D\uC2A4\uD2B8 (- \uC785\uB825 \uC2DC stdin\uC5D0\uC11C \uC77D\uAE30)").option("--body-file <path>", "\uBCF8\uBB38 \uD30C\uC77C \uACBD\uB85C (- \uC785\uB825 \uC2DC stdin\uC5D0\uC11C \uC77D\uAE30)").action(async (project, opts) => {
|
|
1865
1945
|
const globalOpts = wikiPageCreateCommand.optsWithGlobals();
|
|
1866
1946
|
const config = await getConfigOrThrow();
|
|
1867
1947
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
1868
|
-
const bodyContent = await
|
|
1948
|
+
const bodyContent = await readBodyInput(opts);
|
|
1869
1949
|
startSpinner("\uC704\uD0A4 \uD398\uC774\uC9C0 \uC0DD\uC131 \uC911...");
|
|
1870
1950
|
const wikiId = await resolveWiki(client, project);
|
|
1951
|
+
const parentPageId = opts.parent ?? await resolveWikiHomePageId(client, wikiId);
|
|
1871
1952
|
const res = await client.createWikiPage(wikiId, {
|
|
1872
1953
|
subject: opts.title,
|
|
1873
1954
|
body: { mimeType: "text/x-markdown", content: bodyContent },
|
|
1874
|
-
parentPageId
|
|
1955
|
+
parentPageId
|
|
1875
1956
|
});
|
|
1876
1957
|
stopSpinner(true, "\uC704\uD0A4 \uD398\uC774\uC9C0 \uC0DD\uC131 \uC644\uB8CC");
|
|
1877
1958
|
if (globalOpts.json) {
|
|
@@ -1886,26 +1967,53 @@ var wikiPageCreateCommand = new import_commander27.Command("create").description
|
|
|
1886
1967
|
|
|
1887
1968
|
// src/commands/wiki/page-edit.ts
|
|
1888
1969
|
var import_commander28 = require("commander");
|
|
1889
|
-
var wikiPageEditCommand = new import_commander28.Command("edit").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uC218\uC815 ($EDITOR)").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").argument("<page-id>", "\uD398\uC774\uC9C0 ID").action(async (project, pageId) => {
|
|
1970
|
+
var wikiPageEditCommand = new import_commander28.Command("edit").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uC218\uC815 (\uD50C\uB798\uADF8 \uC5C6\uC73C\uBA74 $EDITOR)").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").argument("<page-id>", "\uD398\uC774\uC9C0 ID").option("--title <title>", "\uD398\uC774\uC9C0 \uC81C\uBAA9 (\uC9C0\uC815 \uC2DC $EDITOR \uC0DD\uB7B5)").option("--body <text>", "\uBCF8\uBB38 \uD14D\uC2A4\uD2B8 (- \uC785\uB825 \uC2DC stdin\uC5D0\uC11C \uC77D\uAE30)").option("--body-file <path>", "\uBCF8\uBB38 \uD30C\uC77C \uACBD\uB85C (- \uC785\uB825 \uC2DC stdin\uC5D0\uC11C \uC77D\uAE30)").action(async (project, pageId, opts) => {
|
|
1890
1971
|
const config = await getConfigOrThrow();
|
|
1891
1972
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
1892
|
-
|
|
1973
|
+
const hasTitle = opts.title != null;
|
|
1974
|
+
const hasBody = opts.body != null || opts.bodyFile != null;
|
|
1975
|
+
const nonInteractive = hasTitle || hasBody;
|
|
1976
|
+
startSpinner("\uC704\uD0A4 \uC815\uBCF4 \uC870\uD68C \uC911...");
|
|
1893
1977
|
const wikiId = await resolveWiki(client, project);
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1978
|
+
if (!nonInteractive) {
|
|
1979
|
+
const res = await client.getWikiPage(wikiId, pageId);
|
|
1980
|
+
const page = res.result;
|
|
1981
|
+
stopSpinner(true, "\uC704\uD0A4 \uD398\uC774\uC9C0 \uC870\uD68C \uC644\uB8CC");
|
|
1982
|
+
const original = serializeWikiFrontmatter(page);
|
|
1983
|
+
const edited = await openInEditor(original);
|
|
1984
|
+
if (original === edited) {
|
|
1985
|
+
process.stdout.write("\uBCC0\uACBD\uC0AC\uD56D \uC5C6\uC74C\n");
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
const parsed = parseWikiFrontmatter(edited);
|
|
1989
|
+
startSpinner("\uC704\uD0A4 \uD398\uC774\uC9C0 \uC218\uC815 \uC911...");
|
|
1990
|
+
await client.updateWikiPage(wikiId, pageId, {
|
|
1991
|
+
subject: parsed.title,
|
|
1992
|
+
body: { mimeType: "text/x-markdown", content: parsed.body }
|
|
1993
|
+
});
|
|
1994
|
+
stopSpinner(true, "\uC704\uD0A4 \uD398\uC774\uC9C0 \uC218\uC815 \uC644\uB8CC");
|
|
1995
|
+
process.stdout.write(`\uC704\uD0A4 \uD398\uC774\uC9C0\uAC00 \uC218\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${pageId}
|
|
1996
|
+
`);
|
|
1901
1997
|
return;
|
|
1902
1998
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1999
|
+
stopSpinner(true, "\uC704\uD0A4 \uC815\uBCF4 \uC870\uD68C \uC644\uB8CC");
|
|
2000
|
+
if (hasTitle && hasBody) {
|
|
2001
|
+
const bodyContent = await readBodyInput(opts);
|
|
2002
|
+
startSpinner("\uC704\uD0A4 \uD398\uC774\uC9C0 \uC218\uC815 \uC911...");
|
|
2003
|
+
await client.updateWikiPage(wikiId, pageId, {
|
|
2004
|
+
subject: opts.title,
|
|
2005
|
+
body: { mimeType: "text/x-markdown", content: bodyContent }
|
|
2006
|
+
});
|
|
2007
|
+
} else if (hasTitle) {
|
|
2008
|
+
startSpinner("\uC704\uD0A4 \uD398\uC774\uC9C0 \uC81C\uBAA9 \uC218\uC815 \uC911...");
|
|
2009
|
+
await client.updateWikiPageTitle(wikiId, pageId, { subject: opts.title });
|
|
2010
|
+
} else {
|
|
2011
|
+
const bodyContent = await readBodyInput(opts);
|
|
2012
|
+
startSpinner("\uC704\uD0A4 \uD398\uC774\uC9C0 \uBCF8\uBB38 \uC218\uC815 \uC911...");
|
|
2013
|
+
await client.updateWikiPageContent(wikiId, pageId, {
|
|
2014
|
+
body: { mimeType: "text/x-markdown", content: bodyContent }
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
1909
2017
|
stopSpinner(true, "\uC704\uD0A4 \uD398\uC774\uC9C0 \uC218\uC815 \uC644\uB8CC");
|
|
1910
2018
|
process.stdout.write(`\uC704\uD0A4 \uD398\uC774\uC9C0\uAC00 \uC218\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${pageId}
|
|
1911
2019
|
`);
|
|
@@ -2086,7 +2194,7 @@ ${mail.body}
|
|
|
2086
2194
|
|
|
2087
2195
|
// src/commands/mail/send.ts
|
|
2088
2196
|
var import_commander31 = require("commander");
|
|
2089
|
-
var
|
|
2197
|
+
var import_promises10 = require("fs/promises");
|
|
2090
2198
|
|
|
2091
2199
|
// src/api/smtpClient.ts
|
|
2092
2200
|
var import_nodemailer = __toESM(require("nodemailer"));
|
|
@@ -2140,7 +2248,7 @@ var mailSendCommand = new import_commander31.Command("send").description("\uBA54
|
|
|
2140
2248
|
const config = await getConfigOrThrow();
|
|
2141
2249
|
let body = opts.body ?? "";
|
|
2142
2250
|
if (opts.bodyFile) {
|
|
2143
|
-
body = await (0,
|
|
2251
|
+
body = await (0, import_promises10.readFile)(opts.bodyFile, "utf-8");
|
|
2144
2252
|
}
|
|
2145
2253
|
if (!body) {
|
|
2146
2254
|
process.stderr.write("\uC624\uB958: --body \uB610\uB294 --body-file\uC744 \uC9C0\uC815\uD558\uC138\uC694\n");
|
|
@@ -2171,7 +2279,7 @@ var mailSendCommand = new import_commander31.Command("send").description("\uBA54
|
|
|
2171
2279
|
|
|
2172
2280
|
// src/commands/mail/reply.ts
|
|
2173
2281
|
var import_commander32 = require("commander");
|
|
2174
|
-
var
|
|
2282
|
+
var import_promises11 = require("fs/promises");
|
|
2175
2283
|
var import_imapflow2 = require("imapflow");
|
|
2176
2284
|
async function getMessageId(config, uid) {
|
|
2177
2285
|
const imap = getImapConfigOrThrow(config);
|
|
@@ -2203,7 +2311,7 @@ var mailReplyCommand = new import_commander32.Command("reply").description("\uBA
|
|
|
2203
2311
|
const config = await getConfigOrThrow();
|
|
2204
2312
|
let body = opts.body ?? "";
|
|
2205
2313
|
if (opts.bodyFile) {
|
|
2206
|
-
body = await (0,
|
|
2314
|
+
body = await (0, import_promises11.readFile)(opts.bodyFile, "utf-8");
|
|
2207
2315
|
}
|
|
2208
2316
|
if (!body) {
|
|
2209
2317
|
process.stderr.write("\uC624\uB958: --body \uB610\uB294 --body-file\uC744 \uC9C0\uC815\uD558\uC138\uC694\n");
|
|
@@ -2240,7 +2348,7 @@ var mailReplyCommand = new import_commander32.Command("reply").description("\uBA
|
|
|
2240
2348
|
|
|
2241
2349
|
// src/index.ts
|
|
2242
2350
|
var program = new import_commander33.Command();
|
|
2243
|
-
program.name("dooray").description("Dooray REST API CLI").version("0.
|
|
2351
|
+
program.name("dooray").description("Dooray REST API CLI").version("0.5.2").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "ID\uB9CC \uCD9C\uB825").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654");
|
|
2244
2352
|
program.hook("preAction", () => {
|
|
2245
2353
|
const opts = program.opts();
|
|
2246
2354
|
if (opts.color === false || process.env.NO_COLOR) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bifos/dooray-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "CLI tool for Dooray project management — AI agent & terminal friendly",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dooray",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
|
+
"skills",
|
|
25
26
|
"README.md"
|
|
26
27
|
],
|
|
27
28
|
"engines": {
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dooray-cli
|
|
3
|
+
description: Dooray 업무 관리 CLI. 프로젝트/업무/댓글/위키 조회·생성·수정. AI 에이전트가 두레이 업무를 자동화할 때 사용.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dooray-cli
|
|
7
|
+
|
|
8
|
+
NHN Dooray REST API를 래핑한 CLI 도구. 업무 조회, 생성, 수정, 댓글, 위키 등을 터미널에서 수행한다.
|
|
9
|
+
|
|
10
|
+
## 설치
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @bifos/dooray-cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 초기 설정
|
|
17
|
+
|
|
18
|
+
대화형 마법사로 한 번에 설정:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
dooray setup # API endpoint 선택, API key 입력, 메일 설정까지 대화형으로 진행
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
또는 개별 수동 설정:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
dooray config set base-url https://api.dooray.com
|
|
28
|
+
dooray config set api-key <YOUR_API_TOKEN> # https://{org}.dooray.com/setting/api/token
|
|
29
|
+
dooray doctor # 설정 검증
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 출력 모드
|
|
33
|
+
|
|
34
|
+
| 플래그 | 설명 | 용도 |
|
|
35
|
+
|--------|------|------|
|
|
36
|
+
| (없음) | 사람이 읽기 좋은 테이블 | 기본 |
|
|
37
|
+
| `--json` | JSON 출력 (stdout) | 파싱, 체이닝 |
|
|
38
|
+
| `--quiet` | ID만 출력 | 스크립팅 |
|
|
39
|
+
|
|
40
|
+
**AI 에이전트는 `--json`을 사용하여 구조화된 데이터를 파싱하라.**
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 의도 → 커맨드 매핑
|
|
45
|
+
|
|
46
|
+
자연어 요청을 커맨드로 변환할 때 아래 표를 참고한다.
|
|
47
|
+
|
|
48
|
+
| 의도 | 커맨드 |
|
|
49
|
+
|------|--------|
|
|
50
|
+
| 초기 설정 (대화형) | `dooray setup` |
|
|
51
|
+
| 프로젝트 찾기 | `dooray project list --search <keyword>` |
|
|
52
|
+
| 개인 프로젝트 목록 | `dooray project list --type private` |
|
|
53
|
+
| 프로젝트 멤버 보기 | `dooray project members <project>` |
|
|
54
|
+
| 업무 목록 조회 | `dooray post list <project>` |
|
|
55
|
+
| 업무 검색 | `dooray post search <project> "<keyword>"` |
|
|
56
|
+
| 업무 상세 보기 | `dooray post get <project> <number>` |
|
|
57
|
+
| 업무 생성 | `dooray post create <project> --title "..." --body "..."` 또는 `--body-file <path>` (`--body`와 `--body-file`은 동시 사용 불가) |
|
|
58
|
+
| 업무 제목/본문 수정 | `dooray post edit <project> <number> --title "..." --body "..."` 또는 `--body-file <path>` |
|
|
59
|
+
| 업무 완료 처리 | `dooray post done <project> <number>` |
|
|
60
|
+
| 업무 워크플로우 변경 | `dooray post workflow <project> <number> <workflow>` |
|
|
61
|
+
| 댓글 조회 | `dooray post comment list <project> <number>` |
|
|
62
|
+
| 댓글 추가 | `dooray post comment add <project> <number> --body "..."` 또는 `--body-file <path>` |
|
|
63
|
+
| 댓글 수정 | `dooray post comment edit <project> <number> <comment-id> --body "..."` 또는 `--body-file <path>` |
|
|
64
|
+
| 댓글 삭제 | `dooray post comment delete <project> <number> <comment-id>` |
|
|
65
|
+
| 위키 목록 | `dooray wiki list` |
|
|
66
|
+
| 위키 페이지 목록 | `dooray wiki pages <project>` |
|
|
67
|
+
| 위키 페이지 상세 | `dooray wiki page get <project> <page-id>` |
|
|
68
|
+
| 위키 페이지 생성 | `dooray wiki page create <project> --title "..." [--parent <page-id>] [--body "..."]` (--parent 생략 시 위키 home 페이지 아래 생성) |
|
|
69
|
+
| 위키 페이지 수정 (제목) | `dooray wiki page edit <project> <page-id> --title "..."` |
|
|
70
|
+
| 위키 페이지 수정 (본문) | `dooray wiki page edit <project> <page-id> --body "..."` 또는 `--body-file ./new.md` |
|
|
71
|
+
| 위키 페이지 수정 (에디터) | `dooray wiki page edit <project> <page-id>` (플래그 없으면 $EDITOR 열림) |
|
|
72
|
+
| 메일 목록 조회 | `dooray mail list` |
|
|
73
|
+
| 안읽은 메일 | `dooray mail list --unread` |
|
|
74
|
+
| 메일 제목 검색 | `dooray mail list --search "<keyword>"` |
|
|
75
|
+
| 메일 상세 | `dooray mail get <uid>` |
|
|
76
|
+
| 메일 발송 | `dooray mail send --to "..." --subject "..." --body "..."` |
|
|
77
|
+
| 메일 답장 | `dooray mail reply <uid> --body "..."` |
|
|
78
|
+
| 첨부파일 목록 | `dooray post file list <project> <number>` |
|
|
79
|
+
| 첨부파일 다운로드 | `dooray post file download <project> <number> <file-id>` |
|
|
80
|
+
| 전체 첨부파일 다운로드 | `dooray post file download-all <project> <number>` |
|
|
81
|
+
| 첨부파일 업로드 | `dooray post file upload <project> <number> <file-path>` |
|
|
82
|
+
| 첨부파일 삭제 | `dooray post file delete <project> <number> <file-id>` |
|
|
83
|
+
|
|
84
|
+
> **제목 옵션 네이밍**: `post` 와 `wiki page` 모두 `--title` 표준. `post`의 `--subject`는 deprecated alias로 당분간 동작하되, 새 코드에서는 `--title` 사용을 권장.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 제약사항 (Dooray API 한계)
|
|
89
|
+
|
|
90
|
+
CLI로 처리 **불가능한** 작업. 아래 항목을 요청받으면 웹 UI 사용을 안내할 것.
|
|
91
|
+
|
|
92
|
+
| 작업 | 대체 경로 | 근거 |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| 위키 페이지 **삭제** | 웹 UI (`https://{tenant}.dooray.com/wiki/...`) | Dooray REST API에 해당 엔드포인트 없음 (위키 댓글·첨부파일 삭제는 있지만 페이지 자체는 없음, `docs/dooray-api-reference.md` §7 참조) |
|
|
95
|
+
| 프로젝트 삭제 | 웹 UI (admin 페이지) | API 미지원 |
|
|
96
|
+
|
|
97
|
+
위키 페이지를 잘못 만든 경우(테스트/중복) **soft delete(빈 제목·본문) 우회 금지** — 페이지가 트리에 남아 사용자 혼란 유발.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 워크플로우 판단 기준
|
|
102
|
+
|
|
103
|
+
1. **"내 프로젝트", "개인 프로젝트" 언급 시** → `dooray project list --type private --json` 으로 개인 프로젝트 먼저 조회
|
|
104
|
+
2. **프로젝트 코드를 모르면** → `dooray project list --search <keyword>` 로 먼저 찾기
|
|
105
|
+
3. **업무 번호를 모르면** → `dooray post search <project> "<keyword>"` 로 검색
|
|
106
|
+
4. **워크플로우 이름을 모르면** → `dooray project workflows <project>` 로 확인
|
|
107
|
+
5. **멤버 이름을 모르면** → `dooray project members <project>` 로 확인
|
|
108
|
+
6. **결과를 다음 액션에 사용하려면** → `--json` 플래그로 구조화된 데이터 획득
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 체이닝 예시
|
|
113
|
+
|
|
114
|
+
### 업무 찾아서 완료 처리
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# 1. 업무 검색으로 번호 확인
|
|
118
|
+
dooray post search tc-ocr "graceful shutdown" --json
|
|
119
|
+
# → [{ "number": 42, "subject": "graceful shutdown 구현", ... }]
|
|
120
|
+
|
|
121
|
+
# 2. 완료 처리
|
|
122
|
+
dooray post done tc-ocr 42
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 프로젝트 찾아서 업무 생성
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# 1. 프로젝트 코드 확인
|
|
129
|
+
dooray project list --search "AI서비스" --json
|
|
130
|
+
# → [{ "code": "ai-service-dev", ... }]
|
|
131
|
+
|
|
132
|
+
# 2. 업무 생성
|
|
133
|
+
dooray post create ai-service-dev \
|
|
134
|
+
--title "주간보고 2026-W14" \
|
|
135
|
+
--body "## 이번 주 성과\n- 항목1\n- 항목2" \
|
|
136
|
+
--to "김철수"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 업무 상세 조회 후 댓글 추가
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# 1. 업무 조회
|
|
143
|
+
dooray post get tc-ocr 42 --json
|
|
144
|
+
|
|
145
|
+
# 2. 댓글 추가
|
|
146
|
+
dooray post comment add tc-ocr 42 --body "진행 상황 업데이트: 80% 완료"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 위키 페이지 조회
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# 1. 위키 페이지 목록
|
|
153
|
+
dooray wiki pages tc-ocr --json
|
|
154
|
+
# → [{ "id": "3052841366755571094", "subject": "설계 문서", ... }]
|
|
155
|
+
|
|
156
|
+
# 2. 페이지 내용 조회
|
|
157
|
+
dooray wiki page get tc-ocr 3052841366755571094 --json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 커맨드 상세
|
|
163
|
+
|
|
164
|
+
### 업무 생성 (non-interactive)
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
dooray post create <project> \
|
|
168
|
+
--title "제목" \
|
|
169
|
+
--body "본문 마크다운" \
|
|
170
|
+
--to "담당자이름" \ # 여러 명: --to "김철수" --to "이영희"
|
|
171
|
+
--cc "참조자이름" \
|
|
172
|
+
--priority normal \ # highest, high, normal, low, lowest
|
|
173
|
+
--due-date "2026-04-30T18:00:00+09:00"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
본문이 길면 파일로 (`--body`와 `--body-file`은 함께 사용 불가):
|
|
177
|
+
```bash
|
|
178
|
+
dooray post create <project> --title "제목" --body-file ./content.md
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 업무 수정 (non-interactive)
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# 제목만 변경
|
|
185
|
+
dooray post edit <project> <number> --title "새 제목"
|
|
186
|
+
|
|
187
|
+
# 본문만 변경
|
|
188
|
+
dooray post edit <project> <number> --body "새 본문"
|
|
189
|
+
|
|
190
|
+
# 제목 + 본문 동시 변경
|
|
191
|
+
dooray post edit <project> <number> --title "새 제목" --body-file ./updated.md
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 댓글 추가 (non-interactive)
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
dooray post comment add <project> <number> --body "댓글 내용"
|
|
198
|
+
dooray post comment add <project> <number> --body-file ./comment.md
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 에러 핸들링
|
|
204
|
+
|
|
205
|
+
CLI 에러 발생 시 복구 방법:
|
|
206
|
+
|
|
207
|
+
| 에러 메시지 | 원인 | 복구 방법 |
|
|
208
|
+
|------------|------|-----------|
|
|
209
|
+
| `프로젝트를 찾을 수 없습니다: xxx` | 프로젝트 코드/ID 오류 | `dooray project list --search "xxx"` 로 정확한 코드 확인 |
|
|
210
|
+
| `복수의 멤버가 매칭됩니다: "김"` | 이름이 모호함 | 에러 메시지의 후보 목록에서 정확한 이름으로 재시도 |
|
|
211
|
+
| `멤버를 찾을 수 없습니다: xxx` | 해당 프로젝트에 멤버 없음 | `dooray project members <project>` 로 멤버 목록 확인 |
|
|
212
|
+
| `워크플로우를 찾을 수 없습니다: xxx` | 워크플로우 이름 오류 | `dooray project workflows <project>` 로 확인 |
|
|
213
|
+
| `API 호출 실패 (401)` | API 키 만료/오류 | `dooray doctor` 로 설정 검증 |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 캐시
|
|
218
|
+
|
|
219
|
+
프로젝트, 멤버, 워크플로우, 위키 정보는 `~/.dooray/cache/`에 캐시된다.
|
|
220
|
+
캐시가 오래된 것 같으면:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
dooray cache clear # 전체 캐시 삭제 (다음 실행 시 자동 갱신)
|
|
224
|
+
```
|