@tolkbot/cli 0.3.0 → 0.5.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 +1820 -1757
  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) {
@@ -559,7 +559,7 @@ var require_to_regex_range = __commonJS({
559
559
  let zipped = zip(start, stop);
560
560
  let digits = zipped.length;
561
561
  let pattern = "";
562
- let count = 0;
562
+ let count2 = 0;
563
563
  for (let i = 0; i < digits; i++) {
564
564
  let [startDigit, stopDigit] = zipped[i];
565
565
  if (startDigit === stopDigit) {
@@ -567,13 +567,13 @@ var require_to_regex_range = __commonJS({
567
567
  } else if (startDigit !== "0" || stopDigit !== "9") {
568
568
  pattern += toCharacterClass(startDigit, stopDigit, options);
569
569
  } else {
570
- count++;
570
+ count2++;
571
571
  }
572
572
  }
573
- if (count) {
573
+ if (count2) {
574
574
  pattern += options.shorthand === true ? "\\d" : "[0-9]";
575
575
  }
576
- return { pattern, count: [count], digits };
576
+ return { pattern, count: [count2], digits };
577
577
  }
578
578
  function splitToPatterns(min, max, tok, options) {
579
579
  let ranges = splitToRanges(min, max);
@@ -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);
@@ -2320,13 +2320,13 @@ var require_parse2 = __commonJS({
2320
2320
  consume(token.value);
2321
2321
  };
2322
2322
  const negate = () => {
2323
- let count = 1;
2323
+ let count2 = 1;
2324
2324
  while (peek() === "!" && (peek(2) !== "(" || peek(3) === "?")) {
2325
2325
  advance();
2326
2326
  state.start++;
2327
- count++;
2327
+ count2++;
2328
2328
  }
2329
- if (count % 2 === 0) {
2329
+ if (count2 % 2 === 0) {
2330
2330
  return false;
2331
2331
  }
2332
2332
  state.negated = true;
@@ -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 Command8, CommanderError, Option } from "commander";
5788
5788
 
5789
5789
  // package.json
5790
- var version = "0.3.0";
5790
+ var version = "0.5.0";
5791
5791
 
5792
5792
  // src/commands/auth/index.ts
5793
5793
  import { Command } from "commander";
@@ -8068,14 +8068,14 @@ function codePointLength(str) {
8068
8068
  const units = str.length;
8069
8069
  if (!highSurrogate.test(str))
8070
8070
  return units;
8071
- let count = units;
8071
+ let count2 = units;
8072
8072
  for (let i = 0; i < units - 1; i++) {
8073
8073
  if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {
8074
- count--;
8074
+ count2--;
8075
8075
  i++;
8076
8076
  }
8077
8077
  }
8078
- return count;
8078
+ return count2;
8079
8079
  }
8080
8080
  function getLengthableOrigin(input2) {
8081
8081
  if (Array.isArray(input2))
@@ -12432,8 +12432,8 @@ function az_default() {
12432
12432
  localeError: error2()
12433
12433
  };
12434
12434
  }
12435
- function getBelarusianPlural(count, one, few, many) {
12436
- const absCount = Math.abs(count);
12435
+ function getBelarusianPlural(count2, one, few, many) {
12436
+ const absCount = Math.abs(count2);
12437
12437
  const lastDigit = absCount % 10;
12438
12438
  const lastTwoDigits = absCount % 100;
12439
12439
  if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
@@ -15038,8 +15038,8 @@ function hu_default() {
15038
15038
  localeError: error23()
15039
15039
  };
15040
15040
  }
15041
- function getArmenianPlural(count, one, many) {
15042
- return Math.abs(count) === 1 ? one : many;
15041
+ function getArmenianPlural(count2, one, many) {
15042
+ return Math.abs(count2) === 1 ? one : many;
15043
15043
  }
15044
15044
  function withDefiniteArticle(word) {
15045
15045
  if (!word)
@@ -17731,8 +17731,8 @@ function ro_default() {
17731
17731
  localeError: error45()
17732
17732
  };
17733
17733
  }
17734
- function getRussianPlural(count, one, few, many) {
17735
- const absCount = Math.abs(count);
17734
+ function getRussianPlural(count2, one, few, many) {
17735
+ const absCount = Math.abs(count2);
17736
17736
  const lastDigit = absCount % 10;
17737
17737
  const lastTwoDigits = absCount % 100;
17738
17738
  if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
@@ -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,
@@ -27354,6 +27354,7 @@ var analyticsEventNames = [
27354
27354
  "glossary_imported",
27355
27355
  "translation_job_viewed",
27356
27356
  "translation_logs_viewed",
27357
+ "activity_timeline_viewed",
27357
27358
  "cli_login_completed",
27358
27359
  "cli_command_completed",
27359
27360
  "cli_init_completed",
@@ -27544,9 +27545,9 @@ function runtimeProperties(cliVersion) {
27544
27545
  };
27545
27546
  }
27546
27547
  var activeCommand = null;
27547
- function beginCommand(path12, cliVersion) {
27548
+ function beginCommand(path11, cliVersion) {
27548
27549
  activeCommand = {
27549
- path: path12,
27550
+ path: path11,
27550
27551
  startedAt: Date.now(),
27551
27552
  cliVersion,
27552
27553
  reported: false
@@ -28088,24 +28089,14 @@ function InitWizard({
28088
28089
  ".json"
28089
28090
  ] }),
28090
28091
  /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
28091
- "3. Run: tolkbot translate run --src",
28092
- " ",
28093
- setupData.baseLanguage,
28094
- " --dst",
28092
+ "3. Run: tolkbot translate --dst",
28095
28093
  " ",
28096
28094
  setupData.targetLanguages[0] ?? "no"
28097
28095
  ] })
28098
28096
  ] }),
28099
28097
  !setupData.shouldInstall && /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
28100
28098
  /* @__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
- ] })
28099
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "See what a run would do: tolkbot translate --all --dry-run" })
28109
28100
  ] })
28110
28101
  ] });
28111
28102
  }
@@ -28299,380 +28290,76 @@ async function runInteractive(opts) {
28299
28290
  }
28300
28291
  }
28301
28292
 
28302
- // src/commands/learnings/index.ts
28293
+ // src/commands/jobs/index.ts
28303
28294
  import { Command as Command3 } from "commander";
28304
28295
  import { z as z5 } from "zod/v4";
28305
28296
 
28306
- // src/commands/learnings/archive.tsx
28307
- import { Spinner as Spinner9 } from "@inkjs/ui";
28297
+ // src/commands/jobs/apply.tsx
28298
+ import { useState as useState2 } from "react";
28299
+ import { MultiSelect } from "@inkjs/ui";
28308
28300
  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
28301
 
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
28302
+ // src/commands/translate/context.ts
28303
+ import path5 from "path";
28304
+ async function loadPlanContext(cwd) {
28305
+ const config3 = readConfig(cwd);
28306
+ if (!config3) throw noConfigError();
28307
+ const client3 = await getAuthenticatedConvexClient();
28308
+ const project = await client3.query(api.projects.getProjectForCli, {
28309
+ projectId: config3.config.projectId
28414
28310
  });
28311
+ if (!project) throw projectNotFoundError();
28312
+ setTelemetryOrganization(project.organizationId);
28313
+ return {
28314
+ root: path5.dirname(config3.path),
28315
+ project,
28316
+ client: client3,
28317
+ framework: config3.config.framework
28318
+ };
28415
28319
  }
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;
28320
+ async function readCreditBalance(ctx) {
28437
28321
  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
28322
+ const balance = await ctx.client.query(api.usage.getCreditBalance, {
28323
+ organizationId: ctx.project.organizationId
28470
28324
  });
28471
- };
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();
28325
+ return balance?.total ?? null;
28326
+ } catch {
28327
+ return null;
28483
28328
  }
28484
- render13(/* @__PURE__ */ jsx13(RemoveUI2, { state: { type: "success" } })).unmount();
28485
28329
  }
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
- ] });
28330
+ function describePlan(plan) {
28331
+ const { summary } = plan;
28332
+ const lines = [
28333
+ `${summary.totalKeys} keys expected, ${summary.upToDate} up to date`
28334
+ ];
28335
+ if (summary.missing > 0) lines.push(`${summary.missing} missing`);
28336
+ if (summary.changed > 0)
28337
+ lines.push(`${summary.changed} changed since translated`);
28338
+ return [...lines, ...describeLeftOut(plan)];
28537
28339
  }
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"
28340
+ function describeLeftOut(plan) {
28341
+ const { summary } = plan;
28342
+ const lines = [];
28343
+ const protectedCount = plan.protectedKeys.length;
28344
+ if (protectedCount > 0) {
28345
+ const reasons2 = Object.entries(summary.protected).map(([reason, n]) => `${n} ${reason}`).join(", ");
28346
+ lines.push(
28347
+ `${protectedCount} changed but kept for a person (${reasons2})`
28550
28348
  );
28551
28349
  }
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;
28350
+ if (summary.held > 0)
28351
+ lines.push(`${summary.held} missing but held by a person`);
28352
+ if (summary.untracked > 0) {
28353
+ lines.push(
28354
+ 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.`
28355
+ );
28634
28356
  }
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
- );
28357
+ return lines;
28641
28358
  }
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
28359
 
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
28360
+ // src/commands/translate/apply-core.ts
28674
28361
  import fs7 from "fs/promises";
28675
- import path7 from "path";
28362
+ import path8 from "path";
28676
28363
 
28677
28364
  // ../../node_modules/.pnpm/emoji-regex@10.6.0/node_modules/emoji-regex/index.mjs
28678
28365
  var emoji_regex_default = () => {
@@ -28766,7 +28453,7 @@ function eastAsianWidth(codePoint, { ambiguousAsWide = false } = {}) {
28766
28453
  // ../parser/dist/index.js
28767
28454
  var import_fast_glob = __toESM(require_out4(), 1);
28768
28455
  var import_fast_glob2 = __toESM(require_out4(), 1);
28769
- import path5 from "path";
28456
+ import path6 from "path";
28770
28457
  import fs5 from "fs/promises";
28771
28458
  import path23 from "path";
28772
28459
  import fs23 from "fs/promises";
@@ -28875,11 +28562,11 @@ function flattenValues(data) {
28875
28562
  const values = /* @__PURE__ */ new Map();
28876
28563
  function walk(obj, prefix) {
28877
28564
  for (const [key, value] of Object.entries(obj)) {
28878
- const fullKey4 = prefix ? `${prefix}.${key}` : key;
28565
+ const fullKey6 = prefix ? `${prefix}.${key}` : key;
28879
28566
  if (typeof value === "string") {
28880
- values.set(fullKey4, value);
28567
+ values.set(fullKey6, value);
28881
28568
  } else if (typeof value === "object" && value !== null) {
28882
- walk(value, fullKey4);
28569
+ walk(value, fullKey6);
28883
28570
  }
28884
28571
  }
28885
28572
  }
@@ -29917,7 +29604,7 @@ var BaseAdapter = class {
29917
29604
  parseErrors: []
29918
29605
  };
29919
29606
  }
29920
- const relativePaths = files.map((f) => path5.relative(baseDir, f));
29607
+ const relativePaths = files.map((f) => path6.relative(baseDir, f));
29921
29608
  const groupedPaths = groupByLocale(
29922
29609
  relativePaths,
29923
29610
  options?.knownLocales
@@ -29927,7 +29614,7 @@ var BaseAdapter = class {
29927
29614
  for (const [locale, localePaths] of groupedPaths) {
29928
29615
  const translationFiles = [];
29929
29616
  for (const relativePath of localePaths) {
29930
- const absolutePath = path5.join(baseDir, relativePath);
29617
+ const absolutePath = path6.join(baseDir, relativePath);
29931
29618
  try {
29932
29619
  const file2 = await this.parse(absolutePath);
29933
29620
  translationFiles.push(file2);
@@ -30868,10 +30555,175 @@ function pluralFormContext(plural2, locale, sourceKey) {
30868
30555
  return parts.join(" ");
30869
30556
  }
30870
30557
 
30871
- // src/commands/translate/plan-core.ts
30558
+ // src/commands/translate/project-files.ts
30559
+ import fs6 from "fs/promises";
30560
+ import path7 from "path";
30561
+ function fullPattern(scope) {
30562
+ return scope.subpath ? path7.posix.join(scope.subpath, scope.sourcePattern) : scope.sourcePattern;
30563
+ }
30564
+ function formatOf(scope) {
30565
+ return detectFormat(scope.sourcePattern) ?? "json";
30566
+ }
30567
+ async function listLocaleFiles(root, scope) {
30568
+ const pattern = fullPattern(scope);
30569
+ const adapter = createAdapter(formatOf(scope));
30570
+ const match = createPathMatcher(pattern);
30571
+ const found = await adapter.findFiles(root, withNamespaceWildcard(pattern));
30572
+ const files = [];
30573
+ for (const absolutePath of found.sort()) {
30574
+ const relativePath = path7.relative(root, absolutePath).split(path7.sep).join("/");
30575
+ const m = match(relativePath);
30576
+ if (!m) continue;
30577
+ files.push({
30578
+ pathTemplate: replaceNamespacePlaceholder(
30579
+ scope.sourcePattern,
30580
+ m.namespace ?? ""
30581
+ ),
30582
+ namespace: m.namespace,
30583
+ locale: m.locale,
30584
+ relativePath,
30585
+ absolutePath
30586
+ });
30587
+ }
30588
+ return files;
30589
+ }
30590
+ function filesForLocale(files, locale) {
30591
+ return files.filter((file2) => sameLocale(file2.locale, locale));
30592
+ }
30593
+ function targetFileFor(source, targetLocale, files, scope) {
30594
+ const existing = files.find(
30595
+ (file2) => file2.pathTemplate === source.pathTemplate && sameLocale(file2.locale, targetLocale)
30596
+ ) ?? null;
30597
+ if (existing) return { relativePath: existing.relativePath, existing };
30598
+ const spelling = files.find((file2) => sameLocale(file2.locale, targetLocale))?.locale ?? scope.detectedLocales?.find((l) => sameLocale(l, targetLocale)) ?? targetLocale;
30599
+ const relativePath = replaceLocalePlaceholder(
30600
+ replaceNamespacePlaceholder(fullPattern(scope), source.namespace ?? ""),
30601
+ spelling
30602
+ );
30603
+ return { relativePath, existing: null };
30604
+ }
30605
+ async function readTranslationFile(absolutePath, format) {
30606
+ try {
30607
+ const content = await fs6.readFile(absolutePath, "utf-8");
30608
+ const file2 = await createAdapter(format).parse(absolutePath);
30609
+ return { data: file2.data, content };
30610
+ } catch (error62) {
30611
+ return {
30612
+ error: error62 instanceof Error ? error62.message : String(error62)
30613
+ };
30614
+ }
30615
+ }
30616
+
30617
+ // src/commands/translate/apply-core.ts
30872
30618
  function fullKey(namespace, key) {
30873
30619
  return namespace ? `${namespace}.${key}` : key;
30874
30620
  }
30621
+ async function applyTasks(input2) {
30622
+ const { root, scope, dstLocale } = input2;
30623
+ const format = formatOf(scope);
30624
+ const files = await listLocaleFiles(root, scope);
30625
+ const result = { written: [], refused: [] };
30626
+ const refuse = (task, reason) => result.refused.push({ key: fullKey(task.namespace, task.key), reason });
30627
+ const bySourcePath = /* @__PURE__ */ new Map();
30628
+ for (const task of input2.tasks) {
30629
+ if (task.status !== "succeeded" || !task.dst || !task.path) {
30630
+ refuse(task, "not_translated");
30631
+ continue;
30632
+ }
30633
+ const group = bySourcePath.get(task.path) ?? [];
30634
+ group.push(task);
30635
+ bySourcePath.set(task.path, group);
30636
+ }
30637
+ for (const [sourcePath, tasks] of bySourcePath) {
30638
+ const sourceFile = files.find(
30639
+ (file2) => file2.relativePath === sourcePath
30640
+ );
30641
+ const source = sourceFile ? await readTranslationFile(sourceFile.absolutePath, format) : void 0;
30642
+ if (!sourceFile || !source || source.error !== void 0) {
30643
+ for (const task of tasks) refuse(task, "no_source_file");
30644
+ continue;
30645
+ }
30646
+ const baseKeys = new Set(getKeys(source.data));
30647
+ const target = targetFileFor(sourceFile, dstLocale, files, scope);
30648
+ const targetPath = path8.join(root, ...target.relativePath.split("/"));
30649
+ const existing = target.existing ? await readTranslationFile(target.existing.absolutePath, format) : void 0;
30650
+ if (existing?.error !== void 0) {
30651
+ for (const task of tasks) refuse(task, "target_unreadable");
30652
+ continue;
30653
+ }
30654
+ let data = existing?.data ?? {};
30655
+ const written = {
30656
+ pathTemplate: sourceFile.pathTemplate,
30657
+ relativePath: target.relativePath,
30658
+ entries: []
30659
+ };
30660
+ for (const task of tasks) {
30661
+ const sourceKey = sourceKeyFor(task.key, baseKeys);
30662
+ const currentSource = sourceKey === null ? void 0 : getByPath(source.data, sourceKey);
30663
+ if (typeof currentSource !== "string" || computeValueHash(currentSource) !== computeValueHash(task.src)) {
30664
+ refuse(task, "source_changed");
30665
+ continue;
30666
+ }
30667
+ const translatedValue = task.dst ?? "";
30668
+ const current = getByPath(data, task.key);
30669
+ const currentHash = typeof current === "string" ? computeValueHash(current) : null;
30670
+ if (task.expectedTargetHash !== void 0 && task.expectedTargetHash !== currentHash) {
30671
+ refuse(
30672
+ task,
30673
+ current === translatedValue ? "already_applied" : "target_changed"
30674
+ );
30675
+ continue;
30676
+ }
30677
+ data = setByPath(data, task.key, translatedValue);
30678
+ written.entries.push({
30679
+ key: task.key,
30680
+ namespace: task.namespace,
30681
+ baseValue: task.src,
30682
+ translatedValue,
30683
+ changesFile: current !== translatedValue
30684
+ });
30685
+ }
30686
+ if (written.entries.length === 0) continue;
30687
+ result.written.push(written);
30688
+ if (input2.dryRun) continue;
30689
+ await writeTranslationFile({
30690
+ format,
30691
+ targetPath,
30692
+ data,
30693
+ dstLocale,
30694
+ existingContent: existing?.content,
30695
+ sourceContent: source.content
30696
+ });
30697
+ }
30698
+ return result;
30699
+ }
30700
+ async function writeTranslationFile(input2) {
30701
+ const { targetPath, data } = input2;
30702
+ const model = input2.existingContent ?? input2.sourceContent;
30703
+ await fs7.mkdir(path8.dirname(targetPath), { recursive: true });
30704
+ if (input2.format === "json") {
30705
+ await fs7.writeFile(
30706
+ targetPath,
30707
+ serializeJsonContent(data, detectJsonStyle(model)),
30708
+ "utf-8"
30709
+ );
30710
+ return;
30711
+ }
30712
+ const style = detectTsStyle(model);
30713
+ const shape = detectTsExportFormat(model);
30714
+ const content = serializeTsContent(data, {
30715
+ ...style,
30716
+ printWidth: style.printWidth ?? void 0,
30717
+ exportName: input2.existingContent ? shape?.exportName ?? void 0 : localeExportIdentifier(input2.dstLocale),
30718
+ useDefaultExport: shape?.useDefaultExport ?? false
30719
+ });
30720
+ await fs7.writeFile(targetPath, content, "utf-8");
30721
+ }
30722
+
30723
+ // src/commands/translate/plan-core.ts
30724
+ function fullKey2(namespace, key) {
30725
+ return namespace ? `${namespace}.${key}` : key;
30726
+ }
30875
30727
  function expectedFullKeys(sources, targetLocale) {
30876
30728
  const keys = [];
30877
30729
  for (const { file: file2, data } of sources) {
@@ -30879,7 +30731,7 @@ function expectedFullKeys(sources, targetLocale) {
30879
30731
  getKeys(data),
30880
30732
  targetLocale
30881
30733
  ).keys())
30882
- keys.push(fullKey(file2.namespace, key));
30734
+ keys.push(fullKey2(file2.namespace, key));
30883
30735
  }
30884
30736
  return keys;
30885
30737
  }
@@ -30927,7 +30779,7 @@ function planLocale(input2) {
30927
30779
  const src = getByPath(data, sourceKey);
30928
30780
  if (typeof src !== "string") continue;
30929
30781
  plan.summary.totalKeys++;
30930
- const full = fullKey(file2.namespace, key);
30782
+ const full = fullKey2(file2.namespace, key);
30931
30783
  const hold = input2.holds.get(full);
30932
30784
  const current = getByPath(targetData, key);
30933
30785
  const targetValue = typeof current === "string" ? current : void 0;
@@ -30998,74 +30850,8 @@ function planLocale(input2) {
30998
30850
  return plan;
30999
30851
  }
31000
30852
 
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
30853
  // src/commands/translate/task-generator.ts
31061
30854
  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
30855
  function scopeOf(project) {
31070
30856
  return {
31071
30857
  sourcePattern: project.sourcePattern,
@@ -31150,364 +30936,16 @@ async function generateTasks(ctx, srcLocale, dstLocale, opts = {}) {
31150
30936
  };
31151
30937
  }
31152
30938
 
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
30939
+ // src/commands/jobs/apply-job.ts
31317
30940
  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
30941
  var RECENT_JOBS = 50;
31510
30942
  var APPLICABLE_STATUSES = /* @__PURE__ */ new Set(["succeeded", "partial", "failed"]);
30943
+ function isApplicable(status) {
30944
+ return APPLICABLE_STATUSES.has(status);
30945
+ }
30946
+ function isForWorkingTree(job) {
30947
+ return job.source === void 0 || job.source === "cli";
30948
+ }
31511
30949
  function fullKey3(namespace, key) {
31512
30950
  return namespace ? `${namespace}.${key}` : key;
31513
30951
  }
@@ -31518,17 +30956,17 @@ async function readJob(ctx, jobId) {
31518
30956
  organizationId: organizationId2,
31519
30957
  jobId
31520
30958
  });
31521
- if (!job) {
30959
+ if (job?.projectId !== project._id) {
31522
30960
  throw new CliError(
31523
30961
  `Translation job ${jobId} was not found in this project.`,
31524
- void 0,
30962
+ "List the project's jobs with 'tolkbot jobs list'.",
31525
30963
  "job_not_found"
31526
30964
  );
31527
30965
  }
31528
- if (!APPLICABLE_STATUSES.has(job.status)) {
30966
+ if (!isApplicable(job.status)) {
31529
30967
  throw new CliError(
31530
30968
  `Translation job ${jobId} is ${job.status}. Only a finished job can be applied.`,
31531
- `Check it with 'tolkbot translate status --job-id ${jobId}'.`,
30969
+ `Check it with 'tolkbot jobs show ${jobId}'.`,
31532
30970
  "job_not_finished"
31533
30971
  );
31534
30972
  }
@@ -31558,13 +30996,13 @@ async function applyJob(ctx, jobId) {
31558
30996
  translatedValue
31559
30997
  })
31560
30998
  );
31561
- for (let i = 0; i < entries.length; i += STAMP_CHUNK2) {
30999
+ for (let i = 0; i < entries.length; i += STAMP_CHUNK) {
31562
31000
  await client3.mutation(api.localeKeyStates.stampAppliedForCli, {
31563
31001
  organizationId: project.organizationId,
31564
31002
  projectId: project._id,
31565
31003
  locale,
31566
31004
  pathTemplate: file2.pathTemplate,
31567
- entries: entries.slice(i, i + STAMP_CHUNK2)
31005
+ entries: entries.slice(i, i + STAMP_CHUNK)
31568
31006
  });
31569
31007
  }
31570
31008
  }
@@ -31572,7 +31010,7 @@ async function applyJob(ctx, jobId) {
31572
31010
  dstLocale: job.dstLocale,
31573
31011
  files: result.written.length,
31574
31012
  keys: result.written.reduce((n, f) => n + f.entries.length, 0),
31575
- refused: result.refused.filter(
31013
+ notApplied: result.refused.filter(
31576
31014
  (r) => r.reason !== "not_translated" && r.reason !== "already_applied"
31577
31015
  )
31578
31016
  };
@@ -31585,18 +31023,13 @@ var REFUSAL_TEXT = {
31585
31023
  target_unreadable: "the target file cannot be read",
31586
31024
  target_changed: "the target was edited after it was planned"
31587
31025
  };
31588
- function describeRefusals(refused) {
31589
- return refused.map(
31026
+ function describeNotApplied(notApplied) {
31027
+ return notApplied.map(
31590
31028
  (r) => ` ${r.key}: not applied, ${REFUSAL_TEXT[r.reason]}`
31591
31029
  );
31592
31030
  }
31593
- async function findUnappliedJobs(ctx) {
31031
+ function pendingCounter(ctx) {
31594
31032
  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
31033
  const wanted = /* @__PURE__ */ new Map();
31601
31034
  const plannedKeys = (srcLocale, dstLocale) => {
31602
31035
  const id = `${srcLocale}\0${dstLocale}`;
@@ -31613,9 +31046,7 @@ async function findUnappliedJobs(ctx) {
31613
31046
  }
31614
31047
  return keys;
31615
31048
  };
31616
- const unapplied = [];
31617
- for (const job of jobs) {
31618
- if (!APPLICABLE_STATUSES.has(job.status)) continue;
31049
+ return async (job) => {
31619
31050
  const tasks = await client3.query(api.translations.listTasksByJobId, {
31620
31051
  organizationId: project.organizationId,
31621
31052
  jobId: job._id
@@ -31638,6 +31069,23 @@ async function findUnappliedJobs(ctx) {
31638
31069
  }
31639
31070
  }
31640
31071
  }
31072
+ return pending2;
31073
+ };
31074
+ }
31075
+ async function listRecentJobs(ctx, limit) {
31076
+ return await ctx.client.query(api.translations.listJobsByProject, {
31077
+ organizationId: ctx.project.organizationId,
31078
+ projectId: ctx.project._id,
31079
+ limit
31080
+ });
31081
+ }
31082
+ async function findUnappliedJobs(ctx) {
31083
+ const jobs = await listRecentJobs(ctx, RECENT_JOBS);
31084
+ const pendingOf = pendingCounter(ctx);
31085
+ const unapplied = [];
31086
+ for (const job of jobs) {
31087
+ if (!isApplicable(job.status) || !isForWorkingTree(job)) continue;
31088
+ const pending2 = await pendingOf(job);
31641
31089
  if (pending2 === 0) continue;
31642
31090
  unapplied.push({
31643
31091
  jobId: job._id,
@@ -31664,13 +31112,16 @@ function chooseJobs(unapplied) {
31664
31112
  jobs: [...unapplied].sort((a, b) => a.createdAt - b.createdAt)
31665
31113
  };
31666
31114
  }
31115
+
31116
+ // src/commands/jobs/apply.tsx
31117
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
31667
31118
  function describeJob(job) {
31668
31119
  return `${job.jobId} (${job.srcLocale} \u2192 ${job.dstLocale}, ${job.pending} key(s), ${new Date(job.createdAt).toLocaleString()})`;
31669
31120
  }
31670
31121
  function nothingToApply() {
31671
31122
  return new CliError(
31672
31123
  "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.",
31124
+ "Run 'tolkbot translate --all' to translate what is missing, or pass a job id to apply a job again.",
31674
31125
  "nothing_to_apply"
31675
31126
  );
31676
31127
  }
@@ -31681,7 +31132,7 @@ function ambiguousJobs(unapplied, locales) {
31681
31132
  );
31682
31133
  return new UsageError(
31683
31134
  `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(" ")}.`
31135
+ `Pass the job id(s) to apply. The newest is usually the one: 'tolkbot jobs apply ${newest.map((job) => job.jobId).join(" ")}'.`
31685
31136
  );
31686
31137
  }
31687
31138
  function JobPicker({
@@ -31689,10 +31140,10 @@ function JobPicker({
31689
31140
  onSubmit
31690
31141
  }) {
31691
31142
  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(
31143
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
31144
+ /* @__PURE__ */ jsx11(Text11, { bold: true, children: "Select translation jobs to apply:" }),
31145
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "Space to select, Enter to confirm, Ctrl+C to cancel" }),
31146
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(
31696
31147
  MultiSelect,
31697
31148
  {
31698
31149
  options: jobs.map((job) => ({
@@ -31707,8 +31158,8 @@ function JobPicker({
31707
31158
  }
31708
31159
  async function pickJobs(jobs) {
31709
31160
  let chosen = [];
31710
- const ui = render16(
31711
- /* @__PURE__ */ jsx16(
31161
+ const ui = render11(
31162
+ /* @__PURE__ */ jsx11(
31712
31163
  JobPicker,
31713
31164
  {
31714
31165
  jobs,
@@ -31734,15 +31185,15 @@ async function resolveJobIds(ctx, json2) {
31734
31185
  if (picked.length === 0) {
31735
31186
  throw new UsageError(
31736
31187
  "No job selected.",
31737
- "Pass --job-id with the job(s) to apply."
31188
+ "Pass the job id(s) to apply: 'tolkbot jobs apply <id\u2026>'."
31738
31189
  );
31739
31190
  }
31740
31191
  return picked;
31741
31192
  }
31742
- async function runApply(opts) {
31193
+ async function runJobsApply(opts) {
31743
31194
  const ctx = await loadPlanContext(process.cwd());
31744
31195
  const json2 = opts.json ?? false;
31745
- const jobIds = opts.jobId && opts.jobId.length > 0 ? opts.jobId : await resolveJobIds(ctx, json2);
31196
+ const jobIds = opts.jobIds.length > 0 ? opts.jobIds : await resolveJobIds(ctx, json2);
31746
31197
  if (json2) {
31747
31198
  const applied = [];
31748
31199
  for (const jobId of jobIds) {
@@ -31760,16 +31211,19 @@ async function runApply(opts) {
31760
31211
  console.log(`Applying ${jobIds.length} job(s)...`);
31761
31212
  let totalFiles = 0;
31762
31213
  let totalKeys = 0;
31763
- let totalRefused = 0;
31214
+ let totalNotApplied = 0;
31764
31215
  const locales = /* @__PURE__ */ new Set();
31765
31216
  for (const [i, jobId] of jobIds.entries()) {
31766
31217
  console.log(` [${i + 1}/${jobIds.length}] ${jobId}`);
31767
- const { dstLocale, files, keys, refused } = await applyJob(ctx, jobId);
31218
+ const { dstLocale, files, keys, notApplied } = await applyJob(
31219
+ ctx,
31220
+ jobId
31221
+ );
31768
31222
  locales.add(dstLocale);
31769
31223
  totalFiles += files;
31770
31224
  totalKeys += keys;
31771
- totalRefused += refused.length;
31772
- for (const line of describeRefusals(refused)) console.log(line);
31225
+ totalNotApplied += notApplied.length;
31226
+ for (const line of describeNotApplied(notApplied)) console.log(line);
31773
31227
  }
31774
31228
  track("cli_translate_completed", {
31775
31229
  mode: "apply",
@@ -31779,187 +31233,89 @@ async function runApply(opts) {
31779
31233
  json: false
31780
31234
  });
31781
31235
  console.log(
31782
- `\u2713 Applied ${totalKeys} keys to ${totalFiles} files from ${jobIds.length} job(s)` + (totalRefused > 0 ? `, ${totalRefused} not applied` : "")
31236
+ `\u2713 Applied ${totalKeys} keys to ${totalFiles} files from ${jobIds.length} job(s)` + (totalNotApplied > 0 ? `, ${totalNotApplied} not applied` : "")
31783
31237
  );
31784
31238
  }
31785
31239
 
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) {
31240
+ // src/commands/jobs/cancel.ts
31241
+ async function runJobsCancel(opts) {
31809
31242
  const { project } = await loadPlanContext(process.cwd());
31810
31243
  const client3 = await getAuthenticatedConvexClient();
31811
31244
  await client3.action(api.translations.cancelJob, {
31812
31245
  organizationId: project.organizationId,
31813
- jobId
31246
+ jobId: opts.jobId
31814
31247
  });
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();
31832
- }
31833
-
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;
31248
+ if (opts.json) jsonSuccess({ jobId: opts.jobId, ok: true });
31249
+ console.log(`\u2713 Cancelled job ${opts.jobId}`);
31872
31250
  }
31873
31251
 
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
- ] });
31252
+ // src/commands/jobs/list.ts
31253
+ async function describeJobs(ctx, jobs) {
31254
+ const pendingOf = pendingCounter(ctx);
31255
+ const listed = [];
31256
+ for (const job of jobs) {
31257
+ let applied = "none";
31258
+ let pending2 = 0;
31259
+ if (!isForWorkingTree(job)) {
31260
+ } else if (job.status === "pending" || job.status === "running") {
31261
+ applied = "running";
31262
+ } else if (isApplicable(job.status)) {
31263
+ pending2 = await pendingOf(job);
31264
+ applied = pending2 > 0 ? "waiting" : "applied";
31265
+ }
31266
+ listed.push({
31267
+ jobId: job._id,
31268
+ srcLocale: job.srcLocale,
31269
+ dstLocale: job.dstLocale,
31270
+ status: job.status,
31271
+ source: job.source ?? null,
31272
+ createdAt: new Date(job._creationTime).toISOString(),
31273
+ total: job.total,
31274
+ completed: job.completed,
31275
+ failed: job.failed,
31276
+ creditsUsed: job.creditsUsed,
31277
+ applied,
31278
+ pending: pending2
31279
+ });
31882
31280
  }
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
- ] });
31281
+ return listed;
31898
31282
  }
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."
31283
+ var APPLIED_TEXT = {
31284
+ applied: "applied",
31285
+ waiting: "waiting to apply",
31286
+ running: "",
31287
+ none: ""
31288
+ };
31289
+ function formatRow(job) {
31290
+ const when = job.createdAt.slice(0, 16).replace("T", " ");
31291
+ const progress = `${job.completed}/${job.total}`;
31292
+ const applied = job.applied === "waiting" ? `${APPLIED_TEXT.waiting} (${job.pending})` : APPLIED_TEXT[job.applied];
31293
+ return [
31294
+ job.jobId,
31295
+ when,
31296
+ `${job.srcLocale} \u2192 ${job.dstLocale}`.padEnd(14),
31297
+ job.status.padEnd(9),
31298
+ progress.padStart(9),
31299
+ (job.source ?? "cli").padEnd(9),
31300
+ applied
31301
+ ].join(" ").trimEnd();
31302
+ }
31303
+ async function runJobsList(opts) {
31304
+ const ctx = await loadPlanContext(process.cwd());
31305
+ const jobs = await describeJobs(ctx, await listRecentJobs(ctx, opts.limit));
31306
+ if (opts.json) jsonSuccess({ jobs });
31307
+ if (jobs.length === 0) {
31308
+ console.log(
31309
+ "No translation jobs yet. Run 'tolkbot translate --all' to start one."
31917
31310
  );
31311
+ return;
31918
31312
  }
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();
31313
+ for (const job of jobs) console.log(formatRow(job));
31314
+ if (jobs.some((job) => job.applied === "waiting")) {
31315
+ console.log("\nApply the waiting ones with 'tolkbot jobs apply'.");
31956
31316
  }
31957
- render18(/* @__PURE__ */ jsx18(EditUI, { state: { type: "success", ...result } })).unmount();
31958
31317
  }
31959
31318
 
31960
- // src/commands/translate/run.tsx
31961
- import { render as render19 } from "ink";
31962
-
31963
31319
  // ../shared/src/conditions/index.ts
31964
31320
  var BILLING_BLOCKED = "BILLING_BLOCKED";
31965
31321
 
@@ -32061,10 +31417,10 @@ function serverMessage(message) {
32061
31417
  }
32062
31418
  function argumentError(message) {
32063
31419
  if (!message.includes("ArgumentValidationError")) return null;
32064
- const path12 = /Path: \.?(\S+)/.exec(message)?.[1];
31420
+ const path11 = /Path: \.?(\S+)/.exec(message)?.[1];
32065
31421
  const value = /Value: (.+)/.exec(message)?.[1];
32066
31422
  return new UsageError(
32067
- path12 && value ? `TolkBot refused ${value} as ${path12}.` : "TolkBot refused the arguments.",
31423
+ path11 && value ? `TolkBot refused ${value} as ${path11}.` : "TolkBot refused the arguments.",
32068
31424
  "Check the id or value passed; list ids with the command's list subcommand."
32069
31425
  );
32070
31426
  }
@@ -32082,687 +31438,1413 @@ function toCliError(error62) {
32082
31438
  "not_authenticated"
32083
31439
  );
32084
31440
  }
32085
- if (isNetworkError(error62)) {
32086
- return new CliError(
32087
- "Could not reach TolkBot.",
32088
- "Check the network connection and try again.",
32089
- "network"
31441
+ if (isNetworkError(error62)) {
31442
+ return new CliError(
31443
+ "Could not reach TolkBot.",
31444
+ "Check the network connection and try again.",
31445
+ "network"
31446
+ );
31447
+ }
31448
+ const message = describeError(error62);
31449
+ const refused = argumentError(message);
31450
+ if (refused) return refused;
31451
+ if (message.includes("Server Error")) {
31452
+ return new CliError(serverMessage(message));
31453
+ }
31454
+ const internal = new CliError(message, void 0, "internal");
31455
+ if (error62 instanceof Error) internal.stack = error62.stack;
31456
+ return internal;
31457
+ }
31458
+ function errorPayload(error62) {
31459
+ return {
31460
+ error: {
31461
+ code: error62.code,
31462
+ message: error62.baseMessage,
31463
+ hint: error62.hint ?? null
31464
+ }
31465
+ };
31466
+ }
31467
+ function reportError(error62) {
31468
+ const cli = toCliError(error62);
31469
+ if (wantsJson()) {
31470
+ console.error(JSON.stringify(errorPayload(cli), null, 2));
31471
+ } else {
31472
+ console.error(`\u2717 ${cli.baseMessage}`);
31473
+ if (cli.hint) console.error(` ${cli.hint}`);
31474
+ if (cli.code === "internal" && cli.stack) {
31475
+ console.error("");
31476
+ console.error(cli.stack);
31477
+ }
31478
+ }
31479
+ const code = exitCodeFor(cli.code);
31480
+ process.exitCode = code;
31481
+ return code;
31482
+ }
31483
+ function fail(error62) {
31484
+ const cli = toCliError(error62);
31485
+ throw requestExit(exitCodeFor(cli.code), () => {
31486
+ reportError(cli);
31487
+ });
31488
+ }
31489
+
31490
+ // src/commands/translate/translate-job.ts
31491
+ var FINISHED_STATUSES = /* @__PURE__ */ new Set([
31492
+ "succeeded",
31493
+ "partial",
31494
+ "failed",
31495
+ "cancelled"
31496
+ ]);
31497
+ function isJobFinished(status) {
31498
+ return FINISHED_STATUSES.has(status);
31499
+ }
31500
+ function unsuccessfulJobError(jobId, status) {
31501
+ if (status === "cancelled") {
31502
+ return new CliError(
31503
+ `Translation job ${jobId} was cancelled.`,
31504
+ void 0,
31505
+ "job_cancelled"
31506
+ );
31507
+ }
31508
+ if (status === "failed") {
31509
+ return new CliError(
31510
+ `Translation job ${jobId} failed.`,
31511
+ `See it at ${env.TOLKBOT_WEB_URL}/translation-jobs/${jobId}.`,
31512
+ "job_failed"
31513
+ );
31514
+ }
31515
+ return null;
31516
+ }
31517
+ var JOB_WAIT_TIMEOUT_MS = 60 * 6e4;
31518
+ var JOB_POLL_MS = 500;
31519
+ var JobWaitTimeoutError = class extends CliError {
31520
+ constructor(jobId, timeoutMs) {
31521
+ super(
31522
+ `Translation job ${jobId} was still running after ${Math.round(timeoutMs / 6e4)} minutes, so the CLI stopped waiting.`,
31523
+ `The job keeps running. Check it with 'tolkbot jobs show ${jobId}' and apply it with 'tolkbot jobs apply ${jobId}' once it finishes.`,
31524
+ "job_timed_out"
31525
+ );
31526
+ this.jobId = jobId;
31527
+ this.name = "JobWaitTimeoutError";
31528
+ }
31529
+ jobId;
31530
+ };
31531
+ async function waitForJob(jobId, read, opts = {}) {
31532
+ const timeoutMs = opts.timeoutMs ?? JOB_WAIT_TIMEOUT_MS;
31533
+ const pollMs = opts.pollMs ?? JOB_POLL_MS;
31534
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
31535
+ const now = opts.now ?? Date.now;
31536
+ const deadline = now() + timeoutMs;
31537
+ for (; ; ) {
31538
+ const summary = await read();
31539
+ if (!summary) {
31540
+ throw new CliError(
31541
+ `Translation job ${jobId} was not found.`,
31542
+ void 0,
31543
+ "job_not_found"
31544
+ );
31545
+ }
31546
+ opts.onProgress?.(summary);
31547
+ if (isJobFinished(summary.status)) return summary;
31548
+ if (now() >= deadline) {
31549
+ throw new JobWaitTimeoutError(jobId, timeoutMs);
31550
+ }
31551
+ await sleep(pollMs);
31552
+ }
31553
+ }
31554
+ async function startTranslationJob(params) {
31555
+ const { projectId, organizationId: organizationId2, framework, srcLocale, dstLocale } = params;
31556
+ const convexClient = await getAuthenticatedConvexClient();
31557
+ return await convexClient.action(api.translations.startJob, {
31558
+ organizationId: organizationId2,
31559
+ projectId,
31560
+ srcLocale,
31561
+ dstLocale,
31562
+ framework,
31563
+ tasks: params.tasks.map((t) => ({
31564
+ key: t.key,
31565
+ namespace: t.namespace,
31566
+ path: t.path,
31567
+ src: t.src,
31568
+ hash: t.hash,
31569
+ context: t.context,
31570
+ // What `jobs apply` checks the target against. Without it every
31571
+ // result was written over whatever the file held by then.
31572
+ expectedTargetHash: t.expectedTargetHash
31573
+ }))
31574
+ });
31575
+ }
31576
+ async function waitForTranslationJob(organizationId2, jobId, onProgress) {
31577
+ const convexClient = await getAuthenticatedConvexClient();
31578
+ return waitForJob(
31579
+ jobId,
31580
+ () => convexClient.query(api.translations.getJobSummary, {
31581
+ organizationId: organizationId2,
31582
+ jobId
31583
+ }),
31584
+ { onProgress }
31585
+ );
31586
+ }
31587
+
31588
+ // src/commands/jobs/show.ts
31589
+ var STATUS_TEXT = {
31590
+ pending: "is waiting to start",
31591
+ running: "is running",
31592
+ succeeded: "finished",
31593
+ partial: "finished with some keys failed",
31594
+ failed: "failed",
31595
+ cancelled: "was cancelled"
31596
+ };
31597
+ function describe3(job) {
31598
+ const processed = job.completed + job.failed;
31599
+ const percent = job.total > 0 ? Math.round(processed / job.total * 100) : 0;
31600
+ const lines = [
31601
+ `Job ${job.jobId} ${STATUS_TEXT[job.status]}`,
31602
+ ` ${job.srcLocale} \u2192 ${job.dstLocale}, started ${job.createdAt.slice(0, 16).replace("T", " ")} UTC`,
31603
+ ` ${job.completed} translated, ${job.failed} failed, ${job.total} in all (${percent}%)`,
31604
+ ` ${job.creditsUsed} credits used`
31605
+ ];
31606
+ if (job.applied === "applied") lines.push(" Applied to this working tree");
31607
+ if (job.applied === "waiting") {
31608
+ lines.push(
31609
+ ` ${job.pending} key(s) waiting: 'tolkbot jobs apply ${job.jobId}'`
31610
+ );
31611
+ }
31612
+ lines.push(` ${env.TOLKBOT_WEB_URL}/translation-jobs/${job.jobId}`);
31613
+ return lines;
31614
+ }
31615
+ async function runJobsShow(opts) {
31616
+ const ctx = await loadPlanContext(process.cwd());
31617
+ const found = await ctx.client.query(api.translations.getJobDetails, {
31618
+ organizationId: ctx.project.organizationId,
31619
+ jobId: opts.jobId
31620
+ });
31621
+ if (found?.projectId !== ctx.project._id) {
31622
+ throw new CliError(
31623
+ `Translation job ${opts.jobId} was not found in this project.`,
31624
+ "List the project's jobs with 'tolkbot jobs list'.",
31625
+ "job_not_found"
31626
+ );
31627
+ }
31628
+ const [job] = await describeJobs(ctx, [found]);
31629
+ if (!job)
31630
+ throw new CliError(`Translation job ${opts.jobId} was not found.`);
31631
+ const failure2 = unsuccessfulJobError(job.jobId, job.status);
31632
+ if (opts.json) {
31633
+ if (!failure2) jsonSuccess(job);
31634
+ jsonResult(job, 1);
31635
+ fail(failure2);
31636
+ }
31637
+ for (const line of describe3(job)) console.log(line);
31638
+ if (failure2) fail(failure2);
31639
+ }
31640
+
31641
+ // src/commands/jobs/index.ts
31642
+ var jobsCommand = new Command3("jobs").description(
31643
+ "List, inspect, apply and cancel translation jobs"
31644
+ );
31645
+ var JSON_HELP = "Output as JSON (machine-readable)";
31646
+ jobsCommand.command("list").description(
31647
+ "Recent translation jobs for the project, with their status and whether their results are in this working tree"
31648
+ ).option("--limit <n>", "How many jobs to list", "20").option("--json", JSON_HELP).action(async (opts) => {
31649
+ const schema = z5.object({
31650
+ limit: z5.coerce.number().int().min(1).max(100),
31651
+ json: z5.boolean().optional()
31652
+ });
31653
+ await runJobsList(validateOrExit(schema, opts));
31654
+ });
31655
+ jobsCommand.command("show").description("Show one translation job").argument("<id>", "Job ID").option("--json", JSON_HELP).action(async (jobId, opts) => {
31656
+ const schema = z5.object({
31657
+ jobId: z5.string().min(1),
31658
+ json: z5.boolean().optional()
31659
+ });
31660
+ await runJobsShow(validateOrExit(schema, { ...opts, jobId }));
31661
+ });
31662
+ jobsCommand.command("apply").description(
31663
+ "Write finished jobs' results into the working tree. With no ID: the finished jobs not applied yet, one per language"
31664
+ ).argument("[ids...]", "Job ID(s) to apply").option("--json", JSON_HELP).action(async (jobIds, opts) => {
31665
+ const schema = z5.object({
31666
+ jobIds: z5.array(z5.string().min(1)),
31667
+ json: z5.boolean().optional()
31668
+ });
31669
+ await runJobsApply(validateOrExit(schema, { ...opts, jobIds }));
31670
+ });
31671
+ jobsCommand.command("cancel").description("Cancel a running translation job").argument("<id>", "Job ID").option("--json", JSON_HELP).action(async (jobId, opts) => {
31672
+ const schema = z5.object({
31673
+ jobId: z5.string().min(1),
31674
+ json: z5.boolean().optional()
31675
+ });
31676
+ await runJobsCancel(validateOrExit(schema, { ...opts, jobId }));
31677
+ });
31678
+
31679
+ // src/commands/learnings/index.ts
31680
+ import { Command as Command4 } from "commander";
31681
+ import { z as z6 } from "zod/v4";
31682
+
31683
+ // src/commands/learnings/archive.tsx
31684
+ import { Spinner as Spinner9 } from "@inkjs/ui";
31685
+ import { Box as Box12, render as render12, Text as Text12 } from "ink";
31686
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
31687
+ function ArchiveUI({ state }) {
31688
+ if (state.type === "archiving") {
31689
+ return /* @__PURE__ */ jsxs12(Box12, { children: [
31690
+ /* @__PURE__ */ jsx12(Spinner9, {}),
31691
+ /* @__PURE__ */ jsx12(Text12, { children: " Archiving learning\u2026" })
31692
+ ] });
31693
+ }
31694
+ return /* @__PURE__ */ jsxs12(Box12, { children: [
31695
+ /* @__PURE__ */ jsx12(Text12, { color: "green", children: "\u2713" }),
31696
+ /* @__PURE__ */ jsx12(Text12, { children: " Learning archived" })
31697
+ ] });
31698
+ }
31699
+ async function runLearningsArchive(opts) {
31700
+ const run = async () => {
31701
+ const convexClient = await getAuthenticatedConvexClient();
31702
+ const { organizationId: organizationId2 } = await getProjectContext(convexClient);
31703
+ await convexClient.mutation(api.learnings.archive, {
31704
+ organizationId: organizationId2,
31705
+ learningId: opts.id
31706
+ });
31707
+ };
31708
+ if (opts.json) {
31709
+ await run();
31710
+ jsonSuccess({ id: opts.id, ok: true });
31711
+ }
31712
+ const { unmount } = renderTransient(
31713
+ /* @__PURE__ */ jsx12(ArchiveUI, { state: { type: "archiving" } })
31714
+ );
31715
+ try {
31716
+ await run();
31717
+ } finally {
31718
+ unmount();
31719
+ }
31720
+ render12(/* @__PURE__ */ jsx12(ArchiveUI, { state: { type: "success" } })).unmount();
31721
+ }
31722
+
31723
+ // src/commands/learnings/list.tsx
31724
+ import { Spinner as Spinner10 } from "@inkjs/ui";
31725
+ import { Box as Box13, render as render13, Text as Text13 } from "ink";
31726
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
31727
+ var feedbackLabels = {
31728
+ "incorrect-term": "Incorrect term",
31729
+ "wrong-tone": "Wrong tone",
31730
+ "cultural-context": "Cultural context",
31731
+ "technical-accuracy": "Technical accuracy",
31732
+ other: "Other"
31733
+ };
31734
+ function LearningsListUI({ state }) {
31735
+ if (state.type === "loading") {
31736
+ return /* @__PURE__ */ jsxs13(Box13, { children: [
31737
+ /* @__PURE__ */ jsx13(Spinner10, {}),
31738
+ /* @__PURE__ */ jsx13(Text13, { children: " Loading learnings\u2026" })
31739
+ ] });
31740
+ }
31741
+ const { learnings } = state;
31742
+ if (learnings.length === 0) {
31743
+ return /* @__PURE__ */ jsx13(Box13, { children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No learnings found." }) });
31744
+ }
31745
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
31746
+ /* @__PURE__ */ jsx13(Box13, { marginBottom: 1, children: /* @__PURE__ */ jsxs13(Text13, { bold: true, children: [
31747
+ learnings.length,
31748
+ " learning",
31749
+ learnings.length === 1 ? "" : "s"
31750
+ ] }) }),
31751
+ learnings.map((l) => /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", marginBottom: 1, children: [
31752
+ /* @__PURE__ */ jsxs13(Box13, { gap: 1, children: [
31753
+ /* @__PURE__ */ jsxs13(Text13, { bold: true, children: [
31754
+ l.srcLocale,
31755
+ " \u2192 ",
31756
+ l.dstLocale
31757
+ ] }),
31758
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
31759
+ "[",
31760
+ feedbackLabels[l.feedbackType] ?? l.feedbackType,
31761
+ "]"
31762
+ ] }),
31763
+ l.status === "archived" && /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: "[archived]" })
31764
+ ] }),
31765
+ /* @__PURE__ */ jsx13(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsxs13(Text13, { children: [
31766
+ '"',
31767
+ l.sourceText,
31768
+ '" \u2192 "',
31769
+ l.suggestedTranslation,
31770
+ '"'
31771
+ ] }) }),
31772
+ l.reasoning && /* @__PURE__ */ jsx13(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
31773
+ "Reason: ",
31774
+ l.reasoning
31775
+ ] }) }),
31776
+ /* @__PURE__ */ jsx13(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
31777
+ "ID: ",
31778
+ l._id
31779
+ ] }) })
31780
+ ] }, l._id))
31781
+ ] });
31782
+ }
31783
+ async function loadLearnings(opts) {
31784
+ const convexClient = await getAuthenticatedConvexClient();
31785
+ const { projectId, organizationId: organizationId2 } = await getProjectContext(convexClient);
31786
+ return convexClient.query(api.learnings.listByProject, {
31787
+ organizationId: organizationId2,
31788
+ projectId,
31789
+ status: opts.status,
31790
+ limit: opts.limit
31791
+ });
31792
+ }
31793
+ async function runLearningsList(opts) {
31794
+ if (opts.json) {
31795
+ const learnings2 = await loadLearnings(opts);
31796
+ jsonSuccess({
31797
+ learnings: learnings2.map((l) => ({
31798
+ id: l._id,
31799
+ sourceText: l.sourceText,
31800
+ suggestedTranslation: l.suggestedTranslation,
31801
+ srcLocale: l.srcLocale,
31802
+ dstLocale: l.dstLocale,
31803
+ feedbackType: l.feedbackType,
31804
+ reasoning: l.reasoning,
31805
+ status: l.status,
31806
+ source: l.source
31807
+ }))
31808
+ });
31809
+ }
31810
+ const { unmount } = renderTransient(
31811
+ /* @__PURE__ */ jsx13(LearningsListUI, { state: { type: "loading" } })
31812
+ );
31813
+ let learnings;
31814
+ try {
31815
+ learnings = await loadLearnings(opts);
31816
+ } finally {
31817
+ unmount();
31818
+ }
31819
+ render13(
31820
+ /* @__PURE__ */ jsx13(LearningsListUI, { state: { type: "success", learnings } })
31821
+ ).unmount();
31822
+ }
31823
+
31824
+ // src/commands/learnings/remove.tsx
31825
+ import { Spinner as Spinner11 } from "@inkjs/ui";
31826
+ import { Box as Box14, render as render14, Text as Text14 } from "ink";
31827
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
31828
+ function RemoveUI2({ state }) {
31829
+ if (state.type === "removing") {
31830
+ return /* @__PURE__ */ jsxs14(Box14, { children: [
31831
+ /* @__PURE__ */ jsx14(Spinner11, {}),
31832
+ /* @__PURE__ */ jsx14(Text14, { children: " Removing learning\u2026" })
31833
+ ] });
31834
+ }
31835
+ return /* @__PURE__ */ jsxs14(Box14, { children: [
31836
+ /* @__PURE__ */ jsx14(Text14, { color: "green", children: "\u2713" }),
31837
+ /* @__PURE__ */ jsx14(Text14, { children: " Learning removed" })
31838
+ ] });
31839
+ }
31840
+ async function runLearningsRemove(opts) {
31841
+ const run = async () => {
31842
+ const convexClient = await getAuthenticatedConvexClient();
31843
+ const { organizationId: organizationId2 } = await getProjectContext(convexClient);
31844
+ await convexClient.mutation(api.learnings.remove, {
31845
+ organizationId: organizationId2,
31846
+ learningId: opts.id
31847
+ });
31848
+ };
31849
+ if (opts.json) {
31850
+ await run();
31851
+ jsonSuccess({ id: opts.id, ok: true });
31852
+ }
31853
+ const { unmount } = renderTransient(
31854
+ /* @__PURE__ */ jsx14(RemoveUI2, { state: { type: "removing" } })
31855
+ );
31856
+ try {
31857
+ await run();
31858
+ } finally {
31859
+ unmount();
31860
+ }
31861
+ render14(/* @__PURE__ */ jsx14(RemoveUI2, { state: { type: "success" } })).unmount();
31862
+ }
31863
+
31864
+ // src/commands/learnings/show.tsx
31865
+ import { Spinner as Spinner12 } from "@inkjs/ui";
31866
+ import { Box as Box15, render as render15, Text as Text15 } from "ink";
31867
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
31868
+ function LearningShowUI({ state }) {
31869
+ if (state.type === "loading") {
31870
+ return /* @__PURE__ */ jsxs15(Box15, { children: [
31871
+ /* @__PURE__ */ jsx15(Spinner12, {}),
31872
+ /* @__PURE__ */ jsx15(Text15, { children: " Loading learning\u2026" })
31873
+ ] });
31874
+ }
31875
+ const { learning } = state;
31876
+ return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
31877
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31878
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Source text:" }),
31879
+ /* @__PURE__ */ jsx15(Text15, { children: learning.sourceText })
31880
+ ] }),
31881
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31882
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Suggested translation:" }),
31883
+ /* @__PURE__ */ jsx15(Text15, { children: learning.suggestedTranslation })
31884
+ ] }),
31885
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31886
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Locales:" }),
31887
+ /* @__PURE__ */ jsxs15(Text15, { children: [
31888
+ learning.srcLocale,
31889
+ " \u2192 ",
31890
+ learning.dstLocale
31891
+ ] })
31892
+ ] }),
31893
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31894
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Feedback type:" }),
31895
+ /* @__PURE__ */ jsx15(Text15, { children: learning.feedbackType })
31896
+ ] }),
31897
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31898
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Reasoning:" }),
31899
+ /* @__PURE__ */ jsx15(Text15, { children: learning.reasoning })
31900
+ ] }),
31901
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31902
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Status:" }),
31903
+ /* @__PURE__ */ jsx15(Text15, { color: learning.status === "active" ? "green" : "yellow", children: learning.status })
31904
+ ] }),
31905
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
31906
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Source:" }),
31907
+ /* @__PURE__ */ jsx15(Text15, { children: learning.source })
31908
+ ] }),
31909
+ /* @__PURE__ */ jsx15(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text15, { dimColor: true, children: [
31910
+ "ID: ",
31911
+ learning._id
31912
+ ] }) })
31913
+ ] });
31914
+ }
31915
+ async function loadLearning(id) {
31916
+ const convexClient = await getAuthenticatedConvexClient();
31917
+ const { organizationId: organizationId2 } = await getProjectContext(convexClient);
31918
+ const learning = await convexClient.query(api.learnings.getById, {
31919
+ organizationId: organizationId2,
31920
+ learningId: id
31921
+ });
31922
+ if (!learning) {
31923
+ throw new CliError(
31924
+ `Learning ${id} was not found.`,
31925
+ "List them with 'tolkbot learnings list'.",
31926
+ "not_found"
31927
+ );
31928
+ }
31929
+ return learning;
31930
+ }
31931
+ async function runLearningsShow(opts) {
31932
+ if (opts.json) {
31933
+ const learning2 = await loadLearning(opts.id);
31934
+ jsonSuccess({
31935
+ id: learning2._id,
31936
+ sourceText: learning2.sourceText,
31937
+ suggestedTranslation: learning2.suggestedTranslation,
31938
+ srcLocale: learning2.srcLocale,
31939
+ dstLocale: learning2.dstLocale,
31940
+ feedbackType: learning2.feedbackType,
31941
+ reasoning: learning2.reasoning,
31942
+ status: learning2.status,
31943
+ source: learning2.source
31944
+ });
31945
+ }
31946
+ const { unmount } = renderTransient(
31947
+ /* @__PURE__ */ jsx15(LearningShowUI, { state: { type: "loading" } })
31948
+ );
31949
+ let learning;
31950
+ try {
31951
+ learning = await loadLearning(opts.id);
31952
+ } finally {
31953
+ unmount();
31954
+ }
31955
+ render15(/* @__PURE__ */ jsx15(LearningShowUI, { state: { type: "success", learning } })).unmount();
31956
+ }
31957
+
31958
+ // src/commands/learnings/index.ts
31959
+ var learningsCommand = new Command4("learnings").description(
31960
+ "View and manage translation learnings"
31961
+ );
31962
+ 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) => {
31963
+ const schema = z6.object({
31964
+ status: z6.enum(["active", "archived"]).optional(),
31965
+ limit: z6.coerce.number().int().positive().default(100),
31966
+ json: z6.boolean().optional()
31967
+ });
31968
+ const parsed = validateOrExit(schema, opts);
31969
+ await runLearningsList(parsed);
31970
+ });
31971
+ learningsCommand.command("show").description("Show details of a learning").requiredOption("--id <learningId>", "Learning ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
31972
+ const schema = z6.object({
31973
+ id: z6.string().min(1),
31974
+ json: z6.boolean().optional()
31975
+ });
31976
+ const parsed = validateOrExit(schema, opts);
31977
+ await runLearningsShow(parsed);
31978
+ });
31979
+ 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) => {
31980
+ const schema = z6.object({
31981
+ id: z6.string().min(1),
31982
+ json: z6.boolean().optional()
31983
+ });
31984
+ const parsed = validateOrExit(schema, opts);
31985
+ await runLearningsArchive(parsed);
31986
+ });
31987
+ learningsCommand.command("remove").description("Permanently remove a learning").requiredOption("--id <learningId>", "Learning ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
31988
+ const schema = z6.object({
31989
+ id: z6.string().min(1),
31990
+ json: z6.boolean().optional()
31991
+ });
31992
+ const parsed = validateOrExit(schema, opts);
31993
+ await runLearningsRemove(parsed);
31994
+ });
31995
+
31996
+ // src/commands/status/index.ts
31997
+ import { Command as Command5 } from "commander";
31998
+ import { z as z7 } from "zod/v4";
31999
+
32000
+ // src/commands/status/status.tsx
32001
+ import { Spinner as Spinner13 } from "@inkjs/ui";
32002
+ import { Box as Box16, Text as Text16 } from "ink";
32003
+
32004
+ // src/commands/status/status-core.ts
32005
+ function fullKey4(namespace, key) {
32006
+ return namespace ? `${namespace}.${key}` : key;
32007
+ }
32008
+ function isPersonHold(reason) {
32009
+ return reason === "locked" || reason === "pending-draft";
32010
+ }
32011
+ function count(reasons2) {
32012
+ const byReason = {};
32013
+ for (const reason of reasons2)
32014
+ byReason[reason] = (byReason[reason] ?? 0) + 1;
32015
+ return { total: reasons2.length, byReason };
32016
+ }
32017
+ function openPullRequest(rows, locale) {
32018
+ const row = rows.find(
32019
+ (r) => r.prState === "open" && r.prUrl !== void 0 && sameLocale(r.targetLanguage, locale)
32020
+ );
32021
+ if (!row?.prUrl) return null;
32022
+ return { number: row.prNumber ?? null, url: row.prUrl };
32023
+ }
32024
+ function languageStatus(locale, plan, pullRequests, opts = {}) {
32025
+ const { summary } = plan;
32026
+ const held = [
32027
+ ...plan.heldKeys,
32028
+ ...plan.protectedKeys.flatMap(
32029
+ ({ key, reason }) => isPersonHold(reason) ? [{ key, reason }] : []
32030
+ )
32031
+ ];
32032
+ const kept = plan.protectedKeys.flatMap(
32033
+ ({ key, reason }) => isPersonHold(reason) ? [] : [{ key, reason }]
32034
+ );
32035
+ const tasks = (reason) => plan.tasks.filter((t) => t.reason === reason).map((t) => fullKey4(t.namespace, t.key));
32036
+ return {
32037
+ locale,
32038
+ expected: summary.totalKeys,
32039
+ upToDate: summary.upToDate,
32040
+ missing: summary.missing,
32041
+ changed: summary.changed,
32042
+ heldByPerson: count(held.map((h) => h.reason)),
32043
+ keptForPerson: count(kept.map((k) => k.reason)),
32044
+ untracked: summary.untracked,
32045
+ pullRequest: openPullRequest(pullRequests, locale),
32046
+ ...opts.keys ? {
32047
+ keys: {
32048
+ missing: tasks("missing"),
32049
+ changed: tasks("changed"),
32050
+ heldByPerson: held,
32051
+ keptForPerson: kept,
32052
+ untracked: plan.untrackedKeys.map((k) => k.key)
32053
+ }
32054
+ } : {}
32055
+ };
32056
+ }
32057
+ function needsTranslation(language) {
32058
+ return language.missing > 0 || language.changed > 0;
32059
+ }
32060
+ var REASON_WORDS = {
32061
+ locked: "locked",
32062
+ "pending-draft": "pending draft",
32063
+ manual: "written by a person",
32064
+ "target-edited": "edited since TolkBot wrote it",
32065
+ unknown: "no record of who wrote it"
32066
+ };
32067
+ function reasons(counts) {
32068
+ return Object.entries(counts.byReason).map(
32069
+ ([reason, n]) => `${n} ${REASON_WORDS[reason]}`
32070
+ ).join(", ");
32071
+ }
32072
+ function describeLanguage(language) {
32073
+ const counts = [
32074
+ `${language.expected} expected`,
32075
+ `${language.upToDate} up to date`,
32076
+ `${language.missing} missing`,
32077
+ `${language.changed} changed since translated`
32078
+ ].join(", ");
32079
+ const mark = needsTranslation(language) ? "\u2022" : "\u2713";
32080
+ const lines = [`${mark} ${language.locale}: ${counts}`];
32081
+ if (language.heldByPerson.total > 0) {
32082
+ lines.push(
32083
+ ` ${language.heldByPerson.total} held by a person (${reasons(language.heldByPerson)})`
32084
+ );
32085
+ }
32086
+ if (language.keptForPerson.total > 0) {
32087
+ lines.push(
32088
+ ` ${language.keptForPerson.total} changed but kept for a person (${reasons(language.keptForPerson)})`
32089
+ );
32090
+ }
32091
+ if (language.untracked > 0) {
32092
+ lines.push(
32093
+ ` ${language.untracked} untracked: TolkBot has no record of them`
32094
+ );
32095
+ }
32096
+ if (language.pullRequest) {
32097
+ lines.push(` Pull request: ${language.pullRequest.url}`);
32098
+ }
32099
+ const { keys } = language;
32100
+ if (keys) {
32101
+ const list = (label, names) => {
32102
+ if (names.length === 0) return;
32103
+ lines.push(` ${label}:`);
32104
+ for (const name of names) lines.push(` ${name}`);
32105
+ };
32106
+ list("Missing", keys.missing);
32107
+ list("Changed", keys.changed);
32108
+ list(
32109
+ "Held by a person",
32110
+ keys.heldByPerson.map(
32111
+ (k) => `${k.key} (${REASON_WORDS[k.reason]})`
32112
+ )
32113
+ );
32114
+ list(
32115
+ "Kept for a person",
32116
+ keys.keptForPerson.map(
32117
+ (k) => `${k.key} (${REASON_WORDS[k.reason]})`
32118
+ )
32119
+ );
32120
+ list("Untracked", keys.untracked);
32121
+ }
32122
+ return lines;
32123
+ }
32124
+
32125
+ // src/commands/status/status.tsx
32126
+ import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
32127
+ var OFFLINE_HINT = "Without a login, 'tolkbot validate' checks the files on this machine.";
32128
+ async function runStatus(opts) {
32129
+ let ctx;
32130
+ try {
32131
+ ctx = await loadPlanContext(process.cwd());
32132
+ } catch (error62) {
32133
+ if (error62 instanceof AuthError) {
32134
+ throw new AuthError(
32135
+ error62.baseMessage,
32136
+ [error62.hint, OFFLINE_HINT].filter(Boolean).join(" "),
32137
+ error62.code
32138
+ );
32139
+ }
32140
+ throw error62;
32141
+ }
32142
+ const { project } = ctx;
32143
+ const srcLocale = project.baseLanguage;
32144
+ const locales = project.targetLanguages;
32145
+ const progress = renderTransient(
32146
+ /* @__PURE__ */ jsxs16(Box16, { children: [
32147
+ /* @__PURE__ */ jsx16(Spinner13, {}),
32148
+ /* @__PURE__ */ jsxs16(Text16, { children: [
32149
+ " Reading the status of ",
32150
+ project.name,
32151
+ "\u2026"
32152
+ ] })
32153
+ ] })
32154
+ );
32155
+ const languages = [];
32156
+ let credits;
32157
+ try {
32158
+ const pullRequests = locales.length === 0 ? [] : await ctx.client.query(api.prJobs.listByProject, {
32159
+ organizationId: project.organizationId,
32160
+ projectId: project._id
32161
+ });
32162
+ for (const locale of locales) {
32163
+ const plan = await planForLocale(ctx, srcLocale, locale);
32164
+ languages.push(
32165
+ languageStatus(locale, plan, pullRequests, {
32166
+ keys: opts.keys
32167
+ })
32168
+ );
32169
+ }
32170
+ credits = await readCreditBalance(ctx);
32171
+ } finally {
32172
+ progress.unmount();
32173
+ }
32174
+ const payload = {
32175
+ project: {
32176
+ name: project.name,
32177
+ slug: project.slug,
32178
+ url: `${env.TOLKBOT_WEB_URL}/projects/${project.slug}`,
32179
+ baseLanguage: srcLocale,
32180
+ pattern: fullPattern(scopeOf(project)),
32181
+ autoCreatePR: project.workflow?.autoCreatePR ?? DEFAULT_BEHAVIORAL_SETTINGS.workflow.autoCreatePR
32182
+ },
32183
+ credits: { balance: credits },
32184
+ languages
32185
+ };
32186
+ const outOfDate = languages.some(needsTranslation);
32187
+ const exitCode = opts.exitCode && outOfDate ? 1 : 0;
32188
+ if (opts.json) {
32189
+ if (exitCode === 0) jsonSuccess(payload);
32190
+ jsonResult(payload, exitCode);
32191
+ return;
32192
+ }
32193
+ const { project: p } = payload;
32194
+ console.log(`${p.name} (${p.slug}) ${p.url}`);
32195
+ console.log(
32196
+ `Base language ${p.baseLanguage}, files ${p.pattern}, auto-create pull requests ${p.autoCreatePR ? "on" : "off"}`
32197
+ );
32198
+ console.log(
32199
+ credits === null ? "Credits left: could not be read" : `Credits left: ${credits}`
32200
+ );
32201
+ console.log("");
32202
+ if (languages.length === 0) {
32203
+ console.log(
32204
+ `No target languages configured. Add them in the project settings at ${p.url}/settings.`
32205
+ );
32206
+ }
32207
+ for (const language of languages) {
32208
+ for (const line of describeLanguage(language)) console.log(line);
32209
+ }
32210
+ if (outOfDate) {
32211
+ console.log(
32212
+ "\nRun 'tolkbot translate --all' to translate what is missing or changed."
32213
+ );
32214
+ }
32215
+ process.exitCode = exitCode;
32216
+ }
32217
+
32218
+ // src/commands/status/index.ts
32219
+ var statusCommand = new Command5("status").description(
32220
+ "Show what each language is missing, what waits for a person, the open pull requests and the credits left"
32221
+ ).option("--keys", "List the keys by name under each language").option(
32222
+ "--exit-code",
32223
+ "Exit 1 when any language has missing or changed keys"
32224
+ ).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
32225
+ const schema = z7.object({
32226
+ keys: z7.boolean().optional(),
32227
+ exitCode: z7.boolean().optional(),
32228
+ json: z7.boolean().optional()
32229
+ });
32230
+ await runStatus(validateOrExit(schema, opts));
32231
+ });
32232
+
32233
+ // src/commands/telemetry.ts
32234
+ import { Command as Command6 } from "commander";
32235
+ import { z as z8 } from "zod/v4";
32236
+ var REASONS = {
32237
+ env: `disabled by $${TELEMETRY_ENV_VAR}`,
32238
+ "do-not-track": `disabled by $${DO_NOT_TRACK_ENV_VAR}`,
32239
+ preference: "disabled with `tolkbot telemetry off`",
32240
+ "no-key": "disabled: this build has no telemetry key",
32241
+ default: "enabled"
32242
+ };
32243
+ async function printStatus(json2) {
32244
+ const state = await telemetryState();
32245
+ if (json2) {
32246
+ jsonResult({ ...state, preferenceFile: getPreferencePath() }, 0);
32247
+ return;
32248
+ }
32249
+ console.log(
32250
+ `Telemetry is ${state.enabled ? "on" : "off"} (${REASONS[state.reason]}).`
32251
+ );
32252
+ console.log(
32253
+ `Turn it off with \`tolkbot telemetry off\`, $${TELEMETRY_ENV_VAR}=0 or $${DO_NOT_TRACK_ENV_VAR}=1.`
32254
+ );
32255
+ }
32256
+ var telemetryCommand = new Command6("telemetry").description(
32257
+ "Control anonymous usage reporting"
32258
+ );
32259
+ telemetryCommand.command("status", { isDefault: true }).description("Show whether telemetry is on and why").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
32260
+ const parsed = validateOrExit(
32261
+ z8.object({ json: z8.boolean().optional() }),
32262
+ opts
32263
+ );
32264
+ await printStatus(parsed.json);
32265
+ });
32266
+ telemetryCommand.command("off").description("Stop reporting usage from this machine").action(async () => {
32267
+ await setTelemetryPreference(false);
32268
+ console.log(
32269
+ `Telemetry is off. Preference saved to ${getPreferencePath()}.`
32270
+ );
32271
+ });
32272
+ telemetryCommand.command("on").description("Resume reporting usage from this machine").action(async () => {
32273
+ await setTelemetryPreference(true);
32274
+ await printStatus(false);
32275
+ });
32276
+
32277
+ // src/commands/translate/index.ts
32278
+ import { Command as Command7 } from "commander";
32279
+ import { z as z9 } from "zod/v4";
32280
+
32281
+ // src/commands/translate/adopt.tsx
32282
+ var STAMP_CHUNK2 = 200;
32283
+ async function adoptLocale(ctx, srcLocale, dstLocale) {
32284
+ const plan = await planForLocale(ctx, srcLocale, dstLocale);
32285
+ const locale = governanceLocale(ctx.project, dstLocale);
32286
+ const byTemplate = /* @__PURE__ */ new Map();
32287
+ for (const key of plan.untrackedKeys) {
32288
+ const group = byTemplate.get(key.pathTemplate) ?? [];
32289
+ group.push(key);
32290
+ byTemplate.set(key.pathTemplate, group);
32291
+ }
32292
+ const outcome = {
32293
+ dstLocale,
32294
+ untracked: plan.untrackedKeys.length,
32295
+ adopted: 0,
32296
+ skipped: 0
32297
+ };
32298
+ for (const [pathTemplate, keys] of byTemplate) {
32299
+ for (let i = 0; i < keys.length; i += STAMP_CHUNK2) {
32300
+ const result = await ctx.client.mutation(
32301
+ api.localeKeyStates.adoptUntrackedForCli,
32302
+ {
32303
+ organizationId: ctx.project.organizationId,
32304
+ projectId: ctx.project._id,
32305
+ locale,
32306
+ pathTemplate,
32307
+ entries: keys.slice(i, i + STAMP_CHUNK2).map((key) => ({
32308
+ key: key.fileKey,
32309
+ namespace: key.namespace,
32310
+ baseValue: key.src,
32311
+ translatedValue: key.dst
32312
+ }))
32313
+ }
32314
+ );
32315
+ outcome.adopted += result.adopted;
32316
+ outcome.skipped += result.skipped;
32317
+ }
32318
+ }
32319
+ return outcome;
32320
+ }
32321
+ async function runAdopt(opts) {
32322
+ const run = async () => {
32323
+ const ctx = await loadPlanContext(process.cwd());
32324
+ const src = opts.src ?? ctx.project.baseLanguage;
32325
+ const locales = opts.all ? ctx.project.targetLanguages : opts.dst ? [opts.dst] : [];
32326
+ if (locales.length === 0) {
32327
+ throw opts.all ? new CliError(
32328
+ "No target languages configured for this project.",
32329
+ void 0,
32330
+ "no_target_languages"
32331
+ ) : new UsageError(
32332
+ "Either --dst <locale> or --all must be provided."
32333
+ );
32334
+ }
32335
+ const outcomes2 = [];
32336
+ for (const dst of locales)
32337
+ outcomes2.push(await adoptLocale(ctx, src, dst));
32338
+ track("cli_translate_completed", {
32339
+ mode: "adopt",
32340
+ key_count: outcomes2.reduce((n, o) => n + o.adopted, 0),
32341
+ locale_count: outcomes2.length,
32342
+ job_count: 0,
32343
+ json: opts.json === true
32344
+ });
32345
+ return outcomes2;
32346
+ };
32347
+ if (opts.json) {
32348
+ jsonSuccess({ adopted: await run() });
32349
+ }
32350
+ const outcomes = await run();
32351
+ for (const o of outcomes) {
32352
+ console.log(
32353
+ `\u2713 ${o.dstLocale}: adopted ${o.adopted} untracked translation(s)` + (o.skipped > 0 ? `, ${o.skipped} already tracked` : "")
32090
32354
  );
32091
32355
  }
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
32356
  }
