@bifos/dooray-cli 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -32,6 +32,15 @@ dooray setup
|
|
|
32
32
|
dooray doctor # 설정이 제대로 됐는지 확인
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
개별 값만 바꾸려면 `dooray config set` 을 쓴다. 값 자리에 `-` 를 주면 stdin 에서 읽는다.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
printf '%s' "$TOKEN" | dooray config set api-key -
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
토큰을 명령 인자로 넘기면 셸 기록과 프로세스 목록에 남는다. 에이전트가 대신 실행하면 실행 로그에도 남는다.
|
|
42
|
+
stdin 으로 받은 값은 양끝 공백을 지운 뒤 저장하고, 비어 있으면 저장하지 않고 종료 코드 3 으로 끝낸다.
|
|
43
|
+
|
|
35
44
|
에이전트에서 쓰려면 스킬을 설치한다. Claude Code 가 이 CLI 의 사용법을 알게 된다.
|
|
36
45
|
|
|
37
46
|
```bash
|
|
@@ -41,6 +50,41 @@ dooray skill status
|
|
|
41
50
|
|
|
42
51
|
CLI 를 새 버전으로 올린 뒤에는 `dooray skill update` 를 실행해야 스킬도 갱신된다.
|
|
43
52
|
|
|
53
|
+
## Dooray 문체 페르소나
|
|
54
|
+
|
|
55
|
+
`dooray-persona` 스킬은 Dooray에 쌓인 본인 업무 글과 댓글을 모아 개인 업무 문체 문서를 만든다.
|
|
56
|
+
완성한 문서를 AI 에이전트의 규칙으로 연결하면 업무와 댓글 초안을 본인 문체에 맞춰 작성할 수 있다.
|
|
57
|
+
|
|
58
|
+
이 스킬은 사용자 글을 로컬에서 분석하는 별도 워크플로우이므로 `dooray skill install`의 설치 대상이 아니다.
|
|
59
|
+
저장소를 내려받은 뒤 스킬 디렉터리를 `~/.claude/skills/` 아래에 링크하거나 복사한다.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/jon890/dooray-cli.git
|
|
63
|
+
cd dooray-cli
|
|
64
|
+
mkdir -p ~/.claude/skills
|
|
65
|
+
ln -s "$PWD/skills/dooray-persona" ~/.claude/skills/dooray-persona
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
링크 대신 복사해서 사용하려면 마지막 명령을 다음 명령으로 바꾼다.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cp -R skills/dooray-persona ~/.claude/skills/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
설정 파일은 `~/.claude/dooray-persona.config.json`이며, 최초 실행에서는 후보 프로젝트를 탐색해 대상을 고른 뒤 본인 글을 수집한다.
|
|
75
|
+
인증은 `dooray setup`이 만든 `~/.dooray/config.json`을 읽어 사용하므로 토큰을 따로 입력하지 않는다.
|
|
76
|
+
|
|
77
|
+
### 터미널에 익숙하지 않은 동료에게 넘기기
|
|
78
|
+
|
|
79
|
+
`skills/dooray-persona/references/bootstrap.md`에 붙여넣기용 프롬프트가 있다.
|
|
80
|
+
그 블록을 복사해 전달하면 받는 사람은 Claude Code에 한 번 붙여넣는 것으로 CLI 설치, 인증 설정, 스킬 연결, 수집, 문서 생성, 주입까지 진행한다.
|
|
81
|
+
|
|
82
|
+
받는 사람이 직접 해야 하는 것은 둘이다. Claude Code 설치와 Dooray 개인 인증 토큰 발급이다.
|
|
83
|
+
토큰 발급은 웹 로그인이 필요해 자동화할 수 없고, 프롬프트가 발급 화면 주소까지만 안내한다.
|
|
84
|
+
|
|
85
|
+
Claude 데스크톱 앱은 사용자 컴퓨터의 파일과 명령을 기본 상태로 다루지 못한다.
|
|
86
|
+
문서는 Claude Code에서 만들고, 완성한 문서를 데스크톱 앱의 프로젝트 지식이나 스타일 설정에 붙여넣어 쓴다.
|
|
87
|
+
|
|
44
88
|
## 사용법
|
|
45
89
|
|
|
46
90
|
설정을 마치면 에이전트에게 한국어로 시키면 된다.
|
|
@@ -110,7 +154,8 @@ dooray post comment file upload <project> <number> <comment-id> <path>
|
|
|
110
154
|
```
|
|
111
155
|
|
|
112
156
|
이미지 확장자는 이미지 마크다운으로, 그 외 파일은 일반 링크로 댓글 본문에 추가한다.
|
|
113
|
-
`comment file list`는 웹 UI
|
|
157
|
+
`comment file list`는 웹 UI 첨부와 CLI 업로드 파일을 함께 보여주며 `출처` 열로 구분한다.
|
|
158
|
+
CLI로 올린 파일은 댓글의 첨부 카드가 아니라 본문 링크로 표시된다.
|
|
114
159
|
|
|
115
160
|
### 삭제 명령의 확인
|
|
116
161
|
|
package/dist/index.js
CHANGED
|
@@ -157,15 +157,62 @@ async function clearMailCredentials() {
|
|
|
157
157
|
return hadCredentials;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// src/utils/body-input.ts
|
|
161
|
+
var import_promises2 = require("fs/promises");
|
|
162
|
+
async function readBodyInput(opts) {
|
|
163
|
+
if (opts.body != null && opts.bodyFile != null) {
|
|
164
|
+
throw new DoorayCliError(
|
|
165
|
+
"--body\uC640 --body-file\uC740 \uD568\uAED8 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
166
|
+
EXIT_PARAM_ERROR
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (opts.bodyFile) {
|
|
170
|
+
if (opts.bodyFile === "-") return readStdin();
|
|
171
|
+
return (0, import_promises2.readFile)(opts.bodyFile, "utf-8");
|
|
172
|
+
}
|
|
173
|
+
if (opts.body === "-") return readStdin();
|
|
174
|
+
return opts.body ?? "";
|
|
175
|
+
}
|
|
176
|
+
async function readBodyInputOrNull(opts) {
|
|
177
|
+
if (opts.body == null && opts.bodyFile == null) return null;
|
|
178
|
+
return readBodyInput(opts);
|
|
179
|
+
}
|
|
180
|
+
async function readStdin() {
|
|
181
|
+
if (process.stdin.isTTY) {
|
|
182
|
+
throw new DoorayCliError(
|
|
183
|
+
"stdin\uC5D0\uC11C \uC77D\uC73C\uB824\uBA74 \uD30C\uC774\uD504\uB85C \uB370\uC774\uD130\uB97C \uC804\uB2EC\uD574\uC8FC\uC138\uC694.",
|
|
184
|
+
EXIT_PARAM_ERROR
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const chunks = [];
|
|
188
|
+
for await (const chunk of process.stdin) {
|
|
189
|
+
chunks.push(chunk);
|
|
190
|
+
}
|
|
191
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/utils/config-value.ts
|
|
195
|
+
async function resolveConfigValue(raw, read = readStdin) {
|
|
196
|
+
if (raw !== "-") return raw;
|
|
197
|
+
const value = (await read()).trim();
|
|
198
|
+
if (value === "") {
|
|
199
|
+
throw new DoorayCliError(
|
|
200
|
+
"stdin \uC73C\uB85C \uBC1B\uC740 \uAC12\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
201
|
+
EXIT_PARAM_ERROR
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
160
207
|
// src/commands/config.ts
|
|
161
208
|
function maskApiKey(key) {
|
|
162
209
|
if (key.length <= 8) return "****";
|
|
163
210
|
return key.slice(0, 4) + "****" + key.slice(-4);
|
|
164
211
|
}
|
|
165
212
|
var configCommand = new import_commander.Command("config").description("CLI \uC124\uC815 \uAD00\uB9AC");
|
|
166
|
-
configCommand.command("set").description("\uC124\uC815 \uAC12 \uC800\uC7A5").argument("<key>", "\uC124\uC815 \uD0A4 (api-key, base-url)").argument("<value>", "\uC124\uC815 \uAC12").action(async (key, value) => {
|
|
213
|
+
configCommand.command("set").description("\uC124\uC815 \uAC12 \uC800\uC7A5").argument("<key>", "\uC124\uC815 \uD0A4 (api-key, base-url)").argument("<value>", "\uC124\uC815 \uAC12 (`-` \uC774\uBA74 stdin \uC5D0\uC11C \uC77D\uC74C)").action(async (key, value) => {
|
|
167
214
|
try {
|
|
168
|
-
await setConfigValue(key, value);
|
|
215
|
+
await setConfigValue(key, await resolveConfigValue(value));
|
|
169
216
|
console.log(import_chalk.default.green(`\u2713 ${key} \uC124\uC815 \uC644\uB8CC`));
|
|
170
217
|
} catch (err) {
|
|
171
218
|
if (err instanceof DoorayCliError) {
|
|
@@ -205,7 +252,7 @@ var import_commander2 = require("commander");
|
|
|
205
252
|
var import_chalk2 = __toESM(require("chalk"));
|
|
206
253
|
|
|
207
254
|
// src/cache/store.ts
|
|
208
|
-
var
|
|
255
|
+
var import_promises3 = require("fs/promises");
|
|
209
256
|
var import_node_path2 = require("path");
|
|
210
257
|
var import_node_os2 = require("os");
|
|
211
258
|
var CACHE_DIR = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".dooray", "cache");
|
|
@@ -220,11 +267,11 @@ var MEMBER_GROUPS_DIR = (0, import_node_path2.join)(CACHE_DIR, "member-groups");
|
|
|
220
267
|
var WIKIS_PATH = (0, import_node_path2.join)(CACHE_DIR, "wikis.json");
|
|
221
268
|
var TEMPLATES_DIR = (0, import_node_path2.join)(CACHE_DIR, "templates");
|
|
222
269
|
async function ensureDir2(dir) {
|
|
223
|
-
await (0,
|
|
270
|
+
await (0, import_promises3.mkdir)(dir, { recursive: true });
|
|
224
271
|
}
|
|
225
272
|
async function readJson(path6) {
|
|
226
273
|
try {
|
|
227
|
-
const raw = await (0,
|
|
274
|
+
const raw = await (0, import_promises3.readFile)(path6, "utf-8");
|
|
228
275
|
return JSON.parse(raw);
|
|
229
276
|
} catch {
|
|
230
277
|
return null;
|
|
@@ -233,7 +280,7 @@ async function readJson(path6) {
|
|
|
233
280
|
async function writeJson(path6, data) {
|
|
234
281
|
const dir = (0, import_node_path2.dirname)(path6);
|
|
235
282
|
await ensureDir2(dir);
|
|
236
|
-
await (0,
|
|
283
|
+
await (0, import_promises3.writeFile)(path6, JSON.stringify(data, null, 2) + "\n");
|
|
237
284
|
}
|
|
238
285
|
function isExpired(updatedAt, ttlMs) {
|
|
239
286
|
if (!updatedAt) return true;
|
|
@@ -322,7 +369,7 @@ async function setWikis(items) {
|
|
|
322
369
|
}
|
|
323
370
|
async function clearCache() {
|
|
324
371
|
try {
|
|
325
|
-
await (0,
|
|
372
|
+
await (0, import_promises3.rm)(CACHE_DIR, { recursive: true, force: true });
|
|
326
373
|
} catch {
|
|
327
374
|
}
|
|
328
375
|
}
|
|
@@ -331,31 +378,31 @@ async function getCacheStats() {
|
|
|
331
378
|
const projectCount = projects?.data.length ?? 0;
|
|
332
379
|
let memberProjectCount = 0;
|
|
333
380
|
try {
|
|
334
|
-
const files = await (0,
|
|
381
|
+
const files = await (0, import_promises3.readdir)(MEMBERS_DIR);
|
|
335
382
|
memberProjectCount = files.filter((f) => f.endsWith(".json")).length;
|
|
336
383
|
} catch {
|
|
337
384
|
}
|
|
338
385
|
let workflowProjectCount = 0;
|
|
339
386
|
try {
|
|
340
|
-
const files = await (0,
|
|
387
|
+
const files = await (0, import_promises3.readdir)(WORKFLOWS_DIR);
|
|
341
388
|
workflowProjectCount = files.filter((f) => f.endsWith(".json")).length;
|
|
342
389
|
} catch {
|
|
343
390
|
}
|
|
344
391
|
let tagProjectCount = 0;
|
|
345
392
|
try {
|
|
346
|
-
const files = await (0,
|
|
393
|
+
const files = await (0, import_promises3.readdir)(TAGS_DIR);
|
|
347
394
|
tagProjectCount = files.filter((f) => f.endsWith(".json")).length;
|
|
348
395
|
} catch {
|
|
349
396
|
}
|
|
350
397
|
let milestoneProjectCount = 0;
|
|
351
398
|
try {
|
|
352
|
-
const files = await (0,
|
|
399
|
+
const files = await (0, import_promises3.readdir)(MILESTONES_DIR);
|
|
353
400
|
milestoneProjectCount = files.filter((f) => f.endsWith(".json")).length;
|
|
354
401
|
} catch {
|
|
355
402
|
}
|
|
356
403
|
let memberGroupProjectCount = 0;
|
|
357
404
|
try {
|
|
358
|
-
const files = await (0,
|
|
405
|
+
const files = await (0, import_promises3.readdir)(MEMBER_GROUPS_DIR);
|
|
359
406
|
memberGroupProjectCount = files.filter((f) => f.endsWith(".json")).length;
|
|
360
407
|
} catch {
|
|
361
408
|
}
|
|
@@ -378,14 +425,14 @@ cacheCommand.command("refresh").description("\uCE90\uC2DC \uAC31\uC2E0 (API \uD0
|
|
|
378
425
|
var import_commander3 = require("commander");
|
|
379
426
|
var import_chalk3 = __toESM(require("chalk"));
|
|
380
427
|
var import_path = __toESM(require("path"));
|
|
381
|
-
var
|
|
428
|
+
var import_promises5 = __toESM(require("fs/promises"));
|
|
382
429
|
|
|
383
430
|
// src/skill/context.ts
|
|
384
431
|
var import_node_path3 = __toESM(require("path"));
|
|
385
432
|
var import_node_os3 = require("os");
|
|
386
433
|
|
|
387
434
|
// src/version.ts
|
|
388
|
-
var CLI_VERSION = true ? "0.
|
|
435
|
+
var CLI_VERSION = true ? "0.18.0" : "0.0.0-dev";
|
|
389
436
|
|
|
390
437
|
// src/skill/context.ts
|
|
391
438
|
function resolveSkillDataRoot(homeDir, xdgDataHome = process.env.XDG_DATA_HOME) {
|
|
@@ -925,7 +972,7 @@ async function installSkill(context, options = {}) {
|
|
|
925
972
|
|
|
926
973
|
// src/api/client.ts
|
|
927
974
|
var import_ky = __toESM(require("ky"));
|
|
928
|
-
var
|
|
975
|
+
var import_promises4 = require("fs/promises");
|
|
929
976
|
var import_node_path6 = require("path");
|
|
930
977
|
|
|
931
978
|
// src/utils/dooray-message.ts
|
|
@@ -937,6 +984,87 @@ function normalizeDoorayMessage(raw) {
|
|
|
937
984
|
}
|
|
938
985
|
}
|
|
939
986
|
|
|
987
|
+
// src/api/rate-limiter.ts
|
|
988
|
+
var DEFAULT_BURST_CAPACITY = 20;
|
|
989
|
+
var DEFAULT_REPLENISH_RATE = 5;
|
|
990
|
+
function defaultSleep(ms) {
|
|
991
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
992
|
+
}
|
|
993
|
+
function parseRateLimitHeaders(headers) {
|
|
994
|
+
const read = (name) => {
|
|
995
|
+
const raw = headers.get(name);
|
|
996
|
+
if (raw == null) return void 0;
|
|
997
|
+
const value = Number(raw);
|
|
998
|
+
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
999
|
+
};
|
|
1000
|
+
return {
|
|
1001
|
+
remaining: read("x-ratelimit-remaining"),
|
|
1002
|
+
burstCapacity: read("x-ratelimit-burst-capacity"),
|
|
1003
|
+
replenishRate: read("x-ratelimit-replenish-rate")
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function createTokenBucket(options = {}) {
|
|
1007
|
+
const now2 = options.now ?? (() => Date.now());
|
|
1008
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
1009
|
+
const positiveOr = (value, fallback) => value != null && value > 0 ? value : fallback;
|
|
1010
|
+
let capacity = positiveOr(options.capacity, DEFAULT_BURST_CAPACITY);
|
|
1011
|
+
let replenishRate = positiveOr(options.replenishRate, DEFAULT_REPLENISH_RATE);
|
|
1012
|
+
let tokens = capacity;
|
|
1013
|
+
let updatedAt = now2();
|
|
1014
|
+
let queue = Promise.resolve();
|
|
1015
|
+
function projected(at) {
|
|
1016
|
+
const elapsed = Math.max(0, at - updatedAt);
|
|
1017
|
+
return Math.min(capacity, tokens + elapsed / 1e3 * replenishRate);
|
|
1018
|
+
}
|
|
1019
|
+
function refill() {
|
|
1020
|
+
const at = now2();
|
|
1021
|
+
if (at <= updatedAt) return;
|
|
1022
|
+
tokens = projected(at);
|
|
1023
|
+
updatedAt = at;
|
|
1024
|
+
}
|
|
1025
|
+
async function take() {
|
|
1026
|
+
for (; ; ) {
|
|
1027
|
+
refill();
|
|
1028
|
+
if (tokens >= 1) {
|
|
1029
|
+
tokens -= 1;
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const waitMs = Math.max((1 - tokens) / replenishRate * 1e3, 10);
|
|
1033
|
+
await sleep(waitMs);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
acquire() {
|
|
1038
|
+
const next = queue.then(take);
|
|
1039
|
+
queue = next.then(
|
|
1040
|
+
() => void 0,
|
|
1041
|
+
() => void 0
|
|
1042
|
+
);
|
|
1043
|
+
return next;
|
|
1044
|
+
},
|
|
1045
|
+
sync(snapshot) {
|
|
1046
|
+
if (snapshot.burstCapacity != null && snapshot.burstCapacity > 0) {
|
|
1047
|
+
capacity = snapshot.burstCapacity;
|
|
1048
|
+
}
|
|
1049
|
+
if (snapshot.replenishRate != null && snapshot.replenishRate > 0) {
|
|
1050
|
+
replenishRate = snapshot.replenishRate;
|
|
1051
|
+
}
|
|
1052
|
+
if (snapshot.remaining != null) {
|
|
1053
|
+
refill();
|
|
1054
|
+
tokens = Math.min(tokens, Math.min(snapshot.remaining, capacity));
|
|
1055
|
+
updatedAt = now2();
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
drain() {
|
|
1059
|
+
tokens = 0;
|
|
1060
|
+
updatedAt = now2();
|
|
1061
|
+
},
|
|
1062
|
+
get available() {
|
|
1063
|
+
return projected(now2());
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
940
1068
|
// src/api/client.ts
|
|
941
1069
|
function joinIds(ids) {
|
|
942
1070
|
return ids && ids.length > 0 ? ids.join(",") : void 0;
|
|
@@ -968,10 +1096,42 @@ var DoorayApiClient = class {
|
|
|
968
1096
|
constructor(apiKey, baseUrl) {
|
|
969
1097
|
this.authHeader = `dooray-api ${apiKey}`;
|
|
970
1098
|
this.baseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
1099
|
+
const bucket = createTokenBucket();
|
|
971
1100
|
this.api = import_ky.default.create({
|
|
972
1101
|
prefix: baseUrl,
|
|
973
1102
|
headers: {
|
|
974
1103
|
Authorization: this.authHeader
|
|
1104
|
+
},
|
|
1105
|
+
// 429 는 재시도할 가치가 있지만, 동시 요청이 한꺼번에 재시도하면 같은 결과가 반복된다.
|
|
1106
|
+
// jitter 로 재시도 시점을 흩고, 아래 훅이 재시도분도 버킷을 거치게 한다.
|
|
1107
|
+
retry: {
|
|
1108
|
+
limit: 3,
|
|
1109
|
+
statusCodes: [408, 413, 429, 500, 502, 503, 504],
|
|
1110
|
+
backoffLimit: 8e3,
|
|
1111
|
+
jitter: true
|
|
1112
|
+
},
|
|
1113
|
+
hooks: {
|
|
1114
|
+
// 훅 순서는 실측했다 — beforeRequest → afterResponse → beforeRetry → afterResponse.
|
|
1115
|
+
// beforeRequest 는 첫 요청에만 돌고 재시도에는 돌지 않으므로,
|
|
1116
|
+
// beforeRetry 에서도 토큰을 받아야 재시도가 버킷을 우회하지 않는다.
|
|
1117
|
+
beforeRequest: [
|
|
1118
|
+
async () => {
|
|
1119
|
+
await bucket.acquire();
|
|
1120
|
+
}
|
|
1121
|
+
],
|
|
1122
|
+
beforeRetry: [
|
|
1123
|
+
async ({ error }) => {
|
|
1124
|
+
if (error instanceof import_ky.HTTPError && error.response.status === 429) {
|
|
1125
|
+
bucket.drain();
|
|
1126
|
+
}
|
|
1127
|
+
await bucket.acquire();
|
|
1128
|
+
}
|
|
1129
|
+
],
|
|
1130
|
+
afterResponse: [
|
|
1131
|
+
({ response }) => {
|
|
1132
|
+
bucket.sync(parseRateLimitHeaders(response.headers));
|
|
1133
|
+
}
|
|
1134
|
+
]
|
|
975
1135
|
}
|
|
976
1136
|
});
|
|
977
1137
|
}
|
|
@@ -1345,7 +1505,7 @@ var DoorayApiClient = class {
|
|
|
1345
1505
|
async uploadPostFile(projectId, postId, filePath) {
|
|
1346
1506
|
try {
|
|
1347
1507
|
const fileName = (0, import_node_path6.basename)(filePath);
|
|
1348
|
-
const fileBuffer = await (0,
|
|
1508
|
+
const fileBuffer = await (0, import_promises4.readFile)(filePath);
|
|
1349
1509
|
const formData = new FormData();
|
|
1350
1510
|
formData.append("file", new Blob([fileBuffer]), fileName);
|
|
1351
1511
|
const url = `${this.baseUrl}project/v1/projects/${projectId}/posts/${postId}/files`;
|
|
@@ -1390,7 +1550,7 @@ var DoorayApiClient = class {
|
|
|
1390
1550
|
async uploadWikiPageFile(wikiId, pageId, filePath, type) {
|
|
1391
1551
|
try {
|
|
1392
1552
|
const fileName = (0, import_node_path6.basename)(filePath);
|
|
1393
|
-
const fileBuffer = await (0,
|
|
1553
|
+
const fileBuffer = await (0, import_promises4.readFile)(filePath);
|
|
1394
1554
|
const buildFormData = () => {
|
|
1395
1555
|
const fd = new FormData();
|
|
1396
1556
|
fd.append("type", type);
|
|
@@ -1651,7 +1811,7 @@ var doctorCommand = new import_commander3.Command("doctor").description("\uC124\
|
|
|
1651
1811
|
process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
1652
1812
|
".claude"
|
|
1653
1813
|
);
|
|
1654
|
-
const claudeDirExists = await
|
|
1814
|
+
const claudeDirExists = await import_promises5.default.access(claudeDir).then(() => true).catch(() => false);
|
|
1655
1815
|
if (claudeDirExists) {
|
|
1656
1816
|
console.log(import_chalk3.default.bold("\n\u{1F527} Claude Code \uC2A4\uD0AC\n"));
|
|
1657
1817
|
const skillStatus = await inspectSkill(createSkillManagerContext());
|
|
@@ -1673,7 +1833,7 @@ var doctorCommand = new import_commander3.Command("doctor").description("\uC124\
|
|
|
1673
1833
|
var import_commander4 = require("commander");
|
|
1674
1834
|
var import_chalk4 = __toESM(require("chalk"));
|
|
1675
1835
|
var import_path2 = __toESM(require("path"));
|
|
1676
|
-
var
|
|
1836
|
+
var import_promises6 = __toESM(require("fs/promises"));
|
|
1677
1837
|
var setupCommand = new import_commander4.Command("setup").description("\uB300\uD654\uD615 \uCD08\uAE30 \uC124\uC815 \uB9C8\uBC95\uC0AC").action(async () => {
|
|
1678
1838
|
const { input: input2, select, password, confirm: confirm2 } = await import("@inquirer/prompts");
|
|
1679
1839
|
const existing = await getConfig();
|
|
@@ -1750,7 +1910,7 @@ var setupCommand = new import_commander4.Command("setup").description("\uB300\uD
|
|
|
1750
1910
|
process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
1751
1911
|
".claude"
|
|
1752
1912
|
);
|
|
1753
|
-
const claudeDirExists = await
|
|
1913
|
+
const claudeDirExists = await import_promises6.default.access(claudeDir).then(() => true).catch(() => false);
|
|
1754
1914
|
if (claudeDirExists) {
|
|
1755
1915
|
const isNpx = /_npx[/\\]/.test(__dirname) || /\.npm[/\\]_npx/.test(__dirname) || /npx-/.test(__dirname);
|
|
1756
1916
|
if (isNpx) {
|
|
@@ -2992,7 +3152,7 @@ async function resolvePostRef(client, ref) {
|
|
|
2992
3152
|
|
|
2993
3153
|
// src/editor/index.ts
|
|
2994
3154
|
var import_node_child_process = require("child_process");
|
|
2995
|
-
var
|
|
3155
|
+
var import_promises7 = __toESM(require("fs/promises"));
|
|
2996
3156
|
var import_tmp = __toESM(require("tmp"));
|
|
2997
3157
|
var import_js_yaml = require("js-yaml");
|
|
2998
3158
|
function openInEditor(content) {
|
|
@@ -3006,7 +3166,7 @@ function openInEditor(content) {
|
|
|
3006
3166
|
const tmpFile = import_tmp.default.fileSync({ prefix: "dooray-", postfix: ".md" });
|
|
3007
3167
|
return new Promise(async (resolve, reject) => {
|
|
3008
3168
|
try {
|
|
3009
|
-
await
|
|
3169
|
+
await import_promises7.default.writeFile(tmpFile.name, content, "utf-8");
|
|
3010
3170
|
const child = (0, import_node_child_process.spawn)(editor2, [tmpFile.name], {
|
|
3011
3171
|
stdio: "inherit"
|
|
3012
3172
|
});
|
|
@@ -3022,7 +3182,7 @@ function openInEditor(content) {
|
|
|
3022
3182
|
EXIT_PARAM_ERROR
|
|
3023
3183
|
);
|
|
3024
3184
|
}
|
|
3025
|
-
const result = await
|
|
3185
|
+
const result = await import_promises7.default.readFile(tmpFile.name, "utf-8");
|
|
3026
3186
|
resolve(result);
|
|
3027
3187
|
} catch (e) {
|
|
3028
3188
|
reject(e);
|
|
@@ -3115,50 +3275,23 @@ function parseWikiFrontmatter(content) {
|
|
|
3115
3275
|
};
|
|
3116
3276
|
}
|
|
3117
3277
|
|
|
3118
|
-
// src/utils/body-input.ts
|
|
3119
|
-
var import_promises7 = require("fs/promises");
|
|
3120
|
-
async function readBodyInput(opts) {
|
|
3121
|
-
if (opts.body != null && opts.bodyFile != null) {
|
|
3122
|
-
throw new DoorayCliError(
|
|
3123
|
-
"--body\uC640 --body-file\uC740 \uD568\uAED8 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
3124
|
-
EXIT_PARAM_ERROR
|
|
3125
|
-
);
|
|
3126
|
-
}
|
|
3127
|
-
if (opts.bodyFile) {
|
|
3128
|
-
if (opts.bodyFile === "-") return readStdin();
|
|
3129
|
-
return (0, import_promises7.readFile)(opts.bodyFile, "utf-8");
|
|
3130
|
-
}
|
|
3131
|
-
if (opts.body === "-") return readStdin();
|
|
3132
|
-
return opts.body ?? "";
|
|
3133
|
-
}
|
|
3134
|
-
async function readBodyInputOrNull(opts) {
|
|
3135
|
-
if (opts.body == null && opts.bodyFile == null) return null;
|
|
3136
|
-
return readBodyInput(opts);
|
|
3137
|
-
}
|
|
3138
|
-
async function readStdin() {
|
|
3139
|
-
if (process.stdin.isTTY) {
|
|
3140
|
-
throw new DoorayCliError(
|
|
3141
|
-
"stdin\uC5D0\uC11C \uC77D\uC73C\uB824\uBA74 \uD30C\uC774\uD504\uB85C \uB370\uC774\uD130\uB97C \uC804\uB2EC\uD574\uC8FC\uC138\uC694.",
|
|
3142
|
-
EXIT_PARAM_ERROR
|
|
3143
|
-
);
|
|
3144
|
-
}
|
|
3145
|
-
const chunks = [];
|
|
3146
|
-
for await (const chunk of process.stdin) {
|
|
3147
|
-
chunks.push(chunk);
|
|
3148
|
-
}
|
|
3149
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
3150
|
-
}
|
|
3151
|
-
|
|
3152
3278
|
// src/utils/attachment-check.ts
|
|
3153
3279
|
var import_node_readline = __toESM(require("readline"));
|
|
3154
|
-
function
|
|
3155
|
-
const
|
|
3156
|
-
const
|
|
3280
|
+
function extractAttachmentReferences(body) {
|
|
3281
|
+
const references = [];
|
|
3282
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3283
|
+
const re = /!?\[([^\]]*)\]\(\/files\/([^\s)?#]+)/g;
|
|
3157
3284
|
let m;
|
|
3158
3285
|
while ((m = re.exec(body)) !== null) {
|
|
3159
|
-
|
|
3286
|
+
const id = m[2];
|
|
3287
|
+
if (seen.has(id)) continue;
|
|
3288
|
+
seen.add(id);
|
|
3289
|
+
references.push({ id, label: m[1] });
|
|
3160
3290
|
}
|
|
3161
|
-
return
|
|
3291
|
+
return references;
|
|
3292
|
+
}
|
|
3293
|
+
function extractAttachmentFileIds(body) {
|
|
3294
|
+
return new Set(extractAttachmentReferences(body).map((reference) => reference.id));
|
|
3162
3295
|
}
|
|
3163
3296
|
function findDroppedAttachments(oldBody, newBody, attachments) {
|
|
3164
3297
|
const oldIds = extractAttachmentFileIds(oldBody);
|
|
@@ -4117,6 +4250,14 @@ var commentDeleteCommand = new import_commander23.Command("delete").description(
|
|
|
4117
4250
|
// src/commands/post/comment/get.ts
|
|
4118
4251
|
var import_commander24 = require("commander");
|
|
4119
4252
|
|
|
4253
|
+
// src/utils/format-size.ts
|
|
4254
|
+
function formatSize(bytes) {
|
|
4255
|
+
if (bytes == null) return "-";
|
|
4256
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
4257
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
4258
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4120
4261
|
// src/formatters/comment.ts
|
|
4121
4262
|
function formatCommentDetail(comment, opts) {
|
|
4122
4263
|
if (opts.json) {
|
|
@@ -4140,7 +4281,7 @@ function formatCommentDetail(comment, opts) {
|
|
|
4140
4281
|
process.stdout.write("\n[Attachments]\n");
|
|
4141
4282
|
printTable(
|
|
4142
4283
|
["Name", "Size", "ID"],
|
|
4143
|
-
comment.files.map((f) => [f.name,
|
|
4284
|
+
comment.files.map((f) => [f.name ?? "-", formatSize(f.size), f.id])
|
|
4144
4285
|
);
|
|
4145
4286
|
}
|
|
4146
4287
|
}
|
|
@@ -4296,13 +4437,54 @@ async function resolveCommentFileInput(client, args) {
|
|
|
4296
4437
|
return { projectId, postId, commentId, secondary };
|
|
4297
4438
|
}
|
|
4298
4439
|
|
|
4440
|
+
// src/utils/comment-file-merge.ts
|
|
4441
|
+
function labelOrNull(label) {
|
|
4442
|
+
if (label === void 0 || label.length === 0) return null;
|
|
4443
|
+
return label;
|
|
4444
|
+
}
|
|
4445
|
+
function mergeCommentFiles(input2) {
|
|
4446
|
+
const postFilesById = new Map(input2.postFiles.map((file) => [file.id, file]));
|
|
4447
|
+
const bodyRefsById = /* @__PURE__ */ new Map();
|
|
4448
|
+
for (const reference of input2.bodyRefs) {
|
|
4449
|
+
if (!bodyRefsById.has(reference.id)) bodyRefsById.set(reference.id, reference);
|
|
4450
|
+
}
|
|
4451
|
+
const merged = [];
|
|
4452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4453
|
+
for (const commentFile of input2.commentFiles) {
|
|
4454
|
+
if (seen.has(commentFile.id)) continue;
|
|
4455
|
+
seen.add(commentFile.id);
|
|
4456
|
+
const postFile = postFilesById.get(commentFile.id);
|
|
4457
|
+
const bodyRef = bodyRefsById.get(commentFile.id);
|
|
4458
|
+
merged.push({
|
|
4459
|
+
id: commentFile.id,
|
|
4460
|
+
name: postFile?.name ?? commentFile.name ?? labelOrNull(bodyRef?.label),
|
|
4461
|
+
size: postFile?.size ?? null,
|
|
4462
|
+
mimeType: postFile?.mimeType ?? null,
|
|
4463
|
+
source: bodyRef ? "both" : "attachment"
|
|
4464
|
+
});
|
|
4465
|
+
}
|
|
4466
|
+
for (const bodyRef of input2.bodyRefs) {
|
|
4467
|
+
if (seen.has(bodyRef.id)) continue;
|
|
4468
|
+
seen.add(bodyRef.id);
|
|
4469
|
+
const postFile = postFilesById.get(bodyRef.id);
|
|
4470
|
+
merged.push({
|
|
4471
|
+
id: bodyRef.id,
|
|
4472
|
+
name: postFile?.name ?? labelOrNull(bodyRef.label),
|
|
4473
|
+
size: postFile?.size ?? null,
|
|
4474
|
+
mimeType: postFile?.mimeType ?? null,
|
|
4475
|
+
source: "body-link"
|
|
4476
|
+
});
|
|
4477
|
+
}
|
|
4478
|
+
return merged;
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4299
4481
|
// src/commands/post/comment/file/list.ts
|
|
4300
|
-
function
|
|
4301
|
-
if (
|
|
4302
|
-
if (
|
|
4303
|
-
return
|
|
4482
|
+
function formatSource(source) {
|
|
4483
|
+
if (source === "attachment") return "\uCCA8\uBD80";
|
|
4484
|
+
if (source === "body-link") return "\uBCF8\uBB38 \uB9C1\uD06C";
|
|
4485
|
+
return "\uB458 \uB2E4";
|
|
4304
4486
|
}
|
|
4305
|
-
var listCommentFileCommand = new import_commander25.Command("list").description("\uB313\uAE00 \uCCA8\uBD80 \uD30C\uC77C \uBAA9\uB85D \uC870\uD68C").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uB313\uAE00 ID").argument("[arg2]", "\uC5C5\uBB34 \uBC88\uD638 (positional \uBAA8\uB4DC)").argument("[arg3]", "\uB313\uAE00 ID (positional 3\uAC1C \uBAA8\uB4DC)").option("--id <postId>", "Dooray post ID (project/post-number \uB300\uC2E0)").option("--url <url>", "Dooray \uC5C5\uBB34 URL (project/post-number \uB300\uC2E0)").option("--comment-id <logId>", "\uB313\uAE00 ID (positional \uB300\uCCB4)").action(async (arg1, arg2, arg3, opts) => {
|
|
4487
|
+
var listCommentFileCommand = new import_commander25.Command("list").description("\uB313\uAE00 \uCCA8\uBD80 \uD30C\uC77C\uACFC \uBCF8\uBB38 \uD30C\uC77C \uB9C1\uD06C \uBAA9\uB85D \uD1B5\uD569 \uC870\uD68C").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uB313\uAE00 ID").argument("[arg2]", "\uC5C5\uBB34 \uBC88\uD638 (positional \uBAA8\uB4DC)").argument("[arg3]", "\uB313\uAE00 ID (positional 3\uAC1C \uBAA8\uB4DC)").option("--id <postId>", "Dooray post ID (project/post-number \uB300\uC2E0)").option("--url <url>", "Dooray \uC5C5\uBB34 URL (project/post-number \uB300\uC2E0)").option("--comment-id <logId>", "\uB313\uAE00 ID (positional \uB300\uCCB4)").action(async (arg1, arg2, arg3, opts) => {
|
|
4306
4488
|
const globalOpts = listCommentFileCommand.optsWithGlobals();
|
|
4307
4489
|
const config = await getConfigOrThrow();
|
|
4308
4490
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
@@ -4316,23 +4498,50 @@ var listCommentFileCommand = new import_commander25.Command("list").description(
|
|
|
4316
4498
|
requireSecondary: false
|
|
4317
4499
|
});
|
|
4318
4500
|
startSpinner("\uB313\uAE00 \uCCA8\uBD80 \uD30C\uC77C \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
if (
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4501
|
+
try {
|
|
4502
|
+
const res = await client.getPostComment(projectId, postId, commentId);
|
|
4503
|
+
const commentFiles = res.result.files ?? [];
|
|
4504
|
+
const bodyRefs = extractAttachmentReferences(res.result.body.content);
|
|
4505
|
+
if (commentFiles.length === 0 && bodyRefs.length === 0) {
|
|
4506
|
+
stopSpinner(true, "\uCCA8\uBD80 \uD30C\uC77C 0\uAC1C");
|
|
4507
|
+
if (globalOpts.json) {
|
|
4508
|
+
process.stdout.write("[]\n");
|
|
4509
|
+
} else if (!globalOpts.quiet) {
|
|
4510
|
+
process.stdout.write("\uCCA8\uBD80 \uC5C6\uC74C\n");
|
|
4511
|
+
}
|
|
4512
|
+
return;
|
|
4327
4513
|
}
|
|
4328
|
-
|
|
4514
|
+
let postFiles = [];
|
|
4515
|
+
let metadataLookupError = null;
|
|
4516
|
+
try {
|
|
4517
|
+
const postFilesRes = await client.getPostFiles(projectId, postId);
|
|
4518
|
+
postFiles = postFilesRes.result;
|
|
4519
|
+
} catch (error) {
|
|
4520
|
+
metadataLookupError = error instanceof Error ? error.message : String(error);
|
|
4521
|
+
}
|
|
4522
|
+
const merged = mergeCommentFiles({ commentFiles, bodyRefs, postFiles });
|
|
4523
|
+
stopSpinner(true, `\uCCA8\uBD80 \uD30C\uC77C ${merged.length}\uAC1C`);
|
|
4524
|
+
if (metadataLookupError !== null) {
|
|
4525
|
+
process.stderr.write(
|
|
4526
|
+
`\u26A0 \uC5C5\uBB34 \uCCA8\uBD80 \uC774\uB984\xB7\uD06C\uAE30 \uBCF4\uAC15 \uC2E4\uD328 (${metadataLookupError}) \u2014 \uC77C\uBD80 \uD56D\uBAA9\uC758 \uD30C\uC77C\uBA85\xB7\uD06C\uAE30\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.
|
|
4527
|
+
`
|
|
4528
|
+
);
|
|
4529
|
+
}
|
|
4530
|
+
output(globalOpts, {
|
|
4531
|
+
headers: ["\uD30C\uC77C\uBA85", "\uD06C\uAE30", "\uCD9C\uCC98", "ID"],
|
|
4532
|
+
rows: merged.map((file) => [
|
|
4533
|
+
file.name == null ? "-" : sanitizeFileName(file.name),
|
|
4534
|
+
formatSize(file.size),
|
|
4535
|
+
formatSource(file.source),
|
|
4536
|
+
file.id
|
|
4537
|
+
]),
|
|
4538
|
+
raw: merged,
|
|
4539
|
+
ids: merged.map((file) => file.id)
|
|
4540
|
+
});
|
|
4541
|
+
} catch (error) {
|
|
4542
|
+
stopSpinner(false);
|
|
4543
|
+
throw error;
|
|
4329
4544
|
}
|
|
4330
|
-
output(globalOpts, {
|
|
4331
|
-
headers: ["\uD30C\uC77C\uBA85", "\uD06C\uAE30", "ID"],
|
|
4332
|
-
rows: files.map((f) => [f.name, formatSize(f.size), f.id]),
|
|
4333
|
-
raw: files,
|
|
4334
|
-
ids: files.map((f) => f.id)
|
|
4335
|
-
});
|
|
4336
4545
|
});
|
|
4337
4546
|
|
|
4338
4547
|
// src/commands/post/comment/file/upload.ts
|
|
@@ -4359,7 +4568,7 @@ function removeFileReference(body, fileId) {
|
|
|
4359
4568
|
}
|
|
4360
4569
|
|
|
4361
4570
|
// src/commands/post/comment/file/upload.ts
|
|
4362
|
-
var uploadCommentFileCommand = new import_commander26.Command("upload").description("\uB313\uAE00\uC5D0 \uD30C\uC77C \uC5C5\uB85C\uB4DC (\
|
|
4571
|
+
var uploadCommentFileCommand = new import_commander26.Command("upload").description("\uB313\uAE00\uC5D0 \uD30C\uC77C \uC5C5\uB85C\uB4DC (\uCCA8\uBD80 \uCE74\uB4DC\uAC00 \uC544\uB2CC \uBCF8\uBB38 \uB9C1\uD06C\uB85C \uD45C\uC2DC)").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uB313\uAE00 ID").argument("[arg2]", "\uC5C5\uBB34 \uBC88\uD638 \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C \uACBD\uB85C").argument("[arg3]", "\uB313\uAE00 ID (positional \uBAA8\uB4DC)").argument("[arg4]", "\uD30C\uC77C \uACBD\uB85C (positional \uBAA8\uB4DC)").option("--id <postId>", "Dooray post ID (project/post-number \uB300\uC2E0)").option("--url <url>", "Dooray \uC5C5\uBB34 URL (project/post-number \uB300\uC2E0)").option("--comment-id <logId>", "\uB313\uAE00 ID (positional \uB300\uCCB4)").option("--file <path>", "\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uACBD\uB85C (positional \uB300\uCCB4)").action(async (arg1, arg2, arg3, arg4, opts) => {
|
|
4363
4572
|
const globalOpts = uploadCommentFileCommand.optsWithGlobals();
|
|
4364
4573
|
const config = await getConfigOrThrow();
|
|
4365
4574
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
@@ -4502,11 +4711,6 @@ var commentFileCommand = new import_commander29.Command("file").description("\uB
|
|
|
4502
4711
|
|
|
4503
4712
|
// src/commands/post/file/list.ts
|
|
4504
4713
|
var import_commander30 = require("commander");
|
|
4505
|
-
function formatSize2(bytes) {
|
|
4506
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
4507
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
4508
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
4509
|
-
}
|
|
4510
4714
|
var fileListCommand = new import_commander30.Command("list").description("\uC5C5\uBB34 \uCCA8\uBD80\uD30C\uC77C \uBAA9\uB85D \uC870\uD68C").argument("[project]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (\uB610\uB294 \uCCAB \uC778\uC790\uC5D0 Dooray URL)").argument("[post-number]", "\uC5C5\uBB34 \uBC88\uD638 (project\uC640 \uD568\uAED8 \uC0AC\uC6A9)").option("--id <postId>", "Dooray post ID (project/post-number \uB300\uC2E0)").option("--url <url>", "Dooray \uC5C5\uBB34 URL (project/post-number \uB300\uC2E0)").action(async (project, postNumberStr, opts) => {
|
|
4511
4715
|
const globalOpts = fileListCommand.optsWithGlobals();
|
|
4512
4716
|
const config = await getConfigOrThrow();
|
|
@@ -4525,7 +4729,7 @@ var fileListCommand = new import_commander30.Command("list").description("\uC5C5
|
|
|
4525
4729
|
rows: res.result.map((f) => [
|
|
4526
4730
|
f.id,
|
|
4527
4731
|
f.name,
|
|
4528
|
-
|
|
4732
|
+
formatSize(f.size),
|
|
4529
4733
|
f.mimeType,
|
|
4530
4734
|
f.createdAt
|
|
4531
4735
|
]),
|
|
@@ -5190,11 +5394,6 @@ var import_commander47 = require("commander");
|
|
|
5190
5394
|
|
|
5191
5395
|
// src/commands/wiki/page-file/list.ts
|
|
5192
5396
|
var import_commander42 = require("commander");
|
|
5193
|
-
function formatSize3(bytes) {
|
|
5194
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
5195
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
5196
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
5197
|
-
}
|
|
5198
5397
|
var wikiPageFileListCommand = new import_commander42.Command("list").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uCCA8\uBD80\uD30C\uC77C \uBAA9\uB85D \uC870\uD68C (general + inline image)").argument("[project]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (\uB610\uB294 \uCCAB \uC778\uC790\uC5D0 Dooray Wiki URL)").argument("[page-id]", "\uC704\uD0A4 \uD398\uC774\uC9C0 ID (project\uC640 \uD568\uAED8 \uC0AC\uC6A9)").option("--id <pageId>", "\uC704\uD0A4 \uD398\uC774\uC9C0 ID (--project \uB3D9\uBC18 \uD544\uC694)").option("--url <url>", "Dooray Wiki URL").option("--project <code>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (--id \uBAA8\uB4DC\uC5D0\uC11C wikiId \uD574\uC11D\uC6A9)").action(async (project, pageIdArg, opts) => {
|
|
5199
5398
|
const globalOpts = wikiPageFileListCommand.optsWithGlobals();
|
|
5200
5399
|
const config = await getConfigOrThrow();
|
|
@@ -5215,7 +5414,7 @@ var wikiPageFileListCommand = new import_commander42.Command("list").description
|
|
|
5215
5414
|
stopSpinner(true, `\uCCA8\uBD80\uD30C\uC77C ${merged.length}\uAC1C (general ${files.length} + inline ${images.length})`);
|
|
5216
5415
|
output(globalOpts, {
|
|
5217
5416
|
headers: ["ID", "Type", "\uD30C\uC77C\uBA85", "\uD06C\uAE30"],
|
|
5218
|
-
rows: merged.map((f) => [f.id, f.type, f.name,
|
|
5417
|
+
rows: merged.map((f) => [f.id, f.type, f.name, formatSize(f.size)]),
|
|
5219
5418
|
raw: merged,
|
|
5220
5419
|
ids: merged.map((f) => f.id)
|
|
5221
5420
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bifos/dooray-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "CLI tool for Dooray project management — AI agent & terminal friendly",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dooray",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
|
-
"skills",
|
|
25
|
+
"skills/dooray-cli",
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"engines": {
|
|
@@ -41,6 +41,7 @@ NHN Dooray REST API 를 래핑한 CLI 다. 이 파일은 라우터이므로, 작
|
|
|
41
41
|
- 조회는 `--json` 으로 먼저 실행해 응답 구조를 확인한 뒤 쓰기 명령으로 넘어간다
|
|
42
42
|
- 쓰기 명령은 대상 ID 를 명시하고, 지원하면 `--dry-run` 으로 먼저 확인한다
|
|
43
43
|
- 이름 기반 조회(멤버·그룹·워크플로우·태그)는 부분일치를 지원한다. 모호하면 에러와 후보 목록이 나오므로 임의로 고르지 말고 사용자에게 확인한다
|
|
44
|
+
- 멤버를 이름으로 찾는 것은 그 프로젝트의 멤버로 한정된다. 비멤버는 이메일이나 memberId 로 지정한다 — [post.md](references/post.md)
|
|
44
45
|
- 실패하면 [common.md](references/common.md) 의 에러 처리 표와 대조한다
|
|
45
46
|
|
|
46
47
|
## 파일 명령의 `--json` 스키마
|
|
@@ -159,7 +160,13 @@ NHN Dooray REST API 를 래핑한 CLI 다. 이 파일은 라우터이므로, 작
|
|
|
159
160
|
| 댓글 첨부 삭제 | `dooray post comment file delete <project> <number> <comment-id> <file-id>` — 확인 있음, `-y`/`--yes`로 생략 |
|
|
160
161
|
|
|
161
162
|
- 댓글 파일 업로드는 이미지 확장자면 이미지 마크다운을, 그 외에는 일반 링크를 만든다.
|
|
162
|
-
- `comment file list
|
|
163
|
+
- `comment file list`의 `출처`는 다음과 같다.
|
|
164
|
+
- `attachment` (`첨부`): 웹 UI 첨부
|
|
165
|
+
- `body-link` (`본문 링크`): CLI 업로드 링크
|
|
166
|
+
- `both` (`둘 다`): 양쪽에 있는 파일
|
|
167
|
+
- `--json` 항목은 `{ id, name, size, mimeType, source }` 형식이다.
|
|
168
|
+
`source`는 위 값 중 하나다.
|
|
169
|
+
- 메타데이터를 채우지 못하면 `name`, `size`, `mimeType`은 `null`이다.
|
|
163
170
|
|
|
164
171
|
## 위키
|
|
165
172
|
|
|
@@ -22,6 +22,15 @@ dooray config set api-key <YOUR_API_TOKEN> # https://{org}.dooray.com/setting/
|
|
|
22
22
|
dooray doctor # 설정 검증
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
값 자리에 `-` 를 주면 stdin 에서 읽는다. 토큰을 명령 인자로 넘기지 않을 때 쓴다.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
printf '%s' "$TOKEN" | dooray config set api-key -
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
인자로 넘긴 값은 셸 기록과 프로세스 목록에 남는다.
|
|
32
|
+
stdin 값은 양끝 공백을 지운 뒤 저장하고, 비어 있으면 저장하지 않고 종료 코드 3 으로 끝낸다.
|
|
33
|
+
|
|
25
34
|
## Claude Code 스킬 관리
|
|
26
35
|
|
|
27
36
|
```bash
|
|
@@ -61,6 +61,17 @@ title 속성도 없다.
|
|
|
61
61
|
title 은 workflow class 다 — `registered` / `working` / `closed` / `backlog`.
|
|
62
62
|
클릭하면 브라우저가 아니라 Dooray 앱 안에서 이동하며 workflow 상태가 함께 보인다.
|
|
63
63
|
|
|
64
|
+
**표시 텍스트의 대괄호는 엔티티로 바꾼다.** `[` 는 `[`, `]` 는 `]` 다.
|
|
65
|
+
제목에 모듈명을 대괄호로 붙이는 팀이 많은데, 조회한 `subject` 를 그대로 넣으면 링크 문법과 충돌해 깨진다.
|
|
66
|
+
|
|
67
|
+
| 입력 | 결과 |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `[my-project/524 [MOD] 한도 분리](dooray://...)` | 링크 깨짐 |
|
|
70
|
+
| `[my-project/524 [MOD] 한도 분리](dooray://...)` | 정상 |
|
|
71
|
+
|
|
72
|
+
치환 대상은 표시 텍스트뿐이고 URL 은 해당 없다.
|
|
73
|
+
`--link-task` 옵션을 쓰면 CLI 가 알아서 치환하므로 손으로 조립할 때만 신경 쓴다.
|
|
74
|
+
|
|
64
75
|
### 위키 페이지
|
|
65
76
|
|
|
66
77
|
```markdown
|
|
@@ -68,13 +79,31 @@ title 은 workflow class 다 — `registered` / `working` / `closed` / `backlog`
|
|
|
68
79
|
```
|
|
69
80
|
|
|
70
81
|
업무 링크와 같은 구조이고 경로만 `pages/{pageId}` 로 다르다. title 은 페이지 상태다.
|
|
82
|
+
표시 텍스트의 대괄호 치환도 업무 링크와 같다.
|
|
71
83
|
|
|
72
84
|
### ID 를 얻는 곳
|
|
73
85
|
|
|
74
86
|
| ID | 얻는 방법 |
|
|
75
87
|
| --- | --- |
|
|
76
88
|
| `orgId` | `~/.dooray/cache/me.json` 의 `data.orgId` |
|
|
77
|
-
| `memberId` |
|
|
89
|
+
| `memberId` | 응답에 있으면 그 값을 쓴다 (아래 참조). 없을 때만 `dooray member search <name>` |
|
|
78
90
|
| `groupId` | `dooray project groups <project>` |
|
|
79
91
|
| `postId` | `dooray post get <project> <number> --json` 의 `id` |
|
|
80
92
|
| `pageId` | `dooray wiki page get <project> <page-id> --json` 의 `id` |
|
|
93
|
+
|
|
94
|
+
### 답장 대상은 이름으로 찾지 않는다
|
|
95
|
+
|
|
96
|
+
답장할 상대의 ID 는 이미 조회 응답 안에 있다. 검색할 필요가 없다.
|
|
97
|
+
|
|
98
|
+
| 대상 | 응답의 위치 |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| 업무 작성자 | `post get ... --json` 의 `users.from.member.organizationMemberId` |
|
|
101
|
+
| 댓글 작성자 | `post comment list ... --json` 의 `creator.member.organizationMemberId` |
|
|
102
|
+
|
|
103
|
+
이 값을 쓰면 표시 이름이 한자든 영문이든 닉네임이든 항상 정확하다.
|
|
104
|
+
|
|
105
|
+
**검색 결과가 1건이어도 확정 근거로 삼지 않는다.**
|
|
106
|
+
표시 이름이 한자로 되어 있으면 한글 이름 검색에 원 작성자가 아예 안 잡히고, 표기가 비슷한 **다른 사람만** 걸린다.
|
|
107
|
+
후보가 여럿이면 모호하다는 신호라도 있지만, 하나뿐이면 오히려 확신하게 되는 것이 함정이다.
|
|
108
|
+
|
|
109
|
+
이름 검색은 대상이 응답에 없을 때만 쓰고, 그때도 이메일이나 사번으로 교차 확인한다.
|
|
@@ -104,8 +104,14 @@ dooray post comment get <project> <number> <comment-id> --json | jq -r '.body.co
|
|
|
104
104
|
|
|
105
105
|
첨부를 정말 떼려는 것이면 `--no-confirm` 으로 진행한다.
|
|
106
106
|
|
|
107
|
-
`comment file list`는
|
|
108
|
-
|
|
107
|
+
`comment file list`는 웹 UI 첨부와 CLI 업로드 링크를 함께 보여주고 `출처` 열로 구분한다.
|
|
108
|
+
|
|
109
|
+
- `attachment`: 웹 UI 첨부
|
|
110
|
+
- `body-link`: CLI 업로드 링크
|
|
111
|
+
- `both`: 양쪽에 있는 파일
|
|
112
|
+
|
|
113
|
+
`--json` 항목은 `{ id, name, size, mimeType, source }` 형식이다.
|
|
114
|
+
메타데이터를 채우지 못하면 `name`, `size`, `mimeType`은 `null`이다.
|
|
109
115
|
|
|
110
116
|
## 이름이 겹칠 때
|
|
111
117
|
|
|
@@ -124,6 +130,20 @@ dooray post edit --id "$POST_ID" --cc "$MEMBER_ID"
|
|
|
124
130
|
- 이메일 형태 → exact 매칭
|
|
125
131
|
- 그 외 → 이름 부분일치
|
|
126
132
|
|
|
133
|
+
**이름은 그 프로젝트의 멤버만 찾는다.** 이메일과 memberId 는 조직 전체에서 찾는다.
|
|
134
|
+
|
|
135
|
+
그래서 프로젝트 멤버가 아닌 사람을 이름으로 지정하면 실패한다.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
dooray post create <project> --to 홍길동 # 프로젝트 멤버가 아니면 실패
|
|
139
|
+
dooray post create <project> --to user@example.com # 통과
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
에러에 붙는 "사용 가능한 멤버 (N/M)" 은 CLI 의 조회 한계가 아니라 **그 프로젝트의 멤버 목록**이다.
|
|
143
|
+
일부만 가져온 것으로 오해하기 쉬운 표기다.
|
|
144
|
+
|
|
145
|
+
대상이 그 프로젝트 멤버인지 확실하지 않으면 처음부터 이메일을 쓴다. 이메일은 `dooray member search <이름>` 으로 찾는다.
|
|
146
|
+
|
|
127
147
|
## 부모 업무 지정
|
|
128
148
|
|
|
129
149
|
```bash
|