@bifos/dooray-cli 0.10.1 → 0.11.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
@@ -91,6 +91,22 @@ dooray post get <project> 42 --json # JSON 출력
91
91
  대상: `post get`/`edit`/`done`/`workflow`, `post comment list`/`add`/`edit`/`delete`, `post file list`/`upload`/`download`/`download-all`/`delete`.
92
92
  AI 에이전트는 사용자 메시지의 Dooray URL을 그대로 첫 인자로 전달하면 가장 빠르다 (ADR-020).
93
93
 
94
+ **projectId 직접 입력** (Issue #78, ADR-030):
95
+
96
+ `member=me` 응답에 없는 프로젝트 (다른 팀 / 권한만 있는 프로젝트) 도 projectId (15+자리 numeric) 를 직접 입력하면 자동으로 cache 우회.
97
+
98
+ ```bash
99
+ # 코드 매칭 (기존)
100
+ dooray post search <project> "keyword"
101
+
102
+ # projectId 직접 입력 — member 아닌 프로젝트도 자동화 가능
103
+ dooray post search 1234567890123456789 "keyword"
104
+ dooray post list 1234567890123456789
105
+ dooray member list 1234567890123456789
106
+ ```
107
+
108
+ 권한 검증은 후속 API 호출 시점 — 권한 없으면 4xx.
109
+
94
110
  ### 업무 생성
95
111
 
96
112
  ```bash
@@ -383,6 +399,22 @@ dooray wiki page file list "https://<tenant>.dooray.com/wiki/<wikiId>/<pageId>"
383
399
  dooray wiki page file upload --id <pageId> --project <project> --file ./README.md
384
400
  ```
385
401
 
402
+ `--json` 옵션으로 자동화 파이프라인에서 동일 parse 코드를 사용할 수 있습니다 (ADR-031):
403
+
404
+ ```bash
405
+ # download — { outputPath, fileName, size }
406
+ dooray wiki page file download <project> <page-id> --file-id <id> -o ./ --json
407
+
408
+ # download-all — { count, succeeded, failed } (부분 실패 시 exit 1)
409
+ dooray wiki page file download-all <project> <page-id> -o ./ --json | jq '.failed'
410
+
411
+ # delete — { fileId, status: "deleted" }
412
+ dooray wiki page file delete <project> <page-id> --file-id <id> --json
413
+
414
+ # upload — res.result raw (--quiet 는 id 만)
415
+ dooray wiki page file upload <project> <page-id> --file ./report.pdf --json
416
+ ```
417
+
386
418
  **주의**:
387
419
  - `upload` 시 multipart 필드 순서 (`type` → `file`) 가 중요.
388
420
  클라이언트가 자동으로 강제 (ADR-029 참조)
@@ -474,6 +506,25 @@ dooray post file upload <project> <number> ./report.pdf
474
506
  dooray post file delete <project> <number> <file-id>
475
507
  ```
476
508
 
509
+ 자동화 스크립트에서 `--json` 옵션으로 구조화된 출력을 파이프로 가공할 수 있습니다 (ADR-031):
510
+
511
+ ```bash
512
+ # download — { outputPath, fileName, size }
513
+ dooray post file download <project> <number> --file-id <id> -o ./ --json
514
+ # 출력: {"outputPath": "./<fileName>", "fileName": "...", "size": 12345}
515
+
516
+ # download-all — { count, succeeded, failed } (부분 실패 시 exit 1)
517
+ dooray post file download-all <project> <number> -o ./ --json | jq '.failed'
518
+ # 출력: [] 또는 [{"fileId": "...", "error": "..."}]
519
+
520
+ # delete — { fileId, status: "deleted" }
521
+ dooray post file delete <project> <number> --file-id <id> --json
522
+ # 출력: {"fileId": "...", "status": "deleted"}
523
+
524
+ # upload — res.result raw (--quiet 는 id 만)
525
+ dooray post file upload <project> <number> ./report.pdf --json
526
+ ```
527
+
477
528
  URL/`--id`/`--url` 모드에서는 sub-id를 옵션으로 전달:
478
529
  ```bash
479
530
  dooray post file download --url <url> --file-id <fileId> -o ./downloads
package/dist/index.js CHANGED
@@ -1206,6 +1206,7 @@ var setupCommand = new import_commander4.Command("setup").description("\uB300\uD
1206
1206
  var import_commander5 = require("commander");
1207
1207
 
1208
1208
  // src/resolvers/project.ts
1209
+ var PROJECT_ID_RE = /^\d{15,}$/;
1209
1210
  async function fetchAllProjects(client, options) {
1210
1211
  const all = [];
1211
1212
  let page = 0;
@@ -1243,6 +1244,9 @@ async function ensurePrivateProjects(client) {
1243
1244
  return items;
1244
1245
  }
1245
1246
  async function resolveProject(client, input2) {
1247
+ if (PROJECT_ID_RE.test(input2)) {
1248
+ return input2;
1249
+ }
1246
1250
  const projects = await ensureProjects(client);
1247
1251
  const match = projects.find((p) => p.code === input2 || p.id === input2);
1248
1252
  if (match) return match.id;
@@ -1253,7 +1257,8 @@ async function resolveProject(client, input2) {
1253
1257
  }
1254
1258
  throw new DoorayCliError(
1255
1259
  `\uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input2}
1256
- \uAC1C\uC778 \uD504\uB85C\uC81D\uD2B8\uB77C\uBA74: dooray project list --type private \uB85C \uCE90\uC2DC\uB97C \uAC31\uC2E0\uD558\uC138\uC694`,
1260
+ \uAC1C\uC778 \uD504\uB85C\uC81D\uD2B8\uB77C\uBA74: dooray project list --type private \uB85C \uCE90\uC2DC\uB97C \uAC31\uC2E0\uD558\uC138\uC694
1261
+ member=me \uC751\uB2F5\uC5D0 \uC5C6\uB294 \uD504\uB85C\uC81D\uD2B8\uB294 projectId (15+\uC790\uB9AC numeric) \uC9C1\uC811 \uC785\uB825\uC73C\uB85C \uC6B0\uD68C \uAC00\uB2A5 (ADR-030)`,
1257
1262
  EXIT_PARAM_ERROR
1258
1263
  );
1259
1264
  }
@@ -3767,7 +3772,49 @@ var fileListCommand = new import_commander29.Command("list").description("\uC5C5
3767
3772
  var import_commander30 = require("commander");
3768
3773
  var import_promises9 = require("fs/promises");
3769
3774
  var import_node_path6 = require("path");
3775
+
3776
+ // src/formatters/file-output.ts
3777
+ function emitDownloadResult(globalOpts, result) {
3778
+ if (globalOpts.json) {
3779
+ printJson(result);
3780
+ } else {
3781
+ process.stdout.write(`${result.outputPath}
3782
+ `);
3783
+ }
3784
+ }
3785
+ function emitDownloadAllResult(globalOpts, data) {
3786
+ if (globalOpts.json) {
3787
+ printJson(data);
3788
+ } else if (globalOpts.quiet) {
3789
+ for (const s of data.succeeded) {
3790
+ process.stdout.write(`${s.path}
3791
+ `);
3792
+ }
3793
+ if (data.failed.length > 0) {
3794
+ process.stderr.write(`${data.failed.length} failed
3795
+ `);
3796
+ }
3797
+ } else {
3798
+ process.stdout.write(`
3799
+ \uC644\uB8CC: ${data.succeeded.length}/${data.count}
3800
+ `);
3801
+ }
3802
+ }
3803
+ function emitDeleteResult(globalOpts, result) {
3804
+ if (globalOpts.json) {
3805
+ printJson({ fileId: result.fileId, status: "deleted" });
3806
+ } else if (globalOpts.quiet) {
3807
+ process.stdout.write(`${result.fileId}
3808
+ `);
3809
+ } else {
3810
+ process.stdout.write(`\uD30C\uC77C(${result.fileId})\uC774 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
3811
+ `);
3812
+ }
3813
+ }
3814
+
3815
+ // src/commands/post/file/download.ts
3770
3816
  var fileDownloadCommand = new import_commander30.Command("download").description("\uCCA8\uBD80\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg2]", "\uC5C5\uBB34 \uBC88\uD638 \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg3]", "\uD30C\uC77C ID (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("--file-id <fileId>", "\uD30C\uC77C ID (positional \uB300\uCCB4)").option("-o, --output <dir>", "\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC", ".").action(async (arg1, arg2, arg3, opts) => {
3817
+ const globalOpts = fileDownloadCommand.optsWithGlobals();
3771
3818
  const config = await getConfigOrThrow();
3772
3819
  const client = new DoorayApiClient(config.apiKey, config.baseUrl);
3773
3820
  let projectArg;
@@ -3814,11 +3861,11 @@ var fileDownloadCommand = new import_commander30.Command("download").description
3814
3861
  urlOpt: opts.url
3815
3862
  });
3816
3863
  const { buffer, fileName } = await client.downloadPostFile(projectId, postId, fileId);
3817
- const outputPath = (0, import_node_path6.join)(opts.output, fileName);
3864
+ const safeName = (0, import_node_path6.basename)(decodeURIComponent(fileName));
3865
+ const outputPath = (0, import_node_path6.join)(opts.output, safeName);
3818
3866
  await (0, import_promises9.writeFile)(outputPath, Buffer.from(buffer));
3819
3867
  stopSpinner(true, "\uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC");
3820
- process.stdout.write(`${outputPath}
3821
- `);
3868
+ emitDownloadResult(globalOpts, { outputPath, fileName: safeName, size: buffer.byteLength });
3822
3869
  });
3823
3870
 
3824
3871
  // src/commands/post/file/download-all.ts
@@ -3826,9 +3873,10 @@ var import_commander31 = require("commander");
3826
3873
  var import_promises10 = require("fs/promises");
3827
3874
  var import_node_path7 = require("path");
3828
3875
  var fileDownloadAllCommand = new import_commander31.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 \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)").option("-o, --output <dir>", "\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC", ".").action(async (project, postNumberStr, opts) => {
3876
+ const globalOpts = fileDownloadAllCommand.optsWithGlobals();
3829
3877
  const config = await getConfigOrThrow();
3830
3878
  const client = new DoorayApiClient(config.apiKey, config.baseUrl);
3831
- const spinner = startSpinner("\uCCA8\uBD80\uD30C\uC77C \uBAA9\uB85D \uC870\uD68C \uC911...");
3879
+ startSpinner("\uCCA8\uBD80\uD30C\uC77C \uBAA9\uB85D \uC870\uD68C \uC911...");
3832
3880
  const { projectId, postId } = await resolvePostInput(client, {
3833
3881
  projectArg: project,
3834
3882
  postNumberArg: postNumberStr,
@@ -3838,23 +3886,38 @@ var fileDownloadAllCommand = new import_commander31.Command("download-all").desc
3838
3886
  const res = await client.getPostFiles(projectId, postId);
3839
3887
  if (res.result.length === 0) {
3840
3888
  stopSpinner(true, "\uCCA8\uBD80\uD30C\uC77C \uC5C6\uC74C");
3841
- process.stdout.write("\uCCA8\uBD80\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
3889
+ if (globalOpts.json) {
3890
+ printJson({ count: 0, succeeded: [], failed: [] });
3891
+ } else if (!globalOpts.quiet) {
3892
+ process.stdout.write("\uCCA8\uBD80\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
3893
+ }
3842
3894
  return;
3843
3895
  }
3896
+ stopSpinner(true, `${res.result.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2DC\uC791`);
3844
3897
  await (0, import_promises10.mkdir)(opts.output, { recursive: true });
3845
- const downloaded = [];
3898
+ const succeeded = [];
3899
+ const failed = [];
3846
3900
  for (const file of res.result) {
3847
- spinner.text = `\uB2E4\uC6B4\uB85C\uB4DC \uC911: ${file.name} (${downloaded.length + 1}/${res.result.length})`;
3848
- const { buffer, fileName } = await client.downloadPostFile(projectId, postId, file.id);
3849
- const outputPath = (0, import_node_path7.join)(opts.output, fileName);
3850
- await (0, import_promises10.writeFile)(outputPath, Buffer.from(buffer));
3851
- downloaded.push(outputPath);
3852
- }
3853
- stopSpinner(true, `${downloaded.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC`);
3854
- for (const path3 of downloaded) {
3855
- process.stdout.write(`${path3}
3901
+ try {
3902
+ const { buffer, fileName } = await client.downloadPostFile(projectId, postId, file.id);
3903
+ const safeName = (0, import_node_path7.basename)(decodeURIComponent(fileName));
3904
+ const outputPath = (0, import_node_path7.join)(opts.output, safeName);
3905
+ await (0, import_promises10.writeFile)(outputPath, Buffer.from(buffer));
3906
+ succeeded.push({ path: outputPath, fileName: safeName });
3907
+ if (!globalOpts.json && !globalOpts.quiet) {
3908
+ process.stdout.write(`\u2713 ${safeName}
3909
+ `);
3910
+ }
3911
+ } catch (e) {
3912
+ failed.push({ fileId: file.id, error: e instanceof Error ? e.message : String(e) });
3913
+ if (!globalOpts.json) {
3914
+ process.stderr.write(`\u2717 ${file.name} (${file.id}): ${e instanceof Error ? e.message : String(e)}
3856
3915
  `);
3916
+ }
3917
+ }
3857
3918
  }
3919
+ emitDownloadAllResult(globalOpts, { count: res.result.length, succeeded, failed });
3920
+ if (failed.length > 0) process.exitCode = 1;
3858
3921
  });
3859
3922
 
3860
3923
  // src/commands/post/file/upload.ts
@@ -3923,6 +3986,7 @@ var fileUploadCommand = new import_commander32.Command("upload").description("\u
3923
3986
  // src/commands/post/file/delete.ts
3924
3987
  var import_commander33 = require("commander");
3925
3988
  var fileDeleteCommand = new import_commander33.Command("delete").description("\uCCA8\uBD80\uD30C\uC77C \uC0AD\uC81C").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg2]", "\uC5C5\uBB34 \uBC88\uD638 \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg3]", "\uD30C\uC77C 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("--file-id <fileId>", "\uD30C\uC77C ID (positional \uB300\uCCB4)").action(async (arg1, arg2, arg3, opts) => {
3989
+ const globalOpts = fileDeleteCommand.optsWithGlobals();
3926
3990
  const config = await getConfigOrThrow();
3927
3991
  const client = new DoorayApiClient(config.apiKey, config.baseUrl);
3928
3992
  let projectArg;
@@ -3970,8 +4034,7 @@ var fileDeleteCommand = new import_commander33.Command("delete").description("\u
3970
4034
  });
3971
4035
  await client.deletePostFile(projectId, postId, fileId);
3972
4036
  stopSpinner(true, "\uC0AD\uC81C \uC644\uB8CC");
3973
- process.stdout.write(`\uD30C\uC77C(${fileId})\uC774 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
3974
- `);
4037
+ emitDeleteResult(globalOpts, { fileId });
3975
4038
  });
3976
4039
 
3977
4040
  // src/commands/wiki/list.ts
@@ -4279,6 +4342,7 @@ var wikiPageFileListCommand = new import_commander39.Command("list").description
4279
4342
  // src/commands/wiki/page-file/upload.ts
4280
4343
  var import_commander40 = require("commander");
4281
4344
  var wikiPageFileUploadCommand = new import_commander40.Command("upload").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uCCA8\uBD80\uD30C\uC77C \uC5C5\uB85C\uB4DC (multipart type \uC21C\uC11C \uAC15\uC81C, ADR-029)").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray Wiki URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C \uACBD\uB85C").argument("[arg2]", "page-id \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C \uACBD\uB85C").argument("[arg3]", "\uD30C\uC77C \uACBD\uB85C (positional 3\uAC1C \uBAA8\uB4DC)").option("--id <pageId>", "\uC704\uD0A4 \uD398\uC774\uC9C0 ID").option("--url <url>", "Dooray Wiki URL").option("--project <code>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (--id \uBAA8\uB4DC\uC5D0\uC11C wikiId \uD574\uC11D\uC6A9)").option("--file <path>", "\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uACBD\uB85C (positional \uB300\uCCB4)").option("--type <type>", "\uD30C\uC77C \uD0C0\uC785: general | inline_image (\uAE30\uBCF8 general)", "general").action(async (arg1, arg2, arg3, opts) => {
4345
+ const globalOpts = wikiPageFileUploadCommand.optsWithGlobals();
4282
4346
  const config = await getConfigOrThrow();
4283
4347
  const client = new DoorayApiClient(config.apiKey, config.baseUrl);
4284
4348
  const fileType = opts.type;
@@ -4335,18 +4399,25 @@ var wikiPageFileUploadCommand = new import_commander40.Command("upload").descrip
4335
4399
  try {
4336
4400
  const res = await client.uploadWikiPageFile(wikiId, pageId, filePath, fileType);
4337
4401
  stopSpinner(true, "\uC5C5\uB85C\uB4DC \uC644\uB8CC");
4338
- process.stdout.write(`attachFileId: ${res.result.attachFileId}
4402
+ if (globalOpts.json) {
4403
+ printJson(res.result);
4404
+ } else if (globalOpts.quiet) {
4405
+ process.stdout.write(`${res.result.id}
4406
+ `);
4407
+ } else {
4408
+ process.stdout.write(`attachFileId: ${res.result.attachFileId}
4339
4409
  `);
4340
- process.stdout.write(`name: ${res.result.name}
4410
+ process.stdout.write(`name: ${res.result.name}
4341
4411
  `);
4342
- process.stdout.write(`size: ${res.result.size}
4412
+ process.stdout.write(`size: ${res.result.size}
4343
4413
  `);
4344
- process.stdout.write(`type: ${res.result.type}
4414
+ process.stdout.write(`type: ${res.result.type}
4345
4415
  `);
4346
- if (fileType === "inline_image") {
4347
- process.stdout.write("\n\uBCF8\uBB38 \uC0BD\uC785\uC6A9 markdown snippet (\uC9C1\uC811 wiki page edit \uC73C\uB85C \uBCF8\uBB38\uC5D0 \uBC15\uC73C\uC138\uC694):\n");
4348
- process.stdout.write(` ![${res.result.name}](/wikis/${wikiId}/files/${res.result.attachFileId})
4416
+ if (fileType === "inline_image") {
4417
+ process.stdout.write("\n\uBCF8\uBB38 \uC0BD\uC785\uC6A9 markdown snippet (\uC9C1\uC811 wiki page edit \uC73C\uB85C \uBCF8\uBB38\uC5D0 \uBC15\uC73C\uC138\uC694):\n");
4418
+ process.stdout.write(` ![${res.result.name}](/wikis/${wikiId}/files/${res.result.attachFileId})
4349
4419
  `);
4420
+ }
4350
4421
  }
4351
4422
  } catch (e) {
4352
4423
  stopSpinner(false);
@@ -4359,6 +4430,7 @@ var import_commander41 = require("commander");
4359
4430
  var import_promises11 = require("fs/promises");
4360
4431
  var import_node_path9 = require("path");
4361
4432
  var wikiPageFileDownloadCommand = new import_commander41.Command("download").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uCCA8\uBD80\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray Wiki URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg2]", "page-id \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg3]", "\uD30C\uC77C ID (positional 3\uAC1C \uBAA8\uB4DC)").option("--id <pageId>", "\uC704\uD0A4 \uD398\uC774\uC9C0 ID").option("--url <url>", "Dooray Wiki URL").option("--project <code>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (--id \uBAA8\uB4DC\uC5D0\uC11C wikiId \uD574\uC11D\uC6A9)").option("--file-id <fileId>", "\uD30C\uC77C ID (positional \uB300\uCCB4)").option("-o, --output <dir>", "\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC", ".").action(async (arg1, arg2, arg3, opts) => {
4433
+ const globalOpts = wikiPageFileDownloadCommand.optsWithGlobals();
4362
4434
  const config = await getConfigOrThrow();
4363
4435
  const client = new DoorayApiClient(config.apiKey, config.baseUrl);
4364
4436
  let projectArg;
@@ -4407,11 +4479,11 @@ var wikiPageFileDownloadCommand = new import_commander41.Command("download").des
4407
4479
  startSpinner("\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
4408
4480
  try {
4409
4481
  const { buffer, fileName } = await client.downloadWikiPageFile(wikiId, pageId, fileId);
4410
- const outputPath = (0, import_node_path9.join)(opts.output, (0, import_node_path9.basename)(fileName));
4482
+ const safeName = (0, import_node_path9.basename)(decodeURIComponent(fileName));
4483
+ const outputPath = (0, import_node_path9.join)(opts.output, safeName);
4411
4484
  await (0, import_promises11.writeFile)(outputPath, Buffer.from(buffer));
4412
4485
  stopSpinner(true, `\uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC: ${outputPath}`);
4413
- process.stdout.write(`${outputPath}
4414
- `);
4486
+ emitDownloadResult(globalOpts, { outputPath, fileName: safeName, size: buffer.byteLength });
4415
4487
  } catch (e) {
4416
4488
  stopSpinner(false);
4417
4489
  throw e;
@@ -4423,6 +4495,7 @@ var import_commander42 = require("commander");
4423
4495
  var import_promises12 = require("fs/promises");
4424
4496
  var import_node_path10 = require("path");
4425
4497
  var wikiPageFileDownloadAllCommand = new import_commander42.Command("download-all").description("\uC704\uD0A4 \uD398\uC774\uC9C0\uC758 \uBAA8\uB4E0 \uCCA8\uBD80\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC (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)").option("-o, --output <dir>", "\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC", ".").action(async (project, pageIdArg, opts) => {
4498
+ const globalOpts = wikiPageFileDownloadAllCommand.optsWithGlobals();
4426
4499
  const config = await getConfigOrThrow();
4427
4500
  const client = new DoorayApiClient(config.apiKey, config.baseUrl);
4428
4501
  const { wikiId, pageId } = await resolveWikiPageInput(client, {
@@ -4443,40 +4516,44 @@ var wikiPageFileDownloadAllCommand = new import_commander42.Command("download-al
4443
4516
  }
4444
4517
  if (allFiles.length === 0) {
4445
4518
  stopSpinner(true, "\uCCA8\uBD80\uD30C\uC77C \uC5C6\uC74C");
4446
- process.stdout.write("\uCCA8\uBD80\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
4519
+ if (globalOpts.json) {
4520
+ printJson({ count: 0, succeeded: [], failed: [] });
4521
+ } else if (!globalOpts.quiet) {
4522
+ process.stdout.write("\uCCA8\uBD80\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
4523
+ }
4447
4524
  return;
4448
4525
  }
4449
4526
  stopSpinner(true, `${allFiles.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2DC\uC791`);
4450
4527
  await (0, import_promises12.mkdir)(opts.output, { recursive: true });
4451
- let successCount = 0;
4452
- const failures = [];
4528
+ const succeeded = [];
4529
+ const failed = [];
4453
4530
  for (const f of allFiles) {
4454
4531
  try {
4455
4532
  const { buffer, fileName } = await client.downloadWikiPageFile(wikiId, pageId, f.id);
4456
- await (0, import_promises12.writeFile)((0, import_node_path10.join)(opts.output, fileName), Buffer.from(buffer));
4457
- process.stdout.write(`\u2713 ${fileName}
4533
+ const safeName = (0, import_node_path10.basename)(decodeURIComponent(fileName));
4534
+ const outputPath = (0, import_node_path10.join)(opts.output, safeName);
4535
+ await (0, import_promises12.writeFile)(outputPath, Buffer.from(buffer));
4536
+ succeeded.push({ path: outputPath, fileName: safeName });
4537
+ if (!globalOpts.json && !globalOpts.quiet) {
4538
+ process.stdout.write(`\u2713 ${safeName}
4458
4539
  `);
4459
- successCount++;
4540
+ }
4460
4541
  } catch (e) {
4461
- failures.push({ fileId: f.id, error: e instanceof Error ? e.message : String(e) });
4462
- process.stderr.write(`\u2717 ${f.name} (${f.id}): ${e instanceof Error ? e.message : String(e)}
4542
+ failed.push({ fileId: f.id, error: e instanceof Error ? e.message : String(e) });
4543
+ if (!globalOpts.json) {
4544
+ process.stderr.write(`\u2717 ${f.name} (${f.id}): ${e instanceof Error ? e.message : String(e)}
4463
4545
  `);
4546
+ }
4464
4547
  }
4465
4548
  }
4466
- process.stdout.write(`
4467
- \uC644\uB8CC: ${successCount}/${allFiles.length}
4468
- `);
4469
- if (failures.length > 0) {
4470
- throw new DoorayCliError(
4471
- `${failures.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 (\uC131\uACF5: ${successCount}/${allFiles.length})`,
4472
- EXIT_API_ERROR
4473
- );
4474
- }
4549
+ emitDownloadAllResult(globalOpts, { count: allFiles.length, succeeded, failed });
4550
+ if (failed.length > 0) process.exitCode = 1;
4475
4551
  });
4476
4552
 
4477
4553
  // src/commands/wiki/page-file/delete.ts
4478
4554
  var import_commander43 = require("commander");
4479
4555
  var wikiPageFileDeleteCommand = new import_commander43.Command("delete").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uCCA8\uBD80\uD30C\uC77C \uC0AD\uC81C").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray Wiki URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg2]", "page-id \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg3]", "\uD30C\uC77C ID (positional 3\uAC1C \uBAA8\uB4DC)").option("--id <pageId>", "\uC704\uD0A4 \uD398\uC774\uC9C0 ID").option("--url <url>", "Dooray Wiki URL").option("--project <code>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (--id \uBAA8\uB4DC\uC5D0\uC11C wikiId \uD574\uC11D\uC6A9)").option("--file-id <fileId>", "\uD30C\uC77C ID (positional \uB300\uCCB4)").action(async (arg1, arg2, arg3, opts) => {
4556
+ const globalOpts = wikiPageFileDeleteCommand.optsWithGlobals();
4480
4557
  const config = await getConfigOrThrow();
4481
4558
  const client = new DoorayApiClient(config.apiKey, config.baseUrl);
4482
4559
  let projectArg;
@@ -4526,8 +4603,7 @@ var wikiPageFileDeleteCommand = new import_commander43.Command("delete").descrip
4526
4603
  try {
4527
4604
  await client.deleteWikiPageFile(wikiId, pageId, fileId);
4528
4605
  stopSpinner(true, "\uC0AD\uC81C \uC644\uB8CC");
4529
- process.stdout.write(`\uD30C\uC77C(${fileId})\uC774 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
4530
- `);
4606
+ emitDeleteResult(globalOpts, { fileId });
4531
4607
  } catch (e) {
4532
4608
  stopSpinner(false);
4533
4609
  throw e;
@@ -5575,7 +5651,7 @@ function sanitizeArgv(argv) {
5575
5651
 
5576
5652
  // src/index.ts
5577
5653
  var program = new import_commander61.Command();
5578
- program.name("dooray").description("Dooray REST API CLI").version("0.10.1").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "ID\uB9CC \uCD9C\uB825").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654");
5654
+ program.name("dooray").description("Dooray REST API CLI").version("0.11.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");
5579
5655
  program.hook("preAction", () => {
5580
5656
  const opts = program.opts();
5581
5657
  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.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "CLI tool for Dooray project management — AI agent & terminal friendly",
5
5
  "keywords": [
6
6
  "dooray",
@@ -62,7 +62,7 @@ dooray doctor # 설정 검증
62
62
  | 멤버 상세 (organizationMemberId) | `dooray member get <organizationMemberId>` (cache 우회, ADR-021) |
63
63
  | organization 전체 멤버 검색 | `dooray member search <keyword>` (이름 기본), `--email`(이메일 exact), `--user-code`(사번 like), `--user-code-exact`(사번 exact), `--page`/`--size` |
64
64
  | 업무 목록 조회 | `dooray post list <project>` |
65
- | 업무 검색 | `dooray post search <project> "<keyword>"` |
65
+ | 업무 검색 | `dooray post search <project|projectId> "<keyword>"` — projectId (15+자리 numeric) 직접 입력 시 cache 우회 (ADR-030) |
66
66
  | 업무 상세 보기 | `dooray post get <project> <number>` |
67
67
  | 업무 생성 | `dooray post create <project> --title "..." [--body "..." \| --body-file <path>]` (`--tag`/`--parent`/`--workflow`/`--milestone` 지원) |
68
68
  | 템플릿 기반 업무 생성 | `dooray post create <project> --template <name\|id>` — body/users/tags 자동 채움 (사용자 옵션 우선 override, ADR-027) |
@@ -104,6 +104,7 @@ dooray doctor # 설정 검증
104
104
  | 전체 첨부파일 다운로드 | `dooray post file download-all <project> <number>` |
105
105
  | 첨부파일 업로드 | `dooray post file upload <project> <number> <file-path>` |
106
106
  | 첨부파일 삭제 | `dooray post file delete <project> <number> <file-id>` |
107
+ | file 명령군 자동화 파싱 | `dooray post file <verb> ... --json` — `download` = `{outputPath,fileName,size}`, `download-all` = `{count,succeeded,failed}` (부분 실패 시 exit 1), `delete` = `{fileId,status}`, `upload` = `res.result` raw (ADR-031) |
107
108
  | 댓글 첨부 목록 | `dooray post comment file list <project> <number> <comment-id>` |
108
109
  | 댓글 파일 업로드 | `dooray post comment file upload <project> <number> <comment-id> <path>` |
109
110
  | 댓글 파일 다운로드 | `dooray post comment file download <project> <number> <comment-id> <file-id>` |
@@ -207,6 +208,16 @@ dooray wiki pages <project> --json
207
208
  dooray wiki page get <project> <pageId> --json
208
209
  ```
209
210
 
211
+ ### 첨부파일 일괄 다운로드 후 실패 분리 (ADR-031)
212
+
213
+ ```bash
214
+ # --json 으로 구조화 출력 → jq 로 성공/실패 분리
215
+ RESULT=$(dooray post file download-all <project> <number> -o ./ --json)
216
+ echo "$RESULT" | jq -r '.failed[] | "\(.fileId): \(.error)"' >&2
217
+ echo "$RESULT" | jq -r '.succeeded[].path'
218
+ # exit code 1 이 설정되어 있으면 실패 있는 상태
219
+ ```
220
+
210
221
  ### 위키 페이지 첨부파일 — 스킬 파일 팀 공유 (Issue #70)
211
222
 
212
223
  **스킬 파일 팀 공유**: 팀 위키에 스킬 파일 (예: `SKILL.md`) 을 `wiki page file upload` 로 첨부 → 팀원이 `wiki page file download-all` 로 일괄 받아 `~/.claude/skills/` 에 그대로 설치.
@@ -587,6 +598,24 @@ POST_ID=$(dooray post create <project> \
587
598
  템플릿 본문의 `${year}` / `${month}` 등 매크로는 Dooray 가 자동 치환 (`interpolation=true` 기본).
588
599
  사용자 정의 변수는 미지원 — 필요 시 client 측 string replace 로 처리.
589
600
 
601
+ ## projectId 직접 입력 시나리오 (Issue #78, ADR-030)
602
+
603
+ AI agent 가 `member=me` 응답에 없는 프로젝트의 업무를 다뤄야 할 때:
604
+
605
+ 1. **사용자가 projectId (15+자리 numeric) 를 줬으면 그대로 명령에 사용**:
606
+ ```bash
607
+ dooray post search 1234567890123456789 "keyword"
608
+ ```
609
+
610
+ 2. **사용자가 코드만 줬고 cache 매칭 실패 (member 아닌 프로젝트)**:
611
+ - 에러 메시지의 ADR-030 안내 확인
612
+ - 사용자에게 "프로젝트 ID 가 필요합니다 — Dooray UI 의 프로젝트 URL 에서 확인 가능" 요청
613
+ - 또는 `dooray project list --type private` 로 private 캐시 갱신 시도
614
+
615
+ 3. **권한 없는 projectId 입력 시**: resolver 통과 후 후속 API 4xx 발생 — 에러 메시지에서 권한 부재 확인 후 사용자에게 보고
616
+
617
+ 권한 검증이 resolver 단보다 한 단계 지연되는 trade-off — AI 친화적 자동화 우선 (ADR-030).
618
+
590
619
  ## 캐시
591
620
 
592
621
  프로젝트, 멤버, 워크플로우, 위키 정보는 `~/.dooray/cache/`에 캐시된다.