32102
- function errorPayload(error62) {
32103
- return {
32104
- error: {
32105
- code: error62.code,
32106
- message: error62.baseMessage,
32107
- hint: error62.hint ?? null
32108
- }
32109
- };
32357
+
32358
+ // src/commands/translate/edit.tsx
32359
+ import path10 from "path";
32360
+ import { Spinner as Spinner14 } from "@inkjs/ui";
32361
+ import { Box as Box17, render as render16, Text as Text17 } from "ink";
32362
+
32363
+ // src/utils/locale-files.ts
32364
+ import path9 from "path";
32365
+ async function findLocaleFiles(baseDir) {
32366
+ const adapter = createAdapter("json");
32367
+ const result = await adapter.discover(baseDir);
32368
+ const localeFiles = /* @__PURE__ */ new Map();
32369
+ for (const [locale, files] of result.filesByLocale) {
32370
+ localeFiles.set(
32371
+ locale,
32372
+ files.map((f) => f.path)
32373
+ );
32374
+ }
32375
+ return localeFiles;
32110
32376
  }
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);
32377
+ function resolveFileForKey(files, key, namespace) {
32378
+ if (files.length === 0) return null;
32379
+ if (files.length === 1) return files[0] ?? null;
32380
+ const ns = namespace ?? key.split(".")[0];
32381
+ for (const file2 of files) {
32382
+ const fileName = path9.parse(path9.basename(file2)).name;
32383
+ if (fileName === ns) {
32384
+ return file2;
32121
32385
  }
32122
32386
  }
32123
- const code = exitCodeFor(cli.code);
32124
- process.exitCode = code;
32125
- return code;
32387
+ return files[0] ?? null;
32126
32388
  }
