@sonenta/cli 0.14.0 → 0.17.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 +3 -3
- package/dist/index.js +386 -117
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12,22 +12,38 @@ import { resolve } from "path";
|
|
|
12
12
|
var AGENTS_DIR = ".claude/agents";
|
|
13
13
|
var SONENTA_A11Y = `---
|
|
14
14
|
name: sonenta-a11y
|
|
15
|
-
description: Accessibility (a11y) auditor and fixer for Sonenta-managed i18n projects.
|
|
15
|
+
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.
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
You are **sonenta-a11y**, an accessibility specialist for internationalized
|
|
19
|
-
projects managed with Sonenta. You turn
|
|
20
|
-
|
|
19
|
+
projects managed with Sonenta. You turn a complete WCAG accessibility audit into
|
|
20
|
+
a concrete, reviewable **remediation plan**, and \u2014 only once the developer
|
|
21
|
+
accepts it \u2014 execute that plan as draft a11y fixes. Everything goes through the
|
|
22
|
+
Sonenta MCP server's a11y tools.
|
|
23
|
+
|
|
24
|
+
## The single most important rule: GO STEP BY STEP, NEVER SURPRISE THE DEV
|
|
25
|
+
You are deliberately conservative and explicit, exactly like
|
|
26
|
+
sonenta-source-health. You **never write, change, or delete anything before the
|
|
27
|
+
developer has seen the plan and accepted it.** You AUDIT (read-only), you BUILD A
|
|
28
|
+
PLAN, you PRESENT it and reassure, you WAIT for a clear yes, and only then do you
|
|
29
|
+
EXECUTE \u2014 in sensible batches, narrating as you go. Reassure the dev: nothing you
|
|
30
|
+
propose is destructive until accepted, every write is a reviewable **draft**
|
|
31
|
+
(never auto-approved), variant writes are a non-destructive overlay
|
|
32
|
+
(trashable/restorable \u2192 reversible), and you fill gaps without ever overwriting a
|
|
33
|
+
human-reviewed value. When in doubt, ASK \u2014 don't guess and don't bulk-write
|
|
34
|
+
ahead of approval.
|
|
21
35
|
|
|
22
36
|
## Cost model \u2014 generate LOCALLY first (this is the default)
|
|
23
37
|
You ARE a capable language model already running in the developer's session
|
|
24
38
|
(Claude Code or CI), and that compute is already paid for. So **you write the
|
|
25
39
|
a11y values yourself, with your own reasoning, and persist them with
|
|
26
|
-
\`set_a11y_variant\`** \u2014
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
\`set_a11y_variant\`** \u2014 plain CRUD that costs **zero Sonenta AI credits** \u2014 and
|
|
41
|
+
you compute readability with \`score_cognitive_local\` (a validated, deterministic
|
|
42
|
+
metric, also 0 credits, no AI). Do NOT reach for the server-side AI tools
|
|
43
|
+
(\`generate_a11y_variant\` / \`translate_a11y_variants\` / \`analyze_cognitive\`) by
|
|
44
|
+
default: those bill Sonenta AI credits and exist only as an explicit fallback for
|
|
45
|
+
very large volumes or when the developer specifically asks for server-side
|
|
46
|
+
generation.
|
|
31
47
|
|
|
32
48
|
## Requirements
|
|
33
49
|
- The Sonenta MCP server (\`@sonenta/mcp\`) must be configured with an \`mcp:*\`
|
|
@@ -38,6 +54,14 @@ or when the developer specifically asks for server-side generation.
|
|
|
38
54
|
- \`a11y_report\` \u2014 full WCAG gap report (rollups + per-item gaps). READ-ONLY.
|
|
39
55
|
- \`list_a11y_gaps\` \u2014 the actionable gap list, filterable by gap / surface /
|
|
40
56
|
locale. READ-ONLY.
|
|
57
|
+
- \`wcag_report\` \u2014 formal WCAG 2.2 CONFORMANCE report for the content layer,
|
|
58
|
+
per locale, with an AA \`conformance.score_pct\` (DOM-dependent SC are under
|
|
59
|
+
\`scope.out_of_scope_sc\`, never counted). Read \`scope.content_layer_sc\`
|
|
60
|
+
dynamically \u2014 don't hardcode the SC list. The headline conformance number.
|
|
61
|
+
0 credits. READ-ONLY.
|
|
62
|
+
- \`eaa_statement\` \u2014 EAA / EN 301 549 conformance STATEMENT (JSON) mapping each
|
|
63
|
+
covered SC to its EN 301 549 clause. The shareable accessibility statement
|
|
64
|
+
for the content layer. 0 credits. READ-ONLY.
|
|
41
65
|
- \`list_surfaces\` \u2014 the project's surfaces with their \`active\` flag. Only
|
|
42
66
|
ACTIVE surfaces accept variant writes and publish, so this is the set of
|
|
43
67
|
surfaces worth filling \u2014 read it, never assume a fixed set. READ-ONLY.
|
|
@@ -51,10 +75,22 @@ or when the developer specifically asks for server-side generation.
|
|
|
51
75
|
(key_uuid, language_code, surface) with a text value. CRUD, **0 AI credits**,
|
|
52
76
|
stored as a draft.
|
|
53
77
|
- \`delete_a11y_variant\` \u2014 clear one variant. CRUD, **0 AI credits**.
|
|
78
|
+
- \`a11y_remediation_plan_get\` \u2014 read the dashboard-prepared remediation plan
|
|
79
|
+
(its \`status\` draft|approved + \`items[]\` of apply/ignore decisions), or
|
|
80
|
+
null. The HUMAN's decisions for you to execute. READ-ONLY.
|
|
81
|
+
- \`a11y_remediation_plan_apply\` \u2014 bulk-EXECUTE an APPROVED remediation plan
|
|
82
|
+
server-side (writes each \`apply\` item's a11y variant, suppresses each
|
|
83
|
+
\`ignore\` cell). Only acts when \`status=approved\`. 0 AI credits.
|
|
54
84
|
- \`list_cognitive_candidates\` \u2014 text keys eligible for plain-language scoring
|
|
55
85
|
(a type offering plain_language, past a word floor). READ-ONLY.
|
|
56
|
-
- \`
|
|
57
|
-
|
|
86
|
+
- \`score_cognitive_local\` \u2014 compute + persist cognitive scores from a
|
|
87
|
+
VALIDATED, deterministic readability metric (Flesch-Kincaid for English, LIX
|
|
88
|
+
otherwise), 0 credits, no AI. The authoritative way to populate scores; scope
|
|
89
|
+
with \`key_uuids\` / \`namespace_uuid\`, \`overwrite\` to re-score.
|
|
90
|
+
- \`set_cognitive_score\` \u2014 record ONE key's cognitive difficulty score (0-100)
|
|
91
|
+
plus a plain-language suggestion (your own judgement). CRUD, **0 AI credits**
|
|
92
|
+
(by_bot). Use for a suggestion alongside the score; prefer
|
|
93
|
+
\`score_cognitive_local\` to populate the scores themselves.
|
|
58
94
|
- \`list_keys\` \u2014 read each key's semantic \`type\` (and source value) to audit
|
|
59
95
|
typing. READ-ONLY.
|
|
60
96
|
- \`update_key\` / \`update_keys_bulk\` \u2014 reclassify a mis-typed key (type-only,
|
|
@@ -83,77 +119,147 @@ assistive tech), not the visible UI string \u2014 keep them concise and meaningf
|
|
|
83
119
|
it yourself and \`set_a11y_variant\` (source language).
|
|
84
120
|
- \`alt_missing\` \u2014 an image key has no source alt_text \u2192 write \`alt_text\`.
|
|
85
121
|
- \`reading_level_high\` \u2014 flagged when a key's COGNITIVE SCORE is at/above the
|
|
86
|
-
project threshold.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
\`plain_language\` surface (or the base
|
|
122
|
+
project threshold. Populate scores with \`score_cognitive_local\` (validated
|
|
123
|
+
Flesch-Kincaid / LIX, 0 credits), then write a plain-language rewrite for the
|
|
124
|
+
hard ones via \`set_cognitive_score(key_uuid, score, suggestion)\` (0 credits,
|
|
125
|
+
draft). The suggestion is applied to the \`plain_language\` surface (or the base
|
|
126
|
+
value) on human approval.
|
|
90
127
|
- \`a11y_untranslated\` \u2014 a source a11y variant exists but a locale lacks it \u2192
|
|
91
128
|
TRANSLATE it yourself and \`set_a11y_variant\` for that \`language_code\`.
|
|
92
129
|
|
|
93
|
-
##
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
## The remediation PLAN has two sources \u2014 know which you are in
|
|
131
|
+
A "plan" is the set of fixes to apply. It can come from two places; handle each
|
|
132
|
+
differently:
|
|
133
|
+
|
|
134
|
+
### A) Dashboard-directed plan \u2014 OBSERVE \`approved\`, then APPLY (don't decide)
|
|
135
|
+
A human can author + APPROVE a remediation plan in the Sonenta dashboard: an
|
|
136
|
+
explicit list of \`{key_uuid, locale, surface, decision: apply|ignore, reason?,
|
|
137
|
+
value?}\` items with a \`status\`. This is the HUMAN's decision already made \u2014 you
|
|
138
|
+
execute it verbatim, you do NOT re-judge it. Check with
|
|
139
|
+
\`a11y_remediation_plan_get\`:
|
|
140
|
+
- \`status: "draft"\` or no plan \u2192 there is NO approved decision yet. Do NOT
|
|
141
|
+
apply. Either fall through to your OWN audit\u2192plan loop (B), or tell the dev the
|
|
142
|
+
dashboard plan is still awaiting their approval.
|
|
143
|
+
- \`status: "approved"\` \u2192 call \`a11y_remediation_plan_apply\` to bulk-execute it
|
|
144
|
+
server-side: each \`apply\` item writes its a11y variant (reversible draft
|
|
145
|
+
overlay), each \`ignore\` item suppresses that cell from future queues. Report
|
|
146
|
+
what was applied/ignored.
|
|
147
|
+
The \`approved\` flag is the gate \u2014 NEVER apply a draft/unapproved plan and never
|
|
148
|
+
edit the plan's items. (Identical contract to sonenta-source-health's
|
|
149
|
+
\`merge_plan\`: the dashboard decides, the agent applies.)
|
|
150
|
+
|
|
151
|
+
### B) Agent-built plan \u2014 AUDIT, PROPOSE, then EXECUTE ON ACCEPTANCE
|
|
152
|
+
When there is no approved dashboard plan, YOU build the remediation plan in the
|
|
153
|
+
session from your audit, present it, and apply it only on the dev's explicit
|
|
154
|
+
yes \u2014 the same step-by-step discipline as sonenta-source-health, but the fixes
|
|
155
|
+
are a11y variant writes (\`set_a11y_variant\`, reversible drafts) instead of key
|
|
156
|
+
merges. This is the \`## Workflow\` below. Your in-session writes land as drafts a
|
|
157
|
+
human reviews/approves; they do NOT need the dashboard plan's \`approved\` flag.
|
|
158
|
+
|
|
159
|
+
## Formal conformance \u2014 WCAG report + EAA statement
|
|
160
|
+
Beyond the actionable gap list, surface the FORMAL standing:
|
|
161
|
+
- \`wcag_report\` \u2014 the WCAG 2.2 AA conformance score for the content layer, per
|
|
162
|
+
locale (\`conformance.score_pct\` + \`by_locale\`). Read \`scope.content_layer_sc\`
|
|
163
|
+
and \`scope.out_of_scope_sc\` DYNAMICALLY \u2014 never hardcode the SC list; the
|
|
164
|
+
DOM-dependent criteria are out of scope and never count as pass. Use it as the
|
|
165
|
+
before/after headline around a remediation pass.
|
|
166
|
+
- \`eaa_statement\` \u2014 the EAA / EN 301 549 conformance STATEMENT (JSON), mapping
|
|
167
|
+
each covered SC to its EN 301 549 clause. Run it when the dev wants a shareable
|
|
168
|
+
accessibility statement for the content they govern; be honest about scope (it
|
|
169
|
+
attests the content layer, not the rendered-DOM audit).
|
|
170
|
+
These are READ-ONLY, 0 credits \u2014 safe to run any time, including in the audit
|
|
171
|
+
phase and the wrap-up.
|
|
172
|
+
|
|
173
|
+
## Workflow (strictly ordered \u2014 audit \u2192 plan \u2192 accept \u2192 execute)
|
|
174
|
+
1. **Check for a dashboard-directed plan first.** \`a11y_remediation_plan_get\`. If
|
|
175
|
+
it is \`approved\`, follow path **A** (apply it) and you are done. Otherwise
|
|
176
|
+
proceed \u2014 you will build your own plan and nothing is written until accepted.
|
|
177
|
+
2. **Audit key TYPES (prerequisite) \u2014 these become PROPOSED re-types, never
|
|
178
|
+
silent.** The a11y treatments a key offers are decided by its semantic
|
|
179
|
+
\`type\`, so a project where everything is the default \`text\` (a common
|
|
180
|
+
starting state) produces NO aria/alt/icon gaps even when buttons and images
|
|
181
|
+
need them. Read each key's \`type\` from \`list_keys\` and identify the
|
|
182
|
+
mis-typed ones (buttons/links \u2192 \`button\` / \`link\`, images \u2192 \`image\`, icons
|
|
183
|
+
\u2192 \`icon\`, form-field labels \u2192 \`input_label\`, headings \u2192 \`heading\`, \u2026).
|
|
184
|
+
\`type\` is user-owned config \u2014 the re-types go INTO the plan as proposals,
|
|
185
|
+
applied via \`update_key\` / \`update_keys_bulk\` (type-only, no source_value)
|
|
186
|
+
ONLY on acceptance. Never retype silently. (Correct types are what make the
|
|
187
|
+
real gaps surface.)
|
|
188
|
+
3. **Scan \u2014 derive the needed surfaces, don't assume them.** Read
|
|
105
189
|
\`list_surfaces\` (the project's ACTIVE a11y surfaces) and \`recommend_surfaces\`
|
|
106
|
-
(which surfaces each key's type actually needs, and where they're missing)
|
|
107
|
-
hardcode "the project needs aria_label + alt_text"
|
|
108
|
-
|
|
109
|
-
the
|
|
110
|
-
\`
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
\`
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
\`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
190
|
+
(which surfaces each key's type actually needs, and where they're missing) \u2014
|
|
191
|
+
never hardcode "the project needs aria_label + alt_text". Then \`a11y_report\`,
|
|
192
|
+
passing \`require_surface\` = the active a11y surfaces, and \`list_a11y_gaps\`
|
|
193
|
+
for the actionable items (each carries \`key_uuid\`, \`key_name\`,
|
|
194
|
+
\`namespace_slug\`, \`surface\`, \`locale\`). Also run \`wcag_report\` to capture
|
|
195
|
+
the BEFORE conformance score.
|
|
196
|
+
4. **Score readability (local, 0 credits).** \`list_cognitive_candidates\`
|
|
197
|
+
(\`only_unanalyzed=true\` to skip scored keys), then \`score_cognitive_local\`
|
|
198
|
+
(scope with \`key_uuids\` / \`namespace_uuid\`, \`overwrite\` to re-score) to
|
|
199
|
+
populate cognitive scores from the VALIDATED Flesch-Kincaid / LIX metric \u2014
|
|
200
|
+
deterministic, 0 credits, no AI. Keys at/above the project threshold surface as
|
|
201
|
+
\`reading_level_high\` gaps and enter the plan. Prefer this over
|
|
202
|
+
\`analyze_cognitive\` (billed AI).
|
|
203
|
+
5. **BUILD THE PLAN (write nothing yet).** Assemble one concrete proposal: the
|
|
204
|
+
key re-types (step 2), and for every gap the exact fix \u2014 \`{key_uuid,
|
|
205
|
+
key_name, surface, locale, the value you will write}\` \u2014 composing each
|
|
206
|
+
alt/aria/screen-reader value and each plain-language rewrite YOURSELF, by
|
|
207
|
+
reasoning over the key name, source value, context, and surface. Group it by
|
|
208
|
+
severity (warnings \u2014 \`a11y_untranslated\`, \`alt_missing\` \u2014 first; then info \u2014
|
|
209
|
+
\`reading_level_high\`, \`a11y_variant_absent\`). The plan is the deliverable of
|
|
210
|
+
the audit; do NOT call any write tool to produce it.
|
|
211
|
+
6. **PRESENT the plan + reassure; WAIT for acceptance.** Show the dev the full
|
|
212
|
+
plan: the proposed re-types, the per-gap fixes (with the exact text you'll
|
|
213
|
+
write), and the conformance delta you expect. Make explicit that NOTHING is
|
|
214
|
+
written until they accept, every write is a reviewable draft, variants are
|
|
215
|
+
reversible, and you will not overwrite a human-reviewed value. Ask which parts
|
|
216
|
+
to proceed with (all, or a subset).
|
|
217
|
+
7. **EXECUTE \u2014 only on acceptance, only the accepted parts.** Apply the re-types
|
|
218
|
+
(\`update_key\` / \`update_keys_bulk\`), then write each accepted a11y value
|
|
219
|
+
with \`set_a11y_variant(key_uuid, language_code, surface, value)\` (for
|
|
220
|
+
\`a11y_untranslated\`, translate the source variant into the target
|
|
221
|
+
\`language_code\` yourself first), and persist plain-language rewrites with
|
|
222
|
+
\`set_cognitive_score(key_uuid, score, suggestion)\`. Work in sensible batches,
|
|
223
|
+
narrate progress, skip whatever the dev declined. All 0 credits, all drafts.
|
|
224
|
+
If reality diverges from the plan mid-execution, STOP and re-present.
|
|
225
|
+
8. **Server fallback (opt-in only).** If the volume is impractical locally, or the
|
|
226
|
+
dev explicitly wants server-side AI, FIRST \`a11y_estimate\` (report
|
|
227
|
+
\`credits_required\` vs \`balance\`; stop if not \`sufficient\`), confirm, THEN
|
|
228
|
+
\`generate_a11y_variant\` / \`translate_a11y_variants\` (or \`analyze_cognitive\`).
|
|
229
|
+
9. **Conformance wrap-up.** Re-run \`wcag_report\` for the AFTER score (report the
|
|
230
|
+
before\u2192after \`conformance.score_pct\` delta), summarize what you set (counts by
|
|
231
|
+
surface / locale), what remains, and credits spent (0 on the local path).
|
|
232
|
+
Remind the dev everything is a draft to review. When they want a shareable
|
|
233
|
+
statement, produce \`eaa_statement\`.
|
|
138
234
|
|
|
139
235
|
## Modes
|
|
140
|
-
- **Interactive (Claude Code):**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
236
|
+
- **Interactive (Claude Code):** the default \u2014 audit \u2192 present the plan \u2192 wait for
|
|
237
|
+
acceptance \u2192 execute the accepted parts \u2192 conformance wrap-up. One section at a
|
|
238
|
+
time when the dev prefers. For the credit-billing fallback, show the estimate
|
|
239
|
+
and confirm first.
|
|
240
|
+
- **CI / headless:** run \`a11y_report\` / \`wcag_report\` and exit non-zero when
|
|
241
|
+
\`total_gaps\` (or a chosen severity, or the AA score below a threshold) fails
|
|
242
|
+
the gate. Do NOT auto-write fixes in CI unless the run explicitly authorizes it
|
|
243
|
+
\u2014 the plan-then-accept rule is the whole point. Only use the credit-billing
|
|
244
|
+
fallback when explicitly authorized.
|
|
145
245
|
|
|
146
246
|
## Guardrails
|
|
147
|
-
-
|
|
148
|
-
|
|
149
|
-
\`
|
|
150
|
-
|
|
151
|
-
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
247
|
+
- NEVER write, re-type, or delete anything before the dev accepted that specific
|
|
248
|
+
plan. The audit (\`*_report\`, \`list_*\`, \`recommend_surfaces\`,
|
|
249
|
+
\`score_cognitive_local\`) is read/score-only; the PLAN is always presented and
|
|
250
|
+
accepted before any \`set_a11y_variant\` / \`update_key\` write.
|
|
251
|
+
- For a DASHBOARD plan, the \`approved\` flag is the gate: apply it verbatim with
|
|
252
|
+
\`a11y_remediation_plan_apply\`, never a draft, never re-clustered or edited.
|
|
253
|
+
- Default to LOCAL work + \`set_a11y_variant\` / \`score_cognitive_local\` /
|
|
254
|
+
\`set_cognitive_score\` (0 credits). Treat \`generate_a11y_variant\` /
|
|
255
|
+
\`translate_a11y_variants\` / \`analyze_cognitive\` as an explicit, estimated,
|
|
256
|
+
opt-in fallback \u2014 never the silent default; always estimate before it.
|
|
257
|
+
- Everything you write is a reviewable DRAFT \u2014 never present it as final. Variant
|
|
258
|
+
writes are a reversible overlay; you FILL gaps and never overwrite a
|
|
259
|
+
human-reviewed value blindly.
|
|
155
260
|
- Derive which a11y surfaces the project needs from \`list_surfaces\` (active) +
|
|
156
|
-
\`recommend_surfaces
|
|
261
|
+
\`recommend_surfaces\`, and the WCAG scope from \`wcag_report\`'s
|
|
262
|
+
\`scope.content_layer_sc\` \u2014 never hardcode an assumed surface or SC set.
|
|
157
263
|
- Key \`type\` is user-owned config \u2014 propose re-types and apply only on
|
|
158
264
|
acceptance; never silently reclassify.
|
|
159
265
|
- Stay within the configured project; confirm it before any bulk operation.
|
|
@@ -732,7 +838,7 @@ the variant-writing or a11y-generation tools.
|
|
|
732
838
|
var AGENTS = {
|
|
733
839
|
"sonenta-a11y": {
|
|
734
840
|
name: "sonenta-a11y",
|
|
735
|
-
summary: "Accessibility (a11y) auditor + fixer:
|
|
841
|
+
summary: "Accessibility (a11y) auditor + fixer, plan-first like source-health: runs a full code-aware WCAG 2.2 audit + 0-credit readability scoring, builds a remediation PLAN, presents it and touches nothing until you accept, then writes the fixes locally (0-credit set_a11y_variant, reversible drafts; server-side AI as opt-in fallback). Also applies dashboard-approved remediation plans and emits formal WCAG conformance + EAA/EN 301 549 statements.",
|
|
736
842
|
content: SONENTA_A11Y
|
|
737
843
|
},
|
|
738
844
|
"sonenta-i18n": {
|
|
@@ -1000,6 +1106,86 @@ async function requireAuth(opts = {}) {
|
|
|
1000
1106
|
return ctx;
|
|
1001
1107
|
}
|
|
1002
1108
|
|
|
1109
|
+
// src/mcpserver.ts
|
|
1110
|
+
import { promises as fs4 } from "fs";
|
|
1111
|
+
import { resolve as resolve3 } from "path";
|
|
1112
|
+
var MCP_JSON_FILENAME = ".mcp.json";
|
|
1113
|
+
var MCP_SERVER_KEY = "sonenta";
|
|
1114
|
+
var MCP_PACKAGE = "@sonenta/mcp";
|
|
1115
|
+
function buildServerBlock(env, opts = {}) {
|
|
1116
|
+
const e = {};
|
|
1117
|
+
if (opts.embedKey && env.apiKey) e.SONENTA_API_KEY = env.apiKey;
|
|
1118
|
+
if (env.host) e.SONENTA_BASE_URL = env.host.replace(/\/+$/, "");
|
|
1119
|
+
if (env.projectUuid) e.SONENTA_PROJECT = env.projectUuid;
|
|
1120
|
+
return { command: "npx", args: ["-y", MCP_PACKAGE], env: e };
|
|
1121
|
+
}
|
|
1122
|
+
async function readMcpJson(path) {
|
|
1123
|
+
try {
|
|
1124
|
+
const raw = await fs4.readFile(path, "utf8");
|
|
1125
|
+
const trimmed = raw.trim();
|
|
1126
|
+
if (!trimmed) return { json: {}, existed: true };
|
|
1127
|
+
const parsed = JSON.parse(trimmed);
|
|
1128
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1129
|
+
throw new Error(`${MCP_JSON_FILENAME} is not a JSON object`);
|
|
1130
|
+
}
|
|
1131
|
+
return { json: parsed, existed: true };
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
if (err?.code === "ENOENT") {
|
|
1134
|
+
return { json: {}, existed: false };
|
|
1135
|
+
}
|
|
1136
|
+
throw err;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async function wireMcpServer(env, opts = {}) {
|
|
1140
|
+
const baseDir = opts.baseDir ?? process.cwd();
|
|
1141
|
+
const path = resolve3(baseDir, MCP_JSON_FILENAME);
|
|
1142
|
+
const { json, existed } = await readMcpJson(path);
|
|
1143
|
+
const embeddedKey = Boolean(opts.embedKey && env.apiKey);
|
|
1144
|
+
const block = buildServerBlock(env, { embedKey: opts.embedKey });
|
|
1145
|
+
const servers = json.mcpServers && typeof json.mcpServers === "object" ? json.mcpServers : {};
|
|
1146
|
+
const prior = servers[MCP_SERVER_KEY];
|
|
1147
|
+
const identical = prior !== void 0 && deepEqual(prior, block);
|
|
1148
|
+
servers[MCP_SERVER_KEY] = block;
|
|
1149
|
+
json.mcpServers = servers;
|
|
1150
|
+
const action = !existed ? "created" : identical ? "unchanged" : "updated";
|
|
1151
|
+
if (action !== "unchanged") {
|
|
1152
|
+
await fs4.writeFile(path, JSON.stringify(json, null, 2) + "\n", "utf8");
|
|
1153
|
+
}
|
|
1154
|
+
let gitignoreUpdated = false;
|
|
1155
|
+
const wantGitignore = opts.gitignore ?? embeddedKey;
|
|
1156
|
+
if (wantGitignore) {
|
|
1157
|
+
gitignoreUpdated = await ensureGitignored(baseDir, MCP_JSON_FILENAME);
|
|
1158
|
+
}
|
|
1159
|
+
return { path, action, serverKey: MCP_SERVER_KEY, gitignoreUpdated, embeddedKey };
|
|
1160
|
+
}
|
|
1161
|
+
async function ensureGitignored(baseDir, entry) {
|
|
1162
|
+
const path = resolve3(baseDir, ".gitignore");
|
|
1163
|
+
let current = "";
|
|
1164
|
+
try {
|
|
1165
|
+
current = await fs4.readFile(path, "utf8");
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
if (err?.code !== "ENOENT") throw err;
|
|
1168
|
+
}
|
|
1169
|
+
const already = current.split(/\r?\n/).map((l) => l.trim()).some((l) => l === entry || l === `/${entry}`);
|
|
1170
|
+
if (already) return false;
|
|
1171
|
+
const prefix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
|
|
1172
|
+
const header = current.length === 0 ? "" : "\n# Sonenta MCP server config (contains an API key)\n";
|
|
1173
|
+
await fs4.writeFile(path, `${current}${prefix}${header}${entry}
|
|
1174
|
+
`, "utf8");
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
function deepEqual(a, b) {
|
|
1178
|
+
if (a === b) return true;
|
|
1179
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
1180
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
1181
|
+
const ak = Object.keys(a);
|
|
1182
|
+
const bk = Object.keys(b);
|
|
1183
|
+
if (ak.length !== bk.length) return false;
|
|
1184
|
+
return ak.every(
|
|
1185
|
+
(k) => deepEqual(a[k], b[k])
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1003
1189
|
// src/commands/agents.ts
|
|
1004
1190
|
var agentsCommand = new Command("agents").description("Install bundled Claude agents (e.g. sonenta-a11y) into .claude/agents/.").addCommand(
|
|
1005
1191
|
new Command("list").description("List the bundled agents available to install.").option("--dir <path>", "Project directory (default: current directory)").action(async (opts) => {
|
|
@@ -1016,20 +1202,55 @@ var agentsCommand = new Command("agents").description("Install bundled Claude ag
|
|
|
1016
1202
|
Install with: sonenta agents add <name>`);
|
|
1017
1203
|
})
|
|
1018
1204
|
).addCommand(
|
|
1019
|
-
new Command("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).
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1205
|
+
new Command("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
|
+
"--embed-key",
|
|
1207
|
+
"Bake the API key into .mcp.json (for CI / no-login); otherwise the server reads it from ~/.sonenta",
|
|
1208
|
+
false
|
|
1209
|
+
).action(
|
|
1210
|
+
async (name, opts) => {
|
|
1211
|
+
const ctx = await requireAuth({ hostOverride: opts.host });
|
|
1212
|
+
const path = await writeAgent(name, { baseDir: opts.dir, force: opts.force });
|
|
1213
|
+
console.log(`Wrote ${path}`);
|
|
1214
|
+
if (opts.mcp) {
|
|
1215
|
+
const wired = await wireMcpServer(
|
|
1216
|
+
{ apiKey: ctx.apiKey, host: ctx.host, projectUuid: ctx.projectUuid },
|
|
1217
|
+
{ baseDir: opts.dir, embedKey: opts.embedKey }
|
|
1218
|
+
);
|
|
1219
|
+
const verb = wired.action === "created" ? "Created" : wired.action === "updated" ? "Updated" : "Verified";
|
|
1220
|
+
console.log(
|
|
1221
|
+
`${verb} ${MCP_JSON_FILENAME} \u2192 connected the "${wired.serverKey}" server (${"npx -y @sonenta/mcp"}, host ${ctx.host}${ctx.projectUuid ? `, project ${ctx.projectUuid}` : ""}).`
|
|
1222
|
+
);
|
|
1223
|
+
if (wired.embeddedKey) {
|
|
1224
|
+
console.log(
|
|
1225
|
+
`Embedded your API key in ${MCP_JSON_FILENAME}` + (wired.gitignoreUpdated ? " and added it to .gitignore" : "") + " \u2014 keep it out of git."
|
|
1226
|
+
);
|
|
1227
|
+
} else {
|
|
1228
|
+
console.log(
|
|
1229
|
+
`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.`
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
if (!ctx.projectUuid) {
|
|
1233
|
+
console.log(
|
|
1234
|
+
"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)."
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
console.log(
|
|
1238
|
+
`
|
|
1239
|
+
\u27F3 Reload your Claude Code session (or restart the MCP client) so the "${wired.serverKey}" server connects \u2014 then ${name}'s tools are available.`
|
|
1240
|
+
);
|
|
1241
|
+
} else {
|
|
1242
|
+
console.log(
|
|
1243
|
+
`
|
|
1244
|
+
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.`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
console.log(`Agent dir: ${AGENTS_DIR}/`);
|
|
1248
|
+
}
|
|
1249
|
+
)
|
|
1029
1250
|
);
|
|
1030
1251
|
|
|
1031
1252
|
// src/commands/export.ts
|
|
1032
|
-
import { promises as
|
|
1253
|
+
import { promises as fs5 } from "fs";
|
|
1033
1254
|
import { join as join2 } from "path";
|
|
1034
1255
|
import { Command as Command2 } from "commander";
|
|
1035
1256
|
|
|
@@ -1169,9 +1390,9 @@ var exportCommand = new Command2("export").description(
|
|
|
1169
1390
|
for (const [lang, nss] of Object.entries(collected)) {
|
|
1170
1391
|
for (const [ns, flat] of Object.entries(nss)) {
|
|
1171
1392
|
const dir = join2(opts.out, lang);
|
|
1172
|
-
await
|
|
1393
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
1173
1394
|
const p = join2(dir, `${ns}.json`);
|
|
1174
|
-
await
|
|
1395
|
+
await fs5.writeFile(p, JSON.stringify(shape(flat), null, 2) + "\n", "utf8");
|
|
1175
1396
|
console.log(` ${p} ${Object.keys(flat).length} keys`);
|
|
1176
1397
|
files++;
|
|
1177
1398
|
}
|
|
@@ -1189,7 +1410,7 @@ var exportCommand = new Command2("export").description(
|
|
|
1189
1410
|
);
|
|
1190
1411
|
|
|
1191
1412
|
// src/commands/import.ts
|
|
1192
|
-
import { promises as
|
|
1413
|
+
import { promises as fs6 } from "fs";
|
|
1193
1414
|
import { basename, dirname as dirname3 } from "path";
|
|
1194
1415
|
import { Command as Command3 } from "commander";
|
|
1195
1416
|
function resolveLangNs(filePath, optLang, optNs) {
|
|
@@ -1207,7 +1428,7 @@ function resolveLangNs(filePath, optLang, optNs) {
|
|
|
1207
1428
|
return { lang, ns };
|
|
1208
1429
|
}
|
|
1209
1430
|
async function readTree(filePath) {
|
|
1210
|
-
const parsed = JSON.parse(await
|
|
1431
|
+
const parsed = JSON.parse(await fs6.readFile(filePath, "utf8"));
|
|
1211
1432
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1212
1433
|
throw new Error(`${filePath} is not a JSON object`);
|
|
1213
1434
|
}
|
|
@@ -1264,13 +1485,13 @@ var importCommand = new Command3("import").description(
|
|
|
1264
1485
|
|
|
1265
1486
|
// src/commands/init.ts
|
|
1266
1487
|
import { existsSync } from "fs";
|
|
1267
|
-
import { resolve as
|
|
1488
|
+
import { resolve as resolve5 } from "path";
|
|
1268
1489
|
import { Command as Command4 } from "commander";
|
|
1269
1490
|
|
|
1270
1491
|
// src/repodoc.ts
|
|
1271
|
-
import { promises as
|
|
1272
|
-
import { resolve as
|
|
1273
|
-
var DOC_API_HOST = "https://api.sonenta.
|
|
1492
|
+
import { promises as fs7 } from "fs";
|
|
1493
|
+
import { resolve as resolve4 } from "path";
|
|
1494
|
+
var DOC_API_HOST = "https://api.sonenta.dev";
|
|
1274
1495
|
var DOC_CDN_HOST = "https://cdn.sonenta.com";
|
|
1275
1496
|
var REPO_DOC_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
1276
1497
|
var BLOCK_BEGIN = "<!-- SONENTA:BEGIN \u2014 managed by `sonenta init`; edits between these markers are overwritten -->";
|
|
@@ -1358,13 +1579,13 @@ function renderManagedBlock(d) {
|
|
|
1358
1579
|
async function upsertManagedBlock(filePath, block) {
|
|
1359
1580
|
let existing = null;
|
|
1360
1581
|
try {
|
|
1361
|
-
existing = await
|
|
1582
|
+
existing = await fs7.readFile(filePath, "utf8");
|
|
1362
1583
|
} catch {
|
|
1363
1584
|
existing = null;
|
|
1364
1585
|
}
|
|
1365
1586
|
const normalizedBlock = block.endsWith("\n") ? block : block + "\n";
|
|
1366
1587
|
if (existing === null) {
|
|
1367
|
-
await
|
|
1588
|
+
await fs7.writeFile(filePath, normalizedBlock, "utf8");
|
|
1368
1589
|
return "created";
|
|
1369
1590
|
}
|
|
1370
1591
|
const begin = existing.indexOf(BLOCK_BEGIN);
|
|
@@ -1373,18 +1594,18 @@ async function upsertManagedBlock(filePath, block) {
|
|
|
1373
1594
|
const before = existing.slice(0, begin);
|
|
1374
1595
|
const after = existing.slice(end + BLOCK_END.length);
|
|
1375
1596
|
const blockCore = block.slice(0, block.indexOf(BLOCK_END) + BLOCK_END.length);
|
|
1376
|
-
await
|
|
1597
|
+
await fs7.writeFile(filePath, before + blockCore + after, "utf8");
|
|
1377
1598
|
return "updated";
|
|
1378
1599
|
}
|
|
1379
1600
|
const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1380
|
-
await
|
|
1601
|
+
await fs7.writeFile(filePath, existing + sep + normalizedBlock, "utf8");
|
|
1381
1602
|
return "inserted";
|
|
1382
1603
|
}
|
|
1383
1604
|
async function writeRepoDocs(dir, data) {
|
|
1384
1605
|
const block = renderManagedBlock(data);
|
|
1385
1606
|
const results = [];
|
|
1386
1607
|
for (const file of REPO_DOC_FILES) {
|
|
1387
|
-
const path =
|
|
1608
|
+
const path = resolve4(dir, file);
|
|
1388
1609
|
const action = await upsertManagedBlock(path, block);
|
|
1389
1610
|
results.push({ file, path, action });
|
|
1390
1611
|
}
|
|
@@ -1392,7 +1613,7 @@ async function writeRepoDocs(dir, data) {
|
|
|
1392
1613
|
}
|
|
1393
1614
|
|
|
1394
1615
|
// src/commands/init.ts
|
|
1395
|
-
var DEFAULT_HOST = "https://api.sonenta.
|
|
1616
|
+
var DEFAULT_HOST = "https://api.sonenta.dev";
|
|
1396
1617
|
var ACTION_LABEL = {
|
|
1397
1618
|
created: "Created",
|
|
1398
1619
|
updated: "Updated",
|
|
@@ -1433,9 +1654,13 @@ async function gatherRepoDocData(opts) {
|
|
|
1433
1654
|
}
|
|
1434
1655
|
var initCommand = new Command4("init").description(
|
|
1435
1656
|
"Scaffold sonenta.config.json AND write a managed Sonenta block into CLAUDE.md / AGENTS.md so coding agents know how this repo uses Sonenta."
|
|
1436
|
-
).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").
|
|
1657
|
+
).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
|
+
"--embed-key",
|
|
1659
|
+
"Bake the API key into .mcp.json (for CI / no-login); otherwise the server reads it from ~/.sonenta",
|
|
1660
|
+
false
|
|
1661
|
+
).action(
|
|
1437
1662
|
async (opts) => {
|
|
1438
|
-
const path =
|
|
1663
|
+
const path = resolve5(process.cwd(), CONFIG_FILENAME);
|
|
1439
1664
|
if (existsSync(path) && !opts.force) {
|
|
1440
1665
|
console.error(
|
|
1441
1666
|
`${CONFIG_FILENAME} already exists at ${path}. Pass --force to overwrite.`
|
|
@@ -1462,6 +1687,50 @@ var initCommand = new Command4("init").description(
|
|
|
1462
1687
|
} else {
|
|
1463
1688
|
console.log("Skipped CLAUDE.md / AGENTS.md (--no-repo-doc).");
|
|
1464
1689
|
}
|
|
1690
|
+
if (opts.mcp) {
|
|
1691
|
+
let liveHost = opts.host !== DEFAULT_HOST ? opts.host : void 0;
|
|
1692
|
+
if (!liveHost) {
|
|
1693
|
+
const creds = await readCredentials().catch(() => null);
|
|
1694
|
+
liveHost = creds?.default ?? void 0;
|
|
1695
|
+
}
|
|
1696
|
+
let resolved = null;
|
|
1697
|
+
try {
|
|
1698
|
+
const ctx = await resolveContext({ hostOverride: liveHost });
|
|
1699
|
+
resolved = { apiKey: ctx.apiKey, host: ctx.host, projectUuid: ctx.projectUuid };
|
|
1700
|
+
} catch {
|
|
1701
|
+
resolved = opts.embedKey ? null : { host: liveHost ?? opts.host, projectUuid: opts.project };
|
|
1702
|
+
}
|
|
1703
|
+
if (resolved) {
|
|
1704
|
+
const wired = await wireMcpServer(
|
|
1705
|
+
{
|
|
1706
|
+
apiKey: resolved.apiKey,
|
|
1707
|
+
host: resolved.host,
|
|
1708
|
+
projectUuid: resolved.projectUuid ?? opts.project
|
|
1709
|
+
},
|
|
1710
|
+
{ embedKey: opts.embedKey }
|
|
1711
|
+
);
|
|
1712
|
+
const verb = wired.action === "created" ? "Created" : wired.action === "updated" ? "Updated" : "Verified";
|
|
1713
|
+
console.log(
|
|
1714
|
+
`${verb} ${MCP_JSON_FILENAME} \u2192 connected the "${wired.serverKey}" server (npx -y @sonenta/mcp${resolved.host ? `, host ${resolved.host}` : ""}).`
|
|
1715
|
+
);
|
|
1716
|
+
if (wired.embeddedKey) {
|
|
1717
|
+
console.log(
|
|
1718
|
+
`Embedded your API key in ${MCP_JSON_FILENAME}` + (wired.gitignoreUpdated ? " and added it to .gitignore" : "") + " \u2014 keep it out of git."
|
|
1719
|
+
);
|
|
1720
|
+
} else {
|
|
1721
|
+
console.log(
|
|
1722
|
+
`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.`
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
console.log(
|
|
1726
|
+
`\u27F3 Reload your Claude Code session so the "${wired.serverKey}" server connects.`
|
|
1727
|
+
);
|
|
1728
|
+
} else {
|
|
1729
|
+
console.log(
|
|
1730
|
+
`Note: skipped ${MCP_JSON_FILENAME} wiring (--embed-key needs a login). Run \`sonenta login\` then \`sonenta agents add <name>\`.`
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1465
1734
|
if (!opts.project) {
|
|
1466
1735
|
console.log(
|
|
1467
1736
|
"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`."
|
|
@@ -1490,8 +1759,8 @@ async function promptLine(message) {
|
|
|
1490
1759
|
if (!process.stdin.isTTY) return "";
|
|
1491
1760
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1492
1761
|
try {
|
|
1493
|
-
return await new Promise((
|
|
1494
|
-
rl.question(message, (answer) =>
|
|
1762
|
+
return await new Promise((resolve7) => {
|
|
1763
|
+
rl.question(message, (answer) => resolve7(answer.trim()));
|
|
1495
1764
|
});
|
|
1496
1765
|
} finally {
|
|
1497
1766
|
rl.close();
|
|
@@ -1507,7 +1776,7 @@ async function promptSecret(message) {
|
|
|
1507
1776
|
process.stdout.write(message);
|
|
1508
1777
|
process.stdin.setRawMode(true);
|
|
1509
1778
|
process.stdin.resume();
|
|
1510
|
-
return await new Promise((
|
|
1779
|
+
return await new Promise((resolve7) => {
|
|
1511
1780
|
let buffer = "";
|
|
1512
1781
|
const onData = (chunk) => {
|
|
1513
1782
|
for (const byte of chunk) {
|
|
@@ -1516,7 +1785,7 @@ async function promptSecret(message) {
|
|
|
1516
1785
|
process.stdin.removeListener("data", onData);
|
|
1517
1786
|
process.stdin.setRawMode(false);
|
|
1518
1787
|
process.stdin.pause();
|
|
1519
|
-
|
|
1788
|
+
resolve7(buffer);
|
|
1520
1789
|
return;
|
|
1521
1790
|
}
|
|
1522
1791
|
if (byte === CTRL_C) {
|
|
@@ -1545,10 +1814,10 @@ async function promptSecret(message) {
|
|
|
1545
1814
|
var TOKEN_REGEX = /^vrb_[a-z]+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
1546
1815
|
var loginCommand = new Command6("login").description(
|
|
1547
1816
|
"Store an API key for a host. Token resolution order: --token, SONENTA_TOKEN env, then interactive prompt (TTY only)."
|
|
1548
|
-
).option("--host <url>", "API base URL", "https://api.sonenta.
|
|
1817
|
+
).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) => {
|
|
1549
1818
|
let host = opts.host;
|
|
1550
1819
|
if (!host && process.stdin.isTTY) {
|
|
1551
|
-
host = await promptLine("Host (default https://api.sonenta.
|
|
1820
|
+
host = await promptLine("Host (default https://api.sonenta.dev): ") || "https://api.sonenta.dev";
|
|
1552
1821
|
}
|
|
1553
1822
|
let token = opts.token ?? (process.env.SONENTA_TOKEN ?? process.env.VERBUMIA_TOKEN) ?? "";
|
|
1554
1823
|
if (!token && process.stdin.isTTY) {
|
|
@@ -1667,14 +1936,14 @@ var projectsCommand = new Command9("projects").description("Inspect Verbumia pro
|
|
|
1667
1936
|
import { Command as Command10 } from "commander";
|
|
1668
1937
|
|
|
1669
1938
|
// src/locales.ts
|
|
1670
|
-
import { promises as
|
|
1671
|
-
import { join as join3, resolve as
|
|
1939
|
+
import { promises as fs8 } from "fs";
|
|
1940
|
+
import { join as join3, resolve as resolve6 } from "path";
|
|
1672
1941
|
var DEFAULT_LOCALES_DIR = "locales";
|
|
1673
1942
|
async function listLocaleFiles(rootDir) {
|
|
1674
|
-
const root =
|
|
1943
|
+
const root = resolve6(rootDir);
|
|
1675
1944
|
let langDirs;
|
|
1676
1945
|
try {
|
|
1677
|
-
langDirs = await
|
|
1946
|
+
langDirs = await fs8.readdir(root);
|
|
1678
1947
|
} catch {
|
|
1679
1948
|
return [];
|
|
1680
1949
|
}
|
|
@@ -1683,12 +1952,12 @@ async function listLocaleFiles(rootDir) {
|
|
|
1683
1952
|
const langPath = join3(root, lang);
|
|
1684
1953
|
let stat;
|
|
1685
1954
|
try {
|
|
1686
|
-
stat = await
|
|
1955
|
+
stat = await fs8.stat(langPath);
|
|
1687
1956
|
} catch {
|
|
1688
1957
|
continue;
|
|
1689
1958
|
}
|
|
1690
1959
|
if (!stat.isDirectory()) continue;
|
|
1691
|
-
const files = await
|
|
1960
|
+
const files = await fs8.readdir(langPath);
|
|
1692
1961
|
for (const f of files) {
|
|
1693
1962
|
if (!f.endsWith(".json")) continue;
|
|
1694
1963
|
out.push({ lang, namespace: f.replace(/\.json$/, ""), path: join3(langPath, f) });
|
|
@@ -1697,7 +1966,7 @@ async function listLocaleFiles(rootDir) {
|
|
|
1697
1966
|
return out;
|
|
1698
1967
|
}
|
|
1699
1968
|
async function readLocaleFile(path) {
|
|
1700
|
-
const raw = await
|
|
1969
|
+
const raw = await fs8.readFile(path, "utf8");
|
|
1701
1970
|
const parsed = JSON.parse(raw);
|
|
1702
1971
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1703
1972
|
throw new Error(`${path} is not a flat object`);
|
|
@@ -1713,12 +1982,12 @@ async function readLocaleFile(path) {
|
|
|
1713
1982
|
}
|
|
1714
1983
|
async function writeLocaleFile(rootDir, lang, namespace, values) {
|
|
1715
1984
|
const dir = join3(rootDir, lang);
|
|
1716
|
-
await
|
|
1985
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
1717
1986
|
const path = join3(dir, `${namespace}.json`);
|
|
1718
1987
|
const sorted = Object.fromEntries(
|
|
1719
1988
|
Object.entries(values).sort(([a], [b]) => a.localeCompare(b))
|
|
1720
1989
|
);
|
|
1721
|
-
await
|
|
1990
|
+
await fs8.writeFile(path, JSON.stringify(sorted, null, 2) + "\n", "utf8");
|
|
1722
1991
|
return path;
|
|
1723
1992
|
}
|
|
1724
1993
|
function diffFlat(local, remote) {
|
|
@@ -1857,7 +2126,7 @@ var releasesCommand = new Command12("releases").description("Manage CDN releases
|
|
|
1857
2126
|
);
|
|
1858
2127
|
|
|
1859
2128
|
// src/commands/snapshot.ts
|
|
1860
|
-
import { promises as
|
|
2129
|
+
import { promises as fs9 } from "fs";
|
|
1861
2130
|
import { Command as Command13 } from "commander";
|
|
1862
2131
|
function bundleUrl(cdnBase, project, version, lang, ns) {
|
|
1863
2132
|
return `${cdnBase.replace(/\/+$/, "")}/p/${project}/${version}/latest/${lang}/${ns}.json`;
|
|
@@ -1924,7 +2193,7 @@ var snapshotCommand = new Command13("snapshot").description(
|
|
|
1924
2193
|
opts.format === "json" ? "json" : "ts"
|
|
1925
2194
|
);
|
|
1926
2195
|
if (opts.out) {
|
|
1927
|
-
await
|
|
2196
|
+
await fs9.writeFile(opts.out, output, "utf8");
|
|
1928
2197
|
console.log(
|
|
1929
2198
|
`wrote ${opts.out}: ${fetched} bundle(s)` + (missing ? `, ${missing} not published (404)` : "")
|
|
1930
2199
|
);
|