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.
Files changed (170) hide show
  1. package/LICENSE +133 -0
  2. package/README.md +387 -0
  3. package/bin/cli.js +278 -0
  4. package/index.js +135 -0
  5. package/lib/api-key.js +127 -0
  6. package/lib/autofix.js +432 -0
  7. package/lib/bridge/method_bridge.py +430 -0
  8. package/lib/card-source-resolution.mjs +284 -0
  9. package/lib/cards/cache.js +169 -0
  10. package/lib/cards/env.js +82 -0
  11. package/lib/cards/fetch-card-child.js +38 -0
  12. package/lib/cards/reader.js +435 -0
  13. package/lib/cards/refresh.js +111 -0
  14. package/lib/cards/remote.js +387 -0
  15. package/lib/cldf-export.mjs +540 -0
  16. package/lib/cldf-terms.mjs +62 -0
  17. package/lib/command-help.js +790 -0
  18. package/lib/commands/audit.js +49 -0
  19. package/lib/commands/card.js +454 -0
  20. package/lib/commands/doctor.js +559 -0
  21. package/lib/commands/fonts.js +489 -0
  22. package/lib/commands/help.js +91 -0
  23. package/lib/commands/init.js +1259 -0
  24. package/lib/commands/integrity.js +148 -0
  25. package/lib/commands/leaderboard.js +478 -0
  26. package/lib/commands/lint.js +30 -0
  27. package/lib/commands/models.js +177 -0
  28. package/lib/commands/plugin.js +103 -0
  29. package/lib/commands/provenance.js +45 -0
  30. package/lib/commands/recommend.js +75 -0
  31. package/lib/commands/register-corpus.js +678 -0
  32. package/lib/commands/repair-script.js +42 -0
  33. package/lib/commands/seal-corpus.js +355 -0
  34. package/lib/commands/seo.js +72 -0
  35. package/lib/commands/serve.js +147 -0
  36. package/lib/commands/status.js +265 -0
  37. package/lib/commands/submit.js +332 -0
  38. package/lib/commands/sync.js +89 -0
  39. package/lib/commands/tm.js +573 -0
  40. package/lib/commands/verify.js +39 -0
  41. package/lib/commands/watch.js +20 -0
  42. package/lib/commands/wrap.js +138 -0
  43. package/lib/commands/xliff.js +327 -0
  44. package/lib/commercial-eligibility.js +235 -0
  45. package/lib/concurrent.js +87 -0
  46. package/lib/config.js +523 -0
  47. package/lib/contamination-lane.js +76 -0
  48. package/lib/content-sync.js +731 -0
  49. package/lib/content.js +733 -0
  50. package/lib/corpus-registration.mjs +608 -0
  51. package/lib/cost-report.js +346 -0
  52. package/lib/diff.js +155 -0
  53. package/lib/docusaurus-sync.js +1256 -0
  54. package/lib/flatten.js +55 -0
  55. package/lib/format.js +954 -0
  56. package/lib/hash.js +159 -0
  57. package/lib/icu.js +473 -0
  58. package/lib/integrity.js +689 -0
  59. package/lib/license-gate.mjs +478 -0
  60. package/lib/license-identify.mjs +229 -0
  61. package/lib/lint.js +629 -0
  62. package/lib/method-manifest.js +60 -0
  63. package/lib/methods/anthropic.js +140 -0
  64. package/lib/methods/apertium.js +163 -0
  65. package/lib/methods/api.js +316 -0
  66. package/lib/methods/base.js +184 -0
  67. package/lib/methods/content-separator.js +45 -0
  68. package/lib/methods/deepl.js +426 -0
  69. package/lib/methods/direct-llm.js +586 -0
  70. package/lib/methods/external.js +332 -0
  71. package/lib/methods/fetch-with-retry.js +124 -0
  72. package/lib/methods/gemini.js +147 -0
  73. package/lib/methods/google-translate.js +402 -0
  74. package/lib/methods/http-utils.js +122 -0
  75. package/lib/methods/libretranslate.js +314 -0
  76. package/lib/methods/llm-coached.js +670 -0
  77. package/lib/methods/llm.js +592 -0
  78. package/lib/methods/local.js +76 -0
  79. package/lib/methods/microsoft-translator.js +331 -0
  80. package/lib/methods/openai.js +131 -0
  81. package/lib/methods/openrouter-client.js +327 -0
  82. package/lib/methods/openrouter-pricing.js +156 -0
  83. package/lib/methods/provider-env.js +115 -0
  84. package/lib/methods/provider-pricing.js +310 -0
  85. package/lib/methods/tilde.js +150 -0
  86. package/lib/methods/translated.js +229 -0
  87. package/lib/methods/translation-error.js +80 -0
  88. package/lib/models.js +258 -0
  89. package/lib/no-translate.js +233 -0
  90. package/lib/output.js +238 -0
  91. package/lib/pairs.js +547 -0
  92. package/lib/plugins.js +447 -0
  93. package/lib/provenance.js +323 -0
  94. package/lib/recommend.js +648 -0
  95. package/lib/registers.js +1185 -0
  96. package/lib/repair-script.js +266 -0
  97. package/lib/scripts.js +994 -0
  98. package/lib/seal.mjs +464 -0
  99. package/lib/sealed-qualifier.mjs +211 -0
  100. package/lib/security.js +59 -0
  101. package/lib/segment.js +369 -0
  102. package/lib/seo.js +275 -0
  103. package/lib/serve.js +854 -0
  104. package/lib/string-classify.js +85 -0
  105. package/lib/submit.mjs +344 -0
  106. package/lib/sync.js +969 -0
  107. package/lib/tags/bcp47.js +202 -0
  108. package/lib/tags/resolve.js +314 -0
  109. package/lib/terminology.js +111 -0
  110. package/lib/tm-seed.js +294 -0
  111. package/lib/tm.js +515 -0
  112. package/lib/translate-pair.js +197 -0
  113. package/lib/translate.js +203 -0
  114. package/lib/types.js +230 -0
  115. package/lib/validate.js +510 -0
  116. package/lib/verify.js +451 -0
  117. package/lib/watch.js +145 -0
  118. package/lib/xliff.js +184 -0
  119. package/package.json +93 -0
  120. package/shared/ATTRIBUTION.md +145 -0
  121. package/shared/CORPORA-CARDS.md +288 -0
  122. package/shared/DATA-SOVEREIGNTY.md +500 -0
  123. package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
  124. package/shared/card-lint-baseline.json +3189 -0
  125. package/shared/cards-fallback.json +1 -0
  126. package/shared/catalogue/card-config.json +6091 -0
  127. package/shared/catalogue/external-results.json +3888 -0
  128. package/shared/catalogue/gender-guidance.json +1038 -0
  129. package/shared/catalogue/method-coverage.json +1751 -0
  130. package/shared/catalogue/metric-coverage.json +170 -0
  131. package/shared/catalogue/metric-reliability.json +1 -0
  132. package/shared/catalogue/register-presets.json +3180 -0
  133. package/shared/catalogue/vitality-scales.json +55 -0
  134. package/shared/cldr-index.json +1115 -0
  135. package/shared/code-bridge.json +253 -0
  136. package/shared/corpora-cards-v1-reference.md +281 -0
  137. package/shared/curated-dictionary-flags.json +35 -0
  138. package/shared/curated-endonyms.json +35 -0
  139. package/shared/curated-fsts.json +51 -0
  140. package/shared/curated-orthography-conventions.json +26 -0
  141. package/shared/curated-sil-resources.json +374 -0
  142. package/shared/curated-tools.json +41 -0
  143. package/shared/docent/corpus.json +11333 -0
  144. package/shared/docent/faq.en.json +564 -0
  145. package/shared/docent/register-blocks.json +60 -0
  146. package/shared/docent/system-prompt.md +144 -0
  147. package/shared/domain-taxonomy.json +35 -0
  148. package/shared/explainers/glossary.json +2975 -0
  149. package/shared/explainers/tc-features.json +20112 -0
  150. package/shared/explainers/term-watchlist.json +147 -0
  151. package/shared/human-services.json +59 -0
  152. package/shared/license-corrections.json +261 -0
  153. package/shared/license-evidence.json +13452 -0
  154. package/shared/licenses.json +6781 -0
  155. package/shared/method-registry.json +236 -0
  156. package/shared/metric-registry.json +620 -0
  157. package/shared/model-aliases.json +7 -0
  158. package/shared/schemas/champollion-plugin.schema.json +206 -0
  159. package/shared/schemas/corpora-card.schema.json +957 -0
  160. package/shared/schemas/domain-taxonomy.schema.json +64 -0
  161. package/shared/schemas/external-results.schema.json +314 -0
  162. package/shared/schemas/human-services.schema.json +90 -0
  163. package/shared/schemas/language-card.schema.json +1308 -0
  164. package/shared/schemas/licenses.schema.json +155 -0
  165. package/shared/schemas/method-card.schema.json +412 -0
  166. package/shared/schemas/method-registry.schema.json +85 -0
  167. package/shared/schemas/metric-registry.schema.json +96 -0
  168. package/shared/schemas/metric-reliability.schema.json +178 -0
  169. package/shared/schemas/model-aliases.schema.json +27 -0
  170. 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 };