32127
- function fail(error62) {
32128
- const cli = toCliError(error62);
32129
- throw requestExit(exitCodeFor(cli.code), () => {
32130
- reportError(cli);
32131
- });
32389
+ function resolveKeyInFile(key, targetFile, fileCount, namespace) {
32390
+ if (fileCount <= 1 || namespace) return key;
32391
+ const fileName = path9.parse(path9.basename(targetFile)).name;
32392
+ if (key.startsWith(`${fileName}.`)) {
32393
+ return key.slice(fileName.length + 1);
32394
+ }
32395
+ return key;
32132
32396
  }
32133
32397
 
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
- ] })
32398
+ // src/commands/translate/edit.tsx
32399
+ import { jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
32400
+ function EditUI({ state }) {
32401
+ if (state.type === "editing") {
32402
+ return /* @__PURE__ */ jsxs17(Box17, { children: [
32403
+ /* @__PURE__ */ jsx17(Spinner14, {}),
32404
+ /* @__PURE__ */ jsx17(Text17, { children: " Editing translation\u2026" })
32161
32405
  ] });
32162
32406
  }
32163
- if (status === "generating") {
32164
- return /* @__PURE__ */ jsxs19(Box19, { children: [
32165
- /* @__PURE__ */ jsx19(Spinner16, {}),
32166
- /* @__PURE__ */ jsxs19(Text19, { children: [
32407
+ return /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
32408
+ /* @__PURE__ */ jsxs17(Box17, { children: [
32409
+ /* @__PURE__ */ jsx17(Text17, { color: "green", children: "\u2713" }),
32410
+ /* @__PURE__ */ jsxs17(Text17, { children: [
32167
32411
  " ",
32168
- srcLocale,
32169
- " \u2192 ",
32170
- dstLocale,
32171
- ": Generating tasks..."
32412
+ "Updated ",
32413
+ /* @__PURE__ */ jsx17(Text17, { bold: true, children: state.key })
32172
32414
  ] })
32173
- ] });
32415
+ ] }),
32416
+ /* @__PURE__ */ jsx17(Box17, { paddingLeft: 2, children: /* @__PURE__ */ jsx17(Text17, { dimColor: true, children: state.value }) }),
32417
+ /* @__PURE__ */ jsx17(Box17, { marginTop: 1, children: /* @__PURE__ */ jsxs17(Text17, { dimColor: true, children: [
32418
+ "File: ",
32419
+ state.filePath
32420
+ ] }) })
32421
+ ] });
32422
+ }
32423
+ async function performEdit(opts) {
32424
+ const baseDir = process.cwd();
32425
+ const config3 = readConfig(baseDir);
32426
+ if (!config3) throw noConfigError();
32427
+ const localeFiles = await findLocaleFiles(baseDir);
32428
+ const files = localeFiles.get(opts.locale);
32429
+ if (!files || files.length === 0) {
32430
+ throw new CliError(
32431
+ `No translation files found for locale "${opts.locale}".`,
32432
+ `Available locales: ${[...localeFiles.keys()].join(", ") || "none"}.`,
32433
+ "no_translation_files"
32434
+ );
32174
32435
  }
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
- ] });
32436
+ const targetFile = resolveFileForKey(files, opts.key, opts.namespace);
32437
+ if (!targetFile) {
32438
+ throw new CliError(
32439
+ `Could not determine target file for key "${opts.key}" in locale "${opts.locale}".`,
32440
+ "Pass --namespace to say which file the key belongs to."
32441
+ );
32197
32442
  }
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
- ] });
32443
+ const keyInFile = resolveKeyInFile(
32444
+ opts.key,
32445
+ targetFile,
32446
+ files.length,
32447
+ opts.namespace
32448
+ );
32449
+ const adapter = createAdapter("json");
32450
+ let existingData = {};
32451
+ try {
32452
+ const parsed = await adapter.parse(targetFile);
32453
+ existingData = parsed.data;
32454
+ } catch {
32220
32455
  }
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
- ] });
32456
+ const updatedData = setByPath(
32457
+ existingData,
32458
+ keyInFile,
32459
+ opts.value
32460
+ );
32461
+ await adapter.serialize(targetFile, updatedData, { merge: false });
32462
+ const relativePath = path10.relative(baseDir, targetFile);
32463
+ return { filePath: relativePath, key: opts.key, value: opts.value };
32464
+ }
32465
+ async function runEdit(opts) {
32466
+ if (opts.json) {
32467
+ const result2 = await performEdit(opts);
32468
+ jsonSuccess({
32469
+ file: result2.filePath,
32470
+ key: result2.key,
32471
+ value: result2.value
32472
+ });
32235
32473
  }
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
- ] });
32474
+ const { unmount } = renderTransient(/* @__PURE__ */ jsx17(EditUI, { state: { type: "editing" } }));
32475
+ let result;
32476
+ try {
32477
+ result = await performEdit(opts);
32478
+ } finally {
32479
+ unmount();
32249
32480
  }
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
- ] });
32481
+ render16(/* @__PURE__ */ jsx17(EditUI, { state: { type: "success", ...result } })).unmount();
32290
32482
  }
