@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 openWorkspace, c as readInput, d as stdinArg, f as useColor, i as exitCodeOf, l as runPlan, n as displayPath, o as out, p as wireDiagnostics, r as errorInfo, s as printJson, t as cwd, u as singleProject } from "./context-Za_ea1Nk.mjs";
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, X18nError, isJsonObject, parseMessageBatch, placeholderProblems, renderPrompt, toWireRange, validate } from "@x18n/core";
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.6";
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 new X18nError("X18N4008", "pass --value <text> (or --value - to read stdin)");
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 new X18nError("X18N4008", `${source} is not JSON: ${error instanceof Error ? error.message : String(error)}`);
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
- }, [], [new X18nError("X18N4007", `${rejected.length} values`).toJSON()]);
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 new X18nError("X18N0001", `unknown reporter "${id}" (${[...available.keys()].join(", ")})`);
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 errors = inputs.reduce((sum, input) => sum + input.summary.errors, 0);
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 errors > 0 || tooManyWarnings ? 1 : 0;
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 new X18nError("X18N4008", `${file} is not an extraction plan`);
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.code} ${err.message}`).join("\n")}`);
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 new X18nError("X18N0001", `unknown migration source "${source}" (i18n-ally)`);
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 new X18nError("X18N0001", `${existing} already exists (pass --force to replace it)`);
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 new X18nError("X18N0001", `unknown prompt "${kind}" (translate, review)`);
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 new X18nError("X18N0001", kind === "translate" ? "pass --to <locale>" : "pass --locale <locale>");
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 new X18nError("X18N0001", `unknown schema "${which}" (config, plugin)`);
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 new X18nError("X18N0001", `${existing} already exists (pass --force to replace it)`);
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 new X18nError("X18N0001", `unknown format "${format}" (ts, yml, json)`);
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 new X18nError("X18N1001", path);
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 new X18nError("X18N1004", result.issues.map((i) => `${i.path}: ${i.message}`).join("; "));
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-Bp3xyqr_.mjs");
638
- /** Runs a command and turns errors into the JSON envelope or a printed hint, with the matching exit code. */
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
- const info = errorInfo(error);
647
- if (options.json) printJson(command, null, [], [info]);
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 { X18nError, applyTextEdits, envelope, isX18nError, toWireDiagnostic } from "@x18n/core";
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 new X18nError("X18N0003", `pick one with --project (${workspace.projects.map((p) => p.name).join(", ")})`);
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 exitCodeOf(error) {
45
- if (!isX18nError(error)) return 2;
46
- if (error.code === "X18N4003") return 3;
47
- if (error.code === "X18N4004") return 4;
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
- function errorInfo(error) {
51
- if (isX18nError(error)) return error.toJSON();
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 new X18nError("X18N4003", plan.decisions.map((d) => `${d.message} (${d.options.map((o) => o.id).join(" | ")})`).join(" "), plan.decisions.map((d) => d.id));
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 { openWorkspace as a, readInput as c, stdinArg as d, useColor as f, exitCodeOf as i, runPlan as l, displayPath as n, out as o, wireDiagnostics as p, errorInfo as r, printJson as s, cwd as t, singleProject as u };
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 openWorkspace, o as out, s as printJson, t as cwd } from "./context-Za_ea1Nk.mjs";
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: editor ? async ({ project, path, line, column }) => launchEditor(`${join(project.root, path)}:${line}:${column}`, editor) : void 0 });
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.6",
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.6",
42
- "@x18n/core": "0.0.6",
43
- "@x18n/node": "0.0.6",
44
- "@x18n/ui": "0.0.6",
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": {
@@ -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": 1, "command", "data", "diagnostics", "errors" }`.
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