@x18n/cli 0.0.6 → 0.0.8
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/dist/cli.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as out, c as runPlan, d as useColor, f as wireDiagnostics, i as openWorkspace, l as singleProject, n as displayPath, o as printJson, r as exitCodeOf, s as readInput, t as cwd, u as stdinArg } from "./context-j5IMF1tl.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { PluginFileSchema,
|
|
3
|
+
import { PluginFileSchema, errors, isJsonObject, isX18nError, parseMessageBatch, placeholderProblems, renderPrompt, toErrorInfo, toWireRange, validate } from "@x18n/core";
|
|
4
4
|
import process from "node:process";
|
|
5
|
+
import { styleText } from "node:util";
|
|
5
6
|
import cac from "cac";
|
|
7
|
+
import { formatDiagnostic } from "nostics";
|
|
8
|
+
import { ansiFormatter } from "nostics/formatters/ansi";
|
|
6
9
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
10
|
import { dirname, join, resolve } from "node:path";
|
|
8
11
|
import { createInterface } from "node:readline/promises";
|
|
@@ -12,14 +15,14 @@ import { existsSync } from "node:fs";
|
|
|
12
15
|
import { namespaceFromPath } from "@x18n/builtins/finders";
|
|
13
16
|
import { parse, stringify } from "yaml";
|
|
14
17
|
//#region package.json
|
|
15
|
-
var version = "0.0.
|
|
18
|
+
var version = "0.0.8";
|
|
16
19
|
//#endregion
|
|
17
20
|
//#region src/commands/change.ts
|
|
18
21
|
async function project(options) {
|
|
19
22
|
return singleProject(await openWorkspace(options));
|
|
20
23
|
}
|
|
21
24
|
async function set(key, options) {
|
|
22
|
-
if (options.value === void 0) throw
|
|
25
|
+
if (options.value === void 0) throw errors.X18N4008({ reason: "pass --value <text> (or --value - to read stdin)" });
|
|
23
26
|
const loaded = await project(options);
|
|
24
27
|
const value = options.value === "\0stdin" ? (await readInput(stdinArg)).replace(/\r?\n$/, "") : String(options.value);
|
|
25
28
|
const locale = options.locale ?? loaded.project.sourceLocale;
|
|
@@ -37,7 +40,10 @@ async function apply(source, options) {
|
|
|
37
40
|
try {
|
|
38
41
|
data = JSON.parse(await readInput(source));
|
|
39
42
|
} catch (error) {
|
|
40
|
-
throw
|
|
43
|
+
throw errors.X18N4008({
|
|
44
|
+
reason: `${source} is not JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
45
|
+
cause: error
|
|
46
|
+
});
|
|
41
47
|
}
|
|
42
48
|
const ops = parseMessageBatch(p, data);
|
|
43
49
|
const rejected = placeholderProblems(p, ops);
|
|
@@ -46,7 +52,7 @@ async function apply(source, options) {
|
|
|
46
52
|
printJson("apply", {
|
|
47
53
|
applied: false,
|
|
48
54
|
rejected
|
|
49
|
-
}, [], [
|
|
55
|
+
}, [], [toErrorInfo(errors.X18N4007({ count: rejected.length }))]);
|
|
50
56
|
return 1;
|
|
51
57
|
}
|
|
52
58
|
for (const r of rejected) out(`rejected ${r.locale} ${r.key}: ${[r.missing.length ? `missing ${r.missing.join(", ")}` : "", r.extra.length ? `unexpected ${r.extra.join(", ")}` : ""].filter(Boolean).join("; ")}`);
|
|
@@ -144,7 +150,7 @@ async function report(workspace, options) {
|
|
|
144
150
|
const roots = new Map(workspace.projects.map((p) => [p.name, p.root]));
|
|
145
151
|
for (const [i, id] of ids.entries()) {
|
|
146
152
|
const reporter = available.get(id);
|
|
147
|
-
if (!reporter) throw
|
|
153
|
+
if (!reporter) throw errors.X18N0001({ issue: `unknown reporter "${id}" (${[...available.keys()].join(", ")})` });
|
|
148
154
|
const text = await reporter.report(inputs, {
|
|
149
155
|
color: !outputs[i] && useColor(options) && id === "pretty",
|
|
150
156
|
displayPath: (path, project) => displayPath(roots.get(project) ?? "", path)
|
|
@@ -152,10 +158,10 @@ async function report(workspace, options) {
|
|
|
152
158
|
if (outputs[i]) await writeFile(outputs[i], text);
|
|
153
159
|
else if (text) process.stdout.write(text);
|
|
154
160
|
}
|
|
155
|
-
const
|
|
161
|
+
const errorCount = inputs.reduce((sum, input) => sum + input.summary.errors, 0);
|
|
156
162
|
const warnings = inputs.reduce((sum, input) => sum + input.summary.warnings, 0);
|
|
157
163
|
const tooManyWarnings = options.maxWarnings !== void 0 && warnings > Number(options.maxWarnings);
|
|
158
|
-
return
|
|
164
|
+
return errorCount > 0 || tooManyWarnings ? 1 : 0;
|
|
159
165
|
}
|
|
160
166
|
async function check(options) {
|
|
161
167
|
const workspace = await openWorkspace(options);
|
|
@@ -191,7 +197,7 @@ function isPlanItem(value) {
|
|
|
191
197
|
}
|
|
192
198
|
async function readPlan(file) {
|
|
193
199
|
const data = JSON.parse(await readFile(file, "utf8"));
|
|
194
|
-
if (!isJsonObject(data) || !Array.isArray(data.items) || !data.items.every(isPlanItem)) throw
|
|
200
|
+
if (!isJsonObject(data) || !Array.isArray(data.items) || !data.items.every(isPlanItem)) throw errors.X18N4008({ reason: `${file} is not an extraction plan` });
|
|
195
201
|
return data.items;
|
|
196
202
|
}
|
|
197
203
|
/** One question per string in a TTY: extract with the proposed key, a typed key, or skip. */
|
|
@@ -467,7 +473,7 @@ async function doctor(options) {
|
|
|
467
473
|
out("scanners:");
|
|
468
474
|
for (const s of e.scanners) out(` ${s.id} (${s.extensions.join(", ") || "none"}): ${s.files} files`);
|
|
469
475
|
if (e.conflicts.length) out(`conflicts:\n${e.conflicts.map((c) => ` .${c.extension}: ${c.winner} wins over ${c.loser}`).join("\n")}`);
|
|
470
|
-
if (e.errors.length) out(`errors:\n${e.errors.map((err) => ` ${err.
|
|
476
|
+
if (e.errors.length) out(`errors:\n${e.errors.map((err) => ` ${err.name} ${err.why}`).join("\n")}`);
|
|
471
477
|
out("config origins:");
|
|
472
478
|
for (const [key, origin] of Object.entries(e.config.origins)) out(` ${key}: ${origin}`);
|
|
473
479
|
}
|
|
@@ -475,11 +481,11 @@ async function doctor(options) {
|
|
|
475
481
|
//#endregion
|
|
476
482
|
//#region src/commands/migrate.ts
|
|
477
483
|
async function migrate(source, options) {
|
|
478
|
-
if (source !== "i18n-ally") throw
|
|
484
|
+
if (source !== "i18n-ally") throw errors.X18N0001({ issue: `unknown migration source "${source}" (i18n-ally)` });
|
|
479
485
|
const root = resolve(options.root ?? cwd());
|
|
480
486
|
const result = await migrateI18nAlly(root, { format: options.format });
|
|
481
487
|
const existing = findConfigFile(root);
|
|
482
|
-
if (existing && !options.force && !options.dryRun && result.files.length > 0) throw
|
|
488
|
+
if (existing && !options.force && !options.dryRun && result.files.length > 0) throw errors.X18N0001({ issue: `${existing} already exists (pass --force to replace it)` });
|
|
483
489
|
if (!options.dryRun) for (const file of result.files) {
|
|
484
490
|
const path = join(root, file.path);
|
|
485
491
|
await mkdir(dirname(path), { recursive: true });
|
|
@@ -500,9 +506,9 @@ async function migrate(source, options) {
|
|
|
500
506
|
//#endregion
|
|
501
507
|
//#region src/commands/prompt.ts
|
|
502
508
|
async function prompt(kind, options) {
|
|
503
|
-
if (kind !== "translate" && kind !== "review") throw
|
|
509
|
+
if (kind !== "translate" && kind !== "review") throw errors.X18N0001({ issue: `unknown prompt "${kind}" (translate, review)` });
|
|
504
510
|
const locale = kind === "translate" ? options.to : options.locale ?? options.to;
|
|
505
|
-
if (!locale) throw
|
|
511
|
+
if (!locale) throw errors.X18N0001({ issue: kind === "translate" ? "pass --to <locale>" : "pass --locale <locale>" });
|
|
506
512
|
const { project, host } = singleProject(await openWorkspace(options));
|
|
507
513
|
if (options.stale) {
|
|
508
514
|
await project.loadHistory();
|
|
@@ -523,7 +529,7 @@ async function prompt(kind, options) {
|
|
|
523
529
|
//#region src/commands/setup.ts
|
|
524
530
|
async function schema(name, options) {
|
|
525
531
|
const which = name ?? "config";
|
|
526
|
-
if (which !== "config" && which !== "plugin") throw
|
|
532
|
+
if (which !== "config" && which !== "plugin") throw errors.X18N0001({ issue: `unknown schema "${which}" (config, plugin)` });
|
|
527
533
|
const require = createRequire(import.meta.url);
|
|
528
534
|
const text = await readFile(require.resolve(`@x18n/core/schema/${which}.json`), "utf8");
|
|
529
535
|
if (options.json) printJson("schema", {
|
|
@@ -550,7 +556,7 @@ function patternOf(resource, fromPath) {
|
|
|
550
556
|
async function init(options) {
|
|
551
557
|
const root = resolve(options.root ?? cwd());
|
|
552
558
|
const existing = findConfigFile(root);
|
|
553
|
-
if (existing && !options.force) throw
|
|
559
|
+
if (existing && !options.force) throw errors.X18N0001({ issue: `${existing} already exists (pass --force to replace it)` });
|
|
554
560
|
const { project } = (await loadWorkspace({
|
|
555
561
|
cwd: root,
|
|
556
562
|
cache: false
|
|
@@ -578,7 +584,7 @@ async function init(options) {
|
|
|
578
584
|
const body = JSON.stringify(config, null, 2).replace(/"(\w+)":/g, "$1:").replace(/"/g, "'");
|
|
579
585
|
content = `import { defineConfig } from '@x18n/core'\n\n// Detected presets: ${detected.join(", ") || "none"}. They apply automatically.\nexport default defineConfig(${body})\n`;
|
|
580
586
|
}
|
|
581
|
-
if (!configFileNames.some((name) => file.endsWith(name))) throw
|
|
587
|
+
if (!configFileNames.some((name) => file.endsWith(name))) throw errors.X18N0001({ issue: `unknown format "${format}" (ts, yml, json)` });
|
|
582
588
|
await writeFile(file, content);
|
|
583
589
|
if (options.json) printJson("init", {
|
|
584
590
|
file,
|
|
@@ -622,9 +628,15 @@ function ejectPlugin(data) {
|
|
|
622
628
|
}
|
|
623
629
|
async function eject(file, options) {
|
|
624
630
|
const path = resolve(cwd(), file);
|
|
625
|
-
if (!existsSync(path)) throw
|
|
631
|
+
if (!existsSync(path)) throw errors.X18N1001({
|
|
632
|
+
specifier: path,
|
|
633
|
+
reason: "the file does not exist"
|
|
634
|
+
});
|
|
626
635
|
const result = validate(PluginFileSchema, parse(await readFile(path, "utf8")));
|
|
627
|
-
if (!result.success) throw
|
|
636
|
+
if (!result.success) throw errors.X18N1004({
|
|
637
|
+
file: path,
|
|
638
|
+
issues: result.issues.map((i) => `${i.path}: ${i.message}`).join("; ")
|
|
639
|
+
});
|
|
628
640
|
const code = ejectPlugin(result.output);
|
|
629
641
|
if (options.json) printJson("plugin eject", {
|
|
630
642
|
file: path,
|
|
@@ -634,8 +646,17 @@ async function eject(file, options) {
|
|
|
634
646
|
}
|
|
635
647
|
//#endregion
|
|
636
648
|
//#region src/cli.ts
|
|
637
|
-
const uiCommands = () => import("./ui-
|
|
638
|
-
|
|
649
|
+
const uiCommands = () => import("./ui-tfQlhxh6.mjs");
|
|
650
|
+
const paint = (format) => (text) => styleText(format, text, { validateStream: false });
|
|
651
|
+
const formatColored = ansiFormatter({
|
|
652
|
+
red: paint("red"),
|
|
653
|
+
yellow: paint("yellow"),
|
|
654
|
+
cyan: paint("cyan"),
|
|
655
|
+
gray: paint("gray"),
|
|
656
|
+
bold: paint("bold"),
|
|
657
|
+
dim: paint("dim")
|
|
658
|
+
});
|
|
659
|
+
/** Runs a command and turns errors into the JSON envelope or a printed diagnostic, with the matching exit code. */
|
|
639
660
|
function run(command, action) {
|
|
640
661
|
return async (...args) => {
|
|
641
662
|
const options = args.at(-1);
|
|
@@ -643,13 +664,9 @@ function run(command, action) {
|
|
|
643
664
|
const code = await action(...args);
|
|
644
665
|
process.exitCode = code ?? 0;
|
|
645
666
|
} catch (error) {
|
|
646
|
-
|
|
647
|
-
if (
|
|
648
|
-
else {
|
|
649
|
-
process.stderr.write(`error ${info.code}: ${info.message}\n`);
|
|
650
|
-
if (info.hint) process.stderr.write(` ${info.hint}\n`);
|
|
651
|
-
if (!(error instanceof Error && "code" in error) && error instanceof Error && process.env.DEBUG) process.stderr.write(`${error.stack}\n`);
|
|
652
|
-
}
|
|
667
|
+
if (options.json) printJson(command, null, [], [toErrorInfo(error)]);
|
|
668
|
+
else if (isX18nError(error)) process.stderr.write(`${(useColor(options) ? formatColored : formatDiagnostic)(error)}\n`);
|
|
669
|
+
else process.stderr.write(`${error instanceof Error && process.env.DEBUG ? error.stack : `error: ${toErrorInfo(error).why}`}\n`);
|
|
653
670
|
process.exitCode = exitCodeOf(error);
|
|
654
671
|
}
|
|
655
672
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { applyTextEdits, envelope, errors, isX18nError, toWireDiagnostic } from "@x18n/core";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { Buffer } from "node:buffer";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
@@ -24,7 +24,10 @@ async function openWorkspace(options) {
|
|
|
24
24
|
}
|
|
25
25
|
/** Write commands work on one project; a monorepo needs `--project`. */
|
|
26
26
|
function singleProject(workspace) {
|
|
27
|
-
if (workspace.projects.length !== 1) throw
|
|
27
|
+
if (workspace.projects.length !== 1) throw errors.X18N0003({
|
|
28
|
+
name: void 0,
|
|
29
|
+
available: workspace.projects.map((p) => p.name)
|
|
30
|
+
});
|
|
28
31
|
return workspace.projects[0];
|
|
29
32
|
}
|
|
30
33
|
function displayPath(root, path) {
|
|
@@ -40,21 +43,15 @@ function printJson(command, data, diagnostics = [], errors = []) {
|
|
|
40
43
|
function wireDiagnostics(loaded, diagnostics) {
|
|
41
44
|
return diagnostics.map((d) => toWireDiagnostic(loaded.name, d, (path) => loaded.project.text(path)));
|
|
42
45
|
}
|
|
43
|
-
/** Exit code for an error: 2 usage or config, 3 decision needed, 4 stale plan, 1 otherwise. */
|
|
44
|
-
function
|
|
45
|
-
if (
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
return /^X18N[01]/.test(error.code) ? 2 : 1;
|
|
46
|
+
/** Exit code for an error code: 2 usage or config, 3 decision needed, 4 stale plan, 1 otherwise. */
|
|
47
|
+
function exitCodeFor(code) {
|
|
48
|
+
if (code === "X18N4003") return 3;
|
|
49
|
+
if (code === "X18N4004") return 4;
|
|
50
|
+
return /^X18N[01]/.test(code) ? 2 : 1;
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
code: "X18N0002",
|
|
54
|
-
message: error instanceof Error ? error.message : String(error),
|
|
55
|
-
hint: "",
|
|
56
|
-
docs: ""
|
|
57
|
-
};
|
|
52
|
+
/** Errors without an x18n code exit with 2. */
|
|
53
|
+
function exitCodeOf(error) {
|
|
54
|
+
return isX18nError(error) ? exitCodeFor(error.code) : 2;
|
|
58
55
|
}
|
|
59
56
|
/** Stands for a lone `-` argument (stdin). */
|
|
60
57
|
const stdinArg = "\0stdin";
|
|
@@ -72,7 +69,7 @@ async function answer(plan, options) {
|
|
|
72
69
|
for (const decision of plan.decisions) answers[decision.id] = decision.default;
|
|
73
70
|
return answers;
|
|
74
71
|
}
|
|
75
|
-
if (options.json || !process.stdin.isTTY) throw
|
|
72
|
+
if (options.json || !process.stdin.isTTY) throw errors.X18N4003({ decisions: plan.decisions.map((d) => `${d.message} (${d.options.map((o) => o.id).join(" | ")})`) });
|
|
76
73
|
const rl = createInterface({
|
|
77
74
|
input: process.stdin,
|
|
78
75
|
output: process.stderr
|
|
@@ -145,4 +142,4 @@ async function runPlan(command, loaded, plan, options) {
|
|
|
145
142
|
else out(preview);
|
|
146
143
|
}
|
|
147
144
|
//#endregion
|
|
148
|
-
export {
|
|
145
|
+
export { out as a, runPlan as c, useColor as d, wireDiagnostics as f, openWorkspace as i, singleProject as l, displayPath as n, printJson as o, exitCodeOf as r, readInput as s, cwd as t, stdinArg as u };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as out, i as openWorkspace, o as printJson, t as cwd } from "./context-j5IMF1tl.mjs";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { createX18nDevframe } from "@x18n/ui";
|
|
@@ -13,8 +13,8 @@ import { launchEditor } from "devframe/utils/launch-editor";
|
|
|
13
13
|
async function ui(options) {
|
|
14
14
|
const workspace = await openWorkspace(options);
|
|
15
15
|
workspace.projects.forEach((p) => p.project.watch());
|
|
16
|
-
const editor = process.env.LAUNCH_EDITOR || process.env.VISUAL || process.env.EDITOR;
|
|
17
|
-
const definition = createX18nDevframe(workspace.projects, { openInEditor:
|
|
16
|
+
const editor = process.env.LAUNCH_EDITOR || process.env.VISUAL || process.env.EDITOR || void 0;
|
|
17
|
+
const definition = createX18nDevframe(workspace.projects, { openInEditor: async ({ project, path, line, column }) => launchEditor(`${join(project.root, path)}:${line}:${column}`, editor) });
|
|
18
18
|
const mcpToken = randomToken();
|
|
19
19
|
const auth = options.auth !== false;
|
|
20
20
|
if (!auth) process.stderr.write("warning: --no-auth lets anything that reaches the port edit your files. Use it only in CI and e2e tests.\n");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x18n/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.8",
|
|
5
5
|
"description": "Manage and view i18n files of any project, for humans and agents",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -38,12 +38,13 @@
|
|
|
38
38
|
"node": ">=22"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@x18n/builtins": "0.0.
|
|
42
|
-
"@x18n/core": "0.0.
|
|
43
|
-
"@x18n/node": "0.0.
|
|
44
|
-
"@x18n/ui": "0.0.
|
|
41
|
+
"@x18n/builtins": "0.0.8",
|
|
42
|
+
"@x18n/core": "0.0.8",
|
|
43
|
+
"@x18n/node": "0.0.8",
|
|
44
|
+
"@x18n/ui": "0.0.8",
|
|
45
45
|
"cac": "^7.1.0",
|
|
46
46
|
"devframe": "^1.2.6",
|
|
47
|
+
"nostics": "^1.3.0",
|
|
47
48
|
"yaml": "^2.9.1"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
package/skills/x18n/SKILL.md
CHANGED
|
@@ -25,7 +25,7 @@ Batch the questions into one message. Routine work needs no question: adding a k
|
|
|
25
25
|
- Change locale files through `x18n set`, `x18n apply`, `x18n rename` and `x18n delete`. The config file (`x18n.config.ts`) is the one i18n file you edit by hand.
|
|
26
26
|
- Check a key before you use it in code: `x18n get <key> --json`. Reuse an existing key with the same meaning.
|
|
27
27
|
- Keep every placeholder of the source message (`{name}`, `{{count}}`, `%{name}`, `:name`, `%s`, `@:key`).
|
|
28
|
-
- Pass `--json` and read the envelope `{ "x18n":
|
|
28
|
+
- Pass `--json` and read the envelope `{ "x18n": 2, "command", "data", "diagnostics", "errors" }`.
|
|
29
29
|
- Run renames and deletes with `--dry-run` first, and read the plan.
|
|
30
30
|
- Write every key as a full string literal. Do not build keys at runtime (see below).
|
|
31
31
|
|