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