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
package/bin/cli.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Champollion CLI — Dispatcher
|
|
4
|
+
*
|
|
5
|
+
* Thin entry point that parses arguments and routes to command modules
|
|
6
|
+
* in lib/commands/. Each command is a separate module exporting:
|
|
7
|
+
* async function run(args, cwd) → exit code (0 or 1)
|
|
8
|
+
*
|
|
9
|
+
* This file handles ONLY:
|
|
10
|
+
* 1. Argument parsing (via Node.js built-in util.parseArgs)
|
|
11
|
+
* 2. Command routing
|
|
12
|
+
* 3. Per-command --help routing
|
|
13
|
+
* 4. Process exit codes
|
|
14
|
+
* 5. Top-level error handling
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { parseArgs } from 'node:util';
|
|
18
|
+
|
|
19
|
+
// -----------------------------------------------------------------
|
|
20
|
+
// Parse CLI arguments via util.parseArgs (Node 18.3+)
|
|
21
|
+
//
|
|
22
|
+
// strict: false so parseArgs collects unknown flags into `values`
|
|
23
|
+
// (with an inferred type) instead of throwing mid-parse — we validate
|
|
24
|
+
// them ourselves below against CLI_OPTIONS, which enumerates every
|
|
25
|
+
// flag any command reads. This turns a typo like `--forcekeys` or a
|
|
26
|
+
// removed flag like `--content` into a loud error instead of a
|
|
27
|
+
// silently ignored no-op.
|
|
28
|
+
// -----------------------------------------------------------------
|
|
29
|
+
const CLI_OPTIONS = {
|
|
30
|
+
// --- Boolean flags (shared across commands) ---
|
|
31
|
+
dry: { type: 'boolean' },
|
|
32
|
+
'dry-run': { type: 'boolean' }, // alias for --dry (more intuitive)
|
|
33
|
+
help: { type: 'boolean', short: 'h' },
|
|
34
|
+
version: { type: 'boolean', short: 'v' },
|
|
35
|
+
yes: { type: 'boolean', short: 'y' },
|
|
36
|
+
'warn-only': { type: 'boolean' },
|
|
37
|
+
'no-verify': { type: 'boolean' }, // skip post-sync verification
|
|
38
|
+
undo: { type: 'boolean' },
|
|
39
|
+
'force-content': { type: 'boolean' }, // ignore the content lock (TM still serves unchanged bodies/blocks)
|
|
40
|
+
'no-tm': { type: 'boolean' }, // skip Translation Memory for this sync run
|
|
41
|
+
css: { type: 'boolean' }, // fonts install --css: generate CSS snippet
|
|
42
|
+
json: { type: 'boolean' }, // machine-readable NDJSON output
|
|
43
|
+
quiet: { type: 'boolean', short: 'q' }, // suppress info/ok messages, show only warnings/errors
|
|
44
|
+
force: { type: 'boolean' }, // init --force: regenerate config over an existing one
|
|
45
|
+
|
|
46
|
+
// --- String flags (take a value) ---
|
|
47
|
+
config: { type: 'string' },
|
|
48
|
+
dir: { type: 'string' },
|
|
49
|
+
'content-dir': { type: 'string' },
|
|
50
|
+
source: { type: 'string' },
|
|
51
|
+
langs: { type: 'string' },
|
|
52
|
+
model: { type: 'string' },
|
|
53
|
+
method: { type: 'string' },
|
|
54
|
+
format: { type: 'string' },
|
|
55
|
+
'base-url': { type: 'string' },
|
|
56
|
+
out: { type: 'string' },
|
|
57
|
+
src: { type: 'string' },
|
|
58
|
+
'min-length': { type: 'string' },
|
|
59
|
+
'force-keys': { type: 'string' },
|
|
60
|
+
'list-keys': { type: 'boolean' }, // sync --dry: name every queued key per reason
|
|
61
|
+
|
|
62
|
+
concurrency: { type: 'string' }, // sets both json + content concurrency (backward compat)
|
|
63
|
+
'json-concurrency': { type: 'string' }, // max parallel API calls for JSON key-value translation (default: 50)
|
|
64
|
+
'content-concurrency': { type: 'string' }, // max parallel API calls for markdown content (default: 12)
|
|
65
|
+
'batch-size': { type: 'string' }, // keys per translation API call (positive integer; overrides config batchSize)
|
|
66
|
+
'max-cost': { type: 'string' }, // sync: USD cap — abort (exit 2) before any API call if the estimate exceeds it or is unknown
|
|
67
|
+
temperature: { type: 'string' }, // sampling temperature for LLM methods
|
|
68
|
+
'coaching-file': { type: 'string' }, // path to coaching prompt text file
|
|
69
|
+
locale: { type: 'string' }, // target locale for xliff export, tm clear/seed
|
|
70
|
+
'older-than': { type: 'string' }, // tm prune: also remove entries older than N days
|
|
71
|
+
matching: { type: 'string' }, // tm prune: also remove entries whose cached translation matches this regex
|
|
72
|
+
pair: { type: 'string' }, // language pair filter for sync (e.g., en:fr) + leaderboard (e.g., en>crk)
|
|
73
|
+
use: { type: 'string' }, // recommend: license lane (non-commercial|commercial)
|
|
74
|
+
sort: { type: 'string' }, // leaderboard sort key (composite, chrf, exact, etc.)
|
|
75
|
+
top: { type: 'string' }, // leaderboard: show top N results
|
|
76
|
+
install: { type: 'string' }, // leaderboard: install method config from rank N
|
|
77
|
+
apply: { type: 'boolean' }, // leaderboard --install --apply: auto-wire plugin in config
|
|
78
|
+
|
|
79
|
+
// --- register-corpus flags ---
|
|
80
|
+
license: { type: 'string' }, // register-corpus: license key / SPDX id / list number
|
|
81
|
+
tier: { type: 'string' }, // register-corpus: exposure tier (local-only|private|public)
|
|
82
|
+
exposure: { type: 'string' }, // register-corpus: alias for --tier
|
|
83
|
+
name: { type: 'string' }, // register-corpus: corpus name
|
|
84
|
+
publisher: { type: 'string' }, // register-corpus: publisher / author
|
|
85
|
+
description: { type: 'string' }, // register-corpus: one-line description
|
|
86
|
+
'source-lang': { type: 'string' }, // register-corpus: source language code (alt to --pair)
|
|
87
|
+
'target-lang': { type: 'string' }, // register-corpus: target language code (alt to --pair)
|
|
88
|
+
'repo-url': { type: 'string' }, // register-corpus: public-tier fetch-from-source URL
|
|
89
|
+
'source-url': { type: 'string' }, // register-corpus: public-tier canonical URL
|
|
90
|
+
'license-url': { type: 'string' }, // register-corpus: upstream license URL
|
|
91
|
+
builder: { type: 'string' }, // register-corpus: builder adapter id
|
|
92
|
+
sha256: { type: 'string' }, // register-corpus: built-corpus hash (optional)
|
|
93
|
+
contamination: { type: 'string' }, // register-corpus: NONE|LOW|MEDIUM|HIGH
|
|
94
|
+
domain: { type: 'string' }, // register-corpus: corpus domain
|
|
95
|
+
size: { type: 'string' }, // register-corpus: number of sentence pairs
|
|
96
|
+
id: { type: 'string' }, // register-corpus: override card id
|
|
97
|
+
version: { type: 'string' }, // register-corpus: corpus version (major)
|
|
98
|
+
'do-not-train': { type: 'boolean' }, // register-corpus: benchmark-integrity flag (default true)
|
|
99
|
+
'cards-dir': { type: 'string' }, // register-corpus: corpora-cards dir override (advanced/testing)
|
|
100
|
+
list: { type: 'boolean' }, // register-corpus + submit: print catalog / types
|
|
101
|
+
|
|
102
|
+
// --- submit flags ---
|
|
103
|
+
type: { type: 'string' }, // submit: submission type (dataset|resource|method|human-service|external-result)
|
|
104
|
+
values: { type: 'string' }, // submit: JSON object of field id -> value
|
|
105
|
+
field: { type: 'string', multiple: true }, // submit: --field id=value (repeatable)
|
|
106
|
+
attest: { type: 'boolean' }, // submit: confirm the required compliance attestation
|
|
107
|
+
consent: { type: 'boolean' }, // submit: human-service provider listing consent
|
|
108
|
+
repo: { type: 'string' }, // submit: override the GitHub repo for the issue URL
|
|
109
|
+
// --- register-corpus: sealed exposure tier ---
|
|
110
|
+
'seal-input': { type: 'string' }, // sealed: local corpus file to encrypt on-device
|
|
111
|
+
'threshold-pubkey': { type: 'string' }, // sealed: custodian group threshold public key (path or inline)
|
|
112
|
+
'custodian-group': { type: 'string' }, // sealed: custodian group id
|
|
113
|
+
'seal-out': { type: 'string' }, // sealed: where to write the ciphertext artifact
|
|
114
|
+
'qualifier-id': { type: 'string' }, // sealed: paired public qualifier card id (vYYYY)
|
|
115
|
+
'qualifier-threshold': { type: 'string' }, // sealed: qualifier clearance threshold
|
|
116
|
+
'key-scheme': { type: 'string' }, // sealed: custody scheme label (default TSS-3-of-5)
|
|
117
|
+
// --- seal-corpus verbs (keygen/seal/open — the organizer-node crypto bridge) ---
|
|
118
|
+
artifact: { type: 'string' }, // seal-corpus open: sealed artifact path
|
|
119
|
+
privkey: { type: 'string' }, // seal-corpus open: threshold private key (file|b64|PEM)
|
|
120
|
+
'card-block-out': { type: 'string' }, // seal-corpus seal: write the content-free card block here
|
|
121
|
+
// --- seal-corpus signing verbs (sign-keygen/sign/verify — Phase-B airgap score bundles) ---
|
|
122
|
+
payload: { type: 'string' }, // sign/verify: the exact bytes being signed/checked
|
|
123
|
+
'sig-out': { type: 'string' }, // sign: where to write the signature block JSON
|
|
124
|
+
sig: { type: 'string' }, // verify: the signature block JSON
|
|
125
|
+
pubkey: { type: 'string' }, // verify: Ed25519 public key (file|b64|PEM)
|
|
126
|
+
// --- serve flags (self-hosted api-method server) ---
|
|
127
|
+
port: { type: 'string' }, // serve: listen port (default 1822; 0 = ephemeral)
|
|
128
|
+
bind: { type: 'string' }, // serve: bind address (default 127.0.0.1 — loopback only)
|
|
129
|
+
token: { type: 'string' }, // serve: bearer token (or CHAMPOLLION_SERVE_TOKEN env)
|
|
130
|
+
'no-auth': { type: 'boolean' }, // serve: disable auth (loopback binds only)
|
|
131
|
+
'rate-limit': { type: 'string' }, // serve: requests/minute per IP (0 disables)
|
|
132
|
+
'max-body-bytes': { type: 'string' }, // serve: request body size cap
|
|
133
|
+
'max-cost-per-request': { type: 'string' }, // serve: USD cap per request (unknown estimates refuse)
|
|
134
|
+
'max-session-cost': { type: 'string' }, // serve: USD spend ceiling for the server process
|
|
135
|
+
'emit-manifest': { type: 'boolean' }, // serve: write the consumer method.json and exit
|
|
136
|
+
endpoint: { type: 'string' }, // serve --emit-manifest: public endpoint URL for the manifest
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const { values, positionals } = parseArgs({
|
|
140
|
+
args: process.argv.slice(2),
|
|
141
|
+
strict: false,
|
|
142
|
+
allowPositionals: true,
|
|
143
|
+
options: CLI_OPTIONS,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// -----------------------------------------------------------------
|
|
147
|
+
// Strict flag validation — reject flags no command defines.
|
|
148
|
+
//
|
|
149
|
+
// With strict: false, parseArgs happily accepted any `--typo` and the
|
|
150
|
+
// run proceeded as if it weren't there (e.g. `sync --force-key` doing a
|
|
151
|
+
// normal sync). Every real flag is declared in CLI_OPTIONS, so anything
|
|
152
|
+
// else in `values` is unknown: fail loud with pointers to help.
|
|
153
|
+
// -----------------------------------------------------------------
|
|
154
|
+
const unknownFlags = Object.keys(values).filter((name) => !(name in CLI_OPTIONS));
|
|
155
|
+
if (unknownFlags.length > 0) {
|
|
156
|
+
for (const name of unknownFlags) {
|
|
157
|
+
console.error(`[ERR] Unknown option: ${name.length === 1 ? '-' : '--'}${name}`);
|
|
158
|
+
}
|
|
159
|
+
console.error(' Run "champollion <command> --help" for that command\'s flags,');
|
|
160
|
+
console.error(' or "champollion help" for the full command list.');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Build the args object in the shape all command modules expect:
|
|
165
|
+
// { _: ['command', 'subcommand', ...], flagName: value, ... }
|
|
166
|
+
// This preserves backward compatibility with every command module.
|
|
167
|
+
const args = { _: positionals, ...values };
|
|
168
|
+
|
|
169
|
+
// --dry-run is an alias for --dry — merge so command modules only check args.dry
|
|
170
|
+
if (args['dry-run']) args.dry = true;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Exit only once stdout has actually been written.
|
|
174
|
+
*
|
|
175
|
+
* When stdout is a PIPE (`champollion card fra --json > out.json`, or any
|
|
176
|
+
* shell pipeline) Node's writes are asynchronous, so `process.exit()` discards
|
|
177
|
+
* whatever is still buffered. `card --json` for a well-resourced language is
|
|
178
|
+
* ~200 KB and was being truncated mid-object — the command exited 0 and
|
|
179
|
+
* emitted invalid JSON, which is worse than failing. It looked fine in a
|
|
180
|
+
* terminal, where stdout is synchronous, so nobody saw it.
|
|
181
|
+
*
|
|
182
|
+
* Writing an empty string gives us a callback that fires after the buffer
|
|
183
|
+
* drains; the exit code is set first so the process is correct either way.
|
|
184
|
+
*/
|
|
185
|
+
function exitWhenFlushed(code) {
|
|
186
|
+
process.exitCode = code;
|
|
187
|
+
process.stdout.write('', () => process.exit(code));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const command = args._[0] || 'help';
|
|
191
|
+
const cwd = process.cwd();
|
|
192
|
+
|
|
193
|
+
// -----------------------------------------------------------------
|
|
194
|
+
// --version: print version from package.json and exit
|
|
195
|
+
// -----------------------------------------------------------------
|
|
196
|
+
if (args.version) {
|
|
197
|
+
// URL is a global — no need to import node:url.
|
|
198
|
+
// Dynamic import of node:fs is consistent with the lazy-loading
|
|
199
|
+
// strategy used for command modules below.
|
|
200
|
+
import('node:fs').then(fs => {
|
|
201
|
+
const pkgPath = new URL('../package.json', import.meta.url);
|
|
202
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
203
|
+
console.log(`champollion v${pkg.version}`);
|
|
204
|
+
process.exit(0);
|
|
205
|
+
});
|
|
206
|
+
} else
|
|
207
|
+
|
|
208
|
+
// -----------------------------------------------------------------
|
|
209
|
+
// Per-command --help: intercept before loading command modules
|
|
210
|
+
// If the user runs `champollion <cmd> --help`, show focused help for
|
|
211
|
+
// that command without loading its module (fast, no side effects).
|
|
212
|
+
// -----------------------------------------------------------------
|
|
213
|
+
if (args.help && command !== 'help') {
|
|
214
|
+
import('../lib/command-help.js').then(({ showCommandHelp }) => {
|
|
215
|
+
const found = showCommandHelp(command);
|
|
216
|
+
if (!found) {
|
|
217
|
+
console.error(`[ERR] Unknown command: ${command}`);
|
|
218
|
+
console.error(' Run "champollion help" to see all commands.');
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
process.exit(0);
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
// -----------------------------------------------------------------
|
|
225
|
+
// Command routing — dynamic import() keeps startup fast (ESM-native)
|
|
226
|
+
// -----------------------------------------------------------------
|
|
227
|
+
const commands = {
|
|
228
|
+
init: () => import('../lib/commands/init.js'),
|
|
229
|
+
sync: () => import('../lib/commands/sync.js'),
|
|
230
|
+
serve: () => import('../lib/commands/serve.js'),
|
|
231
|
+
watch: () => import('../lib/commands/watch.js'),
|
|
232
|
+
audit: () => import('../lib/commands/audit.js'),
|
|
233
|
+
card: () => import('../lib/commands/card.js'),
|
|
234
|
+
'register-corpus': () => import('../lib/commands/register-corpus.js'),
|
|
235
|
+
'seal-corpus': () => import('../lib/commands/seal-corpus.js'),
|
|
236
|
+
submit: () => import('../lib/commands/submit.js'),
|
|
237
|
+
lint: () => import('../lib/commands/lint.js'),
|
|
238
|
+
status: () => import('../lib/commands/status.js'),
|
|
239
|
+
provenance: () => import('../lib/commands/provenance.js'),
|
|
240
|
+
wrap: () => import('../lib/commands/wrap.js'),
|
|
241
|
+
seo: () => import('../lib/commands/seo.js'),
|
|
242
|
+
integrity: () => import('../lib/commands/integrity.js'),
|
|
243
|
+
'repair-script': () => import('../lib/commands/repair-script.js'),
|
|
244
|
+
plugin: () => import('../lib/commands/plugin.js'),
|
|
245
|
+
fonts: () => import('../lib/commands/fonts.js'),
|
|
246
|
+
tm: () => import('../lib/commands/tm.js'),
|
|
247
|
+
xliff: () => import('../lib/commands/xliff.js'),
|
|
248
|
+
models: () => import('../lib/commands/models.js'),
|
|
249
|
+
verify: () => import('../lib/commands/verify.js'),
|
|
250
|
+
leaderboard: () => import('../lib/commands/leaderboard.js'),
|
|
251
|
+
recommend: () => import('../lib/commands/recommend.js'),
|
|
252
|
+
doctor: () => import('../lib/commands/doctor.js'),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (commands[command]) {
|
|
256
|
+
// Packaged installs only: invalidate card-cache entries whose
|
|
257
|
+
// upstream updated_at moved (TTL-gated to once per day, instant
|
|
258
|
+
// no-op in repo checkouts, and never blocks or fails the command
|
|
259
|
+
// on network trouble — see lib/cards/refresh.js).
|
|
260
|
+
import('../lib/cards/refresh.js')
|
|
261
|
+
.then(mod => mod.maybeRefreshCardCache())
|
|
262
|
+
.catch(() => {})
|
|
263
|
+
.then(() => commands[command]())
|
|
264
|
+
.then(mod => mod.run(args, cwd))
|
|
265
|
+
.then(code => exitWhenFlushed(code || 0))
|
|
266
|
+
.catch(err => {
|
|
267
|
+
console.error(`[ERR] ${command} failed:`, err.message);
|
|
268
|
+
exitWhenFlushed(1);
|
|
269
|
+
});
|
|
270
|
+
} else if (command === 'help') {
|
|
271
|
+
import('../lib/commands/help.js').then(mod => mod.run());
|
|
272
|
+
} else {
|
|
273
|
+
// Unknown command — error loudly so CI typos don't silently pass
|
|
274
|
+
console.error(`[ERR] Unknown command: "${command}"`);
|
|
275
|
+
console.error(' Run "champollion help" to see all commands.');
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* champollion — Programmatic API entry point.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the public API surface for consumers who `import` the package
|
|
5
|
+
* directly (e.g., custom build scripts, programmatic sync, or method class
|
|
6
|
+
* extensions). The CLI (`bin/cli.js`) does NOT use this file — it calls
|
|
7
|
+
* into `lib/` directly.
|
|
8
|
+
*
|
|
9
|
+
* EXPORTS:
|
|
10
|
+
* - Translation methods: LLM, DirectLLM, LLMCoached, GoogleTranslate, API,
|
|
11
|
+
* DeepL, MicrosoftTranslator, LibreTranslate, OpenAI, Anthropic, Gemini
|
|
12
|
+
* - Orchestrator: getMethod, translateBatch, translateRawContent
|
|
13
|
+
* - Configuration: resolveConfig, generateConfigTemplate, resolvePairs
|
|
14
|
+
* - Language cards: getLanguageCard, getLanguageReference, getRegister,
|
|
15
|
+
* getRegisterPresets, getFormality, getGenderGuidance, getAllLanguageCodes,
|
|
16
|
+
* resolveCode
|
|
17
|
+
* - Sync: runSync, runContentSync
|
|
18
|
+
* - Quality: validateTranslations
|
|
19
|
+
* - Utilities: loadApiKey, getEnvOrFileVar
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ── Translation method classes ─────────────────────────────────────
|
|
23
|
+
export { TranslationMethod } from './lib/methods/base.js';
|
|
24
|
+
export { LLMMethod } from './lib/methods/llm.js';
|
|
25
|
+
export { DirectLLMMethod } from './lib/methods/direct-llm.js';
|
|
26
|
+
export { LLMCoachedMethod } from './lib/methods/llm-coached.js';
|
|
27
|
+
export { GoogleTranslateMethod } from './lib/methods/google-translate.js';
|
|
28
|
+
export { APIMethod } from './lib/methods/api.js';
|
|
29
|
+
export { DeepLMethod } from './lib/methods/deepl.js';
|
|
30
|
+
export { MicrosoftTranslatorMethod } from './lib/methods/microsoft-translator.js';
|
|
31
|
+
export { LibreTranslateMethod } from './lib/methods/libretranslate.js';
|
|
32
|
+
export { OpenAIMethod } from './lib/methods/openai.js';
|
|
33
|
+
export { AnthropicMethod } from './lib/methods/anthropic.js';
|
|
34
|
+
export { GeminiMethod } from './lib/methods/gemini.js';
|
|
35
|
+
|
|
36
|
+
// ── Translation orchestrator ───────────────────────────────────────
|
|
37
|
+
export {
|
|
38
|
+
getMethod,
|
|
39
|
+
translateBatch,
|
|
40
|
+
translateRawContent,
|
|
41
|
+
} from './lib/translate.js';
|
|
42
|
+
|
|
43
|
+
// ── Configuration & pair resolution ────────────────────────────────
|
|
44
|
+
export {
|
|
45
|
+
resolveConfig,
|
|
46
|
+
generateConfigTemplate,
|
|
47
|
+
DEFAULT_OPENROUTER_MODEL,
|
|
48
|
+
DEFAULT_BATCH_SIZE,
|
|
49
|
+
} from './lib/config.js';
|
|
50
|
+
export { resolvePairs } from './lib/pairs.js';
|
|
51
|
+
|
|
52
|
+
// ── Language cards & registers ─────────────────────────────────────
|
|
53
|
+
export {
|
|
54
|
+
getLanguageCard,
|
|
55
|
+
getLanguageReference,
|
|
56
|
+
getRegister,
|
|
57
|
+
getRegisterPresets,
|
|
58
|
+
getFormality,
|
|
59
|
+
getGenderGuidance,
|
|
60
|
+
getAllLanguageCodes,
|
|
61
|
+
getMethodSupport,
|
|
62
|
+
resolveCode,
|
|
63
|
+
// Dynamic card tier (packaged installs): warm the per-user cache up
|
|
64
|
+
// front instead of paying a per-miss synchronous fetch.
|
|
65
|
+
prefetchLanguageCards,
|
|
66
|
+
getCardSourceInfo,
|
|
67
|
+
} from './lib/registers.js';
|
|
68
|
+
|
|
69
|
+
// Cache staleness check for the dynamic card tier (no-op in repo
|
|
70
|
+
// checkouts; TTL-gated; never throws). bin/cli.js runs this before
|
|
71
|
+
// every command — programmatic consumers can call it themselves.
|
|
72
|
+
export { maybeRefreshCardCache } from './lib/cards/refresh.js';
|
|
73
|
+
|
|
74
|
+
// The ONE card adapter + reader primitives, re-exported so out-of-repo
|
|
75
|
+
// consumers (the MCP server depends on this package for exactly this) read
|
|
76
|
+
// cards through the same seam as everything else. A ninth private reader is
|
|
77
|
+
// how the MCP server came to serve "[object Object]" for weeks.
|
|
78
|
+
export {
|
|
79
|
+
AGREEMENT, attributions, atlasVersion, coverage, display,
|
|
80
|
+
isAttributed, isDisputed, listCodes, normalizeCard, readCard, requireAtlas,
|
|
81
|
+
} from './lib/cards/reader.js';
|
|
82
|
+
|
|
83
|
+
// ── Sync pipeline ──────────────────────────────────────────────────
|
|
84
|
+
export { runSync, runContentSync } from './lib/sync.js';
|
|
85
|
+
|
|
86
|
+
// ── Quality gate ───────────────────────────────────────────────────
|
|
87
|
+
export { validateTranslations } from './lib/validate.js';
|
|
88
|
+
|
|
89
|
+
// ── API key resolution ─────────────────────────────────────────────
|
|
90
|
+
export { loadApiKey, getEnvOrFileVar } from './lib/api-key.js';
|
|
91
|
+
|
|
92
|
+
// ── Coaching utilities (for custom method implementations) ─────────
|
|
93
|
+
export {
|
|
94
|
+
loadCoachingData,
|
|
95
|
+
findDictionaryMatches,
|
|
96
|
+
buildCoachedSystemMessage,
|
|
97
|
+
buildContentCoachingBlock,
|
|
98
|
+
DEFAULT_COACHING_DIR,
|
|
99
|
+
} from './lib/methods/llm-coached.js';
|
|
100
|
+
|
|
101
|
+
// ── Translation Memory ─────────────────────────────────────────────
|
|
102
|
+
export {
|
|
103
|
+
loadTM,
|
|
104
|
+
saveTM,
|
|
105
|
+
lookupTM,
|
|
106
|
+
storeTM,
|
|
107
|
+
partitionByTM,
|
|
108
|
+
tmMethodKey,
|
|
109
|
+
tmSize,
|
|
110
|
+
} from './lib/tm.js';
|
|
111
|
+
|
|
112
|
+
// ── XLIFF interchange ──────────────────────────────────────────────
|
|
113
|
+
export {
|
|
114
|
+
exportXLIFF,
|
|
115
|
+
importXLIFF,
|
|
116
|
+
} from './lib/xliff.js';
|
|
117
|
+
|
|
118
|
+
// ── ICU MessageFormat ──────────────────────────────────────────────
|
|
119
|
+
export {
|
|
120
|
+
isICUString,
|
|
121
|
+
parseICU,
|
|
122
|
+
reassembleICU,
|
|
123
|
+
extractTranslatableSegments,
|
|
124
|
+
getRequiredPluralCategories,
|
|
125
|
+
} from './lib/icu.js';
|
|
126
|
+
|
|
127
|
+
// ── Terminology enforcement ────────────────────────────────────────
|
|
128
|
+
export { verifyTerminology } from './lib/terminology.js';
|
|
129
|
+
|
|
130
|
+
// ── Integrity auditing ─────────────────────────────────────────────
|
|
131
|
+
export {
|
|
132
|
+
auditLocalePair,
|
|
133
|
+
formatIntegrityReport,
|
|
134
|
+
checkPluralCategories,
|
|
135
|
+
} from './lib/integrity.js';
|
package/lib/api-key.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key loader — reads translation API keys from environment or .env files.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS: The API key resolution logic was embedded in sync.js
|
|
5
|
+
* (the "god module"). Extracting it provides:
|
|
6
|
+
* 1. A single, testable function for key resolution
|
|
7
|
+
* 2. Clear priority chain: process.env → .env.local → .env
|
|
8
|
+
* 3. Consistent handling of quoted values and export prefixes
|
|
9
|
+
*
|
|
10
|
+
* This is a simple key=value parser. It handles:
|
|
11
|
+
* - Standard: KEY=value
|
|
12
|
+
* - Quoted: KEY="value" or KEY='value'
|
|
13
|
+
* - Export prefix: export KEY=value
|
|
14
|
+
* - Comments: lines starting with #
|
|
15
|
+
* - Empty lines
|
|
16
|
+
*
|
|
17
|
+
* It does NOT handle multiline values, variable expansion ($OTHER_KEY),
|
|
18
|
+
* or other dotenv features. For those, install dotenv.
|
|
19
|
+
*
|
|
20
|
+
* Priority: process.env → .env.local → .env
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse a single line from an env file.
|
|
28
|
+
* Returns { key, value } or null if the line is empty/comment.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} line - Raw line from env file
|
|
31
|
+
* @returns {{ key: string, value: string } | null}
|
|
32
|
+
*/
|
|
33
|
+
function parseEnvLine(line) {
|
|
34
|
+
let trimmed = line.trim();
|
|
35
|
+
|
|
36
|
+
// Skip empty lines and comments
|
|
37
|
+
if (!trimmed || trimmed.startsWith('#')) return null;
|
|
38
|
+
|
|
39
|
+
// Handle `export KEY=value` syntax
|
|
40
|
+
if (trimmed.startsWith('export ')) {
|
|
41
|
+
trimmed = trimmed.slice(7).trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const eqIdx = trimmed.indexOf('=');
|
|
45
|
+
if (eqIdx <= 0) return null;
|
|
46
|
+
|
|
47
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
48
|
+
const val = normalizeKeyValue(trimmed.slice(eqIdx + 1));
|
|
49
|
+
|
|
50
|
+
return { key, value: val };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalize a raw key value the SAME way whether it came from a .env file or
|
|
55
|
+
* directly from process.env: trim whitespace and strip one layer of surrounding
|
|
56
|
+
* quotes. Without this, an exported quoted secret (e.g. `set -a; source
|
|
57
|
+
* .env.local` where the file stores KEY="sk-...") reaches the Authorization
|
|
58
|
+
* header verbatim and 401s — even though the literal key is correct.
|
|
59
|
+
*/
|
|
60
|
+
function normalizeKeyValue(raw) {
|
|
61
|
+
return String(raw).trim().replace(/^["']|["']$/g, '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load an API key by name, checking process.env first, then .env.local,
|
|
66
|
+
* then .env in the project root.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} config - Config object with apiKeyEnvVar property
|
|
69
|
+
* @param {string} config.apiKeyEnvVar - Name of the environment variable
|
|
70
|
+
* @param {string} cwd - Project root directory
|
|
71
|
+
* @returns {string|null} The API key value, or null if not found
|
|
72
|
+
*/
|
|
73
|
+
function loadApiKey(config, cwd) {
|
|
74
|
+
return getEnvOrFileVar(config.apiKeyEnvVar, cwd);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load an environment variable or key from file by name, checking process.env first,
|
|
79
|
+
* then .env.local, then .env in the project root.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} keyName - Name of the environment variable
|
|
82
|
+
* @param {string} [cwd=process.cwd()] - Project root directory
|
|
83
|
+
* @returns {string|null} The variable value, or null if not found
|
|
84
|
+
*/
|
|
85
|
+
function getEnvOrFileVar(keyName, cwd = process.cwd()) {
|
|
86
|
+
// Priority 1: Check environment directly. Normalize identically to file
|
|
87
|
+
// values (trim + strip surrounding quotes) so a quoted/padded exported secret
|
|
88
|
+
// never reaches an Authorization header verbatim.
|
|
89
|
+
if (process.env[keyName]) {
|
|
90
|
+
return normalizeKeyValue(process.env[keyName]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Priority 2: .env.local (Vercel/Next.js convention)
|
|
94
|
+
const envLocalPath = path.join(cwd, '.env.local');
|
|
95
|
+
const fromLocal = readKeyFromFile(envLocalPath, keyName);
|
|
96
|
+
if (fromLocal) return fromLocal;
|
|
97
|
+
|
|
98
|
+
// Priority 3: .env
|
|
99
|
+
const envPath = path.join(cwd, '.env');
|
|
100
|
+
const fromEnv = readKeyFromFile(envPath, keyName);
|
|
101
|
+
if (fromEnv) return fromEnv;
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Read a specific key from an env file.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} filePath - Path to the env file
|
|
110
|
+
* @param {string} targetKey - Key name to find
|
|
111
|
+
* @returns {string|null} The value, or null if not found
|
|
112
|
+
*/
|
|
113
|
+
function readKeyFromFile(filePath, targetKey) {
|
|
114
|
+
if (!fs.existsSync(filePath)) return null;
|
|
115
|
+
|
|
116
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
117
|
+
for (const line of content.split('\n')) {
|
|
118
|
+
const parsed = parseEnvLine(line);
|
|
119
|
+
if (parsed && parsed.key === targetKey) {
|
|
120
|
+
return parsed.value;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { loadApiKey, parseEnvLine, getEnvOrFileVar };
|