32291
32483
 
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);
32484
+ // src/commands/translate/translate.tsx
32485
+ import { Spinner as Spinner15 } from "@inkjs/ui";
32486
+ import { Box as Box18, Text as Text18 } from "ink";
32487
+ import { jsx as jsx18, jsxs as jsxs18 } from "react/jsx-runtime";
32488
+ function planOutput(plan) {
32489
+ return {
32490
+ ...plan.summary,
32491
+ protectedKeys: plan.protectedKeys,
32492
+ heldKeys: plan.heldKeys,
32493
+ untrackedKeys: plan.untrackedKeys.map((k) => k.key)
32494
+ };
32301
32495
  }
32302
- function isJobUnsuccessful(status) {
32303
- return status === "failed" || status === "cancelled";
32496
+ function fullKey5(namespace, key) {
32497
+ return namespace ? `${namespace}.${key}` : key;
32304
32498
  }
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"
32499
+ function characters(text) {
32500
+ return Array.from(text).length;
32501
+ }
32502
+ function targetLocales(ctx, opts) {
32503
+ if (opts.dst) return [opts.dst];
32504
+ const locales = ctx.project.targetLanguages;
32505
+ if (locales.length === 0) {
32506
+ throw new CliError(
32507
+ "No target languages configured.",
32508
+ `Configure them in the project settings at ${env.TOLKBOT_WEB_URL}/projects/${ctx.project.slug}/settings.`,
32509
+ "no_target_languages"
32311
32510
  );
32312
32511
  }
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
- );
32512
+ return locales;
32513
+ }
32514
+ async function runTranslate(opts) {
32515
+ const ctx = await loadPlanContext(process.cwd());
32516
+ const srcLocale = opts.src ?? ctx.project.baseLanguage;
32517
+ const dstLocales = targetLocales(ctx, opts);
32518
+ if (opts.dryRun) {
32519
+ await runDryRun(ctx, srcLocale, dstLocales, opts);
32520
+ return;
32319
32521
  }
32320
- return null;
32522
+ await runLanguages(ctx, srcLocale, dstLocales, opts);
32321
32523
  }
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";
32524
+ var NO_ESTIMATE = "No credit estimate exists for this project yet. Keys and characters are what a run would send.";
32525
+ async function runDryRun(ctx, srcLocale, dstLocales, opts) {
32526
+ const languages = [];
32527
+ const progress = renderTransient(
32528
+ /* @__PURE__ */ jsx18(Working, { text: `Planning ${dstLocales.join(", ")}\u2026` })
32529
+ );
32530
+ try {
32531
+ for (const dstLocale of dstLocales) {
32532
+ const { plan } = await generateTasks(ctx, srcLocale, dstLocale, {
32533
+ retranslateUntracked: opts.retranslateUntracked
32534
+ });
32535
+ languages.push({
32536
+ dstLocale,
32537
+ keys: plan.tasks.length,
32538
+ characters: plan.tasks.reduce(
32539
+ (n, task) => n + characters(task.src),
32540
+ 0
32541
+ ),
32542
+ tasks: plan.tasks.map((task) => ({
32543
+ key: fullKey5(task.namespace, task.key),
32544
+ path: task.path,
32545
+ reason: task.reason,
32546
+ ...opts.verbose ? { src: task.src } : {}
32547
+ })),
32548
+ plan
32549
+ });
32550
+ }
32551
+ } finally {
32552
+ progress.unmount();
32333
32553
  }
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"
32554
+ const total = {
32555
+ keys: languages.reduce((n, l) => n + l.keys, 0),
32556
+ characters: languages.reduce((n, l) => n + l.characters, 0)
32557
+ };
32558
+ const balance = await readCreditBalance(ctx);
32559
+ track("cli_translate_completed", {
32560
+ mode: "plan",
32561
+ key_count: total.keys,
32562
+ locale_count: languages.length,
32563
+ job_count: 0,
32564
+ json: opts.json ?? false
32565
+ });
32566
+ if (opts.json) {
32567
+ jsonSuccess({
32568
+ dryRun: true,
32569
+ srcLocale,
32570
+ languages: languages.map(({ plan, ...language }) => ({
32571
+ ...language,
32572
+ plan: planOutput(plan)
32573
+ })),
32574
+ total,
32575
+ estimate: null,
32576
+ estimateNote: NO_ESTIMATE,
32577
+ credits: { balance }
32578
+ });
32579
+ }
32580
+ for (const language of languages) {
32581
+ const head = `${srcLocale} \u2192 ${language.dstLocale}`;
32582
+ if (language.keys === 0) {
32583
+ console.log(`\u2713 ${head}: up to date`);
32584
+ } else {
32585
+ console.log(
32586
+ `\u2022 ${head}: ${language.keys} key(s) to translate, ${language.characters} characters`
32349
32587
  );
32350
32588
  }
32351
- opts.onProgress?.(summary);
32352
- if (isJobFinished(summary.status)) return summary;
32353
- if (now() >= deadline) {
32354
- throw new JobWaitTimeoutError(jobId, timeoutMs);
32589
+ for (const line of describePlan(language.plan)) {
32590
+ console.log(` ${line}`);
32591
+ }
32592
+ if (opts.verbose) {
32593
+ for (const task of language.tasks) {
32594
+ console.log(` ${task.key}: ${JSON.stringify(task.src)}`);
32595
+ }
32355
32596
  }
32356
- await sleep(pollMs);
32357
32597
  }
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 }
32598
+ console.log(
32599
+ `
32600
+ In all: ${total.keys} key(s), ${total.characters} characters.` + (balance === null ? "" : ` Credit balance: ${balance}.`)
32392
32601
  );
32393
- return { jobId, summary };
32602
+ console.log(NO_ESTIMATE);
32603
+ console.log("Nothing was started. Run without --dry-run to translate.");
32394
32604
  }
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());
32605
+ function Working({ text }) {
32606
+ return /* @__PURE__ */ jsxs18(Box18, { children: [
32607
+ /* @__PURE__ */ jsx18(Spinner15, {}),
32608
+ /* @__PURE__ */ jsxs18(Text18, { children: [
32609
+ " ",
32610
+ text
32611
+ ] })
32612
+ ] });
32613
+ }
32614
+ function progressText(dstLocale, summary) {
32615
+ if (!summary) return `${dstLocale}: starting\u2026`;
32616
+ const done = summary.completed + summary.failed;
32617
+ return `${dstLocale}: translating ${done}/${summary.total}\u2026`;
32618
+ }
32619
+ function describeOutcome(outcome) {
32620
+ const { dstLocale, status, keys, written } = outcome;
32621
+ const wrote = written ? `, ${written.keys} written to ${written.files} file(s)` : "";
32622
+ const notApplied = outcome.notApplied.length > 0 ? `, ${outcome.notApplied.length} not applied` : "";
32623
+ switch (status) {
32624
+ case "up-to-date":
32625
+ return `\u2713 ${dstLocale}: up to date`;
32626
+ case "started":
32627
+ return `\u2192 ${dstLocale}: started job ${outcome.jobId} with ${keys} key(s)`;
32628
+ case "succeeded":
32629
+ return `\u2713 ${dstLocale}: ${outcome.completed} translated${wrote}${notApplied}`;
32630
+ case "partial":
32631
+ return `\u26A0 ${dstLocale}: ${outcome.completed} of ${keys} translated, ${outcome.failed} failed${wrote}${notApplied}`;
32632
+ case "failed":
32633
+ return `\u2717 ${dstLocale}: the job failed (${outcome.jobId})${wrote}${notApplied}`;
32634
+ case "cancelled":
32635
+ return `\u2717 ${dstLocale}: the job was cancelled (${outcome.jobId})`;
32636
+ case "timed-out":
32637
+ return `\u2717 ${dstLocale}: ${outcome.error}`;
32638
+ case "not-started":
32639
+ return `\u2717 ${dstLocale}: not started. ${outcome.error}`;
32640
+ case "pending":
32641
+ case "running":
32642
+ return `\u2026 ${dstLocale}: ${status}`;
32643
+ }
32644
+ }
32645
+ async function runLanguages(ctx, srcLocale, dstLocales, opts) {
32400
32646
  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."
32647
+ const json2 = opts.json ?? false;
32648
+ const outcomes = [];
32649
+ let failure2 = null;
32650
+ let stopped = null;
32651
+ const report = (outcome) => {
32652
+ outcomes.push(outcome);
32653
+ if (json2) return;
32654
+ console.log(describeOutcome(outcome));
32655
+ for (const line of describeNotApplied(outcome.notApplied)) {
32656
+ console.log(` ${line}`);
32657
+ }
32658
+ };
32659
+ for (const dstLocale of dstLocales) {
32660
+ const progress = renderTransient(
32661
+ /* @__PURE__ */ jsx18(Working, { text: `${dstLocale}: planning\u2026` })
32418
32662
  );
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;
32663
+ try {
32664
+ const { tasks, plan } = await generateTasks(
32665
+ ctx,
32666
+ srcLocale,
32667
+ dstLocale,
32668
+ { retranslateUntracked: opts.retranslateUntracked }
32669
+ );
32670
+ const base = {
32671
+ dstLocale,
32672
+ status: "up-to-date",
32673
+ jobId: null,
32674
+ keys: tasks.length,
32675
+ completed: null,
32676
+ failed: null,
32677
+ written: null,
32678
+ notApplied: [],
32679
+ error: null,
32680
+ plan: planOutput(plan)
32681
+ };
32682
+ const leftOut = describeLeftOut(plan);
32683
+ if (stopped !== null) {
32684
+ progress.unmount();
32685
+ report({ ...base, status: "not-started", error: stopped });
32686
+ continue;
32687
+ }
32428
32688
  if (tasks.length === 0) {
32429
- results.push({
32430
- dstLocale,
32431
- status: "up-to-date",
32432
- plan: summary
32433
- });
32689
+ progress.unmount();
32690
+ report(base);
32691
+ printNotes(json2, leftOut);
32434
32692
  continue;
32435
32693
  }
32694
+ progress.rerender(/* @__PURE__ */ jsx18(Working, { text: progressText(dstLocale, null) }));
32695
+ let jobId;
32436
32696
  try {
32437
- const { jobId, summary: job } = await runTranslationJob({
32438
- projectId,
32697
+ jobId = await startTranslationJob({
32698
+ projectId: project._id,
32439
32699
  organizationId: project.organizationId,
32440
32700
  framework,
32441
32701
  srcLocale,
32442
32702
  dstLocale,
32443
32703
  tasks
32444
32704
  });
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
32705
  } 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
32706
+ const refused = toCliError(error62);
32707
+ failure2 ??= refused;
32708
+ progress.unmount();
32709
+ report({
32710
+ ...base,
32711
+ status: "not-started",
32712
+ error: refused.baseMessage
32464
32713
  });
32465
- failure3 ??= error62;
32466
- break;
32714
+ if (exitCodeFor(refused.code) === EXIT_CODES.billing) {
32715
+ stopped = `TolkBot refused the ${dstLocale} job: ${refused.baseMessage}`;
32716
+ }
32717
+ continue;
32467
32718
  }
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
- });
32719
+ if (!opts.wait) {
32720
+ progress.unmount();
32721
+ report({ ...base, status: "started", jobId });
32722
+ printNotes(json2, leftOut);
32521
32723
  continue;
32522
32724
  }
32523
- updateJob(dstLocale, {
32524
- status: "translating",
32525
- taskCount: tasks.length,
32526
- notes
32527
- });
32528
- let outcome;
32725
+ let summary;
32529
32726
  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
- });
32727
+ summary = await waitForTranslationJob(
32728
+ project.organizationId,
32729
+ jobId,
32730
+ (latest) => progress.rerender(
32731
+ /* @__PURE__ */ jsx18(Working, { text: progressText(dstLocale, latest) })
32732
+ )
32733
+ );
32541
32734
  } catch (error62) {
32542
32735
  if (!(error62 instanceof JobWaitTimeoutError)) throw error62;
32543
- updateJob(dstLocale, {
32544
- status: "error",
32545
- jobId: error62.jobId,
32546
- error: error62.message
32547
- });
32548
32736
  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,
32737
+ progress.unmount();
32738
+ report({
32739
+ ...base,
32740
+ status: "timed-out",
32565
32741
  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
32742
+ error: error62.baseMessage
32576
32743
  });
32744
+ stopped = `The CLI stopped waiting for the ${dstLocale} job.`;
32745
+ continue;
32746
+ }
32747
+ const finished = {
32748
+ ...base,
32749
+ status: summary.status,
32750
+ jobId,
32751
+ completed: summary.completed,
32752
+ failed: summary.failed
32753
+ };
32754
+ if (summary.status !== "cancelled") {
32755
+ progress.rerender(
32756
+ /* @__PURE__ */ jsx18(Working, { text: `${dstLocale}: writing the files\u2026` })
32757
+ );
32758
+ const applied = await applyJob(ctx, jobId);
32759
+ finished.written = { files: applied.files, keys: applied.keys };
32760
+ finished.notApplied = applied.notApplied;
32761
+ }
32762
+ const unsuccessful = unsuccessfulJobError(jobId, summary.status);
32763
+ if (unsuccessful) {
32764
+ failure2 ??= unsuccessful;
32765
+ finished.error = unsuccessful.baseMessage;
32577
32766
  }
