rootr-cli 0.20.0 → 0.23.3
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.ko.md +51 -2
- package/README.md +53 -7
- package/bin/rootr.js +186 -11
- package/lib/client.js +73 -2
- package/lib/mcp-tools/attachments.js +58 -1
- package/lib/mcp-tools/booking.js +21 -1
- package/lib/mcp-tools/databases.js +289 -4
- package/lib/mcp-tools/forms.js +6 -1
- package/lib/mcp-tools/pages.js +35 -8
- package/lib/mcp-tools/presentations.js +19 -0
- package/lib/mcp-tools/workspaces.js +5 -0
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -192,6 +192,7 @@ rootr rm /notes/todo.md --yes # 확인 없이 (스크립트용)
|
|
|
192
192
|
```bash
|
|
193
193
|
rootr ws # 목록 (현재 워크스페이스에 *, 못 여는 곳은 표시)
|
|
194
194
|
rootr use "영업팀" # 전환 — id · 이름 · rootr.io 주소 아무거나
|
|
195
|
+
rootr emblem ./logo.png # 워크스페이스 엠블럼(사이드바 로고, 관리자) — --clear 면 지움
|
|
195
196
|
```
|
|
196
197
|
|
|
197
198
|
`rootr ws` 출력은 `id\tname` 목록입니다. 등록된 키가 워크스페이스 전용 키뿐이면 목록에는
|
|
@@ -401,6 +402,7 @@ rootr memory forget <id> # 폐기(--hard면 행 삭제)
|
|
|
401
402
|
| `rootr_create_workspace` | **계정 키(PAT) + workspaces:create** | 짓기 3단계 — 계획된 트리로 새 워크스페이스 생성(검사 결과도 같이 돌려준다) |
|
|
402
403
|
| `rootr_scaffold_apply` | docs:write | 짓기 3단계 — 기존 워크스페이스에 트리 적용(검사 결과도 같이 돌려준다) |
|
|
403
404
|
| `rootr_delete_workspace` | **계정 키(PAT) + OWNER** | 짓다 실패해 남은 **빈 워크스페이스 치우기**. 이름을 그대로 다시 적어야 지워진다(되돌릴 수 없음) |
|
|
405
|
+
| `rootr_set_workspace_emblem` | docs:write + **ADMIN** | 워크스페이스 **엠블럼**(사이드바 배지·전환기 로고) 바꾸기 — 이미지 파일·이미 올린 첨부 id·`clear` 중 하나 |
|
|
404
406
|
|
|
405
407
|
스캐폴딩은 **plan → 물어보기 → 설계 → review → 고치기 → apply** 한 바퀴입니다.
|
|
406
408
|
`review` 를 건너뛰면 "얇은 마크다운 파일 묶음"이 됩니다. CLI 로도 같은 세 단계를 쓸 수 있습니다:
|
|
@@ -542,6 +544,26 @@ rootr db view <표> <뷰> --scale year # 달력 보기를 주·달·해
|
|
|
542
544
|
"steps": [{ "kind": "update_rows", "target": "selected", "values": { "오늘 할 일": true } }] }]
|
|
543
545
|
```
|
|
544
546
|
|
|
547
|
+
**같은 행이 이미 있으면 잇기.** 마스터 표에 등록하는 규칙(투입확정 → 사업장 마스터)은 기존 고객이면
|
|
548
|
+
이미 행이 있습니다. `add_row` 에 `match` 를 주면 만들기 전에 찾습니다 — 같은 이름이 **하나**면 새로
|
|
549
|
+
만들지 않고 그 행에 잇고, **없으면** 만들고, **여럿**이면 아무것도 안 만들고 요청 행에 "후보 여러 곳"
|
|
550
|
+
댓글을 남깁니다. 먼저 공백만 무시하고 똑같은 이름을 찾으므로 지점명까지 같으면 그 지점에 잇습니다.
|
|
551
|
+
관계 칸은 `"{배정 매니저}"` 처럼 칸 하나만 적으면(받는 칸이 같은 표를 가리킬 때) 행 자체가 옮겨집니다.
|
|
552
|
+
|
|
553
|
+
```json
|
|
554
|
+
[{ "label": "투입확정 시 사업장 등록",
|
|
555
|
+
"when": [{ "property": "상태", "op": "changed_to", "value": "투입확정" },
|
|
556
|
+
{ "property": "사업장", "op": "is_empty" }],
|
|
557
|
+
"steps": [{ "kind": "add_row", "database": "/사업장 마스터",
|
|
558
|
+
"values": { "업체명": "{요청 건}", "담당 매니저": "{배정 매니저}" },
|
|
559
|
+
"link": { "relation": "채용 요청", "from": "selected" },
|
|
560
|
+
"match": { "key": "{요청 건}", "on": "업체명",
|
|
561
|
+
"normalize": ["spaces", "parens", "suffix:의원", "trailingDigits"], "fill": "empty" } }] }]
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
`rootr db automations <표>` 는 규칙마다 "같은 행이 있으면 잇기"와 마지막 사유(후보 여러 곳·조건에 걸려
|
|
565
|
+
비운 칸)를 함께 보여 줍니다.
|
|
566
|
+
|
|
545
567
|
스캐폴드로 표를 지을 때도 `database.buttons` · `database.automations` 에 같은 모양을 넣으면
|
|
546
568
|
**표와 함께** 달립니다.
|
|
547
569
|
|
|
@@ -633,6 +655,11 @@ rootr db view <표> <뷰> --scale year # 달력 보기를 주·달·해
|
|
|
633
655
|
| `rootr_set_booking` | 예약 켜기/끄기와 규칙(요일·시간·길이·여유·며칠 전·하루 최대·쉬는 날·**막는 시간**·확정 방식·물어볼 칸·**결과 메일**·**구글 밋**·**방문자 직접 취소**·**방문자 직접 시간 변경**·**미리 알림**) |
|
|
634
656
|
| `rootr_booking_slots` | 방문자에게 실제로 보이는 빈 시간(규칙이 뜻대로 걸렸는지 확인) |
|
|
635
657
|
|
|
658
|
+
여유 시간은 둘입니다. `bufferMin`은 이 표로 잡힌 **예약** 앞뒤(CLI `--buffer`), `calendarBufferMin`은
|
|
659
|
+
연결한 구글 캘린더에 **이미 있던 일정** 앞뒤(CLI `--calendar-buffer 60`) — 밖에서 하는 미팅의 이동 시간
|
|
660
|
+
같은 것입니다. 예: 구글 쪽 60분·예약 0분이면 구글에 있는 14~15시 미팅 때문에 13~16시가 닫히고, 온라인
|
|
661
|
+
상담 예약끼리는 붙여 잡힙니다. 이 표가 구글에 적은 예약은 "이미 있던 일정"으로 세지 않습니다.
|
|
662
|
+
|
|
636
663
|
방문자 화면은 **방문자 자신의 시간대**로 빈 시간을 보여주고(고칠 수도 있습니다), 서버와
|
|
637
664
|
주고받는 값은 끝까지 주인 시간대입니다. 확정·이동 메일에는 **일정 초대장(.ics)** 이 붙어
|
|
638
665
|
방문자 캘린더에 바로 들어가고, 시간을 옮기면 그 일정이 고쳐지고 취소하면 사라집니다.
|
|
@@ -789,9 +816,31 @@ rootr db view <표> <뷰> --scale year # 달력 보기를 주·달·해
|
|
|
789
816
|
`rootr_append_page_blocks`, `rootr_update_page`, `rootr_update_page_block`,
|
|
790
817
|
`rootr_read_page_live_data`)로 저작합니다.
|
|
791
818
|
|
|
792
|
-
HTML을 짜는 것이 아니라 **정해진 블록
|
|
819
|
+
HTML을 짜는 것이 아니라 **정해진 블록 15종을 골라 쌓습니다** — `hero` `text`
|
|
793
820
|
`features` `gallery` `pricing` `testimonials` `faq` `timeline` `cta` `footer` +
|
|
794
|
-
표에서 값을 끌어오는 `liveStats` `liveTable` `liveChart` + 신청을 받는 `form` +
|
|
821
|
+
표에서 값을 끌어오는 `liveStats` `liveTable` `liveChart` + 신청을 받는 `form` +
|
|
822
|
+
방문자가 시간을 잡는 `booking`(`{bookingNodeId, headline?, type?}` — 예약을 켜고 공개한 표의 노드 id) +
|
|
823
|
+
자유 블록 `custom`.
|
|
824
|
+
|
|
825
|
+
**랜딩 페이지**용으로 두 블록은 가운데 본문 기둥을 벗어나 화면 폭 전체를 쓸 수 있습니다.
|
|
826
|
+
`hero`에 `size: "screen"`을 주면 화면 한 장을 채우는 큰 제목(화면 폭 전체·화면 높이·큰 글자)이 되고,
|
|
827
|
+
`custom`에 `bleed: true`를 주면 좌우·위아래 여백 없이 화면 폭 전체를 써서 연달아 놓은 블록이
|
|
828
|
+
틈 없는 색 띠로 이어집니다. `custom`에 `minHeight: "screen"`을 주면 어느 기기에서나 틀 높이가
|
|
829
|
+
화면 한 장이 되고(숫자 `height`를 같이 주면 그 값이 하한), `bleed: true`와 함께 쓰면 화면을 꽉 채우는
|
|
830
|
+
섹션이 됩니다. 맨 위 이름 줄은 `config.navEnabled: false`로 끕니다.
|
|
831
|
+
|
|
832
|
+
**메타(페이스북·인스타그램) 광고 성과 재기** — `config.metaPixelId`에 픽셀 ID(숫자)를 넣으면 발행한
|
|
833
|
+
페이지가 방문(`PageView`)과 `booking` 블록의 예약 완료(`Schedule`)를 그 픽셀로 보냅니다. 편집 화면에서는
|
|
834
|
+
싣지 않고, `""`를 보내면 꺼집니다. `config`는 고칠 때 통째로 바뀌니 다른 값도 같이 보내세요. 어느 광고에서
|
|
835
|
+
온 예약인지 표에서 바로 보려면 예약 표에 유입 경로 기록을 켭니다(`rootr booking set <표> --track-source on`,
|
|
836
|
+
MCP는 `rootr_set_booking`의 `trackSource: true`). "유입 경로" 칸에 방문자가 들어온 주소의 꼬리표가
|
|
837
|
+
`source=meta, medium=paid, campaign=ax, content=3-docs`처럼 적힙니다.
|
|
838
|
+
|
|
839
|
+
**스크롤 등장 효과** — 어느 블록이나 `reveal: "fade"`(서서히) 또는 `"fade-up"`(아래에서 위로)을 주면
|
|
840
|
+
(기본 `"none"`), 발행한 페이지에서 그 블록이 화면에 들어올 때 한 번 나타납니다(큰 제목은 제목 → 부제 →
|
|
841
|
+
버튼 순서로). `custom`에 `reveal`을 주면 **틀 안의 CSS 애니메이션도 그 순간에 시작**합니다 — 틀을 블록이
|
|
842
|
+
화면에 들어올 때 만들기 때문입니다. `@keyframes`는 `@media (prefers-reduced-motion: no-preference)` 안에
|
|
843
|
+
적으세요. 움직임 줄이기를 켠 방문자에게는 효과 없이 바로 보입니다.
|
|
795
844
|
|
|
796
845
|
`custom`은 **격리된 방**입니다: HTML/CSS를 마음대로 쓰되 `sandbox` 아이프레임 안에서만
|
|
797
846
|
그려집니다(스크립트 없음·같은 출처 아님·바깥 통신 차단). 그래서 대담한 CSS도 바깥 화면을
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ citations back to the exact paragraph.
|
|
|
11
11
|
*(한국어 문서: [README.ko.md](https://github.com/gwmage/rootr-cli/blob/main/README.ko.md))*
|
|
12
12
|
|
|
13
13
|
- `rootr` CLI — documents, databases, forms, signed documents, publishing, permissions, memory
|
|
14
|
-
- `rootr mcp` — stdio MCP server, **
|
|
14
|
+
- `rootr mcp` — stdio MCP server, **193 tools** (see [MCP tools](#mcp-tools))
|
|
15
15
|
|
|
16
16
|
Plain Node.js ESM, no build step. Node 18+ (uses the built-in `fetch`).
|
|
17
17
|
|
|
@@ -180,6 +180,7 @@ as-is. `write --if-match` returns 412 if the document changed in the meantime.
|
|
|
180
180
|
```bash
|
|
181
181
|
rootr ws # id<TAB>name
|
|
182
182
|
rootr use "Sales" # switch — id, name, or rootr.io URL
|
|
183
|
+
rootr emblem ./logo.png # workspace emblem (sidebar logo, admin) — --clear removes it
|
|
183
184
|
rootr ask "why did latency go up after last week's deploy?"
|
|
184
185
|
rootr ask "why did payment failures rise?" --workspace ws_123
|
|
185
186
|
```
|
|
@@ -197,7 +198,13 @@ rootr db rows-new /projects/tasks --file rows.json # many at once — all or
|
|
|
197
198
|
rootr db rows-set /projects/tasks --ids r1,r2 --values '{"Status":"Done"}'
|
|
198
199
|
rootr db rows-each /projects/tasks --json '[{"rowId":"r1","values":{"Score":3}}]'
|
|
199
200
|
rootr db rows-rm /projects/tasks --ids r1,r2 --yes
|
|
201
|
+
rootr db duplicates /hr/applicants --row r9 # same person (dedupe column) + latest linked history
|
|
202
|
+
rootr db merge /hr/applicants r9 --into r1 # merge a duplicate into the existing row
|
|
203
|
+
rootr db keep-duplicate /hr/applicants r9 --from r1 # keep as a new row, link both
|
|
204
|
+
rootr db history /hr/applicants --row r1 # who changed what, when, how
|
|
205
|
+
rootr db restore /hr/applicants --at 2026-10-15T09:00:00+09:00 --yes # rows + columns/views back to that time
|
|
200
206
|
rootr db view /projects/tasks Calendar --scale month # views: table, board, calendar, timeline
|
|
207
|
+
rootr db view /hr/applicants Board --cards "Interview date,Phone" # fields shown on board cards
|
|
201
208
|
rootr db lock /projects/tasks --schema managers --rows own --locked "Approved,Score"
|
|
202
209
|
rootr db notify /projects/tasks --file rules.json # notification rules
|
|
203
210
|
rootr db buttons /projects/tasks --file buttons.json # buttons that do work when pressed
|
|
@@ -215,6 +222,13 @@ add `on: ["schedule"]` with `schedule: { at: "09:00" }` for a daily job (Korean
|
|
|
215
222
|
Prefer `changed_to` over `is`: `is` stays true on every later edit, so a rule that creates
|
|
216
223
|
rows would create them again each time the row is saved.
|
|
217
224
|
|
|
225
|
+
A rule that registers into a master table should not duplicate customers who are already
|
|
226
|
+
there: give the `add_row` a `match: { key: "{Request}", on: "Company", normalize: ["spaces",
|
|
227
|
+
"parens", "suffix:Clinic", "trailingDigits"], fill: "empty" }`. One matching row → linked
|
|
228
|
+
instead of created; none → created; several → nothing is created and the request row gets a
|
|
229
|
+
"several candidates" comment. A value that is exactly `"{Assigned manager}"` going into a
|
|
230
|
+
relation to the same table copies the linked rows, not their titles.
|
|
231
|
+
|
|
218
232
|
`--upsert-key` is what makes a migration or a sync safe to retry — without it, retrying
|
|
219
233
|
after a partial failure silently double-loads (a real incident: 903 duplicate rows).
|
|
220
234
|
|
|
@@ -394,17 +408,17 @@ is refused and says when the grace was used.
|
|
|
394
408
|
|
|
395
409
|
## MCP tools
|
|
396
410
|
|
|
397
|
-
`rootr mcp` speaks MCP over stdio.
|
|
411
|
+
`rootr mcp` speaks MCP over stdio. 193 tools:
|
|
398
412
|
|
|
399
413
|
| Group | Count | Tools |
|
|
400
414
|
|---|---|---|
|
|
401
415
|
| Documents | 22 | list, read, write, append, edit, search, comment, version history, commits & merges, publish, trash & restore |
|
|
402
|
-
| Databases |
|
|
416
|
+
| Databases | 32 | create, columns, views, rows (single & bulk), row pages, permissions, notifications, buttons, duplicates (find / merge / keep & link), history & point-in-time restore (row / whole table) |
|
|
403
417
|
| CRM | 18 | companies, contacts, deals (incl. pipeline moves), activities, tasks, CSV import |
|
|
404
418
|
| Signed documents & research notes | 14 | create, sign, revise, reject, reopen, approval lines, signing codes, ledger verification, research-note preset |
|
|
405
419
|
| Forms | 10 | create, read, update, responses, share links, collectors |
|
|
406
420
|
| Presentations & images | 10 | decks, slides, diagram slides, image generation and background removal |
|
|
407
|
-
| Workspaces & scaffolding |
|
|
421
|
+
| Workspaces & scaffolding | 10 | list, create, delete, switch, set the emblem (sidebar logo), plan / review / apply a document tree, health check |
|
|
408
422
|
| Pages | 6 | HTML page nodes: create, read, update, append and patch blocks, live data |
|
|
409
423
|
| Spreadsheets | 7 | create, read, update, per-sheet create/update/delete, patch cells & formulas |
|
|
410
424
|
| Recordings | 7 | create, upload & download audio, transcribe, summarize, rename speakers |
|
|
@@ -420,6 +434,10 @@ is refused and says when the grace was used.
|
|
|
420
434
|
| Root-cause Q&A | 1 | ask the workspace knowledge graph, answer with citations |
|
|
421
435
|
| Feedback | 1 | send feedback to the Rootr team |
|
|
422
436
|
|
|
437
|
+
Two gaps keep breathing room: `bufferMin` around bookings made through the table (CLI `--buffer`) and
|
|
438
|
+
`calendarBufferMin` around events already on the connected Google Calendar, e.g. travel time to an outside
|
|
439
|
+
meeting (CLI `--calendar-buffer 60`). Bookings the table wrote to Google count as bookings, not existing events.
|
|
440
|
+
|
|
423
441
|
Open hours can be narrowed by blocked times — one afternoon, lunch every day, Monday mornings, or a whole
|
|
424
442
|
weekday — with `blockedTimes` on `rootr_set_booking` (CLI: `--block-time "daily 12:00-13:00, mon 09:00-11:00, fri"`).
|
|
425
443
|
|
|
@@ -451,9 +469,34 @@ signed-document, issue, CRM and LOG tools need `docs:read` / `docs:write`; `root
|
|
|
451
469
|
### Pages
|
|
452
470
|
|
|
453
471
|
A PAGE is **a screen for people outside the workspace** — an event announcement, a product page,
|
|
454
|
-
a sign-up. You do not write HTML: you stack **
|
|
472
|
+
a sign-up. You do not write HTML: you stack **15 predefined block types** — `hero`, `text`,
|
|
455
473
|
`features`, `gallery`, `pricing`, `testimonials`, `faq`, `timeline`, `cta`, `footer`, the
|
|
456
|
-
table-backed `liveStats`, `liveTable`, `liveChart`, the `form` block,
|
|
474
|
+
table-backed `liveStats`, `liveTable`, `liveChart`, the `form` block, the `booking` block
|
|
475
|
+
(a visitor scheduling calendar: `{bookingNodeId, headline?, type?}` — the node id of a published
|
|
476
|
+
table with visitor booking on) — plus the free-form `custom`.
|
|
477
|
+
|
|
478
|
+
For a **landing page**, two options let a block use the whole page width instead of the centred
|
|
479
|
+
text column: `hero` with `size: "screen"` is a full-screen hero (edge to edge, one viewport tall,
|
|
480
|
+
large headline), and `custom` with `bleed: true` spans the full width with no gap around it, so
|
|
481
|
+
consecutive bleed blocks read as seamless colour bands. `custom` with `minHeight: "screen"` is one
|
|
482
|
+
viewport tall on every device (a numeric `height` given with it is the floor), so
|
|
483
|
+
`bleed: true` + `minHeight: "screen"` is a section that fills the screen. Pair them with
|
|
484
|
+
`config.navEnabled: false`.
|
|
485
|
+
|
|
486
|
+
To **measure Meta (Facebook/Instagram) ads**, set `config.metaPixelId` to the numeric Pixel ID: the
|
|
487
|
+
published page then reports `PageView` on load and `Schedule` when a visitor completes a booking in a
|
|
488
|
+
`booking` block. The editor never loads the pixel; send `""` to turn it off. `config` is replaced as a
|
|
489
|
+
whole on update, so resend the other keys with it. To see which ad each booking came from in the table
|
|
490
|
+
itself, turn on source tracking on the booking table (`rootr booking set <table> --track-source on`,
|
|
491
|
+
or `trackSource: true` in `rootr_set_booking`): a "유입 경로" column is filled from the visitor's
|
|
492
|
+
address, e.g. `source=meta, medium=paid, campaign=ax, content=3-docs`.
|
|
493
|
+
|
|
494
|
+
For **scroll-in animation**, any block takes `reveal: "fade"` or `"fade-up"` (default `"none"`): on
|
|
495
|
+
the published page it appears once, when it scrolls into view (a hero staggers title → subtitle →
|
|
496
|
+
button). On a `custom` block `reveal` also **starts the CSS animations inside the frame at that
|
|
497
|
+
moment** — the frame is created only when the block scrolls in — so put your `@keyframes` there and
|
|
498
|
+
wrap them in `@media (prefers-reduced-motion: no-preference)`. Visitors who ask for reduced motion
|
|
499
|
+
see everything at once.
|
|
457
500
|
|
|
458
501
|
`custom` is an **isolated room**: write any HTML/CSS you like, but it renders inside a `sandbox`
|
|
459
502
|
iframe (no scripts, not same-origin, no outbound calls), so even bold CSS cannot leak onto the
|
|
@@ -506,7 +549,10 @@ Decks are authored with `rootr_create_presentation`, `rootr_read_presentation`,
|
|
|
506
549
|
`rootr_append_presentation_slides`, `rootr_update_presentation_slide`,
|
|
507
550
|
`rootr_reorder_presentation_slides` and `rootr_update_presentation`. Artwork can be produced with
|
|
508
551
|
`rootr_generate_image` (image from a sentence) and `rootr_remove_image_background` — both spend
|
|
509
|
-
AI credits.
|
|
552
|
+
AI credits. Slides may also carry `videos[]` (uploaded mp4/webm ≤75MB, https video files, YouTube/Vimeo links;
|
|
553
|
+
Team plan and above), and any element in slide `html` can animate with `data-anim="fade-up"` (also fade,
|
|
554
|
+
fade-down, fly-left, fly-right, zoom, pop, blur, and looping pulse/float/spin/blink/shake; timing via
|
|
555
|
+
`--anim-dur`/`--anim-delay` inline CSS variables; all plans). The same thing works over plain REST with the same API key (`x-api-key`,
|
|
510
556
|
`docs:write`):
|
|
511
557
|
|
|
512
558
|
```bash
|
package/bin/rootr.js
CHANGED
|
@@ -28,6 +28,7 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
28
28
|
rootr restore <id> 지운 것 되돌리기 (화면의 Ctrl+Z 와 같은 일 — 경로 아닌 id)
|
|
29
29
|
rootr attach <file> [--doc <path|id>] [--heading "## X"] [--alt <text>] 파일 업로드(+문서에 삽입)
|
|
30
30
|
rootr ws 내 워크스페이스 목록 (id\tname, 열 수 없는 곳은 표시)
|
|
31
|
+
rootr emblem <이미지 파일> | --clear 워크스페이스 엠블럼(사이드바 로고) 바꾸기·지우기 (관리자)
|
|
31
32
|
rootr use <id|이름|주소> 활성 워크스페이스 전환 (그 워크스페이스용 키도 기억)
|
|
32
33
|
rootr ask <question> [--workspace <w>] GraphRAG 질의응답 (RCA), 인용 포함 출력
|
|
33
34
|
rootr db create [--file <spec.json>] [--parent <path|id>] 표(DATABASE) 생성 — 컬럼 순서 그대로, 행까지 한 번에
|
|
@@ -38,13 +39,18 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
38
39
|
rootr db button-run <path|id> <buttonId> [--rows id,id] 버튼 한 번 누르기 (행 만들기·값 채우기·AI 채우기)
|
|
39
40
|
rootr db automations <path|id> [--file rules.json] [--clear] 자동 규칙 — "값이 이렇게 되면 이 일을 해라"·정해진 시각
|
|
40
41
|
rootr db import-sheets <파일.xlsx> --ws <id> [--dry] 시트 수백 장짜리 엑셀을 표 하나로 접어 가져오기
|
|
41
|
-
rootr db view <path|id> [뷰이름] [--scale week|month|year] [--name 새이름] 뷰 목록·달력 주/달/해
|
|
42
|
+
rootr db view <path|id> [뷰이름] [--scale week|month|year] [--name 새이름] [--cards "칸,칸"] 뷰 목록·달력 주/달/해 단위·보드 카드에 보일 칸
|
|
42
43
|
rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] 행 추가(상세 내용 포함)
|
|
43
44
|
rootr db row-body <path|id> <rowId> [--file <f>] 행의 상세 내용(본문 메모) 쓰기 — 없으면 표준입력
|
|
44
45
|
rootr db rows-new <path|id> --json '[{…},…]' | --file <파일> 여러 행 한 번에 넣기 (하나라도 틀리면 전부 취소)
|
|
45
46
|
rootr db rows-set <path|id> --ids <id,id> --values '<json>' 고른 행 전부에 같은 값 (날짜 일괄 변경 등)
|
|
46
47
|
rootr db rows-each <path|id> --json '[{"rowId":…,"values":{…}},…]' 행마다 다른 값으로 한 번에 수정
|
|
47
48
|
rootr db rows-rm <path|id> --ids <id,id> [--yes] 고른 행 한 번에 삭제 — 하나라도 막히면 통째로 거절
|
|
49
|
+
rootr db duplicates <path|id> (--row <rowId> | --values '<json>') [--json] 같은 사람(중복 감지 칸 값이 같은 행)과 그 이력
|
|
50
|
+
rootr db merge <path|id> <rowId> --into <기존 rowId> 중복 행을 기존 행에 합치기(빈 칸만 채움, rowId 행은 지움)
|
|
51
|
+
rootr db keep-duplicate <path|id> <rowId> --from <이전 rowId> 새 행으로 두되 이전 행과 서로 링크
|
|
52
|
+
rootr db history <path|id> [--row <rowId>] [--limit N] [--json] 행·표 변경 이력(누가·언제·어떻게·전후 값)
|
|
53
|
+
rootr db restore <path|id> --at <ISO 시각> [--yes] 표 전체(행+칸·보기·버튼·규칙)를 그 시각으로 — --yes 없으면 미리보기만
|
|
48
54
|
rootr db row-page <rowId> [--type RECORDING] 이 행만 원하는 타입으로 상세 붙이기(녹음·스프레드시트·서명문서…)
|
|
49
55
|
rootr db row-page <rowId> --node <path|id> 이미 있는 문서를 이 행에 잇기 (옮기지 않고 가리킴)
|
|
50
56
|
rootr db row-page <rowId> --unlink 연결만 끊기 — 문서는 그대로 남음
|
|
@@ -129,6 +135,7 @@ usage:
|
|
|
129
135
|
rootr restore <id> undo a delete (same as Ctrl+Z in the app — id, not path)
|
|
130
136
|
rootr attach <file> [--doc <path|id>] [--heading "## X"] [--alt <text>] upload a file (+ insert it into a document)
|
|
131
137
|
rootr ws my workspaces (id\tname; unreachable ones are marked)
|
|
138
|
+
rootr emblem <image file> | --clear set or clear the workspace emblem (sidebar logo; admin)
|
|
132
139
|
rootr use <id|name|url> switch the active workspace (remembers its key too)
|
|
133
140
|
rootr ask <question> [--workspace <w>] GraphRAG question answering (RCA), prints citations
|
|
134
141
|
rootr db create [--file <spec.json>] [--parent <path|id>] create a DATABASE — columns in order, rows included
|
|
@@ -146,6 +153,11 @@ usage:
|
|
|
146
153
|
rootr db rows-set <path|id> --ids <id,id> --values '<json>' same value on every chosen row (bulk date changes, …)
|
|
147
154
|
rootr db rows-each <path|id> --json '[{"rowId":…,"values":{…}},…]' a different value per row, in one call
|
|
148
155
|
rootr db rows-rm <path|id> --ids <id,id> [--yes] delete chosen rows at once — one blocked row rejects all
|
|
156
|
+
rootr db duplicates <path|id> (--row <rowId> | --values '<json>') [--json] rows of the same person (same dedupe value) with history
|
|
157
|
+
rootr db merge <path|id> <rowId> --into <existing rowId> merge a duplicate into the existing row (fills blanks, deletes rowId)
|
|
158
|
+
rootr db keep-duplicate <path|id> <rowId> --from <earlier rowId> keep as a new row but link both rows
|
|
159
|
+
rootr db history <path|id> [--row <rowId>] [--limit N] [--json] change history (who, when, how, before/after)
|
|
160
|
+
rootr db restore <path|id> --at <ISO time> [--yes] restore the whole table (rows + columns/views/buttons/rules) — preview without --yes
|
|
149
161
|
rootr db row-page <rowId> [--type RECORDING] give this row a typed detail page (recording, spreadsheet, signed doc…)
|
|
150
162
|
rootr db row-page <rowId> --node <path|id> attach an existing document to this row (points at it, does not move it)
|
|
151
163
|
rootr db row-page <rowId> --unlink unlink only — the document stays
|
|
@@ -587,6 +599,30 @@ async function cmdAttach(args) {
|
|
|
587
599
|
console.log(markdown);
|
|
588
600
|
}
|
|
589
601
|
|
|
602
|
+
/** WS-EMBLEM — 설정 → 일반 → 엠블럼과 같은 일. 이미지를 올려 걸거나(--clear 면) 이름 첫 글자 배지로 되돌린다. */
|
|
603
|
+
async function cmdEmblem(args) {
|
|
604
|
+
const filePath = args._[0];
|
|
605
|
+
if (!filePath && !args.clear) throw new Error(usage(`rootr emblem <image file> | --clear`));
|
|
606
|
+
const cfg = loadConfig();
|
|
607
|
+
const workspace = args.workspace || cfg.workspace;
|
|
608
|
+
if (!workspace) {
|
|
609
|
+
throw new RootrApiError(tr('워크스페이스가 설정되지 않았습니다.', `No workspace is configured.`), {
|
|
610
|
+
hint: tr('--workspace <id>로 넘기거나 rootr config --workspace <id> 또는 ROOTR_WORKSPACE 환경변수를 지정하세요.', `Pass --workspace <id>, or set it with rootr config --workspace <id> or the ROOTR_WORKSPACE environment variable.`),
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
const client = new RootrClient({ ...cfg, workspace, apiKey: keyForWorkspace(cfg, workspace) });
|
|
614
|
+
let id = null;
|
|
615
|
+
if (filePath) {
|
|
616
|
+
const mimeType = args.mime || MIME_BY_EXT[extname(filePath).toLowerCase()];
|
|
617
|
+
if (!mimeType || !mimeType.startsWith('image/')) {
|
|
618
|
+
throw new RootrApiError(tr('엠블럼은 이미지 파일만 됩니다 (png·jpg·webp·svg·gif).', 'The emblem must be an image file (png, jpg, webp, svg, gif).'));
|
|
619
|
+
}
|
|
620
|
+
({ id } = await client.uploadAttachment(workspace, { filename: basename(filePath), mimeType, data: readFileSync(filePath) }));
|
|
621
|
+
}
|
|
622
|
+
await client.setWorkspaceEmblem(workspace, id);
|
|
623
|
+
console.log(id ? tr(`엠블럼을 바꿨습니다: ${client.attachmentRawUrl(id)}`, `Emblem set: ${client.attachmentRawUrl(id)}`) : tr('엠블럼을 지웠습니다 (이름 첫 글자 배지).', 'Emblem cleared (initial-letter badge).'));
|
|
624
|
+
}
|
|
625
|
+
|
|
590
626
|
async function cmdWs() {
|
|
591
627
|
const cfg = loadConfig();
|
|
592
628
|
const candidates = candidateClients(cfg);
|
|
@@ -1361,11 +1397,24 @@ async function cmdDbAutomations(args) {
|
|
|
1361
1397
|
if (a.when) console.log(` ${tr('조건', `if`)}: ${a.when.property} ${a.when.op} ${a.when.value ?? ''}`);
|
|
1362
1398
|
for (const [i, st] of (a.steps ?? []).entries()) {
|
|
1363
1399
|
const where = st.database ? ` → ${st.database}` : '';
|
|
1364
|
-
console.log(` ${i + 1}. ${st.kind}${where}`);
|
|
1400
|
+
console.log(` ${i + 1}. ${st.kind}${where}${matchNote(st)}`);
|
|
1365
1401
|
}
|
|
1402
|
+
// AUTO-MATCH · AUTO-REL-COPY — 후보 여러 곳·조건에 걸려 비운 칸도 여기(사유 자리)에 남는다.
|
|
1403
|
+
if (a.lastError?.message) console.log(` ${tr('사유', `note`)}: ${a.lastError.message}`);
|
|
1366
1404
|
}
|
|
1367
1405
|
}
|
|
1368
1406
|
|
|
1407
|
+
/** AUTO-MATCH — `add_row.match` 가 있으면 "같은 행이 있으면 잇기"를 한 줄로. */
|
|
1408
|
+
function matchNote(st) {
|
|
1409
|
+
if (st.kind !== 'add_row' || !st.match) return '';
|
|
1410
|
+
const norm = st.match.normalize?.length ? ` [${st.match.normalize.join(', ')}]` : '';
|
|
1411
|
+
const fill = st.match.fill === 'empty' ? tr(', 빈 칸 채움', `, fill blanks`) : '';
|
|
1412
|
+
return tr(
|
|
1413
|
+
` · 같은 행이 있으면 잇기(${st.match.on} = ${st.match.key}${norm}${fill})`,
|
|
1414
|
+
` · link to an existing row if one matches (${st.match.on} = ${st.match.key}${norm}${fill})`,
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1369
1418
|
/**
|
|
1370
1419
|
* XLSX-FOLD — 시트가 수백 장인 엑셀을 **표 하나로 접어** 가져온다.
|
|
1371
1420
|
*
|
|
@@ -1544,6 +1593,111 @@ async function cmdDbRowsRm(args) {
|
|
|
1544
1593
|
console.log(tr(`${res.deleted}개 행을 지웠습니다.`, `${res.deleted} rows deleted.`));
|
|
1545
1594
|
}
|
|
1546
1595
|
|
|
1596
|
+
/**
|
|
1597
|
+
* DEDUPE(F5) — `rootr db duplicates <표> --row <rowId>` 또는 `--values '{"연락처":"010-…"}'`.
|
|
1598
|
+
* 같은 사람(중복 감지 칸 값이 같은 행)마다 보일 칸과 이어진 행 최근 5건(연락 이력)을 보인다.
|
|
1599
|
+
*/
|
|
1600
|
+
async function cmdDbDuplicates(args) {
|
|
1601
|
+
const target = args._[1];
|
|
1602
|
+
if (!target || (typeof args.row !== 'string' && typeof args.values !== 'string')) {
|
|
1603
|
+
throw new Error(usage(`rootr db duplicates <path|id> (--row <rowId> | --values '<json>') [--json]`));
|
|
1604
|
+
}
|
|
1605
|
+
let values;
|
|
1606
|
+
if (typeof args.values === 'string') {
|
|
1607
|
+
try {
|
|
1608
|
+
values = JSON.parse(args.values);
|
|
1609
|
+
} catch {
|
|
1610
|
+
throw new Error(tr('--values 를 JSON 으로 읽을 수 없습니다.', 'Could not read --values as JSON.'));
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
const client = makeClient();
|
|
1614
|
+
const id = await resolveDatabaseId(client, target);
|
|
1615
|
+
const res = await client.findDuplicates(id, { rowId: typeof args.row === 'string' ? args.row : undefined, values });
|
|
1616
|
+
if (args.json) return console.log(JSON.stringify(res, null, 2));
|
|
1617
|
+
if (!res.matches.length) return console.log(tr('같은 값의 기존 행이 없습니다.', 'No existing row has the same value.'));
|
|
1618
|
+
for (const m of res.matches) {
|
|
1619
|
+
console.log(tr(`「${m.propName}」 ${m.value} 같은 행:`, `Rows with ${m.propName} = ${m.value}:`));
|
|
1620
|
+
for (const r of m.rows) {
|
|
1621
|
+
console.log(`- ${r.title || '(제목 없음)'}\t${r.id}`);
|
|
1622
|
+
for (const p of r.preview) console.log(` ${p.name}: ${p.value}`);
|
|
1623
|
+
for (const h of r.history ?? []) {
|
|
1624
|
+
console.log(` ${h.propName} ${h.total}${tr('건', '')}`);
|
|
1625
|
+
for (const x of h.rows) console.log(` · ${[x.title, ...x.fields.map((f) => f.value)].filter(Boolean).join(' · ')}`);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/** DEDUPE — `rootr db merge <표> <rowId> --into <기존 rowId>` */
|
|
1632
|
+
async function cmdDbMerge(args) {
|
|
1633
|
+
const [, target, rowId] = args._;
|
|
1634
|
+
if (!target || !rowId || typeof args.into !== 'string') throw new Error(usage(`rootr db merge <path|id> <rowId> --into <rowId>`));
|
|
1635
|
+
const client = makeClient();
|
|
1636
|
+
const id = await resolveDatabaseId(client, target);
|
|
1637
|
+
const res = await client.mergeRows(id, rowId, args.into);
|
|
1638
|
+
console.log(
|
|
1639
|
+
tr(
|
|
1640
|
+
`합쳤습니다 → ${res.into} (빈 칸 채움: ${res.filled.join(', ') || '없음'}). ${rowId} 행은 지웠습니다.`,
|
|
1641
|
+
`Merged into ${res.into} (filled: ${res.filled.join(', ') || 'none'}). Row ${rowId} was deleted.`,
|
|
1642
|
+
),
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/** ROW-HISTORY — `rootr db history <표> [--row <rowId>] [--limit N] [--json]` */
|
|
1647
|
+
async function cmdDbHistory(args) {
|
|
1648
|
+
const target = args._[1];
|
|
1649
|
+
if (!target) throw new Error(usage(`rootr db history <path|id> [--row <rowId>] [--limit N] [--json]`));
|
|
1650
|
+
const client = makeClient();
|
|
1651
|
+
const id = await resolveDatabaseId(client, target);
|
|
1652
|
+
const res = await client.databaseHistory(id, { rowId: typeof args.row === 'string' ? args.row : undefined, limit: args.limit ? Number(args.limit) : 50 });
|
|
1653
|
+
if (args.json) return console.log(JSON.stringify(res, null, 2));
|
|
1654
|
+
for (const c of res.changes) {
|
|
1655
|
+
const cols = [...new Set([...Object.keys(c.before ?? {}), ...Object.keys(c.after ?? {})])];
|
|
1656
|
+
console.log(`${c.at}\t${c.kind}\t${c.rowId}\t${c.actor?.name ?? '-'}${c.via ? ` (${c.via})` : ''}\t${cols.join(',')}\t${c.id}`);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/** ROW-RESTORE — `rootr db restore <표> --at <ISO> [--yes]` (--yes 없으면 미리보기) */
|
|
1661
|
+
async function cmdDbRestore(args) {
|
|
1662
|
+
const target = args._[1];
|
|
1663
|
+
if (!target || typeof args.at !== 'string') throw new Error(usage(`rootr db restore <path|id> --at <ISO time> [--yes]`));
|
|
1664
|
+
const client = makeClient();
|
|
1665
|
+
const id = await resolveDatabaseId(client, target);
|
|
1666
|
+
const preview = await client.restoreDatabase(id, args.at, { dryRun: true });
|
|
1667
|
+
const sc = preview.schema ?? {};
|
|
1668
|
+
console.log(
|
|
1669
|
+
tr(
|
|
1670
|
+
`고칠 행 ${preview.updates} · 되살릴 행 ${preview.creates} · 지울 행 ${preview.deletes}` +
|
|
1671
|
+
(sc.restorable ? (sc.differs ? ` · 표 정의: 되살릴 칸 ${sc.propsAdded}, 뺄 칸 ${sc.propsRemoved}, 바뀌는 칸 ${sc.propsChanged}${sc.views ? ', 보기' : ''}${sc.buttons ? ', 버튼' : ''}${sc.automations ? ', 자동 규칙' : ''}` : ' · 표 정의는 같음') : ' · 그 시각의 표 정의 기록 없음(행만)'),
|
|
1672
|
+
`rows to edit ${preview.updates} · revive ${preview.creates} · delete ${preview.deletes}` +
|
|
1673
|
+
(sc.restorable ? (sc.differs ? ` · definition: ${sc.propsAdded} column(s) back, ${sc.propsRemoved} removed, ${sc.propsChanged} changed${sc.views ? ', views' : ''}${sc.buttons ? ', buttons' : ''}${sc.automations ? ', automations' : ''}` : ' · definition unchanged') : ' · no definition record at that time (rows only)'),
|
|
1674
|
+
),
|
|
1675
|
+
);
|
|
1676
|
+
if (!args.yes) return console.log(tr('미리보기입니다. 되돌리려면 --yes 를 붙이세요.', 'Preview only. Add --yes to restore.'));
|
|
1677
|
+
const res = await client.restoreDatabase(id, args.at);
|
|
1678
|
+
if (res.job) {
|
|
1679
|
+
let job = { done: 0, total: res.job.total, state: 'running' };
|
|
1680
|
+
while (job.state !== 'done') {
|
|
1681
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1682
|
+
job = await client.restoreJob(id, res.job.id);
|
|
1683
|
+
process.stderr.write(`\r${job.done}/${job.total}`);
|
|
1684
|
+
}
|
|
1685
|
+
process.stderr.write('\n');
|
|
1686
|
+
if (job.failed) console.log(tr(`되돌리지 못한 행 ${job.failed}개 — 같은 시각으로 다시 실행하세요.`, `${job.failed} row(s) failed — run the same restore again.`));
|
|
1687
|
+
}
|
|
1688
|
+
console.log(tr('되돌렸습니다.', 'Restored.'));
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/** DEDUPE-KEEP — `rootr db keep-duplicate <표> <rowId> --from <이전 rowId>` */
|
|
1692
|
+
async function cmdDbKeepDuplicate(args) {
|
|
1693
|
+
const [, target, rowId] = args._;
|
|
1694
|
+
if (!target || !rowId || typeof args.from !== 'string') throw new Error(usage(`rootr db keep-duplicate <path|id> <rowId> --from <rowId>`));
|
|
1695
|
+
const client = makeClient();
|
|
1696
|
+
const id = await resolveDatabaseId(client, target);
|
|
1697
|
+
await client.keepDuplicate(id, rowId, args.from);
|
|
1698
|
+
console.log(tr(`새 행으로 두고 ${args.from} 행과 서로 링크를 남겼습니다.`, `Kept as a new row and linked with ${args.from}.`));
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1547
1701
|
/**
|
|
1548
1702
|
* PUBLIC-ANY — `rootr publish <노드> [--children|--off|--status]`
|
|
1549
1703
|
*
|
|
@@ -1789,24 +1943,24 @@ async function cmdBooking(args) {
|
|
|
1789
1943
|
usage(tr(`rootr booking show <path|id>`, `rootr booking show <path|id>`)) +
|
|
1790
1944
|
tr(
|
|
1791
1945
|
`\n rootr booking set <path|id> [--on|--off] [--days 1,2,3,4,5] [--hours 10:00-18:00]\n` +
|
|
1792
|
-
` [--duration 30] [--buffer 10] [--notice 2] [--ahead 60] [--per-day 4]\n` +
|
|
1946
|
+
` [--duration 30] [--buffer 10] [--calendar-buffer 60] [--notice 2] [--ahead 60] [--per-day 4]\n` +
|
|
1793
1947
|
` [--block 2026-10-03,2026-10-09] [--approval instant|manual] [--tz Asia/Seoul]\n` +
|
|
1794
1948
|
` [--block-time "daily 12:00-13:00=점심, mon 09:00-11:00, fri"]\n` +
|
|
1795
1949
|
` [--meet on|off] [--notify-visitor on|off] [--notify-email a@b.com,c@d.com] [--self-cancel on|off]\n` +
|
|
1796
1950
|
` [--self-move on|off] [--remind 1d,1h (안 보내려면 --remind off)]\n` +
|
|
1797
|
-
` [--types "30분 상담=30m,데모=60m|화면 보며 설명"] [--guests on|off] [--max-guests 5]\n` +
|
|
1951
|
+
` [--types "30분 상담=30m,데모=60m|화면 보며 설명"] [--guests on|off] [--max-guests 5] [--track-source on|off]\n` +
|
|
1798
1952
|
` [--after https://example.com/thanks]\n` +
|
|
1799
1953
|
` [--hosts "김상담=a@acme.com,이데모=b@acme.com"] [--assign roundrobin|collective|off]\n` +
|
|
1800
1954
|
` [--ask "연락처:required,상담 종류"] [--message "안내 문구"]\n` +
|
|
1801
1955
|
` [--gcal-connection <id>] [--gcal-busy <캘린더id,...>] [--gcal-write <캘린더id|none>]\n` +
|
|
1802
1956
|
` rootr booking slots <path|id> [--from 2026-10-01] [--to 2026-10-31] [--type <종류 이름표>]\n`,
|
|
1803
1957
|
`\n rootr booking set <path|id> [--on|--off] [--days 1,2,3,4,5] [--hours 10:00-18:00]\n` +
|
|
1804
|
-
` [--duration 30] [--buffer 10] [--notice 2] [--ahead 60] [--per-day 4]\n` +
|
|
1958
|
+
` [--duration 30] [--buffer 10] [--calendar-buffer 60] [--notice 2] [--ahead 60] [--per-day 4]\n` +
|
|
1805
1959
|
` [--block 2026-10-03,2026-10-09] [--approval instant|manual] [--tz Asia/Seoul]\n` +
|
|
1806
1960
|
` [--block-time "daily 12:00-13:00=lunch, mon 09:00-11:00, fri"]\n` +
|
|
1807
1961
|
` [--meet on|off] [--notify-visitor on|off] [--notify-email a@b.com,c@d.com] [--self-cancel on|off]\n` +
|
|
1808
1962
|
` [--self-move on|off] [--remind 1d,1h (--remind off to stop)]\n` +
|
|
1809
|
-
` [--types "Intro=30m,Demo=60m|walk through the product"] [--guests on|off] [--max-guests 5]\n` +
|
|
1963
|
+
` [--types "Intro=30m,Demo=60m|walk through the product"] [--guests on|off] [--max-guests 5] [--track-source on|off]\n` +
|
|
1810
1964
|
` [--after https://example.com/thanks]\n` +
|
|
1811
1965
|
` [--hosts "Sam=a@acme.com,Lee=b@acme.com"] [--assign roundrobin|collective|off]\n` +
|
|
1812
1966
|
` [--ask "Phone:required,Topic"] [--message "note for visitors"]\n` +
|
|
@@ -1830,7 +1984,7 @@ async function cmdBooking(args) {
|
|
|
1830
1984
|
const days = (b.weekdays || []).map((d) => DAY_NAMES[d]).join('·');
|
|
1831
1985
|
console.log(tr(`방문자 일정 등록: 켜짐`, `Visitor booking: on`));
|
|
1832
1986
|
console.log(tr(` 받는 때: ${days} ${b.startTime}~${b.endTime} (${b.timezone})`, ` when: ${days} ${b.startTime}-${b.endTime} (${b.timezone})`));
|
|
1833
|
-
console.log(tr(` 한 번 길이: ${b.durationMin}분 ·
|
|
1987
|
+
console.log(tr(` 한 번 길이: ${b.durationMin}분 · 예약 앞뒤 여유 ${b.bufferMin}분 · 구글 기존 일정 앞뒤 여유 ${b.calendarBufferMin ?? b.bufferMin}분`, ` slot: ${b.durationMin} min · gap around bookings ${b.bufferMin} min · gap around existing Google events ${b.calendarBufferMin ?? b.bufferMin} min`));
|
|
1834
1988
|
console.log(tr(` ${b.minNoticeHours}시간 전까지 · ${b.maxDaysAhead}일 뒤까지 · 하루 ${b.maxPerDay || '제한 없음'}`, ` ${b.minNoticeHours}h notice · up to ${b.maxDaysAhead} days · per day ${b.maxPerDay || 'unlimited'}`));
|
|
1835
1989
|
console.log(tr(` 확정: ${b.approval === 'manual' ? '승인 후' : '바로'}`, ` confirm: ${b.approval === 'manual' ? 'after approval' : 'instantly'}`));
|
|
1836
1990
|
if (b.blockedDates?.length) console.log(tr(` 쉬는 날: ${b.blockedDates.join(', ')}`, ` days off: ${b.blockedDates.join(', ')}`));
|
|
@@ -1848,6 +2002,7 @@ async function cmdBooking(args) {
|
|
|
1848
2002
|
if (b.allowGuests && b.maxGuests > 0) {
|
|
1849
2003
|
console.log(tr(` 손님 함께 부르기: 최대 ${b.maxGuests}명`, ` guests: up to ${b.maxGuests}`));
|
|
1850
2004
|
}
|
|
2005
|
+
if (b.trackSource) console.log(tr(' 유입 경로 기록: 켜짐', ' source tracking: on'));
|
|
1851
2006
|
if (b.redirectUrl) console.log(tr(` 잡은 뒤 보낼 곳: ${b.redirectUrl}`, ` after booking: ${b.redirectUrl}`));
|
|
1852
2007
|
if (b.allowVisitorCancel === false) console.log(tr(' 방문자 직접 취소: 꺼짐', ' visitor self-cancel: off'));
|
|
1853
2008
|
if (b.allowVisitorReschedule === false) console.log(tr(' 방문자 직접 시간 변경: 꺼짐', ' visitor reschedule: off'));
|
|
@@ -1915,6 +2070,8 @@ async function cmdBooking(args) {
|
|
|
1915
2070
|
}
|
|
1916
2071
|
if (args.duration !== undefined) body.durationMin = Number(args.duration);
|
|
1917
2072
|
if (args.buffer !== undefined) body.bufferMin = Number(args.buffer);
|
|
2073
|
+
// 구글 캘린더에 이미 있던 일정 앞뒤 여유(이동 시간 등). --buffer 는 이 표로 잡힌 예약 앞뒤.
|
|
2074
|
+
if (args['calendar-buffer'] !== undefined) body.calendarBufferMin = Number(args['calendar-buffer']);
|
|
1918
2075
|
if (args.notice !== undefined) body.minNoticeHours = Number(args.notice);
|
|
1919
2076
|
if (args.ahead !== undefined) body.maxDaysAhead = Number(args.ahead);
|
|
1920
2077
|
if (args['per-day'] !== undefined) body.maxPerDay = Number(args['per-day']);
|
|
@@ -1926,6 +2083,7 @@ async function cmdBooking(args) {
|
|
|
1926
2083
|
if (args['self-move'] !== undefined) body.allowVisitorReschedule = args['self-move'] !== 'off' && args['self-move'] !== false;
|
|
1927
2084
|
if (args['guests'] !== undefined) body.allowGuests = args['guests'] !== 'off' && args['guests'] !== false;
|
|
1928
2085
|
if (args['max-guests'] !== undefined) body.maxGuests = Number(args['max-guests']);
|
|
2086
|
+
if (args['track-source'] !== undefined) body.trackSource = args['track-source'] !== 'off' && args['track-source'] !== false;
|
|
1929
2087
|
if (args['after'] !== undefined) body.redirectUrl = String(args['after'] === true ? '' : args['after']).trim();
|
|
1930
2088
|
if (args['assign'] !== undefined) {
|
|
1931
2089
|
const a = String(args['assign']).trim().toLowerCase();
|
|
@@ -2161,6 +2319,11 @@ async function cmdDb(args) {
|
|
|
2161
2319
|
if (sub === 'rows-each') return cmdDbRowsEach(args);
|
|
2162
2320
|
if (sub === 'rows-set') return cmdDbRowsSet(args);
|
|
2163
2321
|
if (sub === 'rows-rm') return cmdDbRowsRm(args);
|
|
2322
|
+
if (sub === 'duplicates') return cmdDbDuplicates(args);
|
|
2323
|
+
if (sub === 'merge') return cmdDbMerge(args);
|
|
2324
|
+
if (sub === 'keep-duplicate') return cmdDbKeepDuplicate(args);
|
|
2325
|
+
if (sub === 'history') return cmdDbHistory(args);
|
|
2326
|
+
if (sub === 'restore') return cmdDbRestore(args);
|
|
2164
2327
|
if (sub === 'row-page') return cmdDbRowPage(args);
|
|
2165
2328
|
if (sub === 'row-page-type') return cmdDbRowPageType(args);
|
|
2166
2329
|
if (sub === 'lock') return cmdDbLock(args);
|
|
@@ -2172,7 +2335,7 @@ async function cmdDb(args) {
|
|
|
2172
2335
|
if (sub === 'view') return cmdDbView(args);
|
|
2173
2336
|
if (sub !== 'create') {
|
|
2174
2337
|
throw new Error(
|
|
2175
|
-
usage(tr(`rootr db create|rows|row-add|rows-new|row-body|rows-set|rows-each|rows-rm|row-page|row-page-type|lock|notify|buttons|button-run|automations|import-sheets|view … (자세한 건 rootr --help)`, `rootr db create|rows|row-add|rows-new|row-body|rows-set|rows-each|rows-rm|row-page|row-page-type|lock|notify|buttons|button-run|view … (see rootr --help)`)),
|
|
2338
|
+
usage(tr(`rootr db create|rows|row-add|rows-new|row-body|rows-set|rows-each|rows-rm|duplicates|merge|keep-duplicate|history|restore|row-page|row-page-type|lock|notify|buttons|button-run|automations|import-sheets|view … (자세한 건 rootr --help)`, `rootr db create|rows|row-add|rows-new|row-body|rows-set|rows-each|rows-rm|duplicates|merge|keep-duplicate|history|restore|row-page|row-page-type|lock|notify|buttons|button-run|view … (see rootr --help)`)),
|
|
2176
2339
|
);
|
|
2177
2340
|
}
|
|
2178
2341
|
const raw = typeof args.file === 'string' ? readFileSync(args.file, 'utf8') : await readStdin();
|
|
@@ -2295,7 +2458,7 @@ async function cmdDbNotify(args) {
|
|
|
2295
2458
|
*/
|
|
2296
2459
|
async function cmdDbView(args) {
|
|
2297
2460
|
const target = args._[1];
|
|
2298
|
-
if (!target) throw new Error(usage(tr(`rootr db view <path|id> [뷰이름] [--scale week|month|year] [--name 새이름] [--json]`, `rootr db view <path|id> [view name] [--scale week|month|year] [--name <new name>] [--json]`)));
|
|
2461
|
+
if (!target) throw new Error(usage(tr(`rootr db view <path|id> [뷰이름] [--scale week|month|year] [--name 새이름] [--cards "칸,칸"] [--json]`, `rootr db view <path|id> [view name] [--scale week|month|year] [--name <new name>] [--cards "field,field"] [--json]`)));
|
|
2299
2462
|
|
|
2300
2463
|
const client = makeClient();
|
|
2301
2464
|
const id = await resolveDatabaseId(client, target);
|
|
@@ -2329,8 +2492,13 @@ async function cmdDbView(args) {
|
|
|
2329
2492
|
patch.calendarScale = scale;
|
|
2330
2493
|
}
|
|
2331
2494
|
if (typeof args.name === 'string') patch.name = args.name;
|
|
2495
|
+
// CARD-PROPS — 보드 카드에 보일 칸(이름 또는 id, 쉼표로). "" 이면 기본으로.
|
|
2496
|
+
if (typeof args.cards === 'string') {
|
|
2497
|
+
const cards = args.cards.split(',').map((c) => c.trim()).filter(Boolean);
|
|
2498
|
+
patch.cardProps = cards.length ? cards : null;
|
|
2499
|
+
}
|
|
2332
2500
|
if (!Object.keys(patch).length) {
|
|
2333
|
-
throw new Error(tr('바꿀 것을 주세요: --scale week|month|year 또는 --
|
|
2501
|
+
throw new Error(tr('바꿀 것을 주세요: --scale week|month|year, --name 새이름 또는 --cards "칸,칸"', `Give something to change: --scale week|month|year, --name <new name>, or --cards "field,field"`));
|
|
2334
2502
|
}
|
|
2335
2503
|
|
|
2336
2504
|
const view = await client.updateDatabaseView(id, viewRef, patch);
|
|
@@ -2410,7 +2578,7 @@ async function cmdDbButtons(args) {
|
|
|
2410
2578
|
: st.values
|
|
2411
2579
|
? ' · ' + Object.entries(st.values).map(([k, v]) => `${k}=${v}`).join(', ')
|
|
2412
2580
|
: '';
|
|
2413
|
-
console.log(` ${i + 1}. ${what}${where}${detail}`);
|
|
2581
|
+
console.log(` ${i + 1}. ${what}${where}${detail}${matchNote(st)}`);
|
|
2414
2582
|
}
|
|
2415
2583
|
}
|
|
2416
2584
|
if (myLevel && myLevel !== 'MANAGE' && myLevel !== 'FULL') {
|
|
@@ -2448,6 +2616,9 @@ async function cmdDbButtonRun(args) {
|
|
|
2448
2616
|
if (madeCount) parts.push(tr(`행 ${madeCount}개 만듦`, `${madeCount} rows created`));
|
|
2449
2617
|
if (r.updated) parts.push(tr(`행 ${r.updated}개 고침`, `${r.updated} rows changed`));
|
|
2450
2618
|
if (r.filled) parts.push(tr(`AI로 칸 ${r.filled}개 채움`, `${r.filled} columns filled by AI`));
|
|
2619
|
+
// AUTO-MATCH — 새로 만들지 않고 있던 행에 이은 것 / 후보가 여럿이라 멈춘 것.
|
|
2620
|
+
if (r.matched?.linked) parts.push(tr(`있던 행 ${r.matched.linked}개에 이음`, `${r.matched.linked} linked to existing rows`));
|
|
2621
|
+
if (r.matched?.ambiguous) parts.push(tr(`후보 여러 곳이라 멈춤 ${r.matched.ambiguous}건(행 댓글 확인)`, `${r.matched.ambiguous} stopped — several candidates (see the row comment)`));
|
|
2451
2622
|
console.log(parts.length ? parts.join(' · ') : tr('바뀐 것이 없습니다.', `Nothing changed.`));
|
|
2452
2623
|
if (r.openRowId) console.log(tr(`열 행: ${r.openRowId}`, `Row to open: ${r.openRowId}`));
|
|
2453
2624
|
// AI가 못 채워도 버튼은 계속 돈다 — 조용히 넘어가면 사람이 왜 빈칸인지 모른다.
|
|
@@ -2458,6 +2629,8 @@ async function cmdDbButtonRun(args) {
|
|
|
2458
2629
|
`${r.aiFailed} columns the AI could not fill${r.aiError ? ` — ${r.aiError}` : ''}`,
|
|
2459
2630
|
),
|
|
2460
2631
|
);
|
|
2632
|
+
for (const n of r.notes ?? []) console.log(`· ${n}`);
|
|
2633
|
+
if (r.linkWarning) console.log(`· ${r.linkWarning}`);
|
|
2461
2634
|
}
|
|
2462
2635
|
|
|
2463
2636
|
|
|
@@ -2929,6 +3102,8 @@ async function main() {
|
|
|
2929
3102
|
return cmdRestore(args);
|
|
2930
3103
|
case 'ws':
|
|
2931
3104
|
return cmdWs(args);
|
|
3105
|
+
case 'emblem':
|
|
3106
|
+
return cmdEmblem(args);
|
|
2932
3107
|
case 'ask':
|
|
2933
3108
|
return cmdAsk(args);
|
|
2934
3109
|
case 'config':
|