@tolkbot/cli 0.3.0 → 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +1573 -1750
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -105,7 +105,7 @@ var require_path = __commonJS({
105
105
  Object.defineProperty(exports, "__esModule", { value: true });
106
106
  exports.convertPosixPathToPattern = exports.convertWindowsPathToPattern = exports.convertPathToPattern = exports.escapePosixPath = exports.escapeWindowsPath = exports.escape = exports.removeLeadingDotSegment = exports.makeAbsolute = exports.unixify = void 0;
107
107
  var os3 = __require("os");
108
- var path12 = __require("path");
108
+ var path11 = __require("path");
109
109
  var IS_WINDOWS_PLATFORM = os3.platform() === "win32";
110
110
  var LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2;
111
111
  var POSIX_UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\()|\\(?![!()*+?@[\]{|}]))/g;
@@ -117,7 +117,7 @@ var require_path = __commonJS({
117
117
  }
118
118
  exports.unixify = unixify;
119
119
  function makeAbsolute(cwd, filepath) {
120
- return path12.resolve(cwd, filepath);
120
+ return path11.resolve(cwd, filepath);
121
121
  }
122
122
  exports.makeAbsolute = makeAbsolute;
123
123
  function removeLeadingDotSegment(entry) {
@@ -1416,7 +1416,7 @@ var require_braces = __commonJS({
1416
1416
  var require_constants2 = __commonJS({
1417
1417
  "../../node_modules/.pnpm/picomatch@2.3.2/node_modules/picomatch/lib/constants.js"(exports, module) {
1418
1418
  "use strict";
1419
- var path12 = __require("path");
1419
+ var path11 = __require("path");
1420
1420
  var WIN_SLASH = "\\\\/";
1421
1421
  var WIN_NO_SLASH = `[^${WIN_SLASH}]`;
1422
1422
  var DEFAULT_MAX_EXTGLOB_RECURSION = 0;
@@ -1590,7 +1590,7 @@ var require_constants2 = __commonJS({
1590
1590
  /* | */
1591
1591
  CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279,
1592
1592
  /* \uFEFF */
1593
- SEP: path12.sep,
1593
+ SEP: path11.sep,
1594
1594
  /**
1595
1595
  * Create EXTGLOB_CHARS
1596
1596
  */
@@ -1617,7 +1617,7 @@ var require_constants2 = __commonJS({
1617
1617
  var require_utils2 = __commonJS({
1618
1618
  "../../node_modules/.pnpm/picomatch@2.3.2/node_modules/picomatch/lib/utils.js"(exports) {
1619
1619
  "use strict";
1620
- var path12 = __require("path");
1620
+ var path11 = __require("path");
1621
1621
  var win32 = process.platform === "win32";
1622
1622
  var {
1623
1623
  REGEX_BACKSLASH,
@@ -1646,7 +1646,7 @@ var require_utils2 = __commonJS({
1646
1646
  if (options && typeof options.windows === "boolean") {
1647
1647
  return options.windows;
1648
1648
  }
1649
- return win32 === true || path12.sep === "\\";
1649
+ return win32 === true || path11.sep === "\\";
1650
1650
  };
1651
1651
  exports.escapeLast = (input2, char, lastIdx) => {
1652
1652
  const idx = input2.lastIndexOf(char, lastIdx);
@@ -3010,7 +3010,7 @@ var require_parse2 = __commonJS({
3010
3010
  var require_picomatch = __commonJS({
3011
3011
  "../../node_modules/.pnpm/picomatch@2.3.2/node_modules/picomatch/lib/picomatch.js"(exports, module) {
3012
3012
  "use strict";
3013
- var path12 = __require("path");
3013
+ var path11 = __require("path");
3014
3014
  var scan = require_scan();
3015
3015
  var parse4 = require_parse2();
3016
3016
  var utils = require_utils2();
@@ -3095,7 +3095,7 @@ var require_picomatch = __commonJS({
3095
3095
  };
3096
3096
  picomatch.matchBase = (input2, glob, options, posix = utils.isWindows(options)) => {
3097
3097
  const regex = glob instanceof RegExp ? glob : picomatch.makeRe(glob, options);
3098
- return regex.test(path12.basename(input2));
3098
+ return regex.test(path11.basename(input2));
3099
3099
  };
3100
3100
  picomatch.isMatch = (str, patterns, options) => picomatch(patterns, options)(str);
3101
3101
  picomatch.parse = (pattern, options) => {
@@ -3322,7 +3322,7 @@ var require_pattern = __commonJS({
3322
3322
  "use strict";
3323
3323
  Object.defineProperty(exports, "__esModule", { value: true });
3324
3324
  exports.isAbsolute = exports.partitionAbsoluteAndRelative = exports.removeDuplicateSlashes = exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.isPatternRelatedToParentDirectory = exports.getPatternsOutsideCurrentDirectory = exports.getPatternsInsideCurrentDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0;
3325
- var path12 = __require("path");
3325
+ var path11 = __require("path");
3326
3326
  var globParent = require_glob_parent();
3327
3327
  var micromatch = require_micromatch();
3328
3328
  var GLOBSTAR = "**";
@@ -3417,7 +3417,7 @@ var require_pattern = __commonJS({
3417
3417
  }
3418
3418
  exports.endsWithSlashGlobStar = endsWithSlashGlobStar;
3419
3419
  function isAffectDepthOfReadingPattern(pattern) {
3420
- const basename = path12.basename(pattern);
3420
+ const basename = path11.basename(pattern);
3421
3421
  return endsWithSlashGlobStar(pattern) || isStaticPattern(basename);
3422
3422
  }
3423
3423
  exports.isAffectDepthOfReadingPattern = isAffectDepthOfReadingPattern;
@@ -3475,7 +3475,7 @@ var require_pattern = __commonJS({
3475
3475
  }
3476
3476
  exports.partitionAbsoluteAndRelative = partitionAbsoluteAndRelative;
3477
3477
  function isAbsolute(pattern) {
3478
- return path12.isAbsolute(pattern);
3478
+ return path11.isAbsolute(pattern);
3479
3479
  }
3480
3480
  exports.isAbsolute = isAbsolute;
3481
3481
  }
@@ -3650,10 +3650,10 @@ var require_utils3 = __commonJS({
3650
3650
  exports.array = array2;
3651
3651
  var errno = require_errno();
3652
3652
  exports.errno = errno;
3653
- var fs10 = require_fs();
3654
- exports.fs = fs10;
3655
- var path12 = require_path();
3656
- exports.path = path12;
3653
+ var fs8 = require_fs();
3654
+ exports.fs = fs8;
3655
+ var path11 = require_path();
3656
+ exports.path = path11;
3657
3657
  var pattern = require_pattern();
3658
3658
  exports.pattern = pattern;
3659
3659
  var stream = require_stream();
@@ -3765,8 +3765,8 @@ var require_async = __commonJS({
3765
3765
  "use strict";
3766
3766
  Object.defineProperty(exports, "__esModule", { value: true });
3767
3767
  exports.read = void 0;
3768
- function read(path12, settings, callback) {
3769
- settings.fs.lstat(path12, (lstatError, lstat) => {
3768
+ function read(path11, settings, callback) {
3769
+ settings.fs.lstat(path11, (lstatError, lstat) => {
3770
3770
  if (lstatError !== null) {
3771
3771
  callFailureCallback(callback, lstatError);
3772
3772
  return;
@@ -3775,7 +3775,7 @@ var require_async = __commonJS({
3775
3775
  callSuccessCallback(callback, lstat);
3776
3776
  return;
3777
3777
  }
3778
- settings.fs.stat(path12, (statError, stat) => {
3778
+ settings.fs.stat(path11, (statError, stat) => {
3779
3779
  if (statError !== null) {
3780
3780
  if (settings.throwErrorOnBrokenSymbolicLink) {
3781
3781
  callFailureCallback(callback, statError);
@@ -3807,13 +3807,13 @@ var require_sync = __commonJS({
3807
3807
  "use strict";
3808
3808
  Object.defineProperty(exports, "__esModule", { value: true });
3809
3809
  exports.read = void 0;
3810
- function read(path12, settings) {
3811
- const lstat = settings.fs.lstatSync(path12);
3810
+ function read(path11, settings) {
3811
+ const lstat = settings.fs.lstatSync(path11);
3812
3812
  if (!lstat.isSymbolicLink() || !settings.followSymbolicLink) {
3813
3813
  return lstat;
3814
3814
  }
3815
3815
  try {
3816
- const stat = settings.fs.statSync(path12);
3816
+ const stat = settings.fs.statSync(path11);
3817
3817
  if (settings.markSymbolicLink) {
3818
3818
  stat.isSymbolicLink = () => true;
3819
3819
  }
@@ -3835,12 +3835,12 @@ var require_fs2 = __commonJS({
3835
3835
  "use strict";
3836
3836
  Object.defineProperty(exports, "__esModule", { value: true });
3837
3837
  exports.createFileSystemAdapter = exports.FILE_SYSTEM_ADAPTER = void 0;
3838
- var fs10 = __require("fs");
3838
+ var fs8 = __require("fs");
3839
3839
  exports.FILE_SYSTEM_ADAPTER = {
3840
- lstat: fs10.lstat,
3841
- stat: fs10.stat,
3842
- lstatSync: fs10.lstatSync,
3843
- statSync: fs10.statSync
3840
+ lstat: fs8.lstat,
3841
+ stat: fs8.stat,
3842
+ lstatSync: fs8.lstatSync,
3843
+ statSync: fs8.statSync
3844
3844
  };
3845
3845
  function createFileSystemAdapter(fsMethods) {
3846
3846
  if (fsMethods === void 0) {
@@ -3857,12 +3857,12 @@ var require_settings = __commonJS({
3857
3857
  "../../node_modules/.pnpm/@nodelib+fs.stat@2.0.5/node_modules/@nodelib/fs.stat/out/settings.js"(exports) {
3858
3858
  "use strict";
3859
3859
  Object.defineProperty(exports, "__esModule", { value: true });
3860
- var fs10 = require_fs2();
3860
+ var fs8 = require_fs2();
3861
3861
  var Settings = class {
3862
3862
  constructor(_options = {}) {
3863
3863
  this._options = _options;
3864
3864
  this.followSymbolicLink = this._getValue(this._options.followSymbolicLink, true);
3865
- this.fs = fs10.createFileSystemAdapter(this._options.fs);
3865
+ this.fs = fs8.createFileSystemAdapter(this._options.fs);
3866
3866
  this.markSymbolicLink = this._getValue(this._options.markSymbolicLink, false);
3867
3867
  this.throwErrorOnBrokenSymbolicLink = this._getValue(this._options.throwErrorOnBrokenSymbolicLink, true);
3868
3868
  }
@@ -3884,17 +3884,17 @@ var require_out = __commonJS({
3884
3884
  var sync = require_sync();
3885
3885
  var settings_1 = require_settings();
3886
3886
  exports.Settings = settings_1.default;
3887
- function stat(path12, optionsOrSettingsOrCallback, callback) {
3887
+ function stat(path11, optionsOrSettingsOrCallback, callback) {
3888
3888
  if (typeof optionsOrSettingsOrCallback === "function") {
3889
- async.read(path12, getSettings(), optionsOrSettingsOrCallback);
3889
+ async.read(path11, getSettings(), optionsOrSettingsOrCallback);
3890
3890
  return;
3891
3891
  }
3892
- async.read(path12, getSettings(optionsOrSettingsOrCallback), callback);
3892
+ async.read(path11, getSettings(optionsOrSettingsOrCallback), callback);
3893
3893
  }
3894
3894
  exports.stat = stat;
3895
- function statSync2(path12, optionsOrSettings) {
3895
+ function statSync2(path11, optionsOrSettings) {
3896
3896
  const settings = getSettings(optionsOrSettings);
3897
- return sync.read(path12, settings);
3897
+ return sync.read(path11, settings);
3898
3898
  }
3899
3899
  exports.statSync = statSync2;
3900
3900
  function getSettings(settingsOrOptions = {}) {
@@ -4019,8 +4019,8 @@ var require_utils4 = __commonJS({
4019
4019
  "use strict";
4020
4020
  Object.defineProperty(exports, "__esModule", { value: true });
4021
4021
  exports.fs = void 0;
4022
- var fs10 = require_fs3();
4023
- exports.fs = fs10;
4022
+ var fs8 = require_fs3();
4023
+ exports.fs = fs8;
4024
4024
  }
4025
4025
  });
4026
4026
 
@@ -4112,16 +4112,16 @@ var require_async2 = __commonJS({
4112
4112
  return;
4113
4113
  }
4114
4114
  const tasks = names.map((name) => {
4115
- const path12 = common.joinPathSegments(directory, name, settings.pathSegmentSeparator);
4115
+ const path11 = common.joinPathSegments(directory, name, settings.pathSegmentSeparator);
4116
4116
  return (done) => {
4117
- fsStat.stat(path12, settings.fsStatSettings, (error62, stats) => {
4117
+ fsStat.stat(path11, settings.fsStatSettings, (error62, stats) => {
4118
4118
  if (error62 !== null) {
4119
4119
  done(error62);
4120
4120
  return;
4121
4121
  }
4122
4122
  const entry = {
4123
4123
  name,
4124
- path: path12,
4124
+ path: path11,
4125
4125
  dirent: utils.fs.createDirentFromStats(name, stats)
4126
4126
  };
4127
4127
  if (settings.stats) {
@@ -4215,14 +4215,14 @@ var require_fs4 = __commonJS({
4215
4215
  "use strict";
4216
4216
  Object.defineProperty(exports, "__esModule", { value: true });
4217
4217
  exports.createFileSystemAdapter = exports.FILE_SYSTEM_ADAPTER = void 0;
4218
- var fs10 = __require("fs");
4218
+ var fs8 = __require("fs");
4219
4219
  exports.FILE_SYSTEM_ADAPTER = {
4220
- lstat: fs10.lstat,
4221
- stat: fs10.stat,
4222
- lstatSync: fs10.lstatSync,
4223
- statSync: fs10.statSync,
4224
- readdir: fs10.readdir,
4225
- readdirSync: fs10.readdirSync
4220
+ lstat: fs8.lstat,
4221
+ stat: fs8.stat,
4222
+ lstatSync: fs8.lstatSync,
4223
+ statSync: fs8.statSync,
4224
+ readdir: fs8.readdir,
4225
+ readdirSync: fs8.readdirSync
4226
4226
  };
4227
4227
  function createFileSystemAdapter(fsMethods) {
4228
4228
  if (fsMethods === void 0) {
@@ -4239,15 +4239,15 @@ var require_settings2 = __commonJS({
4239
4239
  "../../node_modules/.pnpm/@nodelib+fs.scandir@2.1.5/node_modules/@nodelib/fs.scandir/out/settings.js"(exports) {
4240
4240
  "use strict";
4241
4241
  Object.defineProperty(exports, "__esModule", { value: true });
4242
- var path12 = __require("path");
4242
+ var path11 = __require("path");
4243
4243
  var fsStat = require_out();
4244
- var fs10 = require_fs4();
4244
+ var fs8 = require_fs4();
4245
4245
  var Settings = class {
4246
4246
  constructor(_options = {}) {
4247
4247
  this._options = _options;
4248
4248
  this.followSymbolicLinks = this._getValue(this._options.followSymbolicLinks, false);
4249
- this.fs = fs10.createFileSystemAdapter(this._options.fs);
4250
- this.pathSegmentSeparator = this._getValue(this._options.pathSegmentSeparator, path12.sep);
4249
+ this.fs = fs8.createFileSystemAdapter(this._options.fs);
4250
+ this.pathSegmentSeparator = this._getValue(this._options.pathSegmentSeparator, path11.sep);
4251
4251
  this.stats = this._getValue(this._options.stats, false);
4252
4252
  this.throwErrorOnBrokenSymbolicLink = this._getValue(this._options.throwErrorOnBrokenSymbolicLink, true);
4253
4253
  this.fsStatSettings = new fsStat.Settings({
@@ -4274,17 +4274,17 @@ var require_out2 = __commonJS({
4274
4274
  var sync = require_sync2();
4275
4275
  var settings_1 = require_settings2();
4276
4276
  exports.Settings = settings_1.default;
4277
- function scandir(path12, optionsOrSettingsOrCallback, callback) {
4277
+ function scandir(path11, optionsOrSettingsOrCallback, callback) {
4278
4278
  if (typeof optionsOrSettingsOrCallback === "function") {
4279
- async.read(path12, getSettings(), optionsOrSettingsOrCallback);
4279
+ async.read(path11, getSettings(), optionsOrSettingsOrCallback);
4280
4280
  return;
4281
4281
  }
4282
- async.read(path12, getSettings(optionsOrSettingsOrCallback), callback);
4282
+ async.read(path11, getSettings(optionsOrSettingsOrCallback), callback);
4283
4283
  }
4284
4284
  exports.scandir = scandir;
4285
- function scandirSync(path12, optionsOrSettings) {
4285
+ function scandirSync(path11, optionsOrSettings) {
4286
4286
  const settings = getSettings(optionsOrSettings);
4287
- return sync.read(path12, settings);
4287
+ return sync.read(path11, settings);
4288
4288
  }
4289
4289
  exports.scandirSync = scandirSync;
4290
4290
  function getSettings(settingsOrOptions = {}) {
@@ -4932,7 +4932,7 @@ var require_settings3 = __commonJS({
4932
4932
  "../../node_modules/.pnpm/@nodelib+fs.walk@1.2.8/node_modules/@nodelib/fs.walk/out/settings.js"(exports) {
4933
4933
  "use strict";
4934
4934
  Object.defineProperty(exports, "__esModule", { value: true });
4935
- var path12 = __require("path");
4935
+ var path11 = __require("path");
4936
4936
  var fsScandir = require_out2();
4937
4937
  var Settings = class {
4938
4938
  constructor(_options = {}) {
@@ -4942,7 +4942,7 @@ var require_settings3 = __commonJS({
4942
4942
  this.deepFilter = this._getValue(this._options.deepFilter, null);
4943
4943
  this.entryFilter = this._getValue(this._options.entryFilter, null);
4944
4944
  this.errorFilter = this._getValue(this._options.errorFilter, null);
4945
- this.pathSegmentSeparator = this._getValue(this._options.pathSegmentSeparator, path12.sep);
4945
+ this.pathSegmentSeparator = this._getValue(this._options.pathSegmentSeparator, path11.sep);
4946
4946
  this.fsScandirSettings = new fsScandir.Settings({
4947
4947
  followSymbolicLinks: this._options.followSymbolicLinks,
4948
4948
  fs: this._options.fs,
@@ -5004,7 +5004,7 @@ var require_reader2 = __commonJS({
5004
5004
  "../../node_modules/.pnpm/fast-glob@3.3.3/node_modules/fast-glob/out/readers/reader.js"(exports) {
5005
5005
  "use strict";
5006
5006
  Object.defineProperty(exports, "__esModule", { value: true });
5007
- var path12 = __require("path");
5007
+ var path11 = __require("path");
5008
5008
  var fsStat = require_out();
5009
5009
  var utils = require_utils3();
5010
5010
  var Reader = class {
@@ -5017,7 +5017,7 @@ var require_reader2 = __commonJS({
5017
5017
  });
5018
5018
  }
5019
5019
  _getFullEntryPath(filepath) {
5020
- return path12.resolve(this._settings.cwd, filepath);
5020
+ return path11.resolve(this._settings.cwd, filepath);
5021
5021
  }
5022
5022
  _makeEntry(stats, pattern) {
5023
5023
  const entry = {
@@ -5433,7 +5433,7 @@ var require_provider = __commonJS({
5433
5433
  "../../node_modules/.pnpm/fast-glob@3.3.3/node_modules/fast-glob/out/providers/provider.js"(exports) {
5434
5434
  "use strict";
5435
5435
  Object.defineProperty(exports, "__esModule", { value: true });
5436
- var path12 = __require("path");
5436
+ var path11 = __require("path");
5437
5437
  var deep_1 = require_deep();
5438
5438
  var entry_1 = require_entry();
5439
5439
  var error_1 = require_error();
@@ -5447,7 +5447,7 @@ var require_provider = __commonJS({
5447
5447
  this.entryTransformer = new entry_2.default(this._settings);
5448
5448
  }
5449
5449
  _getRootDirectory(task) {
5450
- return path12.resolve(this._settings.cwd, task.base);
5450
+ return path11.resolve(this._settings.cwd, task.base);
5451
5451
  }
5452
5452
  _getReaderOptions(task) {
5453
5453
  const basePath = task.base === "." ? "" : task.base;
@@ -5628,16 +5628,16 @@ var require_settings4 = __commonJS({
5628
5628
  "use strict";
5629
5629
  Object.defineProperty(exports, "__esModule", { value: true });
5630
5630
  exports.DEFAULT_FILE_SYSTEM_ADAPTER = void 0;
5631
- var fs10 = __require("fs");
5631
+ var fs8 = __require("fs");
5632
5632
  var os3 = __require("os");
5633
5633
  var CPU_COUNT = Math.max(os3.cpus().length, 1);
5634
5634
  exports.DEFAULT_FILE_SYSTEM_ADAPTER = {
5635
- lstat: fs10.lstat,
5636
- lstatSync: fs10.lstatSync,
5637
- stat: fs10.stat,
5638
- statSync: fs10.statSync,
5639
- readdir: fs10.readdir,
5640
- readdirSync: fs10.readdirSync
5635
+ lstat: fs8.lstat,
5636
+ lstatSync: fs8.lstatSync,
5637
+ stat: fs8.stat,
5638
+ statSync: fs8.statSync,
5639
+ readdir: fs8.readdir,
5640
+ readdirSync: fs8.readdirSync
5641
5641
  };
5642
5642
  var Settings = class {
5643
5643
  constructor(_options = {}) {
@@ -5784,10 +5784,10 @@ var require_out4 = __commonJS({
5784
5784
  });
5785
5785
 
5786
5786
  // src/main.ts
5787
- import { Command as Command6, CommanderError } from "commander";
5787
+ import { Command as Command7, CommanderError, Option } from "commander";
5788
5788
 
5789
5789
  // package.json
5790
- var version = "0.3.0";
5790
+ var version = "0.4.0";
5791
5791
 
5792
5792
  // src/commands/auth/index.ts
5793
5793
  import { Command } from "commander";
@@ -26598,7 +26598,7 @@ function installDependencies(cwd, packageManager, deps) {
26598
26598
  const cmd = packageManager === "npm" ? `npm install ${deps.join(" ")}` : packageManager === "yarn" ? `yarn add ${deps.join(" ")}` : `pnpm add ${deps.join(" ")}`;
26599
26599
  execSync(cmd, { stdio: ["ignore", 2, 2], cwd });
26600
26600
  }
26601
- async function createLocaleFiles(cwd, framework, baseLocale, targetLocales) {
26601
+ async function createLocaleFiles(cwd, framework, baseLocale, targetLocales2) {
26602
26602
  const setup = FRAMEWORK_SETUPS[framework];
26603
26603
  const createdFiles = [];
26604
26604
  for (const file2 of setup.files) {
@@ -26614,7 +26614,7 @@ async function createLocaleFiles(cwd, framework, baseLocale, targetLocales) {
26614
26614
  await fs3.mkdir(path32.dirname(baseFilePath), { recursive: true });
26615
26615
  await fs3.writeFile(baseFilePath, file2.content);
26616
26616
  createdFiles.push(basePath);
26617
- for (const locale of targetLocales) {
26617
+ for (const locale of targetLocales2) {
26618
26618
  const targetPath = file2.path.replace("{{lng}}", locale);
26619
26619
  const targetFilePath = path32.join(cwd, targetPath);
26620
26620
  await fs3.mkdir(path32.dirname(targetFilePath), { recursive: true });
@@ -26625,14 +26625,14 @@ async function createLocaleFiles(cwd, framework, baseLocale, targetLocales) {
26625
26625
  }
26626
26626
  return createdFiles;
26627
26627
  }
26628
- async function setupFramework(cwd, framework, baseLocale, targetLocales, packageManager) {
26628
+ async function setupFramework(cwd, framework, baseLocale, targetLocales2, packageManager) {
26629
26629
  const setup = FRAMEWORK_SETUPS[framework];
26630
26630
  installDependencies(cwd, packageManager, setup.dependencies);
26631
26631
  const createdFiles = await createLocaleFiles(
26632
26632
  cwd,
26633
26633
  framework,
26634
26634
  baseLocale,
26635
- targetLocales
26635
+ targetLocales2
26636
26636
  );
26637
26637
  return {
26638
26638
  installedDeps: setup.dependencies,
@@ -27544,9 +27544,9 @@ function runtimeProperties(cliVersion) {
27544
27544
  };
27545
27545
  }
27546
27546
  var activeCommand = null;
27547
- function beginCommand(path12, cliVersion) {
27547
+ function beginCommand(path11, cliVersion) {
27548
27548
  activeCommand = {
27549
- path: path12,
27549
+ path: path11,
27550
27550
  startedAt: Date.now(),
27551
27551
  cliVersion,
27552
27552
  reported: false
@@ -28088,24 +28088,14 @@ function InitWizard({
28088
28088
  ".json"
28089
28089
  ] }),
28090
28090
  /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
28091
- "3. Run: tolkbot translate run --src",
28092
- " ",
28093
- setupData.baseLanguage,
28094
- " --dst",
28091
+ "3. Run: tolkbot translate --dst",
28095
28092
  " ",
28096
28093
  setupData.targetLanguages[0] ?? "no"
28097
28094
  ] })
28098
28095
  ] }),
28099
28096
  !setupData.shouldInstall && /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
28100
28097
  /* @__PURE__ */ jsx9(Text9, { children: "Next steps:" }),
28101
- /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
28102
- "Run: tolkbot translate plan --src",
28103
- " ",
28104
- setupData.baseLanguage,
28105
- " --dst",
28106
- " ",
28107
- setupData.targetLanguages[0] ?? "no"
28108
- ] })
28098
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "See what a run would do: tolkbot translate --all --dry-run" })
28109
28099
  ] })
28110
28100
  ] });
28111
28101
  }
@@ -28299,380 +28289,66 @@ async function runInteractive(opts) {
28299
28289
  }
28300
28290
  }
28301
28291
 
28302
- // src/commands/learnings/index.ts
28292
+ // src/commands/jobs/index.ts
28303
28293
  import { Command as Command3 } from "commander";
28304
28294
  import { z as z5 } from "zod/v4";
28305
28295
 
28306
- // src/commands/learnings/archive.tsx
28307
- import { Spinner as Spinner9 } from "@inkjs/ui";
28296
+ // src/commands/jobs/apply.tsx
28297
+ import { useState as useState2 } from "react";
28298
+ import { MultiSelect } from "@inkjs/ui";
28308
28299
  import { Box as Box11, render as render11, Text as Text11 } from "ink";
28309
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
28310
- function ArchiveUI({ state }) {
28311
- if (state.type === "archiving") {
28312
- return /* @__PURE__ */ jsxs11(Box11, { children: [
28313
- /* @__PURE__ */ jsx11(Spinner9, {}),
28314
- /* @__PURE__ */ jsx11(Text11, { children: " Archiving learning\u2026" })
28315
- ] });
28316
- }
28317
- return /* @__PURE__ */ jsxs11(Box11, { children: [
28318
- /* @__PURE__ */ jsx11(Text11, { color: "green", children: "\u2713" }),
28319
- /* @__PURE__ */ jsx11(Text11, { children: " Learning archived" })
28320
- ] });
28321
- }
28322
- async function runLearningsArchive(opts) {
28323
- const run = async () => {
28324
- const convexClient = await getAuthenticatedConvexClient();
28325
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
28326
- await convexClient.mutation(api.learnings.archive, {
28327
- organizationId: organizationId2,
28328
- learningId: opts.id
28329
- });
28330
- };
28331
- if (opts.json) {
28332
- await run();
28333
- jsonSuccess({ id: opts.id, ok: true });
28334
- }
28335
- const { unmount } = renderTransient(
28336
- /* @__PURE__ */ jsx11(ArchiveUI, { state: { type: "archiving" } })
28337
- );
28338
- try {
28339
- await run();
28340
- } finally {
28341
- unmount();
28342
- }
28343
- render11(/* @__PURE__ */ jsx11(ArchiveUI, { state: { type: "success" } })).unmount();
28344
- }
28345
28300
 
28346
- // src/commands/learnings/list.tsx
28347
- import { Spinner as Spinner10 } from "@inkjs/ui";
28348
- import { Box as Box12, render as render12, Text as Text12 } from "ink";
28349
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
28350
- var feedbackLabels = {
28351
- "incorrect-term": "Incorrect term",
28352
- "wrong-tone": "Wrong tone",
28353
- "cultural-context": "Cultural context",
28354
- "technical-accuracy": "Technical accuracy",
28355
- other: "Other"
28356
- };
28357
- function LearningsListUI({ state }) {
28358
- if (state.type === "loading") {
28359
- return /* @__PURE__ */ jsxs12(Box12, { children: [
28360
- /* @__PURE__ */ jsx12(Spinner10, {}),
28361
- /* @__PURE__ */ jsx12(Text12, { children: " Loading learnings\u2026" })
28362
- ] });
28363
- }
28364
- const { learnings } = state;
28365
- if (learnings.length === 0) {
28366
- return /* @__PURE__ */ jsx12(Box12, { children: /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "No learnings found." }) });
28367
- }
28368
- return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
28369
- /* @__PURE__ */ jsx12(Box12, { marginBottom: 1, children: /* @__PURE__ */ jsxs12(Text12, { bold: true, children: [
28370
- learnings.length,
28371
- " learning",
28372
- learnings.length === 1 ? "" : "s"
28373
- ] }) }),
28374
- learnings.map((l) => /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginBottom: 1, children: [
28375
- /* @__PURE__ */ jsxs12(Box12, { gap: 1, children: [
28376
- /* @__PURE__ */ jsxs12(Text12, { bold: true, children: [
28377
- l.srcLocale,
28378
- " \u2192 ",
28379
- l.dstLocale
28380
- ] }),
28381
- /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
28382
- "[",
28383
- feedbackLabels[l.feedbackType] ?? l.feedbackType,
28384
- "]"
28385
- ] }),
28386
- l.status === "archived" && /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "[archived]" })
28387
- ] }),
28388
- /* @__PURE__ */ jsx12(Box12, { paddingLeft: 2, children: /* @__PURE__ */ jsxs12(Text12, { children: [
28389
- '"',
28390
- l.sourceText,
28391
- '" \u2192 "',
28392
- l.suggestedTranslation,
28393
- '"'
28394
- ] }) }),
28395
- l.reasoning && /* @__PURE__ */ jsx12(Box12, { paddingLeft: 2, children: /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
28396
- "Reason: ",
28397
- l.reasoning
28398
- ] }) }),
28399
- /* @__PURE__ */ jsx12(Box12, { paddingLeft: 2, children: /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
28400
- "ID: ",
28401
- l._id
28402
- ] }) })
28403
- ] }, l._id))
28404
- ] });
28405
- }
28406
- async function loadLearnings(opts) {
28407
- const convexClient = await getAuthenticatedConvexClient();
28408
- const { projectId, organizationId: organizationId2 } = await getProjectContext(convexClient);
28409
- return convexClient.query(api.learnings.listByProject, {
28410
- organizationId: organizationId2,
28411
- projectId,
28412
- status: opts.status,
28413
- limit: opts.limit
28301
+ // src/commands/translate/context.ts
28302
+ import path5 from "path";
28303
+ async function loadPlanContext(cwd) {
28304
+ const config3 = readConfig(cwd);
28305
+ if (!config3) throw noConfigError();
28306
+ const client3 = await getAuthenticatedConvexClient();
28307
+ const project = await client3.query(api.projects.getProjectForCli, {
28308
+ projectId: config3.config.projectId
28414
28309
  });
28415
- }
28416
- async function runLearningsList(opts) {
28417
- if (opts.json) {
28418
- const learnings2 = await loadLearnings(opts);
28419
- jsonSuccess({
28420
- learnings: learnings2.map((l) => ({
28421
- id: l._id,
28422
- sourceText: l.sourceText,
28423
- suggestedTranslation: l.suggestedTranslation,
28424
- srcLocale: l.srcLocale,
28425
- dstLocale: l.dstLocale,
28426
- feedbackType: l.feedbackType,
28427
- reasoning: l.reasoning,
28428
- status: l.status,
28429
- source: l.source
28430
- }))
28431
- });
28432
- }
28433
- const { unmount } = renderTransient(
28434
- /* @__PURE__ */ jsx12(LearningsListUI, { state: { type: "loading" } })
28435
- );
28436
- let learnings;
28437
- try {
28438
- learnings = await loadLearnings(opts);
28439
- } finally {
28440
- unmount();
28441
- }
28442
- render12(
28443
- /* @__PURE__ */ jsx12(LearningsListUI, { state: { type: "success", learnings } })
28444
- ).unmount();
28445
- }
28446
-
28447
- // src/commands/learnings/remove.tsx
28448
- import { Spinner as Spinner11 } from "@inkjs/ui";
28449
- import { Box as Box13, render as render13, Text as Text13 } from "ink";
28450
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
28451
- function RemoveUI2({ state }) {
28452
- if (state.type === "removing") {
28453
- return /* @__PURE__ */ jsxs13(Box13, { children: [
28454
- /* @__PURE__ */ jsx13(Spinner11, {}),
28455
- /* @__PURE__ */ jsx13(Text13, { children: " Removing learning\u2026" })
28456
- ] });
28457
- }
28458
- return /* @__PURE__ */ jsxs13(Box13, { children: [
28459
- /* @__PURE__ */ jsx13(Text13, { color: "green", children: "\u2713" }),
28460
- /* @__PURE__ */ jsx13(Text13, { children: " Learning removed" })
28461
- ] });
28462
- }
28463
- async function runLearningsRemove(opts) {
28464
- const run = async () => {
28465
- const convexClient = await getAuthenticatedConvexClient();
28466
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
28467
- await convexClient.mutation(api.learnings.remove, {
28468
- organizationId: organizationId2,
28469
- learningId: opts.id
28470
- });
28310
+ if (!project) throw projectNotFoundError();
28311
+ setTelemetryOrganization(project.organizationId);
28312
+ return {
28313
+ root: path5.dirname(config3.path),
28314
+ project,
28315
+ client: client3,
28316
+ framework: config3.config.framework
28471
28317
  };
28472
- if (opts.json) {
28473
- await run();
28474
- jsonSuccess({ id: opts.id, ok: true });
28475
- }
28476
- const { unmount } = renderTransient(
28477
- /* @__PURE__ */ jsx13(RemoveUI2, { state: { type: "removing" } })
28478
- );
28479
- try {
28480
- await run();
28481
- } finally {
28482
- unmount();
28483
- }
28484
- render13(/* @__PURE__ */ jsx13(RemoveUI2, { state: { type: "success" } })).unmount();
28485
28318
  }
28486
-
28487
- // src/commands/learnings/show.tsx
28488
- import { Spinner as Spinner12 } from "@inkjs/ui";
28489
- import { Box as Box14, render as render14, Text as Text14 } from "ink";
28490
- import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
28491
- function LearningShowUI({ state }) {
28492
- if (state.type === "loading") {
28493
- return /* @__PURE__ */ jsxs14(Box14, { children: [
28494
- /* @__PURE__ */ jsx14(Spinner12, {}),
28495
- /* @__PURE__ */ jsx14(Text14, { children: " Loading learning\u2026" })
28496
- ] });
28497
- }
28498
- const { learning } = state;
28499
- return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
28500
- /* @__PURE__ */ jsxs14(Box14, { gap: 1, children: [
28501
- /* @__PURE__ */ jsx14(Text14, { bold: true, children: "Source text:" }),
28502
- /* @__PURE__ */ jsx14(Text14, { children: learning.sourceText })
28503
- ] }),
28504
- /* @__PURE__ */ jsxs14(Box14, { gap: 1, children: [
28505
- /* @__PURE__ */ jsx14(Text14, { bold: true, children: "Suggested translation:" }),
28506
- /* @__PURE__ */ jsx14(Text14, { children: learning.suggestedTranslation })
28507
- ] }),
28508
- /* @__PURE__ */ jsxs14(Box14, { gap: 1, children: [
28509
- /* @__PURE__ */ jsx14(Text14, { bold: true, children: "Locales:" }),
28510
- /* @__PURE__ */ jsxs14(Text14, { children: [
28511
- learning.srcLocale,
28512
- " \u2192 ",
28513
- learning.dstLocale
28514
- ] })
28515
- ] }),
28516
- /* @__PURE__ */ jsxs14(Box14, { gap: 1, children: [
28517
- /* @__PURE__ */ jsx14(Text14, { bold: true, children: "Feedback type:" }),
28518
- /* @__PURE__ */ jsx14(Text14, { children: learning.feedbackType })
28519
- ] }),
28520
- /* @__PURE__ */ jsxs14(Box14, { gap: 1, children: [
28521
- /* @__PURE__ */ jsx14(Text14, { bold: true, children: "Reasoning:" }),
28522
- /* @__PURE__ */ jsx14(Text14, { children: learning.reasoning })
28523
- ] }),
28524
- /* @__PURE__ */ jsxs14(Box14, { gap: 1, children: [
28525
- /* @__PURE__ */ jsx14(Text14, { bold: true, children: "Status:" }),
28526
- /* @__PURE__ */ jsx14(Text14, { color: learning.status === "active" ? "green" : "yellow", children: learning.status })
28527
- ] }),
28528
- /* @__PURE__ */ jsxs14(Box14, { gap: 1, children: [
28529
- /* @__PURE__ */ jsx14(Text14, { bold: true, children: "Source:" }),
28530
- /* @__PURE__ */ jsx14(Text14, { children: learning.source })
28531
- ] }),
28532
- /* @__PURE__ */ jsx14(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
28533
- "ID: ",
28534
- learning._id
28535
- ] }) })
28536
- ] });
28319
+ function describePlan(plan) {
28320
+ const { summary } = plan;
28321
+ const lines = [
28322
+ `${summary.totalKeys} keys expected, ${summary.upToDate} up to date`
28323
+ ];
28324
+ if (summary.missing > 0) lines.push(`${summary.missing} missing`);
28325
+ if (summary.changed > 0)
28326
+ lines.push(`${summary.changed} changed since translated`);
28327
+ return [...lines, ...describeLeftOut(plan)];
28537
28328
  }
28538
- async function loadLearning(id) {
28539
- const convexClient = await getAuthenticatedConvexClient();
28540
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
28541
- const learning = await convexClient.query(api.learnings.getById, {
28542
- organizationId: organizationId2,
28543
- learningId: id
28544
- });
28545
- if (!learning) {
28546
- throw new CliError(
28547
- `Learning ${id} was not found.`,
28548
- "List them with 'tolkbot learnings list'.",
28549
- "not_found"
28329
+ function describeLeftOut(plan) {
28330
+ const { summary } = plan;
28331
+ const lines = [];
28332
+ const protectedCount = plan.protectedKeys.length;
28333
+ if (protectedCount > 0) {
28334
+ const reasons = Object.entries(summary.protected).map(([reason, n]) => `${n} ${reason}`).join(", ");
28335
+ lines.push(
28336
+ `${protectedCount} changed but kept for a person (${reasons})`
28550
28337
  );
28551
28338
  }
28552
- return learning;
28553
- }
28554
- async function runLearningsShow(opts) {
28555
- if (opts.json) {
28556
- const learning2 = await loadLearning(opts.id);
28557
- jsonSuccess({
28558
- id: learning2._id,
28559
- sourceText: learning2.sourceText,
28560
- suggestedTranslation: learning2.suggestedTranslation,
28561
- srcLocale: learning2.srcLocale,
28562
- dstLocale: learning2.dstLocale,
28563
- feedbackType: learning2.feedbackType,
28564
- reasoning: learning2.reasoning,
28565
- status: learning2.status,
28566
- source: learning2.source
28567
- });
28568
- }
28569
- const { unmount } = renderTransient(
28570
- /* @__PURE__ */ jsx14(LearningShowUI, { state: { type: "loading" } })
28571
- );
28572
- let learning;
28573
- try {
28574
- learning = await loadLearning(opts.id);
28575
- } finally {
28576
- unmount();
28577
- }
28578
- render14(/* @__PURE__ */ jsx14(LearningShowUI, { state: { type: "success", learning } })).unmount();
28579
- }
28580
-
28581
- // src/commands/learnings/index.ts
28582
- var learningsCommand = new Command3("learnings").description(
28583
- "View and manage translation learnings"
28584
- );
28585
- learningsCommand.command("list").description("List translation learnings for the current project").option("--status <status>", "Filter by status (active or archived)").option("--limit <n>", "Max number of results", (v) => Number(v), 100).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
28586
- const schema = z5.object({
28587
- status: z5.enum(["active", "archived"]).optional(),
28588
- limit: z5.coerce.number().int().positive().default(100),
28589
- json: z5.boolean().optional()
28590
- });
28591
- const parsed = validateOrExit(schema, opts);
28592
- await runLearningsList(parsed);
28593
- });
28594
- learningsCommand.command("show").description("Show details of a learning").requiredOption("--id <learningId>", "Learning ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
28595
- const schema = z5.object({
28596
- id: z5.string().min(1),
28597
- json: z5.boolean().optional()
28598
- });
28599
- const parsed = validateOrExit(schema, opts);
28600
- await runLearningsShow(parsed);
28601
- });
28602
- learningsCommand.command("archive").description("Archive a learning (keeps it but excludes from translations)").requiredOption("--id <learningId>", "Learning ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
28603
- const schema = z5.object({
28604
- id: z5.string().min(1),
28605
- json: z5.boolean().optional()
28606
- });
28607
- const parsed = validateOrExit(schema, opts);
28608
- await runLearningsArchive(parsed);
28609
- });
28610
- learningsCommand.command("remove").description("Permanently remove a learning").requiredOption("--id <learningId>", "Learning ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
28611
- const schema = z5.object({
28612
- id: z5.string().min(1),
28613
- json: z5.boolean().optional()
28614
- });
28615
- const parsed = validateOrExit(schema, opts);
28616
- await runLearningsRemove(parsed);
28617
- });
28618
-
28619
- // src/commands/telemetry.ts
28620
- import { Command as Command4 } from "commander";
28621
- import { z as z6 } from "zod/v4";
28622
- var REASONS = {
28623
- env: `disabled by $${TELEMETRY_ENV_VAR}`,
28624
- "do-not-track": `disabled by $${DO_NOT_TRACK_ENV_VAR}`,
28625
- preference: "disabled with `tolkbot telemetry off`",
28626
- "no-key": "disabled: this build has no telemetry key",
28627
- default: "enabled"
28628
- };
28629
- async function printStatus(json2) {
28630
- const state = await telemetryState();
28631
- if (json2) {
28632
- jsonResult({ ...state, preferenceFile: getPreferencePath() }, 0);
28633
- return;
28339
+ if (summary.held > 0)
28340
+ lines.push(`${summary.held} missing but held by a person`);
28341
+ if (summary.untracked > 0) {
28342
+ lines.push(
28343
+ plan.tasks.some((t) => t.reason === "untracked") ? `${summary.untracked} untracked, being retranslated` : `${summary.untracked} untracked translations kept: TolkBot has no record of them. Run "tolkbot translate adopt" to keep them as reviewed, or translate with --retranslate-untracked to replace them.`
28344
+ );
28634
28345
  }
28635
- console.log(
28636
- `Telemetry is ${state.enabled ? "on" : "off"} (${REASONS[state.reason]}).`
28637
- );
28638
- console.log(
28639
- `Turn it off with \`tolkbot telemetry off\`, $${TELEMETRY_ENV_VAR}=0 or $${DO_NOT_TRACK_ENV_VAR}=1.`
28640
- );
28346
+ return lines;
28641
28347
  }
28642
- var telemetryCommand = new Command4("telemetry").description(
28643
- "Control anonymous usage reporting"
28644
- );
28645
- telemetryCommand.command("status", { isDefault: true }).description("Show whether telemetry is on and why").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
28646
- const parsed = validateOrExit(
28647
- z6.object({ json: z6.boolean().optional() }),
28648
- opts
28649
- );
28650
- await printStatus(parsed.json);
28651
- });
28652
- telemetryCommand.command("off").description("Stop reporting usage from this machine").action(async () => {
28653
- await setTelemetryPreference(false);
28654
- console.log(
28655
- `Telemetry is off. Preference saved to ${getPreferencePath()}.`
28656
- );
28657
- });
28658
- telemetryCommand.command("on").description("Resume reporting usage from this machine").action(async () => {
28659
- await setTelemetryPreference(true);
28660
- await printStatus(false);
28661
- });
28662
-
28663
- // src/commands/translate/index.ts
28664
- import { Command as Command5 } from "commander";
28665
- import { z as z7 } from "zod/v4";
28666
28348
 
28667
- // src/commands/translate/plan.tsx
28668
- import fs8 from "fs/promises";
28669
- import path8 from "path";
28670
- import { Spinner as Spinner13 } from "@inkjs/ui";
28671
- import { Box as Box15, render as render15, Text as Text15 } from "ink";
28672
-
28673
- // src/commands/translate/task-generator.ts
28349
+ // src/commands/translate/apply-core.ts
28674
28350
  import fs7 from "fs/promises";
28675
- import path7 from "path";
28351
+ import path8 from "path";
28676
28352
 
28677
28353
  // ../../node_modules/.pnpm/emoji-regex@10.6.0/node_modules/emoji-regex/index.mjs
28678
28354
  var emoji_regex_default = () => {
@@ -28766,7 +28442,7 @@ function eastAsianWidth(codePoint, { ambiguousAsWide = false } = {}) {
28766
28442
  // ../parser/dist/index.js
28767
28443
  var import_fast_glob = __toESM(require_out4(), 1);
28768
28444
  var import_fast_glob2 = __toESM(require_out4(), 1);
28769
- import path5 from "path";
28445
+ import path6 from "path";
28770
28446
  import fs5 from "fs/promises";
28771
28447
  import path23 from "path";
28772
28448
  import fs23 from "fs/promises";
@@ -28875,11 +28551,11 @@ function flattenValues(data) {
28875
28551
  const values = /* @__PURE__ */ new Map();
28876
28552
  function walk(obj, prefix) {
28877
28553
  for (const [key, value] of Object.entries(obj)) {
28878
- const fullKey4 = prefix ? `${prefix}.${key}` : key;
28554
+ const fullKey5 = prefix ? `${prefix}.${key}` : key;
28879
28555
  if (typeof value === "string") {
28880
- values.set(fullKey4, value);
28556
+ values.set(fullKey5, value);
28881
28557
  } else if (typeof value === "object" && value !== null) {
28882
- walk(value, fullKey4);
28558
+ walk(value, fullKey5);
28883
28559
  }
28884
28560
  }
28885
28561
  }
@@ -29917,7 +29593,7 @@ var BaseAdapter = class {
29917
29593
  parseErrors: []
29918
29594
  };
29919
29595
  }
29920
- const relativePaths = files.map((f) => path5.relative(baseDir, f));
29596
+ const relativePaths = files.map((f) => path6.relative(baseDir, f));
29921
29597
  const groupedPaths = groupByLocale(
29922
29598
  relativePaths,
29923
29599
  options?.knownLocales
@@ -29927,7 +29603,7 @@ var BaseAdapter = class {
29927
29603
  for (const [locale, localePaths] of groupedPaths) {
29928
29604
  const translationFiles = [];
29929
29605
  for (const relativePath of localePaths) {
29930
- const absolutePath = path5.join(baseDir, relativePath);
29606
+ const absolutePath = path6.join(baseDir, relativePath);
29931
29607
  try {
29932
29608
  const file2 = await this.parse(absolutePath);
29933
29609
  translationFiles.push(file2);
@@ -30868,10 +30544,175 @@ function pluralFormContext(plural2, locale, sourceKey) {
30868
30544
  return parts.join(" ");
30869
30545
  }
30870
30546
 
30871
- // src/commands/translate/plan-core.ts
30547
+ // src/commands/translate/project-files.ts
30548
+ import fs6 from "fs/promises";
30549
+ import path7 from "path";
30550
+ function fullPattern(scope) {
30551
+ return scope.subpath ? path7.posix.join(scope.subpath, scope.sourcePattern) : scope.sourcePattern;
30552
+ }
30553
+ function formatOf(scope) {
30554
+ return detectFormat(scope.sourcePattern) ?? "json";
30555
+ }
30556
+ async function listLocaleFiles(root, scope) {
30557
+ const pattern = fullPattern(scope);
30558
+ const adapter = createAdapter(formatOf(scope));
30559
+ const match = createPathMatcher(pattern);
30560
+ const found = await adapter.findFiles(root, withNamespaceWildcard(pattern));
30561
+ const files = [];
30562
+ for (const absolutePath of found.sort()) {
30563
+ const relativePath = path7.relative(root, absolutePath).split(path7.sep).join("/");
30564
+ const m = match(relativePath);
30565
+ if (!m) continue;
30566
+ files.push({
30567
+ pathTemplate: replaceNamespacePlaceholder(
30568
+ scope.sourcePattern,
30569
+ m.namespace ?? ""
30570
+ ),
30571
+ namespace: m.namespace,
30572
+ locale: m.locale,
30573
+ relativePath,
30574
+ absolutePath
30575
+ });
30576
+ }
30577
+ return files;
30578
+ }
30579
+ function filesForLocale(files, locale) {
30580
+ return files.filter((file2) => sameLocale(file2.locale, locale));
30581
+ }
30582
+ function targetFileFor(source, targetLocale, files, scope) {
30583
+ const existing = files.find(
30584
+ (file2) => file2.pathTemplate === source.pathTemplate && sameLocale(file2.locale, targetLocale)
30585
+ ) ?? null;
30586
+ if (existing) return { relativePath: existing.relativePath, existing };
30587
+ const spelling = files.find((file2) => sameLocale(file2.locale, targetLocale))?.locale ?? scope.detectedLocales?.find((l) => sameLocale(l, targetLocale)) ?? targetLocale;
30588
+ const relativePath = replaceLocalePlaceholder(
30589
+ replaceNamespacePlaceholder(fullPattern(scope), source.namespace ?? ""),
30590
+ spelling
30591
+ );
30592
+ return { relativePath, existing: null };
30593
+ }
30594
+ async function readTranslationFile(absolutePath, format) {
30595
+ try {
30596
+ const content = await fs6.readFile(absolutePath, "utf-8");
30597
+ const file2 = await createAdapter(format).parse(absolutePath);
30598
+ return { data: file2.data, content };
30599
+ } catch (error62) {
30600
+ return {
30601
+ error: error62 instanceof Error ? error62.message : String(error62)
30602
+ };
30603
+ }
30604
+ }
30605
+
30606
+ // src/commands/translate/apply-core.ts
30872
30607
  function fullKey(namespace, key) {
30873
30608
  return namespace ? `${namespace}.${key}` : key;
30874
30609
  }
30610
+ async function applyTasks(input2) {
30611
+ const { root, scope, dstLocale } = input2;
30612
+ const format = formatOf(scope);
30613
+ const files = await listLocaleFiles(root, scope);
30614
+ const result = { written: [], refused: [] };
30615
+ const refuse = (task, reason) => result.refused.push({ key: fullKey(task.namespace, task.key), reason });
30616
+ const bySourcePath = /* @__PURE__ */ new Map();
30617
+ for (const task of input2.tasks) {
30618
+ if (task.status !== "succeeded" || !task.dst || !task.path) {
30619
+ refuse(task, "not_translated");
30620
+ continue;
30621
+ }
30622
+ const group = bySourcePath.get(task.path) ?? [];
30623
+ group.push(task);
30624
+ bySourcePath.set(task.path, group);
30625
+ }
30626
+ for (const [sourcePath, tasks] of bySourcePath) {
30627
+ const sourceFile = files.find(
30628
+ (file2) => file2.relativePath === sourcePath
30629
+ );
30630
+ const source = sourceFile ? await readTranslationFile(sourceFile.absolutePath, format) : void 0;
30631
+ if (!sourceFile || !source || source.error !== void 0) {
30632
+ for (const task of tasks) refuse(task, "no_source_file");
30633
+ continue;
30634
+ }
30635
+ const baseKeys = new Set(getKeys(source.data));
30636
+ const target = targetFileFor(sourceFile, dstLocale, files, scope);
30637
+ const targetPath = path8.join(root, ...target.relativePath.split("/"));
30638
+ const existing = target.existing ? await readTranslationFile(target.existing.absolutePath, format) : void 0;
30639
+ if (existing?.error !== void 0) {
30640
+ for (const task of tasks) refuse(task, "target_unreadable");
30641
+ continue;
30642
+ }
30643
+ let data = existing?.data ?? {};
30644
+ const written = {
30645
+ pathTemplate: sourceFile.pathTemplate,
30646
+ relativePath: target.relativePath,
30647
+ entries: []
30648
+ };
30649
+ for (const task of tasks) {
30650
+ const sourceKey = sourceKeyFor(task.key, baseKeys);
30651
+ const currentSource = sourceKey === null ? void 0 : getByPath(source.data, sourceKey);
30652
+ if (typeof currentSource !== "string" || computeValueHash(currentSource) !== computeValueHash(task.src)) {
30653
+ refuse(task, "source_changed");
30654
+ continue;
30655
+ }
30656
+ const translatedValue = task.dst ?? "";
30657
+ const current = getByPath(data, task.key);
30658
+ const currentHash = typeof current === "string" ? computeValueHash(current) : null;
30659
+ if (task.expectedTargetHash !== void 0 && task.expectedTargetHash !== currentHash) {
30660
+ refuse(
30661
+ task,
30662
+ current === translatedValue ? "already_applied" : "target_changed"
30663
+ );
30664
+ continue;
30665
+ }
30666
+ data = setByPath(data, task.key, translatedValue);
30667
+ written.entries.push({
30668
+ key: task.key,
30669
+ namespace: task.namespace,
30670
+ baseValue: task.src,
30671
+ translatedValue,
30672
+ changesFile: current !== translatedValue
30673
+ });
30674
+ }
30675
+ if (written.entries.length === 0) continue;
30676
+ result.written.push(written);
30677
+ if (input2.dryRun) continue;
30678
+ await writeTranslationFile({
30679
+ format,
30680
+ targetPath,
30681
+ data,
30682
+ dstLocale,
30683
+ existingContent: existing?.content,
30684
+ sourceContent: source.content
30685
+ });
30686
+ }
30687
+ return result;
30688
+ }
30689
+ async function writeTranslationFile(input2) {
30690
+ const { targetPath, data } = input2;
30691
+ const model = input2.existingContent ?? input2.sourceContent;
30692
+ await fs7.mkdir(path8.dirname(targetPath), { recursive: true });
30693
+ if (input2.format === "json") {
30694
+ await fs7.writeFile(
30695
+ targetPath,
30696
+ serializeJsonContent(data, detectJsonStyle(model)),
30697
+ "utf-8"
30698
+ );
30699
+ return;
30700
+ }
30701
+ const style = detectTsStyle(model);
30702
+ const shape = detectTsExportFormat(model);
30703
+ const content = serializeTsContent(data, {
30704
+ ...style,
30705
+ printWidth: style.printWidth ?? void 0,
30706
+ exportName: input2.existingContent ? shape?.exportName ?? void 0 : localeExportIdentifier(input2.dstLocale),
30707
+ useDefaultExport: shape?.useDefaultExport ?? false
30708
+ });
30709
+ await fs7.writeFile(targetPath, content, "utf-8");
30710
+ }
30711
+
30712
+ // src/commands/translate/plan-core.ts
30713
+ function fullKey2(namespace, key) {
30714
+ return namespace ? `${namespace}.${key}` : key;
30715
+ }
30875
30716
  function expectedFullKeys(sources, targetLocale) {
30876
30717
  const keys = [];
30877
30718
  for (const { file: file2, data } of sources) {
@@ -30879,7 +30720,7 @@ function expectedFullKeys(sources, targetLocale) {
30879
30720
  getKeys(data),
30880
30721
  targetLocale
30881
30722
  ).keys())
30882
- keys.push(fullKey(file2.namespace, key));
30723
+ keys.push(fullKey2(file2.namespace, key));
30883
30724
  }
30884
30725
  return keys;
30885
30726
  }
@@ -30927,7 +30768,7 @@ function planLocale(input2) {
30927
30768
  const src = getByPath(data, sourceKey);
30928
30769
  if (typeof src !== "string") continue;
30929
30770
  plan.summary.totalKeys++;
30930
- const full = fullKey(file2.namespace, key);
30771
+ const full = fullKey2(file2.namespace, key);
30931
30772
  const hold = input2.holds.get(full);
30932
30773
  const current = getByPath(targetData, key);
30933
30774
  const targetValue = typeof current === "string" ? current : void 0;
@@ -30998,74 +30839,8 @@ function planLocale(input2) {
30998
30839
  return plan;
30999
30840
  }
31000
30841
 
31001
- // src/commands/translate/project-files.ts
31002
- import fs6 from "fs/promises";
31003
- import path6 from "path";
31004
- function fullPattern(scope) {
31005
- return scope.subpath ? path6.posix.join(scope.subpath, scope.sourcePattern) : scope.sourcePattern;
31006
- }
31007
- function formatOf(scope) {
31008
- return detectFormat(scope.sourcePattern) ?? "json";
31009
- }
31010
- async function listLocaleFiles(root, scope) {
31011
- const pattern = fullPattern(scope);
31012
- const adapter = createAdapter(formatOf(scope));
31013
- const match = createPathMatcher(pattern);
31014
- const found = await adapter.findFiles(root, withNamespaceWildcard(pattern));
31015
- const files = [];
31016
- for (const absolutePath of found.sort()) {
31017
- const relativePath = path6.relative(root, absolutePath).split(path6.sep).join("/");
31018
- const m = match(relativePath);
31019
- if (!m) continue;
31020
- files.push({
31021
- pathTemplate: replaceNamespacePlaceholder(
31022
- scope.sourcePattern,
31023
- m.namespace ?? ""
31024
- ),
31025
- namespace: m.namespace,
31026
- locale: m.locale,
31027
- relativePath,
31028
- absolutePath
31029
- });
31030
- }
31031
- return files;
31032
- }
31033
- function filesForLocale(files, locale) {
31034
- return files.filter((file2) => sameLocale(file2.locale, locale));
31035
- }
31036
- function targetFileFor(source, targetLocale, files, scope) {
31037
- const existing = files.find(
31038
- (file2) => file2.pathTemplate === source.pathTemplate && sameLocale(file2.locale, targetLocale)
31039
- ) ?? null;
31040
- if (existing) return { relativePath: existing.relativePath, existing };
31041
- const spelling = files.find((file2) => sameLocale(file2.locale, targetLocale))?.locale ?? scope.detectedLocales?.find((l) => sameLocale(l, targetLocale)) ?? targetLocale;
31042
- const relativePath = replaceLocalePlaceholder(
31043
- replaceNamespacePlaceholder(fullPattern(scope), source.namespace ?? ""),
31044
- spelling
31045
- );
31046
- return { relativePath, existing: null };
31047
- }
31048
- async function readTranslationFile(absolutePath, format) {
31049
- try {
31050
- const content = await fs6.readFile(absolutePath, "utf-8");
31051
- const file2 = await createAdapter(format).parse(absolutePath);
31052
- return { data: file2.data, content };
31053
- } catch (error62) {
31054
- return {
31055
- error: error62 instanceof Error ? error62.message : String(error62)
31056
- };
31057
- }
31058
- }
31059
-
31060
30842
  // src/commands/translate/task-generator.ts
31061
30843
  var HOLD_CHUNK = 2e3;
31062
- async function loadTasksFromFile(tasksPath) {
31063
- const abs = path7.resolve(process.cwd(), tasksPath);
31064
- const raw = await fs7.readFile(abs, "utf8");
31065
- const parsed = JSON.parse(raw);
31066
- if (!Array.isArray(parsed)) throw new Error("tasks file must be an array");
31067
- return parsed;
31068
- }
31069
30844
  function scopeOf(project) {
31070
30845
  return {
31071
30846
  sourcePattern: project.sourcePattern,
@@ -31150,364 +30925,16 @@ async function generateTasks(ctx, srcLocale, dstLocale, opts = {}) {
31150
30925
  };
31151
30926
  }
31152
30927
 
31153
- // src/commands/translate/plan.tsx
31154
- import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
31155
- async function loadPlanContext(cwd) {
31156
- const config3 = readConfig(cwd);
31157
- if (!config3) throw noConfigError();
31158
- const client3 = await getAuthenticatedConvexClient();
31159
- const project = await client3.query(api.projects.getProjectForCli, {
31160
- projectId: config3.config.projectId
31161
- });
31162
- if (!project) throw projectNotFoundError();
31163
- setTelemetryOrganization(project.organizationId);
31164
- return {
31165
- root: path8.dirname(config3.path),
31166
- project,
31167
- client: client3,
31168
- framework: config3.config.framework
31169
- };
31170
- }
31171
- function describePlan(plan) {
31172
- const { summary } = plan;
31173
- const lines = [
31174
- `${summary.totalKeys} keys expected, ${summary.upToDate} up to date`
31175
- ];
31176
- if (summary.missing > 0) lines.push(`${summary.missing} missing`);
31177
- if (summary.changed > 0)
31178
- lines.push(`${summary.changed} changed since translated`);
31179
- const protectedCount = plan.protectedKeys.length;
31180
- if (protectedCount > 0) {
31181
- const reasons = Object.entries(summary.protected).map(([reason, n]) => `${n} ${reason}`).join(", ");
31182
- lines.push(
31183
- `${protectedCount} changed but kept for a person (${reasons})`
31184
- );
31185
- }
31186
- if (summary.held > 0)
31187
- lines.push(`${summary.held} missing but held by a person`);
31188
- if (summary.untracked > 0) {
31189
- lines.push(
31190
- plan.tasks.some((t) => t.reason === "untracked") ? `${summary.untracked} untracked, being retranslated` : `${summary.untracked} untracked translations kept: TolkBot has no record of them. Run "tolkbot translate adopt" to keep them as reviewed, or plan with --retranslate-untracked to replace them.`
31191
- );
31192
- }
31193
- return lines;
31194
- }
31195
- function PlanUI({
31196
- state,
31197
- src,
31198
- dst
31199
- }) {
31200
- if (state.type === "loading") {
31201
- return /* @__PURE__ */ jsxs15(Box15, { children: [
31202
- /* @__PURE__ */ jsx15(Spinner13, {}),
31203
- /* @__PURE__ */ jsx15(Text15, { children: " Loading configuration\u2026" })
31204
- ] });
31205
- }
31206
- if (state.type === "planning") {
31207
- return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
31208
- /* @__PURE__ */ jsxs15(Box15, { children: [
31209
- /* @__PURE__ */ jsx15(Spinner13, {}),
31210
- /* @__PURE__ */ jsx15(Text15, { children: " Analyzing translation needs\u2026" })
31211
- ] }),
31212
- /* @__PURE__ */ jsx15(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text15, { dimColor: true, children: [
31213
- src,
31214
- " \u2192 ",
31215
- dst
31216
- ] }) })
31217
- ] });
31218
- }
31219
- const { plan } = state;
31220
- return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
31221
- /* @__PURE__ */ jsxs15(Box15, { children: [
31222
- /* @__PURE__ */ jsx15(Text15, { color: "green", children: "\u2713" }),
31223
- /* @__PURE__ */ jsx15(Text15, { children: " Translation plan generated" })
31224
- ] }),
31225
- /* @__PURE__ */ jsx15(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text15, { dimColor: true, children: [
31226
- src,
31227
- " \u2192 ",
31228
- dst,
31229
- ": ",
31230
- plan.tasks.length,
31231
- " task(s)"
31232
- ] }) }),
31233
- describePlan(plan).map((line) => /* @__PURE__ */ jsx15(Box15, { children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: line }) }, line)),
31234
- plan.tasks.length === 0 && /* @__PURE__ */ jsx15(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text15, { color: "yellow", children: [
31235
- "No translations needed - ",
31236
- dst,
31237
- " is up to date."
31238
- ] }) })
31239
- ] });
31240
- }
31241
- function planOutput(plan) {
31242
- return {
31243
- tasks: plan.tasks,
31244
- summary: plan.summary,
31245
- protectedKeys: plan.protectedKeys,
31246
- heldKeys: plan.heldKeys,
31247
- untrackedKeys: plan.untrackedKeys.map((k) => k.key)
31248
- };
31249
- }
31250
- async function runPlan(opts) {
31251
- const cwd = process.cwd();
31252
- if (opts.json) {
31253
- const ctx = await loadPlanContext(cwd);
31254
- const src2 = opts.src ?? ctx.project.baseLanguage;
31255
- const plan2 = await planForLocale(ctx, src2, opts.dst, {
31256
- retranslateUntracked: opts.retranslateUntracked
31257
- });
31258
- track("cli_translate_completed", {
31259
- mode: "plan",
31260
- key_count: plan2.tasks.length,
31261
- locale_count: 1,
31262
- job_count: 0,
31263
- json: true
31264
- });
31265
- if (opts.write) {
31266
- const outputPath = await writeTasks(cwd, opts.write, plan2);
31267
- jsonSuccess({ ...planOutput(plan2), writtenTo: outputPath });
31268
- }
31269
- jsonSuccess(planOutput(plan2));
31270
- }
31271
- let src = opts.src ?? "";
31272
- const progress = renderTransient(
31273
- /* @__PURE__ */ jsx15(PlanUI, { state: { type: "loading" }, src, dst: opts.dst })
31274
- );
31275
- let plan;
31276
- try {
31277
- const ctx = await loadPlanContext(cwd);
31278
- src = opts.src ?? ctx.project.baseLanguage;
31279
- progress.rerender(
31280
- /* @__PURE__ */ jsx15(PlanUI, { state: { type: "planning" }, src, dst: opts.dst })
31281
- );
31282
- plan = await planForLocale(ctx, src, opts.dst, {
31283
- retranslateUntracked: opts.retranslateUntracked
31284
- });
31285
- } finally {
31286
- progress.unmount();
31287
- }
31288
- render15(
31289
- /* @__PURE__ */ jsx15(PlanUI, { state: { type: "success", plan }, src, dst: opts.dst })
31290
- ).unmount();
31291
- track("cli_translate_completed", {
31292
- mode: "plan",
31293
- key_count: plan.tasks.length,
31294
- locale_count: 1,
31295
- job_count: 0,
31296
- json: false
31297
- });
31298
- if (opts.write) {
31299
- const outputPath = await writeTasks(cwd, opts.write, plan);
31300
- console.log(`
31301
- Tasks written to: ${outputPath}`);
31302
- } else {
31303
- console.log("\n" + JSON.stringify(plan.tasks, null, 2));
31304
- }
31305
- }
31306
- async function writeTasks(cwd, file2, plan) {
31307
- const outputPath = path8.resolve(cwd, file2);
31308
- await fs8.writeFile(
31309
- outputPath,
31310
- JSON.stringify(plan.tasks, null, 2) + "\n",
31311
- "utf-8"
31312
- );
31313
- return outputPath;
31314
- }
31315
-
31316
- // src/commands/translate/adopt.tsx
30928
+ // src/commands/jobs/apply-job.ts
31317
30929
  var STAMP_CHUNK = 200;
31318
- async function adoptLocale(ctx, srcLocale, dstLocale) {
31319
- const plan = await planForLocale(ctx, srcLocale, dstLocale);
31320
- const locale = governanceLocale(ctx.project, dstLocale);
31321
- const byTemplate = /* @__PURE__ */ new Map();
31322
- for (const key of plan.untrackedKeys) {
31323
- const group = byTemplate.get(key.pathTemplate) ?? [];
31324
- group.push(key);
31325
- byTemplate.set(key.pathTemplate, group);
31326
- }
31327
- const outcome = {
31328
- dstLocale,
31329
- untracked: plan.untrackedKeys.length,
31330
- adopted: 0,
31331
- skipped: 0
31332
- };
31333
- for (const [pathTemplate, keys] of byTemplate) {
31334
- for (let i = 0; i < keys.length; i += STAMP_CHUNK) {
31335
- const result = await ctx.client.mutation(
31336
- api.localeKeyStates.adoptUntrackedForCli,
31337
- {
31338
- organizationId: ctx.project.organizationId,
31339
- projectId: ctx.project._id,
31340
- locale,
31341
- pathTemplate,
31342
- entries: keys.slice(i, i + STAMP_CHUNK).map((key) => ({
31343
- key: key.fileKey,
31344
- namespace: key.namespace,
31345
- baseValue: key.src,
31346
- translatedValue: key.dst
31347
- }))
31348
- }
31349
- );
31350
- outcome.adopted += result.adopted;
31351
- outcome.skipped += result.skipped;
31352
- }
31353
- }
31354
- return outcome;
31355
- }
31356
- async function runAdopt(opts) {
31357
- const run = async () => {
31358
- const ctx = await loadPlanContext(process.cwd());
31359
- const src = opts.src ?? ctx.project.baseLanguage;
31360
- const locales = opts.all ? ctx.project.targetLanguages : opts.dst ? [opts.dst] : [];
31361
- if (locales.length === 0) {
31362
- throw opts.all ? new CliError(
31363
- "No target languages configured for this project.",
31364
- void 0,
31365
- "no_target_languages"
31366
- ) : new UsageError(
31367
- "Either --dst <locale> or --all must be provided."
31368
- );
31369
- }
31370
- const outcomes2 = [];
31371
- for (const dst of locales)
31372
- outcomes2.push(await adoptLocale(ctx, src, dst));
31373
- track("cli_translate_completed", {
31374
- mode: "adopt",
31375
- key_count: outcomes2.reduce((n, o) => n + o.adopted, 0),
31376
- locale_count: outcomes2.length,
31377
- job_count: 0,
31378
- json: opts.json === true
31379
- });
31380
- return outcomes2;
31381
- };
31382
- if (opts.json) {
31383
- jsonSuccess({ adopted: await run() });
31384
- }
31385
- const outcomes = await run();
31386
- for (const o of outcomes) {
31387
- console.log(
31388
- `\u2713 ${o.dstLocale}: adopted ${o.adopted} untracked translation(s)` + (o.skipped > 0 ? `, ${o.skipped} already tracked` : "")
31389
- );
31390
- }
31391
- }
31392
-
31393
- // src/commands/translate/apply.tsx
31394
- import { useState as useState2 } from "react";
31395
- import { MultiSelect } from "@inkjs/ui";
31396
- import { Box as Box16, render as render16, Text as Text16 } from "ink";
31397
-
31398
- // src/commands/translate/apply-core.ts
31399
- import fs9 from "fs/promises";
31400
- import path9 from "path";
31401
- function fullKey2(namespace, key) {
31402
- return namespace ? `${namespace}.${key}` : key;
31403
- }
31404
- async function applyTasks(input2) {
31405
- const { root, scope, dstLocale } = input2;
31406
- const format = formatOf(scope);
31407
- const files = await listLocaleFiles(root, scope);
31408
- const result = { written: [], refused: [] };
31409
- const refuse = (task, reason) => result.refused.push({ key: fullKey2(task.namespace, task.key), reason });
31410
- const bySourcePath = /* @__PURE__ */ new Map();
31411
- for (const task of input2.tasks) {
31412
- if (task.status !== "succeeded" || !task.dst || !task.path) {
31413
- refuse(task, "not_translated");
31414
- continue;
31415
- }
31416
- const group = bySourcePath.get(task.path) ?? [];
31417
- group.push(task);
31418
- bySourcePath.set(task.path, group);
31419
- }
31420
- for (const [sourcePath, tasks] of bySourcePath) {
31421
- const sourceFile = files.find(
31422
- (file2) => file2.relativePath === sourcePath
31423
- );
31424
- const source = sourceFile ? await readTranslationFile(sourceFile.absolutePath, format) : void 0;
31425
- if (!sourceFile || !source || source.error !== void 0) {
31426
- for (const task of tasks) refuse(task, "no_source_file");
31427
- continue;
31428
- }
31429
- const baseKeys = new Set(getKeys(source.data));
31430
- const target = targetFileFor(sourceFile, dstLocale, files, scope);
31431
- const targetPath = path9.join(root, ...target.relativePath.split("/"));
31432
- const existing = target.existing ? await readTranslationFile(target.existing.absolutePath, format) : void 0;
31433
- if (existing?.error !== void 0) {
31434
- for (const task of tasks) refuse(task, "target_unreadable");
31435
- continue;
31436
- }
31437
- let data = existing?.data ?? {};
31438
- const written = {
31439
- pathTemplate: sourceFile.pathTemplate,
31440
- relativePath: target.relativePath,
31441
- entries: []
31442
- };
31443
- for (const task of tasks) {
31444
- const sourceKey = sourceKeyFor(task.key, baseKeys);
31445
- const currentSource = sourceKey === null ? void 0 : getByPath(source.data, sourceKey);
31446
- if (typeof currentSource !== "string" || computeValueHash(currentSource) !== computeValueHash(task.src)) {
31447
- refuse(task, "source_changed");
31448
- continue;
31449
- }
31450
- const translatedValue = task.dst ?? "";
31451
- const current = getByPath(data, task.key);
31452
- const currentHash = typeof current === "string" ? computeValueHash(current) : null;
31453
- if (task.expectedTargetHash !== void 0 && task.expectedTargetHash !== currentHash) {
31454
- refuse(
31455
- task,
31456
- current === translatedValue ? "already_applied" : "target_changed"
31457
- );
31458
- continue;
31459
- }
31460
- data = setByPath(data, task.key, translatedValue);
31461
- written.entries.push({
31462
- key: task.key,
31463
- namespace: task.namespace,
31464
- baseValue: task.src,
31465
- translatedValue,
31466
- changesFile: current !== translatedValue
31467
- });
31468
- }
31469
- if (written.entries.length === 0) continue;
31470
- result.written.push(written);
31471
- if (input2.dryRun) continue;
31472
- await writeTranslationFile({
31473
- format,
31474
- targetPath,
31475
- data,
31476
- dstLocale,
31477
- existingContent: existing?.content,
31478
- sourceContent: source.content
31479
- });
31480
- }
31481
- return result;
31482
- }
31483
- async function writeTranslationFile(input2) {
31484
- const { targetPath, data } = input2;
31485
- const model = input2.existingContent ?? input2.sourceContent;
31486
- await fs9.mkdir(path9.dirname(targetPath), { recursive: true });
31487
- if (input2.format === "json") {
31488
- await fs9.writeFile(
31489
- targetPath,
31490
- serializeJsonContent(data, detectJsonStyle(model)),
31491
- "utf-8"
31492
- );
31493
- return;
31494
- }
31495
- const style = detectTsStyle(model);
31496
- const shape = detectTsExportFormat(model);
31497
- const content = serializeTsContent(data, {
31498
- ...style,
31499
- printWidth: style.printWidth ?? void 0,
31500
- exportName: input2.existingContent ? shape?.exportName ?? void 0 : localeExportIdentifier(input2.dstLocale),
31501
- useDefaultExport: shape?.useDefaultExport ?? false
31502
- });
31503
- await fs9.writeFile(targetPath, content, "utf-8");
31504
- }
31505
-
31506
- // src/commands/translate/apply.tsx
31507
- import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
31508
- var STAMP_CHUNK2 = 200;
31509
30930
  var RECENT_JOBS = 50;
31510
30931
  var APPLICABLE_STATUSES = /* @__PURE__ */ new Set(["succeeded", "partial", "failed"]);
30932
+ function isApplicable(status) {
30933
+ return APPLICABLE_STATUSES.has(status);
30934
+ }
30935
+ function isForWorkingTree(job) {
30936
+ return job.source === void 0 || job.source === "cli";
30937
+ }
31511
30938
  function fullKey3(namespace, key) {
31512
30939
  return namespace ? `${namespace}.${key}` : key;
31513
30940
  }
@@ -31518,17 +30945,17 @@ async function readJob(ctx, jobId) {
31518
30945
  organizationId: organizationId2,
31519
30946
  jobId
31520
30947
  });
31521
- if (!job) {
30948
+ if (job?.projectId !== project._id) {
31522
30949
  throw new CliError(
31523
30950
  `Translation job ${jobId} was not found in this project.`,
31524
- void 0,
30951
+ "List the project's jobs with 'tolkbot jobs list'.",
31525
30952
  "job_not_found"
31526
30953
  );
31527
30954
  }
31528
- if (!APPLICABLE_STATUSES.has(job.status)) {
30955
+ if (!isApplicable(job.status)) {
31529
30956
  throw new CliError(
31530
30957
  `Translation job ${jobId} is ${job.status}. Only a finished job can be applied.`,
31531
- `Check it with 'tolkbot translate status --job-id ${jobId}'.`,
30958
+ `Check it with 'tolkbot jobs show ${jobId}'.`,
31532
30959
  "job_not_finished"
31533
30960
  );
31534
30961
  }
@@ -31558,13 +30985,13 @@ async function applyJob(ctx, jobId) {
31558
30985
  translatedValue
31559
30986
  })
31560
30987
  );
31561
- for (let i = 0; i < entries.length; i += STAMP_CHUNK2) {
30988
+ for (let i = 0; i < entries.length; i += STAMP_CHUNK) {
31562
30989
  await client3.mutation(api.localeKeyStates.stampAppliedForCli, {
31563
30990
  organizationId: project.organizationId,
31564
30991
  projectId: project._id,
31565
30992
  locale,
31566
30993
  pathTemplate: file2.pathTemplate,
31567
- entries: entries.slice(i, i + STAMP_CHUNK2)
30994
+ entries: entries.slice(i, i + STAMP_CHUNK)
31568
30995
  });
31569
30996
  }
31570
30997
  }
@@ -31572,7 +30999,7 @@ async function applyJob(ctx, jobId) {
31572
30999
  dstLocale: job.dstLocale,
31573
31000
  files: result.written.length,
31574
31001
  keys: result.written.reduce((n, f) => n + f.entries.length, 0),
31575
- refused: result.refused.filter(
31002
+ notApplied: result.refused.filter(
31576
31003
  (r) => r.reason !== "not_translated" && r.reason !== "already_applied"
31577
31004
  )
31578
31005
  };
@@ -31585,18 +31012,13 @@ var REFUSAL_TEXT = {
31585
31012
  target_unreadable: "the target file cannot be read",
31586
31013
  target_changed: "the target was edited after it was planned"
31587
31014
  };
31588
- function describeRefusals(refused) {
31589
- return refused.map(
31015
+ function describeNotApplied(notApplied) {
31016
+ return notApplied.map(
31590
31017
  (r) => ` ${r.key}: not applied, ${REFUSAL_TEXT[r.reason]}`
31591
31018
  );
31592
31019
  }
31593
- async function findUnappliedJobs(ctx) {
31020
+ function pendingCounter(ctx) {
31594
31021
  const { client: client3, project } = ctx;
31595
- const jobs = await client3.query(api.translations.listJobsByProject, {
31596
- organizationId: project.organizationId,
31597
- projectId: project._id,
31598
- limit: RECENT_JOBS
31599
- });
31600
31022
  const wanted = /* @__PURE__ */ new Map();
31601
31023
  const plannedKeys = (srcLocale, dstLocale) => {
31602
31024
  const id = `${srcLocale}\0${dstLocale}`;
@@ -31613,9 +31035,7 @@ async function findUnappliedJobs(ctx) {
31613
31035
  }
31614
31036
  return keys;
31615
31037
  };
31616
- const unapplied = [];
31617
- for (const job of jobs) {
31618
- if (!APPLICABLE_STATUSES.has(job.status)) continue;
31038
+ return async (job) => {
31619
31039
  const tasks = await client3.query(api.translations.listTasksByJobId, {
31620
31040
  organizationId: project.organizationId,
31621
31041
  jobId: job._id
@@ -31638,6 +31058,23 @@ async function findUnappliedJobs(ctx) {
31638
31058
  }
31639
31059
  }
31640
31060
  }
31061
+ return pending2;
31062
+ };
31063
+ }
31064
+ async function listRecentJobs(ctx, limit) {
31065
+ return await ctx.client.query(api.translations.listJobsByProject, {
31066
+ organizationId: ctx.project.organizationId,
31067
+ projectId: ctx.project._id,
31068
+ limit
31069
+ });
31070
+ }
31071
+ async function findUnappliedJobs(ctx) {
31072
+ const jobs = await listRecentJobs(ctx, RECENT_JOBS);
31073
+ const pendingOf = pendingCounter(ctx);
31074
+ const unapplied = [];
31075
+ for (const job of jobs) {
31076
+ if (!isApplicable(job.status) || !isForWorkingTree(job)) continue;
31077
+ const pending2 = await pendingOf(job);
31641
31078
  if (pending2 === 0) continue;
31642
31079
  unapplied.push({
31643
31080
  jobId: job._id,
@@ -31664,13 +31101,16 @@ function chooseJobs(unapplied) {
31664
31101
  jobs: [...unapplied].sort((a, b) => a.createdAt - b.createdAt)
31665
31102
  };
31666
31103
  }
31104
+
31105
+ // src/commands/jobs/apply.tsx
31106
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
31667
31107
  function describeJob(job) {
31668
31108
  return `${job.jobId} (${job.srcLocale} \u2192 ${job.dstLocale}, ${job.pending} key(s), ${new Date(job.createdAt).toLocaleString()})`;
31669
31109
  }
31670
31110
  function nothingToApply() {
31671
31111
  return new CliError(
31672
31112
  "No finished translation job is waiting to be applied.",
31673
- "Run 'tolkbot translate run --all' to translate what is missing, or pass --job-id to apply a job again.",
31113
+ "Run 'tolkbot translate --all' to translate what is missing, or pass a job id to apply a job again.",
31674
31114
  "nothing_to_apply"
31675
31115
  );
31676
31116
  }
@@ -31681,7 +31121,7 @@ function ambiguousJobs(unapplied, locales) {
31681
31121
  );
31682
31122
  return new UsageError(
31683
31123
  `More than one translation job is waiting to be applied for ${locales.join(", ")}: ${waiting.map(describeJob).join("; ")}.`,
31684
- `Pass --job-id with the job(s) to apply. The newest is usually the one: --job-id ${newest.map((job) => job.jobId).join(" ")}.`
31124
+ `Pass the job id(s) to apply. The newest is usually the one: 'tolkbot jobs apply ${newest.map((job) => job.jobId).join(" ")}'.`
31685
31125
  );
31686
31126
  }
31687
31127
  function JobPicker({
@@ -31689,10 +31129,10 @@ function JobPicker({
31689
31129
  onSubmit
31690
31130
  }) {
31691
31131
  const [selected, setSelected] = useState2([]);
31692
- return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
31693
- /* @__PURE__ */ jsx16(Text16, { bold: true, children: "Select translation jobs to apply:" }),
31694
- /* @__PURE__ */ jsx16(Text16, { dimColor: true, children: "Space to select, Enter to confirm, Ctrl+C to cancel" }),
31695
- /* @__PURE__ */ jsx16(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx16(
31132
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
31133
+ /* @__PURE__ */ jsx11(Text11, { bold: true, children: "Select translation jobs to apply:" }),
31134
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "Space to select, Enter to confirm, Ctrl+C to cancel" }),
31135
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(
31696
31136
  MultiSelect,
31697
31137
  {
31698
31138
  options: jobs.map((job) => ({
@@ -31707,8 +31147,8 @@ function JobPicker({
31707
31147
  }
31708
31148
  async function pickJobs(jobs) {
31709
31149
  let chosen = [];
31710
- const ui = render16(
31711
- /* @__PURE__ */ jsx16(
31150
+ const ui = render11(
31151
+ /* @__PURE__ */ jsx11(
31712
31152
  JobPicker,
31713
31153
  {
31714
31154
  jobs,
@@ -31734,15 +31174,15 @@ async function resolveJobIds(ctx, json2) {
31734
31174
  if (picked.length === 0) {
31735
31175
  throw new UsageError(
31736
31176
  "No job selected.",
31737
- "Pass --job-id with the job(s) to apply."
31177
+ "Pass the job id(s) to apply: 'tolkbot jobs apply <id\u2026>'."
31738
31178
  );
31739
31179
  }
31740
31180
  return picked;
31741
31181
  }
31742
- async function runApply(opts) {
31182
+ async function runJobsApply(opts) {
31743
31183
  const ctx = await loadPlanContext(process.cwd());
31744
31184
  const json2 = opts.json ?? false;
31745
- const jobIds = opts.jobId && opts.jobId.length > 0 ? opts.jobId : await resolveJobIds(ctx, json2);
31185
+ const jobIds = opts.jobIds.length > 0 ? opts.jobIds : await resolveJobIds(ctx, json2);
31746
31186
  if (json2) {
31747
31187
  const applied = [];
31748
31188
  for (const jobId of jobIds) {
@@ -31760,16 +31200,19 @@ async function runApply(opts) {
31760
31200
  console.log(`Applying ${jobIds.length} job(s)...`);
31761
31201
  let totalFiles = 0;
31762
31202
  let totalKeys = 0;
31763
- let totalRefused = 0;
31203
+ let totalNotApplied = 0;
31764
31204
  const locales = /* @__PURE__ */ new Set();
31765
31205
  for (const [i, jobId] of jobIds.entries()) {
31766
31206
  console.log(` [${i + 1}/${jobIds.length}] ${jobId}`);
31767
- const { dstLocale, files, keys, refused } = await applyJob(ctx, jobId);
31207
+ const { dstLocale, files, keys, notApplied } = await applyJob(
31208
+ ctx,
31209
+ jobId
31210
+ );
31768
31211
  locales.add(dstLocale);
31769
31212
  totalFiles += files;
31770
31213
  totalKeys += keys;
31771
- totalRefused += refused.length;
31772
- for (const line of describeRefusals(refused)) console.log(line);
31214
+ totalNotApplied += notApplied.length;
31215
+ for (const line of describeNotApplied(notApplied)) console.log(line);
31773
31216
  }
31774
31217
  track("cli_translate_completed", {
31775
31218
  mode: "apply",
@@ -31779,187 +31222,89 @@ async function runApply(opts) {
31779
31222
  json: false
31780
31223
  });
31781
31224
  console.log(
31782
- `\u2713 Applied ${totalKeys} keys to ${totalFiles} files from ${jobIds.length} job(s)` + (totalRefused > 0 ? `, ${totalRefused} not applied` : "")
31225
+ `\u2713 Applied ${totalKeys} keys to ${totalFiles} files from ${jobIds.length} job(s)` + (totalNotApplied > 0 ? `, ${totalNotApplied} not applied` : "")
31783
31226
  );
31784
31227
  }
31785
31228
 
31786
- // src/commands/translate/cancel.tsx
31787
- import { Spinner as Spinner14 } from "@inkjs/ui";
31788
- import { Box as Box17, render as render17, Text as Text17 } from "ink";
31789
- import { jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
31790
- function CancelUI({ state }) {
31791
- if (state.type === "cancelling") {
31792
- return /* @__PURE__ */ jsxs17(Box17, { children: [
31793
- /* @__PURE__ */ jsx17(Spinner14, {}),
31794
- /* @__PURE__ */ jsx17(Text17, { children: " Cancelling job\u2026" })
31795
- ] });
31796
- }
31797
- return /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
31798
- /* @__PURE__ */ jsxs17(Box17, { children: [
31799
- /* @__PURE__ */ jsx17(Text17, { color: "green", children: "\u2713" }),
31800
- /* @__PURE__ */ jsx17(Text17, { children: " Job cancelled successfully" })
31801
- ] }),
31802
- /* @__PURE__ */ jsx17(Box17, { marginTop: 1, children: /* @__PURE__ */ jsxs17(Text17, { dimColor: true, children: [
31803
- "Job ID: ",
31804
- state.jobId
31805
- ] }) })
31806
- ] });
31807
- }
31808
- async function cancelJob(jobId) {
31229
+ // src/commands/jobs/cancel.ts
31230
+ async function runJobsCancel(opts) {
31809
31231
  const { project } = await loadPlanContext(process.cwd());
31810
31232
  const client3 = await getAuthenticatedConvexClient();
31811
31233
  await client3.action(api.translations.cancelJob, {
31812
31234
  organizationId: project.organizationId,
31813
- jobId
31235
+ jobId: opts.jobId
31814
31236
  });
31815
- }
31816
- async function runCancel(opts) {
31817
- if (opts.json) {
31818
- await cancelJob(opts.jobId);
31819
- jsonSuccess({ jobId: opts.jobId, ok: true });
31820
- }
31821
- const { unmount } = renderTransient(
31822
- /* @__PURE__ */ jsx17(CancelUI, { state: { type: "cancelling" } })
31823
- );
31824
- try {
31825
- await cancelJob(opts.jobId);
31826
- } finally {
31827
- unmount();
31828
- }
31829
- render17(
31830
- /* @__PURE__ */ jsx17(CancelUI, { state: { type: "success", jobId: opts.jobId } })
31831
- ).unmount();
31237
+ if (opts.json) jsonSuccess({ jobId: opts.jobId, ok: true });
31238
+ console.log(`\u2713 Cancelled job ${opts.jobId}`);
31832
31239
  }
31833
31240
 
31834
- // src/commands/translate/edit.tsx
31835
- import path11 from "path";
31836
- import { Spinner as Spinner15 } from "@inkjs/ui";
31837
- import { Box as Box18, render as render18, Text as Text18 } from "ink";
31838
-
31839
- // src/utils/locale-files.ts
31840
- import path10 from "path";
31841
- async function findLocaleFiles(baseDir) {
31842
- const adapter = createAdapter("json");
31843
- const result = await adapter.discover(baseDir);
31844
- const localeFiles = /* @__PURE__ */ new Map();
31845
- for (const [locale, files] of result.filesByLocale) {
31846
- localeFiles.set(
31847
- locale,
31848
- files.map((f) => f.path)
31849
- );
31850
- }
31851
- return localeFiles;
31852
- }
31853
- function resolveFileForKey(files, key, namespace) {
31854
- if (files.length === 0) return null;
31855
- if (files.length === 1) return files[0] ?? null;
31856
- const ns = namespace ?? key.split(".")[0];
31857
- for (const file2 of files) {
31858
- const fileName = path10.parse(path10.basename(file2)).name;
31859
- if (fileName === ns) {
31860
- return file2;
31861
- }
31862
- }
31863
- return files[0] ?? null;
31864
- }
31865
- function resolveKeyInFile(key, targetFile, fileCount, namespace) {
31866
- if (fileCount <= 1 || namespace) return key;
31867
- const fileName = path10.parse(path10.basename(targetFile)).name;
31868
- if (key.startsWith(`${fileName}.`)) {
31869
- return key.slice(fileName.length + 1);
31870
- }
31871
- return key;
31872
- }
31873
-
31874
- // src/commands/translate/edit.tsx
31875
- import { jsx as jsx18, jsxs as jsxs18 } from "react/jsx-runtime";
31876
- function EditUI({ state }) {
31877
- if (state.type === "editing") {
31878
- return /* @__PURE__ */ jsxs18(Box18, { children: [
31879
- /* @__PURE__ */ jsx18(Spinner15, {}),
31880
- /* @__PURE__ */ jsx18(Text18, { children: " Editing translation\u2026" })
31881
- ] });
31241
+ // src/commands/jobs/list.ts
31242
+ async function describeJobs(ctx, jobs) {
31243
+ const pendingOf = pendingCounter(ctx);
31244
+ const listed = [];
31245
+ for (const job of jobs) {
31246
+ let applied = "none";
31247
+ let pending2 = 0;
31248
+ if (!isForWorkingTree(job)) {
31249
+ } else if (job.status === "pending" || job.status === "running") {
31250
+ applied = "running";
31251
+ } else if (isApplicable(job.status)) {
31252
+ pending2 = await pendingOf(job);
31253
+ applied = pending2 > 0 ? "waiting" : "applied";
31254
+ }
31255
+ listed.push({
31256
+ jobId: job._id,
31257
+ srcLocale: job.srcLocale,
31258
+ dstLocale: job.dstLocale,
31259
+ status: job.status,
31260
+ source: job.source ?? null,
31261
+ createdAt: new Date(job._creationTime).toISOString(),
31262
+ total: job.total,
31263
+ completed: job.completed,
31264
+ failed: job.failed,
31265
+ creditsUsed: job.creditsUsed,
31266
+ applied,
31267
+ pending: pending2
31268
+ });
31882
31269
  }
31883
- return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
31884
- /* @__PURE__ */ jsxs18(Box18, { children: [
31885
- /* @__PURE__ */ jsx18(Text18, { color: "green", children: "\u2713" }),
31886
- /* @__PURE__ */ jsxs18(Text18, { children: [
31887
- " ",
31888
- "Updated ",
31889
- /* @__PURE__ */ jsx18(Text18, { bold: true, children: state.key })
31890
- ] })
31891
- ] }),
31892
- /* @__PURE__ */ jsx18(Box18, { paddingLeft: 2, children: /* @__PURE__ */ jsx18(Text18, { dimColor: true, children: state.value }) }),
31893
- /* @__PURE__ */ jsx18(Box18, { marginTop: 1, children: /* @__PURE__ */ jsxs18(Text18, { dimColor: true, children: [
31894
- "File: ",
31895
- state.filePath
31896
- ] }) })
31897
- ] });
31270
+ return listed;
31898
31271
  }
31899
- async function performEdit(opts) {
31900
- const baseDir = process.cwd();
31901
- const config3 = readConfig(baseDir);
31902
- if (!config3) throw noConfigError();
31903
- const localeFiles = await findLocaleFiles(baseDir);
31904
- const files = localeFiles.get(opts.locale);
31905
- if (!files || files.length === 0) {
31906
- throw new CliError(
31907
- `No translation files found for locale "${opts.locale}".`,
31908
- `Available locales: ${[...localeFiles.keys()].join(", ") || "none"}.`,
31909
- "no_translation_files"
31910
- );
31911
- }
31912
- const targetFile = resolveFileForKey(files, opts.key, opts.namespace);
31913
- if (!targetFile) {
31914
- throw new CliError(
31915
- `Could not determine target file for key "${opts.key}" in locale "${opts.locale}".`,
31916
- "Pass --namespace to say which file the key belongs to."
31272
+ var APPLIED_TEXT = {
31273
+ applied: "applied",
31274
+ waiting: "waiting to apply",
31275
+ running: "",
31276
+ none: ""
31277
+ };
31278
+ function formatRow(job) {
31279
+ const when = job.createdAt.slice(0, 16).replace("T", " ");
31280
+ const progress = `${job.completed}/${job.total}`;
31281
+ const applied = job.applied === "waiting" ? `${APPLIED_TEXT.waiting} (${job.pending})` : APPLIED_TEXT[job.applied];
31282
+ return [
31283
+ job.jobId,
31284
+ when,
31285
+ `${job.srcLocale} \u2192 ${job.dstLocale}`.padEnd(14),
31286
+ job.status.padEnd(9),
31287
+ progress.padStart(9),
31288
+ (job.source ?? "cli").padEnd(9),
31289
+ applied
31290
+ ].join(" ").trimEnd();
31291
+ }
31292
+ async function runJobsList(opts) {
31293
+ const ctx = await loadPlanContext(process.cwd());
31294
+ const jobs = await describeJobs(ctx, await listRecentJobs(ctx, opts.limit));
31295
+ if (opts.json) jsonSuccess({ jobs });
31296
+ if (jobs.length === 0) {
31297
+ console.log(
31298
+ "No translation jobs yet. Run 'tolkbot translate --all' to start one."
31917
31299
  );
31300
+ return;
31918
31301
  }
31919
- const keyInFile = resolveKeyInFile(
31920
- opts.key,
31921
- targetFile,
31922
- files.length,
31923
- opts.namespace
31924
- );
31925
- const adapter = createAdapter("json");
31926
- let existingData = {};
31927
- try {
31928
- const parsed = await adapter.parse(targetFile);
31929
- existingData = parsed.data;
31930
- } catch {
31931
- }
31932
- const updatedData = setByPath(
31933
- existingData,
31934
- keyInFile,
31935
- opts.value
31936
- );
31937
- await adapter.serialize(targetFile, updatedData, { merge: false });
31938
- const relativePath = path11.relative(baseDir, targetFile);
31939
- return { filePath: relativePath, key: opts.key, value: opts.value };
31940
- }
31941
- async function runEdit(opts) {
31942
- if (opts.json) {
31943
- const result2 = await performEdit(opts);
31944
- jsonSuccess({
31945
- file: result2.filePath,
31946
- key: result2.key,
31947
- value: result2.value
31948
- });
31949
- }
31950
- const { unmount } = renderTransient(/* @__PURE__ */ jsx18(EditUI, { state: { type: "editing" } }));
31951
- let result;
31952
- try {
31953
- result = await performEdit(opts);
31954
- } finally {
31955
- unmount();
31302
+ for (const job of jobs) console.log(formatRow(job));
31303
+ if (jobs.some((job) => job.applied === "waiting")) {
31304
+ console.log("\nApply the waiting ones with 'tolkbot jobs apply'.");
31956
31305
  }
31957
- render18(/* @__PURE__ */ jsx18(EditUI, { state: { type: "success", ...result } })).unmount();
31958
31306
  }
31959
31307
 
31960
- // src/commands/translate/run.tsx
31961
- import { render as render19 } from "ink";
31962
-
31963
31308
  // ../shared/src/conditions/index.ts
31964
31309
  var BILLING_BLOCKED = "BILLING_BLOCKED";
31965
31310
 
@@ -32061,10 +31406,10 @@ function serverMessage(message) {
32061
31406
  }
32062
31407
  function argumentError(message) {
32063
31408
  if (!message.includes("ArgumentValidationError")) return null;
32064
- const path12 = /Path: \.?(\S+)/.exec(message)?.[1];
31409
+ const path11 = /Path: \.?(\S+)/.exec(message)?.[1];
32065
31410
  const value = /Value: (.+)/.exec(message)?.[1];
32066
31411
  return new UsageError(
32067
- path12 && value ? `TolkBot refused ${value} as ${path12}.` : "TolkBot refused the arguments.",
31412
+ path11 && value ? `TolkBot refused ${value} as ${path11}.` : "TolkBot refused the arguments.",
32068
31413
  "Check the id or value passed; list ids with the command's list subcommand."
32069
31414
  );
32070
31415
  }
@@ -32075,694 +31420,1193 @@ function toCliError(error62) {
32075
31420
  const known = fromConvexError(convex.data);
32076
31421
  if (known) return known;
32077
31422
  }
32078
- if (isUnauthenticatedError(error62)) {
32079
- return new CliError(
32080
- "Not authenticated.",
32081
- "Run 'tolkbot auth login', or set $TOLKBOT_TOKEN for CI and headless environments.",
32082
- "not_authenticated"
31423
+ if (isUnauthenticatedError(error62)) {
31424
+ return new CliError(
31425
+ "Not authenticated.",
31426
+ "Run 'tolkbot auth login', or set $TOLKBOT_TOKEN for CI and headless environments.",
31427
+ "not_authenticated"
31428
+ );
31429
+ }
31430
+ if (isNetworkError(error62)) {
31431
+ return new CliError(
31432
+ "Could not reach TolkBot.",
31433
+ "Check the network connection and try again.",
31434
+ "network"
31435
+ );
31436
+ }
31437
+ const message = describeError(error62);
31438
+ const refused = argumentError(message);
31439
+ if (refused) return refused;
31440
+ if (message.includes("Server Error")) {
31441
+ return new CliError(serverMessage(message));
31442
+ }
31443
+ const internal = new CliError(message, void 0, "internal");
31444
+ if (error62 instanceof Error) internal.stack = error62.stack;
31445
+ return internal;
31446
+ }
31447
+ function errorPayload(error62) {
31448
+ return {
31449
+ error: {
31450
+ code: error62.code,
31451
+ message: error62.baseMessage,
31452
+ hint: error62.hint ?? null
31453
+ }
31454
+ };
31455
+ }
31456
+ function reportError(error62) {
31457
+ const cli = toCliError(error62);
31458
+ if (wantsJson()) {
31459
+ console.error(JSON.stringify(errorPayload(cli), null, 2));
31460
+ } else {
31461
+ console.error(`\u2717 ${cli.baseMessage}`);
31462
+ if (cli.hint) console.error(` ${cli.hint}`);
31463
+ if (cli.code === "internal" && cli.stack) {
31464
+ console.error("");
31465
+ console.error(cli.stack);
31466
+ }
31467
+ }
31468
+ const code = exitCodeFor(cli.code);
31469
+ process.exitCode = code;
31470
+ return code;
31471
+ }
31472
+ function fail(error62) {
31473
+ const cli = toCliError(error62);
31474
+ throw requestExit(exitCodeFor(cli.code), () => {
31475
+ reportError(cli);
31476
+ });
31477
+ }
31478
+
31479
+ // src/commands/translate/translate-job.ts
31480
+ var FINISHED_STATUSES = /* @__PURE__ */ new Set([
31481
+ "succeeded",
31482
+ "partial",
31483
+ "failed",
31484
+ "cancelled"
31485
+ ]);
31486
+ function isJobFinished(status) {
31487
+ return FINISHED_STATUSES.has(status);
31488
+ }
31489
+ function unsuccessfulJobError(jobId, status) {
31490
+ if (status === "cancelled") {
31491
+ return new CliError(
31492
+ `Translation job ${jobId} was cancelled.`,
31493
+ void 0,
31494
+ "job_cancelled"
31495
+ );
31496
+ }
31497
+ if (status === "failed") {
31498
+ return new CliError(
31499
+ `Translation job ${jobId} failed.`,
31500
+ `See it at ${env.TOLKBOT_WEB_URL}/translation-jobs/${jobId}.`,
31501
+ "job_failed"
31502
+ );
31503
+ }
31504
+ return null;
31505
+ }
31506
+ var JOB_WAIT_TIMEOUT_MS = 60 * 6e4;
31507
+ var JOB_POLL_MS = 500;
31508
+ var JobWaitTimeoutError = class extends CliError {
31509
+ constructor(jobId, timeoutMs) {
31510
+ super(
31511
+ `Translation job ${jobId} was still running after ${Math.round(timeoutMs / 6e4)} minutes, so the CLI stopped waiting.`,
31512
+ `The job keeps running. Check it with 'tolkbot jobs show ${jobId}' and apply it with 'tolkbot jobs apply ${jobId}' once it finishes.`,
31513
+ "job_timed_out"
31514
+ );
31515
+ this.jobId = jobId;
31516
+ this.name = "JobWaitTimeoutError";
31517
+ }
31518
+ jobId;
31519
+ };
31520
+ async function waitForJob(jobId, read, opts = {}) {
31521
+ const timeoutMs = opts.timeoutMs ?? JOB_WAIT_TIMEOUT_MS;
31522
+ const pollMs = opts.pollMs ?? JOB_POLL_MS;
31523
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
31524
+ const now = opts.now ?? Date.now;
31525
+ const deadline = now() + timeoutMs;
31526
+ for (; ; ) {
31527
+ const summary = await read();
31528
+ if (!summary) {
31529
+ throw new CliError(
31530
+ `Translation job ${jobId} was not found.`,
31531
+ void 0,
31532
+ "job_not_found"
31533
+ );
31534
+ }
31535
+ opts.onProgress?.(summary);
31536
+ if (isJobFinished(summary.status)) return summary;
31537
+ if (now() >= deadline) {
31538
+ throw new JobWaitTimeoutError(jobId, timeoutMs);
31539
+ }
31540
+ await sleep(pollMs);
31541
+ }
31542
+ }
31543
+ async function startTranslationJob(params) {
31544
+ const { projectId, organizationId: organizationId2, framework, srcLocale, dstLocale } = params;
31545
+ const convexClient = await getAuthenticatedConvexClient();
31546
+ return await convexClient.action(api.translations.startJob, {
31547
+ organizationId: organizationId2,
31548
+ projectId,
31549
+ srcLocale,
31550
+ dstLocale,
31551
+ framework,
31552
+ tasks: params.tasks.map((t) => ({
31553
+ key: t.key,
31554
+ namespace: t.namespace,
31555
+ path: t.path,
31556
+ src: t.src,
31557
+ hash: t.hash,
31558
+ context: t.context,
31559
+ // What `jobs apply` checks the target against. Without it every
31560
+ // result was written over whatever the file held by then.
31561
+ expectedTargetHash: t.expectedTargetHash
31562
+ }))
31563
+ });
31564
+ }
31565
+ async function waitForTranslationJob(organizationId2, jobId, onProgress) {
31566
+ const convexClient = await getAuthenticatedConvexClient();
31567
+ return waitForJob(
31568
+ jobId,
31569
+ () => convexClient.query(api.translations.getJobSummary, {
31570
+ organizationId: organizationId2,
31571
+ jobId
31572
+ }),
31573
+ { onProgress }
31574
+ );
31575
+ }
31576
+
31577
+ // src/commands/jobs/show.ts
31578
+ var STATUS_TEXT = {
31579
+ pending: "is waiting to start",
31580
+ running: "is running",
31581
+ succeeded: "finished",
31582
+ partial: "finished with some keys failed",
31583
+ failed: "failed",
31584
+ cancelled: "was cancelled"
31585
+ };
31586
+ function describe3(job) {
31587
+ const processed = job.completed + job.failed;
31588
+ const percent = job.total > 0 ? Math.round(processed / job.total * 100) : 0;
31589
+ const lines = [
31590
+ `Job ${job.jobId} ${STATUS_TEXT[job.status]}`,
31591
+ ` ${job.srcLocale} \u2192 ${job.dstLocale}, started ${job.createdAt.slice(0, 16).replace("T", " ")} UTC`,
31592
+ ` ${job.completed} translated, ${job.failed} failed, ${job.total} in all (${percent}%)`,
31593
+ ` ${job.creditsUsed} credits used`
31594
+ ];
31595
+ if (job.applied === "applied") lines.push(" Applied to this working tree");
31596
+ if (job.applied === "waiting") {
31597
+ lines.push(
31598
+ ` ${job.pending} key(s) waiting: 'tolkbot jobs apply ${job.jobId}'`
31599
+ );
31600
+ }
31601
+ lines.push(` ${env.TOLKBOT_WEB_URL}/translation-jobs/${job.jobId}`);
31602
+ return lines;
31603
+ }
31604
+ async function runJobsShow(opts) {
31605
+ const ctx = await loadPlanContext(process.cwd());
31606
+ const found = await ctx.client.query(api.translations.getJobDetails, {
31607
+ organizationId: ctx.project.organizationId,
31608
+ jobId: opts.jobId
31609
+ });
31610
+ if (found?.projectId !== ctx.project._id) {
31611
+ throw new CliError(
31612
+ `Translation job ${opts.jobId} was not found in this project.`,
31613
+ "List the project's jobs with 'tolkbot jobs list'.",
31614
+ "job_not_found"
31615
+ );
31616
+ }
31617
+ const [job] = await describeJobs(ctx, [found]);
31618
+ if (!job)
31619
+ throw new CliError(`Translation job ${opts.jobId} was not found.`);
31620
+ const failure2 = unsuccessfulJobError(job.jobId, job.status);
31621
+ if (opts.json) {
31622
+ if (!failure2) jsonSuccess(job);
31623
+ jsonResult(job, 1);
31624
+ fail(failure2);
31625
+ }
31626
+ for (const line of describe3(job)) console.log(line);
31627
+ if (failure2) fail(failure2);
31628
+ }
31629
+
31630
+ // src/commands/jobs/index.ts
31631
+ var jobsCommand = new Command3("jobs").description(
31632
+ "List, inspect, apply and cancel translation jobs"
31633
+ );
31634
+ var JSON_HELP = "Output as JSON (machine-readable)";
31635
+ jobsCommand.command("list").description(
31636
+ "Recent translation jobs for the project, with their status and whether their results are in this working tree"
31637
+ ).option("--limit <n>", "How many jobs to list", "20").option("--json", JSON_HELP).action(async (opts) => {
31638
+ const schema = z5.object({
31639
+ limit: z5.coerce.number().int().min(1).max(100),
31640
+ json: z5.boolean().optional()
31641
+ });
31642
+ await runJobsList(validateOrExit(schema, opts));
31643
+ });
31644
+ jobsCommand.command("show").description("Show one translation job").argument("<id>", "Job ID").option("--json", JSON_HELP).action(async (jobId, opts) => {
31645
+ const schema = z5.object({
31646
+ jobId: z5.string().min(1),
31647
+ json: z5.boolean().optional()
31648
+ });
31649
+ await runJobsShow(validateOrExit(schema, { ...opts, jobId }));
31650
+ });
31651
+ jobsCommand.command("apply").description(
31652
+ "Write finished jobs' results into the working tree. With no ID: the finished jobs not applied yet, one per language"
31653
+ ).argument("[ids...]", "Job ID(s) to apply").option("--json", JSON_HELP).action(async (jobIds, opts) => {
31654
+ const schema = z5.object({
31655
+ jobIds: z5.array(z5.string().min(1)),
31656
+ json: z5.boolean().optional()
31657
+ });
31658
+ await runJobsApply(validateOrExit(schema, { ...opts, jobIds }));
31659
+ });
31660
+ jobsCommand.command("cancel").description("Cancel a running translation job").argument("<id>", "Job ID").option("--json", JSON_HELP).action(async (jobId, opts) => {
31661
+ const schema = z5.object({
31662
+ jobId: z5.string().min(1),
31663
+ json: z5.boolean().optional()
31664
+ });
31665
+ await runJobsCancel(validateOrExit(schema, { ...opts, jobId }));
31666
+ });
31667
+
31668
+ // src/commands/learnings/index.ts
31669
+ import { Command as Command4 } from "commander";
31670
+ import { z as z6 } from "zod/v4";
31671
+
31672
+ // src/commands/learnings/archive.tsx
31673
+ import { Spinner as Spinner9 } from "@inkjs/ui";
31674
+ import { Box as Box12, render as render12, Text as Text12 } from "ink";
31675
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
31676
+ function ArchiveUI({ state }) {
31677
+ if (state.type === "archiving") {
31678
+ return /* @__PURE__ */ jsxs12(Box12, { children: [
31679
+ /* @__PURE__ */ jsx12(Spinner9, {}),
31680
+ /* @__PURE__ */ jsx12(Text12, { children: " Archiving learning\u2026" })
31681
+ ] });
31682
+ }
31683
+ return /* @__PURE__ */ jsxs12(Box12, { children: [
31684
+ /* @__PURE__ */ jsx12(Text12, { color: "green", children: "\u2713" }),
31685
+ /* @__PURE__ */ jsx12(Text12, { children: " Learning archived" })
31686
+ ] });
31687
+ }
31688
+ async function runLearningsArchive(opts) {
31689
+ const run = async () => {
31690
+ const convexClient = await getAuthenticatedConvexClient();
31691
+ const { organizationId: organizationId2 } = await getProjectContext(convexClient);
31692
+ await convexClient.mutation(api.learnings.archive, {
31693
+ organizationId: organizationId2,
31694
+ learningId: opts.id
31695
+ });
31696
+ };
31697
+ if (opts.json) {
31698
+ await run();
31699
+ jsonSuccess({ id: opts.id, ok: true });
31700
+ }
31701
+ const { unmount } = renderTransient(
31702
+ /* @__PURE__ */ jsx12(ArchiveUI, { state: { type: "archiving" } })
31703
+ );
31704
+ try {
31705
+ await run();
31706
+ } finally {
31707
+ unmount();
31708
+ }
31709
+ render12(/* @__PURE__ */ jsx12(ArchiveUI, { state: { type: "success" } })).unmount();
31710
+ }
31711
+
31712
+ // src/commands/learnings/list.tsx
31713
+ import { Spinner as Spinner10 } from "@inkjs/ui";
31714
+ import { Box as Box13, render as render13, Text as Text13 } from "ink";
31715
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
31716
+ var feedbackLabels = {
31717
+ "incorrect-term": "Incorrect term",
31718
+ "wrong-tone": "Wrong tone",
31719
+ "cultural-context": "Cultural context",
31720
+ "technical-accuracy": "Technical accuracy",
31721
+ other: "Other"
31722
+ };
31723
+ function LearningsListUI({ state }) {
31724
+ if (state.type === "loading") {
31725
+ return /* @__PURE__ */ jsxs13(Box13, { children: [
31726
+ /* @__PURE__ */ jsx13(Spinner10, {}),
31727
+ /* @__PURE__ */ jsx13(Text13, { children: " Loading learnings\u2026" })
31728
+ ] });
31729
+ }
31730
+ const { learnings } = state;
31731
+ if (learnings.length === 0) {
31732
+ return /* @__PURE__ */ jsx13(Box13, { children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No learnings found." }) });
31733
+ }
31734
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
31735
+ /* @__PURE__ */ jsx13(Box13, { marginBottom: 1, children: /* @__PURE__ */ jsxs13(Text13, { bold: true, children: [
31736
+ learnings.length,
31737
+ " learning",
31738
+ learnings.length === 1 ? "" : "s"
31739
+ ] }) }),
31740
+ learnings.map((l) => /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", marginBottom: 1, children: [
31741
+ /* @__PURE__ */ jsxs13(Box13, { gap: 1, children: [
31742
+ /* @__PURE__ */ jsxs13(Text13, { bold: true, children: [
31743
+ l.srcLocale,
31744
+ " \u2192 ",
31745
+ l.dstLocale
31746
+ ] }),
31747
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
31748
+ "[",
31749
+ feedbackLabels[l.feedbackType] ?? l.feedbackType,
31750
+ "]"
31751
+ ] }),
31752
+ l.status === "archived" && /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: "[archived]" })
31753
+ ] }),
31754
+ /* @__PURE__ */ jsx13(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsxs13(Text13, { children: [
31755
+ '"',
31756
+ l.sourceText,
31757
+ '" \u2192 "',
31758
+ l.suggestedTranslation,
31759
+ '"'
31760
+ ] }) }),
31761
+ l.reasoning && /* @__PURE__ */ jsx13(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
31762
+ "Reason: ",
31763
+ l.reasoning
31764
+ ] }) }),
31765
+ /* @__PURE__ */ jsx13(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
31766
+ "ID: ",
31767
+ l._id
31768
+ ] }) })
31769
+ ] }, l._id))
31770
+ ] });
31771
+ }
31772
+ async function loadLearnings(opts) {
31773
+ const convexClient = await getAuthenticatedConvexClient();
31774
+ const { projectId, organizationId: organizationId2 } = await getProjectContext(convexClient);
31775
+ return convexClient.query(api.learnings.listByProject, {
31776
+ organizationId: organizationId2,
31777
+ projectId,
31778
+ status: opts.status,
31779
+ limit: opts.limit
31780
+ });
31781
+ }
31782
+ async function runLearningsList(opts) {
31783
+ if (opts.json) {
31784
+ const learnings2 = await loadLearnings(opts);
31785
+ jsonSuccess({
31786
+ learnings: learnings2.map((l) => ({
31787
+ id: l._id,
31788
+ sourceText: l.sourceText,
31789
+ suggestedTranslation: l.suggestedTranslation,
31790
+ srcLocale: l.srcLocale,
31791
+ dstLocale: l.dstLocale,
31792
+ feedbackType: l.feedbackType,
31793
+ reasoning: l.reasoning,
31794
+ status: l.status,
31795
+ source: l.source
31796
+ }))
31797
+ });
31798
+ }
31799
+ const { unmount } = renderTransient(
31800
+ /* @__PURE__ */ jsx13(LearningsListUI, { state: { type: "loading" } })
31801
+ );
31802
+ let learnings;
31803
+ try {
31804
+ learnings = await loadLearnings(opts);
31805
+ } finally {
31806
+ unmount();
31807
+ }
31808
+ render13(
31809
+ /* @__PURE__ */ jsx13(LearningsListUI, { state: { type: "success", learnings } })
31810
+ ).unmount();
31811
+ }
31812
+
31813
+ // src/commands/learnings/remove.tsx
31814
+ import { Spinner as Spinner11 } from "@inkjs/ui";
31815
+ import { Box as Box14, render as render14, Text as Text14 } from "ink";
31816
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
31817
+ function RemoveUI2({ state }) {
31818
+ if (state.type === "removing") {
31819
+ return /* @__PURE__ */ jsxs14(Box14, { children: [
31820
+ /* @__PURE__ */ jsx14(Spinner11, {}),
31821
+ /* @__PURE__ */ jsx14(Text14, { children: " Removing learning\u2026" })
31822
+ ] });
31823
+ }
31824
+ return /* @__PURE__ */ jsxs14(Box14, { children: [
31825
+ /* @__PURE__ */ jsx14(Text14, { color: "green", children: "\u2713" }),
31826
+ /* @__PURE__ */ jsx14(Text14, { children: " Learning removed" })
31827
+ ] });
31828
+ }
31829
+ async function runLearningsRemove(opts) {
31830
+ const run = async () => {
31831
+ const convexClient = await getAuthenticatedConvexClient();
31832
+ const { organizationId: organizationId2 } = await getProjectContext(convexClient);
31833
+ await convexClient.mutation(api.learnings.remove, {
31834
+ organizationId: organizationId2,
31835
+ learningId: opts.id
31836
+ });
31837
+ };
31838
+ if (opts.json) {
31839
+ await run();
31840
+ jsonSuccess({ id: opts.id, ok: true });
31841
+ }
31842
+ const { unmount } = renderTransient(
31843
+ /* @__PURE__ */ jsx14(RemoveUI2, { state: { type: "removing" } })
31844
+ );
31845
+ try {
31846
+ await run();
31847
+ } finally {
31848
+ unmount();
31849
+ }
31850
+ render14(/* @__PURE__ */ jsx14(RemoveUI2, { state: { type: "success" } })).unmount();
31851
+ }
31852
+
31853
+ // src/commands/learnings/show.tsx
31854
+ import { Spinner as Spinner12 } from "@inkjs/ui";
31855
+ import { Box as Box15, render as render15, Text as Text15 } from "ink";
31856
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
31857
+ function LearningShowUI({ state }) {
31858
+ if (state.type === "loading") {
31859
+ return /* @__PURE__ */ jsxs15(Box15, { children: [
31860
+ /* @__PURE__ */ jsx15(Spinner12, {}),
31861
+ /* @__PURE__ */ jsx15(Text15, { children: " Loading learning\u2026" })
31862
+ ] });
31863
+ }
31864
+ const { learning } = state;
31865
+ return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
31866
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31867
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Source text:" }),
31868
+ /* @__PURE__ */ jsx15(Text15, { children: learning.sourceText })
31869
+ ] }),
31870
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31871
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Suggested translation:" }),
31872
+ /* @__PURE__ */ jsx15(Text15, { children: learning.suggestedTranslation })
31873
+ ] }),
31874
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31875
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Locales:" }),
31876
+ /* @__PURE__ */ jsxs15(Text15, { children: [
31877
+ learning.srcLocale,
31878
+ " \u2192 ",
31879
+ learning.dstLocale
31880
+ ] })
31881
+ ] }),
31882
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31883
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Feedback type:" }),
31884
+ /* @__PURE__ */ jsx15(Text15, { children: learning.feedbackType })
31885
+ ] }),
31886
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31887
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Reasoning:" }),
31888
+ /* @__PURE__ */ jsx15(Text15, { children: learning.reasoning })
31889
+ ] }),
31890
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31891
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Status:" }),
31892
+ /* @__PURE__ */ jsx15(Text15, { color: learning.status === "active" ? "green" : "yellow", children: learning.status })
31893
+ ] }),
31894
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31895
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Source:" }),
31896
+ /* @__PURE__ */ jsx15(Text15, { children: learning.source })
31897
+ ] }),
31898
+ /* @__PURE__ */ jsx15(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text15, { dimColor: true, children: [
31899
+ "ID: ",
31900
+ learning._id
31901
+ ] }) })
31902
+ ] });
31903
+ }
31904
+ async function loadLearning(id) {
31905
+ const convexClient = await getAuthenticatedConvexClient();
31906
+ const { organizationId: organizationId2 } = await getProjectContext(convexClient);
31907
+ const learning = await convexClient.query(api.learnings.getById, {
31908
+ organizationId: organizationId2,
31909
+ learningId: id
31910
+ });
31911
+ if (!learning) {
31912
+ throw new CliError(
31913
+ `Learning ${id} was not found.`,
31914
+ "List them with 'tolkbot learnings list'.",
31915
+ "not_found"
31916
+ );
31917
+ }
31918
+ return learning;
31919
+ }
31920
+ async function runLearningsShow(opts) {
31921
+ if (opts.json) {
31922
+ const learning2 = await loadLearning(opts.id);
31923
+ jsonSuccess({
31924
+ id: learning2._id,
31925
+ sourceText: learning2.sourceText,
31926
+ suggestedTranslation: learning2.suggestedTranslation,
31927
+ srcLocale: learning2.srcLocale,
31928
+ dstLocale: learning2.dstLocale,
31929
+ feedbackType: learning2.feedbackType,
31930
+ reasoning: learning2.reasoning,
31931
+ status: learning2.status,
31932
+ source: learning2.source
31933
+ });
31934
+ }
31935
+ const { unmount } = renderTransient(
31936
+ /* @__PURE__ */ jsx15(LearningShowUI, { state: { type: "loading" } })
31937
+ );
31938
+ let learning;
31939
+ try {
31940
+ learning = await loadLearning(opts.id);
31941
+ } finally {
31942
+ unmount();
31943
+ }
31944
+ render15(/* @__PURE__ */ jsx15(LearningShowUI, { state: { type: "success", learning } })).unmount();
31945
+ }
31946
+
31947
+ // src/commands/learnings/index.ts
31948
+ var learningsCommand = new Command4("learnings").description(
31949
+ "View and manage translation learnings"
31950
+ );
31951
+ learningsCommand.command("list").description("List translation learnings for the current project").option("--status <status>", "Filter by status (active or archived)").option("--limit <n>", "Max number of results", (v) => Number(v), 100).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
31952
+ const schema = z6.object({
31953
+ status: z6.enum(["active", "archived"]).optional(),
31954
+ limit: z6.coerce.number().int().positive().default(100),
31955
+ json: z6.boolean().optional()
31956
+ });
31957
+ const parsed = validateOrExit(schema, opts);
31958
+ await runLearningsList(parsed);
31959
+ });
31960
+ learningsCommand.command("show").description("Show details of a learning").requiredOption("--id <learningId>", "Learning ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
31961
+ const schema = z6.object({
31962
+ id: z6.string().min(1),
31963
+ json: z6.boolean().optional()
31964
+ });
31965
+ const parsed = validateOrExit(schema, opts);
31966
+ await runLearningsShow(parsed);
31967
+ });
31968
+ learningsCommand.command("archive").description("Archive a learning (keeps it but excludes from translations)").requiredOption("--id <learningId>", "Learning ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
31969
+ const schema = z6.object({
31970
+ id: z6.string().min(1),
31971
+ json: z6.boolean().optional()
31972
+ });
31973
+ const parsed = validateOrExit(schema, opts);
31974
+ await runLearningsArchive(parsed);
31975
+ });
31976
+ learningsCommand.command("remove").description("Permanently remove a learning").requiredOption("--id <learningId>", "Learning ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
31977
+ const schema = z6.object({
31978
+ id: z6.string().min(1),
31979
+ json: z6.boolean().optional()
31980
+ });
31981
+ const parsed = validateOrExit(schema, opts);
31982
+ await runLearningsRemove(parsed);
31983
+ });
31984
+
31985
+ // src/commands/telemetry.ts
31986
+ import { Command as Command5 } from "commander";
31987
+ import { z as z7 } from "zod/v4";
31988
+ var REASONS = {
31989
+ env: `disabled by $${TELEMETRY_ENV_VAR}`,
31990
+ "do-not-track": `disabled by $${DO_NOT_TRACK_ENV_VAR}`,
31991
+ preference: "disabled with `tolkbot telemetry off`",
31992
+ "no-key": "disabled: this build has no telemetry key",
31993
+ default: "enabled"
31994
+ };
31995
+ async function printStatus(json2) {
31996
+ const state = await telemetryState();
31997
+ if (json2) {
31998
+ jsonResult({ ...state, preferenceFile: getPreferencePath() }, 0);
31999
+ return;
32000
+ }
32001
+ console.log(
32002
+ `Telemetry is ${state.enabled ? "on" : "off"} (${REASONS[state.reason]}).`
32003
+ );
32004
+ console.log(
32005
+ `Turn it off with \`tolkbot telemetry off\`, $${TELEMETRY_ENV_VAR}=0 or $${DO_NOT_TRACK_ENV_VAR}=1.`
32006
+ );
32007
+ }
32008
+ var telemetryCommand = new Command5("telemetry").description(
32009
+ "Control anonymous usage reporting"
32010
+ );
32011
+ telemetryCommand.command("status", { isDefault: true }).description("Show whether telemetry is on and why").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
32012
+ const parsed = validateOrExit(
32013
+ z7.object({ json: z7.boolean().optional() }),
32014
+ opts
32015
+ );
32016
+ await printStatus(parsed.json);
32017
+ });
32018
+ telemetryCommand.command("off").description("Stop reporting usage from this machine").action(async () => {
32019
+ await setTelemetryPreference(false);
32020
+ console.log(
32021
+ `Telemetry is off. Preference saved to ${getPreferencePath()}.`
32022
+ );
32023
+ });
32024
+ telemetryCommand.command("on").description("Resume reporting usage from this machine").action(async () => {
32025
+ await setTelemetryPreference(true);
32026
+ await printStatus(false);
32027
+ });
32028
+
32029
+ // src/commands/translate/index.ts
32030
+ import { Command as Command6 } from "commander";
32031
+ import { z as z8 } from "zod/v4";
32032
+
32033
+ // src/commands/translate/adopt.tsx
32034
+ var STAMP_CHUNK2 = 200;
32035
+ async function adoptLocale(ctx, srcLocale, dstLocale) {
32036
+ const plan = await planForLocale(ctx, srcLocale, dstLocale);
32037
+ const locale = governanceLocale(ctx.project, dstLocale);
32038
+ const byTemplate = /* @__PURE__ */ new Map();
32039
+ for (const key of plan.untrackedKeys) {
32040
+ const group = byTemplate.get(key.pathTemplate) ?? [];
32041
+ group.push(key);
32042
+ byTemplate.set(key.pathTemplate, group);
32043
+ }
32044
+ const outcome = {
32045
+ dstLocale,
32046
+ untracked: plan.untrackedKeys.length,
32047
+ adopted: 0,
32048
+ skipped: 0
32049
+ };
32050
+ for (const [pathTemplate, keys] of byTemplate) {
32051
+ for (let i = 0; i < keys.length; i += STAMP_CHUNK2) {
32052
+ const result = await ctx.client.mutation(
32053
+ api.localeKeyStates.adoptUntrackedForCli,
32054
+ {
32055
+ organizationId: ctx.project.organizationId,
32056
+ projectId: ctx.project._id,
32057
+ locale,
32058
+ pathTemplate,
32059
+ entries: keys.slice(i, i + STAMP_CHUNK2).map((key) => ({
32060
+ key: key.fileKey,
32061
+ namespace: key.namespace,
32062
+ baseValue: key.src,
32063
+ translatedValue: key.dst
32064
+ }))
32065
+ }
32066
+ );
32067
+ outcome.adopted += result.adopted;
32068
+ outcome.skipped += result.skipped;
32069
+ }
32070
+ }
32071
+ return outcome;
32072
+ }
32073
+ async function runAdopt(opts) {
32074
+ const run = async () => {
32075
+ const ctx = await loadPlanContext(process.cwd());
32076
+ const src = opts.src ?? ctx.project.baseLanguage;
32077
+ const locales = opts.all ? ctx.project.targetLanguages : opts.dst ? [opts.dst] : [];
32078
+ if (locales.length === 0) {
32079
+ throw opts.all ? new CliError(
32080
+ "No target languages configured for this project.",
32081
+ void 0,
32082
+ "no_target_languages"
32083
+ ) : new UsageError(
32084
+ "Either --dst <locale> or --all must be provided."
32085
+ );
32086
+ }
32087
+ const outcomes2 = [];
32088
+ for (const dst of locales)
32089
+ outcomes2.push(await adoptLocale(ctx, src, dst));
32090
+ track("cli_translate_completed", {
32091
+ mode: "adopt",
32092
+ key_count: outcomes2.reduce((n, o) => n + o.adopted, 0),
32093
+ locale_count: outcomes2.length,
32094
+ job_count: 0,
32095
+ json: opts.json === true
32096
+ });
32097
+ return outcomes2;
32098
+ };
32099
+ if (opts.json) {
32100
+ jsonSuccess({ adopted: await run() });
32101
+ }
32102
+ const outcomes = await run();
32103
+ for (const o of outcomes) {
32104
+ console.log(
32105
+ `\u2713 ${o.dstLocale}: adopted ${o.adopted} untracked translation(s)` + (o.skipped > 0 ? `, ${o.skipped} already tracked` : "")
32083
32106
  );
32084
32107
  }
32085
- if (isNetworkError(error62)) {
32086
- return new CliError(
32087
- "Could not reach TolkBot.",
32088
- "Check the network connection and try again.",
32089
- "network"
32108
+ }
32109
+
32110
+ // src/commands/translate/edit.tsx
32111
+ import path10 from "path";
32112
+ import { Spinner as Spinner13 } from "@inkjs/ui";
32113
+ import { Box as Box16, render as render16, Text as Text16 } from "ink";
32114
+
32115
+ // src/utils/locale-files.ts
32116
+ import path9 from "path";
32117
+ async function findLocaleFiles(baseDir) {
32118
+ const adapter = createAdapter("json");
32119
+ const result = await adapter.discover(baseDir);
32120
+ const localeFiles = /* @__PURE__ */ new Map();
32121
+ for (const [locale, files] of result.filesByLocale) {
32122
+ localeFiles.set(
32123
+ locale,
32124
+ files.map((f) => f.path)
32090
32125
  );
32091
32126
  }
32092
- const message = describeError(error62);
32093
- const refused = argumentError(message);
32094
- if (refused) return refused;
32095
- if (message.includes("Server Error")) {
32096
- return new CliError(serverMessage(message));
32097
- }
32098
- const internal = new CliError(message, void 0, "internal");
32099
- if (error62 instanceof Error) internal.stack = error62.stack;
32100
- return internal;
32101
- }
32102
- function errorPayload(error62) {
32103
- return {
32104
- error: {
32105
- code: error62.code,
32106
- message: error62.baseMessage,
32107
- hint: error62.hint ?? null
32108
- }
32109
- };
32127
+ return localeFiles;
32110
32128
  }
32111
- function reportError(error62) {
32112
- const cli = toCliError(error62);
32113
- if (wantsJson()) {
32114
- console.error(JSON.stringify(errorPayload(cli), null, 2));
32115
- } else {
32116
- console.error(`\u2717 ${cli.baseMessage}`);
32117
- if (cli.hint) console.error(` ${cli.hint}`);
32118
- if (cli.code === "internal" && cli.stack) {
32119
- console.error("");
32120
- console.error(cli.stack);
32129
+ function resolveFileForKey(files, key, namespace) {
32130
+ if (files.length === 0) return null;
32131
+ if (files.length === 1) return files[0] ?? null;
32132
+ const ns = namespace ?? key.split(".")[0];
32133
+ for (const file2 of files) {
32134
+ const fileName = path9.parse(path9.basename(file2)).name;
32135
+ if (fileName === ns) {
32136
+ return file2;
32121
32137
  }
32122
32138
  }
32123
- const code = exitCodeFor(cli.code);
32124
- process.exitCode = code;
32125
- return code;
32139
+ return files[0] ?? null;
32126
32140
  }
32127
- function fail(error62) {
32128
- const cli = toCliError(error62);
32129
- throw requestExit(exitCodeFor(cli.code), () => {
32130
- reportError(cli);
32131
- });
32141
+ function resolveKeyInFile(key, targetFile, fileCount, namespace) {
32142
+ if (fileCount <= 1 || namespace) return key;
32143
+ const fileName = path9.parse(path9.basename(targetFile)).name;
32144
+ if (key.startsWith(`${fileName}.`)) {
32145
+ return key.slice(fileName.length + 1);
32146
+ }
32147
+ return key;
32132
32148
  }
32133
32149
 
32134
- // src/commands/translate/run-ui.tsx
32135
- import { Spinner as Spinner16 } from "@inkjs/ui";
32136
- import { Box as Box19, Text as Text19 } from "ink";
32137
- import { jsx as jsx19, jsxs as jsxs19 } from "react/jsx-runtime";
32138
- function JobLink({ jobId }) {
32139
- const url2 = `${env.TOLKBOT_WEB_URL}/translation-jobs/${jobId}`;
32140
- return /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
32141
- "Job:",
32142
- " ",
32143
- /* @__PURE__ */ jsx19(Text19, { color: "blue", underline: true, children: url2 })
32144
- ] });
32145
- }
32146
- function PlanNotes({ notes }) {
32147
- if (!notes || notes.length === 0) return null;
32148
- return /* @__PURE__ */ jsx19(Box19, { flexDirection: "column", paddingLeft: 2, children: notes.map((note) => /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: note }, note)) });
32149
- }
32150
- function JobRow({ srcLocale, job }) {
32151
- const { dstLocale, status, taskCount, progress, jobId, error: error62, notes } = job;
32152
- if (status === "waiting") {
32153
- return /* @__PURE__ */ jsxs19(Box19, { children: [
32154
- /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: "\u23F8 " }),
32155
- /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
32156
- srcLocale,
32157
- " \u2192 ",
32158
- dstLocale,
32159
- ": Waiting..."
32160
- ] })
32150
+ // src/commands/translate/edit.tsx
32151
+ import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
32152
+ function EditUI({ state }) {
32153
+ if (state.type === "editing") {
32154
+ return /* @__PURE__ */ jsxs16(Box16, { children: [
32155
+ /* @__PURE__ */ jsx16(Spinner13, {}),
32156
+ /* @__PURE__ */ jsx16(Text16, { children: " Editing translation\u2026" })
32161
32157
  ] });
32162
32158
  }
32163
- if (status === "generating") {
32164
- return /* @__PURE__ */ jsxs19(Box19, { children: [
32165
- /* @__PURE__ */ jsx19(Spinner16, {}),
32166
- /* @__PURE__ */ jsxs19(Text19, { children: [
32159
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
32160
+ /* @__PURE__ */ jsxs16(Box16, { children: [
32161
+ /* @__PURE__ */ jsx16(Text16, { color: "green", children: "\u2713" }),
32162
+ /* @__PURE__ */ jsxs16(Text16, { children: [
32167
32163
  " ",
32168
- srcLocale,
32169
- " \u2192 ",
32170
- dstLocale,
32171
- ": Generating tasks..."
32164
+ "Updated ",
32165
+ /* @__PURE__ */ jsx16(Text16, { bold: true, children: state.key })
32172
32166
  ] })
32173
- ] });
32167
+ ] }),
32168
+ /* @__PURE__ */ jsx16(Box16, { paddingLeft: 2, children: /* @__PURE__ */ jsx16(Text16, { dimColor: true, children: state.value }) }),
32169
+ /* @__PURE__ */ jsx16(Box16, { marginTop: 1, children: /* @__PURE__ */ jsxs16(Text16, { dimColor: true, children: [
32170
+ "File: ",
32171
+ state.filePath
32172
+ ] }) })
32173
+ ] });
32174
+ }
32175
+ async function performEdit(opts) {
32176
+ const baseDir = process.cwd();
32177
+ const config3 = readConfig(baseDir);
32178
+ if (!config3) throw noConfigError();
32179
+ const localeFiles = await findLocaleFiles(baseDir);
32180
+ const files = localeFiles.get(opts.locale);
32181
+ if (!files || files.length === 0) {
32182
+ throw new CliError(
32183
+ `No translation files found for locale "${opts.locale}".`,
32184
+ `Available locales: ${[...localeFiles.keys()].join(", ") || "none"}.`,
32185
+ "no_translation_files"
32186
+ );
32174
32187
  }
32175
- if (status === "translating" && progress) {
32176
- const processed = progress.completed + progress.failed;
32177
- const percent = progress.total > 0 ? Math.round(processed / progress.total * 100) : 0;
32178
- return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
32179
- /* @__PURE__ */ jsxs19(Box19, { children: [
32180
- /* @__PURE__ */ jsx19(Spinner16, {}),
32181
- /* @__PURE__ */ jsxs19(Text19, { children: [
32182
- " ",
32183
- srcLocale,
32184
- " \u2192 ",
32185
- dstLocale,
32186
- ": Translating... ",
32187
- processed,
32188
- "/",
32189
- progress.total,
32190
- " (",
32191
- percent,
32192
- "%)"
32193
- ] })
32194
- ] }),
32195
- jobId && /* @__PURE__ */ jsx19(Box19, { paddingLeft: 2, children: /* @__PURE__ */ jsx19(JobLink, { jobId }) })
32196
- ] });
32188
+ const targetFile = resolveFileForKey(files, opts.key, opts.namespace);
32189
+ if (!targetFile) {
32190
+ throw new CliError(
32191
+ `Could not determine target file for key "${opts.key}" in locale "${opts.locale}".`,
32192
+ "Pass --namespace to say which file the key belongs to."
32193
+ );
32197
32194
  }
32198
- if (status === "completed" && progress) {
32199
- const icon = progress.status === "succeeded" ? "\u2713" : progress.status === "partial" ? "\u26A0" : "\u2717";
32200
- const color = progress.status === "succeeded" ? "green" : progress.status === "partial" ? "yellow" : "red";
32201
- return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
32202
- /* @__PURE__ */ jsxs19(Box19, { children: [
32203
- /* @__PURE__ */ jsx19(Text19, { color, children: icon }),
32204
- /* @__PURE__ */ jsxs19(Text19, { children: [
32205
- " ",
32206
- srcLocale,
32207
- " \u2192 ",
32208
- dstLocale,
32209
- ": ",
32210
- progress.completed,
32211
- "/",
32212
- progress.total,
32213
- " completed",
32214
- progress.failed > 0 && ` (${progress.failed} failed)`
32215
- ] })
32216
- ] }),
32217
- jobId && /* @__PURE__ */ jsx19(Box19, { paddingLeft: 2, children: /* @__PURE__ */ jsx19(JobLink, { jobId }) }),
32218
- /* @__PURE__ */ jsx19(PlanNotes, { notes })
32219
- ] });
32195
+ const keyInFile = resolveKeyInFile(
32196
+ opts.key,
32197
+ targetFile,
32198
+ files.length,
32199
+ opts.namespace
32200
+ );
32201
+ const adapter = createAdapter("json");
32202
+ let existingData = {};
32203
+ try {
32204
+ const parsed = await adapter.parse(targetFile);
32205
+ existingData = parsed.data;
32206
+ } catch {
32220
32207
  }
32221
- if (status === "error") {
32222
- return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
32223
- /* @__PURE__ */ jsxs19(Box19, { children: [
32224
- /* @__PURE__ */ jsx19(Text19, { color: "red", children: "\u2717 " }),
32225
- /* @__PURE__ */ jsxs19(Text19, { children: [
32226
- srcLocale,
32227
- " \u2192 ",
32228
- dstLocale,
32229
- ": Failed"
32230
- ] })
32231
- ] }),
32232
- error62 && /* @__PURE__ */ jsx19(Box19, { paddingLeft: 2, children: /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: error62 }) }),
32233
- jobId && /* @__PURE__ */ jsx19(Box19, { paddingLeft: 2, children: /* @__PURE__ */ jsx19(JobLink, { jobId }) })
32234
- ] });
32208
+ const updatedData = setByPath(
32209
+ existingData,
32210
+ keyInFile,
32211
+ opts.value
32212
+ );
32213
+ await adapter.serialize(targetFile, updatedData, { merge: false });
32214
+ const relativePath = path10.relative(baseDir, targetFile);
32215
+ return { filePath: relativePath, key: opts.key, value: opts.value };
32216
+ }
32217
+ async function runEdit(opts) {
32218
+ if (opts.json) {
32219
+ const result2 = await performEdit(opts);
32220
+ jsonSuccess({
32221
+ file: result2.filePath,
32222
+ key: result2.key,
32223
+ value: result2.value
32224
+ });
32235
32225
  }
32236
- if (status === "completed" && !progress && taskCount === 0) {
32237
- return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
32238
- /* @__PURE__ */ jsxs19(Box19, { children: [
32239
- /* @__PURE__ */ jsx19(Text19, { color: "green", children: "\u2713 " }),
32240
- /* @__PURE__ */ jsxs19(Text19, { children: [
32241
- srcLocale,
32242
- " \u2192 ",
32243
- dstLocale,
32244
- ": Up to date (no translations needed)"
32245
- ] })
32246
- ] }),
32247
- /* @__PURE__ */ jsx19(PlanNotes, { notes })
32248
- ] });
32226
+ const { unmount } = renderTransient(/* @__PURE__ */ jsx16(EditUI, { state: { type: "editing" } }));
32227
+ let result;
32228
+ try {
32229
+ result = await performEdit(opts);
32230
+ } finally {
32231
+ unmount();
32249
32232
  }
32250
- return null;
32251
- }
32252
- function RunUI({ srcLocale, jobs }) {
32253
- const hasMultiple = jobs.length > 1;
32254
- const allCompleted = jobs.every(
32255
- (j) => j.status === "completed" || j.status === "error"
32256
- );
32257
- return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
32258
- hasMultiple && /* @__PURE__ */ jsx19(Box19, { marginBottom: 1, children: /* @__PURE__ */ jsxs19(Text19, { bold: true, children: [
32259
- "\u{1F30D} Translating from ",
32260
- srcLocale,
32261
- " to ",
32262
- jobs.length,
32263
- " ",
32264
- "languages"
32265
- ] }) }),
32266
- jobs.map((job) => /* @__PURE__ */ jsx19(JobRow, { srcLocale, job }, job.dstLocale)),
32267
- hasMultiple && allCompleted && /* @__PURE__ */ jsxs19(Box19, { marginTop: 1, flexDirection: "column", children: [
32268
- /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: "\u2500".repeat(60) }),
32269
- /* @__PURE__ */ jsx19(Box19, { children: /* @__PURE__ */ jsx19(Text19, { bold: true, children: "\u{1F4CA} Summary" }) }),
32270
- jobs.map((job) => {
32271
- if (!job.progress) return null;
32272
- const icon = job.progress.status === "succeeded" ? "\u2713" : job.progress.status === "partial" ? "\u26A0" : "\u2717";
32273
- return /* @__PURE__ */ jsx19(Box19, { children: /* @__PURE__ */ jsxs19(Text19, { children: [
32274
- icon,
32275
- " ",
32276
- srcLocale,
32277
- " \u2192 ",
32278
- job.dstLocale,
32279
- ":",
32280
- " ",
32281
- job.progress.completed,
32282
- "/",
32283
- job.progress.total,
32284
- job.progress.failed > 0 && ` (${job.progress.failed} failed)`
32285
- ] }) }, job.dstLocale);
32286
- }),
32287
- /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: "\u2500".repeat(60) })
32288
- ] })
32289
- ] });
32233
+ render16(/* @__PURE__ */ jsx16(EditUI, { state: { type: "success", ...result } })).unmount();
32290
32234
  }
32291
32235
 
32292
- // src/commands/translate/translate-job.ts
32293
- var FINISHED_STATUSES = /* @__PURE__ */ new Set([
32294
- "succeeded",
32295
- "partial",
32296
- "failed",
32297
- "cancelled"
32298
- ]);
32299
- function isJobFinished(status) {
32300
- return FINISHED_STATUSES.has(status);
32236
+ // src/commands/translate/translate.tsx
32237
+ import { Spinner as Spinner14 } from "@inkjs/ui";
32238
+ import { Box as Box17, Text as Text17 } from "ink";
32239
+ import { jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
32240
+ function planOutput(plan) {
32241
+ return {
32242
+ ...plan.summary,
32243
+ protectedKeys: plan.protectedKeys,
32244
+ heldKeys: plan.heldKeys,
32245
+ untrackedKeys: plan.untrackedKeys.map((k) => k.key)
32246
+ };
32301
32247
  }
32302
- function isJobUnsuccessful(status) {
32303
- return status === "failed" || status === "cancelled";
32248
+ function fullKey4(namespace, key) {
32249
+ return namespace ? `${namespace}.${key}` : key;
32304
32250
  }
32305
- function unsuccessfulJobError(jobId, status) {
32306
- if (status === "cancelled") {
32307
- return new CliError(
32308
- `Translation job ${jobId} was cancelled.`,
32309
- void 0,
32310
- "job_cancelled"
32251
+ function characters(text) {
32252
+ return Array.from(text).length;
32253
+ }
32254
+ function targetLocales(ctx, opts) {
32255
+ if (opts.dst) return [opts.dst];
32256
+ const locales = ctx.project.targetLanguages;
32257
+ if (locales.length === 0) {
32258
+ throw new CliError(
32259
+ "No target languages configured.",
32260
+ `Configure them in the project settings at ${env.TOLKBOT_WEB_URL}/projects/${ctx.project.slug}/settings.`,
32261
+ "no_target_languages"
32311
32262
  );
32312
32263
  }
32313
- if (status === "failed") {
32314
- return new CliError(
32315
- `Translation job ${jobId} failed.`,
32316
- `See it at ${env.TOLKBOT_WEB_URL}/translation-jobs/${jobId}.`,
32317
- "job_failed"
32318
- );
32264
+ return locales;
32265
+ }
32266
+ async function runTranslate(opts) {
32267
+ const ctx = await loadPlanContext(process.cwd());
32268
+ const srcLocale = opts.src ?? ctx.project.baseLanguage;
32269
+ const dstLocales = targetLocales(ctx, opts);
32270
+ if (opts.dryRun) {
32271
+ await runDryRun(ctx, srcLocale, dstLocales, opts);
32272
+ return;
32319
32273
  }
32320
- return null;
32274
+ await runLanguages(ctx, srcLocale, dstLocales, opts);
32321
32275
  }
32322
- var JOB_WAIT_TIMEOUT_MS = 60 * 6e4;
32323
- var JOB_POLL_MS = 500;
32324
- var JobWaitTimeoutError = class extends CliError {
32325
- constructor(jobId, timeoutMs) {
32326
- super(
32327
- `Translation job ${jobId} was still running after ${Math.round(timeoutMs / 6e4)} minutes, so the CLI stopped waiting.`,
32328
- `The job keeps running. Check it with 'tolkbot translate status --job-id ${jobId}' and apply it with 'tolkbot translate apply --job-id ${jobId}' once it finishes.`,
32329
- "job_timed_out"
32330
- );
32331
- this.jobId = jobId;
32332
- this.name = "JobWaitTimeoutError";
32276
+ async function creditBalance(ctx) {
32277
+ try {
32278
+ const balance = await ctx.client.query(api.usage.getCreditBalance, {
32279
+ organizationId: ctx.project.organizationId
32280
+ });
32281
+ return balance?.total ?? null;
32282
+ } catch {
32283
+ return null;
32333
32284
  }
32334
- jobId;
32335
- };
32336
- async function waitForJob(jobId, read, opts = {}) {
32337
- const timeoutMs = opts.timeoutMs ?? JOB_WAIT_TIMEOUT_MS;
32338
- const pollMs = opts.pollMs ?? JOB_POLL_MS;
32339
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
32340
- const now = opts.now ?? Date.now;
32341
- const deadline = now() + timeoutMs;
32342
- for (; ; ) {
32343
- const summary = await read();
32344
- if (!summary) {
32345
- throw new CliError(
32346
- `Translation job ${jobId} was not found.`,
32347
- void 0,
32348
- "job_not_found"
32285
+ }
32286
+ var NO_ESTIMATE = "No credit estimate exists for this project yet. Keys and characters are what a run would send.";
32287
+ async function runDryRun(ctx, srcLocale, dstLocales, opts) {
32288
+ const languages = [];
32289
+ const progress = renderTransient(
32290
+ /* @__PURE__ */ jsx17(Working, { text: `Planning ${dstLocales.join(", ")}\u2026` })
32291
+ );
32292
+ try {
32293
+ for (const dstLocale of dstLocales) {
32294
+ const { plan } = await generateTasks(ctx, srcLocale, dstLocale, {
32295
+ retranslateUntracked: opts.retranslateUntracked
32296
+ });
32297
+ languages.push({
32298
+ dstLocale,
32299
+ keys: plan.tasks.length,
32300
+ characters: plan.tasks.reduce(
32301
+ (n, task) => n + characters(task.src),
32302
+ 0
32303
+ ),
32304
+ tasks: plan.tasks.map((task) => ({
32305
+ key: fullKey4(task.namespace, task.key),
32306
+ path: task.path,
32307
+ reason: task.reason,
32308
+ ...opts.verbose ? { src: task.src } : {}
32309
+ })),
32310
+ plan
32311
+ });
32312
+ }
32313
+ } finally {
32314
+ progress.unmount();
32315
+ }
32316
+ const total = {
32317
+ keys: languages.reduce((n, l) => n + l.keys, 0),
32318
+ characters: languages.reduce((n, l) => n + l.characters, 0)
32319
+ };
32320
+ const balance = await creditBalance(ctx);
32321
+ track("cli_translate_completed", {
32322
+ mode: "plan",
32323
+ key_count: total.keys,
32324
+ locale_count: languages.length,
32325
+ job_count: 0,
32326
+ json: opts.json ?? false
32327
+ });
32328
+ if (opts.json) {
32329
+ jsonSuccess({
32330
+ dryRun: true,
32331
+ srcLocale,
32332
+ languages: languages.map(({ plan, ...language }) => ({
32333
+ ...language,
32334
+ plan: planOutput(plan)
32335
+ })),
32336
+ total,
32337
+ estimate: null,
32338
+ estimateNote: NO_ESTIMATE,
32339
+ credits: { balance }
32340
+ });
32341
+ }
32342
+ for (const language of languages) {
32343
+ const head = `${srcLocale} \u2192 ${language.dstLocale}`;
32344
+ if (language.keys === 0) {
32345
+ console.log(`\u2713 ${head}: up to date`);
32346
+ } else {
32347
+ console.log(
32348
+ `\u2022 ${head}: ${language.keys} key(s) to translate, ${language.characters} characters`
32349
32349
  );
32350
32350
  }
32351
- opts.onProgress?.(summary);
32352
- if (isJobFinished(summary.status)) return summary;
32353
- if (now() >= deadline) {
32354
- throw new JobWaitTimeoutError(jobId, timeoutMs);
32351
+ for (const line of describePlan(language.plan)) {
32352
+ console.log(` ${line}`);
32353
+ }
32354
+ if (opts.verbose) {
32355
+ for (const task of language.tasks) {
32356
+ console.log(` ${task.key}: ${JSON.stringify(task.src)}`);
32357
+ }
32355
32358
  }
32356
- await sleep(pollMs);
32357
32359
  }
32358
- }
32359
- async function runTranslationJob(params) {
32360
- const {
32361
- projectId,
32362
- organizationId: organizationId2,
32363
- framework,
32364
- srcLocale,
32365
- dstLocale,
32366
- tasks,
32367
- onProgress
32368
- } = params;
32369
- const convexClient = await getAuthenticatedConvexClient();
32370
- const jobId = await convexClient.action(api.translations.startJob, {
32371
- organizationId: organizationId2,
32372
- projectId,
32373
- srcLocale,
32374
- dstLocale,
32375
- framework,
32376
- tasks: tasks.map((t) => ({
32377
- key: t.key,
32378
- namespace: t.namespace,
32379
- path: t.path,
32380
- src: t.src,
32381
- hash: t.hash,
32382
- context: t.context
32383
- }))
32384
- });
32385
- const summary = await waitForJob(
32386
- jobId,
32387
- () => convexClient.query(api.translations.getJobSummary, {
32388
- organizationId: organizationId2,
32389
- jobId
32390
- }),
32391
- { onProgress }
32360
+ console.log(
32361
+ `
32362
+ In all: ${total.keys} key(s), ${total.characters} characters.` + (balance === null ? "" : ` Credit balance: ${balance}.`)
32392
32363
  );
32393
- return { jobId, summary };
32364
+ console.log(NO_ESTIMATE);
32365
+ console.log("Nothing was started. Run without --dry-run to translate.");
32394
32366
  }
32395
-
32396
- // src/commands/translate/run.tsx
32397
- import { jsx as jsx20 } from "react/jsx-runtime";
32398
- async function runTranslate(opts) {
32399
- const ctx = await loadPlanContext(process.cwd());
32367
+ function Working({ text }) {
32368
+ return /* @__PURE__ */ jsxs17(Box17, { children: [
32369
+ /* @__PURE__ */ jsx17(Spinner14, {}),
32370
+ /* @__PURE__ */ jsxs17(Text17, { children: [
32371
+ " ",
32372
+ text
32373
+ ] })
32374
+ ] });
32375
+ }
32376
+ function progressText(dstLocale, summary) {
32377
+ if (!summary) return `${dstLocale}: starting\u2026`;
32378
+ const done = summary.completed + summary.failed;
32379
+ return `${dstLocale}: translating ${done}/${summary.total}\u2026`;
32380
+ }
32381
+ function describeOutcome(outcome) {
32382
+ const { dstLocale, status, keys, written } = outcome;
32383
+ const wrote = written ? `, ${written.keys} written to ${written.files} file(s)` : "";
32384
+ const notApplied = outcome.notApplied.length > 0 ? `, ${outcome.notApplied.length} not applied` : "";
32385
+ switch (status) {
32386
+ case "up-to-date":
32387
+ return `\u2713 ${dstLocale}: up to date`;
32388
+ case "started":
32389
+ return `\u2192 ${dstLocale}: started job ${outcome.jobId} with ${keys} key(s)`;
32390
+ case "succeeded":
32391
+ return `\u2713 ${dstLocale}: ${outcome.completed} translated${wrote}${notApplied}`;
32392
+ case "partial":
32393
+ return `\u26A0 ${dstLocale}: ${outcome.completed} of ${keys} translated, ${outcome.failed} failed${wrote}${notApplied}`;
32394
+ case "failed":
32395
+ return `\u2717 ${dstLocale}: the job failed (${outcome.jobId})${wrote}${notApplied}`;
32396
+ case "cancelled":
32397
+ return `\u2717 ${dstLocale}: the job was cancelled (${outcome.jobId})`;
32398
+ case "timed-out":
32399
+ return `\u2717 ${dstLocale}: ${outcome.error}`;
32400
+ case "not-started":
32401
+ return `\u2717 ${dstLocale}: not started. ${outcome.error}`;
32402
+ case "pending":
32403
+ case "running":
32404
+ return `\u2026 ${dstLocale}: ${status}`;
32405
+ }
32406
+ }
32407
+ async function runLanguages(ctx, srcLocale, dstLocales, opts) {
32400
32408
  const { project, framework } = ctx;
32401
- const projectId = project._id;
32402
- const srcLocale = opts.src ?? project.baseLanguage;
32403
- let dstLocales;
32404
- if (opts.all) {
32405
- dstLocales = project.targetLanguages;
32406
- if (dstLocales.length === 0) {
32407
- throw new CliError(
32408
- "No target languages configured.",
32409
- `Configure them in the project settings at ${env.TOLKBOT_WEB_URL}/projects/${project.slug}/settings.`,
32410
- "no_target_languages"
32411
- );
32412
- }
32413
- } else if (opts.dst) {
32414
- dstLocales = [opts.dst];
32415
- } else {
32416
- throw new UsageError(
32417
- "Either --dst <locale> or --all must be provided."
32409
+ const json2 = opts.json ?? false;
32410
+ const outcomes = [];
32411
+ let failure2 = null;
32412
+ let stopped = null;
32413
+ const report = (outcome) => {
32414
+ outcomes.push(outcome);
32415
+ if (json2) return;
32416
+ console.log(describeOutcome(outcome));
32417
+ for (const line of describeNotApplied(outcome.notApplied)) {
32418
+ console.log(` ${line}`);
32419
+ }
32420
+ };
32421
+ for (const dstLocale of dstLocales) {
32422
+ const progress = renderTransient(
32423
+ /* @__PURE__ */ jsx17(Working, { text: `${dstLocale}: planning\u2026` })
32418
32424
  );
32419
- }
32420
- const planOpts = { retranslateUntracked: opts.retranslateUntracked };
32421
- const tasksFor = async (dstLocale) => opts.tasks ? { tasks: await loadTasksFromFile(opts.tasks), plan: void 0 } : await generateTasks(ctx, srcLocale, dstLocale, planOpts);
32422
- if (opts.json) {
32423
- const results = [];
32424
- let failure3 = null;
32425
- for (const dstLocale of dstLocales) {
32426
- const { tasks, plan } = await tasksFor(dstLocale);
32427
- const summary = plan ? summarize(plan) : void 0;
32425
+ try {
32426
+ const { tasks, plan } = await generateTasks(
32427
+ ctx,
32428
+ srcLocale,
32429
+ dstLocale,
32430
+ { retranslateUntracked: opts.retranslateUntracked }
32431
+ );
32432
+ const base = {
32433
+ dstLocale,
32434
+ status: "up-to-date",
32435
+ jobId: null,
32436
+ keys: tasks.length,
32437
+ completed: null,
32438
+ failed: null,
32439
+ written: null,
32440
+ notApplied: [],
32441
+ error: null,
32442
+ plan: planOutput(plan)
32443
+ };
32444
+ const leftOut = describeLeftOut(plan);
32445
+ if (stopped !== null) {
32446
+ progress.unmount();
32447
+ report({ ...base, status: "not-started", error: stopped });
32448
+ continue;
32449
+ }
32428
32450
  if (tasks.length === 0) {
32429
- results.push({
32430
- dstLocale,
32431
- status: "up-to-date",
32432
- plan: summary
32433
- });
32451
+ progress.unmount();
32452
+ report(base);
32453
+ printNotes(json2, leftOut);
32434
32454
  continue;
32435
32455
  }
32456
+ progress.rerender(/* @__PURE__ */ jsx17(Working, { text: progressText(dstLocale, null) }));
32457
+ let jobId;
32436
32458
  try {
32437
- const { jobId, summary: job } = await runTranslationJob({
32438
- projectId,
32459
+ jobId = await startTranslationJob({
32460
+ projectId: project._id,
32439
32461
  organizationId: project.organizationId,
32440
32462
  framework,
32441
32463
  srcLocale,
32442
32464
  dstLocale,
32443
32465
  tasks
32444
32466
  });
32445
- results.push({
32446
- dstLocale,
32447
- jobId,
32448
- status: job.status,
32449
- completed: job.completed,
32450
- failed: job.failed,
32451
- total: job.total,
32452
- error: job.status === "cancelled" ? "The job was cancelled." : void 0,
32453
- plan: summary
32454
- });
32455
- failure3 ??= unsuccessfulJobError(jobId, job.status);
32456
32467
  } catch (error62) {
32457
- if (!(error62 instanceof JobWaitTimeoutError)) throw error62;
32458
- results.push({
32459
- dstLocale,
32460
- jobId: error62.jobId,
32461
- status: "timed-out",
32462
- error: error62.message,
32463
- plan: summary
32468
+ const refused = toCliError(error62);
32469
+ failure2 ??= refused;
32470
+ progress.unmount();
32471
+ report({
32472
+ ...base,
32473
+ status: "not-started",
32474
+ error: refused.baseMessage
32464
32475
  });
32465
- failure3 ??= error62;
32466
- break;
32476
+ if (exitCodeFor(refused.code) === EXIT_CODES.billing) {
32477
+ stopped = `TolkBot refused the ${dstLocale} job: ${refused.baseMessage}`;
32478
+ }
32479
+ continue;
32467
32480
  }
32468
- if (results.at(-1)?.status === "cancelled") break;
32469
- }
32470
- track("cli_translate_completed", {
32471
- mode: "run",
32472
- key_count: results.reduce((n, r) => n + (r.total ?? 0), 0),
32473
- locale_count: results.length,
32474
- job_count: results.filter((r) => r.jobId).length,
32475
- json: true
32476
- });
32477
- if (!failure3) jsonSuccess({ jobs: results });
32478
- jsonResult({ jobs: results }, 1);
32479
- fail(failure3);
32480
- }
32481
- const jobs = dstLocales.map((locale) => ({
32482
- dstLocale: locale,
32483
- status: "waiting"
32484
- }));
32485
- const { rerender, unmount } = render19(
32486
- /* @__PURE__ */ jsx20(RunUI, { srcLocale, jobs })
32487
- );
32488
- const updateJob = (locale, update) => {
32489
- const index = jobs.findIndex((j) => j.dstLocale === locale);
32490
- const currentJob = jobs[index];
32491
- if (index !== -1 && currentJob) {
32492
- jobs[index] = {
32493
- ...currentJob,
32494
- ...update,
32495
- dstLocale: currentJob.dstLocale,
32496
- status: update.status ?? currentJob.status
32497
- };
32498
- rerender(/* @__PURE__ */ jsx20(RunUI, { srcLocale, jobs }));
32499
- }
32500
- };
32501
- const skipRemaining = (locale, why) => {
32502
- for (const later of dstLocales.slice(dstLocales.indexOf(locale) + 1)) {
32503
- updateJob(later, {
32504
- status: "error",
32505
- error: `Not started, because ${why} the ${locale} job.`
32506
- });
32507
- }
32508
- };
32509
- let failure2 = null;
32510
- try {
32511
- for (const dstLocale of dstLocales) {
32512
- updateJob(dstLocale, { status: "generating" });
32513
- const { tasks, plan } = await tasksFor(dstLocale);
32514
- const notes = plan ? describePlan(plan) : [];
32515
- if (tasks.length === 0) {
32516
- updateJob(dstLocale, {
32517
- status: "completed",
32518
- taskCount: 0,
32519
- notes
32520
- });
32481
+ if (!opts.wait) {
32482
+ progress.unmount();
32483
+ report({ ...base, status: "started", jobId });
32484
+ printNotes(json2, leftOut);
32521
32485
  continue;
32522
32486
  }
32523
- updateJob(dstLocale, {
32524
- status: "translating",
32525
- taskCount: tasks.length,
32526
- notes
32527
- });
32528
- let outcome;
32487
+ let summary;
32529
32488
  try {
32530
- outcome = await runTranslationJob({
32531
- projectId,
32532
- organizationId: project.organizationId,
32533
- framework,
32534
- srcLocale,
32535
- dstLocale,
32536
- tasks,
32537
- onProgress: (progress) => {
32538
- updateJob(dstLocale, { progress });
32539
- }
32540
- });
32489
+ summary = await waitForTranslationJob(
32490
+ project.organizationId,
32491
+ jobId,
32492
+ (latest) => progress.rerender(
32493
+ /* @__PURE__ */ jsx17(Working, { text: progressText(dstLocale, latest) })
32494
+ )
32495
+ );
32541
32496
  } catch (error62) {
32542
32497
  if (!(error62 instanceof JobWaitTimeoutError)) throw error62;
32543
- updateJob(dstLocale, {
32544
- status: "error",
32545
- jobId: error62.jobId,
32546
- error: error62.message
32547
- });
32548
32498
  failure2 ??= error62;
32549
- skipRemaining(dstLocale, "the CLI stopped waiting for");
32550
- break;
32551
- }
32552
- const { jobId, summary } = outcome;
32553
- if (summary.status === "failed") {
32554
- updateJob(dstLocale, {
32555
- status: "error",
32556
- progress: summary,
32557
- jobId,
32558
- error: "Translation failed"
32559
- });
32560
- failure2 ??= unsuccessfulJobError(jobId, summary.status);
32561
- } else if (summary.status === "cancelled") {
32562
- updateJob(dstLocale, {
32563
- status: "error",
32564
- progress: summary,
32499
+ progress.unmount();
32500
+ report({
32501
+ ...base,
32502
+ status: "timed-out",
32565
32503
  jobId,
32566
- error: "The job was cancelled."
32567
- });
32568
- failure2 ??= unsuccessfulJobError(jobId, summary.status);
32569
- skipRemaining(dstLocale, "someone cancelled");
32570
- break;
32571
- } else {
32572
- updateJob(dstLocale, {
32573
- status: "completed",
32574
- progress: summary,
32575
- jobId
32504
+ error: error62.baseMessage
32576
32505
  });
32506
+ stopped = `The CLI stopped waiting for the ${dstLocale} job.`;
32507
+ continue;
32508
+ }
32509
+ const finished = {
32510
+ ...base,
32511
+ status: summary.status,
32512
+ jobId,
32513
+ completed: summary.completed,
32514
+ failed: summary.failed
32515
+ };
32516
+ if (summary.status !== "cancelled") {
32517
+ progress.rerender(
32518
+ /* @__PURE__ */ jsx17(Working, { text: `${dstLocale}: writing the files\u2026` })
32519
+ );
32520
+ const applied = await applyJob(ctx, jobId);
32521
+ finished.written = { files: applied.files, keys: applied.keys };
32522
+ finished.notApplied = applied.notApplied;
32523
+ }
32524
+ const unsuccessful = unsuccessfulJobError(jobId, summary.status);
32525
+ if (unsuccessful) {
32526
+ failure2 ??= unsuccessful;
32527
+ finished.error = unsuccessful.baseMessage;
32577
32528
  }
32529
+ progress.unmount();
32530
+ report(finished);
32531
+ printNotes(json2, leftOut);
32532
+ if (summary.status === "cancelled") {
32533
+ stopped = `The ${dstLocale} job was cancelled.`;
32534
+ }
32535
+ } finally {
32536
+ progress.unmount();
32578
32537
  }
32579
- track("cli_translate_completed", {
32580
- mode: "run",
32581
- key_count: jobs.reduce((n, j) => n + (j.taskCount ?? 0), 0),
32582
- locale_count: dstLocales.length,
32583
- job_count: jobs.filter((j) => j.jobId).length,
32584
- json: false
32585
- });
32586
- } finally {
32587
- unmount();
32588
- }
32589
- if (failure2) fail(failure2);
32590
- }
32591
- function summarize(plan) {
32592
- return {
32593
- ...plan.summary,
32594
- protectedKeys: plan.protectedKeys,
32595
- heldKeys: plan.heldKeys,
32596
- untrackedKeys: plan.untrackedKeys.map((k) => k.key)
32597
- };
32598
- }
32599
-
32600
- // src/commands/translate/status.tsx
32601
- import { Spinner as Spinner17 } from "@inkjs/ui";
32602
- import { Box as Box20, render as render20, Text as Text20 } from "ink";
32603
- import { jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
32604
- function StatusUI({ state }) {
32605
- if (state.type === "loading") {
32606
- return /* @__PURE__ */ jsxs20(Box20, { children: [
32607
- /* @__PURE__ */ jsx21(Spinner17, {}),
32608
- /* @__PURE__ */ jsx21(Text20, { children: " Loading job status\u2026" })
32609
- ] });
32610
- }
32611
- const { summary } = state;
32612
- const processed = summary.completed + summary.failed;
32613
- const progress = summary.total > 0 ? Math.round(processed / summary.total * 100) : 0;
32614
- if (state.type === "running") {
32615
- return /* @__PURE__ */ jsxs20(Box20, { flexDirection: "column", children: [
32616
- /* @__PURE__ */ jsxs20(Box20, { children: [
32617
- /* @__PURE__ */ jsx21(Spinner17, {}),
32618
- /* @__PURE__ */ jsx21(Text20, { children: " Job is running\u2026" })
32619
- ] }),
32620
- /* @__PURE__ */ jsx21(Box20, { marginTop: 1, children: /* @__PURE__ */ jsxs20(Text20, { children: [
32621
- "Progress: ",
32622
- processed,
32623
- "/",
32624
- summary.total,
32625
- " (",
32626
- progress,
32627
- "%)"
32628
- ] }) }),
32629
- /* @__PURE__ */ jsx21(Box20, { children: /* @__PURE__ */ jsxs20(Text20, { dimColor: true, children: [
32630
- "Completed: ",
32631
- summary.completed,
32632
- " | Failed:",
32633
- " ",
32634
- summary.failed
32635
- ] }) }),
32636
- /* @__PURE__ */ jsx21(Box20, { marginTop: 1, children: /* @__PURE__ */ jsxs20(Text20, { dimColor: true, children: [
32637
- "Status: ",
32638
- summary.status
32639
- ] }) })
32640
- ] });
32641
32538
  }
32642
- const statusColor = summary.status === "succeeded" ? "green" : summary.status === "partial" ? "yellow" : "red";
32643
- const statusIcon = summary.status === "succeeded" ? "\u2713" : summary.status === "partial" ? "\u26A0" : "\u2717";
32644
- const statusText = summary.status === "succeeded" ? "completed successfully" : summary.status === "partial" ? "completed with some failures" : summary.status === "cancelled" ? "was cancelled" : "failed";
32645
- return /* @__PURE__ */ jsxs20(Box20, { flexDirection: "column", children: [
32646
- /* @__PURE__ */ jsxs20(Box20, { children: [
32647
- /* @__PURE__ */ jsx21(Text20, { color: statusColor, children: statusIcon }),
32648
- /* @__PURE__ */ jsxs20(Text20, { children: [
32649
- " Job ",
32650
- statusText
32651
- ] })
32652
- ] }),
32653
- /* @__PURE__ */ jsx21(Box20, { marginTop: 1, children: /* @__PURE__ */ jsxs20(Text20, { children: [
32654
- "Completed: ",
32655
- summary.completed,
32656
- " | Failed: ",
32657
- summary.failed,
32658
- " | Total: ",
32659
- summary.total
32660
- ] }) }),
32661
- /* @__PURE__ */ jsx21(Box20, { marginTop: 1, children: /* @__PURE__ */ jsxs20(Text20, { dimColor: true, children: [
32662
- "Status: ",
32663
- summary.status
32664
- ] }) })
32665
- ] });
32666
- }
32667
- async function fetchJobSummary(jobId) {
32668
- const { client: client3, project } = await loadPlanContext(process.cwd());
32669
- const summary = await client3.query(api.translations.getJobSummary, {
32670
- organizationId: project.organizationId,
32671
- jobId
32539
+ track("cli_translate_completed", {
32540
+ mode: "run",
32541
+ key_count: outcomes.reduce((n, o) => n + o.keys, 0),
32542
+ locale_count: outcomes.length,
32543
+ job_count: outcomes.filter((o) => o.jobId).length,
32544
+ json: json2
32672
32545
  });
32673
- if (!summary) {
32674
- throw new CliError(
32675
- `Translation job ${jobId} was not found in this project.`,
32676
- void 0,
32677
- "job_not_found"
32546
+ const payload = { srcLocale, languages: outcomes };
32547
+ if (json2) {
32548
+ if (!failure2) jsonSuccess(payload);
32549
+ jsonResult(payload, 1);
32550
+ fail(failure2);
32551
+ }
32552
+ if (!opts.wait && outcomes.some((o) => o.status === "started")) {
32553
+ console.log(
32554
+ "\nThe jobs keep running. Follow them with 'tolkbot jobs list' and write them into the files with 'tolkbot jobs apply'."
32678
32555
  );
32679
32556
  }
32680
- return summary;
32557
+ if (failure2) fail(failure2);
32681
32558
  }
32682
- async function runStatus(opts) {
32683
- if (opts.json) {
32684
- const summary2 = await fetchJobSummary(opts.jobId);
32685
- const output2 = {
32686
- jobId: opts.jobId,
32687
- status: summary2.status,
32688
- completed: summary2.completed,
32689
- failed: summary2.failed,
32690
- total: summary2.total
32691
- };
32692
- const failure2 = unsuccessfulJobError(opts.jobId, summary2.status);
32693
- if (!failure2) jsonSuccess(output2);
32694
- jsonResult(output2, 1);
32695
- fail(failure2);
32559
+ function printNotes(json2, notes) {
32560
+ if (json2) return;
32561
+ for (const note of notes) console.log(` ${note}`);
32562
+ }
32563
+ function checkTranslateOptions(opts) {
32564
+ if (!opts.dst && !opts.all) {
32565
+ throw new UsageError(
32566
+ "Either --dst <locale> or --all must be provided.",
32567
+ "Run 'tolkbot translate --help'."
32568
+ );
32696
32569
  }
32697
- const { unmount } = renderTransient(
32698
- /* @__PURE__ */ jsx21(StatusUI, { state: { type: "loading" } })
32699
- );
32700
- let summary;
32701
- try {
32702
- summary = await fetchJobSummary(opts.jobId);
32703
- } finally {
32704
- unmount();
32570
+ if (opts.dst && opts.all) {
32571
+ throw new UsageError("Cannot use both --dst and --all.");
32705
32572
  }
32706
- const finalUi = render20(
32707
- /* @__PURE__ */ jsx21(
32708
- StatusUI,
32709
- {
32710
- state: {
32711
- type: isJobFinished(summary.status) ? "completed" : "running",
32712
- summary
32713
- }
32714
- }
32715
- )
32716
- );
32717
- finalUi.unmount();
32718
- if (isJobUnsuccessful(summary.status)) {
32719
- const failure2 = unsuccessfulJobError(opts.jobId, summary.status);
32720
- if (failure2) fail(failure2);
32573
+ if (opts.dryRun && !opts.wait) {
32574
+ throw new UsageError(
32575
+ "Cannot use --dry-run with --no-wait: a dry run starts nothing."
32576
+ );
32721
32577
  }
32722
32578
  }
32723
32579
 
32724
32580
  // src/commands/translate/index.ts
32725
- var translateCommand = new Command5("translate").description(
32726
- "Translate with TolkBot"
32727
- );
32728
32581
  var RETRANSLATE_UNTRACKED_HELP = "Also retranslate translations TolkBot has no record of (kept by default)";
32729
- translateCommand.command("plan").description(
32730
- "Plan what needs translating: missing keys, and keys whose source changed since they were translated"
32582
+ var translateCommand = new Command6("translate").description(
32583
+ "Translate what is missing or changed, and write the results into your locale files"
32731
32584
  ).option(
32732
32585
  "--src <locale>",
32733
32586
  "Source locale (defaults to the project's base language)"
32734
- ).requiredOption("--dst <locale>", "Destination locale").option("--write <path>", "Write tasks to JSON file instead of stdout").option("--retranslate-untracked", RETRANSLATE_UNTRACKED_HELP).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
32735
- const schema = z7.object({
32736
- src: z7.string().min(1).optional(),
32737
- dst: z7.string().min(1),
32738
- write: z7.string().optional(),
32739
- retranslateUntracked: z7.boolean().optional(),
32740
- json: z7.boolean().optional()
32741
- });
32742
- const parsed = validateOrExit(schema, opts);
32743
- await runPlan(parsed);
32744
- });
32745
- translateCommand.command("run").description("Run translation job (auto-generates tasks if not provided)").option(
32746
- "--src <locale>",
32747
- "Source locale (defaults to repository base language)"
32748
- ).option("--dst <locale>", "Destination locale").option("--all", "Translate to all target languages").option("--tasks <path>", "Path to tasks JSON array (optional)").option("--retranslate-untracked", RETRANSLATE_UNTRACKED_HELP).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
32749
- const schema = z7.object({
32750
- src: z7.string().optional(),
32751
- dst: z7.string().optional(),
32752
- all: z7.boolean().optional(),
32753
- tasks: z7.string().optional(),
32754
- retranslateUntracked: z7.boolean().optional(),
32755
- json: z7.boolean().optional()
32756
- });
32757
- const parsed = validateOrExit(schema, opts);
32758
- if (!parsed.dst && !parsed.all) {
32587
+ ).option("--dst <locale>", "Destination locale").option("--all", "Every target language").option("--dry-run", "Print what a run would translate, and start nothing").option(
32588
+ "--no-wait",
32589
+ "Start the jobs and print their IDs without waiting; apply them later with 'tolkbot jobs apply'"
32590
+ ).option("--retranslate-untracked", RETRANSLATE_UNTRACKED_HELP).option("--verbose", "With --dry-run, include each key's source string").option("--json", "Output as JSON (machine-readable)").allowExcessArguments(true).enablePositionalOptions().action(async (opts, command) => {
32591
+ const [word] = command.args;
32592
+ if (word !== void 0) {
32759
32593
  throw new UsageError(
32760
- "Either --dst <locale> or --all must be provided."
32594
+ `unknown command 'translate ${word}'`,
32595
+ "Run 'tolkbot translate --help'. Jobs are under 'tolkbot jobs'."
32761
32596
  );
32762
32597
  }
32763
- if (parsed.dst && parsed.all) {
32764
- throw new UsageError("Cannot use both --dst and --all.");
32765
- }
32598
+ const schema = z8.object({
32599
+ src: z8.string().min(1).optional(),
32600
+ dst: z8.string().min(1).optional(),
32601
+ all: z8.boolean().optional(),
32602
+ dryRun: z8.boolean().optional(),
32603
+ wait: z8.boolean(),
32604
+ retranslateUntracked: z8.boolean().optional(),
32605
+ verbose: z8.boolean().optional(),
32606
+ json: z8.boolean().optional()
32607
+ });
32608
+ const parsed = validateOrExit(schema, opts);
32609
+ checkTranslateOptions(parsed);
32766
32610
  await runTranslate(parsed);
32767
32611
  });
32768
32612
  translateCommand.command("adopt").description(
@@ -32771,11 +32615,11 @@ translateCommand.command("adopt").description(
32771
32615
  "--src <locale>",
32772
32616
  "Source locale (defaults to the project's base language)"
32773
32617
  ).option("--dst <locale>", "Destination locale").option("--all", "Every target language").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
32774
- const schema = z7.object({
32775
- src: z7.string().min(1).optional(),
32776
- dst: z7.string().min(1).optional(),
32777
- all: z7.boolean().optional(),
32778
- json: z7.boolean().optional()
32618
+ const schema = z8.object({
32619
+ src: z8.string().min(1).optional(),
32620
+ dst: z8.string().min(1).optional(),
32621
+ all: z8.boolean().optional(),
32622
+ json: z8.boolean().optional()
32779
32623
  });
32780
32624
  const parsed = validateOrExit(schema, opts);
32781
32625
  if (parsed.dst && parsed.all) {
@@ -32783,46 +32627,20 @@ translateCommand.command("adopt").description(
32783
32627
  }
32784
32628
  await runAdopt(parsed);
32785
32629
  });
32786
- translateCommand.command("cancel").description("Cancel translation job by job ID").requiredOption("--job-id <id>", "Job ID to cancel").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
32787
- const schema = z7.object({
32788
- jobId: z7.string().min(1),
32789
- json: z7.boolean().optional()
32790
- });
32791
- const parsed = validateOrExit(schema, opts);
32792
- await runCancel(parsed);
32793
- });
32794
- translateCommand.command("apply").description(
32795
- "Apply finished translation jobs to the working tree. Without --job-id: the one unapplied job per language, or a picker in a terminal"
32796
- ).option("--job-id <id...>", "Job ID(s) to apply").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
32797
- const schema = z7.object({
32798
- jobId: z7.array(z7.string()).optional(),
32799
- json: z7.boolean().optional()
32800
- });
32801
- const parsed = validateOrExit(schema, opts);
32802
- await runApply(parsed);
32803
- });
32804
32630
  translateCommand.command("edit").description("Edit a translation value in a locale file").requiredOption("--key <key>", "Translation key (e.g. common.hello)").requiredOption("--locale <locale>", "Target locale (e.g. fr, de)").requiredOption("--value <text>", "New translation value").option("--namespace <ns>", "Namespace to resolve file (if ambiguous)").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
32805
- const schema = z7.object({
32806
- key: z7.string().min(1),
32807
- locale: z7.string().min(1),
32808
- value: z7.string().min(1),
32809
- namespace: z7.string().optional(),
32810
- json: z7.boolean().optional()
32631
+ const schema = z8.object({
32632
+ key: z8.string().min(1),
32633
+ locale: z8.string().min(1),
32634
+ value: z8.string().min(1),
32635
+ namespace: z8.string().optional(),
32636
+ json: z8.boolean().optional()
32811
32637
  });
32812
32638
  const parsed = validateOrExit(schema, opts);
32813
32639
  await runEdit(parsed);
32814
32640
  });
32815
- translateCommand.command("status").description("Check status of translation job by job ID").requiredOption("--job-id <id>", "Job ID to check status").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
32816
- const schema = z7.object({
32817
- jobId: z7.string().min(1),
32818
- json: z7.boolean().optional()
32819
- });
32820
- const parsed = validateOrExit(schema, opts);
32821
- await runStatus(parsed);
32822
- });
32823
32641
 
32824
32642
  // src/commands/validate.tsx
32825
- import { Box as Box21, render as render21, Text as Text21 } from "ink";
32643
+ import { Box as Box18, render as render17, Text as Text18 } from "ink";
32826
32644
 
32827
32645
  // ../ai/src/validators/placeholders.ts
32828
32646
  var IDENTIFIER = /^[A-Za-z0-9_$][A-Za-z0-9_$.[\]-]*$/;
@@ -33915,7 +33733,7 @@ function finalize2(issues, stats, strict) {
33915
33733
  }
33916
33734
 
33917
33735
  // src/commands/validate.tsx
33918
- import { jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
33736
+ import { jsx as jsx18, jsxs as jsxs18 } from "react/jsx-runtime";
33919
33737
  var LEVEL_COLOR = {
33920
33738
  error: "red",
33921
33739
  warning: "yellow",
@@ -33924,12 +33742,12 @@ var LEVEL_COLOR = {
33924
33742
  var GUTTER = 9;
33925
33743
  function IssueLine({ issue: issue2 }) {
33926
33744
  const location = [issue2.file, issue2.key].filter(Boolean).join(" \xB7 ");
33927
- return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", children: [
33928
- /* @__PURE__ */ jsxs21(Box21, { children: [
33929
- /* @__PURE__ */ jsx22(Box21, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx22(Text21, { color: LEVEL_COLOR[issue2.level], children: issue2.level }) }),
33930
- /* @__PURE__ */ jsx22(Box21, { flexGrow: 1, children: /* @__PURE__ */ jsx22(Text21, { children: issue2.message }) })
33745
+ return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
33746
+ /* @__PURE__ */ jsxs18(Box18, { children: [
33747
+ /* @__PURE__ */ jsx18(Box18, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx18(Text18, { color: LEVEL_COLOR[issue2.level], children: issue2.level }) }),
33748
+ /* @__PURE__ */ jsx18(Box18, { flexGrow: 1, children: /* @__PURE__ */ jsx18(Text18, { children: issue2.message }) })
33931
33749
  ] }),
33932
- /* @__PURE__ */ jsx22(Box21, { marginLeft: GUTTER, children: /* @__PURE__ */ jsxs21(Text21, { dimColor: true, children: [
33750
+ /* @__PURE__ */ jsx18(Box18, { marginLeft: GUTTER, children: /* @__PURE__ */ jsxs18(Text18, { dimColor: true, children: [
33933
33751
  issue2.rule,
33934
33752
  location ? ` \u2014 ${location}` : ""
33935
33753
  ] }) })
@@ -33938,20 +33756,20 @@ function IssueLine({ issue: issue2 }) {
33938
33756
  function ReportUI({ report }) {
33939
33757
  const { counts, stats } = report;
33940
33758
  const summary = ["error", "warning", "info"].filter((level) => counts[level] > 0).map((level) => `${counts[level]} ${level}(s)`).join(", ");
33941
- return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", children: [
33942
- /* @__PURE__ */ jsxs21(Box21, { children: [
33943
- /* @__PURE__ */ jsx22(Text21, { color: report.valid ? "green" : "red", children: report.valid ? "\u2713" : "\u2717" }),
33944
- /* @__PURE__ */ jsxs21(Text21, { children: [
33759
+ return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
33760
+ /* @__PURE__ */ jsxs18(Box18, { children: [
33761
+ /* @__PURE__ */ jsx18(Text18, { color: report.valid ? "green" : "red", children: report.valid ? "\u2713" : "\u2717" }),
33762
+ /* @__PURE__ */ jsxs18(Text18, { children: [
33945
33763
  " Validation ",
33946
33764
  report.valid ? "passed" : "failed"
33947
33765
  ] }),
33948
- summary ? /* @__PURE__ */ jsxs21(Text21, { dimColor: true, children: [
33766
+ summary ? /* @__PURE__ */ jsxs18(Text18, { dimColor: true, children: [
33949
33767
  " \u2014 ",
33950
33768
  summary
33951
33769
  ] }) : null
33952
33770
  ] }),
33953
- stats.locales.length > 0 && /* @__PURE__ */ jsxs21(Box21, { marginTop: 1, flexDirection: "column", children: [
33954
- /* @__PURE__ */ jsxs21(Text21, { dimColor: true, children: [
33771
+ stats.locales.length > 0 && /* @__PURE__ */ jsxs18(Box18, { marginTop: 1, flexDirection: "column", children: [
33772
+ /* @__PURE__ */ jsxs18(Text18, { dimColor: true, children: [
33955
33773
  stats.filesChecked,
33956
33774
  " ",
33957
33775
  stats.format,
@@ -33960,18 +33778,18 @@ function ReportUI({ report }) {
33960
33778
  stats.totalKeys,
33961
33779
  " keys"
33962
33780
  ] }),
33963
- /* @__PURE__ */ jsxs21(Text21, { dimColor: true, children: [
33781
+ /* @__PURE__ */ jsxs18(Text18, { dimColor: true, children: [
33964
33782
  "Locales: ",
33965
33783
  stats.locales.join(", "),
33966
33784
  stats.baseLocale ? ` (base: ${stats.baseLocale})` : ""
33967
33785
  ] }),
33968
- stats.pattern && /* @__PURE__ */ jsxs21(Text21, { dimColor: true, children: [
33786
+ stats.pattern && /* @__PURE__ */ jsxs18(Text18, { dimColor: true, children: [
33969
33787
  "Pattern: ",
33970
33788
  stats.pattern
33971
33789
  ] })
33972
33790
  ] }),
33973
- report.issues.length > 0 && /* @__PURE__ */ jsx22(Box21, { marginTop: 1, flexDirection: "column", gap: 1, children: report.issues.map((issue2, idx) => /* @__PURE__ */ jsx22(IssueLine, { issue: issue2 }, idx)) }),
33974
- !report.strict && counts.warning > 0 && report.valid && /* @__PURE__ */ jsx22(Box21, { marginTop: 1, children: /* @__PURE__ */ jsx22(Text21, { dimColor: true, children: "Warnings did not fail this run. Use --strict to treat them as errors." }) })
33791
+ report.issues.length > 0 && /* @__PURE__ */ jsx18(Box18, { marginTop: 1, flexDirection: "column", gap: 1, children: report.issues.map((issue2, idx) => /* @__PURE__ */ jsx18(IssueLine, { issue: issue2 }, idx)) }),
33792
+ !report.strict && counts.warning > 0 && report.valid && /* @__PURE__ */ jsx18(Box18, { marginTop: 1, children: /* @__PURE__ */ jsx18(Text18, { dimColor: true, children: "Warnings did not fail this run. Use --strict to treat them as errors." }) })
33975
33793
  ] });
33976
33794
  }
33977
33795
  function emit(report, json2) {
@@ -33991,7 +33809,7 @@ function emit(report, json2) {
33991
33809
  if (!report.valid) fail(validationFailure(report));
33992
33810
  return;
33993
33811
  }
33994
- render21(/* @__PURE__ */ jsx22(ReportUI, { report })).unmount();
33812
+ render17(/* @__PURE__ */ jsx18(ReportUI, { report })).unmount();
33995
33813
  process.exitCode = exitCode;
33996
33814
  }
33997
33815
  function validationFailure(report) {
@@ -34050,12 +33868,9 @@ function commandPath(command) {
34050
33868
  return names.join(" ");
34051
33869
  }
34052
33870
  function buildProgram() {
34053
- const program = new Command6().name("tolkbot").description("Localization workflow for your repo").version(version, "-v, --version", "Output the version number").helpOption("-h, --help", "Show help").helpCommand("help [command]", "Show help for a command").option(
34054
- "--token <token>",
34055
- `Auth token to use for this invocation (overrides $${TOKEN_ENV_VAR} and the stored token)`
34056
- );
34057
- program.hook("preAction", (thisCommand, actionCommand) => {
34058
- const { token } = thisCommand.opts();
33871
+ const program = new Command7().name("tolkbot").description("Localization workflow for your repo").version(version, "-v, --version", "Output the version number").helpOption("-h, --help", "Show help").helpCommand("help [command]", "Show help for a command").option("--token <token>", TOKEN_HELP).enablePositionalOptions();
33872
+ program.hook("preAction", (_thisCommand, actionCommand) => {
33873
+ const { token } = actionCommand.optsWithGlobals();
34059
33874
  setTokenOverride(token);
34060
33875
  resolveJsonOption(actionCommand);
34061
33876
  beginCommand(commandPath(actionCommand), version);
@@ -34081,11 +33896,13 @@ function buildProgram() {
34081
33896
  );
34082
33897
  program.addCommand(authCommand);
34083
33898
  program.addCommand(glossaryCommand);
33899
+ program.addCommand(jobsCommand);
34084
33900
  program.addCommand(learningsCommand);
34085
33901
  program.addCommand(telemetryCommand);
34086
33902
  program.addCommand(translateCommand);
34087
33903
  program.addHelpText("after", PROGRAM_HELP_FOOTER);
34088
33904
  forEachCommand(program, (command) => {
33905
+ if (command !== program) addTokenOption(command);
34089
33906
  addNoJsonOption(command);
34090
33907
  reportUsageErrors(command);
34091
33908
  });
@@ -34095,6 +33912,11 @@ function forEachCommand(command, visit2) {
34095
33912
  visit2(command);
34096
33913
  for (const sub of command.commands) forEachCommand(sub, visit2);
34097
33914
  }
33915
+ var TOKEN_HELP = `Auth token to use for this invocation (overrides $${TOKEN_ENV_VAR} and the stored token)`;
33916
+ function addTokenOption(command) {
33917
+ if (command.options.some((o) => o.long === "--token")) return;
33918
+ command.addOption(new Option("--token <token>", TOKEN_HELP).hideHelp());
33919
+ }
34098
33920
  function jsonOptionOf(command) {
34099
33921
  return command.options.find((option) => option.long === "--json");
34100
33922
  }
@@ -34133,8 +33955,9 @@ var PROGRAM_HELP_FOOTER = `
34133
33955
  Examples:
34134
33956
  $ tolkbot auth login
34135
33957
  $ tolkbot init
34136
- $ tolkbot translate run --all
34137
- $ tolkbot translate apply
33958
+ $ tolkbot translate --all
33959
+ $ tolkbot translate --all --dry-run
33960
+ $ tolkbot jobs list
34138
33961
  $ tolkbot validate
34139
33962
 
34140
33963
  Without a terminal (an agent, CI), nothing prompts: commands take their