32767
+ progress.unmount();
32768
+ report(finished);
32769
+ printNotes(json2, leftOut);
32770
+ if (summary.status === "cancelled") {
32771
+ stopped = `The ${dstLocale} job was cancelled.`;
32772
+ }
32773
+ } finally {
32774
+ progress.unmount();
32578
32775
  }
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
32776
  }
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
32777
+ track("cli_translate_completed", {
32778
+ mode: "run",
32779
+ key_count: outcomes.reduce((n, o) => n + o.keys, 0),
32780
+ locale_count: outcomes.length,
32781
+ job_count: outcomes.filter((o) => o.jobId).length,
32782
+ json: json2
32672
32783
  });
32673
- if (!summary) {
32674
- throw new CliError(
32675
- `Translation job ${jobId} was not found in this project.`,
32676
- void 0,
32677
- "job_not_found"
32784
+ const payload = { srcLocale, languages: outcomes };
32785
+ if (json2) {
32786
+ if (!failure2) jsonSuccess(payload);
32787
+ jsonResult(payload, 1);
32788
+ fail(failure2);
32789
+ }
32790
+ if (!opts.wait && outcomes.some((o) => o.status === "started")) {
32791
+ console.log(
32792
+ "\nThe jobs keep running. Follow them with 'tolkbot jobs list' and write them into the files with 'tolkbot jobs apply'."
32678
32793
  );
32679
32794
  }
32680
- return summary;
32795
+ if (failure2) fail(failure2);
32681
32796
  }
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);
32797
+ function printNotes(json2, notes) {
32798
+ if (json2) return;
32799
+ for (const note of notes) console.log(` ${note}`);
32800
+ }
32801
+ function checkTranslateOptions(opts) {
32802
+ if (!opts.dst && !opts.all) {
32803
+ throw new UsageError(
32804
+ "Either --dst <locale> or --all must be provided.",
32805
+ "Run 'tolkbot translate --help'."
32806
+ );
32696
32807
  }
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();
32808
+ if (opts.dst && opts.all) {
32809
+ throw new UsageError("Cannot use both --dst and --all.");
32705
32810
  }
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);
32811
+ if (opts.dryRun && !opts.wait) {
32812
+ throw new UsageError(
32813
+ "Cannot use --dry-run with --no-wait: a dry run starts nothing."
32814
+ );
32721
32815
  }
