rootr-cli 0.17.1 → 0.17.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 +16 -0
- package/README.md +14 -0
- package/bin/rootr.js +481 -298
- package/lib/client.js +57 -23
- package/lib/credentials.js +41 -16
- package/lib/i18n.js +40 -0
- package/lib/mcp-tools/memory.js +25 -5
- package/lib/mcp.js +23 -11
- package/lib/resolve.js +25 -9
- package/package.json +1 -1
package/bin/rootr.js
CHANGED
|
@@ -7,11 +7,12 @@ import { loadConfig, saveConfig, keyForWorkspace, maskKey, upsertKey, CONFIG_PAT
|
|
|
7
7
|
import { candidateClients, rememberWorkspace, resolveWorkspaceSwitch, surveyWorkspaces } from '../lib/credentials.js';
|
|
8
8
|
import { RootrClient, RootrApiError } from '../lib/client.js';
|
|
9
9
|
import { resolveTarget, requireResolved, resolveMoveDestination } from '../lib/resolve.js';
|
|
10
|
+
import { t as tr, lang } from '../lib/i18n.js';
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
+
const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
15
16
|
|
|
16
17
|
사용법:
|
|
17
18
|
rootr ls [path] 워크스페이스 트리 조회
|
|
@@ -98,8 +99,108 @@ const USAGE = `Rootr CLI v${pkg.version}
|
|
|
98
99
|
설정 우선순위: 환경변수(ROOTR_API_KEY, ROOTR_WORKSPACE, ROOTR_BASE_URL) > ~/.rootr/config.json
|
|
99
100
|
|
|
100
101
|
경로 인자는 '/'로 시작해야 합니다 (예: /notes/todo.md). 그 외 값은 노드 id로 취급됩니다.
|
|
102
|
+
|
|
103
|
+
출력 언어: 로케일을 따릅니다. 영어로 보려면 ROOTR_LANG=en, 한국어로 고정하려면 ROOTR_LANG=ko.
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
const USAGE_EN = `Rootr CLI v${pkg.version}
|
|
107
|
+
|
|
108
|
+
usage:
|
|
109
|
+
rootr ls [path] list the workspace tree
|
|
110
|
+
rootr read <path|id> [--json] read a document (markdown, or JSON)
|
|
111
|
+
rootr write <path|id> [--file <f>] [--if-match <etag>] replace a document (creates it if missing)
|
|
112
|
+
rootr append <path|id> [<text>] [--heading "## X"] [--file <f>] append safely to the end (or under a heading)
|
|
113
|
+
rootr edit <path|id> --find <text> --replace <text> [--all] replace text inside the body
|
|
114
|
+
rootr comments <path|id> [--block <id>] [--json] read comments (including ones left on the public page)
|
|
115
|
+
rootr search <query> search the workspace
|
|
116
|
+
rootr mv <path|id> <new path|folder id> move and/or rename a document or folder
|
|
117
|
+
rootr rm <path|id> [--yes] move to the trash (recoverable)
|
|
118
|
+
rootr restore <id> undo a delete (same as Ctrl+Z in the app — id, not path)
|
|
119
|
+
rootr attach <file> [--doc <path|id>] [--heading "## X"] [--alt <text>] upload a file (+ insert it into a document)
|
|
120
|
+
rootr ws my workspaces (id\tname; unreachable ones are marked)
|
|
121
|
+
rootr use <id|name|url> switch the active workspace (remembers its key too)
|
|
122
|
+
rootr ask <question> [--workspace <w>] GraphRAG question answering (RCA), prints citations
|
|
123
|
+
rootr db create [--file <spec.json>] [--parent <path|id>] create a DATABASE — columns in order, rows included
|
|
124
|
+
rootr db rows <path|id> [--body] [--backlinks] [--json] list rows (--body: row bodies, --backlinks: rows pointing here)
|
|
125
|
+
rootr db lock <path|id> [--schema managers] [--rows own] [--locked "Approved,Score"] view/set table locks
|
|
126
|
+
rootr db notify <path|id> [--file rules.json] [--clear] view/save notification rules
|
|
127
|
+
rootr db buttons <path|id> [--file buttons.json] [--clear] view/save buttons that do work when pressed
|
|
128
|
+
rootr db button-run <path|id> <buttonId> [--rows id,id] press a button once (create rows, fill values, AI fill)
|
|
129
|
+
rootr db view <path|id> [view name] [--scale week|month|year] [--name <new name>] list views; calendar week/month/year
|
|
130
|
+
rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] add a row (with its body)
|
|
131
|
+
rootr db row-body <path|id> <rowId> [--file <f>] write a row's body — reads stdin if no file
|
|
132
|
+
rootr db rows-new <path|id> --json '[{…},…]' | --file <file> insert many rows at once (one bad row cancels all)
|
|
133
|
+
rootr db rows-set <path|id> --ids <id,id> --values '<json>' same value on every chosen row (bulk date changes, …)
|
|
134
|
+
rootr db rows-each <path|id> --json '[{"rowId":…,"values":{…}},…]' a different value per row, in one call
|
|
135
|
+
rootr db rows-rm <path|id> --ids <id,id> [--yes] delete chosen rows at once — one blocked row rejects all
|
|
136
|
+
rootr db row-page <rowId> [--type RECORDING] give this row a typed detail page (recording, spreadsheet, signed doc…)
|
|
137
|
+
rootr db row-page <rowId> --node <path|id> attach an existing document to this row (points at it, does not move it)
|
|
138
|
+
rootr db row-page <rowId> --unlink unlink only — the document stays
|
|
139
|
+
rootr db row-page-type <path|id> <TYPE> default type for new rows — existing rows are untouched
|
|
140
|
+
rootr publish <path|id> [--children] [--view <id>] publish — works for every node type
|
|
141
|
+
rootr publish <path|id> --off stop publishing (children that came along are closed too)
|
|
142
|
+
rootr publish <path|id> --status is it public, at what URL, children included?
|
|
143
|
+
rootr publish <table> --layout board --skin paper how a published table looks (9 layouts × 6 skins)
|
|
144
|
+
rootr publish <doc> --comments [false] [--expires <ISO>] [--skin <skin>] documents only — comments, expiry, 16 skins
|
|
145
|
+
rootr readreq <path|id> [--json] read receipts — who has read it, completion rate
|
|
146
|
+
rootr readreq <path|id> --users <id,id> [--due <ISO>] [--ack] set who must read it (replaces the whole list)
|
|
147
|
+
rootr readreq <path|id> --clear clear the read requirement
|
|
148
|
+
rootr access <path|id> [--json] view visibility and permissions (needs MANAGE)
|
|
149
|
+
rootr access <path|id> --private|--public|--inherit change visibility
|
|
150
|
+
rootr access <path|id> --grant <userId> --level READ|WRITE|MANAGE grant a permission
|
|
151
|
+
rootr access <path|id> --revoke <userId> revoke a permission
|
|
152
|
+
rootr form new <path> --fields '<json>' [--collect db|sheet] create a form (18 question types: text·email·phone·url·rating·scale·time·file·matrix·ranking·csat·nps…)
|
|
153
|
+
rootr form show <path|id> [--json] read questions and settings (do this before changing them)
|
|
154
|
+
rootr form set <path|id> --fields '<json>' | --config '<json>' change questions/settings (settings merge)
|
|
155
|
+
rootr form collect <path|id> --to db|sheet create and wire a destination (a table gets a column per question)
|
|
156
|
+
rootr form responses <path|id> [--latest] [--json] read responses (--latest: newest one per person)
|
|
157
|
+
rootr form response-edit <responseId> --values '<json>' edit a submitted response (revalidated against the form)
|
|
158
|
+
rootr form response-rm <responseId> [--yes] delete one response (cannot be undone)
|
|
159
|
+
rootr sign new <path> [--file <f>] [--reviewer <userId>] [--line <line.json>] create a signed document (body + approval line)
|
|
160
|
+
rootr sign show <path|id> [--json] approval-line progress and signatures
|
|
161
|
+
rootr sign line <path|id> --file <line.json> rebuild the approval line (before submitting only)
|
|
162
|
+
signers are {"userId"} or {"email","displayName"} — an email can sign without an account
|
|
163
|
+
rootr sign code <path|id> | --bulk email a confirmation code to yourself (required before signing)
|
|
164
|
+
rootr sign submit <path|id> [--code <6 digits>] submit — the body freezes. --code is needed if you are the first signer
|
|
165
|
+
rootr sign approve <path|id> --code <6 digits> sign when it is your turn
|
|
166
|
+
rootr sign reject <path|id> --comment <reason> [--code <6 digits>] reject (reason required)
|
|
167
|
+
rootr sign reopen <path|id> reopen a draft nobody has signed yet
|
|
168
|
+
rootr sign revise <path|id> --note <reason> issue a revision (the original stays)
|
|
169
|
+
rootr sign verify <path|id> [--json] prove it is untampered (signatures, hashes, timestamps)
|
|
170
|
+
rootr sign list [--pending] [--status <S>] [--mode <M>] [--json] list signed documents (--pending: waiting on you)
|
|
171
|
+
rootr sign bulk <id…> --code <6 digits> submit+sign many at once (one code, drafts included)
|
|
172
|
+
rootr sign ledger [--json] verify the whole workspace ledger (hash chain)
|
|
173
|
+
rootr config [--api-key <k>] [--workspace <w>] [--base-url <u>] read/save configuration
|
|
174
|
+
rootr config --add-key <k> [--label <name>] [--workspace <w>] register an extra per-workspace key
|
|
175
|
+
rootr feedback "<what is not working>" [--kind bug|friction|idea|praise] [--email <address>]
|
|
176
|
+
send feedback — no login, no key, and it never hits usage limits
|
|
177
|
+
rootr scaffold plan "<what the workspace is for>" build step 1 — what to ask, and the design rules
|
|
178
|
+
rootr scaffold review --file tree.json [--json] build step 2 — get graded before anything is created
|
|
179
|
+
rootr scaffold apply --file tree.json [--root /path] build step 3 — actually create it (grades come along)
|
|
180
|
+
rootr scaffold built [--root /path] [--json] after building — inspect the **shape** of what exists
|
|
181
|
+
rootr scaffold test [--root /path] [--json] after building — actually **exercise** it (relations, rollups, formulas, forms, sheets). Exits 1 on failure
|
|
182
|
+
rootr memory [--query <q>] [--kind <k>] [--json] recall team memory — the same list AI agents read
|
|
183
|
+
rootr memory add "<content>" [--kind instruction|person|project|reference] [--source <path|id>] [--related a,b] save a memory
|
|
184
|
+
rootr memory edit <id> [--content "<content>"] [--kind …] [--source …] [--related a,b] edit a memory (only the fields you pass)
|
|
185
|
+
rootr memory forget <id> [--hard] retire a memory (--hard deletes the row)
|
|
186
|
+
rootr mcp run the MCP stdio server (167 tools — see the README)
|
|
187
|
+
rootr --help this help
|
|
188
|
+
rootr --version print the version
|
|
189
|
+
|
|
190
|
+
Configuration precedence: env vars (ROOTR_API_KEY, ROOTR_WORKSPACE, ROOTR_BASE_URL) > ~/.rootr/config.json
|
|
191
|
+
|
|
192
|
+
Path arguments must start with '/' (e.g. /notes/todo.md). Anything else is treated as a node id.
|
|
193
|
+
|
|
194
|
+
Language: this help follows your locale. Set ROOTR_LANG=ko for Korean, ROOTR_LANG=en for English.
|
|
101
195
|
`;
|
|
102
196
|
|
|
197
|
+
const USAGE = lang() === 'ko' ? USAGE_KO : USAGE_EN;
|
|
198
|
+
|
|
199
|
+
/** 앞머리만 언어를 타고, 뒤의 명령 문법은 양쪽이 같다. */
|
|
200
|
+
function usage(syntax) {
|
|
201
|
+
return tr('\uC0AC\uC6A9\uBC95: ' + syntax, 'usage: ' + syntax);
|
|
202
|
+
}
|
|
203
|
+
|
|
103
204
|
function parseArgs(argv) {
|
|
104
205
|
const args = { _: [] };
|
|
105
206
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -144,11 +245,11 @@ function makeClient() {
|
|
|
144
245
|
|
|
145
246
|
function printErrorAndExit(err) {
|
|
146
247
|
if (err instanceof RootrApiError) {
|
|
147
|
-
console.error(`오류: ${err.message}`);
|
|
248
|
+
console.error(tr(`오류: ${err.message}`, `error: ${err.message}`));
|
|
148
249
|
if (err.status) console.error(`(HTTP ${err.status})`);
|
|
149
|
-
if (err.hint) console.error(`힌트: ${err.hint}`);
|
|
250
|
+
if (err.hint) console.error(tr(`힌트: ${err.hint}`, `hint: ${err.hint}`));
|
|
150
251
|
} else {
|
|
151
|
-
console.error(`오류: ${err.message || err}`);
|
|
252
|
+
console.error(tr(`오류: ${err.message || err}`, `error: ${err.message || err}`));
|
|
152
253
|
}
|
|
153
254
|
process.exit(1);
|
|
154
255
|
}
|
|
@@ -183,7 +284,7 @@ async function cmdLs(args) {
|
|
|
183
284
|
*/
|
|
184
285
|
async function cmdComments(args) {
|
|
185
286
|
const target = args._[0];
|
|
186
|
-
if (!target) throw new Error(
|
|
287
|
+
if (!target) throw new Error(usage(`rootr comments <path|id> [--block <blockId>] [--json]`));
|
|
187
288
|
|
|
188
289
|
const client = makeClient();
|
|
189
290
|
const id = requireResolved(await resolveTarget(client, target));
|
|
@@ -194,16 +295,16 @@ async function cmdComments(args) {
|
|
|
194
295
|
return;
|
|
195
296
|
}
|
|
196
297
|
if (!threads.length) {
|
|
197
|
-
console.log('댓글이 없습니다.');
|
|
298
|
+
console.log(tr('댓글이 없습니다.', `No comments.`));
|
|
198
299
|
return;
|
|
199
300
|
}
|
|
200
301
|
for (const t of threads) {
|
|
201
302
|
const when = String(t.createdAt ?? '').slice(0, 16).replace('T', ' ');
|
|
202
|
-
console.log(`- ${t.author?.name ?? '(알 수 없음)'} · ${when}`);
|
|
303
|
+
console.log(tr(`- ${t.author?.name ?? '(알 수 없음)'} · ${when}`, `- ${t.author?.name ?? '(unknown)'} · ${when}`));
|
|
203
304
|
console.log(` ${String(t.body ?? '').replace(/\n/g, '\n ')}`);
|
|
204
305
|
for (const r of t.replies ?? []) {
|
|
205
306
|
const rWhen = String(r.createdAt ?? '').slice(0, 16).replace('T', ' ');
|
|
206
|
-
console.log(` ↳ ${r.author?.name ?? '(알 수 없음)'} · ${rWhen}`);
|
|
307
|
+
console.log(tr(` ↳ ${r.author?.name ?? '(알 수 없음)'} · ${rWhen}`, ` ↳ ${r.author?.name ?? '(unknown)'} · ${rWhen}`));
|
|
207
308
|
console.log(` ${String(r.body ?? '').replace(/\n/g, '\n ')}`);
|
|
208
309
|
}
|
|
209
310
|
}
|
|
@@ -211,7 +312,7 @@ async function cmdComments(args) {
|
|
|
211
312
|
|
|
212
313
|
async function cmdRead(args) {
|
|
213
314
|
const target = args._[0];
|
|
214
|
-
if (!target) throw new Error(
|
|
315
|
+
if (!target) throw new Error(usage(`rootr read <path|id> [--json]`));
|
|
215
316
|
|
|
216
317
|
const client = makeClient();
|
|
217
318
|
const resolved = await resolveTarget(client, target);
|
|
@@ -228,7 +329,7 @@ async function cmdRead(args) {
|
|
|
228
329
|
|
|
229
330
|
async function cmdWrite(args) {
|
|
230
331
|
const target = args._[0];
|
|
231
|
-
if (!target) throw new Error(
|
|
332
|
+
if (!target) throw new Error(usage(`rootr write <path|id> [--file <f>] [--if-match <etag>]`));
|
|
232
333
|
|
|
233
334
|
let content;
|
|
234
335
|
if (typeof args.file === 'string') {
|
|
@@ -242,7 +343,7 @@ async function cmdWrite(args) {
|
|
|
242
343
|
|
|
243
344
|
if (!resolved.existed) {
|
|
244
345
|
if (!resolved.path) {
|
|
245
|
-
throw new Error(`대상을 찾을 수 없습니다: ${target}`);
|
|
346
|
+
throw new Error(tr(`대상을 찾을 수 없습니다: ${target}`, `Target not found: ${target}`));
|
|
246
347
|
}
|
|
247
348
|
// 생성 자체가 쓰기 — 곧바로 PUT을 또 하면 버전/추출만 이중으로 쌓인다
|
|
248
349
|
const created = await client.createNode({
|
|
@@ -263,7 +364,7 @@ async function cmdWrite(args) {
|
|
|
263
364
|
|
|
264
365
|
async function cmdAppend(args) {
|
|
265
366
|
const target = args._[0];
|
|
266
|
-
if (!target) throw new Error(
|
|
367
|
+
if (!target) throw new Error(usage(`rootr append <path|id> [<text>] [--heading "## X"] [--file <f>]`));
|
|
267
368
|
|
|
268
369
|
let content;
|
|
269
370
|
if (typeof args._[1] === 'string') {
|
|
@@ -275,7 +376,7 @@ async function cmdAppend(args) {
|
|
|
275
376
|
}
|
|
276
377
|
|
|
277
378
|
if (!content) {
|
|
278
|
-
throw new Error('추가할 내용이 없습니다 (텍스트 인자, --file, 또는 stdin 필요).');
|
|
379
|
+
throw new Error(tr('추가할 내용이 없습니다 (텍스트 인자, --file, 또는 stdin 필요).', `Nothing to append (needs a text argument, --file, or stdin).`));
|
|
279
380
|
}
|
|
280
381
|
|
|
281
382
|
const client = makeClient();
|
|
@@ -292,9 +393,9 @@ async function cmdAppend(args) {
|
|
|
292
393
|
|
|
293
394
|
async function cmdEdit(args) {
|
|
294
395
|
const target = args._[0];
|
|
295
|
-
if (!target) throw new Error(
|
|
396
|
+
if (!target) throw new Error(usage(`rootr edit <path|id> --find <text> --replace <text> [--all]`));
|
|
296
397
|
if (typeof args.find !== 'string' || typeof args.replace !== 'string') {
|
|
297
|
-
throw new Error('--find와 --replace가 모두 필요합니다.');
|
|
398
|
+
throw new Error(tr('--find와 --replace가 모두 필요합니다.', `Both --find and --replace are required.`));
|
|
298
399
|
}
|
|
299
400
|
|
|
300
401
|
const client = makeClient();
|
|
@@ -312,7 +413,7 @@ async function cmdEdit(args) {
|
|
|
312
413
|
|
|
313
414
|
async function cmdSearch(args) {
|
|
314
415
|
const query = args._[0];
|
|
315
|
-
if (!query) throw new Error(
|
|
416
|
+
if (!query) throw new Error(usage(`rootr search <query>`));
|
|
316
417
|
|
|
317
418
|
const client = makeClient();
|
|
318
419
|
const results = await client.search(query);
|
|
@@ -325,7 +426,7 @@ async function cmdSearch(args) {
|
|
|
325
426
|
async function cmdMv(args) {
|
|
326
427
|
const source = args._[0];
|
|
327
428
|
const dest = args._[1];
|
|
328
|
-
if (!source || !dest) throw new Error(
|
|
429
|
+
if (!source || !dest) throw new Error(usage(tr(`rootr mv <path|id> <새 경로|폴더 id>`, `rootr mv <path|id> <new path|folder id>`)));
|
|
329
430
|
|
|
330
431
|
const client = makeClient();
|
|
331
432
|
const resolved = await resolveTarget(client, source);
|
|
@@ -334,12 +435,12 @@ async function cmdMv(args) {
|
|
|
334
435
|
const { parentId, name } = await resolveMoveDestination(client, dest);
|
|
335
436
|
const updated = await client.updateNode(id, { parentId, name });
|
|
336
437
|
|
|
337
|
-
console.log(`이동했습니다: ${resolved.path || id} → ${updated.path || dest}`);
|
|
438
|
+
console.log(tr(`이동했습니다: ${resolved.path || id} → ${updated.path || dest}`, `Moved: ${resolved.path || id} → ${updated.path || dest}`));
|
|
338
439
|
}
|
|
339
440
|
|
|
340
441
|
async function cmdRm(args) {
|
|
341
442
|
const target = args._[0];
|
|
342
|
-
if (!target) throw new Error(
|
|
443
|
+
if (!target) throw new Error(usage(`rootr rm <path|id> [--yes]`));
|
|
343
444
|
|
|
344
445
|
const client = makeClient();
|
|
345
446
|
const resolved = await resolveTarget(client, target);
|
|
@@ -349,17 +450,17 @@ async function cmdRm(args) {
|
|
|
349
450
|
// teammate's tree — confirm unless the caller opted out for scripting.
|
|
350
451
|
if (!args.yes) {
|
|
351
452
|
const label = resolved.path || resolved.name || id;
|
|
352
|
-
const ok = await confirmPrompt(`"${label}" 을(를) 휴지통으로 옮깁니다. 계속할까요? [y/N] `);
|
|
453
|
+
const ok = await confirmPrompt(tr(`"${label}" 을(를) 휴지통으로 옮깁니다. 계속할까요? [y/N] `, `Move "${label}" to the trash. Continue? [y/N] `));
|
|
353
454
|
if (!ok) {
|
|
354
|
-
console.log('취소했습니다.');
|
|
455
|
+
console.log(tr('취소했습니다.', `Cancelled.`));
|
|
355
456
|
return;
|
|
356
457
|
}
|
|
357
458
|
}
|
|
358
459
|
|
|
359
460
|
await client.deleteNode(id);
|
|
360
|
-
console.log(`휴지통으로 옮겼습니다: ${resolved.path || id}`);
|
|
461
|
+
console.log(tr(`휴지통으로 옮겼습니다: ${resolved.path || id}`, `Moved to the trash: ${resolved.path || id}`));
|
|
361
462
|
// 되돌리려면 id 가 있어야 한다 — 휴지통에 든 것은 경로로는 못 찾는다.
|
|
362
|
-
console.log(`되돌리려면: rootr restore ${id} (30일 뒤 자동 삭제)`);
|
|
463
|
+
console.log(tr(`되돌리려면: rootr restore ${id} (30일 뒤 자동 삭제)`, `To undo: rootr restore ${id} (deleted for good after 30 days)`));
|
|
363
464
|
}
|
|
364
465
|
|
|
365
466
|
/**
|
|
@@ -373,8 +474,8 @@ async function cmdRestore(args) {
|
|
|
373
474
|
const id = args._[0];
|
|
374
475
|
if (!id) {
|
|
375
476
|
throw new Error(
|
|
376
|
-
|
|
377
|
-
' (경로가 아니라 id 입니다 — 휴지통에 든 것은 경로로 못 찾습니다. rootr rm 이 찍어 준 id 를 쓰세요.)',
|
|
477
|
+
usage(`rootr restore <id>\n`) +
|
|
478
|
+
tr(' (경로가 아니라 id 입니다 — 휴지통에 든 것은 경로로 못 찾습니다. rootr rm 이 찍어 준 id 를 쓰세요.)', ` (that is an id, not a path — items in the trash cannot be found by path. Use the id rootr rm printed.)`),
|
|
378
479
|
);
|
|
379
480
|
}
|
|
380
481
|
const client = makeClient();
|
|
@@ -383,7 +484,7 @@ async function cmdRestore(args) {
|
|
|
383
484
|
console.log(JSON.stringify(node, null, 2));
|
|
384
485
|
return;
|
|
385
486
|
}
|
|
386
|
-
console.log(`되돌렸습니다: ${node.path ?? node.name ?? id}`);
|
|
487
|
+
console.log(tr(`되돌렸습니다: ${node.path ?? node.name ?? id}`, `Restored: ${node.path ?? node.name ?? id}`));
|
|
387
488
|
}
|
|
388
489
|
|
|
389
490
|
/** Minimal y/N prompt. Non-interactive stdin (pipes, CI) answers "no". */
|
|
@@ -411,13 +512,13 @@ const MIME_BY_EXT = {
|
|
|
411
512
|
|
|
412
513
|
async function cmdAttach(args) {
|
|
413
514
|
const filePath = args._[0];
|
|
414
|
-
if (!filePath) throw new Error(
|
|
515
|
+
if (!filePath) throw new Error(usage(`rootr attach <file> [--doc <path|id>] [--heading "## X"] [--alt <text>]`));
|
|
415
516
|
|
|
416
517
|
const cfg = loadConfig();
|
|
417
518
|
const workspace = args.workspace || cfg.workspace;
|
|
418
519
|
if (!workspace) {
|
|
419
|
-
throw new RootrApiError('워크스페이스가 설정되지 않았습니다.', {
|
|
420
|
-
hint: '--workspace <id>로 넘기거나 rootr config --workspace <id> 또는 ROOTR_WORKSPACE 환경변수를 지정하세요.',
|
|
520
|
+
throw new RootrApiError(tr('워크스페이스가 설정되지 않았습니다.', `No workspace is configured.`), {
|
|
521
|
+
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.`),
|
|
421
522
|
});
|
|
422
523
|
}
|
|
423
524
|
|
|
@@ -449,8 +550,8 @@ async function cmdWs() {
|
|
|
449
550
|
const cfg = loadConfig();
|
|
450
551
|
const candidates = candidateClients(cfg);
|
|
451
552
|
if (!candidates.length) {
|
|
452
|
-
throw new RootrApiError('이 컴퓨터에 등록된 Rootr 키가 없습니다.', {
|
|
453
|
-
hint: 'rootr.io > 설정 > API 키에서 계정 키(PAT)를 발급해 `rootr config --api-key <키>` 로 등록하세요.',
|
|
553
|
+
throw new RootrApiError(tr('이 컴퓨터에 등록된 Rootr 키가 없습니다.', `No Rootr key is registered on this machine.`), {
|
|
554
|
+
hint: tr('rootr.io > 설정 > API 키에서 계정 키(PAT)를 발급해 `rootr config --api-key <키>` 로 등록하세요.', 'Create an account key (PAT) under rootr.io > Settings > API keys and register it with `rootr config --api-key <key>`.'),
|
|
454
555
|
});
|
|
455
556
|
}
|
|
456
557
|
// Probing costs an API call per workspace, so only when it diagnoses something:
|
|
@@ -459,8 +560,8 @@ async function cmdWs() {
|
|
|
459
560
|
const { workspaces, reachable, failures } = await surveyWorkspaces(candidates, { probe });
|
|
460
561
|
if (!workspaces.length) {
|
|
461
562
|
throw new RootrApiError(
|
|
462
|
-
failures ? '등록된 키로 워크스페이스 목록을 조회하지 못했습니다 (키 만료/취소 가능성).' : '보이는 워크스페이스가 없습니다.',
|
|
463
|
-
{ hint: 'rootr.io > 설정 > API 키에서 계정 키(PAT)를 발급해 `rootr config --api-key <키>` 로 등록하세요.' },
|
|
563
|
+
failures ? tr('등록된 키로 워크스페이스 목록을 조회하지 못했습니다 (키 만료/취소 가능성).', `Could not list workspaces with the registered keys (they may be expired or revoked).`) : tr('보이는 워크스페이스가 없습니다.', `No workspaces are visible.`),
|
|
564
|
+
{ hint: tr('rootr.io > 설정 > API 키에서 계정 키(PAT)를 발급해 `rootr config --api-key <키>` 로 등록하세요.', 'Create an account key (PAT) under rootr.io > Settings > API keys and register it with `rootr config --api-key <key>`.') },
|
|
464
565
|
);
|
|
465
566
|
}
|
|
466
567
|
let blocked = false;
|
|
@@ -468,42 +569,45 @@ async function cmdWs() {
|
|
|
468
569
|
const unreachable = probe && !reachable.has(w.id);
|
|
469
570
|
blocked = blocked || unreachable;
|
|
470
571
|
console.log(
|
|
471
|
-
`${w.id === cfg.workspace ? '* ' : ' '}${w.id}\t${w.name || w.slug || ''}${unreachable ? '\t[등록된 키로는 접근 불가]' : ''}`,
|
|
572
|
+
tr(`${w.id === cfg.workspace ? '* ' : ' '}${w.id}\t${w.name || w.slug || ''}${unreachable ? '\t[등록된 키로는 접근 불가]' : ''}`, `${w.id === cfg.workspace ? '* ' : ' '}${w.id}\t${w.name || w.slug || ''}${unreachable ? '\t[not reachable with the registered keys]' : ''}`),
|
|
472
573
|
);
|
|
473
574
|
}
|
|
474
575
|
if (blocked) {
|
|
475
576
|
console.error(
|
|
476
|
-
'\n"접근 불가" = 내 소속 워크스페이스지만 등록된 키가 워크스페이스 전용이라 못 엽니다.\n' +
|
|
477
|
-
|
|
577
|
+
tr('\n"접근 불가" = 내 소속 워크스페이스지만 등록된 키가 워크스페이스 전용이라 못 엽니다.\n', `\n"not reachable" = a workspace you belong to, but the registered key is scoped to another one.\n`) +
|
|
578
|
+
tr(
|
|
579
|
+
'계정 키(PAT)를 등록하면 전부 열립니다: rootr.io > 설정 > API 키 → `rootr config --api-key <키>`',
|
|
580
|
+
'Register an account key (PAT) and they all open: rootr.io > Settings > API keys → `rootr config --api-key <key>`',
|
|
581
|
+
),
|
|
478
582
|
);
|
|
479
583
|
}
|
|
480
584
|
}
|
|
481
585
|
|
|
482
586
|
async function cmdUse(args) {
|
|
483
587
|
const wanted = args._.join(' ').trim();
|
|
484
|
-
if (!wanted) throw new Error(
|
|
588
|
+
if (!wanted) throw new Error(usage(tr(`rootr use <워크스페이스 id | 이름 | rootr.io 주소>`, `rootr use <workspace id | name | rootr.io URL>`)));
|
|
485
589
|
const cfg = loadConfig();
|
|
486
590
|
const match = await resolveWorkspaceSwitch(wanted, cfg);
|
|
487
591
|
if (match.error) throw new RootrApiError(match.error);
|
|
488
592
|
rememberWorkspace(match, cfg);
|
|
489
|
-
console.log(`활성 워크스페이스: "${match.workspace.name || match.workspace.slug || ''}" (${match.workspace.id})`);
|
|
593
|
+
console.log(tr(`활성 워크스페이스: "${match.workspace.name || match.workspace.slug || ''}" (${match.workspace.id})`, `Active workspace: "${match.workspace.name || match.workspace.slug || ''}" (${match.workspace.id})`));
|
|
490
594
|
}
|
|
491
595
|
|
|
492
596
|
async function cmdAsk(args) {
|
|
493
597
|
const question = args._.join(' ');
|
|
494
|
-
if (!question) throw new Error(
|
|
598
|
+
if (!question) throw new Error(usage(`rootr ask <question> [--workspace <w>]`));
|
|
495
599
|
|
|
496
600
|
const cfg = loadConfig();
|
|
497
601
|
const workspace = args.workspace || cfg.workspace;
|
|
498
602
|
if (!workspace) {
|
|
499
|
-
throw new RootrApiError('워크스페이스가 설정되지 않았습니다.', {
|
|
500
|
-
hint: '--workspace <id>로 넘기거나 rootr config --workspace <id> 또는 ROOTR_WORKSPACE 환경변수를 지정하세요.',
|
|
603
|
+
throw new RootrApiError(tr('워크스페이스가 설정되지 않았습니다.', `No workspace is configured.`), {
|
|
604
|
+
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.`),
|
|
501
605
|
});
|
|
502
606
|
}
|
|
503
607
|
|
|
504
608
|
const client = new RootrClient({ ...cfg, workspace });
|
|
505
609
|
const result = await client.ask(workspace, question);
|
|
506
|
-
console.log(result.text || '(응답 없음)');
|
|
610
|
+
console.log(result.text || tr('(응답 없음)', `(no response)`));
|
|
507
611
|
const citations = Array.isArray(result.citations) ? result.citations : [];
|
|
508
612
|
if (citations.length) {
|
|
509
613
|
console.log('\nCitations:');
|
|
@@ -525,7 +629,7 @@ async function cmdFeedback(args) {
|
|
|
525
629
|
if (!body) body = (await readStdin()).trim();
|
|
526
630
|
if (!body) {
|
|
527
631
|
throw new Error(
|
|
528
|
-
|
|
632
|
+
usage(tr(`rootr feedback "무엇을 하려다 무엇이 일어났는지" [--kind bug|friction|idea|praise]\n`, `rootr feedback "what you tried and what happened" [--kind bug|friction|idea|praise]\n`)) +
|
|
529
633
|
' rootr feedback --file report.md / cat log | rootr feedback --kind bug',
|
|
530
634
|
);
|
|
531
635
|
}
|
|
@@ -534,7 +638,7 @@ async function cmdFeedback(args) {
|
|
|
534
638
|
if (args.kind) {
|
|
535
639
|
kind = KIND_MAP[String(args.kind).toLowerCase()];
|
|
536
640
|
if (!kind) {
|
|
537
|
-
throw new Error(`--kind 는 ${Object.keys(KIND_MAP).join('|')} 중
|
|
641
|
+
throw new Error(tr(`--kind 는 ${Object.keys(KIND_MAP).join('|')} 중 하나입니다.`, `--kind must be one of ${Object.keys(KIND_MAP).join('|')}.`));
|
|
538
642
|
}
|
|
539
643
|
}
|
|
540
644
|
|
|
@@ -551,12 +655,12 @@ async function cmdFeedback(args) {
|
|
|
551
655
|
client: 'cli',
|
|
552
656
|
});
|
|
553
657
|
if (result.deduped) {
|
|
554
|
-
console.log(`이미 같은 제보가 접수돼 있어 합쳤습니다. (id: ${result.id})`);
|
|
658
|
+
console.log(tr(`이미 같은 제보가 접수돼 있어 합쳤습니다. (id: ${result.id})`, `The same report already exists, so it was merged. (id: ${result.id})`));
|
|
555
659
|
return;
|
|
556
660
|
}
|
|
557
|
-
console.log(`제보를 보냈습니다. 고맙습니다. (id: ${result.id})`);
|
|
661
|
+
console.log(tr(`제보를 보냈습니다. 고맙습니다. (id: ${result.id})`, `Feedback sent. Thank you. (id: ${result.id})`));
|
|
558
662
|
if (!args.email) {
|
|
559
|
-
console.log('답을 받고 싶으면 --email <주소>를 함께 주세요.');
|
|
663
|
+
console.log(tr('답을 받고 싶으면 --email <주소>를 함께 주세요.', `Pass --email <address> if you want a reply.`));
|
|
560
664
|
}
|
|
561
665
|
}
|
|
562
666
|
|
|
@@ -570,7 +674,7 @@ async function cmdConfig(args) {
|
|
|
570
674
|
workspace: typeof args.workspace === 'string' ? args.workspace : undefined,
|
|
571
675
|
label: typeof args.label === 'string' ? args.label : undefined,
|
|
572
676
|
});
|
|
573
|
-
console.log(`키를 등록했습니다 (총 ${keys.length}개): ${CONFIG_PATH}`);
|
|
677
|
+
console.log(tr(`키를 등록했습니다 (총 ${keys.length}개): ${CONFIG_PATH}`, `Key registered (${keys.length} total): ${CONFIG_PATH}`));
|
|
574
678
|
return;
|
|
575
679
|
}
|
|
576
680
|
|
|
@@ -593,7 +697,7 @@ async function cmdConfig(args) {
|
|
|
593
697
|
if (typeof args['base-url'] === 'string') partial.baseUrl = args['base-url'];
|
|
594
698
|
|
|
595
699
|
saveConfig(partial);
|
|
596
|
-
console.log(`설정을 저장했습니다: ${CONFIG_PATH}`);
|
|
700
|
+
console.log(tr(`설정을 저장했습니다: ${CONFIG_PATH}`, `Configuration saved: ${CONFIG_PATH}`));
|
|
597
701
|
}
|
|
598
702
|
|
|
599
703
|
async function cmdMcp() {
|
|
@@ -608,6 +712,28 @@ async function cmdMcp() {
|
|
|
608
712
|
|
|
609
713
|
const MEMORY_KINDS = ['지시', '사람', '프로젝트', '참고'];
|
|
610
714
|
|
|
715
|
+
/**
|
|
716
|
+
* 저장되는 값은 한국어 네 가지로 고정이다 — 화면·MCP·이미 쌓인 행이 전부 그 값을 쓴다.
|
|
717
|
+
* 영어로 치는 사람을 위해 입력만 받아 준다(출력·저장은 그대로).
|
|
718
|
+
*/
|
|
719
|
+
const MEMORY_KIND_ALIASES = {
|
|
720
|
+
instruction: '지시',
|
|
721
|
+
person: '사람',
|
|
722
|
+
people: '사람',
|
|
723
|
+
project: '프로젝트',
|
|
724
|
+
reference: '참고',
|
|
725
|
+
ref: '참고',
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
function normalizeMemoryKind(value) {
|
|
729
|
+
if (!value || typeof value !== 'string') return value;
|
|
730
|
+
return MEMORY_KIND_ALIASES[value.trim().toLowerCase()] ?? value;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function memoryKindChoices() {
|
|
734
|
+
return tr(MEMORY_KINDS.join('/'), Object.keys(MEMORY_KIND_ALIASES).filter((k) => k !== 'people' && k !== 'ref').join('/'));
|
|
735
|
+
}
|
|
736
|
+
|
|
611
737
|
function parseRelated(val) {
|
|
612
738
|
if (!val) return undefined;
|
|
613
739
|
if (Array.isArray(val)) return val;
|
|
@@ -628,7 +754,7 @@ async function cmdMemory(args) {
|
|
|
628
754
|
return;
|
|
629
755
|
}
|
|
630
756
|
if (!result.memories.length) {
|
|
631
|
-
console.log('아직 기억이 없습니다. rootr memory add "한 줄 사실" 으로 남겨 보세요.');
|
|
757
|
+
console.log(tr('아직 기억이 없습니다. rootr memory add "한 줄 사실" 으로 남겨 보세요.', `No memories yet. Try: rootr memory add "a one-line fact"`));
|
|
632
758
|
return;
|
|
633
759
|
}
|
|
634
760
|
for (const m of result.memories) {
|
|
@@ -641,9 +767,15 @@ async function cmdMemory(args) {
|
|
|
641
767
|
|
|
642
768
|
if (sub === 'add') {
|
|
643
769
|
const content = args._.join(' ');
|
|
644
|
-
if (!content) throw new Error(
|
|
770
|
+
if (!content) throw new Error(usage(tr(`rootr memory add "<내용>" [--kind 지시|사람|프로젝트|참고] [--source <경로|id>] [--related a,b]`, `rootr memory add "<content>" [--kind instruction|person|project|reference] [--source <path|id>] [--related a,b]`)));
|
|
771
|
+
args.kind = normalizeMemoryKind(args.kind);
|
|
645
772
|
if (args.kind && !MEMORY_KINDS.includes(args.kind)) {
|
|
646
|
-
throw new Error(
|
|
773
|
+
throw new Error(
|
|
774
|
+
tr(
|
|
775
|
+
tr(`종류는 ${memoryKindChoices()} 중 하나여야 합니다: ${args.kind}`, `--kind must be one of ${memoryKindChoices()}: ${args.kind}`),
|
|
776
|
+
`--kind must be one of ${memoryKindChoices()}: ${args.kind}`,
|
|
777
|
+
),
|
|
778
|
+
);
|
|
647
779
|
}
|
|
648
780
|
const result = await client.remember({
|
|
649
781
|
content,
|
|
@@ -651,20 +783,26 @@ async function cmdMemory(args) {
|
|
|
651
783
|
source: args.source,
|
|
652
784
|
related: parseRelated(args.related),
|
|
653
785
|
});
|
|
654
|
-
console.log(`${result.replaced ? '기억을 갈아끼웠습니다' : '기억했습니다'}: ${result.memory.content}`);
|
|
655
|
-
console.log(`id: ${result.memory.id} · 상태: ${result.memory.status}`);
|
|
786
|
+
console.log(tr(`${result.replaced ? '기억을 갈아끼웠습니다' : '기억했습니다'}: ${result.memory.content}`, `${result.replaced ? 'Memory replaced' : 'Remembered'}: ${result.memory.content}`));
|
|
787
|
+
console.log(tr(`id: ${result.memory.id} · 상태: ${result.memory.status}`, `id: ${result.memory.id} · status: ${result.memory.status}`));
|
|
656
788
|
return;
|
|
657
789
|
}
|
|
658
790
|
|
|
659
791
|
if (sub === 'edit') {
|
|
660
792
|
const id = args._[0];
|
|
661
|
-
if (!id) throw new Error(
|
|
793
|
+
if (!id) throw new Error(usage(tr(`rootr memory edit <id> [--content "<내용>"] [--kind …] [--source …] [--related a,b]`, `rootr memory edit <id> [--content "<content>"] [--kind …] [--source …] [--related a,b]`)));
|
|
662
794
|
const content = args._.slice(1).join(' ') || args.content;
|
|
663
795
|
if (!content && !args.kind && !args.source && !args.related) {
|
|
664
|
-
throw new Error('고칠 칸이 하나도 없습니다 — --content/--kind/--source/--related 중 하나는 주어야 합니다.');
|
|
796
|
+
throw new Error(tr('고칠 칸이 하나도 없습니다 — --content/--kind/--source/--related 중 하나는 주어야 합니다.', `Nothing to change — pass at least one of --content/--kind/--source/--related.`));
|
|
665
797
|
}
|
|
798
|
+
args.kind = normalizeMemoryKind(args.kind);
|
|
666
799
|
if (args.kind && !MEMORY_KINDS.includes(args.kind)) {
|
|
667
|
-
throw new Error(
|
|
800
|
+
throw new Error(
|
|
801
|
+
tr(
|
|
802
|
+
tr(`종류는 ${memoryKindChoices()} 중 하나여야 합니다: ${args.kind}`, `--kind must be one of ${memoryKindChoices()}: ${args.kind}`),
|
|
803
|
+
`--kind must be one of ${memoryKindChoices()}: ${args.kind}`,
|
|
804
|
+
),
|
|
805
|
+
);
|
|
668
806
|
}
|
|
669
807
|
const result = await client.revise(id, {
|
|
670
808
|
...(content ? { content } : {}),
|
|
@@ -672,20 +810,25 @@ async function cmdMemory(args) {
|
|
|
672
810
|
...(args.source !== undefined ? { source: args.source } : {}),
|
|
673
811
|
...(args.related !== undefined ? { related: parseRelated(args.related) } : {}),
|
|
674
812
|
});
|
|
675
|
-
console.log(`고쳤습니다: ${result.content}`);
|
|
676
|
-
console.log(
|
|
813
|
+
console.log(tr(`고쳤습니다: ${result.content}`, `Updated: ${result.content}`));
|
|
814
|
+
console.log(
|
|
815
|
+
tr(
|
|
816
|
+
`상태: ${result.status}${result.mergedIds?.length ? ` · 같은 사실 ${result.mergedIds.length}건을 폐기로 내렸습니다` : ''}`,
|
|
817
|
+
`status: ${result.status}${result.mergedIds?.length ? ` · retired ${result.mergedIds.length} duplicate facts` : ''}`,
|
|
818
|
+
),
|
|
819
|
+
);
|
|
677
820
|
return;
|
|
678
821
|
}
|
|
679
822
|
|
|
680
823
|
if (sub === 'forget') {
|
|
681
824
|
const id = args._[0];
|
|
682
|
-
if (!id) throw new Error(
|
|
825
|
+
if (!id) throw new Error(usage(`rootr memory forget <id> [--hard]`));
|
|
683
826
|
await client.forget(id, { hard: args.hard === true });
|
|
684
|
-
console.log(args.hard ? '기억을 지웠습니다.' : '기억을 폐기 상태로 내렸습니다.');
|
|
827
|
+
console.log(args.hard ? tr('기억을 지웠습니다.', `Memory deleted.`) : tr('기억을 폐기 상태로 내렸습니다.', `Memory retired.`));
|
|
685
828
|
return;
|
|
686
829
|
}
|
|
687
830
|
|
|
688
|
-
throw new Error(`알 수 없는 하위명령: ${sub}\n사용법: rootr memory [ls|add|edit|forget] …`);
|
|
831
|
+
throw new Error(tr(`알 수 없는 하위명령: ${sub}\n사용법: rootr memory [ls|add|edit|forget] …`, `Unknown subcommand: ${sub}\nusage: rootr memory [ls|add|edit|forget] …`));
|
|
689
832
|
}
|
|
690
833
|
|
|
691
834
|
// ---- entry ------------------------------------------------------------
|
|
@@ -708,7 +851,7 @@ async function cmdAccess(args) {
|
|
|
708
851
|
const target = args._[0];
|
|
709
852
|
if (!target) {
|
|
710
853
|
throw new Error(
|
|
711
|
-
|
|
854
|
+
usage(`rootr access <path|id> [--private|--public|--inherit] [--grant <userId> --level READ|WRITE|MANAGE] [--revoke <userId>]`),
|
|
712
855
|
);
|
|
713
856
|
}
|
|
714
857
|
const client = makeClient();
|
|
@@ -720,22 +863,22 @@ async function cmdAccess(args) {
|
|
|
720
863
|
const visibility = args.private ? 'PRIVATE' : args.public ? 'PUBLIC' : null;
|
|
721
864
|
await client.setNodeVisibility(id, visibility);
|
|
722
865
|
console.log(
|
|
723
|
-
`공개범위: ${visibility === 'PRIVATE' ? '비공개(만든 사람·관리자·따로 준 사람만)' : visibility === 'PUBLIC' ? '워크스페이스 전체' : '상위 폴더/워크스페이스 기본값을 따름'}`,
|
|
866
|
+
tr(`공개범위: ${visibility === 'PRIVATE' ? '비공개(만든 사람·관리자·따로 준 사람만)' : visibility === 'PUBLIC' ? '워크스페이스 전체' : '상위 폴더/워크스페이스 기본값을 따름'}`, `Visibility: ${visibility === 'PRIVATE' ? 'private (creator, managers, and people granted access)' : visibility === 'PUBLIC' ? 'the whole workspace' : 'inherits from the parent folder/workspace'}`),
|
|
724
867
|
);
|
|
725
868
|
changed = true;
|
|
726
869
|
}
|
|
727
870
|
if (typeof args.grant === 'string') {
|
|
728
871
|
const level = String(args.level || '').toUpperCase();
|
|
729
872
|
if (!['READ', 'WRITE', 'MANAGE'].includes(level)) {
|
|
730
|
-
throw new Error('--grant 에는 --level READ|WRITE|MANAGE 를 함께 주세요.');
|
|
873
|
+
throw new Error(tr('--grant 에는 --level READ|WRITE|MANAGE 를 함께 주세요.', `Pass --level READ|WRITE|MANAGE together with --grant.`));
|
|
731
874
|
}
|
|
732
875
|
await client.grantNodePermission(id, { subjectType: group, subjectId: args.grant, level });
|
|
733
|
-
console.log(`권한 부여: ${args.grant} → ${level}`);
|
|
876
|
+
console.log(tr(`권한 부여: ${args.grant} → ${level}`, `Granted: ${args.grant} → ${level}`));
|
|
734
877
|
changed = true;
|
|
735
878
|
}
|
|
736
879
|
if (typeof args.revoke === 'string') {
|
|
737
880
|
await client.revokeNodePermission(id, { subjectType: group, subjectId: args.revoke });
|
|
738
|
-
console.log(`권한 회수: ${args.revoke}`);
|
|
881
|
+
console.log(tr(`권한 회수: ${args.revoke}`, `Revoked: ${args.revoke}`));
|
|
739
882
|
changed = true;
|
|
740
883
|
}
|
|
741
884
|
|
|
@@ -746,10 +889,10 @@ async function cmdAccess(args) {
|
|
|
746
889
|
}
|
|
747
890
|
if (!changed) {
|
|
748
891
|
const eff = info.effectiveVisibility ?? info.visibility;
|
|
749
|
-
console.log(`공개범위: ${eff === 'PRIVATE' ? '비공개' : '워크스페이스 전체'}${info.visibility === null ? ' (상속받음)' : ''}`);
|
|
892
|
+
console.log(tr(`공개범위: ${eff === 'PRIVATE' ? '비공개' : '워크스페이스 전체'}${info.visibility === null ? ' (상속받음)' : ''}`, `Visibility: ${eff === 'PRIVATE' ? 'private' : 'the whole workspace'}${info.visibility === null ? ' (inherited)' : ''}`));
|
|
750
893
|
}
|
|
751
894
|
const direct = info.direct ?? [];
|
|
752
|
-
console.log(`따로 준 권한: ${direct.length ? '' : '없음'}`);
|
|
895
|
+
console.log(tr(`따로 준 권한: ${direct.length ? '' : '없음'}`, `Direct grants: ${direct.length ? '' : 'none'}`));
|
|
753
896
|
for (const p of direct) console.log(` - ${p.subjectType} ${p.subjectId} → ${p.level}`);
|
|
754
897
|
}
|
|
755
898
|
|
|
@@ -769,7 +912,7 @@ async function cmdForm(args) {
|
|
|
769
912
|
if (sub === 'response-edit') return cmdFormResponseEdit(args);
|
|
770
913
|
if (sub === 'response-rm') return cmdFormResponseRm(args);
|
|
771
914
|
throw new Error(
|
|
772
|
-
|
|
915
|
+
usage(tr(`rootr form new|show|set|collect|responses|response-edit|response-rm … (자세한 건 rootr --help)`, `rootr form new|show|set|collect|responses|response-edit|response-rm … (see rootr --help)`)),
|
|
773
916
|
);
|
|
774
917
|
}
|
|
775
918
|
|
|
@@ -792,13 +935,13 @@ function parseFieldsArg(args) {
|
|
|
792
935
|
try {
|
|
793
936
|
parsed = JSON.parse(args.fields);
|
|
794
937
|
} catch {
|
|
795
|
-
throw new Error('--fields 는 JSON 배열이어야 합니다. 예: [{"name":"이메일","type":"email","required":true}]');
|
|
938
|
+
throw new Error(tr('--fields 는 JSON 배열이어야 합니다. 예: [{"name":"이메일","type":"email","required":true}]', `--fields must be a JSON array. Example: [{"name":"Email","type":"email","required":true}]`));
|
|
796
939
|
}
|
|
797
|
-
if (!Array.isArray(parsed)) throw new Error('--fields 는 배열이어야 합니다.');
|
|
940
|
+
if (!Array.isArray(parsed)) throw new Error(tr('--fields 는 배열이어야 합니다.', `--fields must be an array.`));
|
|
798
941
|
for (const [i, f] of parsed.entries()) {
|
|
799
|
-
if (!f || typeof f.name !== 'string') throw new Error(`fields[${i}].name 이
|
|
942
|
+
if (!f || typeof f.name !== 'string') throw new Error(tr(`fields[${i}].name 이 필요합니다.`, `fields[${i}].name is required.`));
|
|
800
943
|
if (!FORM_FIELD_TYPES.includes(f.type)) {
|
|
801
|
-
throw new Error(`fields[${i}].type 은 다음 중 하나여야 합니다: ${FORM_FIELD_TYPES.join(', ')}`);
|
|
944
|
+
throw new Error(tr(`fields[${i}].type 은 다음 중 하나여야 합니다: ${FORM_FIELD_TYPES.join(', ')}`, `fields[${i}].type must be one of: ${FORM_FIELD_TYPES.join(', ')}`));
|
|
802
945
|
}
|
|
803
946
|
}
|
|
804
947
|
return parsed;
|
|
@@ -809,14 +952,14 @@ function parseConfigArg(args) {
|
|
|
809
952
|
try {
|
|
810
953
|
return JSON.parse(args.config);
|
|
811
954
|
} catch {
|
|
812
|
-
throw new Error('--config 는 JSON 객체여야 합니다.');
|
|
955
|
+
throw new Error(tr('--config 는 JSON 객체여야 합니다.', `--config must be a JSON object.`));
|
|
813
956
|
}
|
|
814
957
|
}
|
|
815
958
|
|
|
816
959
|
async function cmdFormNew(args) {
|
|
817
960
|
const target = args._[1];
|
|
818
961
|
if (!target) {
|
|
819
|
-
throw new Error("
|
|
962
|
+
throw new Error(usage(tr("rootr form new <경로> --fields '<json>' [--config '<json>'] [--collect db|sheet]", "rootr form new <path> --fields '<json>' [--config '<json>'] [--collect db|sheet]")));
|
|
820
963
|
}
|
|
821
964
|
const client = makeClient();
|
|
822
965
|
const created = await client.createForm(client.workspace, {
|
|
@@ -826,19 +969,19 @@ async function cmdFormNew(args) {
|
|
|
826
969
|
config: parseConfigArg(args),
|
|
827
970
|
});
|
|
828
971
|
const id = created.id ?? created.nodeId;
|
|
829
|
-
console.log(`폼을 만들었습니다: ${target}`);
|
|
972
|
+
console.log(tr(`폼을 만들었습니다: ${target}`, `Form created: ${target}`));
|
|
830
973
|
console.log(` id: ${id}`);
|
|
831
974
|
if (args.collect === 'db' || args.collect === 'sheet') {
|
|
832
975
|
const to = args.collect === 'db' ? 'database' : 'spreadsheet';
|
|
833
976
|
await client.request(`/forms/${encodeURIComponent(id)}/collect`, { method: 'POST', body: { to } });
|
|
834
|
-
console.log(` 응답 모을 곳: ${args.collect === 'db' ? '표' : '스프레드시트'}
|
|
977
|
+
console.log(tr(` 응답 모을 곳: ${args.collect === 'db' ? '표' : '스프레드시트'} 생성됨`, ` Responses collected into: a ${args.collect === 'db' ? 'table' : 'spreadsheet'} was created`));
|
|
835
978
|
}
|
|
836
979
|
}
|
|
837
980
|
|
|
838
981
|
/** `rootr form show <path|id>` — 질문·설정을 그대로 본다(고치기 전에 읽으라고 있는 것). */
|
|
839
982
|
async function cmdFormShow(args) {
|
|
840
983
|
const target = args._[1];
|
|
841
|
-
if (!target) throw new Error(
|
|
984
|
+
if (!target) throw new Error(usage(`rootr form show <path|id> [--json]`));
|
|
842
985
|
const client = makeClient();
|
|
843
986
|
// 경로도 id도 받는다(이 저장소의 다른 명령과 같은 규칙).
|
|
844
987
|
const id = await resolveDatabaseId(client, target);
|
|
@@ -847,32 +990,32 @@ async function cmdFormShow(args) {
|
|
|
847
990
|
console.log(JSON.stringify(form, null, 2));
|
|
848
991
|
return;
|
|
849
992
|
}
|
|
850
|
-
console.log(`${form.name} (응답 ${form.responseCount ?? 0}건, 방문 ${form.viewCount ?? 0}회)`);
|
|
993
|
+
console.log(tr(`${form.name} (응답 ${form.responseCount ?? 0}건, 방문 ${form.viewCount ?? 0}회)`, `${form.name} (${form.responseCount ?? 0} responses, ${form.viewCount ?? 0} views)`));
|
|
851
994
|
for (const [i, f] of (form.fields ?? []).entries()) {
|
|
852
995
|
const marks = [
|
|
853
|
-
f.required ? '필수' : null,
|
|
854
|
-
f.hidden ? '숨김' : null,
|
|
855
|
-
f.page && f.page > 1 ? `${f.page}
|
|
856
|
-
f.showIf ? '조건부' : null,
|
|
996
|
+
f.required ? tr('필수', `required`) : null,
|
|
997
|
+
f.hidden ? tr('숨김', `hidden`) : null,
|
|
998
|
+
f.page && f.page > 1 ? tr(`${f.page}쪽`, `page ${f.page}`) : null,
|
|
999
|
+
f.showIf ? tr('조건부', `conditional`) : null,
|
|
857
1000
|
].filter(Boolean);
|
|
858
1001
|
console.log(` ${i + 1}. ${f.name} [${f.type}]${marks.length ? ' · ' + marks.join('·') : ''}`);
|
|
859
1002
|
if (f.help) console.log(` ${f.help}`);
|
|
860
1003
|
}
|
|
861
1004
|
const cfg = form.config ?? {};
|
|
862
1005
|
const bits = [];
|
|
863
|
-
if (cfg.publicSubmit && cfg.publicSubmit !== 'off') bits.push(`공개 제출: ${cfg.publicSubmit}`);
|
|
864
|
-
if (cfg.closesAt) bits.push(`마감: ${cfg.closesAt}`);
|
|
865
|
-
if (cfg.maxResponses) bits.push(`정원: ${cfg.maxResponses}`);
|
|
866
|
-
if (Array.isArray(cfg.notifyEmails) && cfg.notifyEmails.length) bits.push(`알림: ${cfg.notifyEmails.join(', ')}`);
|
|
867
|
-
if (cfg.webhookUrl) bits.push('웹훅 있음');
|
|
868
|
-
if (cfg.theme) bits.push(`분위기: ${cfg.theme}`);
|
|
1006
|
+
if (cfg.publicSubmit && cfg.publicSubmit !== 'off') bits.push(tr(`공개 제출: ${cfg.publicSubmit}`, `public submission: ${cfg.publicSubmit}`));
|
|
1007
|
+
if (cfg.closesAt) bits.push(tr(`마감: ${cfg.closesAt}`, `closes: ${cfg.closesAt}`));
|
|
1008
|
+
if (cfg.maxResponses) bits.push(tr(`정원: ${cfg.maxResponses}`, `capacity: ${cfg.maxResponses}`));
|
|
1009
|
+
if (Array.isArray(cfg.notifyEmails) && cfg.notifyEmails.length) bits.push(tr(`알림: ${cfg.notifyEmails.join(', ')}`, `notify: ${cfg.notifyEmails.join(', ')}`));
|
|
1010
|
+
if (cfg.webhookUrl) bits.push(tr('웹훅 있음', `has a webhook`));
|
|
1011
|
+
if (cfg.theme) bits.push(tr(`분위기: ${cfg.theme}`, `theme: ${cfg.theme}`));
|
|
869
1012
|
// 켜 놓고 잊기 쉬운 것들 — 안 보이면 "왜 두 번 못 내지?" 하고 한참 찾는다.
|
|
870
|
-
if (cfg.oncePerPerson) bits.push('한 사람 한 번만');
|
|
871
|
-
if (cfg.confirmEmailField) bits.push('확인 메일 보냄');
|
|
872
|
-
if (cfg.intro) bits.push('안내문 있음');
|
|
873
|
-
if (cfg.thankYou) bits.push('인사말 있음');
|
|
874
|
-
if (cfg.redirectUrl) bits.push(`낸 뒤 이동: ${cfg.redirectUrl}`);
|
|
875
|
-
if (bits.length) console.log(` 설정 — ${bits.join(' · ')}`);
|
|
1013
|
+
if (cfg.oncePerPerson) bits.push(tr('한 사람 한 번만', `one response per person`));
|
|
1014
|
+
if (cfg.confirmEmailField) bits.push(tr('확인 메일 보냄', `sends a confirmation email`));
|
|
1015
|
+
if (cfg.intro) bits.push(tr('안내문 있음', `has an intro`));
|
|
1016
|
+
if (cfg.thankYou) bits.push(tr('인사말 있음', `has a greeting`));
|
|
1017
|
+
if (cfg.redirectUrl) bits.push(tr(`낸 뒤 이동: ${cfg.redirectUrl}`, `after submit, go to: ${cfg.redirectUrl}`));
|
|
1018
|
+
if (bits.length) console.log(tr(` 설정 — ${bits.join(' · ')}`, ` settings — ${bits.join(' · ')}`));
|
|
876
1019
|
}
|
|
877
1020
|
|
|
878
1021
|
/**
|
|
@@ -886,15 +1029,15 @@ async function cmdFormSet(args) {
|
|
|
886
1029
|
const fields = parseFieldsArg(args);
|
|
887
1030
|
const config = parseConfigArg(args);
|
|
888
1031
|
if (!target || (fields === undefined && config === undefined)) {
|
|
889
|
-
throw new Error("
|
|
1032
|
+
throw new Error(usage("rootr form set <path|id> --fields '<json>' | --config '<json>'"));
|
|
890
1033
|
}
|
|
891
1034
|
const client = makeClient();
|
|
892
1035
|
const id = await resolveDatabaseId(client, target);
|
|
893
1036
|
// 설정은 **보낸 것만** 바뀐다(서버가 합쳐 준다) — 지우려면 그 항목에 null.
|
|
894
1037
|
await client.updateForm(id, { ...(fields ? { fields } : {}), ...(config ? { config } : {}) });
|
|
895
|
-
console.log('폼을 고쳤습니다.');
|
|
896
|
-
if (fields) console.log(` 질문 ${fields.length}
|
|
897
|
-
if (config) console.log(` 바꾼 설정: ${Object.keys(config).join(', ')}`);
|
|
1038
|
+
console.log(tr('폼을 고쳤습니다.', `Form updated.`));
|
|
1039
|
+
if (fields) console.log(tr(` 질문 ${fields.length}개`, ` ${fields.length} questions`));
|
|
1040
|
+
if (config) console.log(tr(` 바꾼 설정: ${Object.keys(config).join(', ')}`, ` settings changed: ${Object.keys(config).join(', ')}`));
|
|
898
1041
|
}
|
|
899
1042
|
|
|
900
1043
|
/**
|
|
@@ -905,10 +1048,10 @@ async function cmdFormSet(args) {
|
|
|
905
1048
|
*/
|
|
906
1049
|
async function cmdFormCollect(args) {
|
|
907
1050
|
const target = args._[1];
|
|
908
|
-
if (!target) throw new Error(
|
|
1051
|
+
if (!target) throw new Error(usage(`rootr form collect <path|id> --to db|sheet`));
|
|
909
1052
|
const to = String(args.to || '').toLowerCase();
|
|
910
1053
|
const map = { db: 'database', database: 'database', sheet: 'spreadsheet', spreadsheet: 'spreadsheet' };
|
|
911
|
-
if (!map[to]) throw new Error('--to 에 db 또는 sheet 를 주세요.');
|
|
1054
|
+
if (!map[to]) throw new Error(tr('--to 에 db 또는 sheet 를 주세요.', `Pass db or sheet to --to.`));
|
|
912
1055
|
|
|
913
1056
|
const client = makeClient();
|
|
914
1057
|
const id = requireResolved(await resolveTarget(client, target));
|
|
@@ -919,14 +1062,14 @@ async function cmdFormCollect(args) {
|
|
|
919
1062
|
}
|
|
920
1063
|
console.log(
|
|
921
1064
|
made.kind === 'database'
|
|
922
|
-
? `표를 만들어 연결했습니다: ${made.nodeId} (폼 질문 그대로 칸이 생겼습니다)`
|
|
923
|
-
: `스프레드시트를 만들어 연결했습니다: ${made.nodeId}`,
|
|
1065
|
+
? tr(`표를 만들어 연결했습니다: ${made.nodeId} (폼 질문 그대로 칸이 생겼습니다)`, `Created and wired a table: ${made.nodeId} (one column per question)`)
|
|
1066
|
+
: tr(`스프레드시트를 만들어 연결했습니다: ${made.nodeId}`, `Created and wired a spreadsheet: ${made.nodeId}`),
|
|
924
1067
|
);
|
|
925
1068
|
}
|
|
926
1069
|
|
|
927
1070
|
async function cmdFormResponses(args) {
|
|
928
1071
|
const target = args._[1];
|
|
929
|
-
if (!target) throw new Error(
|
|
1072
|
+
if (!target) throw new Error(usage(`rootr form responses <path|id> [--latest] [--json]`));
|
|
930
1073
|
const client = makeClient();
|
|
931
1074
|
const id = requireResolved(await resolveTarget(client, target));
|
|
932
1075
|
const rows = await client.listFormResponses(id, { latestPerRespondent: Boolean(args.latest) });
|
|
@@ -936,26 +1079,26 @@ async function cmdFormResponses(args) {
|
|
|
936
1079
|
return;
|
|
937
1080
|
}
|
|
938
1081
|
if (!rows.length) {
|
|
939
|
-
console.log('아직 들어온 응답이 없습니다.');
|
|
1082
|
+
console.log(tr('아직 들어온 응답이 없습니다.', `No responses yet.`));
|
|
940
1083
|
return;
|
|
941
1084
|
}
|
|
942
|
-
console.log(`응답 ${rows.length}건${args.latest ? ' (사람마다 최신 1건만)' : ''}`);
|
|
1085
|
+
console.log(tr(`응답 ${rows.length}건${args.latest ? ' (사람마다 최신 1건만)' : ''}`, `${rows.length} responses${args.latest ? ' (newest one per person)' : ''}`));
|
|
943
1086
|
for (const r of rows) {
|
|
944
|
-
const who = r.submitterUserId ? r.submitterUserId : '익명';
|
|
945
|
-
console.log(`- ${r.id} ${new Date(r.createdAt).toISOString()} 제출자:${who}`);
|
|
1087
|
+
const who = r.submitterUserId ? r.submitterUserId : tr('익명', `anonymous`);
|
|
1088
|
+
console.log(tr(`- ${r.id} ${new Date(r.createdAt).toISOString()} 제출자:${who}`, `- ${r.id} ${new Date(r.createdAt).toISOString()} by:${who}`));
|
|
946
1089
|
console.log(` ${JSON.stringify(r.values)}`);
|
|
947
1090
|
}
|
|
948
1091
|
}
|
|
949
1092
|
|
|
950
1093
|
async function cmdFormResponseEdit(args) {
|
|
951
1094
|
const responseId = args._[1];
|
|
952
|
-
if (!responseId) throw new Error("
|
|
1095
|
+
if (!responseId) throw new Error(usage(tr("rootr form response-edit <응답id> --values '<json>'", "rootr form response-edit <responseId> --values '<json>'")));
|
|
953
1096
|
const raw = typeof args.values === 'string' ? args.values : await readStdin();
|
|
954
1097
|
let values;
|
|
955
1098
|
try {
|
|
956
1099
|
values = JSON.parse(raw);
|
|
957
1100
|
} catch {
|
|
958
|
-
throw new Error('--values 를 JSON으로 읽을 수 없습니다. {"필드id":"값"} 형태여야 합니다.');
|
|
1101
|
+
throw new Error(tr('--values 를 JSON으로 읽을 수 없습니다. {"필드id":"값"} 형태여야 합니다.', `Could not read --values as JSON. It must look like {"fieldId":"value"}.`));
|
|
959
1102
|
}
|
|
960
1103
|
// 통째로 바뀐다 — 남기고 싶은 답도 같이 보내야 한다.
|
|
961
1104
|
const result = await makeClient().updateFormResponse(responseId, values);
|
|
@@ -963,18 +1106,18 @@ async function cmdFormResponseEdit(args) {
|
|
|
963
1106
|
console.log(JSON.stringify(result, null, 2));
|
|
964
1107
|
return;
|
|
965
1108
|
}
|
|
966
|
-
console.log(`응답 수정됨: ${result.id ?? responseId}`);
|
|
1109
|
+
console.log(tr(`응답 수정됨: ${result.id ?? responseId}`, `Response updated: ${result.id ?? responseId}`));
|
|
967
1110
|
console.log(` ${JSON.stringify(result.values ?? values)}`);
|
|
968
1111
|
}
|
|
969
1112
|
|
|
970
1113
|
async function cmdFormResponseRm(args) {
|
|
971
1114
|
const responseId = args._[1];
|
|
972
|
-
if (!responseId) throw new Error(
|
|
1115
|
+
if (!responseId) throw new Error(usage(tr(`rootr form response-rm <응답id> [--yes]`, `rootr form response-rm <responseId> [--yes]`)));
|
|
973
1116
|
if (!args.yes) {
|
|
974
|
-
throw new Error('응답 삭제는 되돌릴 수 없습니다. 정말 지우려면 --yes 를 붙이세요.');
|
|
1117
|
+
throw new Error(tr('응답 삭제는 되돌릴 수 없습니다. 정말 지우려면 --yes 를 붙이세요.', `Deleting a response cannot be undone. Add --yes if you really mean it.`));
|
|
975
1118
|
}
|
|
976
1119
|
await makeClient().deleteFormResponse(responseId);
|
|
977
|
-
console.log(`응답 삭제됨: ${responseId}`);
|
|
1120
|
+
console.log(tr(`응답 삭제됨: ${responseId}`, `Response deleted: ${responseId}`));
|
|
978
1121
|
}
|
|
979
1122
|
|
|
980
1123
|
/**
|
|
@@ -987,13 +1130,18 @@ async function cmdDbRowsSet(args) {
|
|
|
987
1130
|
const target = args._[1];
|
|
988
1131
|
const ids = typeof args.ids === 'string' ? args.ids.split(',').map((i) => i.trim()).filter(Boolean) : [];
|
|
989
1132
|
if (!target || !ids.length || typeof args.values !== 'string') {
|
|
990
|
-
throw new Error("
|
|
1133
|
+
throw new Error(usage("rootr db rows-set <path|id> --ids <id,id> --values '<json>'"));
|
|
991
1134
|
}
|
|
992
1135
|
let values;
|
|
993
1136
|
try {
|
|
994
1137
|
values = JSON.parse(args.values);
|
|
995
1138
|
} catch {
|
|
996
|
-
throw new Error(
|
|
1139
|
+
throw new Error(
|
|
1140
|
+
tr(
|
|
1141
|
+
'--values 를 JSON으로 읽을 수 없습니다. 예: \'{"작성일":{"start":"2026-08-01"}}\'',
|
|
1142
|
+
'--values could not be read as JSON. Example: \'{"Created":{"start":"2026-08-01"}}\'',
|
|
1143
|
+
),
|
|
1144
|
+
);
|
|
997
1145
|
}
|
|
998
1146
|
const client = makeClient();
|
|
999
1147
|
const id = await resolveDatabaseId(client, target);
|
|
@@ -1004,7 +1152,7 @@ async function cmdDbRowsSet(args) {
|
|
|
1004
1152
|
Object.entries(values).map(([k, v]) => [byName.get(k) ?? k, v]),
|
|
1005
1153
|
);
|
|
1006
1154
|
const res = await client.bulkUpdateRows(id, ids, resolved);
|
|
1007
|
-
console.log(`${res.updated}개 행을
|
|
1155
|
+
console.log(tr(`${res.updated}개 행을 바꿨습니다.`, `${res.updated} rows changed.`));
|
|
1008
1156
|
}
|
|
1009
1157
|
|
|
1010
1158
|
/**
|
|
@@ -1016,13 +1164,13 @@ async function cmdDbRowsSet(args) {
|
|
|
1016
1164
|
async function cmdDbRowsNew(args) {
|
|
1017
1165
|
const target = args._[1];
|
|
1018
1166
|
const raw = readJsonArg(args, 'rows-new');
|
|
1019
|
-
if (!target) throw new Error("
|
|
1167
|
+
if (!target) throw new Error(usage(tr("rootr db rows-new <path|id> --json '<[{…},…]>' | --file <파일>", "rootr db rows-new <path|id> --json '<[{…},…]>' | --file <file>")));
|
|
1020
1168
|
const rows = Array.isArray(raw) ? raw : [raw];
|
|
1021
|
-
if (!rows.length) throw new Error('넣을 행이 없습니다.');
|
|
1169
|
+
if (!rows.length) throw new Error(tr('넣을 행이 없습니다.', `No rows to insert.`));
|
|
1022
1170
|
const client = makeClient();
|
|
1023
1171
|
const id = await resolveDatabaseId(client, target);
|
|
1024
1172
|
const res = await client.bulkCreateRows(id, rows);
|
|
1025
|
-
console.log(`${res.created}개 행을
|
|
1173
|
+
console.log(tr(`${res.created}개 행을 넣었습니다.`, `${res.created} rows inserted.`));
|
|
1026
1174
|
}
|
|
1027
1175
|
|
|
1028
1176
|
/**
|
|
@@ -1033,17 +1181,17 @@ async function cmdDbRowsNew(args) {
|
|
|
1033
1181
|
async function cmdDbRowsEach(args) {
|
|
1034
1182
|
const target = args._[1];
|
|
1035
1183
|
const raw = readJsonArg(args, 'rows-each');
|
|
1036
|
-
if (!target) throw new Error("
|
|
1184
|
+
if (!target) throw new Error(usage(tr("rootr db rows-each <path|id> --json '<[{rowId,values},…]>' | --file <파일>", "rootr db rows-each <path|id> --json '<[{rowId,values},…]>' | --file <file>")));
|
|
1037
1185
|
const updates = Array.isArray(raw) ? raw : [raw];
|
|
1038
1186
|
for (const u of updates) {
|
|
1039
1187
|
if (!u || typeof u.rowId !== 'string' || typeof u.values !== 'object' || !u.values) {
|
|
1040
|
-
throw new Error('각 항목은 { "rowId": "…", "values": { … } } 형태여야 합니다.');
|
|
1188
|
+
throw new Error(tr('각 항목은 { "rowId": "…", "values": { … } } 형태여야 합니다.', `Each item must look like { "rowId": "…", "values": { … } }.`));
|
|
1041
1189
|
}
|
|
1042
1190
|
}
|
|
1043
1191
|
const client = makeClient();
|
|
1044
1192
|
const id = await resolveDatabaseId(client, target);
|
|
1045
1193
|
const res = await client.bulkUpdateRowsEach(id, updates);
|
|
1046
|
-
console.log(`${res.updated}개 행을
|
|
1194
|
+
console.log(tr(`${res.updated}개 행을 바꿨습니다.`, `${res.updated} rows changed.`));
|
|
1047
1195
|
}
|
|
1048
1196
|
|
|
1049
1197
|
/** `--json '<...>'` 또는 `--file <파일>` 중 하나를 읽는다. 행이 많으면 파일이 편하다. */
|
|
@@ -1061,7 +1209,7 @@ async function cmdScaffold(args) {
|
|
|
1061
1209
|
const client = makeClient();
|
|
1062
1210
|
if (sub === 'plan') {
|
|
1063
1211
|
const intent = args._.slice(1).join(' ').trim();
|
|
1064
|
-
if (!intent) throw new Error(
|
|
1212
|
+
if (!intent) throw new Error(usage(tr(`rootr scaffold plan "무엇을 위한 워크스페이스인지"`, `rootr scaffold plan "what the workspace is for"`)));
|
|
1065
1213
|
console.log(JSON.stringify(await client.scaffoldPlan(intent), null, 2));
|
|
1066
1214
|
return;
|
|
1067
1215
|
}
|
|
@@ -1074,14 +1222,14 @@ async function cmdScaffold(args) {
|
|
|
1074
1222
|
}
|
|
1075
1223
|
console.log(r.summary);
|
|
1076
1224
|
for (const f of r.findings) {
|
|
1077
|
-
const mark = f.level === 'error' ? '고쳐야 함' : f.level === 'warn' ? '약함' : '더 나아짐';
|
|
1225
|
+
const mark = f.level === 'error' ? tr('고쳐야 함', `needs work`) : f.level === 'warn' ? tr('약함', `weak`) : tr('더 나아짐', `improved`);
|
|
1078
1226
|
console.log(` [${mark}] ${f.at} ${f.message}`);
|
|
1079
1227
|
}
|
|
1080
1228
|
return;
|
|
1081
1229
|
}
|
|
1082
1230
|
if (sub === 'built') {
|
|
1083
1231
|
const ws = args.workspace || loadConfig().workspace;
|
|
1084
|
-
if (!ws) throw new Error('워크스페이스가 없습니다: --workspace <id>');
|
|
1232
|
+
if (!ws) throw new Error(tr('워크스페이스가 없습니다: --workspace <id>', `No workspace: --workspace <id>`));
|
|
1085
1233
|
const r = await client.scaffoldReviewBuilt(ws, typeof args.root === 'string' ? args.root : undefined);
|
|
1086
1234
|
if (args.json) {
|
|
1087
1235
|
console.log(JSON.stringify(r, null, 2));
|
|
@@ -1089,14 +1237,14 @@ async function cmdScaffold(args) {
|
|
|
1089
1237
|
}
|
|
1090
1238
|
console.log(r.summary);
|
|
1091
1239
|
for (const f of r.findings) {
|
|
1092
|
-
const mark = f.level === 'error' ? '고쳐야 함' : f.level === 'warn' ? '약함' : '더 나아짐';
|
|
1240
|
+
const mark = f.level === 'error' ? tr('고쳐야 함', `needs work`) : f.level === 'warn' ? tr('약함', `weak`) : tr('더 나아짐', `improved`);
|
|
1093
1241
|
console.log(` [${mark}] ${f.at} ${f.message}`);
|
|
1094
1242
|
}
|
|
1095
1243
|
return;
|
|
1096
1244
|
}
|
|
1097
1245
|
if (sub === 'test') {
|
|
1098
1246
|
const ws = args.workspace || loadConfig().workspace;
|
|
1099
|
-
if (!ws) throw new Error('워크스페이스가 없습니다: --workspace <id>');
|
|
1247
|
+
if (!ws) throw new Error(tr('워크스페이스가 없습니다: --workspace <id>', `No workspace: --workspace <id>`));
|
|
1100
1248
|
const r = await client.scaffoldSelfTest(ws, typeof args.root === 'string' ? args.root : undefined);
|
|
1101
1249
|
if (args.json) {
|
|
1102
1250
|
console.log(JSON.stringify(r, null, 2));
|
|
@@ -1104,7 +1252,7 @@ async function cmdScaffold(args) {
|
|
|
1104
1252
|
}
|
|
1105
1253
|
console.log(r.summary);
|
|
1106
1254
|
for (const c of r.checks) {
|
|
1107
|
-
console.log(` [${c.level === 'fail' ? '실패' : '경고'}] ${c.at} (${c.check}) ${c.message}`);
|
|
1255
|
+
console.log(tr(` [${c.level === 'fail' ? '실패' : '경고'}] ${c.at} (${c.check}) ${c.message}`, ` [${c.level === 'fail' ? 'FAIL' : 'WARN'}] ${c.at} (${c.check}) ${c.message}`));
|
|
1108
1256
|
}
|
|
1109
1257
|
if (r.failed) process.exitCode = 1;
|
|
1110
1258
|
return;
|
|
@@ -1112,27 +1260,27 @@ async function cmdScaffold(args) {
|
|
|
1112
1260
|
if (sub === 'apply') {
|
|
1113
1261
|
const tree = readJsonArg(args, 'scaffold apply');
|
|
1114
1262
|
const ws = args.workspace || loadConfig().workspace;
|
|
1115
|
-
if (!ws) throw new Error('워크스페이스가 없습니다: --workspace <id> 또는 rootr config --workspace <id>');
|
|
1263
|
+
if (!ws) throw new Error(tr('워크스페이스가 없습니다: --workspace <id> 또는 rootr config --workspace <id>', `No workspace: --workspace <id>, or rootr config --workspace <id>`));
|
|
1116
1264
|
const r = await client.scaffoldApply(ws, {
|
|
1117
1265
|
rootPath: typeof args.root === 'string' ? args.root : undefined,
|
|
1118
1266
|
tree: Array.isArray(tree) ? tree : tree.tree,
|
|
1119
1267
|
});
|
|
1120
|
-
console.log(`${r.created.length}개를 ${r.rootPath || '/'} 아래에
|
|
1121
|
-
if (r.review) console.log(`검사: ${r.review.summary}`);
|
|
1268
|
+
console.log(tr(`${r.created.length}개를 ${r.rootPath || '/'} 아래에 만들었습니다.`, `Created ${r.created.length} items under ${r.rootPath || '/'}.`));
|
|
1269
|
+
if (r.review) console.log(tr(`검사: ${r.review.summary}`, `Review: ${r.review.summary}`));
|
|
1122
1270
|
return;
|
|
1123
1271
|
}
|
|
1124
|
-
throw new Error(
|
|
1272
|
+
throw new Error(usage(tr(`rootr scaffold plan|review|apply (review/apply 는 --file tree.json)`, `rootr scaffold plan|review|apply (review/apply take --file tree.json)`)));
|
|
1125
1273
|
}
|
|
1126
1274
|
|
|
1127
1275
|
function readJsonArg(args, cmd) {
|
|
1128
1276
|
let text = null;
|
|
1129
1277
|
if (typeof args.file === 'string') text = readFileSync(args.file, 'utf8');
|
|
1130
1278
|
else if (typeof args.json === 'string') text = args.json;
|
|
1131
|
-
if (text == null) throw new Error(`${cmd}: --json 또는 --file 이
|
|
1279
|
+
if (text == null) throw new Error(tr(`${cmd}: --json 또는 --file 이 필요합니다.`, `${cmd}: --json or --file is required.`));
|
|
1132
1280
|
try {
|
|
1133
1281
|
return JSON.parse(text);
|
|
1134
1282
|
} catch {
|
|
1135
|
-
throw new Error(`${cmd}: JSON으로 읽을 수
|
|
1283
|
+
throw new Error(tr(`${cmd}: JSON으로 읽을 수 없습니다.`, `${cmd}: could not read it as JSON.`));
|
|
1136
1284
|
}
|
|
1137
1285
|
}
|
|
1138
1286
|
|
|
@@ -1141,15 +1289,15 @@ async function cmdDbRowsRm(args) {
|
|
|
1141
1289
|
const target = args._[1];
|
|
1142
1290
|
const ids = typeof args.ids === 'string' ? args.ids.split(',').map((i) => i.trim()).filter(Boolean) : [];
|
|
1143
1291
|
if (!target || !ids.length) {
|
|
1144
|
-
throw new Error(
|
|
1292
|
+
throw new Error(usage(`rootr db rows-rm <path|id> --ids <id,id> [--yes]`));
|
|
1145
1293
|
}
|
|
1146
1294
|
if (!args.yes) {
|
|
1147
|
-
throw new Error(`${ids.length}개 행을 지웁니다. 되돌릴 수 없으니 정말 지우려면 --yes 를
|
|
1295
|
+
throw new Error(tr(`${ids.length}개 행을 지웁니다. 되돌릴 수 없으니 정말 지우려면 --yes 를 붙이세요.`, `This deletes ${ids.length} rows and cannot be undone. Add --yes if you really mean it.`));
|
|
1148
1296
|
}
|
|
1149
1297
|
const client = makeClient();
|
|
1150
1298
|
const id = await resolveDatabaseId(client, target);
|
|
1151
1299
|
const res = await client.bulkDeleteRows(id, ids);
|
|
1152
|
-
console.log(`${res.deleted}개 행을
|
|
1300
|
+
console.log(tr(`${res.deleted}개 행을 지웠습니다.`, `${res.deleted} rows deleted.`));
|
|
1153
1301
|
}
|
|
1154
1302
|
|
|
1155
1303
|
/**
|
|
@@ -1177,7 +1325,7 @@ async function cmdPublish(args) {
|
|
|
1177
1325
|
const target = args._[0];
|
|
1178
1326
|
if (!target) {
|
|
1179
1327
|
throw new Error(
|
|
1180
|
-
|
|
1328
|
+
usage(tr(`rootr publish <path|id> [--children] [--view <id>] [--layout <배치>] [--skin <분위기>] `, `rootr publish <path|id> [--children] [--view <id>] [--layout <layout>] [--skin <skin>] `)) +
|
|
1181
1329
|
'[--comments] [--expires <ISO>] | --off | --status',
|
|
1182
1330
|
);
|
|
1183
1331
|
}
|
|
@@ -1190,24 +1338,24 @@ async function cmdPublish(args) {
|
|
|
1190
1338
|
if (args.status) {
|
|
1191
1339
|
const st = isDocument ? await client.getDocumentPublicStatus(id) : await client.nodePublicStatus(id);
|
|
1192
1340
|
if (!isDocument && !st.publishable) {
|
|
1193
|
-
console.log('이 종류는 공개할 수 없습니다(폴더).');
|
|
1341
|
+
console.log(tr('이 종류는 공개할 수 없습니다(폴더).', `This node type cannot be published (folder).`));
|
|
1194
1342
|
return;
|
|
1195
1343
|
}
|
|
1196
1344
|
if (!st.isPublic) {
|
|
1197
|
-
console.log(st.slug ? `닫혀 있습니다(주소는 남아 있음: /p/${st.slug})` : '아직 공개한 적이 없습니다.');
|
|
1345
|
+
console.log(st.slug ? tr(`닫혀 있습니다(주소는 남아 있음: /p/${st.slug})`, `Closed (the address is still reserved: /p/${st.slug})`) : tr('아직 공개한 적이 없습니다.', `Never published.`));
|
|
1198
1346
|
return;
|
|
1199
1347
|
}
|
|
1200
|
-
console.log(`공개 중: /p/${st.slug}`);
|
|
1201
|
-
if (st.expiresAt) console.log(` 만료: ${st.expiresAt}`);
|
|
1348
|
+
console.log(tr(`공개 중: /p/${st.slug}`, `Public: /p/${st.slug}`));
|
|
1349
|
+
if (st.expiresAt) console.log(tr(` 만료: ${st.expiresAt}`, ` expires: ${st.expiresAt}`));
|
|
1202
1350
|
if (isDocument) {
|
|
1203
|
-
console.log(` 댓글(편집 제안) 허용: ${st.allowCommits ? '예' : '아니오'}`);
|
|
1204
|
-
if (st.publicConfig?.skin) console.log(` 스킨: ${st.publicConfig.skin}`);
|
|
1351
|
+
console.log(tr(` 댓글(편집 제안) 허용: ${st.allowCommits ? '예' : '아니오'}`, ` comments (edit suggestions) allowed: ${st.allowCommits ? 'yes' : 'no'}`));
|
|
1352
|
+
if (st.publicConfig?.skin) console.log(tr(` 스킨: ${st.publicConfig.skin}`, ` skin: ${st.publicConfig.skin}`));
|
|
1205
1353
|
} else {
|
|
1206
|
-
if (st.hasPassword) console.log(' 비밀번호 걸려 있음');
|
|
1207
|
-
console.log(` 하위 문서도 공개: ${st.includeChildren ? '예' : '아니오'}`);
|
|
1208
|
-
console.log(` 복제 허용: ${st.allowClone === false ? '아니오' : '예'}`);
|
|
1354
|
+
if (st.hasPassword) console.log(tr(' 비밀번호 걸려 있음', ` password protected`));
|
|
1355
|
+
console.log(tr(` 하위 문서도 공개: ${st.includeChildren ? '예' : '아니오'}`, ` children published too: ${st.includeChildren ? 'yes' : 'no'}`));
|
|
1356
|
+
console.log(tr(` 복제 허용: ${st.allowClone === false ? '아니오' : '예'}`, ` cloning allowed: ${st.allowClone === false ? 'no' : 'yes'}`));
|
|
1209
1357
|
if (st.design && (st.design.layout || st.design.skin)) {
|
|
1210
|
-
console.log(` 보이는 방식: ${st.design.layout ?? 'table'} / ${st.design.skin ?? 'clean'}`);
|
|
1358
|
+
console.log(tr(` 보이는 방식: ${st.design.layout ?? 'table'} / ${st.design.skin ?? 'clean'}`, ` looks like: ${st.design.layout ?? 'table'} / ${st.design.skin ?? 'clean'}`));
|
|
1211
1359
|
}
|
|
1212
1360
|
}
|
|
1213
1361
|
return;
|
|
@@ -1216,18 +1364,18 @@ async function cmdPublish(args) {
|
|
|
1216
1364
|
if (args.off) {
|
|
1217
1365
|
if (isDocument) await client.setDocumentPublic(id, { isPublic: false });
|
|
1218
1366
|
else await client.setNodePublic(id, { isPublic: false });
|
|
1219
|
-
console.log('공개를 닫았습니다. 딸려 나갔던 하위 문서도 같이 닫혔습니다(주소는 남아 있습니다).');
|
|
1367
|
+
console.log(tr('공개를 닫았습니다. 딸려 나갔던 하위 문서도 같이 닫혔습니다(주소는 남아 있습니다).', `Publishing stopped. Children that came along were closed too (the addresses stay reserved).`));
|
|
1220
1368
|
return;
|
|
1221
1369
|
}
|
|
1222
1370
|
|
|
1223
1371
|
if (isDocument) {
|
|
1224
1372
|
if (args.skin !== undefined && !DOC_SKINS.includes(args.skin)) {
|
|
1225
|
-
throw new Error(`--skin 은 다음 중 하나여야 합니다: ${DOC_SKINS.join(', ')}`);
|
|
1373
|
+
throw new Error(tr(`--skin 은 다음 중 하나여야 합니다: ${DOC_SKINS.join(', ')}`, `--skin must be one of: ${DOC_SKINS.join(', ')}`));
|
|
1226
1374
|
}
|
|
1227
1375
|
const cur = await client.getDocumentPublicStatus(id);
|
|
1228
1376
|
if (!cur.isPublic) {
|
|
1229
1377
|
const res = await client.publishDocument(id);
|
|
1230
|
-
console.log(`공개했습니다: /p/${res.slug}`);
|
|
1378
|
+
console.log(tr(`공개했습니다: /p/${res.slug}`, `Published: /p/${res.slug}`));
|
|
1231
1379
|
}
|
|
1232
1380
|
const patch = {};
|
|
1233
1381
|
const comments = flagToBool(args.comments);
|
|
@@ -1237,10 +1385,10 @@ async function cmdPublish(args) {
|
|
|
1237
1385
|
if (Object.keys(patch).length) {
|
|
1238
1386
|
await client.setDocumentPublic(id, patch);
|
|
1239
1387
|
if (patch.allowCommits !== undefined) {
|
|
1240
|
-
console.log(` 댓글(편집 제안) 허용: ${patch.allowCommits ? '예' : '아니오'}`);
|
|
1388
|
+
console.log(tr(` 댓글(편집 제안) 허용: ${patch.allowCommits ? '예' : '아니오'}`, ` comments (edit suggestions) allowed: ${patch.allowCommits ? 'yes' : 'no'}`));
|
|
1241
1389
|
}
|
|
1242
|
-
if (patch.expiresAt !== undefined) console.log(` 만료: ${patch.expiresAt}`);
|
|
1243
|
-
if (patch.skin !== undefined) console.log(` 스킨: ${patch.skin}`);
|
|
1390
|
+
if (patch.expiresAt !== undefined) console.log(tr(` 만료: ${patch.expiresAt}`, ` expires: ${patch.expiresAt}`));
|
|
1391
|
+
if (patch.skin !== undefined) console.log(tr(` 스킨: ${patch.skin}`, ` skin: ${patch.skin}`));
|
|
1244
1392
|
}
|
|
1245
1393
|
return;
|
|
1246
1394
|
}
|
|
@@ -1251,20 +1399,20 @@ async function cmdPublish(args) {
|
|
|
1251
1399
|
...(typeof args.skin === 'string' ? { skin: args.skin } : {}),
|
|
1252
1400
|
};
|
|
1253
1401
|
if (design.layout && !DB_LAYOUTS.includes(design.layout)) {
|
|
1254
|
-
throw new Error(`--layout 은 다음 중 하나여야 합니다: ${DB_LAYOUTS.join(', ')}`);
|
|
1402
|
+
throw new Error(tr(`--layout 은 다음 중 하나여야 합니다: ${DB_LAYOUTS.join(', ')}`, `--layout must be one of: ${DB_LAYOUTS.join(', ')}`));
|
|
1255
1403
|
}
|
|
1256
1404
|
if (design.skin && !DB_SKINS.includes(design.skin)) {
|
|
1257
|
-
throw new Error(`--skin 은 다음 중 하나여야 합니다: ${DB_SKINS.join(', ')}`);
|
|
1405
|
+
throw new Error(tr(`--skin 은 다음 중 하나여야 합니다: ${DB_SKINS.join(', ')}`, `--skin must be one of: ${DB_SKINS.join(', ')}`));
|
|
1258
1406
|
}
|
|
1259
1407
|
const res = await client.publishNode(id, {
|
|
1260
1408
|
...(args.children ? { includeChildren: true } : {}),
|
|
1261
1409
|
...(typeof args.view === 'string' ? { view: args.view } : {}),
|
|
1262
1410
|
...(Object.keys(design).length ? { design } : {}),
|
|
1263
1411
|
});
|
|
1264
|
-
console.log(`공개했습니다: /p/${res.slug}`);
|
|
1265
|
-
if (args.children) console.log(' 이 문서 아래 전부(손자·증손자 포함)도 각자 주소로 공개됩니다.');
|
|
1412
|
+
console.log(tr(`공개했습니다: /p/${res.slug}`, `Published: /p/${res.slug}`));
|
|
1413
|
+
if (args.children) console.log(tr(' 이 문서 아래 전부(손자·증손자 포함)도 각자 주소로 공개됩니다.', ` Everything under this document (grandchildren included) gets its own public address.`));
|
|
1266
1414
|
if (Object.keys(design).length) {
|
|
1267
|
-
console.log(` 보이는 방식: ${res.design?.layout ?? 'table'} / ${res.design?.skin ?? 'clean'}`);
|
|
1415
|
+
console.log(tr(` 보이는 방식: ${res.design?.layout ?? 'table'} / ${res.design?.skin ?? 'clean'}`, ` looks like: ${res.design?.layout ?? 'table'} / ${res.design?.skin ?? 'clean'}`));
|
|
1268
1416
|
}
|
|
1269
1417
|
}
|
|
1270
1418
|
|
|
@@ -1278,7 +1426,7 @@ async function cmdReadReq(args) {
|
|
|
1278
1426
|
const target = args._[0];
|
|
1279
1427
|
if (!target) {
|
|
1280
1428
|
throw new Error(
|
|
1281
|
-
|
|
1429
|
+
usage(`rootr readreq <path|id> [--json] | --users <id,id> [--due <ISO>] [--ack] | --clear`),
|
|
1282
1430
|
);
|
|
1283
1431
|
}
|
|
1284
1432
|
const client = makeClient();
|
|
@@ -1286,19 +1434,19 @@ async function cmdReadReq(args) {
|
|
|
1286
1434
|
|
|
1287
1435
|
if (args.clear) {
|
|
1288
1436
|
await client.clearReadRequirement(id);
|
|
1289
|
-
console.log('읽음 확인 해제됨');
|
|
1437
|
+
console.log(tr('읽음 확인 해제됨', `Read requirement cleared`));
|
|
1290
1438
|
return;
|
|
1291
1439
|
}
|
|
1292
1440
|
|
|
1293
1441
|
if (typeof args.users === 'string') {
|
|
1294
1442
|
const userIds = args.users.split(',').map((u) => u.trim()).filter(Boolean);
|
|
1295
|
-
if (!userIds.length) throw new Error('--users 에 사람 id를 쉼표로 주세요.');
|
|
1443
|
+
if (!userIds.length) throw new Error(tr('--users 에 사람 id를 쉼표로 주세요.', `Pass user ids to --users, comma separated.`));
|
|
1296
1444
|
const res = await client.setReadRequirement(id, {
|
|
1297
1445
|
userIds,
|
|
1298
1446
|
dueAt: typeof args.due === 'string' ? args.due : null,
|
|
1299
1447
|
requireExplicitAck: Boolean(args.ack),
|
|
1300
1448
|
});
|
|
1301
|
-
console.log(`읽어야 할 사람 ${userIds.length}명 지정됨${args.ack ? ' (확인 버튼까지 눌러야 함)' : ''}`);
|
|
1449
|
+
console.log(tr(`읽어야 할 사람 ${userIds.length}명 지정됨${args.ack ? ' (확인 버튼까지 눌러야 함)' : ''}`, `${userIds.length} people must read it${args.ack ? ' (they must press the acknowledge button too)' : ''}`));
|
|
1302
1450
|
if (args.json) console.log(JSON.stringify(res, null, 2));
|
|
1303
1451
|
return;
|
|
1304
1452
|
}
|
|
@@ -1311,19 +1459,19 @@ async function cmdReadReq(args) {
|
|
|
1311
1459
|
if (!res.requirement) {
|
|
1312
1460
|
console.log(
|
|
1313
1461
|
res.ancestorRequirementId
|
|
1314
|
-
? `이 문서 자체에는 없고, 위쪽(${res.ancestorRequirementId})에 걸려
|
|
1315
|
-
: '읽음 확인이 걸려 있지 않습니다.',
|
|
1462
|
+
? tr(`이 문서 자체에는 없고, 위쪽(${res.ancestorRequirementId})에 걸려 있습니다.`, `Not on this document — it is set higher up (${res.ancestorRequirementId}).`)
|
|
1463
|
+
: tr('읽음 확인이 걸려 있지 않습니다.', `No read requirement is set.`),
|
|
1316
1464
|
);
|
|
1317
1465
|
return;
|
|
1318
1466
|
}
|
|
1319
1467
|
const r = res.requirement;
|
|
1320
1468
|
console.log(
|
|
1321
|
-
`${r.done}/${r.total}명
|
|
1322
|
-
` · 마감: ${r.dueAt ?? '없음'}${r.overdue ? ' (지남)' : ''}` +
|
|
1323
|
-
`${r.requireExplicitAck ? ' · 확인 버튼 필요' : ''}`,
|
|
1469
|
+
tr(`${r.done}/${r.total}명 완료`, `${r.done}/${r.total} done`) +
|
|
1470
|
+
tr(` · 마감: ${r.dueAt ?? '없음'}${r.overdue ? ' (지남)' : ''}`, ` · due: ${r.dueAt ?? 'none'}${r.overdue ? ' (overdue)' : ''}`) +
|
|
1471
|
+
tr(`${r.requireExplicitAck ? ' · 확인 버튼 필요' : ''}`, `${r.requireExplicitAck ? ' · acknowledge button required' : ''}`),
|
|
1324
1472
|
);
|
|
1325
1473
|
for (const target of r.targets ?? []) {
|
|
1326
|
-
const mark = target.ackedAt ? '확인' : target.firstViewedAt ? '읽음' : '아직';
|
|
1474
|
+
const mark = target.ackedAt ? tr('확인', `acknowledged`) : target.firstViewedAt ? tr('읽음', `read`) : tr('아직', `not yet`);
|
|
1327
1475
|
// 표·폴더에 걸었으면 "안의 문서를 몇 개 읽었나"가 진짜 진도다.
|
|
1328
1476
|
const progress =
|
|
1329
1477
|
r.childTotal > 0 ? ` ${target.readCount ?? 0}/${r.childTotal}` : '';
|
|
@@ -1355,7 +1503,7 @@ async function cmdDb(args) {
|
|
|
1355
1503
|
if (sub === 'view') return cmdDbView(args);
|
|
1356
1504
|
if (sub !== 'create') {
|
|
1357
1505
|
throw new Error(
|
|
1358
|
-
|
|
1506
|
+
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|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)`)),
|
|
1359
1507
|
);
|
|
1360
1508
|
}
|
|
1361
1509
|
const raw = typeof args.file === 'string' ? readFileSync(args.file, 'utf8') : await readStdin();
|
|
@@ -1363,25 +1511,25 @@ async function cmdDb(args) {
|
|
|
1363
1511
|
try {
|
|
1364
1512
|
spec = JSON.parse(raw);
|
|
1365
1513
|
} catch {
|
|
1366
|
-
throw new Error('스펙을 JSON으로 읽을 수 없습니다. { "name": "...", "properties": [...] } 형태여야 합니다.');
|
|
1514
|
+
throw new Error(tr('스펙을 JSON으로 읽을 수 없습니다. { "name": "...", "properties": [...] } 형태여야 합니다.', `Could not read the spec as JSON. It must look like { "name": "...", "properties": [...] }.`));
|
|
1367
1515
|
}
|
|
1368
|
-
if (!spec.name && !spec.path) throw new Error('스펙에 name 또는 path가 있어야 합니다.');
|
|
1516
|
+
if (!spec.name && !spec.path) throw new Error(tr('스펙에 name 또는 path가 있어야 합니다.', `The spec needs a name or a path.`));
|
|
1369
1517
|
|
|
1370
1518
|
const client = makeClient();
|
|
1371
1519
|
let parentId = spec.parentId;
|
|
1372
1520
|
if (typeof args.parent === 'string') {
|
|
1373
1521
|
const resolved = await resolveTarget(client, args.parent);
|
|
1374
|
-
if (!resolved.existed) throw new Error(`부모를 찾을 수 없습니다: ${args.parent}`);
|
|
1522
|
+
if (!resolved.existed) throw new Error(tr(`부모를 찾을 수 없습니다: ${args.parent}`, `Parent not found: ${args.parent}`));
|
|
1375
1523
|
parentId = resolved.id;
|
|
1376
1524
|
}
|
|
1377
1525
|
const ws = args.ws || client.workspace;
|
|
1378
|
-
if (!ws) throw new Error('워크스페이스를 알 수 없습니다. --ws 를 주거나 rootr use 로 정하세요.');
|
|
1526
|
+
if (!ws) throw new Error(tr('워크스페이스를 알 수 없습니다. --ws 를 주거나 rootr use 로 정하세요.', `No workspace. Pass --ws, or pick one with rootr use.`));
|
|
1379
1527
|
|
|
1380
1528
|
const db = await client.createDatabase(ws, { ...spec, parentId });
|
|
1381
1529
|
const cols = (db.properties || []).map((p) => p.name).join(', ');
|
|
1382
|
-
console.log(`표 생성: ${db.path ?? db.name} (${db.id})`);
|
|
1383
|
-
if (cols) console.log(`컬럼 순서: ${cols}`);
|
|
1384
|
-
if (typeof db.rowCount === 'number') console.log(`행: ${db.rowCount}
|
|
1530
|
+
console.log(tr(`표 생성: ${db.path ?? db.name} (${db.id})`, `Table created: ${db.path ?? db.name} (${db.id})`));
|
|
1531
|
+
if (cols) console.log(tr(`컬럼 순서: ${cols}`, `Column order: ${cols}`));
|
|
1532
|
+
if (typeof db.rowCount === 'number') console.log(tr(`행: ${db.rowCount}개`, `Rows: ${db.rowCount}`));
|
|
1385
1533
|
}
|
|
1386
1534
|
|
|
1387
1535
|
/** 표의 행 목록. --body 를 주면 행마다 상세 내용(본문 메모)까지 가져온다. */
|
|
@@ -1392,7 +1540,7 @@ async function cmdDb(args) {
|
|
|
1392
1540
|
*/
|
|
1393
1541
|
async function cmdDbLock(args) {
|
|
1394
1542
|
const target = args._[1];
|
|
1395
|
-
if (!target) throw new Error(
|
|
1543
|
+
if (!target) throw new Error(usage(tr(`rootr db lock <path|id> [--schema managers|open] [--rows own|all] [--locked "승인,점수"]`, `rootr db lock <path|id> [--schema managers|open] [--rows own|all] [--locked "Approved,Score"]`)));
|
|
1396
1544
|
|
|
1397
1545
|
const client = makeClient();
|
|
1398
1546
|
const id = requireResolved(await resolveTarget(client, target));
|
|
@@ -1415,9 +1563,9 @@ async function cmdDbLock(args) {
|
|
|
1415
1563
|
console.log(JSON.stringify(result, null, 2));
|
|
1416
1564
|
return;
|
|
1417
1565
|
}
|
|
1418
|
-
console.log(`칸·뷰 잠금: ${result.schemaLock === 'managers' ? '관리자만' : '누구나'}`);
|
|
1419
|
-
console.log(`행 보기 : ${result.rowScope === 'own' ? '본인이 만든 행만' : '전부'}`);
|
|
1420
|
-
console.log(`잠긴 칸 : ${(result.lockedProps ?? []).length ? result.lockedProps.join(', ') : '없음'}`);
|
|
1566
|
+
console.log(tr(`칸·뷰 잠금: ${result.schemaLock === 'managers' ? '관리자만' : '누구나'}`, `Columns & views locked: ${result.schemaLock === 'managers' ? 'managers only' : 'anyone'}`));
|
|
1567
|
+
console.log(tr(`행 보기 : ${result.rowScope === 'own' ? '본인이 만든 행만' : '전부'}`, `Row visibility : ${result.rowScope === 'own' ? 'only rows you created' : 'all'}`));
|
|
1568
|
+
console.log(tr(`잠긴 칸 : ${(result.lockedProps ?? []).length ? result.lockedProps.join(', ') : '없음'}`, `Locked columns : ${(result.lockedProps ?? []).length ? result.lockedProps.join(', ') : 'none'}`));
|
|
1421
1569
|
}
|
|
1422
1570
|
|
|
1423
1571
|
/**
|
|
@@ -1428,21 +1576,21 @@ async function cmdDbLock(args) {
|
|
|
1428
1576
|
*/
|
|
1429
1577
|
async function cmdDbNotify(args) {
|
|
1430
1578
|
const target = args._[1];
|
|
1431
|
-
if (!target) throw new Error(
|
|
1579
|
+
if (!target) throw new Error(usage(`rootr db notify <path|id> [--file rules.json] [--clear] [--json]`));
|
|
1432
1580
|
|
|
1433
1581
|
const client = makeClient();
|
|
1434
1582
|
const id = requireResolved(await resolveTarget(client, target));
|
|
1435
1583
|
|
|
1436
1584
|
if (args.clear) {
|
|
1437
1585
|
const cleared = await client.setDatabaseNotifications(id, []);
|
|
1438
|
-
console.log('알림 규칙을 모두 지웠습니다.');
|
|
1586
|
+
console.log(tr('알림 규칙을 모두 지웠습니다.', `All notification rules cleared.`));
|
|
1439
1587
|
if (args.json) console.log(JSON.stringify(cleared, null, 2));
|
|
1440
1588
|
return;
|
|
1441
1589
|
}
|
|
1442
1590
|
if (args.file) {
|
|
1443
1591
|
const raw = readFileSync(args.file, 'utf8');
|
|
1444
1592
|
const saved = await client.setDatabaseNotifications(id, JSON.parse(raw));
|
|
1445
|
-
console.log(`알림 규칙 ${saved.watchers.length}개를
|
|
1593
|
+
console.log(tr(`알림 규칙 ${saved.watchers.length}개를 저장했습니다.`, `Saved ${saved.watchers.length} notification rules.`));
|
|
1446
1594
|
if (args.json) console.log(JSON.stringify(saved, null, 2));
|
|
1447
1595
|
return;
|
|
1448
1596
|
}
|
|
@@ -1453,19 +1601,19 @@ async function cmdDbNotify(args) {
|
|
|
1453
1601
|
return;
|
|
1454
1602
|
}
|
|
1455
1603
|
if (!watchers.length) {
|
|
1456
|
-
console.log('알림 규칙이 없습니다.');
|
|
1604
|
+
console.log(tr('알림 규칙이 없습니다.', `No notification rules.`));
|
|
1457
1605
|
return;
|
|
1458
1606
|
}
|
|
1459
|
-
const WHEN = { created: '행 추가', updated: '행 수정', deleted: '행 삭제' };
|
|
1607
|
+
const WHEN = { created: tr('행 추가', `row added`), updated: tr('행 수정', `row changed`), deleted: tr('행 삭제', `row deleted`) };
|
|
1460
1608
|
for (const w of watchers) {
|
|
1461
1609
|
const when = (w.events ?? []).map((e) => WHEN[e] ?? e).join(' · ');
|
|
1462
1610
|
const f = w.filter;
|
|
1463
1611
|
const only = f
|
|
1464
1612
|
? f.op === 'changed'
|
|
1465
|
-
? ` (${f.propId} 칸이 바뀌었을 때만)`
|
|
1613
|
+
? tr(` (${f.propId} 칸이 바뀌었을 때만)`, ` (only when the ${f.propId} column changes)`)
|
|
1466
1614
|
: ` (${f.propId} ${f.op === 'is' ? '=' : '⊃'} ${f.value})`
|
|
1467
1615
|
: '';
|
|
1468
|
-
console.log(`- ${when}${only} → ${(w.userIds ?? []).length}
|
|
1616
|
+
console.log(tr(`- ${when}${only} → ${(w.userIds ?? []).length}명`, `- ${when}${only} → ${(w.userIds ?? []).length} people`));
|
|
1469
1617
|
}
|
|
1470
1618
|
}
|
|
1471
1619
|
|
|
@@ -1478,7 +1626,7 @@ async function cmdDbNotify(args) {
|
|
|
1478
1626
|
*/
|
|
1479
1627
|
async function cmdDbView(args) {
|
|
1480
1628
|
const target = args._[1];
|
|
1481
|
-
if (!target) throw new Error(
|
|
1629
|
+
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]`)));
|
|
1482
1630
|
|
|
1483
1631
|
const client = makeClient();
|
|
1484
1632
|
const id = await resolveDatabaseId(client, target);
|
|
@@ -1492,12 +1640,12 @@ async function cmdDbView(args) {
|
|
|
1492
1640
|
return;
|
|
1493
1641
|
}
|
|
1494
1642
|
if (!views.length) {
|
|
1495
|
-
console.log('뷰가 없습니다.');
|
|
1643
|
+
console.log(tr('뷰가 없습니다.', `No views.`));
|
|
1496
1644
|
return;
|
|
1497
1645
|
}
|
|
1498
|
-
const SPAN = { week: '주', month: '달', year: '해' };
|
|
1646
|
+
const SPAN = { week: tr('주', `week`), month: tr('달', `month`), year: tr('해', `year`) };
|
|
1499
1647
|
for (const v of views) {
|
|
1500
|
-
const span = v.type === 'calendar' ? ` · ${SPAN[v.config?.calendarScale] ?? '달'} 단위` : '';
|
|
1648
|
+
const span = v.type === 'calendar' ? ` · ${SPAN[v.config?.calendarScale] ?? tr('달', `month`)} 단위` : '';
|
|
1501
1649
|
console.log(`- ${v.name} (${v.type})${span} ${v.id}`);
|
|
1502
1650
|
}
|
|
1503
1651
|
return;
|
|
@@ -1507,13 +1655,13 @@ async function cmdDbView(args) {
|
|
|
1507
1655
|
if (args.scale !== undefined) {
|
|
1508
1656
|
const scale = String(args.scale);
|
|
1509
1657
|
if (!['week', 'month', 'year'].includes(scale)) {
|
|
1510
|
-
throw new Error('--scale 은 week · month · year 중 하나여야 합니다.');
|
|
1658
|
+
throw new Error(tr('--scale 은 week · month · year 중 하나여야 합니다.', `--scale must be one of week · month · year.`));
|
|
1511
1659
|
}
|
|
1512
1660
|
patch.calendarScale = scale;
|
|
1513
1661
|
}
|
|
1514
1662
|
if (typeof args.name === 'string') patch.name = args.name;
|
|
1515
1663
|
if (!Object.keys(patch).length) {
|
|
1516
|
-
throw new Error('바꿀 것을 주세요: --scale week|month|year 또는 --name 새이름');
|
|
1664
|
+
throw new Error(tr('바꿀 것을 주세요: --scale week|month|year 또는 --name 새이름', `Give something to change: --scale week|month|year, or --name <new name>`));
|
|
1517
1665
|
}
|
|
1518
1666
|
|
|
1519
1667
|
const view = await client.updateDatabaseView(id, viewRef, patch);
|
|
@@ -1521,9 +1669,9 @@ async function cmdDbView(args) {
|
|
|
1521
1669
|
console.log(JSON.stringify(view, null, 2));
|
|
1522
1670
|
return;
|
|
1523
1671
|
}
|
|
1524
|
-
const SPAN = { week: '주', month: '달', year: '해' };
|
|
1525
|
-
const span = view.type === 'calendar' ? ` · ${SPAN[view.config?.calendarScale] ?? '달'} 단위` : '';
|
|
1526
|
-
console.log(`뷰를 고쳤습니다: ${view.name} (${view.type})${span}`);
|
|
1672
|
+
const SPAN = { week: tr('주', `week`), month: tr('달', `month`), year: tr('해', `year`) };
|
|
1673
|
+
const span = view.type === 'calendar' ? ` · ${SPAN[view.config?.calendarScale] ?? tr('달', `month`)} 단위` : '';
|
|
1674
|
+
console.log(tr(`뷰를 고쳤습니다: ${view.name} (${view.type})${span}`, `View updated: ${view.name} (${view.type})${span}`));
|
|
1527
1675
|
}
|
|
1528
1676
|
|
|
1529
1677
|
|
|
@@ -1540,14 +1688,14 @@ async function cmdDbView(args) {
|
|
|
1540
1688
|
*/
|
|
1541
1689
|
async function cmdDbButtons(args) {
|
|
1542
1690
|
const target = args._[1];
|
|
1543
|
-
if (!target) throw new Error(
|
|
1691
|
+
if (!target) throw new Error(usage(`rootr db buttons <path|id> [--file buttons.json] [--clear] [--json]`));
|
|
1544
1692
|
|
|
1545
1693
|
const client = makeClient();
|
|
1546
1694
|
const id = await resolveDatabaseId(client, target);
|
|
1547
1695
|
|
|
1548
1696
|
if (args.clear) {
|
|
1549
1697
|
const cleared = await client.setDatabaseButtons(id, []);
|
|
1550
|
-
console.log('버튼을 모두 지웠습니다.');
|
|
1698
|
+
console.log(tr('버튼을 모두 지웠습니다.', `All buttons cleared.`));
|
|
1551
1699
|
if (args.json) console.log(JSON.stringify(cleared, null, 2));
|
|
1552
1700
|
return;
|
|
1553
1701
|
}
|
|
@@ -1557,11 +1705,11 @@ async function cmdDbButtons(args) {
|
|
|
1557
1705
|
try {
|
|
1558
1706
|
buttons = JSON.parse(raw);
|
|
1559
1707
|
} catch {
|
|
1560
|
-
throw new Error('버튼 파일을 JSON으로 읽을 수 없습니다. [{ "label": "...", "steps": [...] }] 형태여야 합니다.');
|
|
1708
|
+
throw new Error(tr('버튼 파일을 JSON으로 읽을 수 없습니다. [{ "label": "...", "steps": [...] }] 형태여야 합니다.', `Could not read the button file as JSON. It must look like [{ "label": "...", "steps": [...] }].`));
|
|
1561
1709
|
}
|
|
1562
|
-
if (!Array.isArray(buttons)) throw new Error('버튼 파일은 배열이어야 합니다(버튼 목록 전체).');
|
|
1710
|
+
if (!Array.isArray(buttons)) throw new Error(tr('버튼 파일은 배열이어야 합니다(버튼 목록 전체).', `The button file must be an array (the whole button list).`));
|
|
1563
1711
|
const saved = await client.setDatabaseButtons(id, buttons);
|
|
1564
|
-
console.log(`버튼 ${saved.buttons.length}개를
|
|
1712
|
+
console.log(tr(`버튼 ${saved.buttons.length}개를 저장했습니다.`, `Saved ${saved.buttons.length} buttons.`));
|
|
1565
1713
|
if (args.json) console.log(JSON.stringify(saved, null, 2));
|
|
1566
1714
|
return;
|
|
1567
1715
|
}
|
|
@@ -1572,16 +1720,16 @@ async function cmdDbButtons(args) {
|
|
|
1572
1720
|
return;
|
|
1573
1721
|
}
|
|
1574
1722
|
if (!buttons.length) {
|
|
1575
|
-
console.log('버튼이 없습니다.');
|
|
1723
|
+
console.log(tr('버튼이 없습니다.', `No buttons.`));
|
|
1576
1724
|
return;
|
|
1577
1725
|
}
|
|
1578
1726
|
const WHAT = {
|
|
1579
|
-
add_row: '행 만들기',
|
|
1580
|
-
update_rows: '값 바꾸기',
|
|
1581
|
-
ai_fill: 'AI로 칸 채우기',
|
|
1582
|
-
open_row: '만든 행 열기',
|
|
1727
|
+
add_row: tr('행 만들기', `create rows`),
|
|
1728
|
+
update_rows: tr('값 바꾸기', `change values`),
|
|
1729
|
+
ai_fill: tr('AI로 칸 채우기', `fill a column with AI`),
|
|
1730
|
+
open_row: tr('만든 행 열기', `open the created row`),
|
|
1583
1731
|
};
|
|
1584
|
-
const WHERE = { created: '방금 만든 행', selected: '고른 행' };
|
|
1732
|
+
const WHERE = { created: tr('방금 만든 행', `the row just created`), selected: tr('고른 행', `the chosen rows`) };
|
|
1585
1733
|
for (const b of buttons) {
|
|
1586
1734
|
console.log(`${b.icon ? b.icon + ' ' : ''}${b.label} (${b.id})`);
|
|
1587
1735
|
for (const [i, st] of (b.steps ?? []).entries()) {
|
|
@@ -1589,7 +1737,7 @@ async function cmdDbButtons(args) {
|
|
|
1589
1737
|
const where = st.target ? ` · ${WHERE[st.target] ?? st.target}` : '';
|
|
1590
1738
|
const detail =
|
|
1591
1739
|
st.kind === 'ai_fill'
|
|
1592
|
-
? ` · ${st.propId} 칸 ← "${st.prompt}"`
|
|
1740
|
+
? tr(` · ${st.propId} 칸 ← "${st.prompt}"`, ` · column ${st.propId} ← "${st.prompt}"`)
|
|
1593
1741
|
: st.values
|
|
1594
1742
|
? ' · ' + Object.entries(st.values).map(([k, v]) => `${k}=${v}`).join(', ')
|
|
1595
1743
|
: '';
|
|
@@ -1597,7 +1745,7 @@ async function cmdDbButtons(args) {
|
|
|
1597
1745
|
}
|
|
1598
1746
|
}
|
|
1599
1747
|
if (myLevel && myLevel !== 'MANAGE' && myLevel !== 'FULL') {
|
|
1600
|
-
console.log('\n(버튼을 고치려면 이 표의 관리 권한이 필요합니다. 누르는 것은 지금 권한으로도 됩니다.)');
|
|
1748
|
+
console.log(tr('\n(버튼을 고치려면 이 표의 관리 권한이 필요합니다. 누르는 것은 지금 권한으로도 됩니다.)', `\n(Changing buttons needs MANAGE on this table. Pressing them works with your current permission.)`));
|
|
1601
1749
|
}
|
|
1602
1750
|
}
|
|
1603
1751
|
|
|
@@ -1611,7 +1759,7 @@ async function cmdDbButtonRun(args) {
|
|
|
1611
1759
|
const target = args._[1];
|
|
1612
1760
|
const buttonId = args._[2];
|
|
1613
1761
|
if (!target || !buttonId) {
|
|
1614
|
-
throw new Error(
|
|
1762
|
+
throw new Error(usage(`rootr db button-run <path|id> <buttonId> [--rows id1,id2] [--json]`));
|
|
1615
1763
|
}
|
|
1616
1764
|
const client = makeClient();
|
|
1617
1765
|
const id = await resolveDatabaseId(client, target);
|
|
@@ -1628,19 +1776,25 @@ async function cmdDbButtonRun(args) {
|
|
|
1628
1776
|
// created 는 **만든 행의 id 목록**이다(개수가 아니다) — 세어서 보여 준다.
|
|
1629
1777
|
const madeCount = Array.isArray(r.created) ? r.created.length : (r.created ?? 0);
|
|
1630
1778
|
const parts = [];
|
|
1631
|
-
if (madeCount) parts.push(`행 ${madeCount}개
|
|
1632
|
-
if (r.updated) parts.push(`행 ${r.updated}개
|
|
1633
|
-
if (r.filled) parts.push(`AI로 칸 ${r.filled}개
|
|
1634
|
-
console.log(parts.length ? parts.join(' · ') : '바뀐 것이 없습니다.');
|
|
1635
|
-
if (r.openRowId) console.log(`열 행: ${r.openRowId}`);
|
|
1779
|
+
if (madeCount) parts.push(tr(`행 ${madeCount}개 만듦`, `${madeCount} rows created`));
|
|
1780
|
+
if (r.updated) parts.push(tr(`행 ${r.updated}개 고침`, `${r.updated} rows changed`));
|
|
1781
|
+
if (r.filled) parts.push(tr(`AI로 칸 ${r.filled}개 채움`, `${r.filled} columns filled by AI`));
|
|
1782
|
+
console.log(parts.length ? parts.join(' · ') : tr('바뀐 것이 없습니다.', `Nothing changed.`));
|
|
1783
|
+
if (r.openRowId) console.log(tr(`열 행: ${r.openRowId}`, `Row to open: ${r.openRowId}`));
|
|
1636
1784
|
// AI가 못 채워도 버튼은 계속 돈다 — 조용히 넘어가면 사람이 왜 빈칸인지 모른다.
|
|
1637
|
-
if (r.aiFailed)
|
|
1785
|
+
if (r.aiFailed)
|
|
1786
|
+
console.log(
|
|
1787
|
+
tr(
|
|
1788
|
+
`AI가 못 채운 칸 ${r.aiFailed}개${r.aiError ? ` — ${r.aiError}` : ''}`,
|
|
1789
|
+
`${r.aiFailed} columns the AI could not fill${r.aiError ? ` — ${r.aiError}` : ''}`,
|
|
1790
|
+
),
|
|
1791
|
+
);
|
|
1638
1792
|
}
|
|
1639
1793
|
|
|
1640
1794
|
|
|
1641
1795
|
async function cmdDbRows(args) {
|
|
1642
1796
|
const target = args._[1];
|
|
1643
|
-
if (!target) throw new Error(
|
|
1797
|
+
if (!target) throw new Error(usage(`rootr db rows <path|id> [--body] [--backlinks] [--json]`));
|
|
1644
1798
|
const client = makeClient();
|
|
1645
1799
|
const id = await resolveDatabaseId(client, target);
|
|
1646
1800
|
const rows = await client.listRows(id, { withBody: !!args.body, backlinks: !!args.backlinks });
|
|
@@ -1669,13 +1823,13 @@ async function cmdDbRows(args) {
|
|
|
1669
1823
|
async function cmdDbRowAdd(args) {
|
|
1670
1824
|
const target = args._[1];
|
|
1671
1825
|
if (!target || typeof args.values !== 'string') {
|
|
1672
|
-
throw new Error("사용법: rootr db row-add <path|id> --values '{\"제목\":\"...\"}' [--body <md>|--body-file <f>]");
|
|
1826
|
+
throw new Error("사용법: rootr db row-add <path|id> --values tr('{\"제목\":\"...\"}', `{\"Title\":\"...\"}`) [--body <md>|--body-file <f>]");
|
|
1673
1827
|
}
|
|
1674
1828
|
let values;
|
|
1675
1829
|
try {
|
|
1676
1830
|
values = JSON.parse(args.values);
|
|
1677
1831
|
} catch {
|
|
1678
|
-
throw new Error('--values 는 JSON이어야 합니다. 예: --values \'{"제목":"킥오프"}\'');
|
|
1832
|
+
throw new Error(tr('--values 는 JSON이어야 합니다. 예: --values \'{"제목":"킥오프"}\'', `--values must be JSON. Example: --values '{"Title":"Kickoff"}'`));
|
|
1679
1833
|
}
|
|
1680
1834
|
const body =
|
|
1681
1835
|
typeof args['body-file'] === 'string'
|
|
@@ -1686,8 +1840,8 @@ async function cmdDbRowAdd(args) {
|
|
|
1686
1840
|
const client = makeClient();
|
|
1687
1841
|
const id = await resolveDatabaseId(client, target);
|
|
1688
1842
|
const row = await client.createRow(id, { values, body });
|
|
1689
|
-
console.log(`행 추가: ${row.id}`);
|
|
1690
|
-
if (row.pageNodeId) console.log(`상세 내용 문서: ${row.pageNodeId}`);
|
|
1843
|
+
console.log(tr(`행 추가: ${row.id}`, `Row added: ${row.id}`));
|
|
1844
|
+
if (row.pageNodeId) console.log(tr(`상세 내용 문서: ${row.pageNodeId}`, `Row body document: ${row.pageNodeId}`));
|
|
1691
1845
|
}
|
|
1692
1846
|
|
|
1693
1847
|
/** 행의 상세 내용(본문 메모)을 통째로 쓴다. 내용은 인자·--file·표준입력 순으로 받는다. */
|
|
@@ -1695,7 +1849,7 @@ async function cmdDbRowBody(args) {
|
|
|
1695
1849
|
const target = args._[1];
|
|
1696
1850
|
const rowId = args._[2];
|
|
1697
1851
|
if (!target || !rowId) {
|
|
1698
|
-
throw new Error(
|
|
1852
|
+
throw new Error(usage(tr(`rootr db row-body <path|id> <rowId> [<본문>] [--file <f>]`, `rootr db row-body <path|id> <rowId> [<body>] [--file <f>]`)));
|
|
1699
1853
|
}
|
|
1700
1854
|
const body =
|
|
1701
1855
|
typeof args.file === 'string'
|
|
@@ -1703,11 +1857,16 @@ async function cmdDbRowBody(args) {
|
|
|
1703
1857
|
: args._[3] !== undefined
|
|
1704
1858
|
? String(args._[3])
|
|
1705
1859
|
: await readStdin();
|
|
1706
|
-
if (!body) throw new Error('쓸 내용이 없습니다 (본문 인자, --file, 또는 표준입력 필요).');
|
|
1860
|
+
if (!body) throw new Error(tr('쓸 내용이 없습니다 (본문 인자, --file, 또는 표준입력 필요).', `Nothing to write (needs a body argument, --file, or stdin).`));
|
|
1707
1861
|
const client = makeClient();
|
|
1708
1862
|
const id = await resolveDatabaseId(client, target);
|
|
1709
1863
|
const row = await client.updateRow(id, rowId, { body });
|
|
1710
|
-
console.log(
|
|
1864
|
+
console.log(
|
|
1865
|
+
tr(
|
|
1866
|
+
`상세 내용 저장: 행 ${rowId}${row.pageNodeId ? ` → 문서 ${row.pageNodeId}` : ''}`,
|
|
1867
|
+
`Row body saved: row ${rowId}${row.pageNodeId ? ` → document ${row.pageNodeId}` : ''}`,
|
|
1868
|
+
),
|
|
1869
|
+
);
|
|
1711
1870
|
}
|
|
1712
1871
|
|
|
1713
1872
|
/**
|
|
@@ -1720,7 +1879,7 @@ async function cmdDbRowPage(args) {
|
|
|
1720
1879
|
const rowId = args._[1];
|
|
1721
1880
|
if (!rowId) {
|
|
1722
1881
|
throw new Error(
|
|
1723
|
-
|
|
1882
|
+
usage(`rootr db row-page <rowId> [--type RECORDING | --node <path|id> | --unlink]`),
|
|
1724
1883
|
);
|
|
1725
1884
|
}
|
|
1726
1885
|
const client = makeClient();
|
|
@@ -1728,28 +1887,28 @@ async function cmdDbRowPage(args) {
|
|
|
1728
1887
|
// ROW-PAGE-LINK — 연결만 끊는다. 문서는 그대로 남는다.
|
|
1729
1888
|
if (args.unlink) {
|
|
1730
1889
|
const res = await client.unlinkRowPage(rowId);
|
|
1731
|
-
console.log(res.unlinked ? `연결을 끊었습니다(문서 ${res.unlinked}는 그대로 있습니다).` : '붙어 있는 문서가 없습니다.');
|
|
1890
|
+
console.log(res.unlinked ? tr(`연결을 끊었습니다(문서 ${res.unlinked}는 그대로 있습니다).`, `Unlinked (document ${res.unlinked} is still there).`) : tr('붙어 있는 문서가 없습니다.', `No document is attached.`));
|
|
1732
1891
|
return;
|
|
1733
1892
|
}
|
|
1734
1893
|
|
|
1735
1894
|
// ROW-PAGE-LINK — 이미 있는 문서를 잇는다. 경로로 줘도 된다.
|
|
1736
1895
|
if (typeof args.node === 'string') {
|
|
1737
1896
|
if (typeof args.type === 'string') {
|
|
1738
|
-
throw new Error('--type 과 --node 는 같이 못 씁니다. 새로 만들려면 --type, 있는 문서를 이으려면 --node 하나만 주세요.');
|
|
1897
|
+
throw new Error(tr('--type 과 --node 는 같이 못 씁니다. 새로 만들려면 --type, 있는 문서를 이으려면 --node 하나만 주세요.', `--type and --node cannot be used together. Use --type to create one, or --node to attach an existing document.`));
|
|
1739
1898
|
}
|
|
1740
1899
|
const resolved = await resolveTarget(client, args.node);
|
|
1741
|
-
if (!resolved.existed) throw new Error(`이을 문서를 찾을 수 없습니다: ${args.node}`);
|
|
1900
|
+
if (!resolved.existed) throw new Error(tr(`이을 문서를 찾을 수 없습니다: ${args.node}`, `Could not find the document to attach: ${args.node}`));
|
|
1742
1901
|
const linked = await client.linkRowPage(rowId, resolved.id);
|
|
1743
|
-
console.log(`행 ${rowId}에 이었습니다: ${linked.type ?? ''} ${linked.nodeId}
|
|
1902
|
+
console.log(tr(`행 ${rowId}에 이었습니다: ${linked.type ?? ''} ${linked.nodeId}`, `Attached to row ${rowId}: ${linked.type ?? ''} ${linked.nodeId}`).trim());
|
|
1744
1903
|
return;
|
|
1745
1904
|
}
|
|
1746
1905
|
|
|
1747
1906
|
const type = typeof args.type === 'string' ? args.type.toUpperCase() : undefined;
|
|
1748
1907
|
if (type && !ROW_PAGE_TYPES.includes(type)) {
|
|
1749
|
-
throw new Error(`--type 은 ${ROW_PAGE_TYPES.join(' · ')} 중 하나여야
|
|
1908
|
+
throw new Error(tr(`--type 은 ${ROW_PAGE_TYPES.join(' · ')} 중 하나여야 합니다.`, `--type must be one of ${ROW_PAGE_TYPES.join(' · ')}.`));
|
|
1750
1909
|
}
|
|
1751
1910
|
const page = await client.createRowPage(rowId, type);
|
|
1752
|
-
console.log(`행 ${rowId} 상세: ${page.type ?? ''} ${page.nodeId}
|
|
1911
|
+
console.log(tr(`행 ${rowId} 상세: ${page.type ?? ''} ${page.nodeId}`, `Row ${rowId} detail: ${page.type ?? ''} ${page.nodeId}`).trim());
|
|
1753
1912
|
}
|
|
1754
1913
|
|
|
1755
1914
|
/** `rootr db row-page-type <path|id> <TYPE>` — 새 행의 기본값을 바꾼다. */
|
|
@@ -1757,22 +1916,22 @@ async function cmdDbRowPageType(args) {
|
|
|
1757
1916
|
const target = args._[1];
|
|
1758
1917
|
const type = typeof args._[2] === 'string' ? String(args._[2]).toUpperCase() : undefined;
|
|
1759
1918
|
if (!target || !type) {
|
|
1760
|
-
throw new Error(
|
|
1919
|
+
throw new Error(usage(`rootr db row-page-type <path|id> <${ROW_PAGE_TYPES.join('|')}>`));
|
|
1761
1920
|
}
|
|
1762
1921
|
if (!ROW_PAGE_TYPES.includes(type)) {
|
|
1763
|
-
throw new Error(`타입은 ${ROW_PAGE_TYPES.join(' · ')} 중 하나여야
|
|
1922
|
+
throw new Error(tr(`타입은 ${ROW_PAGE_TYPES.join(' · ')} 중 하나여야 합니다.`, `The type must be one of ${ROW_PAGE_TYPES.join(' · ')}.`));
|
|
1764
1923
|
}
|
|
1765
1924
|
const client = makeClient();
|
|
1766
1925
|
const id = await resolveDatabaseId(client, target);
|
|
1767
1926
|
await client.setDatabaseRowPageType(id, type);
|
|
1768
|
-
console.log(`새 행 기본값: ${type} — 이미 만들어진 행은
|
|
1927
|
+
console.log(tr(`새 행 기본값: ${type} — 이미 만들어진 행은 그대로입니다.`, `Default for new rows: ${type} — existing rows are untouched.`));
|
|
1769
1928
|
}
|
|
1770
1929
|
|
|
1771
1930
|
/** 경로로 줘도 되게 — 표 인자도 문서 도구들과 같은 규칙을 따른다. */
|
|
1772
1931
|
async function resolveDatabaseId(client, target) {
|
|
1773
1932
|
if (typeof target === 'string' && target.startsWith('/')) {
|
|
1774
1933
|
const resolved = await resolveTarget(client, target);
|
|
1775
|
-
if (!resolved.existed) throw new Error(`표를 찾을 수 없습니다: ${target}`);
|
|
1934
|
+
if (!resolved.existed) throw new Error(tr(`표를 찾을 수 없습니다: ${target}`, `Table not found: ${target}`));
|
|
1776
1935
|
return resolved.id;
|
|
1777
1936
|
}
|
|
1778
1937
|
return target;
|
|
@@ -1786,12 +1945,12 @@ async function resolveDatabaseId(client, target) {
|
|
|
1786
1945
|
// 읽은 값을 `--code`로 넘긴다.
|
|
1787
1946
|
|
|
1788
1947
|
const SIGN_USAGE =
|
|
1789
|
-
|
|
1790
|
-
' (자세한 건 rootr --help)';
|
|
1948
|
+
usage(`rootr sign new|show|line|code|submit|approve|reject|reopen|revise|verify|list|bulk|ledger …\n`) +
|
|
1949
|
+
tr(' (자세한 건 rootr --help)', ` (see rootr --help)`);
|
|
1791
1950
|
|
|
1792
1951
|
function signWorkspace(client, args) {
|
|
1793
1952
|
const ws = args.ws || client.workspace;
|
|
1794
|
-
if (!ws) throw new Error('워크스페이스를 알 수 없습니다. --ws 를 주거나 rootr use 로 정하세요.');
|
|
1953
|
+
if (!ws) throw new Error(tr('워크스페이스를 알 수 없습니다. --ws 를 주거나 rootr use 로 정하세요.', `No workspace. Pass --ws, or pick one with rootr use.`));
|
|
1795
1954
|
return ws;
|
|
1796
1955
|
}
|
|
1797
1956
|
|
|
@@ -1804,49 +1963,60 @@ async function resolveSignedId(client, target) {
|
|
|
1804
1963
|
function requireCode(args, what) {
|
|
1805
1964
|
if (typeof args.code !== 'string' || !args.code.trim()) {
|
|
1806
1965
|
throw new Error(
|
|
1807
|
-
`${what}에는 확인 코드가 필요합니다: 먼저 rootr sign code <path|id> 로 메일을 받고 --code <6자리> 로 넘기세요.`,
|
|
1966
|
+
tr(`${what}에는 확인 코드가 필요합니다: 먼저 rootr sign code <path|id> 로 메일을 받고 --code <6자리> 로 넘기세요.`, `${what} needs a confirmation code: run rootr sign code <path|id> to get one by email, then pass --code <6 digits>.`),
|
|
1808
1967
|
);
|
|
1809
1968
|
}
|
|
1810
1969
|
return args.code.trim();
|
|
1811
1970
|
}
|
|
1812
1971
|
|
|
1813
1972
|
const SIGN_STATUS_KO = {
|
|
1814
|
-
DRAFT: '작성 중',
|
|
1815
|
-
IN_PROGRESS: '서명 진행 중',
|
|
1816
|
-
COMPLETED: '완료',
|
|
1817
|
-
REJECTED: '반려됨',
|
|
1818
|
-
SUPERSEDED: '개정되어 대체됨',
|
|
1819
|
-
VOIDED: '무효',
|
|
1973
|
+
DRAFT: tr('작성 중', `draft`),
|
|
1974
|
+
IN_PROGRESS: tr('서명 진행 중', `signing in progress`),
|
|
1975
|
+
COMPLETED: tr('완료', `complete`),
|
|
1976
|
+
REJECTED: tr('반려됨', `rejected`),
|
|
1977
|
+
SUPERSEDED: tr('개정되어 대체됨', `superseded by a revision`),
|
|
1978
|
+
VOIDED: tr('무효', `void`),
|
|
1820
1979
|
};
|
|
1821
1980
|
|
|
1822
1981
|
function printSignedState(st) {
|
|
1823
|
-
console.log(`${st.name} [${SIGN_STATUS_KO[st.status] ?? st.status}]${st.mode === 'RESEARCH_NOTE' ? ' (연구노트)' : ''}`);
|
|
1824
|
-
if (st.revisionNo)
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1982
|
+
console.log(tr(`${st.name} [${SIGN_STATUS_KO[st.status] ?? st.status}]${st.mode === 'RESEARCH_NOTE' ? ' (연구노트)' : ''}`, `${st.name} [${SIGN_STATUS_KO[st.status] ?? st.status}]${st.mode === 'RESEARCH_NOTE' ? ' (research note)' : ''}`));
|
|
1983
|
+
if (st.revisionNo)
|
|
1984
|
+
console.log(
|
|
1985
|
+
tr(
|
|
1986
|
+
`개정 ${st.revisionNo}판${st.revisionNote ? ` — ${st.revisionNote}` : ''}`,
|
|
1987
|
+
`revision ${st.revisionNo}${st.revisionNote ? ` — ${st.revisionNote}` : ''}`,
|
|
1988
|
+
),
|
|
1989
|
+
);
|
|
1990
|
+
if (st.latestRevisionId) console.log(tr(`※ 더 최신 판이 있습니다: ${st.latestRevisionId} (${st.latestRevisionNo}판)`, `※ A newer revision exists: ${st.latestRevisionId} (rev ${st.latestRevisionNo})`));
|
|
1991
|
+
console.log(st.lockedAt ? tr(`잠김: ${st.lockedAt} (본문·결재라인 수정 불가)`, `Frozen: ${st.lockedAt} (body and approval line can no longer change)`) : tr('잠기지 않음 (아직 고칠 수 있음)', `Not frozen (still editable)`));
|
|
1992
|
+
if (!st.contentIntact) console.log(tr('⚠️ 본문이 서명 당시와 다릅니다 — rootr sign verify 로 확인하세요.', `⚠️ The body differs from what was signed — check it with rootr sign verify.`));
|
|
1828
1993
|
for (const step of st.steps ?? []) {
|
|
1829
|
-
const here = step.order === st.currentStepOrder ? ' ← 지금 이 차례' : '';
|
|
1830
|
-
const rule = step.rule === 'ANY' ? '아무나 1명' : step.rule === 'N_OF_M' ? `${step.requiredCount}
|
|
1994
|
+
const here = step.order === st.currentStepOrder ? tr(' ← 지금 이 차례', ` ← your turn`) : '';
|
|
1995
|
+
const rule = step.rule === 'ANY' ? tr('아무나 1명', `any 1 person`) : step.rule === 'N_OF_M' ? tr(`${step.requiredCount}명`, `${step.requiredCount} people`) : tr('전원', `everyone`);
|
|
1831
1996
|
console.log(`\n${step.order}. ${step.label} (${rule})${here}`);
|
|
1832
1997
|
for (const m of step.members ?? []) {
|
|
1833
1998
|
// 메일 결재자(SIGN-002)는 userId가 없다. 누구인지 화면에 보여야 하므로
|
|
1834
1999
|
// 이름·주소로 대신 적고, 밖의 사람이라는 표시를 붙인다.
|
|
1835
|
-
const who =
|
|
1836
|
-
|
|
2000
|
+
const who =
|
|
2001
|
+
m.userId ??
|
|
2002
|
+
tr(
|
|
2003
|
+
`${m.displayName ? `${m.displayName} <${m.email}>` : m.email} (메일 서명)`,
|
|
2004
|
+
`${m.displayName ? `${m.displayName} <${m.email}>` : m.email} (signs by email)`,
|
|
2005
|
+
);
|
|
2006
|
+
const sent = !m.userId && !m.invitedAt && m.state === 'PENDING' ? tr(' [요청 메일 아직 안 감]', ` [request email not sent yet]`) : '';
|
|
1837
2007
|
console.log(` - ${who} ${m.role} ${m.state}${m.actedAt ? ` ${m.actedAt}` : ''}${sent}${m.comment ? ` "${m.comment}"` : ''}`);
|
|
1838
2008
|
}
|
|
1839
2009
|
}
|
|
1840
2010
|
if (st.signatures?.length) {
|
|
1841
|
-
console.log('\n서명:');
|
|
2011
|
+
console.log(tr('\n서명:', `\nSignatures:`));
|
|
1842
2012
|
for (const sig of st.signatures) {
|
|
1843
2013
|
console.log(
|
|
1844
2014
|
` #${sig.seq} ${sig.signerName} <${sig.signerEmail}> ${sig.role} ${sig.signedAt}` +
|
|
1845
|
-
(sig.timestampPending ? ' [시점확인 대기중]' : ` [시점확인 ${sig.tsTime} · ${sig.tsaName}]`),
|
|
2015
|
+
(sig.timestampPending ? tr(' [시점확인 대기중]', ` [timestamp pending]`) : tr(` [시점확인 ${sig.tsTime} · ${sig.tsaName}]`, ` [timestamped ${sig.tsTime} · ${sig.tsaName}]`)),
|
|
1846
2016
|
);
|
|
1847
2017
|
}
|
|
1848
2018
|
}
|
|
1849
|
-
if (st.canSign) console.log('\n→ 지금 서명할 수 있습니다: rootr sign code <id> 후 rootr sign approve <id> --code <6자리>');
|
|
2019
|
+
if (st.canSign) console.log(tr('\n→ 지금 서명할 수 있습니다: rootr sign code <id> 후 rootr sign approve <id> --code <6자리>', `\n→ You can sign now: rootr sign code <id>, then rootr sign approve <id> --code <6 digits>`));
|
|
1850
2020
|
}
|
|
1851
2021
|
|
|
1852
2022
|
async function cmdSign(args) {
|
|
@@ -1856,7 +2026,7 @@ async function cmdSign(args) {
|
|
|
1856
2026
|
switch (sub) {
|
|
1857
2027
|
case 'new': {
|
|
1858
2028
|
const target = args._[1];
|
|
1859
|
-
if (!target) throw new Error(
|
|
2029
|
+
if (!target) throw new Error(usage(tr(`rootr sign new <경로> [--file <본문.md>] [--reviewer <userId>] [--line <line.json>]`, `rootr sign new <path> [--file <body.md>] [--reviewer <userId>] [--line <line.json>]`)));
|
|
1860
2030
|
const ws = signWorkspace(client, args);
|
|
1861
2031
|
const content =
|
|
1862
2032
|
typeof args.file === 'string'
|
|
@@ -1878,9 +2048,9 @@ async function cmdSign(args) {
|
|
|
1878
2048
|
if (target.startsWith('/')) body.path = target;
|
|
1879
2049
|
else body.name = target;
|
|
1880
2050
|
const st = await client.createSignedDoc(ws, body);
|
|
1881
|
-
console.log(`서명문서 생성: ${st.path ?? st.name} (${st.nodeId})`);
|
|
2051
|
+
console.log(tr(`서명문서 생성: ${st.path ?? st.name} (${st.nodeId})`, `Signed document created: ${st.path ?? st.name} (${st.nodeId})`));
|
|
1882
2052
|
if (!st.steps?.length) {
|
|
1883
|
-
console.log('결재라인이 아직 없습니다 — rootr sign line <id> --file <line.json> 으로 짜세요.');
|
|
2053
|
+
console.log(tr('결재라인이 아직 없습니다 — rootr sign line <id> --file <line.json> 으로 짜세요.', `No approval line yet — build one with rootr sign line <id> --file <line.json>.`));
|
|
1884
2054
|
} else {
|
|
1885
2055
|
printSignedState(st);
|
|
1886
2056
|
}
|
|
@@ -1894,22 +2064,22 @@ async function cmdSign(args) {
|
|
|
1894
2064
|
case 'line': {
|
|
1895
2065
|
const id = await resolveSignedId(client, args._[1]);
|
|
1896
2066
|
const raw = typeof args.file === 'string' ? readFileSync(args.file, 'utf8') : await readStdin();
|
|
1897
|
-
if (!raw) throw new Error('결재라인 JSON이 없습니다 (--file 또는 표준입력).');
|
|
2067
|
+
if (!raw) throw new Error(tr('결재라인 JSON이 없습니다 (--file 또는 표준입력).', `No approval-line JSON (use --file or stdin).`));
|
|
1898
2068
|
const parsed = JSON.parse(raw);
|
|
1899
2069
|
const steps = Array.isArray(parsed) ? parsed : parsed.steps;
|
|
1900
2070
|
const st = await client.setApprovalLine(id, steps);
|
|
1901
|
-
console.log(`결재라인 ${st.steps.length}단계
|
|
2071
|
+
console.log(tr(`결재라인 ${st.steps.length}단계 저장됨.`, `Approval line saved: ${st.steps.length} steps.`));
|
|
1902
2072
|
return printSignedState(st);
|
|
1903
2073
|
}
|
|
1904
2074
|
case 'code': {
|
|
1905
2075
|
if (args.bulk) {
|
|
1906
2076
|
const ws = signWorkspace(client, args);
|
|
1907
2077
|
const out = await client.requestBulkSigningCode(ws);
|
|
1908
|
-
console.log(`묶음 서명용 확인 코드를 ${out.sentTo ?? '메일'}로 보냈습니다. 10분 안에 --code 로
|
|
2078
|
+
console.log(tr(`묶음 서명용 확인 코드를 ${out.sentTo ?? '메일'}로 보냈습니다. 10분 안에 --code 로 넘기세요.`, `A confirmation code for bulk signing was sent to ${out.sentTo ?? 'your email'}. Pass it with --code within 10 minutes.`));
|
|
1909
2079
|
return;
|
|
1910
2080
|
}
|
|
1911
2081
|
const out = await client.requestSigningCode(await resolveSignedId(client, args._[1]));
|
|
1912
|
-
console.log(`확인 코드를 ${out.sentTo ?? '메일'}로 보냈습니다. 10분 안에 --code 로
|
|
2082
|
+
console.log(tr(`확인 코드를 ${out.sentTo ?? '메일'}로 보냈습니다. 10분 안에 --code 로 넘기세요.`, `A confirmation code was sent to ${out.sentTo ?? 'your email'}. Pass it with --code within 10 minutes.`));
|
|
1913
2083
|
return;
|
|
1914
2084
|
}
|
|
1915
2085
|
case 'submit': {
|
|
@@ -1925,47 +2095,47 @@ async function cmdSign(args) {
|
|
|
1925
2095
|
} catch (err) {
|
|
1926
2096
|
if (String(err?.body?.code ?? '') === 'REAUTH_REQUIRED') {
|
|
1927
2097
|
throw new Error(
|
|
1928
|
-
'올리는 분이 1단계 서명자라 첫 서명이 함께 찍힙니다. ' +
|
|
1929
|
-
`먼저 "rootr sign code ${args._[1]}" 로 메일을 받고 --code <6자리> 로 다시 넘기세요.`,
|
|
2098
|
+
tr('올리는 분이 1단계 서명자라 첫 서명이 함께 찍힙니다. ', `You are the first signer, so your signature is applied together with the submission. `) +
|
|
2099
|
+
tr(`먼저 "rootr sign code ${args._[1]}" 로 메일을 받고 --code <6자리> 로 다시 넘기세요.`, `Run "rootr sign code ${args._[1]}" first to get the email, then pass --code <6 digits>.`),
|
|
1930
2100
|
);
|
|
1931
2101
|
}
|
|
1932
2102
|
throw err;
|
|
1933
2103
|
}
|
|
1934
2104
|
console.log(
|
|
1935
2105
|
code
|
|
1936
|
-
? '올렸습니다 — 본문이 얼었고 기안자 서명이 찍혔습니다.'
|
|
1937
|
-
: '올렸습니다 — 본문이 얼었고 첫 단계 서명자에게 차례가 넘어갔습니다.',
|
|
2106
|
+
? tr('올렸습니다 — 본문이 얼었고 기안자 서명이 찍혔습니다.', `Submitted — the body is frozen and the author's signature is on it.`)
|
|
2107
|
+
: tr('올렸습니다 — 본문이 얼었고 첫 단계 서명자에게 차례가 넘어갔습니다.', `Submitted — the body is frozen and it is now the first signer's turn.`),
|
|
1938
2108
|
);
|
|
1939
2109
|
return printSignedState(st);
|
|
1940
2110
|
}
|
|
1941
2111
|
case 'approve':
|
|
1942
2112
|
case 'sign': {
|
|
1943
2113
|
const id = await resolveSignedId(client, args._[1]);
|
|
1944
|
-
const st = await client.signSignedDoc(id, { code: requireCode(args, '서명') });
|
|
1945
|
-
console.log('서명했습니다.');
|
|
2114
|
+
const st = await client.signSignedDoc(id, { code: requireCode(args, tr('서명', `signing`)) });
|
|
2115
|
+
console.log(tr('서명했습니다.', `Signed.`));
|
|
1946
2116
|
return printSignedState(st);
|
|
1947
2117
|
}
|
|
1948
2118
|
case 'reject': {
|
|
1949
2119
|
const id = await resolveSignedId(client, args._[1]);
|
|
1950
2120
|
if (typeof args.comment !== 'string' || !args.comment.trim()) {
|
|
1951
|
-
throw new Error('반려에는 사유가 필요합니다: --comment "근거 자료가 빠졌습니다"');
|
|
2121
|
+
throw new Error(tr('반려에는 사유가 필요합니다: --comment "근거 자료가 빠졌습니다"', `Rejecting needs a reason: --comment "supporting data is missing"`));
|
|
1952
2122
|
}
|
|
1953
2123
|
const st = await client.rejectSignedDoc(id, { comment: args.comment, code: args.code });
|
|
1954
|
-
console.log('반려했습니다 — 작성자가 다시 고칠 수 있습니다.');
|
|
2124
|
+
console.log(tr('반려했습니다 — 작성자가 다시 고칠 수 있습니다.', `Rejected — the author can revise it.`));
|
|
1955
2125
|
return printSignedState(st);
|
|
1956
2126
|
}
|
|
1957
2127
|
case 'reopen': {
|
|
1958
2128
|
const st = await client.reopenSignedDoc(await resolveSignedId(client, args._[1]));
|
|
1959
|
-
console.log('다시 열었습니다 — 본문과 결재라인을 고칠 수 있습니다.');
|
|
2129
|
+
console.log(tr('다시 열었습니다 — 본문과 결재라인을 고칠 수 있습니다.', `Reopened — the body and approval line can be changed again.`));
|
|
1960
2130
|
return printSignedState(st);
|
|
1961
2131
|
}
|
|
1962
2132
|
case 'revise': {
|
|
1963
2133
|
const id = await resolveSignedId(client, args._[1]);
|
|
1964
2134
|
if (typeof args.note !== 'string' || !args.note.trim()) {
|
|
1965
|
-
throw new Error('개정에는 사유가 필요합니다: --note "측정값 단위 정정"');
|
|
2135
|
+
throw new Error(tr('개정에는 사유가 필요합니다: --note "측정값 단위 정정"', `A revision needs a reason: --note "corrected the measurement unit"`));
|
|
1966
2136
|
}
|
|
1967
2137
|
const out = await client.reviseSignedDoc(id, args.note);
|
|
1968
|
-
console.log(`개정본을 만들었습니다: ${out.nodeId} (${out.revisionNo}판). 원본은 그대로
|
|
2138
|
+
console.log(tr(`개정본을 만들었습니다: ${out.nodeId} (${out.revisionNo}판). 원본은 그대로 남습니다.`, `Revision created: ${out.nodeId} (rev ${out.revisionNo}). The original stays.`));
|
|
1969
2139
|
return printSignedState(await client.getSignedDoc(out.nodeId));
|
|
1970
2140
|
}
|
|
1971
2141
|
case 'verify': {
|
|
@@ -1973,15 +2143,20 @@ async function cmdSign(args) {
|
|
|
1973
2143
|
if (args.json) return console.log(JSON.stringify(v, null, 2));
|
|
1974
2144
|
const mark = (ok) => (ok ? '✅' : '❌');
|
|
1975
2145
|
console.log(`${v.name} [${SIGN_STATUS_KO[v.status] ?? v.status}]`);
|
|
1976
|
-
console.log(`${mark(v.contentIntact)} 본문이 서명 당시
|
|
1977
|
-
console.log(`${mark(v.allSignaturesValid)} 서명이 전부 유효하고 체인이
|
|
1978
|
-
console.log(`${mark(v.allTimestamped)} 서명마다 공인시점확인이 붙어
|
|
1979
|
-
console.log(
|
|
2146
|
+
console.log(tr(`${mark(v.contentIntact)} 본문이 서명 당시 그대로다`, `${mark(v.contentIntact)} the body is exactly what was signed`));
|
|
2147
|
+
console.log(tr(`${mark(v.allSignaturesValid)} 서명이 전부 유효하고 체인이 이어진다`, `${mark(v.allSignaturesValid)} every signature is valid and the chain is unbroken`));
|
|
2148
|
+
console.log(tr(`${mark(v.allTimestamped)} 서명마다 공인시점확인이 붙어 있다`, `${mark(v.allTimestamped)} every signature carries a trusted timestamp`));
|
|
2149
|
+
console.log(
|
|
2150
|
+
tr(
|
|
2151
|
+
`${mark(v.ledgerIntact)} 워크스페이스 원장이 성하다${v.ledgerBrokenAtSeq ? ` (끊긴 지점 #${v.ledgerBrokenAtSeq})` : ''}`,
|
|
2152
|
+
`${mark(v.ledgerIntact)} the workspace ledger is intact${v.ledgerBrokenAtSeq ? ` (breaks at #${v.ledgerBrokenAtSeq})` : ''}`,
|
|
2153
|
+
),
|
|
2154
|
+
);
|
|
1980
2155
|
for (const sig of v.signatures ?? []) {
|
|
1981
2156
|
console.log(
|
|
1982
2157
|
` #${sig.seq} ${sig.signerName} ${sig.role} ${sig.signedAt} ` +
|
|
1983
|
-
`${mark(sig.signatureValid)}서명 ${mark(sig.chainLinkValid)}체인 ${mark(sig.matchesCurrentContent)}
|
|
1984
|
-
(sig.timestampPending ? ' [시점확인 대기중]' : ''),
|
|
2158
|
+
tr(`${mark(sig.signatureValid)}서명 ${mark(sig.chainLinkValid)}체인 ${mark(sig.matchesCurrentContent)}현재본문`, `${mark(sig.signatureValid)}signature ${mark(sig.chainLinkValid)}chain ${mark(sig.matchesCurrentContent)}current body`) +
|
|
2159
|
+
(sig.timestampPending ? tr(' [시점확인 대기중]', ` [timestamp pending]`) : ''),
|
|
1985
2160
|
);
|
|
1986
2161
|
}
|
|
1987
2162
|
return;
|
|
@@ -1994,18 +2169,18 @@ async function cmdSign(args) {
|
|
|
1994
2169
|
pendingMine: !!args.pending,
|
|
1995
2170
|
});
|
|
1996
2171
|
if (args.json) return console.log(JSON.stringify(rows, null, 2));
|
|
1997
|
-
if (!rows.length) return console.log(args.pending ? '내 차례인 서명문서가 없습니다.' : '서명문서가 없습니다.');
|
|
2172
|
+
if (!rows.length) return console.log(args.pending ? tr('내 차례인 서명문서가 없습니다.', `No signed documents are waiting on you.`) : tr('서명문서가 없습니다.', `No signed documents.`));
|
|
1998
2173
|
for (const r of rows) {
|
|
1999
|
-
const waiting = r.waitingOn?.length ? ` 대기: ${r.waitingOn.join(', ')}` : '';
|
|
2000
|
-
console.log(`${r.nodeId}\t${SIGN_STATUS_KO[r.status] ?? r.status}\t${r.path}${waiting}${r.canSign ? ' ← 내 차례' : ''}`);
|
|
2174
|
+
const waiting = r.waitingOn?.length ? tr(` 대기: ${r.waitingOn.join(', ')}`, ` waiting on: ${r.waitingOn.join(', ')}`) : '';
|
|
2175
|
+
console.log(tr(`${r.nodeId}\t${SIGN_STATUS_KO[r.status] ?? r.status}\t${r.path}${waiting}${r.canSign ? ' ← 내 차례' : ''}`, `${r.nodeId}\t${SIGN_STATUS_KO[r.status] ?? r.status}\t${r.path}${waiting}${r.canSign ? ' ← your turn' : ''}`));
|
|
2001
2176
|
}
|
|
2002
2177
|
return;
|
|
2003
2178
|
}
|
|
2004
2179
|
case 'bulk': {
|
|
2005
2180
|
const ws = signWorkspace(client, args);
|
|
2006
2181
|
const targets = args._.slice(1);
|
|
2007
|
-
if (!targets.length) throw new Error(
|
|
2008
|
-
const code = requireCode(args, '묶음 서명');
|
|
2182
|
+
if (!targets.length) throw new Error(usage(tr(`rootr sign bulk <id|경로 …> --code <6자리>`, `rootr sign bulk <id|path …> --code <6 digits>`)));
|
|
2183
|
+
const code = requireCode(args, tr('묶음 서명', `bulk signing`));
|
|
2009
2184
|
const nodeIds = [];
|
|
2010
2185
|
for (const t of targets) nodeIds.push(await resolveSignedId(client, t));
|
|
2011
2186
|
const out = await client.bulkSignSignedDocs(ws, { nodeIds, code });
|
|
@@ -2015,20 +2190,28 @@ async function cmdSign(args) {
|
|
|
2015
2190
|
// 얼리는 것은 되돌릴 수 없으니 "몇 건"이 아니라 "무엇을 했는지"를 보여준다.
|
|
2016
2191
|
const n = (kind) => results.filter((r) => r.did === kind).length;
|
|
2017
2192
|
const parts = [];
|
|
2018
|
-
if (n('submitted+signed')) parts.push(`올리고 서명 ${n('submitted+signed')}
|
|
2019
|
-
if (n('submitted')) parts.push(`올리기만 ${n('submitted')}
|
|
2020
|
-
if (n('signed')) parts.push(`서명 ${n('signed')}
|
|
2021
|
-
console.log(
|
|
2022
|
-
|
|
2193
|
+
if (n('submitted+signed')) parts.push(tr(`올리고 서명 ${n('submitted+signed')}건`, `${n('submitted+signed')} submitted and signed`));
|
|
2194
|
+
if (n('submitted')) parts.push(tr(`올리기만 ${n('submitted')}건`, `${n('submitted')} submitted only`));
|
|
2195
|
+
if (n('signed')) parts.push(tr(`서명 ${n('signed')}건`, `${n('signed')} signed`));
|
|
2196
|
+
console.log(
|
|
2197
|
+
tr(
|
|
2198
|
+
`${parts.join(', ') || '처리 0건'}${failed.length ? `, 건너뜀 ${failed.length}건` : ''}`,
|
|
2199
|
+
`${parts.join(', ') || 'nothing processed'}${failed.length ? `, ${failed.length} skipped` : ''}`,
|
|
2200
|
+
),
|
|
2201
|
+
);
|
|
2202
|
+
for (const f of failed) console.log(tr(` 건너뜀 ${f.nodeId}: ${f.error}`, ` skipped ${f.nodeId}: ${f.error}`));
|
|
2023
2203
|
return;
|
|
2024
2204
|
}
|
|
2025
2205
|
case 'ledger': {
|
|
2026
2206
|
const ws = signWorkspace(client, args);
|
|
2027
2207
|
const v = await client.verifyLedger(ws);
|
|
2028
2208
|
if (args.json) return console.log(JSON.stringify(v, null, 2));
|
|
2029
|
-
console.log(`${v.ok ? '✅ 원장이 성합니다' : '❌ 원장이 끊겼습니다'} — 서명 ${v.count}
|
|
2030
|
-
if (v.headHash) console.log(`머리 해시: ${v.headHash}`);
|
|
2031
|
-
if (!v.ok)
|
|
2209
|
+
console.log(tr(`${v.ok ? '✅ 원장이 성합니다' : '❌ 원장이 끊겼습니다'} — 서명 ${v.count}건`, `${v.ok ? '✅ the ledger is intact' : '❌ the ledger is broken'} — ${v.count} signatures`));
|
|
2210
|
+
if (v.headHash) console.log(tr(`머리 해시: ${v.headHash}`, `Head hash: ${v.headHash}`));
|
|
2211
|
+
if (!v.ok)
|
|
2212
|
+
console.log(
|
|
2213
|
+
tr(`끊긴 지점: #${v.brokenAtSeq}${v.reason ? ` (${v.reason})` : ''}`, `Breaks at: #${v.brokenAtSeq}${v.reason ? ` (${v.reason})` : ''}`),
|
|
2214
|
+
);
|
|
2032
2215
|
return;
|
|
2033
2216
|
}
|
|
2034
2217
|
default:
|
|
@@ -2102,7 +2285,7 @@ async function main() {
|
|
|
2102
2285
|
case 'mcp':
|
|
2103
2286
|
return cmdMcp(args);
|
|
2104
2287
|
default:
|
|
2105
|
-
console.error(`알 수 없는 명령: ${cmd}\n`);
|
|
2288
|
+
console.error(tr(`알 수 없는 명령: ${cmd}\n`, `Unknown command: ${cmd}\n`));
|
|
2106
2289
|
console.log(USAGE);
|
|
2107
2290
|
process.exit(1);
|
|
2108
2291
|
}
|