rootr-cli 0.8.0 → 0.9.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 +42 -4
- package/bin/rootr.js +375 -1
- package/lib/client.js +147 -12
- package/lib/mcp-tools/databases.js +346 -25
- package/lib/mcp-tools/documents.js +230 -3
- package/lib/mcp-tools/forms.js +87 -2
- package/lib/mcp-tools/misc.js +40 -0
- package/lib/mcp-tools/workspaces.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -296,14 +296,52 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
296
296
|
|
|
297
297
|
| 도구 | 설명 |
|
|
298
298
|
|---|---|
|
|
299
|
-
| `rootr_read_database` | 스키마(열 정의)+행 전체 읽기 — **다른 표 도구를 쓰기 전에 먼저** |
|
|
300
|
-
| `rootr_list_rows` | 행만 조회(스키마
|
|
301
|
-
| `rootr_add_row` | 행 추가 (`values`는 열 이름 또는 id로
|
|
302
|
-
| `rootr_update_row` | 행 수정 (칸반 카드 이동 = select 열 값
|
|
299
|
+
| `rootr_read_database` | 스키마(열 정의)+행 전체 읽기 — **다른 표 도구를 쓰기 전에 먼저** (`includeBody: true` 면 행마다 상세 내용까지) |
|
|
300
|
+
| `rootr_list_rows` | 행만 조회(스키마 제외, `includeBody` 지원) |
|
|
301
|
+
| `rootr_add_row` | 행 추가 (`values`는 열 이름 또는 id로 지정, `body`로 상세 내용까지) |
|
|
302
|
+
| `rootr_update_row` | 행 수정 (칸반 카드 이동 = select 열 값 변경, `body`로 상세 내용 교체) |
|
|
303
303
|
| `rootr_delete_row` | 행 삭제 |
|
|
304
304
|
| `rootr_add_column` | **열 추가** — 기존 열·행은 그대로 |
|
|
305
305
|
| `rootr_update_column` | 열 이름 변경 / select 선택지 교체 |
|
|
306
306
|
| `rootr_delete_column` | 열 삭제(파괴적) |
|
|
307
|
+
| `rootr_read_database_permissions` | 표에 걸린 자물쇠 보기 |
|
|
308
|
+
| `rootr_set_database_permissions` | 표 잠그기 — 칸·뷰 잠금 / 본인 행만 / 관리자만 고칠 칸 |
|
|
309
|
+
| `rootr_read_database_notifications` | 표 알림 규칙 보기 |
|
|
310
|
+
| `rootr_set_database_notifications` | 표 알림 규칙 저장 — 누구에게·언제·어떤 것만 |
|
|
311
|
+
| `rootr_list_document_commits` | 공개 문서에 온 변경 제안 목록 |
|
|
312
|
+
| `rootr_merge_document_commit` | 변경 제안 반영(세 벌 병합 — 덮어쓰기 아님) |
|
|
313
|
+
| `rootr_reject_document_commit` | 변경 제안 거절(사유는 제안자에게 전달) |
|
|
314
|
+
| `rootr_read_node_access` | 이 문서·폴더를 누가 볼 수 있는지(공개범위 + 따로 준 권한) |
|
|
315
|
+
| `rootr_set_node_visibility` | 비공개로 잠그기 / 상위 설정 따르기 |
|
|
316
|
+
| `rootr_grant_node_permission` | 한 사람(또는 그룹)에게 READ·WRITE·MANAGE 주기 |
|
|
317
|
+
| `rootr_revoke_node_permission` | 준 권한 거두기 |
|
|
318
|
+
| `rootr_update_form_response` | 이미 들어온 폼 응답 고치기(폼 검사 규칙 다시 통과) |
|
|
319
|
+
| `rootr_delete_form_response` | 폼 응답 한 건 삭제 |
|
|
320
|
+
| `rootr_create_form_collector` | 응답 모을 표·스프레드시트를 **만들어서** 연결(표는 질문 그대로 칸 생성) |
|
|
321
|
+
|
|
322
|
+
**사람 칸은 여러 명을 담습니다.** 회의 참석자·공동 담당처럼 여럿이 들어가는 자리가
|
|
323
|
+
흔해서, 값은 이름 목록(`["김대표","이팀장"]`)입니다. 이름 하나만 보내도 받아 주고
|
|
324
|
+
목록으로 맞춰 돌려줍니다. 새로 들어온 사람에게만 알림이 갑니다.
|
|
325
|
+
|
|
326
|
+
**"만들면서 같이 잠그기".** 공개범위는 스캐폴드 트리에서도 노드마다 지정할 수 있습니다 —
|
|
327
|
+
`{ name: "채용", visibility: "PRIVATE", access: [{ subjectId: "<userId>", level: "READ" }] }`.
|
|
328
|
+
폴더는 만들어 주면서 잠그는 건 사람이 따로 해야 하는 상태가 되지 않도록, 한 번에 갑니다.
|
|
329
|
+
명령줄에서는 `rootr access <경로|id> --private`, `--grant <userId> --level READ`,
|
|
330
|
+
`--revoke <userId>` 입니다. (웹에 공개하는 것과는 다릅니다 — 그건 `rootr_set_document_public`.)
|
|
331
|
+
|
|
332
|
+
응답 모을 곳은 `rootr form collect <폼> --to db|sheet`로 **만들면서** 연결합니다 —
|
|
333
|
+
표로 만들면 폼 질문 그대로 칸이 생기므로, 표를 미리 만들고 칸 이름을 맞출 필요가 없습니다
|
|
334
|
+
(이름이 하나라도 어긋나면 그 칸은 영영 빈 칸으로 남습니다). 스캐폴드 트리에서는 폼 노드에
|
|
335
|
+
`form.collectInto: "database" | "spreadsheet"`로 같은 일을 합니다.
|
|
336
|
+
|
|
337
|
+
폼 응답은 `rootr form responses <폼> [--latest]`로 보고, `rootr form response-edit <응답id>
|
|
338
|
+
--values '{"필드id":"값"}'`로 고치고, `rootr form response-rm <응답id> --yes`로 지웁니다.
|
|
339
|
+
`--latest`는 같은 사람이 여러 번 낸 것 중 마지막 한 건만 남깁니다(익명 제출은 합치지 않습니다).
|
|
340
|
+
|
|
341
|
+
행의 **상세 내용(본문 메모)** — 화면에서 칸반 카드를 열면 속성 아래에 적는 그 글 — 은
|
|
342
|
+
`body`로 오갑니다. 실체는 표 밑에 달리는 문서 한 장이고, 없으면 쓸 때 만들어집니다.
|
|
343
|
+
명령줄에서도 됩니다: `rootr db row-add <표> --values '{"제목":"킥오프"}' --body "..."`,
|
|
344
|
+
`rootr db row-body <표> <rowId> --file memo.md`, `rootr db rows <표> --body`.
|
|
307
345
|
|
|
308
346
|
열은 **한 번에 하나씩** 다룹니다. REST의 `PATCH /databases/:id`는 `properties`
|
|
309
347
|
배열을 통째로 교체하기 때문에, 에이전트가 배열을 다시 만들다 빠뜨린 열이 그대로
|
package/bin/rootr.js
CHANGED
|
@@ -19,6 +19,7 @@ const USAGE = `Rootr CLI v${pkg.version}
|
|
|
19
19
|
rootr write <path|id> [--file <f>] [--if-match <etag>] 문서 전체 교체(파일 없으면 생성)
|
|
20
20
|
rootr append <path|id> [<text>] [--heading "## X"] [--file <f>] 문서 끝(또는 헤딩 아래)에 안전하게 추가
|
|
21
21
|
rootr edit <path|id> --find <text> --replace <text> [--all] 본문 내 텍스트 치환
|
|
22
|
+
rootr comments <path|id> [--block <id>] [--json] 문서에 달린 댓글 읽기 (공개 페이지 댓글 포함)
|
|
22
23
|
rootr search <query> 워크스페이스 검색
|
|
23
24
|
rootr mv <path|id> <새 경로|폴더 id> 문서/폴더 이동 및/또는 이름 변경
|
|
24
25
|
rootr rm <path|id> [--yes] 문서/폴더를 휴지통으로 이동 (복구 가능)
|
|
@@ -27,6 +28,19 @@ const USAGE = `Rootr CLI v${pkg.version}
|
|
|
27
28
|
rootr use <id|이름|주소> 활성 워크스페이스 전환 (그 워크스페이스용 키도 기억)
|
|
28
29
|
rootr ask <question> [--workspace <w>] GraphRAG 질의응답 (RCA), 인용 포함 출력
|
|
29
30
|
rootr db create [--file <spec.json>] [--parent <path|id>] 표(DATABASE) 생성 — 컬럼 순서 그대로, 행까지 한 번에
|
|
31
|
+
rootr db rows <path|id> [--body] [--json] 표의 행 목록 (--body: 상세 내용까지)
|
|
32
|
+
rootr db lock <path|id> [--schema managers] [--rows own] [--locked "승인,점수"] 표 자물쇠 보기·걸기
|
|
33
|
+
rootr db notify <path|id> [--file rules.json] [--clear] 표 알림 규칙 보기·저장
|
|
34
|
+
rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] 행 추가(상세 내용 포함)
|
|
35
|
+
rootr db row-body <path|id> <rowId> [--file <f>] 행의 상세 내용(본문 메모) 쓰기 — 없으면 표준입력
|
|
36
|
+
rootr access <path|id> [--json] 공개범위·권한 보기 (관리 권한 필요)
|
|
37
|
+
rootr access <path|id> --private|--public|--inherit 공개범위 바꾸기
|
|
38
|
+
rootr access <path|id> --grant <userId> --level READ|WRITE|MANAGE 사람에게 권한 주기
|
|
39
|
+
rootr access <path|id> --revoke <userId> 준 권한 거두기
|
|
40
|
+
rootr form collect <path|id> --to db|sheet 응답 모을 곳을 새로 만들어 연결 (표는 질문 그대로 칸 생성)
|
|
41
|
+
rootr form responses <path|id> [--latest] [--json] 폼에 들어온 응답 보기 (--latest: 사람마다 최신 1건만)
|
|
42
|
+
rootr form response-edit <응답id> --values '<json>' 이미 낸 응답 고치기 (폼 검사 규칙 다시 통과)
|
|
43
|
+
rootr form response-rm <응답id> [--yes] 응답 한 건 삭제 (되돌릴 수 없음)
|
|
30
44
|
rootr config [--api-key <k>] [--workspace <w>] [--base-url <u>] 설정 조회/저장
|
|
31
45
|
rootr config --add-key <k> [--label <name>] [--workspace <w>] 워크스페이스별 키 추가 등록
|
|
32
46
|
rootr mcp MCP stdio 서버 구동 (도구 90종+ — README 참고)
|
|
@@ -112,6 +126,41 @@ async function cmdLs(args) {
|
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
|
|
129
|
+
/**
|
|
130
|
+
* `rootr comments <path|id>` — 그 문서에 달린 댓글을 읽는다.
|
|
131
|
+
*
|
|
132
|
+
* 2026-08-23 창업자 확인 요청: "댓글 써진 거 mcp/api/cli로 읽어올 수 있는 거 맞는지."
|
|
133
|
+
* API와 MCP에는 있었고 CLI에만 없었다. 공개 페이지에 달린 댓글도 같은 곳에 쌓이므로
|
|
134
|
+
* 여기서 함께 나온다.
|
|
135
|
+
*/
|
|
136
|
+
async function cmdComments(args) {
|
|
137
|
+
const target = args._[0];
|
|
138
|
+
if (!target) throw new Error('사용법: rootr comments <path|id> [--block <blockId>] [--json]');
|
|
139
|
+
|
|
140
|
+
const client = makeClient();
|
|
141
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
142
|
+
const threads = await client.listComments(id, { blockId: args.block });
|
|
143
|
+
|
|
144
|
+
if (args.json) {
|
|
145
|
+
console.log(JSON.stringify(threads, null, 2));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!threads.length) {
|
|
149
|
+
console.log('댓글이 없습니다.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const t of threads) {
|
|
153
|
+
const when = String(t.createdAt ?? '').slice(0, 16).replace('T', ' ');
|
|
154
|
+
console.log(`- ${t.author?.name ?? '(알 수 없음)'} · ${when}`);
|
|
155
|
+
console.log(` ${String(t.body ?? '').replace(/\n/g, '\n ')}`);
|
|
156
|
+
for (const r of t.replies ?? []) {
|
|
157
|
+
const rWhen = String(r.createdAt ?? '').slice(0, 16).replace('T', ' ');
|
|
158
|
+
console.log(` ↳ ${r.author?.name ?? '(알 수 없음)'} · ${rWhen}`);
|
|
159
|
+
console.log(` ${String(r.body ?? '').replace(/\n/g, '\n ')}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
115
164
|
async function cmdRead(args) {
|
|
116
165
|
const target = args._[0];
|
|
117
166
|
if (!target) throw new Error('사용법: rootr read <path|id> [--json]');
|
|
@@ -442,10 +491,167 @@ async function cmdMcp() {
|
|
|
442
491
|
* 컬럼을 하나씩 붙이는 방식은 호출 순서가 곧 컬럼 순서가 되어 옮겨온 표가 뒤죽박죽이
|
|
443
492
|
* 됐다 — 그래서 CLI도 "한 번에" 쪽만 제공한다.
|
|
444
493
|
*/
|
|
494
|
+
/**
|
|
495
|
+
* `rootr access <path|id> [--private|--public|--inherit] [--grant <id> --level …] [--revoke <id>]`
|
|
496
|
+
*
|
|
497
|
+
* 화면의 "공개범위"·"권한" 창을 터미널에서. 인자 없이 부르면 지금 상태를 보여 준다.
|
|
498
|
+
* 관리 권한이 없으면 서버가 막는다(보기도 안 된다).
|
|
499
|
+
*/
|
|
500
|
+
async function cmdAccess(args) {
|
|
501
|
+
const target = args._[0];
|
|
502
|
+
if (!target) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
'사용법: rootr access <path|id> [--private|--public|--inherit] [--grant <userId> --level READ|WRITE|MANAGE] [--revoke <userId>]',
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const client = makeClient();
|
|
508
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
509
|
+
const group = args.group ? 'GROUP' : 'USER';
|
|
510
|
+
|
|
511
|
+
let changed = false;
|
|
512
|
+
if (args.private || args.public || args.inherit) {
|
|
513
|
+
const visibility = args.private ? 'PRIVATE' : args.public ? 'PUBLIC' : null;
|
|
514
|
+
await client.setNodeVisibility(id, visibility);
|
|
515
|
+
console.log(
|
|
516
|
+
`공개범위: ${visibility === 'PRIVATE' ? '비공개(만든 사람·관리자·따로 준 사람만)' : visibility === 'PUBLIC' ? '워크스페이스 전체' : '상위 폴더/워크스페이스 기본값을 따름'}`,
|
|
517
|
+
);
|
|
518
|
+
changed = true;
|
|
519
|
+
}
|
|
520
|
+
if (typeof args.grant === 'string') {
|
|
521
|
+
const level = String(args.level || '').toUpperCase();
|
|
522
|
+
if (!['READ', 'WRITE', 'MANAGE'].includes(level)) {
|
|
523
|
+
throw new Error('--grant 에는 --level READ|WRITE|MANAGE 를 함께 주세요.');
|
|
524
|
+
}
|
|
525
|
+
await client.grantNodePermission(id, { subjectType: group, subjectId: args.grant, level });
|
|
526
|
+
console.log(`권한 부여: ${args.grant} → ${level}`);
|
|
527
|
+
changed = true;
|
|
528
|
+
}
|
|
529
|
+
if (typeof args.revoke === 'string') {
|
|
530
|
+
await client.revokeNodePermission(id, { subjectType: group, subjectId: args.revoke });
|
|
531
|
+
console.log(`권한 회수: ${args.revoke}`);
|
|
532
|
+
changed = true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const info = await client.getNodeAccess(id);
|
|
536
|
+
if (args.json) {
|
|
537
|
+
console.log(JSON.stringify(info, null, 2));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (!changed) {
|
|
541
|
+
const eff = info.effectiveVisibility ?? info.visibility;
|
|
542
|
+
console.log(`공개범위: ${eff === 'PRIVATE' ? '비공개' : '워크스페이스 전체'}${info.visibility === null ? ' (상속받음)' : ''}`);
|
|
543
|
+
}
|
|
544
|
+
const direct = info.direct ?? [];
|
|
545
|
+
console.log(`따로 준 권한: ${direct.length ? '' : '없음'}`);
|
|
546
|
+
for (const p of direct) console.log(` - ${p.subjectType} ${p.subjectId} → ${p.level}`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* `rootr form responses|response-edit|response-rm`
|
|
551
|
+
*
|
|
552
|
+
* 폼에 들어온 응답을 터미널에서 보고 고치고 지운다. 화면에서만 되면
|
|
553
|
+
* 에이전트에게는 없는 기능이라 여기까지 열어 둔다.
|
|
554
|
+
*/
|
|
555
|
+
async function cmdForm(args) {
|
|
556
|
+
const sub = args._[0];
|
|
557
|
+
if (sub === 'collect') return cmdFormCollect(args);
|
|
558
|
+
if (sub === 'responses') return cmdFormResponses(args);
|
|
559
|
+
if (sub === 'response-edit') return cmdFormResponseEdit(args);
|
|
560
|
+
if (sub === 'response-rm') return cmdFormResponseRm(args);
|
|
561
|
+
throw new Error('사용법: rootr form collect|responses|response-edit|response-rm … (자세한 건 rootr --help)');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* `rootr form collect <path|id> --to db|sheet`
|
|
566
|
+
*
|
|
567
|
+
* 응답 모을 곳을 새로 만들어 연결한다. 표로 만들면 폼 질문 그대로 칸이 생기므로,
|
|
568
|
+
* 표를 미리 만들고 칸 이름을 하나하나 맞출 필요가 없다.
|
|
569
|
+
*/
|
|
570
|
+
async function cmdFormCollect(args) {
|
|
571
|
+
const target = args._[1];
|
|
572
|
+
if (!target) throw new Error('사용법: rootr form collect <path|id> --to db|sheet');
|
|
573
|
+
const to = String(args.to || '').toLowerCase();
|
|
574
|
+
const map = { db: 'database', database: 'database', sheet: 'spreadsheet', spreadsheet: 'spreadsheet' };
|
|
575
|
+
if (!map[to]) throw new Error('--to 에 db 또는 sheet 를 주세요.');
|
|
576
|
+
|
|
577
|
+
const client = makeClient();
|
|
578
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
579
|
+
const made = await client.createFormCollector(id, map[to]);
|
|
580
|
+
if (args.json) {
|
|
581
|
+
console.log(JSON.stringify(made, null, 2));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
console.log(
|
|
585
|
+
made.kind === 'database'
|
|
586
|
+
? `표를 만들어 연결했습니다: ${made.nodeId} (폼 질문 그대로 칸이 생겼습니다)`
|
|
587
|
+
: `스프레드시트를 만들어 연결했습니다: ${made.nodeId}`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function cmdFormResponses(args) {
|
|
592
|
+
const target = args._[1];
|
|
593
|
+
if (!target) throw new Error('사용법: rootr form responses <path|id> [--latest] [--json]');
|
|
594
|
+
const client = makeClient();
|
|
595
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
596
|
+
const rows = await client.listFormResponses(id, { latestPerRespondent: Boolean(args.latest) });
|
|
597
|
+
|
|
598
|
+
if (args.json) {
|
|
599
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!rows.length) {
|
|
603
|
+
console.log('아직 들어온 응답이 없습니다.');
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
console.log(`응답 ${rows.length}건${args.latest ? ' (사람마다 최신 1건만)' : ''}`);
|
|
607
|
+
for (const r of rows) {
|
|
608
|
+
const who = r.submitterUserId ? r.submitterUserId : '익명';
|
|
609
|
+
console.log(`- ${r.id} ${new Date(r.createdAt).toISOString()} 제출자:${who}`);
|
|
610
|
+
console.log(` ${JSON.stringify(r.values)}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function cmdFormResponseEdit(args) {
|
|
615
|
+
const responseId = args._[1];
|
|
616
|
+
if (!responseId) throw new Error("사용법: rootr form response-edit <응답id> --values '<json>'");
|
|
617
|
+
const raw = typeof args.values === 'string' ? args.values : await readStdin();
|
|
618
|
+
let values;
|
|
619
|
+
try {
|
|
620
|
+
values = JSON.parse(raw);
|
|
621
|
+
} catch {
|
|
622
|
+
throw new Error('--values 를 JSON으로 읽을 수 없습니다. {"필드id":"값"} 형태여야 합니다.');
|
|
623
|
+
}
|
|
624
|
+
// 통째로 바뀐다 — 남기고 싶은 답도 같이 보내야 한다.
|
|
625
|
+
const result = await makeClient().updateFormResponse(responseId, values);
|
|
626
|
+
if (args.json) {
|
|
627
|
+
console.log(JSON.stringify(result, null, 2));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
console.log(`응답 수정됨: ${result.id ?? responseId}`);
|
|
631
|
+
console.log(` ${JSON.stringify(result.values ?? values)}`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function cmdFormResponseRm(args) {
|
|
635
|
+
const responseId = args._[1];
|
|
636
|
+
if (!responseId) throw new Error('사용법: rootr form response-rm <응답id> [--yes]');
|
|
637
|
+
if (!args.yes) {
|
|
638
|
+
throw new Error('응답 삭제는 되돌릴 수 없습니다. 정말 지우려면 --yes 를 붙이세요.');
|
|
639
|
+
}
|
|
640
|
+
await makeClient().deleteFormResponse(responseId);
|
|
641
|
+
console.log(`응답 삭제됨: ${responseId}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
445
644
|
async function cmdDb(args) {
|
|
446
645
|
const sub = args._[0];
|
|
646
|
+
if (sub === 'rows') return cmdDbRows(args);
|
|
647
|
+
if (sub === 'row-add') return cmdDbRowAdd(args);
|
|
648
|
+
if (sub === 'row-body') return cmdDbRowBody(args);
|
|
649
|
+
if (sub === 'lock') return cmdDbLock(args);
|
|
650
|
+
if (sub === 'notify') return cmdDbNotify(args);
|
|
447
651
|
if (sub !== 'create') {
|
|
448
|
-
throw new Error(
|
|
652
|
+
throw new Error(
|
|
653
|
+
'사용법: rootr db create|rows|row-add|row-body|lock|notify … (자세한 건 rootr --help)',
|
|
654
|
+
);
|
|
449
655
|
}
|
|
450
656
|
const raw = typeof args.file === 'string' ? readFileSync(args.file, 'utf8') : await readStdin();
|
|
451
657
|
let spec;
|
|
@@ -472,6 +678,168 @@ async function cmdDb(args) {
|
|
|
472
678
|
if (cols) console.log(`컬럼 순서: ${cols}`);
|
|
473
679
|
if (typeof db.rowCount === 'number') console.log(`행: ${db.rowCount}개`);
|
|
474
680
|
}
|
|
681
|
+
|
|
682
|
+
/** 표의 행 목록. --body 를 주면 행마다 상세 내용(본문 메모)까지 가져온다. */
|
|
683
|
+
/**
|
|
684
|
+
* `rootr db lock <path|id> [--schema managers|open] [--rows own|all] [--locked <칸,칸>]`
|
|
685
|
+
*
|
|
686
|
+
* 표에 거는 자물쇠 — 노션에 없는 것이다. 인자 없이 부르면 지금 상태를 보여 준다.
|
|
687
|
+
*/
|
|
688
|
+
async function cmdDbLock(args) {
|
|
689
|
+
const target = args._[1];
|
|
690
|
+
if (!target) throw new Error('사용법: rootr db lock <path|id> [--schema managers|open] [--rows own|all] [--locked "승인,점수"]');
|
|
691
|
+
|
|
692
|
+
const client = makeClient();
|
|
693
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
694
|
+
|
|
695
|
+
const patch = {};
|
|
696
|
+
if (args.schema !== undefined) patch.schemaLock = args.schema;
|
|
697
|
+
if (args.rows !== undefined) patch.rowScope = args.rows;
|
|
698
|
+
if (args.locked !== undefined) {
|
|
699
|
+
patch.lockedProps = String(args.locked)
|
|
700
|
+
.split(',')
|
|
701
|
+
.map((x) => x.trim())
|
|
702
|
+
.filter(Boolean);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const result = Object.keys(patch).length
|
|
706
|
+
? await client.setDatabasePermissions(id, patch)
|
|
707
|
+
: await client.getDatabasePermissions(id);
|
|
708
|
+
|
|
709
|
+
if (args.json) {
|
|
710
|
+
console.log(JSON.stringify(result, null, 2));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
console.log(`칸·뷰 잠금: ${result.schemaLock === 'managers' ? '관리자만' : '누구나'}`);
|
|
714
|
+
console.log(`행 보기 : ${result.rowScope === 'own' ? '본인이 만든 행만' : '전부'}`);
|
|
715
|
+
console.log(`잠긴 칸 : ${(result.lockedProps ?? []).length ? result.lockedProps.join(', ') : '없음'}`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* `rootr db notify <path|id> [--file rules.json]`
|
|
720
|
+
*
|
|
721
|
+
* 표에 무슨 일이 생기면 누구에게 알릴지. 파일 없이 부르면 지금 규칙을 보여 준다.
|
|
722
|
+
* 규칙 모양: [{ "userIds": ["…"], "events": ["created"], "filter": {"propId":"p1","op":"is","value":"긴급"} }]
|
|
723
|
+
*/
|
|
724
|
+
async function cmdDbNotify(args) {
|
|
725
|
+
const target = args._[1];
|
|
726
|
+
if (!target) throw new Error('사용법: rootr db notify <path|id> [--file rules.json] [--clear] [--json]');
|
|
727
|
+
|
|
728
|
+
const client = makeClient();
|
|
729
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
730
|
+
|
|
731
|
+
if (args.clear) {
|
|
732
|
+
const cleared = await client.setDatabaseNotifications(id, []);
|
|
733
|
+
console.log('알림 규칙을 모두 지웠습니다.');
|
|
734
|
+
if (args.json) console.log(JSON.stringify(cleared, null, 2));
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (args.file) {
|
|
738
|
+
const raw = readFileSync(args.file, 'utf8');
|
|
739
|
+
const saved = await client.setDatabaseNotifications(id, JSON.parse(raw));
|
|
740
|
+
console.log(`알림 규칙 ${saved.watchers.length}개를 저장했습니다.`);
|
|
741
|
+
if (args.json) console.log(JSON.stringify(saved, null, 2));
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const { watchers } = await client.getDatabaseNotifications(id);
|
|
746
|
+
if (args.json) {
|
|
747
|
+
console.log(JSON.stringify(watchers, null, 2));
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (!watchers.length) {
|
|
751
|
+
console.log('알림 규칙이 없습니다.');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const WHEN = { created: '행 추가', updated: '행 수정', deleted: '행 삭제' };
|
|
755
|
+
for (const w of watchers) {
|
|
756
|
+
const when = (w.events ?? []).map((e) => WHEN[e] ?? e).join(' · ');
|
|
757
|
+
const f = w.filter;
|
|
758
|
+
const only = f
|
|
759
|
+
? f.op === 'changed'
|
|
760
|
+
? ` (${f.propId} 칸이 바뀌었을 때만)`
|
|
761
|
+
: ` (${f.propId} ${f.op === 'is' ? '=' : '⊃'} ${f.value})`
|
|
762
|
+
: '';
|
|
763
|
+
console.log(`- ${when}${only} → ${(w.userIds ?? []).length}명`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function cmdDbRows(args) {
|
|
768
|
+
const target = args._[1];
|
|
769
|
+
if (!target) throw new Error('사용법: rootr db rows <path|id> [--body] [--json]');
|
|
770
|
+
const client = makeClient();
|
|
771
|
+
const id = await resolveDatabaseId(client, target);
|
|
772
|
+
const rows = await client.listRows(id, { withBody: !!args.body });
|
|
773
|
+
if (args.json) {
|
|
774
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
for (const row of rows) {
|
|
778
|
+
const values = Object.values(row.values || {})
|
|
779
|
+
.map((v) => (v && typeof v === 'object' ? JSON.stringify(v) : String(v)))
|
|
780
|
+
.join(' | ');
|
|
781
|
+
console.log(`${row.id}\t${values}`);
|
|
782
|
+
if (args.body && row.body) {
|
|
783
|
+
for (const line of String(row.body).trimEnd().split('\n')) console.log(` ${line}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** 행 추가. 값은 JSON으로, 상세 내용(본문 메모)은 --body / --body-file 로. */
|
|
789
|
+
async function cmdDbRowAdd(args) {
|
|
790
|
+
const target = args._[1];
|
|
791
|
+
if (!target || typeof args.values !== 'string') {
|
|
792
|
+
throw new Error("사용법: rootr db row-add <path|id> --values '{\"제목\":\"...\"}' [--body <md>|--body-file <f>]");
|
|
793
|
+
}
|
|
794
|
+
let values;
|
|
795
|
+
try {
|
|
796
|
+
values = JSON.parse(args.values);
|
|
797
|
+
} catch {
|
|
798
|
+
throw new Error('--values 는 JSON이어야 합니다. 예: --values \'{"제목":"킥오프"}\'');
|
|
799
|
+
}
|
|
800
|
+
const body =
|
|
801
|
+
typeof args['body-file'] === 'string'
|
|
802
|
+
? readFileSync(args['body-file'], 'utf8')
|
|
803
|
+
: typeof args.body === 'string'
|
|
804
|
+
? args.body
|
|
805
|
+
: undefined;
|
|
806
|
+
const client = makeClient();
|
|
807
|
+
const id = await resolveDatabaseId(client, target);
|
|
808
|
+
const row = await client.createRow(id, { values, body });
|
|
809
|
+
console.log(`행 추가: ${row.id}`);
|
|
810
|
+
if (row.pageNodeId) console.log(`상세 내용 문서: ${row.pageNodeId}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** 행의 상세 내용(본문 메모)을 통째로 쓴다. 내용은 인자·--file·표준입력 순으로 받는다. */
|
|
814
|
+
async function cmdDbRowBody(args) {
|
|
815
|
+
const target = args._[1];
|
|
816
|
+
const rowId = args._[2];
|
|
817
|
+
if (!target || !rowId) {
|
|
818
|
+
throw new Error('사용법: rootr db row-body <path|id> <rowId> [<본문>] [--file <f>]');
|
|
819
|
+
}
|
|
820
|
+
const body =
|
|
821
|
+
typeof args.file === 'string'
|
|
822
|
+
? readFileSync(args.file, 'utf8')
|
|
823
|
+
: args._[3] !== undefined
|
|
824
|
+
? String(args._[3])
|
|
825
|
+
: await readStdin();
|
|
826
|
+
if (!body) throw new Error('쓸 내용이 없습니다 (본문 인자, --file, 또는 표준입력 필요).');
|
|
827
|
+
const client = makeClient();
|
|
828
|
+
const id = await resolveDatabaseId(client, target);
|
|
829
|
+
const row = await client.updateRow(id, rowId, { body });
|
|
830
|
+
console.log(`상세 내용 저장: 행 ${rowId}${row.pageNodeId ? ` → 문서 ${row.pageNodeId}` : ''}`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** 경로로 줘도 되게 — 표 인자도 문서 도구들과 같은 규칙을 따른다. */
|
|
834
|
+
async function resolveDatabaseId(client, target) {
|
|
835
|
+
if (typeof target === 'string' && target.startsWith('/')) {
|
|
836
|
+
const resolved = await resolveTarget(client, target);
|
|
837
|
+
if (!resolved.existed) throw new Error(`표를 찾을 수 없습니다: ${target}`);
|
|
838
|
+
return resolved.id;
|
|
839
|
+
}
|
|
840
|
+
return target;
|
|
841
|
+
}
|
|
842
|
+
|
|
475
843
|
async function main() {
|
|
476
844
|
const argv = process.argv.slice(2);
|
|
477
845
|
const args = parseArgs(argv);
|
|
@@ -489,6 +857,8 @@ async function main() {
|
|
|
489
857
|
switch (cmd) {
|
|
490
858
|
case 'ls':
|
|
491
859
|
return cmdLs(args);
|
|
860
|
+
case 'comments':
|
|
861
|
+
return cmdComments(args);
|
|
492
862
|
case 'read':
|
|
493
863
|
return cmdRead(args);
|
|
494
864
|
case 'write':
|
|
@@ -515,6 +885,10 @@ async function main() {
|
|
|
515
885
|
return cmdUse(args);
|
|
516
886
|
case 'db':
|
|
517
887
|
return cmdDb(args);
|
|
888
|
+
case 'form':
|
|
889
|
+
return cmdForm(args);
|
|
890
|
+
case 'access':
|
|
891
|
+
return cmdAccess(args);
|
|
518
892
|
case 'mcp':
|
|
519
893
|
return cmdMcp(args);
|
|
520
894
|
default:
|