32722
32816
  }
32723
32817
 
32724
32818
  // src/commands/translate/index.ts
32725
- var translateCommand = new Command5("translate").description(
32726
- "Translate with TolkBot"
32727
- );
32728
32819
  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"
32820
+ var translateCommand = new Command7("translate").description(
32821
+ "Translate what is missing or changed, and write the results into your locale files"
32731
32822
  ).option(
32732
32823
  "--src <locale>",
32733
32824
  "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) {
32825
+ ).option("--dst <locale>", "Destination locale").option("--all", "Every target language").option("--dry-run", "Print what a run would translate, and start nothing").option(
32826
+ "--no-wait",
32827
+ "Start the jobs and print their IDs without waiting; apply them later with 'tolkbot jobs apply'"
32828
+ ).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) => {
32829
+ const [word] = command.args;
32830
+ if (word !== void 0) {
32759
32831
  throw new UsageError(
32760
- "Either --dst <locale> or --all must be provided."
32832
+ `unknown command 'translate ${word}'`,
32833
+ "Run 'tolkbot translate --help'. Jobs are under 'tolkbot jobs'."
32761
32834
  );
32762
32835
  }
32763
- if (parsed.dst && parsed.all) {
32764
- throw new UsageError("Cannot use both --dst and --all.");
32765
- }
32836
+ const schema = z9.object({
32837
+ src: z9.string().min(1).optional(),
32838
+ dst: z9.string().min(1).optional(),
32839
+ all: z9.boolean().optional(),
32840
+ dryRun: z9.boolean().optional(),
32841
+ wait: z9.boolean(),
32842
+ retranslateUntracked: z9.boolean().optional(),
32843
+ verbose: z9.boolean().optional(),
32844
+ json: z9.boolean().optional()
32845
+ });
32846
+ const parsed = validateOrExit(schema, opts);
32847
+ checkTranslateOptions(parsed);
32766
32848
  await runTranslate(parsed);
32767
32849
  });
