@bifos/dooray-cli 0.16.0 → 0.17.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
@@ -110,7 +110,8 @@ dooray post comment file upload <project> <number> <comment-id> <path>
110
110
  ```
111
111
 
112
112
  이미지 확장자는 이미지 마크다운으로, 그 외 파일은 일반 링크로 댓글 본문에 추가한다.
113
- `comment file list`는 웹 UI에서 직접 첨부한 파일을 놓칠 있으며, 경우 `post file list`로 확인한다.
113
+ `comment file list`는 웹 UI 첨부와 CLI 업로드 파일을 함께 보여주며 `출처` 열로 구분한다.
114
+ CLI로 올린 파일은 댓글의 첨부 카드가 아니라 본문 링크로 표시된다.
114
115
 
115
116
  ### 삭제 명령의 확인
116
117
 
package/dist/index.js CHANGED
@@ -385,7 +385,7 @@ var import_node_path3 = __toESM(require("path"));
385
385
  var import_node_os3 = require("os");
386
386
 
387
387
  // src/version.ts
388
- var CLI_VERSION = true ? "0.16.0" : "0.0.0-dev";
388
+ var CLI_VERSION = true ? "0.17.0" : "0.0.0-dev";
389
389
 
390
390
  // src/skill/context.ts
391
391
  function resolveSkillDataRoot(homeDir, xdgDataHome = process.env.XDG_DATA_HOME) {
@@ -3151,14 +3151,21 @@ async function readStdin() {
3151
3151
 
3152
3152
  // src/utils/attachment-check.ts
3153
3153
  var import_node_readline = __toESM(require("readline"));
3154
- function extractAttachmentFileIds(body) {
3155
- const ids = /* @__PURE__ */ new Set();
3156
- const re = /!?\[[^\]]*\]\(\/files\/([^\s)?#]+)/g;
3154
+ function extractAttachmentReferences(body) {
3155
+ const references = [];
3156
+ const seen = /* @__PURE__ */ new Set();
3157
+ const re = /!?\[([^\]]*)\]\(\/files\/([^\s)?#]+)/g;
3157
3158
  let m;
3158
3159
  while ((m = re.exec(body)) !== null) {
3159
- ids.add(m[1]);
3160
+ const id = m[2];
3161
+ if (seen.has(id)) continue;
3162
+ seen.add(id);
3163
+ references.push({ id, label: m[1] });
3160
3164
  }
3161
- return ids;
3165
+ return references;
3166
+ }
3167
+ function extractAttachmentFileIds(body) {
3168
+ return new Set(extractAttachmentReferences(body).map((reference) => reference.id));
3162
3169
  }
3163
3170
  function findDroppedAttachments(oldBody, newBody, attachments) {
3164
3171
  const oldIds = extractAttachmentFileIds(oldBody);
@@ -4117,6 +4124,14 @@ var commentDeleteCommand = new import_commander23.Command("delete").description(
4117
4124
  // src/commands/post/comment/get.ts
4118
4125
  var import_commander24 = require("commander");
4119
4126
 
4127
+ // src/utils/format-size.ts
4128
+ function formatSize(bytes) {
4129
+ if (bytes == null) return "-";
4130
+ if (bytes < 1024) return `${bytes}B`;
4131
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
4132
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
4133
+ }
4134
+
4120
4135
  // src/formatters/comment.ts
4121
4136
  function formatCommentDetail(comment, opts) {
4122
4137
  if (opts.json) {
@@ -4140,7 +4155,7 @@ function formatCommentDetail(comment, opts) {
4140
4155
  process.stdout.write("\n[Attachments]\n");
4141
4156
  printTable(
4142
4157
  ["Name", "Size", "ID"],
4143
- comment.files.map((f) => [f.name, String(f.size), f.id])
4158
+ comment.files.map((f) => [f.name ?? "-", formatSize(f.size), f.id])
4144
4159
  );
4145
4160
  }
4146
4161
  }
@@ -4296,13 +4311,54 @@ async function resolveCommentFileInput(client, args) {
4296
4311
  return { projectId, postId, commentId, secondary };
4297
4312
  }
4298
4313
 
4314
+ // src/utils/comment-file-merge.ts
4315
+ function labelOrNull(label) {
4316
+ if (label === void 0 || label.length === 0) return null;
4317
+ return label;
4318
+ }
4319
+ function mergeCommentFiles(input2) {
4320
+ const postFilesById = new Map(input2.postFiles.map((file) => [file.id, file]));
4321
+ const bodyRefsById = /* @__PURE__ */ new Map();
4322
+ for (const reference of input2.bodyRefs) {
4323
+ if (!bodyRefsById.has(reference.id)) bodyRefsById.set(reference.id, reference);
4324
+ }
4325
+ const merged = [];
4326
+ const seen = /* @__PURE__ */ new Set();
4327
+ for (const commentFile of input2.commentFiles) {
4328
+ if (seen.has(commentFile.id)) continue;
4329
+ seen.add(commentFile.id);
4330
+ const postFile = postFilesById.get(commentFile.id);
4331
+ const bodyRef = bodyRefsById.get(commentFile.id);
4332
+ merged.push({
4333
+ id: commentFile.id,
4334
+ name: postFile?.name ?? commentFile.name ?? labelOrNull(bodyRef?.label),
4335
+ size: postFile?.size ?? null,
4336
+ mimeType: postFile?.mimeType ?? null,
4337
+ source: bodyRef ? "both" : "attachment"
4338
+ });
4339
+ }
4340
+ for (const bodyRef of input2.bodyRefs) {
4341
+ if (seen.has(bodyRef.id)) continue;
4342
+ seen.add(bodyRef.id);
4343
+ const postFile = postFilesById.get(bodyRef.id);
4344
+ merged.push({
4345
+ id: bodyRef.id,
4346
+ name: postFile?.name ?? labelOrNull(bodyRef.label),
4347
+ size: postFile?.size ?? null,
4348
+ mimeType: postFile?.mimeType ?? null,
4349
+ source: "body-link"
4350
+ });
4351
+ }
4352
+ return merged;
4353
+ }
4354
+
4299
4355
  // src/commands/post/comment/file/list.ts
4300
- function formatSize(bytes) {
4301
- if (bytes < 1024) return `${bytes}B`;
4302
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
4303
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
4356
+ function formatSource(source) {
4357
+ if (source === "attachment") return "\uCCA8\uBD80";
4358
+ if (source === "body-link") return "\uBCF8\uBB38 \uB9C1\uD06C";
4359
+ return "\uB458 \uB2E4";
4304
4360
  }
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) => {
4361
+ 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
4362
  const globalOpts = listCommentFileCommand.optsWithGlobals();
4307
4363
  const config = await getConfigOrThrow();
4308
4364
  const client = new DoorayApiClient(config.apiKey, config.baseUrl);
@@ -4316,23 +4372,50 @@ var listCommentFileCommand = new import_commander25.Command("list").description(
4316
4372
  requireSecondary: false
4317
4373
  });
4318
4374
  startSpinner("\uB313\uAE00 \uCCA8\uBD80 \uD30C\uC77C \uBAA9\uB85D \uC870\uD68C \uC911...");
4319
- const res = await client.getPostComment(projectId, postId, commentId);
4320
- const files = res.result.files ?? [];
4321
- stopSpinner(true, `\uCCA8\uBD80 \uD30C\uC77C ${files.length}\uAC1C`);
4322
- if (files.length === 0) {
4323
- if (globalOpts.json) {
4324
- process.stdout.write("[]\n");
4325
- } else if (!globalOpts.quiet) {
4326
- process.stdout.write("\uCCA8\uBD80 \uC5C6\uC74C\n");
4375
+ try {
4376
+ const res = await client.getPostComment(projectId, postId, commentId);
4377
+ const commentFiles = res.result.files ?? [];
4378
+ const bodyRefs = extractAttachmentReferences(res.result.body.content);
4379
+ if (commentFiles.length === 0 && bodyRefs.length === 0) {
4380
+ stopSpinner(true, "\uCCA8\uBD80 \uD30C\uC77C 0\uAC1C");
4381
+ if (globalOpts.json) {
4382
+ process.stdout.write("[]\n");
4383
+ } else if (!globalOpts.quiet) {
4384
+ process.stdout.write("\uCCA8\uBD80 \uC5C6\uC74C\n");
4385
+ }
4386
+ return;
4327
4387
  }
4328
- return;
4388
+ let postFiles = [];
4389
+ let metadataLookupError = null;
4390
+ try {
4391
+ const postFilesRes = await client.getPostFiles(projectId, postId);
4392
+ postFiles = postFilesRes.result;
4393
+ } catch (error) {
4394
+ metadataLookupError = error instanceof Error ? error.message : String(error);
4395
+ }
4396
+ const merged = mergeCommentFiles({ commentFiles, bodyRefs, postFiles });
4397
+ stopSpinner(true, `\uCCA8\uBD80 \uD30C\uC77C ${merged.length}\uAC1C`);
4398
+ if (metadataLookupError !== null) {
4399
+ process.stderr.write(
4400
+ `\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.
4401
+ `
4402
+ );
4403
+ }
4404
+ output(globalOpts, {
4405
+ headers: ["\uD30C\uC77C\uBA85", "\uD06C\uAE30", "\uCD9C\uCC98", "ID"],
4406
+ rows: merged.map((file) => [
4407
+ file.name == null ? "-" : sanitizeFileName(file.name),
4408
+ formatSize(file.size),
4409
+ formatSource(file.source),
4410
+ file.id
4411
+ ]),
4412
+ raw: merged,
4413
+ ids: merged.map((file) => file.id)
4414
+ });
4415
+ } catch (error) {
4416
+ stopSpinner(false);
4417
+ throw error;
4329
4418
  }
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
4419
  });
4337
4420
 
4338
4421
  // src/commands/post/comment/file/upload.ts
@@ -4359,7 +4442,7 @@ function removeFileReference(body, fileId) {
4359
4442
  }
4360
4443
 
4361
4444
  // src/commands/post/comment/file/upload.ts
4362
- var uploadCommentFileCommand = new import_commander26.Command("upload").description("\uB313\uAE00\uC5D0 \uD30C\uC77C \uC5C5\uB85C\uB4DC (\uBCF8\uBB38\uC5D0 reference \uC790\uB3D9 \uCD94\uAC00)").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) => {
4445
+ 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
4446
  const globalOpts = uploadCommentFileCommand.optsWithGlobals();
4364
4447
  const config = await getConfigOrThrow();
4365
4448
  const client = new DoorayApiClient(config.apiKey, config.baseUrl);
@@ -4502,11 +4585,6 @@ var commentFileCommand = new import_commander29.Command("file").description("\uB
4502
4585
 
4503
4586
  // src/commands/post/file/list.ts
4504
4587
  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
4588
  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
4589
  const globalOpts = fileListCommand.optsWithGlobals();
4512
4590
  const config = await getConfigOrThrow();
@@ -4525,7 +4603,7 @@ var fileListCommand = new import_commander30.Command("list").description("\uC5C5
4525
4603
  rows: res.result.map((f) => [
4526
4604
  f.id,
4527
4605
  f.name,
4528
- formatSize2(f.size),
4606
+ formatSize(f.size),
4529
4607
  f.mimeType,
4530
4608
  f.createdAt
4531
4609
  ]),
@@ -5190,11 +5268,6 @@ var import_commander47 = require("commander");
5190
5268
 
5191
5269
  // src/commands/wiki/page-file/list.ts
5192
5270
  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
5271
  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
5272
  const globalOpts = wikiPageFileListCommand.optsWithGlobals();
5200
5273
  const config = await getConfigOrThrow();
@@ -5215,7 +5288,7 @@ var wikiPageFileListCommand = new import_commander42.Command("list").description
5215
5288
  stopSpinner(true, `\uCCA8\uBD80\uD30C\uC77C ${merged.length}\uAC1C (general ${files.length} + inline ${images.length})`);
5216
5289
  output(globalOpts, {
5217
5290
  headers: ["ID", "Type", "\uD30C\uC77C\uBA85", "\uD06C\uAE30"],
5218
- rows: merged.map((f) => [f.id, f.type, f.name, formatSize3(f.size)]),
5291
+ rows: merged.map((f) => [f.id, f.type, f.name, formatSize(f.size)]),
5219
5292
  raw: merged,
5220
5293
  ids: merged.map((f) => f.id)
5221
5294
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bifos/dooray-cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "CLI tool for Dooray project management — AI agent & terminal friendly",
5
5
  "keywords": [
6
6
  "dooray",
@@ -159,7 +159,13 @@ NHN Dooray REST API 를 래핑한 CLI 다. 이 파일은 라우터이므로, 작
159
159
  | 댓글 첨부 삭제 | `dooray post comment file delete <project> <number> <comment-id> <file-id>` — 확인 있음, `-y`/`--yes`로 생략 |
160
160
 
161
161
  - 댓글 파일 업로드는 이미지 확장자면 이미지 마크다운을, 그 외에는 일반 링크를 만든다.
162
- - `comment file list`가 비어도 UI 첨부가 없다고 단정하지 말고 `post file list`로 확인한다.
162
+ - `comment file list`의 `출처`는 다음과 같다.
163
+ - `attachment` (`첨부`): 웹 UI 첨부
164
+ - `body-link` (`본문 링크`): CLI 업로드 링크
165
+ - `both` (`둘 다`): 양쪽에 있는 파일
166
+ - `--json` 항목은 `{ id, name, size, mimeType, source }` 형식이다.
167
+ `source`는 위 값 중 하나다.
168
+ - 메타데이터를 채우지 못하면 `name`, `size`, `mimeType`은 `null`이다.
163
169
 
164
170
  ## 위키
165
171
 
@@ -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`는 댓글 조회 API가 노출한 첨부만 보여주므로 UI에서 직접 첨부한 파일을 놓칠 수 있다.
108
- 목록이 비어 있으면 `post file list`로 업무 전체 첨부를 확인한다.
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