champollion 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +133 -0
- package/README.md +387 -0
- package/bin/cli.js +278 -0
- package/index.js +135 -0
- package/lib/api-key.js +127 -0
- package/lib/autofix.js +432 -0
- package/lib/bridge/method_bridge.py +430 -0
- package/lib/card-source-resolution.mjs +284 -0
- package/lib/cards/cache.js +169 -0
- package/lib/cards/env.js +82 -0
- package/lib/cards/fetch-card-child.js +38 -0
- package/lib/cards/reader.js +435 -0
- package/lib/cards/refresh.js +111 -0
- package/lib/cards/remote.js +387 -0
- package/lib/cldf-export.mjs +540 -0
- package/lib/cldf-terms.mjs +62 -0
- package/lib/command-help.js +790 -0
- package/lib/commands/audit.js +49 -0
- package/lib/commands/card.js +454 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/fonts.js +489 -0
- package/lib/commands/help.js +91 -0
- package/lib/commands/init.js +1259 -0
- package/lib/commands/integrity.js +148 -0
- package/lib/commands/leaderboard.js +478 -0
- package/lib/commands/lint.js +30 -0
- package/lib/commands/models.js +177 -0
- package/lib/commands/plugin.js +103 -0
- package/lib/commands/provenance.js +45 -0
- package/lib/commands/recommend.js +75 -0
- package/lib/commands/register-corpus.js +678 -0
- package/lib/commands/repair-script.js +42 -0
- package/lib/commands/seal-corpus.js +355 -0
- package/lib/commands/seo.js +72 -0
- package/lib/commands/serve.js +147 -0
- package/lib/commands/status.js +265 -0
- package/lib/commands/submit.js +332 -0
- package/lib/commands/sync.js +89 -0
- package/lib/commands/tm.js +573 -0
- package/lib/commands/verify.js +39 -0
- package/lib/commands/watch.js +20 -0
- package/lib/commands/wrap.js +138 -0
- package/lib/commands/xliff.js +327 -0
- package/lib/commercial-eligibility.js +235 -0
- package/lib/concurrent.js +87 -0
- package/lib/config.js +523 -0
- package/lib/contamination-lane.js +76 -0
- package/lib/content-sync.js +731 -0
- package/lib/content.js +733 -0
- package/lib/corpus-registration.mjs +608 -0
- package/lib/cost-report.js +346 -0
- package/lib/diff.js +155 -0
- package/lib/docusaurus-sync.js +1256 -0
- package/lib/flatten.js +55 -0
- package/lib/format.js +954 -0
- package/lib/hash.js +159 -0
- package/lib/icu.js +473 -0
- package/lib/integrity.js +689 -0
- package/lib/license-gate.mjs +478 -0
- package/lib/license-identify.mjs +229 -0
- package/lib/lint.js +629 -0
- package/lib/method-manifest.js +60 -0
- package/lib/methods/anthropic.js +140 -0
- package/lib/methods/apertium.js +163 -0
- package/lib/methods/api.js +316 -0
- package/lib/methods/base.js +184 -0
- package/lib/methods/content-separator.js +45 -0
- package/lib/methods/deepl.js +426 -0
- package/lib/methods/direct-llm.js +586 -0
- package/lib/methods/external.js +332 -0
- package/lib/methods/fetch-with-retry.js +124 -0
- package/lib/methods/gemini.js +147 -0
- package/lib/methods/google-translate.js +402 -0
- package/lib/methods/http-utils.js +122 -0
- package/lib/methods/libretranslate.js +314 -0
- package/lib/methods/llm-coached.js +670 -0
- package/lib/methods/llm.js +592 -0
- package/lib/methods/local.js +76 -0
- package/lib/methods/microsoft-translator.js +331 -0
- package/lib/methods/openai.js +131 -0
- package/lib/methods/openrouter-client.js +327 -0
- package/lib/methods/openrouter-pricing.js +156 -0
- package/lib/methods/provider-env.js +115 -0
- package/lib/methods/provider-pricing.js +310 -0
- package/lib/methods/tilde.js +150 -0
- package/lib/methods/translated.js +229 -0
- package/lib/methods/translation-error.js +80 -0
- package/lib/models.js +258 -0
- package/lib/no-translate.js +233 -0
- package/lib/output.js +238 -0
- package/lib/pairs.js +547 -0
- package/lib/plugins.js +447 -0
- package/lib/provenance.js +323 -0
- package/lib/recommend.js +648 -0
- package/lib/registers.js +1185 -0
- package/lib/repair-script.js +266 -0
- package/lib/scripts.js +994 -0
- package/lib/seal.mjs +464 -0
- package/lib/sealed-qualifier.mjs +211 -0
- package/lib/security.js +59 -0
- package/lib/segment.js +369 -0
- package/lib/seo.js +275 -0
- package/lib/serve.js +854 -0
- package/lib/string-classify.js +85 -0
- package/lib/submit.mjs +344 -0
- package/lib/sync.js +969 -0
- package/lib/tags/bcp47.js +202 -0
- package/lib/tags/resolve.js +314 -0
- package/lib/terminology.js +111 -0
- package/lib/tm-seed.js +294 -0
- package/lib/tm.js +515 -0
- package/lib/translate-pair.js +197 -0
- package/lib/translate.js +203 -0
- package/lib/types.js +230 -0
- package/lib/validate.js +510 -0
- package/lib/verify.js +451 -0
- package/lib/watch.js +145 -0
- package/lib/xliff.js +184 -0
- package/package.json +93 -0
- package/shared/ATTRIBUTION.md +145 -0
- package/shared/CORPORA-CARDS.md +288 -0
- package/shared/DATA-SOVEREIGNTY.md +500 -0
- package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
- package/shared/card-lint-baseline.json +3189 -0
- package/shared/cards-fallback.json +1 -0
- package/shared/catalogue/card-config.json +6091 -0
- package/shared/catalogue/external-results.json +3888 -0
- package/shared/catalogue/gender-guidance.json +1038 -0
- package/shared/catalogue/method-coverage.json +1751 -0
- package/shared/catalogue/metric-coverage.json +170 -0
- package/shared/catalogue/metric-reliability.json +1 -0
- package/shared/catalogue/register-presets.json +3180 -0
- package/shared/catalogue/vitality-scales.json +55 -0
- package/shared/cldr-index.json +1115 -0
- package/shared/code-bridge.json +253 -0
- package/shared/corpora-cards-v1-reference.md +281 -0
- package/shared/curated-dictionary-flags.json +35 -0
- package/shared/curated-endonyms.json +35 -0
- package/shared/curated-fsts.json +51 -0
- package/shared/curated-orthography-conventions.json +26 -0
- package/shared/curated-sil-resources.json +374 -0
- package/shared/curated-tools.json +41 -0
- package/shared/docent/corpus.json +11333 -0
- package/shared/docent/faq.en.json +564 -0
- package/shared/docent/register-blocks.json +60 -0
- package/shared/docent/system-prompt.md +144 -0
- package/shared/domain-taxonomy.json +35 -0
- package/shared/explainers/glossary.json +2975 -0
- package/shared/explainers/tc-features.json +20112 -0
- package/shared/explainers/term-watchlist.json +147 -0
- package/shared/human-services.json +59 -0
- package/shared/license-corrections.json +261 -0
- package/shared/license-evidence.json +13452 -0
- package/shared/licenses.json +6781 -0
- package/shared/method-registry.json +236 -0
- package/shared/metric-registry.json +620 -0
- package/shared/model-aliases.json +7 -0
- package/shared/schemas/champollion-plugin.schema.json +206 -0
- package/shared/schemas/corpora-card.schema.json +957 -0
- package/shared/schemas/domain-taxonomy.schema.json +64 -0
- package/shared/schemas/external-results.schema.json +314 -0
- package/shared/schemas/human-services.schema.json +90 -0
- package/shared/schemas/language-card.schema.json +1308 -0
- package/shared/schemas/licenses.schema.json +155 -0
- package/shared/schemas/method-card.schema.json +412 -0
- package/shared/schemas/method-registry.schema.json +85 -0
- package/shared/schemas/metric-registry.schema.json +96 -0
- package/shared/schemas/metric-reliability.schema.json +178 -0
- package/shared/schemas/model-aliases.schema.json +27 -0
- package/shared/schemas/source-snapshot.schema.json +96 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: doctor
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive diagnostics tool for Champollion installations.
|
|
5
|
+
* Checks system health, validates configuration, and troubleshoots
|
|
6
|
+
* common issues with language cards, FSTs, methods, and scoring.
|
|
7
|
+
*
|
|
8
|
+
* Subcommands:
|
|
9
|
+
* (none) — Run all checks (full system health report)
|
|
10
|
+
* cards — Verify language cards load correctly
|
|
11
|
+
* config — Validate project configuration
|
|
12
|
+
* fst [code] — Check FST installation for a specific language
|
|
13
|
+
* methods — Check method dependencies and API keys
|
|
14
|
+
*
|
|
15
|
+
* Exit codes:
|
|
16
|
+
* 0 — All checks passed (or warnings only)
|
|
17
|
+
* 1 — One or more checks failed
|
|
18
|
+
*
|
|
19
|
+
* WHY this command exists:
|
|
20
|
+
* Champollion has many moving parts — language cards, FSTs, API keys,
|
|
21
|
+
* Python plugins, scoring specs. When something breaks, users need a
|
|
22
|
+
* single command that tells them exactly what's wrong and how to fix it.
|
|
23
|
+
* This is the "easy DX" philosophy: clear diagnostics, not cryptic errors.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { output } from '../output.js';
|
|
29
|
+
import { getAllLanguageCodes, getLanguageCard, resolveCode } from '../registers.js';
|
|
30
|
+
import { getEnvOrFileVar } from '../api-key.js';
|
|
31
|
+
import { resolveProviderEnv, canonicalEnvName } from '../methods/provider-env.js';
|
|
32
|
+
|
|
33
|
+
// ── Result tracking ──────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Accumulates check results for the summary.
|
|
37
|
+
* Each check pushes a { status, label, detail } entry.
|
|
38
|
+
*/
|
|
39
|
+
class DiagnosticReport {
|
|
40
|
+
constructor() {
|
|
41
|
+
this.results = [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pass(label, detail = '') {
|
|
45
|
+
this.results.push({ status: 'pass', label, detail });
|
|
46
|
+
output.raw(` ✅ ${label}${detail ? ` ${detail}` : ''}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
warn(label, detail = '') {
|
|
50
|
+
this.results.push({ status: 'warn', label, detail });
|
|
51
|
+
output.raw(` ⚠️ ${label}${detail ? ` ${detail}` : ''}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fail(label, detail = '') {
|
|
55
|
+
this.results.push({ status: 'fail', label, detail });
|
|
56
|
+
output.raw(` ❌ ${label}${detail ? `\n ${detail}` : ''}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Pass/warn/fail tallies — shared by summarize() and the --json payload. */
|
|
60
|
+
counts() {
|
|
61
|
+
return {
|
|
62
|
+
passed: this.results.filter(r => r.status === 'pass').length,
|
|
63
|
+
warned: this.results.filter(r => r.status === 'warn').length,
|
|
64
|
+
failed: this.results.filter(r => r.status === 'fail').length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Print summary line and return exit code. */
|
|
69
|
+
summarize() {
|
|
70
|
+
const { passed, warned, failed } = this.counts();
|
|
71
|
+
|
|
72
|
+
output.raw('');
|
|
73
|
+
const parts = [];
|
|
74
|
+
if (passed) parts.push(`${passed} passed`);
|
|
75
|
+
if (warned) parts.push(`${warned} warning${warned > 1 ? 's' : ''}`);
|
|
76
|
+
if (failed) parts.push(`${failed} failed`);
|
|
77
|
+
output.raw(` ${parts.join(', ')}`);
|
|
78
|
+
output.raw('');
|
|
79
|
+
|
|
80
|
+
return failed > 0 ? 1 : 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Check: Language Cards ────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Verify language cards load correctly and report coverage stats.
|
|
88
|
+
*/
|
|
89
|
+
function checkCards(report) {
|
|
90
|
+
output.raw('');
|
|
91
|
+
output.raw(' ── Language Cards ──');
|
|
92
|
+
output.raw('');
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const allCodes = getAllLanguageCodes();
|
|
96
|
+
const cardCount = allCodes.length;
|
|
97
|
+
|
|
98
|
+
if (cardCount === 0) {
|
|
99
|
+
report.fail('Language cards', 'No cards loaded. Check shared/language-cards/ directory.');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
report.pass(`Language cards`, `${cardCount.toLocaleString()} cards loaded`);
|
|
104
|
+
|
|
105
|
+
// Check for cards with missing required fields
|
|
106
|
+
let missingScript = 0;
|
|
107
|
+
let missingName = 0;
|
|
108
|
+
let withFST = 0;
|
|
109
|
+
let withFormality = 0;
|
|
110
|
+
let withMetricModel = 0;
|
|
111
|
+
|
|
112
|
+
for (const code of allCodes) {
|
|
113
|
+
const card = getLanguageCard(code);
|
|
114
|
+
if (!card) continue;
|
|
115
|
+
if (!card.name) missingName++;
|
|
116
|
+
if (!card.script && !card.scriptUnicodeName) missingScript++;
|
|
117
|
+
const fsts = card.resources?.fsts || [];
|
|
118
|
+
if (fsts.length > 0 && fsts[0].install) withFST++;
|
|
119
|
+
if (card.formality) withFormality++;
|
|
120
|
+
if (card.metricModelSupport) withMetricModel++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Stats line
|
|
124
|
+
output.raw(` FST install metadata: ${withFST} cards`);
|
|
125
|
+
output.raw(` Formality systems: ${withFormality} cards`);
|
|
126
|
+
output.raw(` Metric model support: ${withMetricModel} cards`);
|
|
127
|
+
|
|
128
|
+
if (missingName > 0) {
|
|
129
|
+
report.warn(`Cards missing name`, `${missingName} cards have no 'name' field`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check genus cards
|
|
133
|
+
const genusDir = path.join(
|
|
134
|
+
findCardsDir() || '',
|
|
135
|
+
'genera'
|
|
136
|
+
);
|
|
137
|
+
if (fs.existsSync(genusDir)) {
|
|
138
|
+
const genusFiles = fs.readdirSync(genusDir).filter(f => f.endsWith('.json'));
|
|
139
|
+
report.pass(`Genus cards`, `${genusFiles.length} genus/family cards in genera/`);
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
report.fail('Language cards', `Failed to load: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Check: Config ────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate the project configuration file.
|
|
150
|
+
*/
|
|
151
|
+
function checkConfig(report, cwd) {
|
|
152
|
+
output.raw('');
|
|
153
|
+
output.raw(' ── Project Config ──');
|
|
154
|
+
output.raw('');
|
|
155
|
+
|
|
156
|
+
// Find config file
|
|
157
|
+
const configNames = ['champollion.config.json', 'champollion.config.js', '.champollionrc.json'];
|
|
158
|
+
let configPath = null;
|
|
159
|
+
|
|
160
|
+
for (const name of configNames) {
|
|
161
|
+
const candidate = path.join(cwd, name);
|
|
162
|
+
if (fs.existsSync(candidate)) {
|
|
163
|
+
configPath = candidate;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!configPath) {
|
|
169
|
+
report.warn('Config file', `No config file found in ${cwd}. Run 'champollion init' to create one.`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
175
|
+
const config = JSON.parse(raw);
|
|
176
|
+
|
|
177
|
+
report.pass('Config file', path.basename(configPath));
|
|
178
|
+
|
|
179
|
+
// Validate language codes resolve
|
|
180
|
+
const languages = Array.isArray(config.languages)
|
|
181
|
+
? config.languages
|
|
182
|
+
: (config.languages ? Object.keys(config.languages) : []);
|
|
183
|
+
|
|
184
|
+
if (languages.length === 0) {
|
|
185
|
+
report.warn('Languages', 'No target languages configured');
|
|
186
|
+
} else {
|
|
187
|
+
let unresolved = 0;
|
|
188
|
+
const unresolvedCodes = [];
|
|
189
|
+
|
|
190
|
+
for (const code of languages) {
|
|
191
|
+
const resolved = resolveCode(code);
|
|
192
|
+
if (!resolved) {
|
|
193
|
+
unresolved++;
|
|
194
|
+
unresolvedCodes.push(code);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (unresolved > 0) {
|
|
199
|
+
report.fail(
|
|
200
|
+
'Language resolution',
|
|
201
|
+
`${unresolved} code(s) not found in language cards: ${unresolvedCodes.join(', ')}`
|
|
202
|
+
);
|
|
203
|
+
} else {
|
|
204
|
+
report.pass('Language resolution', `${languages.length} language(s) all resolve to valid cards`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check for input locale
|
|
209
|
+
if (config.inputLocale) {
|
|
210
|
+
const resolved = resolveCode(config.inputLocale);
|
|
211
|
+
if (resolved) {
|
|
212
|
+
report.pass('Input locale', `${config.inputLocale} → ${resolved}`);
|
|
213
|
+
} else {
|
|
214
|
+
report.fail('Input locale', `"${config.inputLocale}" does not resolve to a language card`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check method. The config schema field is `defaultMethod` (see config.js
|
|
219
|
+
// DEFAULTS) — reading the non-existent `config.method` made doctor warn on
|
|
220
|
+
// EVERY correct project, including the config `champollion init` writes.
|
|
221
|
+
// Accept the legacy `method` alias too, and treat absence as the documented
|
|
222
|
+
// default ('llm') rather than a misconfiguration.
|
|
223
|
+
const method = config.defaultMethod || config.method;
|
|
224
|
+
if (method) {
|
|
225
|
+
report.pass('Method', method);
|
|
226
|
+
} else {
|
|
227
|
+
report.pass('Method', 'llm (default)');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
} catch (err) {
|
|
231
|
+
if (err instanceof SyntaxError) {
|
|
232
|
+
report.fail('Config file', `Invalid JSON in ${path.basename(configPath)}: ${err.message}`);
|
|
233
|
+
} else {
|
|
234
|
+
report.fail('Config file', `Failed to read: ${err.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Check: FST ───────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check FST installation for a specific language or all configured.
|
|
243
|
+
*/
|
|
244
|
+
function checkFST(report, langCode) {
|
|
245
|
+
output.raw('');
|
|
246
|
+
output.raw(` ── FST: ${langCode || 'all'} ──`);
|
|
247
|
+
output.raw('');
|
|
248
|
+
|
|
249
|
+
// If a specific code was given, check just that one
|
|
250
|
+
const codesToCheck = langCode
|
|
251
|
+
? [resolveCode(langCode) || langCode]
|
|
252
|
+
: getAllLanguageCodes().filter(code => {
|
|
253
|
+
const card = getLanguageCard(code);
|
|
254
|
+
const fsts = card?.resources?.fsts || [];
|
|
255
|
+
return fsts.length > 0 && fsts[0].install;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (codesToCheck.length === 0) {
|
|
259
|
+
if (langCode) {
|
|
260
|
+
report.warn(`FST (${langCode})`, 'No FST install metadata on this language card');
|
|
261
|
+
} else {
|
|
262
|
+
report.warn('FST', 'No languages have FST install metadata on their cards');
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const code of codesToCheck) {
|
|
268
|
+
const card = getLanguageCard(code);
|
|
269
|
+
if (!card) {
|
|
270
|
+
report.fail(`FST (${code})`, `No language card found for "${code}"`);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const fsts = card.resources?.fsts || [];
|
|
275
|
+
const installInfo = fsts[0]?.install;
|
|
276
|
+
|
|
277
|
+
if (!installInfo) {
|
|
278
|
+
report.warn(`FST (${code})`, 'Card has resources.fsts but no install metadata');
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check local cache
|
|
283
|
+
const cacheDir = path.join(
|
|
284
|
+
process.env.HOME || process.env.USERPROFILE || '/tmp',
|
|
285
|
+
'.mt-eval', 'fsts', code
|
|
286
|
+
);
|
|
287
|
+
const cacheExists = fs.existsSync(cacheDir);
|
|
288
|
+
|
|
289
|
+
if (cacheExists) {
|
|
290
|
+
// Check for .hfstol files
|
|
291
|
+
try {
|
|
292
|
+
const files = fs.readdirSync(cacheDir).filter(f =>
|
|
293
|
+
f.endsWith('.hfstol') || f.endsWith('.hfst')
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
if (files.length > 0) {
|
|
297
|
+
const totalSize = files.reduce((sum, f) => {
|
|
298
|
+
try { return sum + fs.statSync(path.join(cacheDir, f)).size; } catch { return sum; }
|
|
299
|
+
}, 0);
|
|
300
|
+
const sizeStr = totalSize > 1024 * 1024
|
|
301
|
+
? `${(totalSize / (1024 * 1024)).toFixed(1)}MB`
|
|
302
|
+
: `${(totalSize / 1024).toFixed(0)}KB`;
|
|
303
|
+
|
|
304
|
+
const maturityNote = installInfo.maturity === 'stub'
|
|
305
|
+
? ' (stub — limited vocabulary)'
|
|
306
|
+
: '';
|
|
307
|
+
|
|
308
|
+
report.pass(
|
|
309
|
+
`FST (${code} — ${card.name || code})`,
|
|
310
|
+
`Installed: ${files.join(', ')} (${sizeStr})${maturityNote}`
|
|
311
|
+
);
|
|
312
|
+
} else {
|
|
313
|
+
report.warn(
|
|
314
|
+
`FST (${code})`,
|
|
315
|
+
`Cache dir exists but no .hfstol/.hfst files found in ${cacheDir}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
report.warn(`FST (${code})`, `Could not read cache dir: ${err.message}`);
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
// Not installed — give install guidance
|
|
323
|
+
const detail = installInfo.format === 'manual'
|
|
324
|
+
? `Not installed. Manual setup required — see ${installInfo.repo}`
|
|
325
|
+
: `Not installed. Run the eval harness to auto-download from ${installInfo.repo}@${installInfo.releaseTag || 'latest'}`;
|
|
326
|
+
|
|
327
|
+
report.warn(`FST (${code} — ${card.name || code})`, detail);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Check: Methods ───────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check method dependencies: API keys, packages, server reachability.
|
|
336
|
+
*
|
|
337
|
+
* @param {DiagnosticReport} report
|
|
338
|
+
* @param {string} cwd - Project root, for .env.local/.env key lookup
|
|
339
|
+
*/
|
|
340
|
+
async function checkMethods(report, cwd) {
|
|
341
|
+
output.raw('');
|
|
342
|
+
output.raw(' ── Methods ──');
|
|
343
|
+
output.raw('');
|
|
344
|
+
|
|
345
|
+
// API key checks.
|
|
346
|
+
//
|
|
347
|
+
// Every lookup goes through getEnvOrFileVar (process.env → .env.local →
|
|
348
|
+
// .env) — the SAME resolution chain the translation engine uses. Checking
|
|
349
|
+
// process.env alone made doctor report "not set" for a key the engine
|
|
350
|
+
// would happily read from .env.local (the very file our own setup help
|
|
351
|
+
// tells users to put it in) — a false negative that sent users chasing a
|
|
352
|
+
// problem they didn't have.
|
|
353
|
+
//
|
|
354
|
+
// For self-contained MT providers (deepl/google/microsoft) we resolve
|
|
355
|
+
// through the provider-env SSOT so doctor agrees with the method loader:
|
|
356
|
+
// it must never report "ready" under a name the loader won't read, nor
|
|
357
|
+
// "not set" for a name the loader *would* read. `method` keys into the
|
|
358
|
+
// SSOT; `key` is a single hardcoded name for providers not in the SSOT.
|
|
359
|
+
const apiKeyChecks = [
|
|
360
|
+
{ method: 'deepl', label: 'DeepL' },
|
|
361
|
+
{ method: 'google-translate', label: 'Google Translate' },
|
|
362
|
+
{ method: 'microsoft-translator', label: 'Microsoft Translator' },
|
|
363
|
+
{ key: 'OPENAI_API_KEY', label: 'OpenAI' },
|
|
364
|
+
{ key: 'ANTHROPIC_API_KEY', label: 'Anthropic' },
|
|
365
|
+
{ key: 'GEMINI_API_KEY', label: 'Gemini' },
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
// LLM via OpenRouter
|
|
369
|
+
const openRouterKey = getEnvOrFileVar('OPENROUTER_API_KEY', cwd);
|
|
370
|
+
if (openRouterKey) {
|
|
371
|
+
const source = process.env.OPENROUTER_API_KEY ? 'environment' : '.env.local/.env';
|
|
372
|
+
report.pass('OpenRouter API key', `Set via ${source} (LLM methods available)`);
|
|
373
|
+
|
|
374
|
+
// Try to validate by checking the /api/v1/models endpoint
|
|
375
|
+
try {
|
|
376
|
+
const resp = await fetch('https://openrouter.ai/api/v1/models', {
|
|
377
|
+
headers: { 'Authorization': `Bearer ${openRouterKey}` },
|
|
378
|
+
signal: AbortSignal.timeout(5000),
|
|
379
|
+
});
|
|
380
|
+
if (resp.ok) {
|
|
381
|
+
const data = await resp.json();
|
|
382
|
+
const modelCount = data.data?.length || 0;
|
|
383
|
+
report.pass('OpenRouter API', `Connected — ${modelCount} models available`);
|
|
384
|
+
} else {
|
|
385
|
+
report.warn('OpenRouter API', `HTTP ${resp.status} — key may be invalid`);
|
|
386
|
+
}
|
|
387
|
+
} catch (err) {
|
|
388
|
+
report.warn('OpenRouter API', `Could not reach API: ${err.message}`);
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
report.warn('OpenRouter API key', 'OPENROUTER_API_KEY not set (checked environment, .env.local, .env) — LLM methods unavailable');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Check individual provider API keys
|
|
395
|
+
for (const { method, key, label } of apiKeyChecks) {
|
|
396
|
+
if (method) {
|
|
397
|
+
// SSOT-backed provider: accept canonical + any alias, and report
|
|
398
|
+
// exactly which name resolved so doctor and the loader can't disagree.
|
|
399
|
+
const { value, name } = resolveProviderEnv(method, cwd);
|
|
400
|
+
const canonical = canonicalEnvName(method);
|
|
401
|
+
if (value) {
|
|
402
|
+
const via = name === canonical ? `${name} is set` : `${name} is set (alias for ${canonical})`;
|
|
403
|
+
report.pass(`${label}`, via);
|
|
404
|
+
} else {
|
|
405
|
+
// Not an error — just informational. Most users only use one or two providers.
|
|
406
|
+
output.raw(` ◻️ ${label} ${canonical} not set`);
|
|
407
|
+
}
|
|
408
|
+
} else if (getEnvOrFileVar(key, cwd)) {
|
|
409
|
+
report.pass(`${label}`, `${key} is set`);
|
|
410
|
+
} else {
|
|
411
|
+
// Not an error — just informational. Most users only use one or two providers.
|
|
412
|
+
output.raw(` ◻️ ${label} ${key} not set`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Check LibreTranslate server (if configured). Resolve the endpoint through
|
|
417
|
+
// the SSOT (LIBRETRANSLATE_API_URL canonical, LIBRETRANSLATE_URL / LIBRE_URL
|
|
418
|
+
// aliases) so doctor probes the same URL the LibreTranslate method reads.
|
|
419
|
+
const libreUrl = resolveProviderEnv('libretranslate', cwd).value;
|
|
420
|
+
if (libreUrl) {
|
|
421
|
+
// LIBRETRANSLATE_API_URL is the full translate endpoint (…/translate),
|
|
422
|
+
// but /languages hangs off the server base. Strip a trailing /translate
|
|
423
|
+
// so we probe the same server the method talks to, whether the user set
|
|
424
|
+
// the canonical endpoint var or a legacy base-URL alias.
|
|
425
|
+
const libreBase = libreUrl.replace(/\/translate\/?$/, '').replace(/\/$/, '');
|
|
426
|
+
try {
|
|
427
|
+
const resp = await fetch(`${libreBase}/languages`, {
|
|
428
|
+
signal: AbortSignal.timeout(5000),
|
|
429
|
+
});
|
|
430
|
+
if (resp.ok) {
|
|
431
|
+
const langs = await resp.json();
|
|
432
|
+
report.pass('LibreTranslate', `Server reachable at ${libreBase} — ${langs.length} languages`);
|
|
433
|
+
} else {
|
|
434
|
+
report.warn('LibreTranslate', `Server at ${libreBase} returned HTTP ${resp.status}`);
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
report.fail('LibreTranslate', `Cannot reach ${libreBase}: ${err.message}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Check if crk-translate (or other external methods) are pip-installed
|
|
442
|
+
try {
|
|
443
|
+
const { execSync } = await import('node:child_process');
|
|
444
|
+
const pipList = execSync('pip3 list --format=json 2>/dev/null || pip list --format=json 2>/dev/null', {
|
|
445
|
+
encoding: 'utf-8',
|
|
446
|
+
timeout: 10000,
|
|
447
|
+
});
|
|
448
|
+
const packages = JSON.parse(pipList);
|
|
449
|
+
const mtPackages = packages.filter(p =>
|
|
450
|
+
p.name.includes('translate') || p.name.includes('mt-eval') || p.name.includes('champollion')
|
|
451
|
+
);
|
|
452
|
+
if (mtPackages.length > 0) {
|
|
453
|
+
report.pass(
|
|
454
|
+
'Python packages',
|
|
455
|
+
mtPackages.map(p => `${p.name}==${p.version}`).join(', ')
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
// pip not available or failed — not critical for CLI-only users
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Find the language cards directory by walking up from CWD.
|
|
467
|
+
*/
|
|
468
|
+
function findCardsDir() {
|
|
469
|
+
// The cards are always at cli/shared/language-cards/ relative to the monorepo root
|
|
470
|
+
// Walk up looking for a cli/shared/language-cards/ directory
|
|
471
|
+
let dir = process.cwd();
|
|
472
|
+
for (let i = 0; i < 8; i++) {
|
|
473
|
+
const candidate = path.join(dir, 'cli', 'shared', 'language-cards');
|
|
474
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
475
|
+
|
|
476
|
+
// Also check shared/language-cards/ (when CWD is cli/)
|
|
477
|
+
const candidate2 = path.join(dir, 'shared', 'language-cards');
|
|
478
|
+
if (fs.existsSync(candidate2)) return candidate2;
|
|
479
|
+
|
|
480
|
+
const parent = path.dirname(dir);
|
|
481
|
+
if (parent === dir) break;
|
|
482
|
+
dir = parent;
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── Main Entry Point ─────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* @param {import('../types.js').CLIArgs} args - Parsed CLI arguments
|
|
491
|
+
* @param {string} cwd - Working directory
|
|
492
|
+
* @returns {Promise<number>} Exit code (0 = success, 1 = errors found)
|
|
493
|
+
*/
|
|
494
|
+
async function run(args, cwd) {
|
|
495
|
+
const subcommand = args._[1];
|
|
496
|
+
const report = new DiagnosticReport();
|
|
497
|
+
|
|
498
|
+
// --json: stdout carries exactly one JSON document. Quiet mode suppresses
|
|
499
|
+
// every check's raw line; the results array captures the same information.
|
|
500
|
+
const json = !!args.json;
|
|
501
|
+
if (json) output.setMode('quiet');
|
|
502
|
+
|
|
503
|
+
output.raw('');
|
|
504
|
+
output.raw(' Champollion Doctor — System Health Check');
|
|
505
|
+
output.raw(' ────────────────────────────────────────');
|
|
506
|
+
|
|
507
|
+
if (!subcommand || subcommand === 'all') {
|
|
508
|
+
// Full diagnostic
|
|
509
|
+
checkCards(report);
|
|
510
|
+
checkConfig(report, cwd);
|
|
511
|
+
checkFST(report, null);
|
|
512
|
+
await checkMethods(report, cwd);
|
|
513
|
+
} else if (subcommand === 'cards') {
|
|
514
|
+
checkCards(report);
|
|
515
|
+
} else if (subcommand === 'config') {
|
|
516
|
+
checkConfig(report, cwd);
|
|
517
|
+
} else if (subcommand === 'fst') {
|
|
518
|
+
const langCode = args._[2] || null;
|
|
519
|
+
checkFST(report, langCode);
|
|
520
|
+
} else if (subcommand === 'methods') {
|
|
521
|
+
await checkMethods(report, cwd);
|
|
522
|
+
} else {
|
|
523
|
+
if (json) {
|
|
524
|
+
console.log(JSON.stringify({
|
|
525
|
+
command: 'doctor',
|
|
526
|
+
error: `Unknown subcommand "${subcommand}"`,
|
|
527
|
+
subcommands: ['all', 'cards', 'config', 'fst', 'methods'],
|
|
528
|
+
}, null, 2));
|
|
529
|
+
return 0;
|
|
530
|
+
}
|
|
531
|
+
output.raw(`
|
|
532
|
+
Doctor Subcommands:
|
|
533
|
+
|
|
534
|
+
champollion doctor Run all checks
|
|
535
|
+
champollion doctor cards Verify language cards
|
|
536
|
+
champollion doctor config Validate project config
|
|
537
|
+
champollion doctor fst [code] Check FST installation
|
|
538
|
+
champollion doctor methods Check API keys and dependencies
|
|
539
|
+
`);
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (json) {
|
|
544
|
+
const { passed, warned, failed } = report.counts();
|
|
545
|
+
console.log(JSON.stringify({
|
|
546
|
+
command: 'doctor',
|
|
547
|
+
subcommand: subcommand || 'all',
|
|
548
|
+
results: report.results,
|
|
549
|
+
passed,
|
|
550
|
+
warned,
|
|
551
|
+
failed,
|
|
552
|
+
}, null, 2));
|
|
553
|
+
return failed > 0 ? 1 : 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return report.summarize();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export { run };
|