32768
32850
  translateCommand.command("adopt").description(
@@ -32771,11 +32853,11 @@ translateCommand.command("adopt").description(
32771
32853
  "--src <locale>",
32772
32854
  "Source locale (defaults to the project's base language)"
32773
32855
  ).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()
32856
+ const schema = z9.object({
32857
+ src: z9.string().min(1).optional(),
32858
+ dst: z9.string().min(1).optional(),
32859
+ all: z9.boolean().optional(),
32860
+ json: z9.boolean().optional()
32779
32861
  });
32780
32862
  const parsed = validateOrExit(schema, opts);
32781
32863
  if (parsed.dst && parsed.all) {
@@ -32783,46 +32865,20 @@ translateCommand.command("adopt").description(
32783
32865
  }
32784
32866
  await runAdopt(parsed);
32785
32867
  });
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
32868
  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()
32869
+ const schema = z9.object({
32870
+ key: z9.string().min(1),
32871
+ locale: z9.string().min(1),
32872
+ value: z9.string().min(1),
32873
+ namespace: z9.string().optional(),
32874
+ json: z9.boolean().optional()
32811
32875
  });
32812
32876
  const parsed = validateOrExit(schema, opts);
32813
32877
  await runEdit(parsed);
32814
32878
  });
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
32879
 
