@sonenta/cli 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -4
- package/dist/index.js +621 -89
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,73 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command17 } from "commander";
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "@sonenta/cli",
|
|
9
|
+
version: "0.18.0",
|
|
10
|
+
description: "Command-line interface for Sonenta translation management.",
|
|
11
|
+
license: "MIT",
|
|
12
|
+
homepage: "https://sonenta.com",
|
|
13
|
+
repository: {
|
|
14
|
+
type: "git",
|
|
15
|
+
url: "https://github.com/verbumia/verbumia-tools.git",
|
|
16
|
+
directory: "cli"
|
|
17
|
+
},
|
|
18
|
+
bugs: {
|
|
19
|
+
url: "https://github.com/verbumia/verbumia-tools/issues"
|
|
20
|
+
},
|
|
21
|
+
keywords: [
|
|
22
|
+
"sonenta",
|
|
23
|
+
"verbumia",
|
|
24
|
+
"cli",
|
|
25
|
+
"i18n",
|
|
26
|
+
"translations",
|
|
27
|
+
"localization"
|
|
28
|
+
],
|
|
29
|
+
author: "Sonenta",
|
|
30
|
+
type: "module",
|
|
31
|
+
bin: {
|
|
32
|
+
sonenta: "bin/sonenta.js",
|
|
33
|
+
verbumia: "bin/verbumia.js"
|
|
34
|
+
},
|
|
35
|
+
main: "./dist/index.js",
|
|
36
|
+
types: "./dist/index.d.ts",
|
|
37
|
+
files: [
|
|
38
|
+
"bin/",
|
|
39
|
+
"dist/",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
engines: {
|
|
44
|
+
node: ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
scripts: {
|
|
47
|
+
build: "tsup",
|
|
48
|
+
test: "vitest run",
|
|
49
|
+
typecheck: "tsc --noEmit",
|
|
50
|
+
dev: "tsx src/index.ts",
|
|
51
|
+
prepublishOnly: "npm run typecheck && npm run test && npm run build"
|
|
52
|
+
},
|
|
53
|
+
dependencies: {
|
|
54
|
+
commander: "^12.1.0"
|
|
55
|
+
},
|
|
56
|
+
devDependencies: {
|
|
57
|
+
"@types/node": "^20.0.0",
|
|
58
|
+
tsup: "^8.3.0",
|
|
59
|
+
tsx: "^4.19.0",
|
|
60
|
+
typescript: "^5.5.0",
|
|
61
|
+
vitest: "^2.1.0"
|
|
62
|
+
},
|
|
63
|
+
publishConfig: {
|
|
64
|
+
access: "public",
|
|
65
|
+
registry: "https://registry.npmjs.org/"
|
|
66
|
+
}
|
|
67
|
+
};
|
|
5
68
|
|
|
6
69
|
// src/commands/agents.ts
|
|
7
|
-
import { Command } from "commander";
|
|
70
|
+
import { Command as Command2 } from "commander";
|
|
8
71
|
|
|
9
72
|
// src/agents.ts
|
|
10
73
|
import { promises as fs } from "fs";
|
|
@@ -295,9 +358,27 @@ is an explicit, estimate-first, opt-in fallback for very large volumes.
|
|
|
295
358
|
\`update_keys_bulk\`, \`acknowledge_missing_keys\`.
|
|
296
359
|
- **Translate:** \`list_untranslated_keys\`, \`propose_translations_bulk\`
|
|
297
360
|
(your primary write \u2014 CRUD, 0 credits), \`validate_translations\`.
|
|
298
|
-
- **Consistency:** \`glossary_list\`, \`project_context_get
|
|
361
|
+
- **Consistency:** \`glossary_list\`, \`project_context_get\`,
|
|
362
|
+
\`list_placeholder_mismatches\` (audit: translations whose interpolation
|
|
363
|
+
variables drifted from the source \u2014 see **Placeholders**).
|
|
299
364
|
- **Ship:** \`publish_cdn\`.
|
|
300
365
|
|
|
366
|
+
## Placeholders \u2014 NEVER ship a translation that breaks interpolation
|
|
367
|
+
A translation must carry the SAME interpolation variables as its source value \u2014
|
|
368
|
+
by NAME, brace-agnostic (i18next \`{{name}}\`, ICU \`{name}\`, Ruby \`%{name}\`).
|
|
369
|
+
Dropping \`{{count}}\` or inventing \`{{total}}\` is a runtime bug (missing data or
|
|
370
|
+
a crash), so:
|
|
371
|
+
- When you translate, COPY the source's variables verbatim into your output (keep
|
|
372
|
+
the same names; you may reorder them for grammar). Don't translate, rename, or
|
|
373
|
+
drop a variable token.
|
|
374
|
+
- The project may set \`placeholder_enforcement = strict\`. Then
|
|
375
|
+
\`propose_translations_bulk\` REFUSES an offending item: that item comes back
|
|
376
|
+
\`{status:"error", error:{code:"PLACEHOLDER_MISMATCH"}, placeholder_mismatch:
|
|
377
|
+
{missing, extra, expected, got}}\` (the envelope also carries
|
|
378
|
+
\`placeholder_mismatch_count\`); good items still apply. In \`warn\` mode the item
|
|
379
|
+
applies but carries \`variable_warnings:{missing, extra}\`. Treat BOTH as a defect
|
|
380
|
+
to fix \u2014 see the workflow.
|
|
381
|
+
|
|
301
382
|
## Workflow
|
|
302
383
|
1. **Assess.** \`get_project_info\` (source + target languages, namespaces);
|
|
303
384
|
\`coverage_report\` (per-language completeness); \`health_report\`
|
|
@@ -318,14 +399,25 @@ is an explicit, estimate-first, opt-in fallback for very large volumes.
|
|
|
318
399
|
language, \`list_untranslated_keys\`. BEFORE translating, read
|
|
319
400
|
\`glossary_list\` (respect \`forbidden\` / \`do_not_translate\`, apply
|
|
320
401
|
\`translation\` rules) and \`project_context_get\` (brand voice, domain, tone).
|
|
321
|
-
Translate each value YOURSELF
|
|
402
|
+
Translate each value YOURSELF \u2014 **carrying the source's interpolation
|
|
403
|
+
variables verbatim** (see **Placeholders**) \u2014 then write a batch with
|
|
322
404
|
\`propose_translations_bulk\` (status draft/proposed). Work through the
|
|
323
405
|
languages and namespaces in sensible batches.
|
|
324
|
-
4. **
|
|
406
|
+
4. **Honor strict placeholders \u2014 fix and resubmit, never leave a mismatch.**
|
|
407
|
+
Inspect the \`propose_translations_bulk\` result: for any item with
|
|
408
|
+
\`error.code == "PLACEHOLDER_MISMATCH"\` (strict) OR \`variable_warnings\`
|
|
409
|
+
(warn), read \`placeholder_mismatch.{missing, extra, expected, got}\` (each
|
|
410
|
+
item is ONE language, so this is already per-language), REWRITE that value so
|
|
411
|
+
its variables are exactly \`expected\` \u2014 add every \`missing\`, drop every
|
|
412
|
+
\`extra\`, keep the names \u2014 and RESUBMIT the corrected item. Repeat until the
|
|
413
|
+
batch returns no \`PLACEHOLDER_MISMATCH\` and \`placeholder_mismatch_count == 0\`.
|
|
414
|
+
A strict-refused item was NOT written, so it is not done until it re-submits
|
|
415
|
+
clean. (Optionally \`list_placeholder_mismatches\` to audit the whole project.)
|
|
416
|
+
5. **Validate (optional).** \`validate_translations\` lints an i18next blob
|
|
325
417
|
server-side; fix anything it flags.
|
|
326
|
-
|
|
418
|
+
6. **Publish.** \`publish_cdn\` to release the bundles \u2014 with confirmation in
|
|
327
419
|
interactive mode, only on explicit authorization in CI.
|
|
328
|
-
|
|
420
|
+
7. **Report.** Show the coverage delta (before/after), counts of keys created and
|
|
329
421
|
strings translated, what remains, and that translations are drafts to review.
|
|
330
422
|
|
|
331
423
|
## Modes
|
|
@@ -346,12 +438,15 @@ is an explicit, estimate-first, opt-in fallback for very large volumes.
|
|
|
346
438
|
- Local translation + \`propose_translations_bulk\` is the default and costs 0
|
|
347
439
|
credits; any server-side AI translation is an explicit, estimated, opt-in
|
|
348
440
|
fallback.
|
|
441
|
+
- A translation must preserve the source's interpolation variables (by name).
|
|
442
|
+
Never leave a \`PLACEHOLDER_MISMATCH\` (strict) or \`variable_warnings\` (warn)
|
|
443
|
+
unresolved \u2014 fix the value and resubmit before considering the key done.
|
|
349
444
|
- Never \`publish_cdn\` without confirmation/authorization; stay within the
|
|
350
445
|
configured project.
|
|
351
446
|
`;
|
|
352
447
|
var SONENTA_SOURCE_HEALTH = `---
|
|
353
448
|
name: sonenta-source-health
|
|
354
|
-
description: Source-health repairer for Sonenta-managed i18n projects. Finds DUPLICATE source strings (the same source value spread across many keys \u2014 which inflates translation cost and lets the same string drift into N different translations) and fixes them, working STRICTLY step by step: it lists the affected files first, presents a plan and reassures you before touching anything, edits ONLY on your acceptance, and marks each group resolved through the Sonenta MCP tools. When the Sonenta dashboard has prepared a merge plan, it applies that plan verbatim \u2014 merging the clustered keys (value-safe), then differentiating or allowing whatever still shares text. Use interactively in Claude Code or headless in CI.
|
|
449
|
+
description: Source-health repairer for Sonenta-managed i18n projects. Finds DUPLICATE source strings (the same source value spread across many keys \u2014 which inflates translation cost and lets the same string drift into N different translations) and fixes them, working STRICTLY step by step: it lists the affected files first, presents a plan and reassures you before touching anything, edits ONLY on your acceptance, and marks each group resolved through the Sonenta MCP tools. When the Sonenta dashboard has prepared a merge plan, it applies that plan verbatim \u2014 merging the clustered keys (value-safe), then differentiating or allowing whatever still shares text. It also repairs PLACEHOLDER MISMATCHES \u2014 translations whose interpolation variables ({{name}}, {count}, %{n}) drifted from the source \u2014 by rewriting the target value to match and re-checking. Use interactively in Claude Code or headless in CI.
|
|
355
450
|
---
|
|
356
451
|
|
|
357
452
|
You are **sonenta-source-health**, a careful repair specialist for
|
|
@@ -394,6 +489,14 @@ every change is a reviewable draft.
|
|
|
394
489
|
optional \`note\` recording why. CRUD. Only on the dev's EXPLICIT acceptance \u2014
|
|
395
490
|
\`allowed\` in particular is the user's business decision, never an agent
|
|
396
491
|
default; never auto-mark a group the dev hasn't decided.
|
|
492
|
+
- \`list_placeholder_mismatches\` \u2014 your SECOND worklist (PM2): translations
|
|
493
|
+
whose interpolation variables drifted from the source. Each item =
|
|
494
|
+
{key_uuid, key, namespace_slug, language, source_value, source_vars[],
|
|
495
|
+
target_value, target_vars[], missing[], extra[]}. READ-ONLY \u2014 see
|
|
496
|
+
**Placeholder mismatch repair**.
|
|
497
|
+
- \`propose_translation\` \u2014 write ONE corrected target translation
|
|
498
|
+
(\`{namespace, key, language_code, value}\`, draft). CRUD, 0 credits. Your
|
|
499
|
+
write tool for placeholder repairs (the source value is untouched).
|
|
397
500
|
|
|
398
501
|
## Repair strategies (pick per group, propose explicitly)
|
|
399
502
|
For a group of keys sharing one source value, the right fix is usually one of:
|
|
@@ -453,6 +556,30 @@ decision to apply, so you PROPOSE a strategy (consolidate / disambiguate / allow
|
|
|
453
556
|
from the section above and act ONLY on the dev's explicit acceptance \u2014 never
|
|
454
557
|
auto-resolve or auto-allow a group the dev hasn't decided.
|
|
455
558
|
|
|
559
|
+
## Placeholder mismatch repair (PM2) \u2014 list, rewrite, RE-CHECK
|
|
560
|
+
Your second job: translations whose interpolation VARIABLES drifted from the
|
|
561
|
+
source (a broken \`{{name}}\` / \`{count}\` is a runtime bug \u2014 missing data or a
|
|
562
|
+
crash). Detection is by variable NAME, brace-agnostic (i18next \`{{}}\`, ICU
|
|
563
|
+
\`{}\`, Ruby \`%{}\`). This repair edits a TARGET TRANSLATION value, never the
|
|
564
|
+
source \u2014 so it's low-risk and reversible (a draft), but you still go step by step.
|
|
565
|
+
1. **List.** \`list_placeholder_mismatches\` (optionally scoped by \`key_id\` /
|
|
566
|
+
\`language_code\`). Each item gives \`source_vars\`, the offending
|
|
567
|
+
\`target_value\` / \`target_vars\`, and the \`missing\` / \`extra\` variables for
|
|
568
|
+
that language. Present the worklist (key, language, what's missing/extra).
|
|
569
|
+
2. **Rewrite \u2014 yourself, 0 credits.** For each item, rewrite \`target_value\` so
|
|
570
|
+
its variables are exactly \`source_vars\`: ADD every \`missing\` token, REMOVE
|
|
571
|
+
every \`extra\` one, keep the names identical (you may reposition them for
|
|
572
|
+
grammar), and preserve the translation's meaning. Don't translate or rename a
|
|
573
|
+
variable token.
|
|
574
|
+
3. **Write on acceptance.** Persist each corrected value with
|
|
575
|
+
\`propose_translation(namespace, key, language_code, value)\` (a draft; the
|
|
576
|
+
source value is untouched). In interactive mode propose the rewrites and apply
|
|
577
|
+
on the dev's yes; in CI apply only when the run authorizes auto-fix.
|
|
578
|
+
4. **RE-CHECK (mandatory).** Re-call \`list_placeholder_mismatches\` \u2014 there is NO
|
|
579
|
+
server-side auto-fix, so the repair is only confirmed when \`total == 0\` (or
|
|
580
|
+
the item is gone). If something still mismatches, you mis-rewrote it \u2014 fix and
|
|
581
|
+
re-check again.
|
|
582
|
+
|
|
456
583
|
## Workflow (strictly ordered)
|
|
457
584
|
1. **List the affected files first.** Call \`list_source_duplicates(status=to_fix)\`.
|
|
458
585
|
For each group, resolve the keys to their human locations with \`list_keys\`
|
|
@@ -843,12 +970,12 @@ var AGENTS = {
|
|
|
843
970
|
},
|
|
844
971
|
"sonenta-i18n": {
|
|
845
972
|
name: "sonenta-i18n",
|
|
846
|
-
summary: "i18n automation: audits coverage, creates missing keys, translates the untranslated locally (0-credit propose_translations_bulk), and publishes \u2014 server-side AI translation as an opt-in fallback.",
|
|
973
|
+
summary: "i18n automation: audits coverage, creates missing keys, translates the untranslated locally (0-credit propose_translations_bulk), and publishes \u2014 server-side AI translation as an opt-in fallback. Preserves interpolation variables and honors strict placeholder mode: self-corrects + resubmits on a PLACEHOLDER_MISMATCH so it never writes a translation that breaks placeholders.",
|
|
847
974
|
content: SONENTA_I18N
|
|
848
975
|
},
|
|
849
976
|
"sonenta-source-health": {
|
|
850
977
|
name: "sonenta-source-health",
|
|
851
|
-
summary: "Duplicate-source repairer. Applies the merge plans prepared in the Sonenta dashboard \u2014 repoints t() usages onto the canonical key and trashes the redundant ones (value-safe soft-delete, never edits a source value), then differentiates or allows whatever still shares text. Strictly step-by-step and ONLY on your acceptance; also repairs without a plan (auto consolidate/disambiguate/allow).",
|
|
978
|
+
summary: "Duplicate-source repairer. Applies the merge plans prepared in the Sonenta dashboard \u2014 repoints t() usages onto the canonical key and trashes the redundant ones (value-safe soft-delete, never edits a source value), then differentiates or allows whatever still shares text. Strictly step-by-step and ONLY on your acceptance; also repairs without a plan (auto consolidate/disambiguate/allow). Plus PLACEHOLDER MISMATCH repair: lists translations whose interpolation vars drifted from the source, rewrites the target to match (propose_translation), and re-checks until clean.",
|
|
852
979
|
content: SONENTA_SOURCE_HEALTH
|
|
853
980
|
},
|
|
854
981
|
"sonenta-knowledge": {
|
|
@@ -1106,9 +1233,327 @@ async function requireAuth(opts = {}) {
|
|
|
1106
1233
|
return ctx;
|
|
1107
1234
|
}
|
|
1108
1235
|
|
|
1236
|
+
// src/mcpserver.ts
|
|
1237
|
+
import { promises as fs4 } from "fs";
|
|
1238
|
+
import { resolve as resolve3 } from "path";
|
|
1239
|
+
var MCP_JSON_FILENAME = ".mcp.json";
|
|
1240
|
+
var MCP_SERVER_KEY = "sonenta";
|
|
1241
|
+
var MCP_PACKAGE = "@sonenta/mcp";
|
|
1242
|
+
function buildServerBlock(env, opts = {}) {
|
|
1243
|
+
const e = {};
|
|
1244
|
+
if (opts.embedKey && env.apiKey) e.SONENTA_API_KEY = env.apiKey;
|
|
1245
|
+
if (env.host) e.SONENTA_BASE_URL = env.host.replace(/\/+$/, "");
|
|
1246
|
+
if (env.projectUuid) e.SONENTA_PROJECT = env.projectUuid;
|
|
1247
|
+
return { command: "npx", args: ["-y", MCP_PACKAGE], env: e };
|
|
1248
|
+
}
|
|
1249
|
+
async function readWiredServer(baseDir = process.cwd()) {
|
|
1250
|
+
const { json } = await readMcpJson(resolve3(baseDir, MCP_JSON_FILENAME));
|
|
1251
|
+
const servers = json.mcpServers;
|
|
1252
|
+
if (!servers || typeof servers !== "object") return null;
|
|
1253
|
+
const block = servers[MCP_SERVER_KEY];
|
|
1254
|
+
if (!block || typeof block !== "object") return null;
|
|
1255
|
+
return block;
|
|
1256
|
+
}
|
|
1257
|
+
async function readMcpJson(path) {
|
|
1258
|
+
try {
|
|
1259
|
+
const raw = await fs4.readFile(path, "utf8");
|
|
1260
|
+
const trimmed = raw.trim();
|
|
1261
|
+
if (!trimmed) return { json: {}, existed: true };
|
|
1262
|
+
const parsed = JSON.parse(trimmed);
|
|
1263
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1264
|
+
throw new Error(`${MCP_JSON_FILENAME} is not a JSON object`);
|
|
1265
|
+
}
|
|
1266
|
+
return { json: parsed, existed: true };
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
if (err?.code === "ENOENT") {
|
|
1269
|
+
return { json: {}, existed: false };
|
|
1270
|
+
}
|
|
1271
|
+
throw err;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
async function wireMcpServer(env, opts = {}) {
|
|
1275
|
+
const baseDir = opts.baseDir ?? process.cwd();
|
|
1276
|
+
const path = resolve3(baseDir, MCP_JSON_FILENAME);
|
|
1277
|
+
const { json, existed } = await readMcpJson(path);
|
|
1278
|
+
const embeddedKey = Boolean(opts.embedKey && env.apiKey);
|
|
1279
|
+
const block = buildServerBlock(env, { embedKey: opts.embedKey });
|
|
1280
|
+
const servers = json.mcpServers && typeof json.mcpServers === "object" ? json.mcpServers : {};
|
|
1281
|
+
const prior = servers[MCP_SERVER_KEY];
|
|
1282
|
+
const identical = prior !== void 0 && deepEqual(prior, block);
|
|
1283
|
+
servers[MCP_SERVER_KEY] = block;
|
|
1284
|
+
json.mcpServers = servers;
|
|
1285
|
+
const action = !existed ? "created" : identical ? "unchanged" : "updated";
|
|
1286
|
+
if (action !== "unchanged") {
|
|
1287
|
+
await fs4.writeFile(path, JSON.stringify(json, null, 2) + "\n", "utf8");
|
|
1288
|
+
}
|
|
1289
|
+
let gitignoreUpdated = false;
|
|
1290
|
+
const wantGitignore = opts.gitignore ?? embeddedKey;
|
|
1291
|
+
if (wantGitignore) {
|
|
1292
|
+
gitignoreUpdated = await ensureGitignored(baseDir, MCP_JSON_FILENAME);
|
|
1293
|
+
}
|
|
1294
|
+
return { path, action, serverKey: MCP_SERVER_KEY, gitignoreUpdated, embeddedKey };
|
|
1295
|
+
}
|
|
1296
|
+
async function ensureGitignored(baseDir, entry) {
|
|
1297
|
+
const path = resolve3(baseDir, ".gitignore");
|
|
1298
|
+
let current = "";
|
|
1299
|
+
try {
|
|
1300
|
+
current = await fs4.readFile(path, "utf8");
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
if (err?.code !== "ENOENT") throw err;
|
|
1303
|
+
}
|
|
1304
|
+
const already = current.split(/\r?\n/).map((l) => l.trim()).some((l) => l === entry || l === `/${entry}`);
|
|
1305
|
+
if (already) return false;
|
|
1306
|
+
const prefix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
|
|
1307
|
+
const header = current.length === 0 ? "" : "\n# Sonenta MCP server config (contains an API key)\n";
|
|
1308
|
+
await fs4.writeFile(path, `${current}${prefix}${header}${entry}
|
|
1309
|
+
`, "utf8");
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
function deepEqual(a, b) {
|
|
1313
|
+
if (a === b) return true;
|
|
1314
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
1315
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
1316
|
+
const ak = Object.keys(a);
|
|
1317
|
+
const bk = Object.keys(b);
|
|
1318
|
+
if (ak.length !== bk.length) return false;
|
|
1319
|
+
return ak.every(
|
|
1320
|
+
(k) => deepEqual(a[k], b[k])
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/doctor.ts
|
|
1325
|
+
var CANON_HOST = "https://api.sonenta.dev";
|
|
1326
|
+
var KEYS_HINT = "create one in the dashboard \u2192 Org Settings \u2192 API Keys (scope mcp:*), then run `sonenta login`";
|
|
1327
|
+
function hasMcpScope(scopes) {
|
|
1328
|
+
if (!scopes) return false;
|
|
1329
|
+
return scopes.some((s) => s === "*" || s === "mcp:*" || s.startsWith("mcp:"));
|
|
1330
|
+
}
|
|
1331
|
+
async function probe(fetchImpl, host, apiKey, path) {
|
|
1332
|
+
const url = `${host.replace(/\/+$/, "")}${path}`;
|
|
1333
|
+
try {
|
|
1334
|
+
const res = await fetchImpl(url, { headers: { Authorization: `ApiKey ${apiKey}` } });
|
|
1335
|
+
let json;
|
|
1336
|
+
try {
|
|
1337
|
+
json = await res.json();
|
|
1338
|
+
} catch {
|
|
1339
|
+
json = void 0;
|
|
1340
|
+
}
|
|
1341
|
+
return { status: res.status, ok: res.ok, json };
|
|
1342
|
+
} catch (e) {
|
|
1343
|
+
return { status: 0, ok: false, networkError: e.message };
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
async function runDoctor(opts = {}) {
|
|
1347
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
1348
|
+
const dir = opts.dir ?? process.cwd();
|
|
1349
|
+
const checks = [];
|
|
1350
|
+
const { path: cfgPath, config } = await readConfig(dir).catch(() => ({
|
|
1351
|
+
path: null,
|
|
1352
|
+
config: {}
|
|
1353
|
+
}));
|
|
1354
|
+
checks.push({
|
|
1355
|
+
id: "config",
|
|
1356
|
+
title: "Project config",
|
|
1357
|
+
status: cfgPath ? "pass" : "warn",
|
|
1358
|
+
detail: cfgPath ? `sonenta.config.json found${config.project_uuid ? ` (project ${config.project_uuid})` : ""}` : "no sonenta.config.json in this directory",
|
|
1359
|
+
fix: cfgPath ? void 0 : "run `sonenta init --project <uuid>` to scaffold it and bind a project"
|
|
1360
|
+
});
|
|
1361
|
+
let ctx = null;
|
|
1362
|
+
try {
|
|
1363
|
+
ctx = await resolveContext({ hostOverride: opts.hostOverride, cwd: dir });
|
|
1364
|
+
checks.push({
|
|
1365
|
+
id: "login",
|
|
1366
|
+
title: "Login",
|
|
1367
|
+
status: "pass",
|
|
1368
|
+
detail: `credentials resolved for ${ctx.host}`
|
|
1369
|
+
});
|
|
1370
|
+
} catch (e) {
|
|
1371
|
+
checks.push({
|
|
1372
|
+
id: "login",
|
|
1373
|
+
title: "Login",
|
|
1374
|
+
status: "fail",
|
|
1375
|
+
detail: e.message,
|
|
1376
|
+
fix: `run \`sonenta login --host ${opts.hostOverride ?? CANON_HOST}\``
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
const wired = await readWiredServer(dir).catch(() => null);
|
|
1380
|
+
checks.push({
|
|
1381
|
+
id: "mcp_wired",
|
|
1382
|
+
title: "MCP server wired",
|
|
1383
|
+
status: wired ? "pass" : "fail",
|
|
1384
|
+
detail: wired ? `.mcp.json declares the "sonenta" server (${MCP_PACKAGE})` : 'no "sonenta" server in .mcp.json',
|
|
1385
|
+
fix: wired ? void 0 : "run `sonenta agents add <name>` (or `sonenta init`) to wire it, then reload your Claude session"
|
|
1386
|
+
});
|
|
1387
|
+
if (!ctx) {
|
|
1388
|
+
return finalize(checks);
|
|
1389
|
+
}
|
|
1390
|
+
const me = await probe(fetchImpl, ctx.host, ctx.apiKey, "/v1/me");
|
|
1391
|
+
if (me.networkError) {
|
|
1392
|
+
checks.push({
|
|
1393
|
+
id: "api",
|
|
1394
|
+
title: "API reachable",
|
|
1395
|
+
status: "fail",
|
|
1396
|
+
detail: `could not reach ${ctx.host}: ${me.networkError}`,
|
|
1397
|
+
fix: `check your connection and the host \u2014 the canonical host is ${CANON_HOST} (\`sonenta login --host ${CANON_HOST}\`)`
|
|
1398
|
+
});
|
|
1399
|
+
return finalize(checks);
|
|
1400
|
+
}
|
|
1401
|
+
if (me.status === 404) {
|
|
1402
|
+
checks.push({
|
|
1403
|
+
id: "api",
|
|
1404
|
+
title: "API reachable",
|
|
1405
|
+
status: "fail",
|
|
1406
|
+
detail: `${ctx.host} returned 404 for /v1/me \u2014 almost certainly the wrong host`,
|
|
1407
|
+
fix: `use the canonical host: \`sonenta login --host ${CANON_HOST}\``
|
|
1408
|
+
});
|
|
1409
|
+
return finalize(checks);
|
|
1410
|
+
}
|
|
1411
|
+
if (me.status === 401) {
|
|
1412
|
+
checks.push({
|
|
1413
|
+
id: "api",
|
|
1414
|
+
title: "API key valid",
|
|
1415
|
+
status: "fail",
|
|
1416
|
+
detail: "the API returned 401 \u2014 your key is invalid or revoked",
|
|
1417
|
+
fix: `run \`sonenta login\` with a current key (${KEYS_HINT})`
|
|
1418
|
+
});
|
|
1419
|
+
return finalize(checks);
|
|
1420
|
+
}
|
|
1421
|
+
if (!me.ok) {
|
|
1422
|
+
checks.push({
|
|
1423
|
+
id: "api",
|
|
1424
|
+
title: "API reachable",
|
|
1425
|
+
status: "fail",
|
|
1426
|
+
detail: `unexpected ${me.status} from ${ctx.host}/v1/me`,
|
|
1427
|
+
fix: `verify the host (${CANON_HOST}) and try again`
|
|
1428
|
+
});
|
|
1429
|
+
return finalize(checks);
|
|
1430
|
+
}
|
|
1431
|
+
checks.push({
|
|
1432
|
+
id: "api",
|
|
1433
|
+
title: "API reachable",
|
|
1434
|
+
status: "pass",
|
|
1435
|
+
detail: `${ctx.host} responded 200 to /v1/me`
|
|
1436
|
+
});
|
|
1437
|
+
const accountActive = me.json?.account_active !== false;
|
|
1438
|
+
if (!accountActive) {
|
|
1439
|
+
checks.push({
|
|
1440
|
+
id: "account",
|
|
1441
|
+
title: "Account active",
|
|
1442
|
+
status: "fail",
|
|
1443
|
+
detail: "your account/subscription is inactive",
|
|
1444
|
+
fix: "reactivate your subscription in the Sonenta dashboard"
|
|
1445
|
+
});
|
|
1446
|
+
} else {
|
|
1447
|
+
checks.push({ id: "account", title: "Account active", status: "pass", detail: "account active" });
|
|
1448
|
+
}
|
|
1449
|
+
const scopes = Array.isArray(me.json?.scopes) ? me.json.scopes : void 0;
|
|
1450
|
+
const mcpOk = hasMcpScope(scopes);
|
|
1451
|
+
checks.push({
|
|
1452
|
+
id: "mcp_scope",
|
|
1453
|
+
title: "Key has mcp:* scope",
|
|
1454
|
+
status: mcpOk ? "pass" : "fail",
|
|
1455
|
+
detail: mcpOk ? "the key carries the mcp:* scope" : `the key lacks the mcp:* scope${scopes ? ` (has: ${scopes.join(", ") || "none"})` : ""}`,
|
|
1456
|
+
fix: mcpOk ? void 0 : `the agents drive the MCP tools, which need an mcp:* key \u2014 ${KEYS_HINT}`
|
|
1457
|
+
});
|
|
1458
|
+
if (!ctx.projectUuid) {
|
|
1459
|
+
checks.push({
|
|
1460
|
+
id: "a11y",
|
|
1461
|
+
title: "a11y tools respond",
|
|
1462
|
+
status: "skip",
|
|
1463
|
+
detail: "no project bound, so the per-project a11y surface can't be probed",
|
|
1464
|
+
fix: "bind a project: `sonenta init --project <uuid>`"
|
|
1465
|
+
});
|
|
1466
|
+
return finalize(checks);
|
|
1467
|
+
}
|
|
1468
|
+
const surf = await probe(
|
|
1469
|
+
fetchImpl,
|
|
1470
|
+
ctx.host,
|
|
1471
|
+
ctx.apiKey,
|
|
1472
|
+
`/v1/mcp/projects/${ctx.projectUuid}/surfaces`
|
|
1473
|
+
);
|
|
1474
|
+
if (surf.ok) {
|
|
1475
|
+
checks.push({
|
|
1476
|
+
id: "a11y",
|
|
1477
|
+
title: "a11y tools respond",
|
|
1478
|
+
status: "pass",
|
|
1479
|
+
detail: "the project's a11y/surface MCP endpoint responded"
|
|
1480
|
+
});
|
|
1481
|
+
} else if (surf.status === 403) {
|
|
1482
|
+
const detail = surf.json?.detail ?? surf.json;
|
|
1483
|
+
const isMissingScope = detail?.code === "MISSING_SCOPE";
|
|
1484
|
+
const howTo = typeof detail?.how_to_get === "string" ? detail.how_to_get : void 0;
|
|
1485
|
+
const msg = typeof detail?.message === "string" ? detail.message : void 0;
|
|
1486
|
+
checks.push({
|
|
1487
|
+
id: "a11y",
|
|
1488
|
+
title: "a11y tools respond",
|
|
1489
|
+
status: "fail",
|
|
1490
|
+
detail: isMissingScope ? `the a11y MCP surface returned 403: this key lacks the ${detail?.required_scope ?? "mcp:*"} scope for this project` : "the a11y MCP surface returned 403 \u2014 the key can't act on this project",
|
|
1491
|
+
fix: msg ?? (howTo ? `get a scoped key: ${howTo}` : `the key needs the mcp:* scope for this project \u2014 ${KEYS_HINT}`)
|
|
1492
|
+
});
|
|
1493
|
+
} else if (surf.status === 404) {
|
|
1494
|
+
checks.push({
|
|
1495
|
+
id: "a11y",
|
|
1496
|
+
title: "a11y tools respond",
|
|
1497
|
+
status: "fail",
|
|
1498
|
+
detail: `project ${ctx.projectUuid} not found on ${ctx.host} (404)`,
|
|
1499
|
+
fix: "check project_uuid in sonenta.config.json and the host (`sonenta init --project <uuid>`)"
|
|
1500
|
+
});
|
|
1501
|
+
} else {
|
|
1502
|
+
checks.push({
|
|
1503
|
+
id: "a11y",
|
|
1504
|
+
title: "a11y tools respond",
|
|
1505
|
+
status: "fail",
|
|
1506
|
+
detail: surf.networkError ? `could not reach the a11y surface: ${surf.networkError}` : `unexpected ${surf.status} from the a11y MCP surface`,
|
|
1507
|
+
fix: `verify the host (${CANON_HOST}) and that the project is reachable`
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
return finalize(checks);
|
|
1511
|
+
}
|
|
1512
|
+
function finalize(checks) {
|
|
1513
|
+
return { checks, ok: !checks.some((c) => c.status === "fail") };
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/commands/doctor.ts
|
|
1517
|
+
import { Command } from "commander";
|
|
1518
|
+
var MARK = {
|
|
1519
|
+
pass: "\u2713",
|
|
1520
|
+
fail: "\u2717",
|
|
1521
|
+
warn: "!",
|
|
1522
|
+
skip: "\xB7"
|
|
1523
|
+
};
|
|
1524
|
+
function formatReport(report) {
|
|
1525
|
+
const lines = ["sonenta doctor \u2014 agent preflight", ""];
|
|
1526
|
+
const width = Math.max(...report.checks.map((c) => c.title.length));
|
|
1527
|
+
for (const c of report.checks) {
|
|
1528
|
+
lines.push(`${MARK[c.status]} ${c.title.padEnd(width)} ${c.detail}`);
|
|
1529
|
+
if (c.fix && (c.status === "fail" || c.status === "warn" || c.status === "skip")) {
|
|
1530
|
+
lines.push(`${" ".repeat(width + 4)}\u2192 Fix: ${c.fix}`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
lines.push("");
|
|
1534
|
+
if (report.ok) {
|
|
1535
|
+
lines.push(
|
|
1536
|
+
"All checks passed. If your agent still shows no tools, reload your Claude Code session \u2014 the MCP server connects at session start."
|
|
1537
|
+
);
|
|
1538
|
+
} else {
|
|
1539
|
+
const failed = report.checks.filter((c) => c.status === "fail").length;
|
|
1540
|
+
lines.push(
|
|
1541
|
+
`${failed} check${failed === 1 ? "" : "s"} failed \u2014 fix the step(s) above, then re-run \`sonenta doctor\`.`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
return lines;
|
|
1545
|
+
}
|
|
1546
|
+
var doctorCommand = new Command("doctor").description(
|
|
1547
|
+
"Preflight the agent setup: verifies the @sonenta/mcp server is wired + reachable, the key has the mcp:* scope, and the project's a11y tools respond \u2014 with an exact fix for anything that's off."
|
|
1548
|
+
).option("--dir <path>", "Project directory (default: current directory)").option("--host <url>", "Override host (otherwise from config/credentials)").action(async (opts) => {
|
|
1549
|
+
const report = await runDoctor({ dir: opts.dir, hostOverride: opts.host });
|
|
1550
|
+
for (const line of formatReport(report)) console.log(line);
|
|
1551
|
+
if (!report.ok) process.exit(1);
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1109
1554
|
// src/commands/agents.ts
|
|
1110
|
-
var agentsCommand = new
|
|
1111
|
-
new
|
|
1555
|
+
var agentsCommand = new Command2("agents").description("Install bundled Claude agents (e.g. sonenta-a11y) into .claude/agents/.").addCommand(
|
|
1556
|
+
new Command2("list").description("List the bundled agents available to install.").option("--dir <path>", "Project directory (default: current directory)").action(async (opts) => {
|
|
1112
1557
|
const baseDir = opts.dir;
|
|
1113
1558
|
const agents = listAgents();
|
|
1114
1559
|
console.log(`Available agents (${agents.length}):`);
|
|
@@ -1122,22 +1567,60 @@ var agentsCommand = new Command("agents").description("Install bundled Claude ag
|
|
|
1122
1567
|
Install with: sonenta agents add <name>`);
|
|
1123
1568
|
})
|
|
1124
1569
|
).addCommand(
|
|
1125
|
-
new
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1570
|
+
new Command2("add").description("Write a bundled agent definition into <dir>/.claude/agents/<name>.md.").argument("<name>", "Agent name (e.g. sonenta-a11y)").option("--dir <path>", "Project directory (default: current directory)").option("--host <url>", "Override host (otherwise from config/credentials)").option("--force", "Overwrite an existing agent definition", false).option("--no-mcp", "Skip auto-wiring the @sonenta/mcp server into .mcp.json").option(
|
|
1571
|
+
"--embed-key",
|
|
1572
|
+
"Bake the API key into .mcp.json (for CI / no-login); otherwise the server reads it from ~/.sonenta",
|
|
1573
|
+
false
|
|
1574
|
+
).action(
|
|
1575
|
+
async (name, opts) => {
|
|
1576
|
+
const ctx = await requireAuth({ hostOverride: opts.host });
|
|
1577
|
+
const path = await writeAgent(name, { baseDir: opts.dir, force: opts.force });
|
|
1578
|
+
console.log(`Wrote ${path}`);
|
|
1579
|
+
if (opts.mcp) {
|
|
1580
|
+
const wired = await wireMcpServer(
|
|
1581
|
+
{ apiKey: ctx.apiKey, host: ctx.host, projectUuid: ctx.projectUuid },
|
|
1582
|
+
{ baseDir: opts.dir, embedKey: opts.embedKey }
|
|
1583
|
+
);
|
|
1584
|
+
const verb = wired.action === "created" ? "Created" : wired.action === "updated" ? "Updated" : "Verified";
|
|
1585
|
+
console.log(
|
|
1586
|
+
`${verb} ${MCP_JSON_FILENAME} \u2192 connected the "${wired.serverKey}" server (${"npx -y @sonenta/mcp"}, host ${ctx.host}${ctx.projectUuid ? `, project ${ctx.projectUuid}` : ""}).`
|
|
1587
|
+
);
|
|
1588
|
+
if (wired.embeddedKey) {
|
|
1589
|
+
console.log(
|
|
1590
|
+
`Embedded your API key in ${MCP_JSON_FILENAME}` + (wired.gitignoreUpdated ? " and added it to .gitignore" : "") + " \u2014 keep it out of git."
|
|
1591
|
+
);
|
|
1592
|
+
} else {
|
|
1593
|
+
console.log(
|
|
1594
|
+
`No secret stored in ${MCP_JSON_FILENAME} \u2014 the server reads your API key from ~/.sonenta at startup (run \`sonenta login\` if it can't). Safe to commit.`
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
if (!ctx.projectUuid) {
|
|
1598
|
+
console.log(
|
|
1599
|
+
"Note: no project bound \u2014 run `sonenta init --project <uuid>` so the agent's tools default to one project (or pass project_uuid per call)."
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
console.log(
|
|
1603
|
+
`
|
|
1604
|
+
\u27F3 Reload your Claude Code session (or restart the MCP client) so the "${wired.serverKey}" server connects \u2014 then ${name}'s tools are available.`
|
|
1605
|
+
);
|
|
1606
|
+
} else {
|
|
1607
|
+
console.log(
|
|
1608
|
+
`
|
|
1609
|
+
Skipped .mcp.json (--no-mcp). The ${name} agent needs the Sonenta MCP server (npx -y @sonenta/mcp) with an mcp:* SONENTA_API_KEY to have any tools.`
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
console.log(`Agent dir: ${AGENTS_DIR}/`);
|
|
1613
|
+
console.log("");
|
|
1614
|
+
const report = await runDoctor({ dir: opts.dir, hostOverride: opts.host });
|
|
1615
|
+
for (const line of formatReport(report)) console.log(line);
|
|
1616
|
+
}
|
|
1617
|
+
)
|
|
1135
1618
|
);
|
|
1136
1619
|
|
|
1137
1620
|
// src/commands/export.ts
|
|
1138
|
-
import { promises as
|
|
1621
|
+
import { promises as fs5 } from "fs";
|
|
1139
1622
|
import { join as join2 } from "path";
|
|
1140
|
-
import { Command as
|
|
1623
|
+
import { Command as Command3 } from "commander";
|
|
1141
1624
|
|
|
1142
1625
|
// src/i18next_tree.ts
|
|
1143
1626
|
function unflatten(flat, sep = ".") {
|
|
@@ -1262,7 +1745,7 @@ async function collect(ctx, languages, namespace) {
|
|
|
1262
1745
|
}
|
|
1263
1746
|
return out;
|
|
1264
1747
|
}
|
|
1265
|
-
var exportCommand = new
|
|
1748
|
+
var exportCommand = new Command3("export").description(
|
|
1266
1749
|
"Export Verbumia translations as i18next JSON. Flat dot-notation by default (--nested for nested trees). Writes <out>/<lang>/<namespace>.json with --out, otherwise prints { locale: { namespace: tree } } to stdout."
|
|
1267
1750
|
).option("--language <code>", "Restrict to a single language code").option("--namespace <slug>", "Restrict to a single namespace slug").option("--nested", "Emit nested JSON instead of flat dot-notation", false).option("--out <dir>", "Write files instead of printing to stdout").option("--host <url>", "Override host (otherwise from config/credentials)").action(
|
|
1268
1751
|
async (opts) => {
|
|
@@ -1275,9 +1758,9 @@ var exportCommand = new Command2("export").description(
|
|
|
1275
1758
|
for (const [lang, nss] of Object.entries(collected)) {
|
|
1276
1759
|
for (const [ns, flat] of Object.entries(nss)) {
|
|
1277
1760
|
const dir = join2(opts.out, lang);
|
|
1278
|
-
await
|
|
1761
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
1279
1762
|
const p = join2(dir, `${ns}.json`);
|
|
1280
|
-
await
|
|
1763
|
+
await fs5.writeFile(p, JSON.stringify(shape(flat), null, 2) + "\n", "utf8");
|
|
1281
1764
|
console.log(` ${p} ${Object.keys(flat).length} keys`);
|
|
1282
1765
|
files++;
|
|
1283
1766
|
}
|
|
@@ -1295,9 +1778,9 @@ var exportCommand = new Command2("export").description(
|
|
|
1295
1778
|
);
|
|
1296
1779
|
|
|
1297
1780
|
// src/commands/import.ts
|
|
1298
|
-
import { promises as
|
|
1781
|
+
import { promises as fs6 } from "fs";
|
|
1299
1782
|
import { basename, dirname as dirname3 } from "path";
|
|
1300
|
-
import { Command as
|
|
1783
|
+
import { Command as Command4 } from "commander";
|
|
1301
1784
|
function resolveLangNs(filePath, optLang, optNs) {
|
|
1302
1785
|
const stem = basename(filePath).replace(/\.json$/i, "");
|
|
1303
1786
|
const parent = basename(dirname3(filePath));
|
|
@@ -1313,7 +1796,7 @@ function resolveLangNs(filePath, optLang, optNs) {
|
|
|
1313
1796
|
return { lang, ns };
|
|
1314
1797
|
}
|
|
1315
1798
|
async function readTree(filePath) {
|
|
1316
|
-
const parsed = JSON.parse(await
|
|
1799
|
+
const parsed = JSON.parse(await fs6.readFile(filePath, "utf8"));
|
|
1317
1800
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1318
1801
|
throw new Error(`${filePath} is not a JSON object`);
|
|
1319
1802
|
}
|
|
@@ -1327,7 +1810,7 @@ function countLeaves(tree) {
|
|
|
1327
1810
|
}
|
|
1328
1811
|
return n;
|
|
1329
1812
|
}
|
|
1330
|
-
var importCommand = new
|
|
1813
|
+
var importCommand = new Command4("import").description(
|
|
1331
1814
|
"Import i18next JSON file(s) into Verbumia in ONE call \u2014 creates missing keys and upserts translations (idempotent). Accepts nested or flat JSON. Language/namespace are inferred from the path (<lang>/<namespace>.json) or forced with --language / --namespace."
|
|
1332
1815
|
).argument("<files...>", "i18next JSON file(s), e.g. locales/fr/common.json or fr.json").option("--language <code>", "Force the language code for every file").option("--namespace <slug>", "Force the namespace slug for every file").option("--status <status>", "draft | translated (default: translated)").option("--version <slug>", "Target version slug (default: production version)").option("--dry-run", "Print what would be imported without sending", false).option("--host <url>", "Override host (otherwise from config/credentials)").action(
|
|
1333
1816
|
async (files, opts) => {
|
|
@@ -1370,13 +1853,13 @@ var importCommand = new Command3("import").description(
|
|
|
1370
1853
|
|
|
1371
1854
|
// src/commands/init.ts
|
|
1372
1855
|
import { existsSync } from "fs";
|
|
1373
|
-
import { resolve as
|
|
1374
|
-
import { Command as
|
|
1856
|
+
import { resolve as resolve5 } from "path";
|
|
1857
|
+
import { Command as Command5 } from "commander";
|
|
1375
1858
|
|
|
1376
1859
|
// src/repodoc.ts
|
|
1377
|
-
import { promises as
|
|
1378
|
-
import { resolve as
|
|
1379
|
-
var DOC_API_HOST = "https://api.sonenta.
|
|
1860
|
+
import { promises as fs7 } from "fs";
|
|
1861
|
+
import { resolve as resolve4 } from "path";
|
|
1862
|
+
var DOC_API_HOST = "https://api.sonenta.dev";
|
|
1380
1863
|
var DOC_CDN_HOST = "https://cdn.sonenta.com";
|
|
1381
1864
|
var REPO_DOC_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
1382
1865
|
var BLOCK_BEGIN = "<!-- SONENTA:BEGIN \u2014 managed by `sonenta init`; edits between these markers are overwritten -->";
|
|
@@ -1464,13 +1947,13 @@ function renderManagedBlock(d) {
|
|
|
1464
1947
|
async function upsertManagedBlock(filePath, block) {
|
|
1465
1948
|
let existing = null;
|
|
1466
1949
|
try {
|
|
1467
|
-
existing = await
|
|
1950
|
+
existing = await fs7.readFile(filePath, "utf8");
|
|
1468
1951
|
} catch {
|
|
1469
1952
|
existing = null;
|
|
1470
1953
|
}
|
|
1471
1954
|
const normalizedBlock = block.endsWith("\n") ? block : block + "\n";
|
|
1472
1955
|
if (existing === null) {
|
|
1473
|
-
await
|
|
1956
|
+
await fs7.writeFile(filePath, normalizedBlock, "utf8");
|
|
1474
1957
|
return "created";
|
|
1475
1958
|
}
|
|
1476
1959
|
const begin = existing.indexOf(BLOCK_BEGIN);
|
|
@@ -1479,18 +1962,18 @@ async function upsertManagedBlock(filePath, block) {
|
|
|
1479
1962
|
const before = existing.slice(0, begin);
|
|
1480
1963
|
const after = existing.slice(end + BLOCK_END.length);
|
|
1481
1964
|
const blockCore = block.slice(0, block.indexOf(BLOCK_END) + BLOCK_END.length);
|
|
1482
|
-
await
|
|
1965
|
+
await fs7.writeFile(filePath, before + blockCore + after, "utf8");
|
|
1483
1966
|
return "updated";
|
|
1484
1967
|
}
|
|
1485
1968
|
const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1486
|
-
await
|
|
1969
|
+
await fs7.writeFile(filePath, existing + sep + normalizedBlock, "utf8");
|
|
1487
1970
|
return "inserted";
|
|
1488
1971
|
}
|
|
1489
1972
|
async function writeRepoDocs(dir, data) {
|
|
1490
1973
|
const block = renderManagedBlock(data);
|
|
1491
1974
|
const results = [];
|
|
1492
1975
|
for (const file of REPO_DOC_FILES) {
|
|
1493
|
-
const path =
|
|
1976
|
+
const path = resolve4(dir, file);
|
|
1494
1977
|
const action = await upsertManagedBlock(path, block);
|
|
1495
1978
|
results.push({ file, path, action });
|
|
1496
1979
|
}
|
|
@@ -1498,7 +1981,7 @@ async function writeRepoDocs(dir, data) {
|
|
|
1498
1981
|
}
|
|
1499
1982
|
|
|
1500
1983
|
// src/commands/init.ts
|
|
1501
|
-
var DEFAULT_HOST = "https://api.sonenta.
|
|
1984
|
+
var DEFAULT_HOST = "https://api.sonenta.dev";
|
|
1502
1985
|
var ACTION_LABEL = {
|
|
1503
1986
|
created: "Created",
|
|
1504
1987
|
updated: "Updated",
|
|
@@ -1537,11 +2020,15 @@ async function gatherRepoDocData(opts) {
|
|
|
1537
2020
|
};
|
|
1538
2021
|
}
|
|
1539
2022
|
}
|
|
1540
|
-
var initCommand = new
|
|
2023
|
+
var initCommand = new Command5("init").description(
|
|
1541
2024
|
"Scaffold sonenta.config.json AND write a managed Sonenta block into CLAUDE.md / AGENTS.md so coding agents know how this repo uses Sonenta."
|
|
1542
|
-
).option("--host <url>", "API base URL", DEFAULT_HOST).option("--project <uuid>", "Project UUID").option("--version <slug>", "Version slug (default: main)", "main").option("--force", "Overwrite an existing sonenta.config.json", false).option("--no-repo-doc", "Skip writing the managed block into CLAUDE.md / AGENTS.md").
|
|
2025
|
+
).option("--host <url>", "API base URL", DEFAULT_HOST).option("--project <uuid>", "Project UUID").option("--version <slug>", "Version slug (default: main)", "main").option("--force", "Overwrite an existing sonenta.config.json", false).option("--no-repo-doc", "Skip writing the managed block into CLAUDE.md / AGENTS.md").option("--no-mcp", "Skip auto-wiring the @sonenta/mcp server into .mcp.json").option(
|
|
2026
|
+
"--embed-key",
|
|
2027
|
+
"Bake the API key into .mcp.json (for CI / no-login); otherwise the server reads it from ~/.sonenta",
|
|
2028
|
+
false
|
|
2029
|
+
).action(
|
|
1543
2030
|
async (opts) => {
|
|
1544
|
-
const path =
|
|
2031
|
+
const path = resolve5(process.cwd(), CONFIG_FILENAME);
|
|
1545
2032
|
if (existsSync(path) && !opts.force) {
|
|
1546
2033
|
console.error(
|
|
1547
2034
|
`${CONFIG_FILENAME} already exists at ${path}. Pass --force to overwrite.`
|
|
@@ -1568,6 +2055,50 @@ var initCommand = new Command4("init").description(
|
|
|
1568
2055
|
} else {
|
|
1569
2056
|
console.log("Skipped CLAUDE.md / AGENTS.md (--no-repo-doc).");
|
|
1570
2057
|
}
|
|
2058
|
+
if (opts.mcp) {
|
|
2059
|
+
let liveHost = opts.host !== DEFAULT_HOST ? opts.host : void 0;
|
|
2060
|
+
if (!liveHost) {
|
|
2061
|
+
const creds = await readCredentials().catch(() => null);
|
|
2062
|
+
liveHost = creds?.default ?? void 0;
|
|
2063
|
+
}
|
|
2064
|
+
let resolved = null;
|
|
2065
|
+
try {
|
|
2066
|
+
const ctx = await resolveContext({ hostOverride: liveHost });
|
|
2067
|
+
resolved = { apiKey: ctx.apiKey, host: ctx.host, projectUuid: ctx.projectUuid };
|
|
2068
|
+
} catch {
|
|
2069
|
+
resolved = opts.embedKey ? null : { host: liveHost ?? opts.host, projectUuid: opts.project };
|
|
2070
|
+
}
|
|
2071
|
+
if (resolved) {
|
|
2072
|
+
const wired = await wireMcpServer(
|
|
2073
|
+
{
|
|
2074
|
+
apiKey: resolved.apiKey,
|
|
2075
|
+
host: resolved.host,
|
|
2076
|
+
projectUuid: resolved.projectUuid ?? opts.project
|
|
2077
|
+
},
|
|
2078
|
+
{ embedKey: opts.embedKey }
|
|
2079
|
+
);
|
|
2080
|
+
const verb = wired.action === "created" ? "Created" : wired.action === "updated" ? "Updated" : "Verified";
|
|
2081
|
+
console.log(
|
|
2082
|
+
`${verb} ${MCP_JSON_FILENAME} \u2192 connected the "${wired.serverKey}" server (npx -y @sonenta/mcp${resolved.host ? `, host ${resolved.host}` : ""}).`
|
|
2083
|
+
);
|
|
2084
|
+
if (wired.embeddedKey) {
|
|
2085
|
+
console.log(
|
|
2086
|
+
`Embedded your API key in ${MCP_JSON_FILENAME}` + (wired.gitignoreUpdated ? " and added it to .gitignore" : "") + " \u2014 keep it out of git."
|
|
2087
|
+
);
|
|
2088
|
+
} else {
|
|
2089
|
+
console.log(
|
|
2090
|
+
`No secret stored in ${MCP_JSON_FILENAME} \u2014 the server reads your API key from ~/.sonenta at startup (run \`sonenta login\` if it can't). Safe to commit.`
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
console.log(
|
|
2094
|
+
`\u27F3 Reload your Claude Code session so the "${wired.serverKey}" server connects.`
|
|
2095
|
+
);
|
|
2096
|
+
} else {
|
|
2097
|
+
console.log(
|
|
2098
|
+
`Note: skipped ${MCP_JSON_FILENAME} wiring (--embed-key needs a login). Run \`sonenta login\` then \`sonenta agents add <name>\`.`
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
1571
2102
|
if (!opts.project) {
|
|
1572
2103
|
console.log(
|
|
1573
2104
|
"Tip: pass --project <uuid> to bind this directory to a specific project (or edit project_uuid in the file later), then re-run `sonenta init --force`."
|
|
@@ -1577,9 +2108,9 @@ var initCommand = new Command4("init").description(
|
|
|
1577
2108
|
);
|
|
1578
2109
|
|
|
1579
2110
|
// src/commands/keys.ts
|
|
1580
|
-
import { Command as
|
|
1581
|
-
var keysCommand = new
|
|
1582
|
-
new
|
|
2111
|
+
import { Command as Command6 } from "commander";
|
|
2112
|
+
var keysCommand = new Command6("keys").description("Inspect translation keys for the current project.").addCommand(
|
|
2113
|
+
new Command6("list").description("List keys for the configured project (addressed by namespace slug + key name).").option("--namespace <slug>", "Filter by namespace slug").option("--host <url>", "Override host (otherwise from config/credentials)").action(async (opts) => {
|
|
1583
2114
|
const ctx = await requireAuth({ hostOverride: opts.host });
|
|
1584
2115
|
const items = await listKeys(ctx, { namespace: opts.namespace });
|
|
1585
2116
|
console.log(`total: ${items.length}`);
|
|
@@ -1588,7 +2119,7 @@ var keysCommand = new Command5("keys").description("Inspect translation keys for
|
|
|
1588
2119
|
);
|
|
1589
2120
|
|
|
1590
2121
|
// src/commands/login.ts
|
|
1591
|
-
import { Command as
|
|
2122
|
+
import { Command as Command7 } from "commander";
|
|
1592
2123
|
|
|
1593
2124
|
// src/prompt.ts
|
|
1594
2125
|
import { createInterface } from "readline";
|
|
@@ -1596,8 +2127,8 @@ async function promptLine(message) {
|
|
|
1596
2127
|
if (!process.stdin.isTTY) return "";
|
|
1597
2128
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1598
2129
|
try {
|
|
1599
|
-
return await new Promise((
|
|
1600
|
-
rl.question(message, (answer) =>
|
|
2130
|
+
return await new Promise((resolve7) => {
|
|
2131
|
+
rl.question(message, (answer) => resolve7(answer.trim()));
|
|
1601
2132
|
});
|
|
1602
2133
|
} finally {
|
|
1603
2134
|
rl.close();
|
|
@@ -1613,7 +2144,7 @@ async function promptSecret(message) {
|
|
|
1613
2144
|
process.stdout.write(message);
|
|
1614
2145
|
process.stdin.setRawMode(true);
|
|
1615
2146
|
process.stdin.resume();
|
|
1616
|
-
return await new Promise((
|
|
2147
|
+
return await new Promise((resolve7) => {
|
|
1617
2148
|
let buffer = "";
|
|
1618
2149
|
const onData = (chunk) => {
|
|
1619
2150
|
for (const byte of chunk) {
|
|
@@ -1622,7 +2153,7 @@ async function promptSecret(message) {
|
|
|
1622
2153
|
process.stdin.removeListener("data", onData);
|
|
1623
2154
|
process.stdin.setRawMode(false);
|
|
1624
2155
|
process.stdin.pause();
|
|
1625
|
-
|
|
2156
|
+
resolve7(buffer);
|
|
1626
2157
|
return;
|
|
1627
2158
|
}
|
|
1628
2159
|
if (byte === CTRL_C) {
|
|
@@ -1649,12 +2180,12 @@ async function promptSecret(message) {
|
|
|
1649
2180
|
|
|
1650
2181
|
// src/commands/login.ts
|
|
1651
2182
|
var TOKEN_REGEX = /^vrb_[a-z]+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
1652
|
-
var loginCommand = new
|
|
2183
|
+
var loginCommand = new Command7("login").description(
|
|
1653
2184
|
"Store an API key for a host. Token resolution order: --token, SONENTA_TOKEN env, then interactive prompt (TTY only)."
|
|
1654
|
-
).option("--host <url>", "API base URL", "https://api.sonenta.
|
|
2185
|
+
).option("--host <url>", "API base URL", "https://api.sonenta.dev").option("--token <vrb_live_\u2026>", "API key token (prefix.secret form)").option("--email <email>", "User email associated with the token (optional)").option("--default", "Set this host as the default for future commands", false).action(async (opts) => {
|
|
1655
2186
|
let host = opts.host;
|
|
1656
2187
|
if (!host && process.stdin.isTTY) {
|
|
1657
|
-
host = await promptLine("Host (default https://api.sonenta.
|
|
2188
|
+
host = await promptLine("Host (default https://api.sonenta.dev): ") || "https://api.sonenta.dev";
|
|
1658
2189
|
}
|
|
1659
2190
|
let token = opts.token ?? (process.env.SONENTA_TOKEN ?? process.env.VERBUMIA_TOKEN) ?? "";
|
|
1660
2191
|
if (!token && process.stdin.isTTY) {
|
|
@@ -1700,8 +2231,8 @@ var loginCommand = new Command6("login").description(
|
|
|
1700
2231
|
});
|
|
1701
2232
|
|
|
1702
2233
|
// src/commands/logout.ts
|
|
1703
|
-
import { Command as
|
|
1704
|
-
var logoutCommand = new
|
|
2234
|
+
import { Command as Command8 } from "commander";
|
|
2235
|
+
var logoutCommand = new Command8("logout").description("Remove stored credentials for a host (default: the current default host).").option("--host <url>", "Host to forget. Omit to forget the current default.").action(async (opts) => {
|
|
1705
2236
|
const creds = await readCredentials();
|
|
1706
2237
|
const target = opts.host ?? creds.default;
|
|
1707
2238
|
if (!target) {
|
|
@@ -1717,8 +2248,8 @@ var logoutCommand = new Command7("logout").description("Remove stored credential
|
|
|
1717
2248
|
});
|
|
1718
2249
|
|
|
1719
2250
|
// src/commands/missing.ts
|
|
1720
|
-
import { Command as
|
|
1721
|
-
var missingCommand = new
|
|
2251
|
+
import { Command as Command9 } from "commander";
|
|
2252
|
+
var missingCommand = new Command9("missing").description(
|
|
1722
2253
|
"List runtime-detected missing keys for the configured project. Requires the API key to carry the `mcp:*` scope."
|
|
1723
2254
|
).option("--namespace <slug>", "Filter by namespace slug").option("--language <code>", "Filter by language code").option("--status <state>", "Filter by status (open|resolved|...)").option("--limit <n>", "Page size (1-200, default 50)", "50").option("--host <url>", "Override host (otherwise from config/credentials)").action(
|
|
1724
2255
|
async (opts) => {
|
|
@@ -1752,9 +2283,9 @@ var missingCommand = new Command8("missing").description(
|
|
|
1752
2283
|
);
|
|
1753
2284
|
|
|
1754
2285
|
// src/commands/projects.ts
|
|
1755
|
-
import { Command as
|
|
1756
|
-
var projectsCommand = new
|
|
1757
|
-
new
|
|
2286
|
+
import { Command as Command10 } from "commander";
|
|
2287
|
+
var projectsCommand = new Command10("projects").description("Inspect Verbumia projects accessible to your API key.").addCommand(
|
|
2288
|
+
new Command10("list").description("List the projects this API key can reach.").option("--host <url>", "Override host (otherwise from config/credentials)").action(async (opts) => {
|
|
1758
2289
|
const ctx = await requireAuth({ hostOverride: opts.host });
|
|
1759
2290
|
const items = await listProjects(ctx);
|
|
1760
2291
|
if (items.length === 0) {
|
|
@@ -1770,17 +2301,17 @@ var projectsCommand = new Command9("projects").description("Inspect Verbumia pro
|
|
|
1770
2301
|
);
|
|
1771
2302
|
|
|
1772
2303
|
// src/commands/pull.ts
|
|
1773
|
-
import { Command as
|
|
2304
|
+
import { Command as Command11 } from "commander";
|
|
1774
2305
|
|
|
1775
2306
|
// src/locales.ts
|
|
1776
|
-
import { promises as
|
|
1777
|
-
import { join as join3, resolve as
|
|
2307
|
+
import { promises as fs8 } from "fs";
|
|
2308
|
+
import { join as join3, resolve as resolve6 } from "path";
|
|
1778
2309
|
var DEFAULT_LOCALES_DIR = "locales";
|
|
1779
2310
|
async function listLocaleFiles(rootDir) {
|
|
1780
|
-
const root =
|
|
2311
|
+
const root = resolve6(rootDir);
|
|
1781
2312
|
let langDirs;
|
|
1782
2313
|
try {
|
|
1783
|
-
langDirs = await
|
|
2314
|
+
langDirs = await fs8.readdir(root);
|
|
1784
2315
|
} catch {
|
|
1785
2316
|
return [];
|
|
1786
2317
|
}
|
|
@@ -1789,12 +2320,12 @@ async function listLocaleFiles(rootDir) {
|
|
|
1789
2320
|
const langPath = join3(root, lang);
|
|
1790
2321
|
let stat;
|
|
1791
2322
|
try {
|
|
1792
|
-
stat = await
|
|
2323
|
+
stat = await fs8.stat(langPath);
|
|
1793
2324
|
} catch {
|
|
1794
2325
|
continue;
|
|
1795
2326
|
}
|
|
1796
2327
|
if (!stat.isDirectory()) continue;
|
|
1797
|
-
const files = await
|
|
2328
|
+
const files = await fs8.readdir(langPath);
|
|
1798
2329
|
for (const f of files) {
|
|
1799
2330
|
if (!f.endsWith(".json")) continue;
|
|
1800
2331
|
out.push({ lang, namespace: f.replace(/\.json$/, ""), path: join3(langPath, f) });
|
|
@@ -1803,7 +2334,7 @@ async function listLocaleFiles(rootDir) {
|
|
|
1803
2334
|
return out;
|
|
1804
2335
|
}
|
|
1805
2336
|
async function readLocaleFile(path) {
|
|
1806
|
-
const raw = await
|
|
2337
|
+
const raw = await fs8.readFile(path, "utf8");
|
|
1807
2338
|
const parsed = JSON.parse(raw);
|
|
1808
2339
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1809
2340
|
throw new Error(`${path} is not a flat object`);
|
|
@@ -1819,12 +2350,12 @@ async function readLocaleFile(path) {
|
|
|
1819
2350
|
}
|
|
1820
2351
|
async function writeLocaleFile(rootDir, lang, namespace, values) {
|
|
1821
2352
|
const dir = join3(rootDir, lang);
|
|
1822
|
-
await
|
|
2353
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
1823
2354
|
const path = join3(dir, `${namespace}.json`);
|
|
1824
2355
|
const sorted = Object.fromEntries(
|
|
1825
2356
|
Object.entries(values).sort(([a], [b]) => a.localeCompare(b))
|
|
1826
2357
|
);
|
|
1827
|
-
await
|
|
2358
|
+
await fs8.writeFile(path, JSON.stringify(sorted, null, 2) + "\n", "utf8");
|
|
1828
2359
|
return path;
|
|
1829
2360
|
}
|
|
1830
2361
|
function diffFlat(local, remote) {
|
|
@@ -1848,7 +2379,7 @@ function diffFlat(local, remote) {
|
|
|
1848
2379
|
}
|
|
1849
2380
|
|
|
1850
2381
|
// src/commands/pull.ts
|
|
1851
|
-
var pullCommand = new
|
|
2382
|
+
var pullCommand = new Command11("pull").description(
|
|
1852
2383
|
"Pull translations from Verbumia into locales/<lang>/<namespace>.json (flat dot-notation). Overwrites local files \u2014 pair with `git status`."
|
|
1853
2384
|
).option("--language <code>", "Restrict to a single language code").option("--namespace <slug>", "Restrict to a single namespace slug").option("--dest <dir>", "Output directory", DEFAULT_LOCALES_DIR).option("--host <url>", "Override host (otherwise from config/credentials)").action(
|
|
1854
2385
|
async (opts) => {
|
|
@@ -1887,8 +2418,8 @@ var pullCommand = new Command10("pull").description(
|
|
|
1887
2418
|
);
|
|
1888
2419
|
|
|
1889
2420
|
// src/commands/push.ts
|
|
1890
|
-
import { Command as
|
|
1891
|
-
var pushCommand = new
|
|
2421
|
+
import { Command as Command12 } from "commander";
|
|
2422
|
+
var pushCommand = new Command12("push").description(
|
|
1892
2423
|
"Push the whole local locales/ tree to Verbumia in ONE import call \u2014 creates missing keys and upserts translations (idempotent). Reads locales/<lang>/<namespace>.json (flat dot-notation)."
|
|
1893
2424
|
).option("--language <code>", "Restrict to a single language code").option("--namespace <slug>", "Restrict to a single namespace slug").option("--src <dir>", "Locales directory", DEFAULT_LOCALES_DIR).option("--status <status>", "draft | translated (default: translated)").option("--version <slug>", "Target version slug (default: production version)").option("--dry-run", "Print what would be pushed without sending", false).option("--host <url>", "Override host (otherwise from config/credentials)").action(
|
|
1894
2425
|
async (opts) => {
|
|
@@ -1935,9 +2466,9 @@ var pushCommand = new Command11("push").description(
|
|
|
1935
2466
|
);
|
|
1936
2467
|
|
|
1937
2468
|
// src/commands/releases.ts
|
|
1938
|
-
import { Command as
|
|
1939
|
-
var releasesCommand = new
|
|
1940
|
-
new
|
|
2469
|
+
import { Command as Command13 } from "commander";
|
|
2470
|
+
var releasesCommand = new Command13("releases").description("Manage CDN releases for the project.").addCommand(
|
|
2471
|
+
new Command13("publish").description(
|
|
1941
2472
|
"Trigger a CDN release: build bundles for every (language, namespace) and push them to the public CDN. Idempotent \u2014 unchanged bundles are reused. Subscribed SDKs receive a live `translations_published` event."
|
|
1942
2473
|
).option("--language <code>", "Restrict to one language (default: all)").option("--namespace <slug>", "Restrict to one namespace (default: all)").option("--version <slug>", "Version slug (default: production version)").option("--dry-run", "Print the planned release without publishing", false).option("--host <url>", "Override host (otherwise from config/credentials)").action(
|
|
1943
2474
|
async (opts) => {
|
|
@@ -1963,8 +2494,8 @@ var releasesCommand = new Command12("releases").description("Manage CDN releases
|
|
|
1963
2494
|
);
|
|
1964
2495
|
|
|
1965
2496
|
// src/commands/snapshot.ts
|
|
1966
|
-
import { promises as
|
|
1967
|
-
import { Command as
|
|
2497
|
+
import { promises as fs9 } from "fs";
|
|
2498
|
+
import { Command as Command14 } from "commander";
|
|
1968
2499
|
function bundleUrl(cdnBase, project, version, lang, ns) {
|
|
1969
2500
|
return `${cdnBase.replace(/\/+$/, "")}/p/${project}/${version}/latest/${lang}/${ns}.json`;
|
|
1970
2501
|
}
|
|
@@ -1988,7 +2519,7 @@ async function fetchBundle(url) {
|
|
|
1988
2519
|
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
1989
2520
|
return await res.json();
|
|
1990
2521
|
}
|
|
1991
|
-
var snapshotCommand = new
|
|
2522
|
+
var snapshotCommand = new Command14("snapshot").description(
|
|
1992
2523
|
"Generate a build-time translations snapshot for @sonenta/react-i18next `initialBundles` (offline-first fallback). Fetches the PUBLIC CDN bundles (exactly what the SDK loads at runtime) and assembles Record<locale, Record<namespace, tree>>. Emits a .ts module (default) or .json."
|
|
1993
2524
|
).option("--language <code>", "Restrict to a single language code").option("--namespace <slug>", "Restrict to a single namespace slug").option("--version <slug>", "Version slug (default: the configured version_slug)").option("--format <fmt>", "ts | json (default: ts)", "ts").option("--cdn <base>", "CDN base URL", "https://cdn.sonenta.com").option("--out <file>", "Write to a file instead of stdout").option("--host <url>", "Override host (used to discover languages/namespaces)").action(
|
|
1994
2525
|
async (opts) => {
|
|
@@ -2030,7 +2561,7 @@ var snapshotCommand = new Command13("snapshot").description(
|
|
|
2030
2561
|
opts.format === "json" ? "json" : "ts"
|
|
2031
2562
|
);
|
|
2032
2563
|
if (opts.out) {
|
|
2033
|
-
await
|
|
2564
|
+
await fs9.writeFile(opts.out, output, "utf8");
|
|
2034
2565
|
console.log(
|
|
2035
2566
|
`wrote ${opts.out}: ${fetched} bundle(s)` + (missing ? `, ${missing} not published (404)` : "")
|
|
2036
2567
|
);
|
|
@@ -2042,8 +2573,8 @@ var snapshotCommand = new Command13("snapshot").description(
|
|
|
2042
2573
|
);
|
|
2043
2574
|
|
|
2044
2575
|
// src/commands/status.ts
|
|
2045
|
-
import { Command as
|
|
2046
|
-
var statusCommand = new
|
|
2576
|
+
import { Command as Command15 } from "commander";
|
|
2577
|
+
var statusCommand = new Command15("status").description("Diff local locales/ against the remote project state.").option("--language <code>", "Restrict to one language code").option("--namespace <slug>", "Restrict to one namespace slug").option("--src <dir>", "Locales directory", DEFAULT_LOCALES_DIR).option("--host <url>", "Override host (otherwise from config/credentials)").action(
|
|
2047
2578
|
async (opts) => {
|
|
2048
2579
|
const ctx = await requireAuth({ hostOverride: opts.host });
|
|
2049
2580
|
const knownLangs = new Set((await getProjectInfo(ctx)).languages);
|
|
@@ -2101,8 +2632,8 @@ var statusCommand = new Command14("status").description("Diff local locales/ aga
|
|
|
2101
2632
|
);
|
|
2102
2633
|
|
|
2103
2634
|
// src/commands/whoami.ts
|
|
2104
|
-
import { Command as
|
|
2105
|
-
var whoamiCommand = new
|
|
2635
|
+
import { Command as Command16 } from "commander";
|
|
2636
|
+
var whoamiCommand = new Command16("whoami").description("Show the configured default host + which API key is in use.").option("--host <url>", "Inspect a specific host instead of the default").action(async (opts) => {
|
|
2106
2637
|
const creds = await readCredentials();
|
|
2107
2638
|
if (!creds.default && Object.keys(creds.hosts).length === 0) {
|
|
2108
2639
|
console.log("Not logged in. Run `sonenta login --host <url> --token <\u2026>`.");
|
|
@@ -2130,8 +2661,8 @@ var whoamiCommand = new Command15("whoami").description("Show the configured def
|
|
|
2130
2661
|
});
|
|
2131
2662
|
|
|
2132
2663
|
// src/index.ts
|
|
2133
|
-
var program = new
|
|
2134
|
-
program.name("sonenta").description("CLI for Sonenta translation management.").version(
|
|
2664
|
+
var program = new Command17();
|
|
2665
|
+
program.name("sonenta").description("CLI for Sonenta translation management.").version(package_default.version);
|
|
2135
2666
|
program.addCommand(loginCommand);
|
|
2136
2667
|
program.addCommand(logoutCommand);
|
|
2137
2668
|
program.addCommand(whoamiCommand);
|
|
@@ -2147,6 +2678,7 @@ program.addCommand(releasesCommand);
|
|
|
2147
2678
|
program.addCommand(snapshotCommand);
|
|
2148
2679
|
program.addCommand(missingCommand);
|
|
2149
2680
|
program.addCommand(agentsCommand);
|
|
2681
|
+
program.addCommand(doctorCommand);
|
|
2150
2682
|
program.parseAsync(process.argv).catch((err) => {
|
|
2151
2683
|
console.error(err instanceof Error ? err.message : err);
|
|
2152
2684
|
process.exit(1);
|