@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 +51 -0
- package/dist/index.js +124 -48
- package/package.json +1 -1
- package/skills/dooray-cli/SKILL.md +30 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3898
|
+
const succeeded = [];
|
|
3899
|
+
const failed = [];
|
|
3846
3900
|
for (const file of res.result) {
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4410
|
+
process.stdout.write(`name: ${res.result.name}
|
|
4341
4411
|
`);
|
|
4342
|
-
|
|
4412
|
+
process.stdout.write(`size: ${res.result.size}
|
|
4343
4413
|
`);
|
|
4344
|
-
|
|
4414
|
+
process.stdout.write(`type: ${res.result.type}
|
|
4345
4415
|
`);
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
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(` 
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4452
|
-
const
|
|
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
|
-
|
|
4457
|
-
|
|
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
|
-
|
|
4540
|
+
}
|
|
4460
4541
|
} catch (e) {
|
|
4461
|
-
|
|
4462
|
-
|
|
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
|
-
|
|
4467
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
@@ -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/`에 캐시된다.
|