32824
32880
  // src/commands/validate.tsx
32825
- import { Box as Box21, render as render21, Text as Text21 } from "ink";
32881
+ import { Box as Box19, render as render17, Text as Text19 } from "ink";
32826
32882
 
32827
32883
  // ../ai/src/validators/placeholders.ts
32828
32884
  var IDENTIFIER = /^[A-Za-z0-9_$][A-Za-z0-9_$.[\]-]*$/;
@@ -33915,7 +33971,7 @@ function finalize2(issues, stats, strict) {
33915
33971
  }
33916
33972
 
33917
33973
  // src/commands/validate.tsx
33918
- import { jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
33974
+ import { jsx as jsx19, jsxs as jsxs19 } from "react/jsx-runtime";
33919
33975
  var LEVEL_COLOR = {
33920
33976
  error: "red",
33921
33977
  warning: "yellow",
@@ -33924,12 +33980,12 @@ var LEVEL_COLOR = {
33924
33980
  var GUTTER = 9;
33925
33981
  function IssueLine({ issue: issue2 }) {
33926
33982
  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 }) })
33983
+ return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
33984
+ /* @__PURE__ */ jsxs19(Box19, { children: [
33985
+ /* @__PURE__ */ jsx19(Box19, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx19(Text19, { color: LEVEL_COLOR[issue2.level], children: issue2.level }) }),
33986
+ /* @__PURE__ */ jsx19(Box19, { flexGrow: 1, children: /* @__PURE__ */ jsx19(Text19, { children: issue2.message }) })
33931
33987
  ] }),
33932
- /* @__PURE__ */ jsx22(Box21, { marginLeft: GUTTER, children: /* @__PURE__ */ jsxs21(Text21, { dimColor: true, children: [
33988
+ /* @__PURE__ */ jsx19(Box19, { marginLeft: GUTTER, children: /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
33933
33989
  issue2.rule,
33934
33990
  location ? ` \u2014 ${location}` : ""
33935
33991
  ] }) })
@@ -33938,20 +33994,20 @@ function IssueLine({ issue: issue2 }) {
33938
33994
  function ReportUI({ report }) {
33939
33995
  const { counts, stats } = report;
33940
33996
  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: [
33997
+ return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
33998
+ /* @__PURE__ */ jsxs19(Box19, { children: [
33999
+ /* @__PURE__ */ jsx19(Text19, { color: report.valid ? "green" : "red", children: report.valid ? "\u2713" : "\u2717" }),
34000
+ /* @__PURE__ */ jsxs19(Text19, { children: [
33945
34001
  " Validation ",
33946
34002
  report.valid ? "passed" : "failed"
33947
34003
  ] }),
33948
- summary ? /* @__PURE__ */ jsxs21(Text21, { dimColor: true, children: [
34004
+ summary ? /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
33949
34005
  " \u2014 ",
33950
34006
  summary
33951
34007
  ] }) : null
33952
34008
  ] }),
33953
- stats.locales.length > 0 && /* @__PURE__ */ jsxs21(Box21, { marginTop: 1, flexDirection: "column", children: [
33954
- /* @__PURE__ */ jsxs21(Text21, { dimColor: true, children: [
34009
+ stats.locales.length > 0 && /* @__PURE__ */ jsxs19(Box19, { marginTop: 1, flexDirection: "column", children: [
34010
+ /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
33955
34011
  stats.filesChecked,
33956
34012
  " ",
33957
34013
  stats.format,
@@ -33960,18 +34016,18 @@ function ReportUI({ report }) {
33960
34016
  stats.totalKeys,
33961
34017
  " keys"
33962
34018
  ] }),
33963
- /* @__PURE__ */ jsxs21(Text21, { dimColor: true, children: [
34019
+ /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
33964
34020
  "Locales: ",
33965
34021
  stats.locales.join(", "),
33966
34022
  stats.baseLocale ? ` (base: ${stats.baseLocale})` : ""
33967
34023
  ] }),
33968
- stats.pattern && /* @__PURE__ */ jsxs21(Text21, { dimColor: true, children: [
34024
+ stats.pattern && /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
33969
34025
  "Pattern: ",
33970
34026
  stats.pattern
33971
34027
  ] })
33972
34028
  ] }),
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." }) })
34029
+ report.issues.length > 0 && /* @__PURE__ */ jsx19(Box19, { marginTop: 1, flexDirection: "column", gap: 1, children: report.issues.map((issue2, idx) => /* @__PURE__ */ jsx19(IssueLine, { issue: issue2 }, idx)) }),
34030
+ !report.strict && counts.warning > 0 && report.valid && /* @__PURE__ */ jsx19(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: "Warnings did not fail this run. Use --strict to treat them as errors." }) })
33975
34031
  ] });
33976
34032
  }
33977
34033
  function emit(report, json2) {
@@ -33991,7 +34047,7 @@ function emit(report, json2) {
33991
34047
  if (!report.valid) fail(validationFailure(report));
33992
34048
  return;
33993
34049
  }
33994
- render21(/* @__PURE__ */ jsx22(ReportUI, { report })).unmount();
34050
+ render17(/* @__PURE__ */ jsx19(ReportUI, { report })).unmount();
33995
34051
  process.exitCode = exitCode;
33996
34052
  }
33997
34053
  function validationFailure(report) {
@@ -34050,12 +34106,9 @@ function commandPath(command) {
34050
34106
  return names.join(" ");
34051
34107
  }
34052
34108
  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();
34109
+ const program = new Command8().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();
34110
+ program.hook("preAction", (_thisCommand, actionCommand) => {
34111
+ const { token } = actionCommand.optsWithGlobals();
34059
34112
  setTokenOverride(token);
34060
34113
  resolveJsonOption(actionCommand);
34061
34114
  beginCommand(commandPath(actionCommand), version);
@@ -34081,11 +34134,14 @@ function buildProgram() {
34081
34134
  );
34082
34135
  program.addCommand(authCommand);
34083
34136
  program.addCommand(glossaryCommand);
34137
+ program.addCommand(jobsCommand);
34084
34138
  program.addCommand(learningsCommand);
34139
+ program.addCommand(statusCommand);
34085
34140
  program.addCommand(telemetryCommand);
34086
34141
  program.addCommand(translateCommand);
34087
34142
  program.addHelpText("after", PROGRAM_HELP_FOOTER);
34088
34143
  forEachCommand(program, (command) => {
34144
+ if (command !== program) addTokenOption(command);
34089
34145
  addNoJsonOption(command);
34090
34146
  reportUsageErrors(command);
34091
34147
  });
@@ -34095,6 +34151,11 @@ function forEachCommand(command, visit2) {
34095
34151
  visit2(command);
34096
34152
  for (const sub of command.commands) forEachCommand(sub, visit2);
34097
34153
  }
34154
+ var TOKEN_HELP = `Auth token to use for this invocation (overrides $${TOKEN_ENV_VAR} and the stored token)`;
34155
+ function addTokenOption(command) {
34156
+ if (command.options.some((o) => o.long === "--token")) return;
34157
+ command.addOption(new Option("--token <token>", TOKEN_HELP).hideHelp());
34158
+ }
34098
34159
  function jsonOptionOf(command) {
34099
34160
  return command.options.find((option) => option.long === "--json");
34100
34161
  }
@@ -34133,8 +34194,10 @@ var PROGRAM_HELP_FOOTER = `
34133
34194
  Examples:
34134
34195
  $ tolkbot auth login
34135
34196
  $ tolkbot init
34136
- $ tolkbot translate run --all
34137
- $ tolkbot translate apply
34197
+ $ tolkbot status
34198
+ $ tolkbot translate --all
34199
+ $ tolkbot translate --all --dry-run
34200
+ $ tolkbot jobs list
34138
34201
  $ tolkbot validate
34139
34202
 
34140
34203
  Without a terminal (an agent, CI), nothing prompts: commands take their