@sonenta/cli 0.17.0 → 0.19.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 +18 -2
- package/dist/index.js +441 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,99 @@
|
|
|
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.19.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";
|
|
11
74
|
import { resolve } from "path";
|
|
12
75
|
var AGENTS_DIR = ".claude/agents";
|
|
76
|
+
var PREFLIGHT = `## Preflight \u2014 verify your tools FIRST (fail fast, don't guess)
|
|
77
|
+
Before ANY work, confirm the Sonenta MCP tools are actually available to you in
|
|
78
|
+
this session \u2014 make a SINGLE cheap read (e.g. \`get_project_info\` or the first
|
|
79
|
+
read your task needs). Treat the result as a gate:
|
|
80
|
+
- **Tools missing** (you don't see the Sonenta MCP tools in your toolset, or the
|
|
81
|
+
call errors with "tool not found" / a connection failure) \u2192 the \`@sonenta/mcp\`
|
|
82
|
+
server isn't connected. **STOP and say so**, e.g.: "I can't run \u2014 I need the
|
|
83
|
+
\`@sonenta/mcp\` server connected with an \`mcp:*\` API key. Fix it: run
|
|
84
|
+
\`sonenta agents add <name>\` (it wires \`.mcp.json\` and preflights), then
|
|
85
|
+
RELOAD this Claude session so the server connects. Run \`sonenta doctor\` to see
|
|
86
|
+
the exact missing piece \u2014 it checks the wiring, host, key scope, and tools and
|
|
87
|
+
prints the precise next step."
|
|
88
|
+
- **Auth / scope error** (401 invalid key, or 403 \`MISSING_SCOPE\`) \u2192 the key is
|
|
89
|
+
missing or lacks \`mcp:*\`. STOP and relay the fix verbatim: a 403 carries
|
|
90
|
+
\`detail.how_to_get\` / \`detail.message\` (a ready-to-run command) \u2014 show it; or
|
|
91
|
+
point to \`sonenta doctor\`. Do NOT retry blindly.
|
|
92
|
+
- **Tools respond** \u2192 proceed with the workflow below.
|
|
93
|
+
NEVER probe arbitrary endpoints, guess tool names or URL paths, or hammer a
|
|
94
|
+
failing call \u2014 one clear, actionable message beats a pile of 404s.
|
|
95
|
+
|
|
96
|
+
`;
|
|
13
97
|
var SONENTA_A11Y = `---
|
|
14
98
|
name: sonenta-a11y
|
|
15
99
|
description: Accessibility (a11y) auditor and fixer for Sonenta-managed i18n projects. Runs a complete code-aware WCAG 2.2 audit, then works like sonenta-source-health \u2014 it builds a remediation PLAN, presents it and reassures you, touches NOTHING until you accept, and only then executes the fixes (a11y variants in bulk, reversible drafts). Generates the alt/aria/screen-reader/plain-language text itself and computes real readability locally, at zero AI-credit cost; server-side AI is an explicit opt-in fallback. Also applies the remediation plans prepared + approved in the Sonenta dashboard, and produces formal WCAG conformance + EAA / EN 301 549 statements. Use interactively in Claude Code or headless in CI.
|
|
@@ -45,7 +129,7 @@ default: those bill Sonenta AI credits and exist only as an explicit fallback fo
|
|
|
45
129
|
very large volumes or when the developer specifically asks for server-side
|
|
46
130
|
generation.
|
|
47
131
|
|
|
48
|
-
## Requirements
|
|
132
|
+
${PREFLIGHT}## Requirements
|
|
49
133
|
- The Sonenta MCP server (\`@sonenta/mcp\`) must be configured with an \`mcp:*\`
|
|
50
134
|
API key. Every operation goes through its tools \u2014 never call the HTTP API
|
|
51
135
|
directly. If the a11y tools are missing, tell the user to add the server
|
|
@@ -283,7 +367,7 @@ results with \`propose_translations_bulk\` as **drafts/proposed** \u2014 plain C
|
|
|
283
367
|
translation by default; where such a path exists it BILLS Sonenta AI credits and
|
|
284
368
|
is an explicit, estimate-first, opt-in fallback for very large volumes.
|
|
285
369
|
|
|
286
|
-
## Requirements
|
|
370
|
+
${PREFLIGHT}## Requirements
|
|
287
371
|
- The Sonenta MCP server (\`@sonenta/mcp\`) must be configured with an \`mcp:*\`
|
|
288
372
|
API key. Everything goes through its tools \u2014 never call the HTTP API directly.
|
|
289
373
|
If the tools are missing, tell the user to add the server
|
|
@@ -295,9 +379,27 @@ is an explicit, estimate-first, opt-in fallback for very large volumes.
|
|
|
295
379
|
\`update_keys_bulk\`, \`acknowledge_missing_keys\`.
|
|
296
380
|
- **Translate:** \`list_untranslated_keys\`, \`propose_translations_bulk\`
|
|
297
381
|
(your primary write \u2014 CRUD, 0 credits), \`validate_translations\`.
|
|
298
|
-
- **Consistency:** \`glossary_list\`, \`project_context_get
|
|
382
|
+
- **Consistency:** \`glossary_list\`, \`project_context_get\`,
|
|
383
|
+
\`list_placeholder_mismatches\` (audit: translations whose interpolation
|
|
384
|
+
variables drifted from the source \u2014 see **Placeholders**).
|
|
299
385
|
- **Ship:** \`publish_cdn\`.
|
|
300
386
|
|
|
387
|
+
## Placeholders \u2014 NEVER ship a translation that breaks interpolation
|
|
388
|
+
A translation must carry the SAME interpolation variables as its source value \u2014
|
|
389
|
+
by NAME, brace-agnostic (i18next \`{{name}}\`, ICU \`{name}\`, Ruby \`%{name}\`).
|
|
390
|
+
Dropping \`{{count}}\` or inventing \`{{total}}\` is a runtime bug (missing data or
|
|
391
|
+
a crash), so:
|
|
392
|
+
- When you translate, COPY the source's variables verbatim into your output (keep
|
|
393
|
+
the same names; you may reorder them for grammar). Don't translate, rename, or
|
|
394
|
+
drop a variable token.
|
|
395
|
+
- The project may set \`placeholder_enforcement = strict\`. Then
|
|
396
|
+
\`propose_translations_bulk\` REFUSES an offending item: that item comes back
|
|
397
|
+
\`{status:"error", error:{code:"PLACEHOLDER_MISMATCH"}, placeholder_mismatch:
|
|
398
|
+
{missing, extra, expected, got}}\` (the envelope also carries
|
|
399
|
+
\`placeholder_mismatch_count\`); good items still apply. In \`warn\` mode the item
|
|
400
|
+
applies but carries \`variable_warnings:{missing, extra}\`. Treat BOTH as a defect
|
|
401
|
+
to fix \u2014 see the workflow.
|
|
402
|
+
|
|
301
403
|
## Workflow
|
|
302
404
|
1. **Assess.** \`get_project_info\` (source + target languages, namespaces);
|
|
303
405
|
\`coverage_report\` (per-language completeness); \`health_report\`
|
|
@@ -318,14 +420,25 @@ is an explicit, estimate-first, opt-in fallback for very large volumes.
|
|
|
318
420
|
language, \`list_untranslated_keys\`. BEFORE translating, read
|
|
319
421
|
\`glossary_list\` (respect \`forbidden\` / \`do_not_translate\`, apply
|
|
320
422
|
\`translation\` rules) and \`project_context_get\` (brand voice, domain, tone).
|
|
321
|
-
Translate each value YOURSELF
|
|
423
|
+
Translate each value YOURSELF \u2014 **carrying the source's interpolation
|
|
424
|
+
variables verbatim** (see **Placeholders**) \u2014 then write a batch with
|
|
322
425
|
\`propose_translations_bulk\` (status draft/proposed). Work through the
|
|
323
426
|
languages and namespaces in sensible batches.
|
|
324
|
-
4. **
|
|
427
|
+
4. **Honor strict placeholders \u2014 fix and resubmit, never leave a mismatch.**
|
|
428
|
+
Inspect the \`propose_translations_bulk\` result: for any item with
|
|
429
|
+
\`error.code == "PLACEHOLDER_MISMATCH"\` (strict) OR \`variable_warnings\`
|
|
430
|
+
(warn), read \`placeholder_mismatch.{missing, extra, expected, got}\` (each
|
|
431
|
+
item is ONE language, so this is already per-language), REWRITE that value so
|
|
432
|
+
its variables are exactly \`expected\` \u2014 add every \`missing\`, drop every
|
|
433
|
+
\`extra\`, keep the names \u2014 and RESUBMIT the corrected item. Repeat until the
|
|
434
|
+
batch returns no \`PLACEHOLDER_MISMATCH\` and \`placeholder_mismatch_count == 0\`.
|
|
435
|
+
A strict-refused item was NOT written, so it is not done until it re-submits
|
|
436
|
+
clean. (Optionally \`list_placeholder_mismatches\` to audit the whole project.)
|
|
437
|
+
5. **Validate (optional).** \`validate_translations\` lints an i18next blob
|
|
325
438
|
server-side; fix anything it flags.
|
|
326
|
-
|
|
439
|
+
6. **Publish.** \`publish_cdn\` to release the bundles \u2014 with confirmation in
|
|
327
440
|
interactive mode, only on explicit authorization in CI.
|
|
328
|
-
|
|
441
|
+
7. **Report.** Show the coverage delta (before/after), counts of keys created and
|
|
329
442
|
strings translated, what remains, and that translations are drafts to review.
|
|
330
443
|
|
|
331
444
|
## Modes
|
|
@@ -346,12 +459,15 @@ is an explicit, estimate-first, opt-in fallback for very large volumes.
|
|
|
346
459
|
- Local translation + \`propose_translations_bulk\` is the default and costs 0
|
|
347
460
|
credits; any server-side AI translation is an explicit, estimated, opt-in
|
|
348
461
|
fallback.
|
|
462
|
+
- A translation must preserve the source's interpolation variables (by name).
|
|
463
|
+
Never leave a \`PLACEHOLDER_MISMATCH\` (strict) or \`variable_warnings\` (warn)
|
|
464
|
+
unresolved \u2014 fix the value and resubmit before considering the key done.
|
|
349
465
|
- Never \`publish_cdn\` without confirmation/authorization; stay within the
|
|
350
466
|
configured project.
|
|
351
467
|
`;
|
|
352
468
|
var SONENTA_SOURCE_HEALTH = `---
|
|
353
469
|
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.
|
|
470
|
+
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
471
|
---
|
|
356
472
|
|
|
357
473
|
You are **sonenta-source-health**, a careful repair specialist for
|
|
@@ -369,7 +485,7 @@ narrate what you are about to do, and wait for a clear yes. Reassure: nothing yo
|
|
|
369
485
|
propose is destructive until accepted, deletes are soft (trash, restorable), and
|
|
370
486
|
every change is a reviewable draft.
|
|
371
487
|
|
|
372
|
-
## Requirements
|
|
488
|
+
${PREFLIGHT}## Requirements
|
|
373
489
|
- The Sonenta MCP server (\`@sonenta/mcp\`) must be configured with an \`mcp:*\`
|
|
374
490
|
API key. Every operation goes through its tools \u2014 never call the HTTP API
|
|
375
491
|
directly. If the tools are missing, tell the user to add the server
|
|
@@ -394,6 +510,14 @@ every change is a reviewable draft.
|
|
|
394
510
|
optional \`note\` recording why. CRUD. Only on the dev's EXPLICIT acceptance \u2014
|
|
395
511
|
\`allowed\` in particular is the user's business decision, never an agent
|
|
396
512
|
default; never auto-mark a group the dev hasn't decided.
|
|
513
|
+
- \`list_placeholder_mismatches\` \u2014 your SECOND worklist (PM2): translations
|
|
514
|
+
whose interpolation variables drifted from the source. Each item =
|
|
515
|
+
{key_uuid, key, namespace_slug, language, source_value, source_vars[],
|
|
516
|
+
target_value, target_vars[], missing[], extra[]}. READ-ONLY \u2014 see
|
|
517
|
+
**Placeholder mismatch repair**.
|
|
518
|
+
- \`propose_translation\` \u2014 write ONE corrected target translation
|
|
519
|
+
(\`{namespace, key, language_code, value}\`, draft). CRUD, 0 credits. Your
|
|
520
|
+
write tool for placeholder repairs (the source value is untouched).
|
|
397
521
|
|
|
398
522
|
## Repair strategies (pick per group, propose explicitly)
|
|
399
523
|
For a group of keys sharing one source value, the right fix is usually one of:
|
|
@@ -453,6 +577,30 @@ decision to apply, so you PROPOSE a strategy (consolidate / disambiguate / allow
|
|
|
453
577
|
from the section above and act ONLY on the dev's explicit acceptance \u2014 never
|
|
454
578
|
auto-resolve or auto-allow a group the dev hasn't decided.
|
|
455
579
|
|
|
580
|
+
## Placeholder mismatch repair (PM2) \u2014 list, rewrite, RE-CHECK
|
|
581
|
+
Your second job: translations whose interpolation VARIABLES drifted from the
|
|
582
|
+
source (a broken \`{{name}}\` / \`{count}\` is a runtime bug \u2014 missing data or a
|
|
583
|
+
crash). Detection is by variable NAME, brace-agnostic (i18next \`{{}}\`, ICU
|
|
584
|
+
\`{}\`, Ruby \`%{}\`). This repair edits a TARGET TRANSLATION value, never the
|
|
585
|
+
source \u2014 so it's low-risk and reversible (a draft), but you still go step by step.
|
|
586
|
+
1. **List.** \`list_placeholder_mismatches\` (optionally scoped by \`key_id\` /
|
|
587
|
+
\`language_code\`). Each item gives \`source_vars\`, the offending
|
|
588
|
+
\`target_value\` / \`target_vars\`, and the \`missing\` / \`extra\` variables for
|
|
589
|
+
that language. Present the worklist (key, language, what's missing/extra).
|
|
590
|
+
2. **Rewrite \u2014 yourself, 0 credits.** For each item, rewrite \`target_value\` so
|
|
591
|
+
its variables are exactly \`source_vars\`: ADD every \`missing\` token, REMOVE
|
|
592
|
+
every \`extra\` one, keep the names identical (you may reposition them for
|
|
593
|
+
grammar), and preserve the translation's meaning. Don't translate or rename a
|
|
594
|
+
variable token.
|
|
595
|
+
3. **Write on acceptance.** Persist each corrected value with
|
|
596
|
+
\`propose_translation(namespace, key, language_code, value)\` (a draft; the
|
|
597
|
+
source value is untouched). In interactive mode propose the rewrites and apply
|
|
598
|
+
on the dev's yes; in CI apply only when the run authorizes auto-fix.
|
|
599
|
+
4. **RE-CHECK (mandatory).** Re-call \`list_placeholder_mismatches\` \u2014 there is NO
|
|
600
|
+
server-side auto-fix, so the repair is only confirmed when \`total == 0\` (or
|
|
601
|
+
the item is gone). If something still mismatches, you mis-rewrote it \u2014 fix and
|
|
602
|
+
re-check again.
|
|
603
|
+
|
|
456
604
|
## Workflow (strictly ordered)
|
|
457
605
|
1. **List the affected files first.** Call \`list_source_duplicates(status=to_fix)\`.
|
|
458
606
|
For each group, resolve the keys to their human locations with \`list_keys\`
|
|
@@ -544,7 +692,7 @@ Knowledge is human-owned. You **propose, then confirm before you overwrite**.
|
|
|
544
692
|
- Everything you write is a reviewable change \u2014 present proposals as proposals,
|
|
545
693
|
not as done deals.
|
|
546
694
|
|
|
547
|
-
## Requirements
|
|
695
|
+
${PREFLIGHT}## Requirements
|
|
548
696
|
- The Sonenta MCP server (\`@sonenta/mcp\`) must be configured with an \`mcp:*\`
|
|
549
697
|
API key. Every operation goes through its tools \u2014 never call the HTTP API
|
|
550
698
|
directly. If the tools are missing, tell the user to add the server
|
|
@@ -705,7 +853,7 @@ There is no server-side AI in this agent.
|
|
|
705
853
|
\`update_surface\` response \`affected_variants\` reports how many variants a
|
|
706
854
|
toggle impacts \u2014 always state that blast radius before toggling.
|
|
707
855
|
|
|
708
|
-
## Requirements
|
|
856
|
+
${PREFLIGHT}## Requirements
|
|
709
857
|
- The Sonenta MCP server (\`@sonenta/mcp\`) must be configured with an \`mcp:*\`
|
|
710
858
|
API key. Every operation goes through its tools \u2014 never call the HTTP API
|
|
711
859
|
directly. If the tools are missing, tell the user to add the server
|
|
@@ -843,12 +991,12 @@ var AGENTS = {
|
|
|
843
991
|
},
|
|
844
992
|
"sonenta-i18n": {
|
|
845
993
|
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.",
|
|
994
|
+
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
995
|
content: SONENTA_I18N
|
|
848
996
|
},
|
|
849
997
|
"sonenta-source-health": {
|
|
850
998
|
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).",
|
|
999
|
+
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
1000
|
content: SONENTA_SOURCE_HEALTH
|
|
853
1001
|
},
|
|
854
1002
|
"sonenta-knowledge": {
|
|
@@ -1119,6 +1267,14 @@ function buildServerBlock(env, opts = {}) {
|
|
|
1119
1267
|
if (env.projectUuid) e.SONENTA_PROJECT = env.projectUuid;
|
|
1120
1268
|
return { command: "npx", args: ["-y", MCP_PACKAGE], env: e };
|
|
1121
1269
|
}
|
|
1270
|
+
async function readWiredServer(baseDir = process.cwd()) {
|
|
1271
|
+
const { json } = await readMcpJson(resolve3(baseDir, MCP_JSON_FILENAME));
|
|
1272
|
+
const servers = json.mcpServers;
|
|
1273
|
+
if (!servers || typeof servers !== "object") return null;
|
|
1274
|
+
const block = servers[MCP_SERVER_KEY];
|
|
1275
|
+
if (!block || typeof block !== "object") return null;
|
|
1276
|
+
return block;
|
|
1277
|
+
}
|
|
1122
1278
|
async function readMcpJson(path) {
|
|
1123
1279
|
try {
|
|
1124
1280
|
const raw = await fs4.readFile(path, "utf8");
|
|
@@ -1186,9 +1342,239 @@ function deepEqual(a, b) {
|
|
|
1186
1342
|
);
|
|
1187
1343
|
}
|
|
1188
1344
|
|
|
1345
|
+
// src/doctor.ts
|
|
1346
|
+
var CANON_HOST = "https://api.sonenta.dev";
|
|
1347
|
+
var KEYS_HINT = "create one in the dashboard \u2192 Org Settings \u2192 API Keys (scope mcp:*), then run `sonenta login`";
|
|
1348
|
+
function hasMcpScope(scopes) {
|
|
1349
|
+
if (!scopes) return false;
|
|
1350
|
+
return scopes.some((s) => s === "*" || s === "mcp:*" || s.startsWith("mcp:"));
|
|
1351
|
+
}
|
|
1352
|
+
async function probe(fetchImpl, host, apiKey, path) {
|
|
1353
|
+
const url = `${host.replace(/\/+$/, "")}${path}`;
|
|
1354
|
+
try {
|
|
1355
|
+
const res = await fetchImpl(url, { headers: { Authorization: `ApiKey ${apiKey}` } });
|
|
1356
|
+
let json;
|
|
1357
|
+
try {
|
|
1358
|
+
json = await res.json();
|
|
1359
|
+
} catch {
|
|
1360
|
+
json = void 0;
|
|
1361
|
+
}
|
|
1362
|
+
return { status: res.status, ok: res.ok, json };
|
|
1363
|
+
} catch (e) {
|
|
1364
|
+
return { status: 0, ok: false, networkError: e.message };
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
async function runDoctor(opts = {}) {
|
|
1368
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
1369
|
+
const dir = opts.dir ?? process.cwd();
|
|
1370
|
+
const checks = [];
|
|
1371
|
+
const { path: cfgPath, config } = await readConfig(dir).catch(() => ({
|
|
1372
|
+
path: null,
|
|
1373
|
+
config: {}
|
|
1374
|
+
}));
|
|
1375
|
+
checks.push({
|
|
1376
|
+
id: "config",
|
|
1377
|
+
title: "Project config",
|
|
1378
|
+
status: cfgPath ? "pass" : "warn",
|
|
1379
|
+
detail: cfgPath ? `sonenta.config.json found${config.project_uuid ? ` (project ${config.project_uuid})` : ""}` : "no sonenta.config.json in this directory",
|
|
1380
|
+
fix: cfgPath ? void 0 : "run `sonenta init --project <uuid>` to scaffold it and bind a project"
|
|
1381
|
+
});
|
|
1382
|
+
let ctx = null;
|
|
1383
|
+
try {
|
|
1384
|
+
ctx = await resolveContext({ hostOverride: opts.hostOverride, cwd: dir });
|
|
1385
|
+
checks.push({
|
|
1386
|
+
id: "login",
|
|
1387
|
+
title: "Login",
|
|
1388
|
+
status: "pass",
|
|
1389
|
+
detail: `credentials resolved for ${ctx.host}`
|
|
1390
|
+
});
|
|
1391
|
+
} catch (e) {
|
|
1392
|
+
checks.push({
|
|
1393
|
+
id: "login",
|
|
1394
|
+
title: "Login",
|
|
1395
|
+
status: "fail",
|
|
1396
|
+
detail: e.message,
|
|
1397
|
+
fix: `run \`sonenta login --host ${opts.hostOverride ?? CANON_HOST}\``
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
const wired = await readWiredServer(dir).catch(() => null);
|
|
1401
|
+
checks.push({
|
|
1402
|
+
id: "mcp_wired",
|
|
1403
|
+
title: "MCP server wired",
|
|
1404
|
+
status: wired ? "pass" : "fail",
|
|
1405
|
+
detail: wired ? `.mcp.json declares the "sonenta" server (${MCP_PACKAGE})` : 'no "sonenta" server in .mcp.json',
|
|
1406
|
+
fix: wired ? void 0 : "run `sonenta agents add <name>` (or `sonenta init`) to wire it, then reload your Claude session"
|
|
1407
|
+
});
|
|
1408
|
+
if (!ctx) {
|
|
1409
|
+
return finalize(checks);
|
|
1410
|
+
}
|
|
1411
|
+
const me = await probe(fetchImpl, ctx.host, ctx.apiKey, "/v1/me");
|
|
1412
|
+
if (me.networkError) {
|
|
1413
|
+
checks.push({
|
|
1414
|
+
id: "api",
|
|
1415
|
+
title: "API reachable",
|
|
1416
|
+
status: "fail",
|
|
1417
|
+
detail: `could not reach ${ctx.host}: ${me.networkError}`,
|
|
1418
|
+
fix: `check your connection and the host \u2014 the canonical host is ${CANON_HOST} (\`sonenta login --host ${CANON_HOST}\`)`
|
|
1419
|
+
});
|
|
1420
|
+
return finalize(checks);
|
|
1421
|
+
}
|
|
1422
|
+
if (me.status === 404) {
|
|
1423
|
+
checks.push({
|
|
1424
|
+
id: "api",
|
|
1425
|
+
title: "API reachable",
|
|
1426
|
+
status: "fail",
|
|
1427
|
+
detail: `${ctx.host} returned 404 for /v1/me \u2014 almost certainly the wrong host`,
|
|
1428
|
+
fix: `use the canonical host: \`sonenta login --host ${CANON_HOST}\``
|
|
1429
|
+
});
|
|
1430
|
+
return finalize(checks);
|
|
1431
|
+
}
|
|
1432
|
+
if (me.status === 401) {
|
|
1433
|
+
checks.push({
|
|
1434
|
+
id: "api",
|
|
1435
|
+
title: "API key valid",
|
|
1436
|
+
status: "fail",
|
|
1437
|
+
detail: "the API returned 401 \u2014 your key is invalid or revoked",
|
|
1438
|
+
fix: `run \`sonenta login\` with a current key (${KEYS_HINT})`
|
|
1439
|
+
});
|
|
1440
|
+
return finalize(checks);
|
|
1441
|
+
}
|
|
1442
|
+
if (!me.ok) {
|
|
1443
|
+
checks.push({
|
|
1444
|
+
id: "api",
|
|
1445
|
+
title: "API reachable",
|
|
1446
|
+
status: "fail",
|
|
1447
|
+
detail: `unexpected ${me.status} from ${ctx.host}/v1/me`,
|
|
1448
|
+
fix: `verify the host (${CANON_HOST}) and try again`
|
|
1449
|
+
});
|
|
1450
|
+
return finalize(checks);
|
|
1451
|
+
}
|
|
1452
|
+
checks.push({
|
|
1453
|
+
id: "api",
|
|
1454
|
+
title: "API reachable",
|
|
1455
|
+
status: "pass",
|
|
1456
|
+
detail: `${ctx.host} responded 200 to /v1/me`
|
|
1457
|
+
});
|
|
1458
|
+
const accountActive = me.json?.account_active !== false;
|
|
1459
|
+
if (!accountActive) {
|
|
1460
|
+
checks.push({
|
|
1461
|
+
id: "account",
|
|
1462
|
+
title: "Account active",
|
|
1463
|
+
status: "fail",
|
|
1464
|
+
detail: "your account/subscription is inactive",
|
|
1465
|
+
fix: "reactivate your subscription in the Sonenta dashboard"
|
|
1466
|
+
});
|
|
1467
|
+
} else {
|
|
1468
|
+
checks.push({ id: "account", title: "Account active", status: "pass", detail: "account active" });
|
|
1469
|
+
}
|
|
1470
|
+
const scopes = Array.isArray(me.json?.scopes) ? me.json.scopes : void 0;
|
|
1471
|
+
const mcpOk = hasMcpScope(scopes);
|
|
1472
|
+
checks.push({
|
|
1473
|
+
id: "mcp_scope",
|
|
1474
|
+
title: "Key has mcp:* scope",
|
|
1475
|
+
status: mcpOk ? "pass" : "fail",
|
|
1476
|
+
detail: mcpOk ? "the key carries the mcp:* scope" : `the key lacks the mcp:* scope${scopes ? ` (has: ${scopes.join(", ") || "none"})` : ""}`,
|
|
1477
|
+
fix: mcpOk ? void 0 : `the agents drive the MCP tools, which need an mcp:* key \u2014 ${KEYS_HINT}`
|
|
1478
|
+
});
|
|
1479
|
+
if (!ctx.projectUuid) {
|
|
1480
|
+
checks.push({
|
|
1481
|
+
id: "a11y",
|
|
1482
|
+
title: "a11y tools respond",
|
|
1483
|
+
status: "skip",
|
|
1484
|
+
detail: "no project bound, so the per-project a11y surface can't be probed",
|
|
1485
|
+
fix: "bind a project: `sonenta init --project <uuid>`"
|
|
1486
|
+
});
|
|
1487
|
+
return finalize(checks);
|
|
1488
|
+
}
|
|
1489
|
+
const surf = await probe(
|
|
1490
|
+
fetchImpl,
|
|
1491
|
+
ctx.host,
|
|
1492
|
+
ctx.apiKey,
|
|
1493
|
+
`/v1/mcp/projects/${ctx.projectUuid}/surfaces`
|
|
1494
|
+
);
|
|
1495
|
+
if (surf.ok) {
|
|
1496
|
+
checks.push({
|
|
1497
|
+
id: "a11y",
|
|
1498
|
+
title: "a11y tools respond",
|
|
1499
|
+
status: "pass",
|
|
1500
|
+
detail: "the project's a11y/surface MCP endpoint responded"
|
|
1501
|
+
});
|
|
1502
|
+
} else if (surf.status === 403) {
|
|
1503
|
+
const detail = surf.json?.detail ?? surf.json;
|
|
1504
|
+
const isMissingScope = detail?.code === "MISSING_SCOPE";
|
|
1505
|
+
const howTo = typeof detail?.how_to_get === "string" ? detail.how_to_get : void 0;
|
|
1506
|
+
const msg = typeof detail?.message === "string" ? detail.message : void 0;
|
|
1507
|
+
checks.push({
|
|
1508
|
+
id: "a11y",
|
|
1509
|
+
title: "a11y tools respond",
|
|
1510
|
+
status: "fail",
|
|
1511
|
+
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",
|
|
1512
|
+
fix: msg ?? (howTo ? `get a scoped key: ${howTo}` : `the key needs the mcp:* scope for this project \u2014 ${KEYS_HINT}`)
|
|
1513
|
+
});
|
|
1514
|
+
} else if (surf.status === 404) {
|
|
1515
|
+
checks.push({
|
|
1516
|
+
id: "a11y",
|
|
1517
|
+
title: "a11y tools respond",
|
|
1518
|
+
status: "fail",
|
|
1519
|
+
detail: `project ${ctx.projectUuid} not found on ${ctx.host} (404)`,
|
|
1520
|
+
fix: "check project_uuid in sonenta.config.json and the host (`sonenta init --project <uuid>`)"
|
|
1521
|
+
});
|
|
1522
|
+
} else {
|
|
1523
|
+
checks.push({
|
|
1524
|
+
id: "a11y",
|
|
1525
|
+
title: "a11y tools respond",
|
|
1526
|
+
status: "fail",
|
|
1527
|
+
detail: surf.networkError ? `could not reach the a11y surface: ${surf.networkError}` : `unexpected ${surf.status} from the a11y MCP surface`,
|
|
1528
|
+
fix: `verify the host (${CANON_HOST}) and that the project is reachable`
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
return finalize(checks);
|
|
1532
|
+
}
|
|
1533
|
+
function finalize(checks) {
|
|
1534
|
+
return { checks, ok: !checks.some((c) => c.status === "fail") };
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/commands/doctor.ts
|
|
1538
|
+
import { Command } from "commander";
|
|
1539
|
+
var MARK = {
|
|
1540
|
+
pass: "\u2713",
|
|
1541
|
+
fail: "\u2717",
|
|
1542
|
+
warn: "!",
|
|
1543
|
+
skip: "\xB7"
|
|
1544
|
+
};
|
|
1545
|
+
function formatReport(report) {
|
|
1546
|
+
const lines = ["sonenta doctor \u2014 agent preflight", ""];
|
|
1547
|
+
const width = Math.max(...report.checks.map((c) => c.title.length));
|
|
1548
|
+
for (const c of report.checks) {
|
|
1549
|
+
lines.push(`${MARK[c.status]} ${c.title.padEnd(width)} ${c.detail}`);
|
|
1550
|
+
if (c.fix && (c.status === "fail" || c.status === "warn" || c.status === "skip")) {
|
|
1551
|
+
lines.push(`${" ".repeat(width + 4)}\u2192 Fix: ${c.fix}`);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
lines.push("");
|
|
1555
|
+
if (report.ok) {
|
|
1556
|
+
lines.push(
|
|
1557
|
+
"All checks passed. If your agent still shows no tools, reload your Claude Code session \u2014 the MCP server connects at session start."
|
|
1558
|
+
);
|
|
1559
|
+
} else {
|
|
1560
|
+
const failed = report.checks.filter((c) => c.status === "fail").length;
|
|
1561
|
+
lines.push(
|
|
1562
|
+
`${failed} check${failed === 1 ? "" : "s"} failed \u2014 fix the step(s) above, then re-run \`sonenta doctor\`.`
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
return lines;
|
|
1566
|
+
}
|
|
1567
|
+
var doctorCommand = new Command("doctor").description(
|
|
1568
|
+
"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."
|
|
1569
|
+
).option("--dir <path>", "Project directory (default: current directory)").option("--host <url>", "Override host (otherwise from config/credentials)").action(async (opts) => {
|
|
1570
|
+
const report = await runDoctor({ dir: opts.dir, hostOverride: opts.host });
|
|
1571
|
+
for (const line of formatReport(report)) console.log(line);
|
|
1572
|
+
if (!report.ok) process.exit(1);
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1189
1575
|
// src/commands/agents.ts
|
|
1190
|
-
var agentsCommand = new
|
|
1191
|
-
new
|
|
1576
|
+
var agentsCommand = new Command2("agents").description("Install bundled Claude agents (e.g. sonenta-a11y) into .claude/agents/.").addCommand(
|
|
1577
|
+
new Command2("list").description("List the bundled agents available to install.").option("--dir <path>", "Project directory (default: current directory)").action(async (opts) => {
|
|
1192
1578
|
const baseDir = opts.dir;
|
|
1193
1579
|
const agents = listAgents();
|
|
1194
1580
|
console.log(`Available agents (${agents.length}):`);
|
|
@@ -1202,7 +1588,7 @@ var agentsCommand = new Command("agents").description("Install bundled Claude ag
|
|
|
1202
1588
|
Install with: sonenta agents add <name>`);
|
|
1203
1589
|
})
|
|
1204
1590
|
).addCommand(
|
|
1205
|
-
new
|
|
1591
|
+
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(
|
|
1206
1592
|
"--embed-key",
|
|
1207
1593
|
"Bake the API key into .mcp.json (for CI / no-login); otherwise the server reads it from ~/.sonenta",
|
|
1208
1594
|
false
|
|
@@ -1245,6 +1631,9 @@ Skipped .mcp.json (--no-mcp). The ${name} agent needs the Sonenta MCP server (np
|
|
|
1245
1631
|
);
|
|
1246
1632
|
}
|
|
1247
1633
|
console.log(`Agent dir: ${AGENTS_DIR}/`);
|
|
1634
|
+
console.log("");
|
|
1635
|
+
const report = await runDoctor({ dir: opts.dir, hostOverride: opts.host });
|
|
1636
|
+
for (const line of formatReport(report)) console.log(line);
|
|
1248
1637
|
}
|
|
1249
1638
|
)
|
|
1250
1639
|
);
|
|
@@ -1252,7 +1641,7 @@ Skipped .mcp.json (--no-mcp). The ${name} agent needs the Sonenta MCP server (np
|
|
|
1252
1641
|
// src/commands/export.ts
|
|
1253
1642
|
import { promises as fs5 } from "fs";
|
|
1254
1643
|
import { join as join2 } from "path";
|
|
1255
|
-
import { Command as
|
|
1644
|
+
import { Command as Command3 } from "commander";
|
|
1256
1645
|
|
|
1257
1646
|
// src/i18next_tree.ts
|
|
1258
1647
|
function unflatten(flat, sep = ".") {
|
|
@@ -1377,7 +1766,7 @@ async function collect(ctx, languages, namespace) {
|
|
|
1377
1766
|
}
|
|
1378
1767
|
return out;
|
|
1379
1768
|
}
|
|
1380
|
-
var exportCommand = new
|
|
1769
|
+
var exportCommand = new Command3("export").description(
|
|
1381
1770
|
"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."
|
|
1382
1771
|
).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(
|
|
1383
1772
|
async (opts) => {
|
|
@@ -1412,7 +1801,7 @@ var exportCommand = new Command2("export").description(
|
|
|
1412
1801
|
// src/commands/import.ts
|
|
1413
1802
|
import { promises as fs6 } from "fs";
|
|
1414
1803
|
import { basename, dirname as dirname3 } from "path";
|
|
1415
|
-
import { Command as
|
|
1804
|
+
import { Command as Command4 } from "commander";
|
|
1416
1805
|
function resolveLangNs(filePath, optLang, optNs) {
|
|
1417
1806
|
const stem = basename(filePath).replace(/\.json$/i, "");
|
|
1418
1807
|
const parent = basename(dirname3(filePath));
|
|
@@ -1442,7 +1831,7 @@ function countLeaves(tree) {
|
|
|
1442
1831
|
}
|
|
1443
1832
|
return n;
|
|
1444
1833
|
}
|
|
1445
|
-
var importCommand = new
|
|
1834
|
+
var importCommand = new Command4("import").description(
|
|
1446
1835
|
"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."
|
|
1447
1836
|
).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(
|
|
1448
1837
|
async (files, opts) => {
|
|
@@ -1486,7 +1875,7 @@ var importCommand = new Command3("import").description(
|
|
|
1486
1875
|
// src/commands/init.ts
|
|
1487
1876
|
import { existsSync } from "fs";
|
|
1488
1877
|
import { resolve as resolve5 } from "path";
|
|
1489
|
-
import { Command as
|
|
1878
|
+
import { Command as Command5 } from "commander";
|
|
1490
1879
|
|
|
1491
1880
|
// src/repodoc.ts
|
|
1492
1881
|
import { promises as fs7 } from "fs";
|
|
@@ -1652,7 +2041,7 @@ async function gatherRepoDocData(opts) {
|
|
|
1652
2041
|
};
|
|
1653
2042
|
}
|
|
1654
2043
|
}
|
|
1655
|
-
var initCommand = new
|
|
2044
|
+
var initCommand = new Command5("init").description(
|
|
1656
2045
|
"Scaffold sonenta.config.json AND write a managed Sonenta block into CLAUDE.md / AGENTS.md so coding agents know how this repo uses Sonenta."
|
|
1657
2046
|
).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(
|
|
1658
2047
|
"--embed-key",
|
|
@@ -1740,9 +2129,9 @@ var initCommand = new Command4("init").description(
|
|
|
1740
2129
|
);
|
|
1741
2130
|
|
|
1742
2131
|
// src/commands/keys.ts
|
|
1743
|
-
import { Command as
|
|
1744
|
-
var keysCommand = new
|
|
1745
|
-
new
|
|
2132
|
+
import { Command as Command6 } from "commander";
|
|
2133
|
+
var keysCommand = new Command6("keys").description("Inspect translation keys for the current project.").addCommand(
|
|
2134
|
+
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) => {
|
|
1746
2135
|
const ctx = await requireAuth({ hostOverride: opts.host });
|
|
1747
2136
|
const items = await listKeys(ctx, { namespace: opts.namespace });
|
|
1748
2137
|
console.log(`total: ${items.length}`);
|
|
@@ -1751,7 +2140,7 @@ var keysCommand = new Command5("keys").description("Inspect translation keys for
|
|
|
1751
2140
|
);
|
|
1752
2141
|
|
|
1753
2142
|
// src/commands/login.ts
|
|
1754
|
-
import { Command as
|
|
2143
|
+
import { Command as Command7 } from "commander";
|
|
1755
2144
|
|
|
1756
2145
|
// src/prompt.ts
|
|
1757
2146
|
import { createInterface } from "readline";
|
|
@@ -1812,7 +2201,7 @@ async function promptSecret(message) {
|
|
|
1812
2201
|
|
|
1813
2202
|
// src/commands/login.ts
|
|
1814
2203
|
var TOKEN_REGEX = /^vrb_[a-z]+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
1815
|
-
var loginCommand = new
|
|
2204
|
+
var loginCommand = new Command7("login").description(
|
|
1816
2205
|
"Store an API key for a host. Token resolution order: --token, SONENTA_TOKEN env, then interactive prompt (TTY only)."
|
|
1817
2206
|
).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) => {
|
|
1818
2207
|
let host = opts.host;
|
|
@@ -1863,8 +2252,8 @@ var loginCommand = new Command6("login").description(
|
|
|
1863
2252
|
});
|
|
1864
2253
|
|
|
1865
2254
|
// src/commands/logout.ts
|
|
1866
|
-
import { Command as
|
|
1867
|
-
var logoutCommand = new
|
|
2255
|
+
import { Command as Command8 } from "commander";
|
|
2256
|
+
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) => {
|
|
1868
2257
|
const creds = await readCredentials();
|
|
1869
2258
|
const target = opts.host ?? creds.default;
|
|
1870
2259
|
if (!target) {
|
|
@@ -1880,8 +2269,8 @@ var logoutCommand = new Command7("logout").description("Remove stored credential
|
|
|
1880
2269
|
});
|
|
1881
2270
|
|
|
1882
2271
|
// src/commands/missing.ts
|
|
1883
|
-
import { Command as
|
|
1884
|
-
var missingCommand = new
|
|
2272
|
+
import { Command as Command9 } from "commander";
|
|
2273
|
+
var missingCommand = new Command9("missing").description(
|
|
1885
2274
|
"List runtime-detected missing keys for the configured project. Requires the API key to carry the `mcp:*` scope."
|
|
1886
2275
|
).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(
|
|
1887
2276
|
async (opts) => {
|
|
@@ -1915,9 +2304,9 @@ var missingCommand = new Command8("missing").description(
|
|
|
1915
2304
|
);
|
|
1916
2305
|
|
|
1917
2306
|
// src/commands/projects.ts
|
|
1918
|
-
import { Command as
|
|
1919
|
-
var projectsCommand = new
|
|
1920
|
-
new
|
|
2307
|
+
import { Command as Command10 } from "commander";
|
|
2308
|
+
var projectsCommand = new Command10("projects").description("Inspect Verbumia projects accessible to your API key.").addCommand(
|
|
2309
|
+
new Command10("list").description("List the projects this API key can reach.").option("--host <url>", "Override host (otherwise from config/credentials)").action(async (opts) => {
|
|
1921
2310
|
const ctx = await requireAuth({ hostOverride: opts.host });
|
|
1922
2311
|
const items = await listProjects(ctx);
|
|
1923
2312
|
if (items.length === 0) {
|
|
@@ -1933,7 +2322,7 @@ var projectsCommand = new Command9("projects").description("Inspect Verbumia pro
|
|
|
1933
2322
|
);
|
|
1934
2323
|
|
|
1935
2324
|
// src/commands/pull.ts
|
|
1936
|
-
import { Command as
|
|
2325
|
+
import { Command as Command11 } from "commander";
|
|
1937
2326
|
|
|
1938
2327
|
// src/locales.ts
|
|
1939
2328
|
import { promises as fs8 } from "fs";
|
|
@@ -2011,7 +2400,7 @@ function diffFlat(local, remote) {
|
|
|
2011
2400
|
}
|
|
2012
2401
|
|
|
2013
2402
|
// src/commands/pull.ts
|
|
2014
|
-
var pullCommand = new
|
|
2403
|
+
var pullCommand = new Command11("pull").description(
|
|
2015
2404
|
"Pull translations from Verbumia into locales/<lang>/<namespace>.json (flat dot-notation). Overwrites local files \u2014 pair with `git status`."
|
|
2016
2405
|
).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(
|
|
2017
2406
|
async (opts) => {
|
|
@@ -2050,8 +2439,8 @@ var pullCommand = new Command10("pull").description(
|
|
|
2050
2439
|
);
|
|
2051
2440
|
|
|
2052
2441
|
// src/commands/push.ts
|
|
2053
|
-
import { Command as
|
|
2054
|
-
var pushCommand = new
|
|
2442
|
+
import { Command as Command12 } from "commander";
|
|
2443
|
+
var pushCommand = new Command12("push").description(
|
|
2055
2444
|
"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)."
|
|
2056
2445
|
).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(
|
|
2057
2446
|
async (opts) => {
|
|
@@ -2098,9 +2487,9 @@ var pushCommand = new Command11("push").description(
|
|
|
2098
2487
|
);
|
|
2099
2488
|
|
|
2100
2489
|
// src/commands/releases.ts
|
|
2101
|
-
import { Command as
|
|
2102
|
-
var releasesCommand = new
|
|
2103
|
-
new
|
|
2490
|
+
import { Command as Command13 } from "commander";
|
|
2491
|
+
var releasesCommand = new Command13("releases").description("Manage CDN releases for the project.").addCommand(
|
|
2492
|
+
new Command13("publish").description(
|
|
2104
2493
|
"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."
|
|
2105
2494
|
).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(
|
|
2106
2495
|
async (opts) => {
|
|
@@ -2127,7 +2516,7 @@ var releasesCommand = new Command12("releases").description("Manage CDN releases
|
|
|
2127
2516
|
|
|
2128
2517
|
// src/commands/snapshot.ts
|
|
2129
2518
|
import { promises as fs9 } from "fs";
|
|
2130
|
-
import { Command as
|
|
2519
|
+
import { Command as Command14 } from "commander";
|
|
2131
2520
|
function bundleUrl(cdnBase, project, version, lang, ns) {
|
|
2132
2521
|
return `${cdnBase.replace(/\/+$/, "")}/p/${project}/${version}/latest/${lang}/${ns}.json`;
|
|
2133
2522
|
}
|
|
@@ -2151,7 +2540,7 @@ async function fetchBundle(url) {
|
|
|
2151
2540
|
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
2152
2541
|
return await res.json();
|
|
2153
2542
|
}
|
|
2154
|
-
var snapshotCommand = new
|
|
2543
|
+
var snapshotCommand = new Command14("snapshot").description(
|
|
2155
2544
|
"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."
|
|
2156
2545
|
).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(
|
|
2157
2546
|
async (opts) => {
|
|
@@ -2205,8 +2594,8 @@ var snapshotCommand = new Command13("snapshot").description(
|
|
|
2205
2594
|
);
|
|
2206
2595
|
|
|
2207
2596
|
// src/commands/status.ts
|
|
2208
|
-
import { Command as
|
|
2209
|
-
var statusCommand = new
|
|
2597
|
+
import { Command as Command15 } from "commander";
|
|
2598
|
+
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(
|
|
2210
2599
|
async (opts) => {
|
|
2211
2600
|
const ctx = await requireAuth({ hostOverride: opts.host });
|
|
2212
2601
|
const knownLangs = new Set((await getProjectInfo(ctx)).languages);
|
|
@@ -2264,8 +2653,8 @@ var statusCommand = new Command14("status").description("Diff local locales/ aga
|
|
|
2264
2653
|
);
|
|
2265
2654
|
|
|
2266
2655
|
// src/commands/whoami.ts
|
|
2267
|
-
import { Command as
|
|
2268
|
-
var whoamiCommand = new
|
|
2656
|
+
import { Command as Command16 } from "commander";
|
|
2657
|
+
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) => {
|
|
2269
2658
|
const creds = await readCredentials();
|
|
2270
2659
|
if (!creds.default && Object.keys(creds.hosts).length === 0) {
|
|
2271
2660
|
console.log("Not logged in. Run `sonenta login --host <url> --token <\u2026>`.");
|
|
@@ -2293,8 +2682,8 @@ var whoamiCommand = new Command15("whoami").description("Show the configured def
|
|
|
2293
2682
|
});
|
|
2294
2683
|
|
|
2295
2684
|
// src/index.ts
|
|
2296
|
-
var program = new
|
|
2297
|
-
program.name("sonenta").description("CLI for Sonenta translation management.").version(
|
|
2685
|
+
var program = new Command17();
|
|
2686
|
+
program.name("sonenta").description("CLI for Sonenta translation management.").version(package_default.version);
|
|
2298
2687
|
program.addCommand(loginCommand);
|
|
2299
2688
|
program.addCommand(logoutCommand);
|
|
2300
2689
|
program.addCommand(whoamiCommand);
|
|
@@ -2310,6 +2699,7 @@ program.addCommand(releasesCommand);
|
|
|
2310
2699
|
program.addCommand(snapshotCommand);
|
|
2311
2700
|
program.addCommand(missingCommand);
|
|
2312
2701
|
program.addCommand(agentsCommand);
|
|
2702
|
+
program.addCommand(doctorCommand);
|
|
2313
2703
|
program.parseAsync(process.argv).catch((err) => {
|
|
2314
2704
|
console.error(err instanceof Error ? err.message : err);
|
|
2315
2705
|
process.exit(1);
|