@verbaly/compiler 0.25.0 → 0.26.0

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.js CHANGED
@@ -8,6 +8,7 @@ import { glob } from "tinyglobby";
8
8
  import { parse as parse$1 } from "@babel/parser";
9
9
  import { createHash } from "node:crypto";
10
10
  import MagicString from "magic-string";
11
+ import "picomatch";
11
12
  //#region src/catalog.ts
12
13
  function catalogPath(cfg, locale) {
13
14
  return join(cfg.dir, `${locale}.json`);
@@ -98,6 +99,57 @@ function formatCheckResult(result) {
98
99
  function truncate(text, max) {
99
100
  return text.length > max ? text.slice(0, max - 1) + "…" : text;
100
101
  }
102
+ function githubCheckAnnotations(result, registry, root) {
103
+ const messages = registry.messages();
104
+ const contents = /* @__PURE__ */ new Map();
105
+ const readSource = (file) => {
106
+ if (!contents.has(file)) try {
107
+ contents.set(file, readFileSync(file, "utf8"));
108
+ } catch {
109
+ contents.set(file, void 0);
110
+ }
111
+ return contents.get(file);
112
+ };
113
+ const lines = [];
114
+ const byKey = /* @__PURE__ */ new Map();
115
+ for (const entry of result.missing) {
116
+ const grouped = byKey.get(entry.key);
117
+ if (grouped) grouped.locales.push(entry.locale);
118
+ else byKey.set(entry.key, {
119
+ ...entry,
120
+ locales: [entry.locale]
121
+ });
122
+ }
123
+ for (const entry of byKey.values()) {
124
+ const origin = messages.get(entry.key);
125
+ const hint = entry.source ? `: "${truncate(entry.source, 60)}"` : "";
126
+ const text = escapeData(`missing [${entry.locales.join(", ")}] ${entry.key}${hint}`);
127
+ if (origin) {
128
+ const file = relative(root, origin.file).replaceAll("\\", "/");
129
+ const content = readSource(origin.file);
130
+ const line = content === void 0 ? void 0 : lineAt(content, origin.start);
131
+ lines.push(`::error file=${escapeProperty(file)}${line ? `,line=${line}` : ""}::${text}`);
132
+ } else lines.push(`::error::${text}`);
133
+ }
134
+ for (const entry of result.unknown) {
135
+ const text = escapeData(`unknown key "${entry.key}" (not in any catalog)`);
136
+ const file = entry.files[0];
137
+ lines.push(file ? `::error file=${escapeProperty(relative(root, file).replaceAll("\\", "/"))}::${text}` : `::error::${text}`);
138
+ }
139
+ return lines;
140
+ }
141
+ function lineAt(content, offset) {
142
+ let line = 1;
143
+ const end = Math.min(offset, content.length);
144
+ for (let i = 0; i < end; i++) if (content[i] === "\n") line += 1;
145
+ return line;
146
+ }
147
+ function escapeData(text) {
148
+ return text.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
149
+ }
150
+ function escapeProperty(text) {
151
+ return escapeData(text).replaceAll(":", "%3A").replaceAll(",", "%2C");
152
+ }
101
153
  //#endregion
102
154
  //#region src/params.ts
103
155
  const PLURAL_KEYS = /* @__PURE__ */ new Set([
@@ -199,12 +251,13 @@ declare module 'virtual:verbaly/locale/*' {
199
251
  }
200
252
  `;
201
253
  }
202
- function writeDts(cfg, catalog) {
203
- const file = join(cfg.root, "verbaly.d.ts");
254
+ function writeDts(cfg, catalog, file) {
255
+ file ??= join(cfg.root, "verbaly.d.ts");
204
256
  const content = generateDts(catalog);
205
257
  try {
206
258
  if (readFileSync(file, "utf8") === content) return;
207
259
  } catch {}
260
+ mkdirSync(dirname(file), { recursive: true });
208
261
  writeFileSync(file, content);
209
262
  }
210
263
  //#endregion
@@ -341,8 +394,9 @@ function resolveConfig(config = {}) {
341
394
  dir,
342
395
  sourceLocale,
343
396
  locales: [...locales],
344
- include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue}"],
397
+ include: config.include ?? ["{src,app}/**/*.{js,jsx,ts,tsx,mjs,mts,svelte,vue,astro}"],
345
398
  exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
399
+ dts: typeof config.dts === "string" ? resolve(root, config.dts) : config.dts,
346
400
  translate: config.translate ?? {},
347
401
  render: config.render ?? {}
348
402
  };
@@ -705,13 +759,14 @@ function isNode(value) {
705
759
  }
706
760
  //#endregion
707
761
  //#region src/sfc.ts
708
- const SFC_FILE_RE = /\.(?:svelte|vue)$/;
762
+ const SFC_FILE_RE = /\.(?:svelte|vue|astro)$/;
709
763
  function analyzeFile(code, file) {
710
764
  return SFC_FILE_RE.test(file) ? analyzeSfc(code, file) : analyze(code, file);
711
765
  }
712
766
  const SCRIPT_RE = /(<script\b[^>]*>)([\s\S]*?)(<\/script\s*>)/gi;
713
767
  const STYLE_RE = /<style\b[^>]*>[\s\S]*?<\/style\s*>/gi;
714
768
  const COMMENT_RE = /<!--[\s\S]*?-->/g;
769
+ const FRONTMATTER_RE = /^(\uFEFF?\s*---\r?\n)([\s\S]*?)\r?\n---(?=\r?\n|$)/;
715
770
  const CANDIDATE_SVELTE = /(?<![\w$])\$?t(?=[`(]|\.id\()/g;
716
771
  const CANDIDATE_VUE = /(?<![\w$])t(?=[`(]|\.id\()/g;
717
772
  function analyzeSfc(code, file) {
@@ -719,12 +774,14 @@ function analyzeSfc(code, file) {
719
774
  const options = svelte ? { tNames: ["t", "$t"] } : void 0;
720
775
  const tagged = [];
721
776
  const usedKeys = [];
777
+ const frontmatter = file.endsWith(".astro") ? FRONTMATTER_RE.exec(code) : null;
778
+ if (frontmatter?.[2]) merge(analyzeSegment(frontmatter[2], file, options), frontmatter[1].length, false);
722
779
  for (const match of code.matchAll(SCRIPT_RE)) {
723
780
  const content = match[2];
724
781
  if (!content) continue;
725
782
  merge(analyzeSegment(content, file, options), match.index + match[1].length, false);
726
783
  }
727
- const markup = blank(blank(blank(code, SCRIPT_RE), STYLE_RE), COMMENT_RE);
784
+ const markup = blank(blank(blank(frontmatter ? " ".repeat(frontmatter[0].length) + code.slice(frontmatter[0].length) : code, SCRIPT_RE), STYLE_RE), COMMENT_RE);
728
785
  const candidates = svelte ? CANDIDATE_SVELTE : CANDIDATE_VUE;
729
786
  let consumedTo = 0;
730
787
  for (const match of markup.matchAll(candidates)) {
@@ -854,6 +911,7 @@ function pruneCatalogs(cfg, catalogs, registry) {
854
911
  //#endregion
855
912
  //#region src/init.ts
856
913
  const BUNDLERS = [
914
+ "astro",
857
915
  "vite",
858
916
  "webpack",
859
917
  "rollup",
@@ -907,7 +965,8 @@ function init(options = {}) {
907
965
  }
908
966
  const bundler = detectBundler(root);
909
967
  const next = [];
910
- if (bundler === "vite") next.push("install the plugin: pnpm add -D @verbaly/vite", "add verbaly() to the plugins in vite.config");
968
+ if (bundler === "astro") next.push("install the integration: pnpm add -D @verbaly/astro", "add verbaly() to the integrations in astro.config");
969
+ else if (bundler === "vite") next.push("install the plugin: pnpm add -D @verbaly/vite", "add verbaly() to the plugins in vite.config");
911
970
  else if (bundler) next.push("install the plugin: pnpm add -D @verbaly/unplugin", `add the verbaly ${bundler} plugin to your build config`);
912
971
  else next.push("run \"verbaly extract\" after writing your first t`…` message");
913
972
  return {
@@ -980,8 +1039,8 @@ async function doctor(cfg) {
980
1039
  else if (wired) ok("plugin", `${deps["@verbaly/vite"] ? "@verbaly/vite" : "@verbaly/unplugin"} installed for ${bundler}`);
981
1040
  else if (bundler === "vite") warn("plugin", "vite detected but @verbaly/vite is not installed", "pnpm add -D @verbaly/vite and add verbaly() to the plugins in vite.config");
982
1041
  else warn("plugin", `${bundler} detected but @verbaly/unplugin is not installed`, `pnpm add -D @verbaly/unplugin and add the verbaly ${bundler} plugin to your build config`);
983
- if (source) {
984
- const dtsPath = join(cfg.root, "verbaly.d.ts");
1042
+ if (source && cfg.dts !== false) {
1043
+ const dtsPath = cfg.dts ?? join(cfg.root, "verbaly.d.ts");
985
1044
  if (!existsSync(dtsPath)) warn("types", "verbaly.d.ts has not been generated", "run `npx verbaly extract`");
986
1045
  else if (readFileSync(dtsPath, "utf8") !== generateDts(source)) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
987
1046
  else ok("types", "verbaly.d.ts is up to date");
@@ -1644,7 +1703,7 @@ function formatStatusResult(result) {
1644
1703
  }
1645
1704
  return lines.join("\n");
1646
1705
  }
1647
- const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|svelte|vue)$/;
1706
+ const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|svelte|vue|astro)$/;
1648
1707
  //#endregion
1649
1708
  //#region src/watch.ts
1650
1709
  function watchProject(cfg, run, options = {}) {
@@ -1687,12 +1746,225 @@ function watchProject(cfg, run, options = {}) {
1687
1746
  };
1688
1747
  }
1689
1748
  //#endregion
1749
+ //#region src/wrap.ts
1750
+ const WRAP_ATTRS = /* @__PURE__ */ new Set([
1751
+ "title",
1752
+ "alt",
1753
+ "placeholder",
1754
+ "aria-label"
1755
+ ]);
1756
+ const T_NAMES = /* @__PURE__ */ new Set(["t"]);
1757
+ const LETTER = /\p{L}/u;
1758
+ async function wrapProject(cfg, options = {}) {
1759
+ const files = (await glob(cfg.include, {
1760
+ cwd: cfg.root,
1761
+ ignore: cfg.exclude,
1762
+ absolute: true
1763
+ })).filter((file) => /\.[jt]sx$/.test(file));
1764
+ const result = {
1765
+ files: files.length,
1766
+ changed: [],
1767
+ wrapped: [],
1768
+ skipped: []
1769
+ };
1770
+ for (const file of files) {
1771
+ const code = readFileSync(file, "utf8");
1772
+ const label = relative(cfg.root, file).replaceAll("\\", "/");
1773
+ const out = wrapCode(code, label);
1774
+ result.wrapped.push(...out.wrapped);
1775
+ result.skipped.push(...out.skipped);
1776
+ if (out.code !== void 0) {
1777
+ result.changed.push(label);
1778
+ if (options.write) writeFileSync(file, out.code);
1779
+ }
1780
+ }
1781
+ return result;
1782
+ }
1783
+ function wrapCode(code, file) {
1784
+ const wrapped = [];
1785
+ const skipped = [];
1786
+ let ast;
1787
+ try {
1788
+ ast = parse$1(code, {
1789
+ sourceType: "module",
1790
+ errorRecovery: true,
1791
+ plugins: ["typescript", "jsx"]
1792
+ }).program;
1793
+ } catch {
1794
+ return {
1795
+ wrapped,
1796
+ skipped
1797
+ };
1798
+ }
1799
+ const s = new MagicString(code);
1800
+ const visited = /* @__PURE__ */ new Set();
1801
+ let changed = false;
1802
+ walk(ast, (node) => {
1803
+ if (node.type !== "JSXElement" && node.type !== "JSXFragment") return;
1804
+ if (visited.has(node)) return;
1805
+ processElement(node);
1806
+ });
1807
+ return changed ? {
1808
+ code: s.toString(),
1809
+ wrapped,
1810
+ skipped
1811
+ } : {
1812
+ wrapped,
1813
+ skipped
1814
+ };
1815
+ function processElement(node) {
1816
+ visited.add(node);
1817
+ if (node.type === "JSXElement") {
1818
+ const opening = node.openingElement;
1819
+ const name = opening.name;
1820
+ if (name.type === "JSXIdentifier" && name.name === "Trans") return markSubtree(node);
1821
+ if (hasVerbalyAttr(opening)) return markSubtree(node);
1822
+ wrapAttributes(opening);
1823
+ }
1824
+ const children = node.children ?? [];
1825
+ const hasElementChild = children.some((c) => c.type === "JSXElement" || c.type === "JSXFragment");
1826
+ const hasText = children.some((c) => c.type === "JSXText" && LETTER.test(c.value) || c.type === "JSXExpressionContainer" && c.expression.type === "StringLiteral" && LETTER.test(c.expression.value));
1827
+ if (hasElementChild && hasText) {
1828
+ skip(node, snippet(children), "mixed text and markup: wrap by hand or use <Trans>");
1829
+ return markSubtree(node);
1830
+ }
1831
+ if (hasText) {
1832
+ wrapSegment(node, children);
1833
+ return markSubtree(node);
1834
+ }
1835
+ for (const child of children) if (child.type === "JSXElement" || child.type === "JSXFragment") processElement(child);
1836
+ }
1837
+ function wrapSegment(parent, children) {
1838
+ const meaningful = children.filter((c) => c.type === "JSXText" && c.value.trim() !== "" || c.type === "JSXExpressionContainer" && c.expression.type !== "JSXEmptyExpression");
1839
+ if (meaningful.length === 0) return;
1840
+ const first = meaningful[0];
1841
+ const last = meaningful[meaningful.length - 1];
1842
+ let template = "";
1843
+ let report = "";
1844
+ for (let i = children.indexOf(first); i <= children.indexOf(last); i++) {
1845
+ const child = children[i];
1846
+ if (child.type === "JSXText") {
1847
+ let value = cleanJsxText(child.value);
1848
+ if (child === first) value = value.trimStart();
1849
+ if (child === last) value = value.trimEnd();
1850
+ template += escapeTemplate(value);
1851
+ report += value;
1852
+ } else if (child.type === "JSXExpressionContainer") {
1853
+ const expr = child.expression;
1854
+ if (expr.type === "JSXEmptyExpression") continue;
1855
+ if (usesT(expr)) {
1856
+ skip(parent, snippet(children), "already uses t: finish it by hand");
1857
+ return;
1858
+ }
1859
+ if (containsJsx(expr)) {
1860
+ skip(parent, snippet(children), "an expression renders markup: wrap by hand or use <Trans>");
1861
+ markSubtree(child);
1862
+ return;
1863
+ }
1864
+ if (expr.type === "StringLiteral") {
1865
+ template += escapeTemplate(expr.value);
1866
+ report += expr.value;
1867
+ } else {
1868
+ const source = code.slice(expr.start, expr.end);
1869
+ template += "${" + source + "}";
1870
+ report += "${" + source + "}";
1871
+ }
1872
+ } else return;
1873
+ }
1874
+ if (!LETTER.test(report)) return;
1875
+ const start = first.type === "JSXText" ? first.start + leadingWs(first.value) : first.start;
1876
+ const end = last.type === "JSXText" ? last.end - trailingWs(last.value) : last.end;
1877
+ s.overwrite(start, end, "{t`" + template + "`}");
1878
+ changed = true;
1879
+ wrapped.push({
1880
+ file,
1881
+ line: line(first),
1882
+ text: report,
1883
+ kind: "text"
1884
+ });
1885
+ }
1886
+ function wrapAttributes(opening) {
1887
+ for (const attr of opening.attributes) {
1888
+ if (attr.type !== "JSXAttribute") continue;
1889
+ const name = attr.name;
1890
+ if (name.type !== "JSXIdentifier" || !WRAP_ATTRS.has(name.name)) continue;
1891
+ const value = attr.value;
1892
+ if (value?.type !== "StringLiteral") continue;
1893
+ const raw = value.value;
1894
+ if (!LETTER.test(raw)) continue;
1895
+ s.overwrite(value.start, value.end, "{t`" + escapeTemplate(raw) + "`}");
1896
+ changed = true;
1897
+ wrapped.push({
1898
+ file,
1899
+ line: line(value),
1900
+ text: raw,
1901
+ kind: "attribute",
1902
+ attribute: name.name
1903
+ });
1904
+ }
1905
+ }
1906
+ function skip(node, text, reason) {
1907
+ skipped.push({
1908
+ file,
1909
+ line: line(node),
1910
+ text,
1911
+ reason
1912
+ });
1913
+ }
1914
+ function snippet(children) {
1915
+ const start = children[0]?.start ?? 0;
1916
+ const end = children[children.length - 1]?.end ?? start;
1917
+ const text = code.slice(start, end).replace(/\s+/g, " ").trim();
1918
+ return text.length > 60 ? text.slice(0, 59) + "…" : text;
1919
+ }
1920
+ function markSubtree(node) {
1921
+ walk(node, (n) => {
1922
+ if (n.type === "JSXElement" || n.type === "JSXFragment") visited.add(n);
1923
+ });
1924
+ }
1925
+ }
1926
+ function hasVerbalyAttr(opening) {
1927
+ return opening.attributes.some((attr) => {
1928
+ if (attr.type !== "JSXAttribute") return false;
1929
+ const name = attr.name;
1930
+ return name.type === "JSXIdentifier" && name.name.startsWith("data-verbaly");
1931
+ });
1932
+ }
1933
+ function usesT(node) {
1934
+ let found = false;
1935
+ walk(node, (n) => {
1936
+ if (n.type === "TaggedTemplateExpression" && isTReference(n.tag, T_NAMES)) found = true;
1937
+ if (n.type === "CallExpression" && isTReference(n.callee, T_NAMES)) found = true;
1938
+ });
1939
+ return found;
1940
+ }
1941
+ function containsJsx(node) {
1942
+ let found = false;
1943
+ walk(node, (n) => {
1944
+ if (n.type === "JSXElement" || n.type === "JSXFragment") found = true;
1945
+ });
1946
+ return found;
1947
+ }
1948
+ function escapeTemplate(text) {
1949
+ return text.replace(/[\\`]/g, "\\$&").replace(/\$\{/g, "\\${");
1950
+ }
1951
+ function leadingWs(text) {
1952
+ return text.length - text.trimStart().length;
1953
+ }
1954
+ function trailingWs(text) {
1955
+ return text.length - text.trimEnd().length;
1956
+ }
1957
+ function line(node) {
1958
+ return node.loc?.start?.line ?? 1;
1959
+ }
1960
+ //#endregion
1690
1961
  //#region src/run.ts
1691
1962
  const HELP = `verbaly · i18n compiler
1692
1963
 
1693
1964
  Usage:
1694
1965
  verbaly init scaffold config + locale catalogs (detects your bundler)
1695
1966
  verbaly doctor diagnose the setup (config, catalogs, plugin, types, keys)
1967
+ verbaly wrap find hardcoded JSX text and wrap it in t\`…\` (report; --write applies)
1696
1968
  verbaly extract scan sources, update catalogs and types
1697
1969
  verbaly status translation coverage per locale, at a glance
1698
1970
  verbaly check verify translations are complete (CI)
@@ -1709,6 +1981,9 @@ Options:
1709
1981
  --locales <csv> extra locales; for translate: target locales to fill
1710
1982
  --prune drop keys no longer referenced (extract)
1711
1983
  --watch keep extracting as source files change (extract)
1984
+ --write apply the rewrites instead of only reporting (wrap)
1985
+ --json machine-readable output (status)
1986
+ --reporter <name> failure format: text (default) or github annotations (check)
1712
1987
  --model <id> model override for the claude provider (translate)
1713
1988
  --dry-run list what would happen, write nothing (translate, import, extract)
1714
1989
  --format <f> export format: xliff (default), csv, android-xml or ios-strings (export)
@@ -1736,6 +2011,9 @@ async function runCli(args = process.argv.slice(2)) {
1736
2011
  locales: { type: "string" },
1737
2012
  prune: { type: "boolean" },
1738
2013
  watch: { type: "boolean" },
2014
+ write: { type: "boolean" },
2015
+ json: { type: "boolean" },
2016
+ reporter: { type: "string" },
1739
2017
  model: { type: "string" },
1740
2018
  locale: { type: "string" },
1741
2019
  site: { type: "string" },
@@ -1818,7 +2096,7 @@ async function runCli(args = process.argv.slice(2)) {
1818
2096
  const { added } = syncCatalogs(cfg, catalogs, registry);
1819
2097
  if (!dryRun) {
1820
2098
  for (const locale of cfg.locales) writeCatalog(cfg, locale, catalogs[locale] ?? {});
1821
- writeDts(cfg, catalogs[cfg.sourceLocale] ?? {});
2099
+ if (cfg.dts !== false) writeDts(cfg, catalogs[cfg.sourceLocale] ?? {}, cfg.dts);
1822
2100
  }
1823
2101
  const total = registry.messages().size;
1824
2102
  console.log(`[verbaly] ${total} messages · locales: ${cfg.locales.join(", ")}${dryRun ? " (dry run, nothing written)" : ""}`);
@@ -1831,19 +2109,49 @@ async function runCli(args = process.argv.slice(2)) {
1831
2109
  }
1832
2110
  return;
1833
2111
  }
2112
+ if (command === "wrap") {
2113
+ const result = await wrapProject(cfg, { write: values.write });
2114
+ if (result.wrapped.length === 0 && result.skipped.length === 0) {
2115
+ console.log(`[verbaly] nothing to wrap (${result.files} files scanned) ✓`);
2116
+ return;
2117
+ }
2118
+ const verb = values.write ? "wrapped" : "would wrap";
2119
+ const note = values.write ? "" : " (report only, use --write to apply)";
2120
+ console.log(`[verbaly] ${verb} ${result.wrapped.length} texts in ${result.changed.length} files${note}`);
2121
+ for (const entry of result.wrapped) {
2122
+ const attr = entry.kind === "attribute" ? `${entry.attribute} → ` : "";
2123
+ console.log(` ${entry.file}:${entry.line} ${attr}"${entry.text}"`);
2124
+ }
2125
+ if (result.skipped.length > 0) {
2126
+ console.log(" needs a human:");
2127
+ for (const entry of result.skipped) console.log(` ${entry.file}:${entry.line} "${entry.text}" (${entry.reason})`);
2128
+ }
2129
+ if (values.write && result.changed.length > 0) console.log(" next: make t available where TS complains (React: const t = useT()), then run verbaly extract");
2130
+ return;
2131
+ }
1834
2132
  if (command === "status") {
1835
2133
  const registry = await extractProject(cfg);
1836
- console.log(formatStatusResult(status(cfg, loadCatalogs(cfg), registry)));
2134
+ const result = status(cfg, loadCatalogs(cfg), registry);
2135
+ console.log(values.json ? JSON.stringify(result) : formatStatusResult(result));
1837
2136
  return;
1838
2137
  }
1839
2138
  if (command === "check") {
2139
+ const reporter = values.reporter ?? "text";
2140
+ if (reporter !== "text" && reporter !== "github") {
2141
+ console.error(`[verbaly] unknown reporter "${values.reporter}", use text or github`);
2142
+ process.exitCode = 1;
2143
+ return;
2144
+ }
1840
2145
  const registry = await extractProject(cfg);
1841
2146
  const result = check(cfg, loadCatalogs(cfg), registry);
1842
2147
  if (result.ok) {
1843
2148
  console.log("[verbaly] all translations complete ✓");
1844
2149
  return;
1845
2150
  }
1846
- console.error(`[verbaly] check failed\n${formatCheckResult(result)}`);
2151
+ if (reporter === "github") {
2152
+ for (const line of githubCheckAnnotations(result, registry, cfg.root)) console.error(line);
2153
+ console.error(`[verbaly] check failed: ${result.missing.length} missing, ${result.unknown.length} unknown`);
2154
+ } else console.error(`[verbaly] check failed\n${formatCheckResult(result)}`);
1847
2155
  process.exitCode = 1;
1848
2156
  return;
1849
2157
  }
@@ -1967,8 +2275,9 @@ const COMMAND_FLAGS = {
1967
2275
  "dry-run",
1968
2276
  "watch"
1969
2277
  ],
1970
- status: [],
1971
- check: [],
2278
+ wrap: ["write"],
2279
+ status: ["json"],
2280
+ check: ["reporter"],
1972
2281
  translate: ["model", "dry-run"],
1973
2282
  export: [
1974
2283
  "format",