@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 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
- --subject "업무 제목" \
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 --subject "새 제목" --body "새 본문"
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 # 페이지 생성 ($EDITOR)
93
- dooray wiki page edit tc-ocr <page-id> # 페이지 수정 ($EDITOR)
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
- 프로젝트, 멤버, 워크플로우 정보는 `~/.dooray/cache/`에 캐시됩니다.
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(path) {
205
+ async function readJson(path3) {
204
206
  try {
205
- const raw = await (0, import_promises2.readFile)(path, "utf-8");
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(path, data) {
212
- const dir = path.substring(0, path.lastIndexOf("/"));
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)(path, JSON.stringify(data, null, 2) + "\n");
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").action(async (opts) => {
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
- startSpinner("\uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uC870\uD68C \uC911...");
879
- const projects = await ensureProjects(client);
880
- stopSpinner(true, "\uD504\uB85C\uC81D\uD2B8 \uBAA9\uB85D \uC870\uD68C \uC644\uB8CC");
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 import_promises4 = __toESM(require("fs/promises"));
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 import_promises4.default.writeFile(tmpFile.name, content, "utf-8");
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 import_promises4.default.readFile(tmpFile.name, "utf-8");
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/commands/post/edit.ts
1285
- async function resolveUsers(client, projectId, emails) {
1286
- const users = [];
1287
- for (const email of emails) {
1288
- const memberId = await resolveMember(client, projectId, email);
1289
- users.push({ type: "member", member: { organizationMemberId: memberId } });
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
- return users;
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
- async function resolveBody(opts) {
1307
- if (opts.body) {
1308
- if (opts.body === "-") return readStdin();
1309
- return opts.body;
1310
- }
1311
- if (opts.bodyFile) {
1312
- if (opts.bodyFile === "-") return readStdin();
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 null;
1454
+ return users;
1316
1455
  }
1317
- var postEditCommand = new import_commander11.Command("edit").description("\uC5C5\uBB34 \uC218\uC815 ($EDITOR \uB610\uB294 --subject/--body \uC635\uC158)").argument("<project>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uB610\uB294 ID").argument("<post-number>", "\uC5C5\uBB34 \uBC88\uD638").option("--subject <title>", "\uC81C\uBAA9 \uBCC0\uACBD (non-interactive)").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) => {
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 nonInteractive = opts.subject || opts.body || opts.bodyFile;
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 resolveBody(opts);
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: opts.subject ?? post.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").requiredOption("--subject <title>", "\uC5C5\uBB34 \uC81C\uBAA9").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) => {
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 bodyContent = await readBody(opts);
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: opts.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 resolveBody2(opts);
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 resolveBody3(opts);
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 import_promises9 = require("fs/promises");
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, import_promises9.writeFile)(outputPath, Buffer.from(buffer));
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 import_promises10 = require("fs/promises");
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, import_promises10.mkdir)(opts.output, { recursive: true });
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, import_promises10.writeFile)(outputPath, Buffer.from(buffer));
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 path of downloaded) {
1693
- process.stdout.write(`${path}
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 readBody2(opts);
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: opts.parent ?? ""
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
- startSpinner("\uC704\uD0A4 \uD398\uC774\uC9C0 \uC870\uD68C \uC911...");
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
- const res = await client.getWikiPage(wikiId, pageId);
1895
- const page = res.result;
1896
- stopSpinner(true, "\uC704\uD0A4 \uD398\uC774\uC9C0 \uC870\uD68C \uC644\uB8CC");
1897
- const original = serializeWikiFrontmatter(page);
1898
- const edited = await openInEditor(original);
1899
- if (original === edited) {
1900
- process.stdout.write("\uBCC0\uACBD\uC0AC\uD56D \uC5C6\uC74C\n");
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
- const parsed = parseWikiFrontmatter(edited);
1904
- startSpinner("\uC704\uD0A4 \uD398\uC774\uC9C0 \uC218\uC815 \uC911...");
1905
- await client.updateWikiPage(wikiId, pageId, {
1906
- subject: parsed.title,
1907
- body: { mimeType: "text/x-markdown", content: parsed.body }
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 import_promises12 = require("fs/promises");
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, import_promises12.readFile)(opts.bodyFile, "utf-8");
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 import_promises13 = require("fs/promises");
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, import_promises13.readFile)(opts.bodyFile, "utf-8");
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.4.0").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "ID\uB9CC \uCD9C\uB825").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654");
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.4.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
+ ```