@tolkbot/cli 0.2.0 → 0.3.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 +1713 -1931
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3650,8 +3650,8 @@ var require_utils3 = __commonJS({
3650
3650
  exports.array = array2;
3651
3651
  var errno = require_errno();
3652
3652
  exports.errno = errno;
3653
- var fs9 = require_fs();
3654
- exports.fs = fs9;
3653
+ var fs10 = require_fs();
3654
+ exports.fs = fs10;
3655
3655
  var path12 = require_path();
3656
3656
  exports.path = path12;
3657
3657
  var pattern = require_pattern();
@@ -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 fs9 = __require("fs");
3838
+ var fs10 = __require("fs");
3839
3839
  exports.FILE_SYSTEM_ADAPTER = {
3840
- lstat: fs9.lstat,
3841
- stat: fs9.stat,
3842
- lstatSync: fs9.lstatSync,
3843
- statSync: fs9.statSync
3840
+ lstat: fs10.lstat,
3841
+ stat: fs10.stat,
3842
+ lstatSync: fs10.lstatSync,
3843
+ statSync: fs10.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 fs9 = require_fs2();
3860
+ var fs10 = 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 = fs9.createFileSystemAdapter(this._options.fs);
3865
+ this.fs = fs10.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
  }
@@ -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 fs9 = require_fs3();
4023
- exports.fs = fs9;
4022
+ var fs10 = require_fs3();
4023
+ exports.fs = fs10;
4024
4024
  }
4025
4025
  });
4026
4026
 
@@ -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 fs9 = __require("fs");
4218
+ var fs10 = __require("fs");
4219
4219
  exports.FILE_SYSTEM_ADAPTER = {
4220
- lstat: fs9.lstat,
4221
- stat: fs9.stat,
4222
- lstatSync: fs9.lstatSync,
4223
- statSync: fs9.statSync,
4224
- readdir: fs9.readdir,
4225
- readdirSync: fs9.readdirSync
4220
+ lstat: fs10.lstat,
4221
+ stat: fs10.stat,
4222
+ lstatSync: fs10.lstatSync,
4223
+ statSync: fs10.statSync,
4224
+ readdir: fs10.readdir,
4225
+ readdirSync: fs10.readdirSync
4226
4226
  };
4227
4227
  function createFileSystemAdapter(fsMethods) {
4228
4228
  if (fsMethods === void 0) {
@@ -4241,12 +4241,12 @@ var require_settings2 = __commonJS({
4241
4241
  Object.defineProperty(exports, "__esModule", { value: true });
4242
4242
  var path12 = __require("path");
4243
4243
  var fsStat = require_out();
4244
- var fs9 = require_fs4();
4244
+ var fs10 = 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 = fs9.createFileSystemAdapter(this._options.fs);
4249
+ this.fs = fs10.createFileSystemAdapter(this._options.fs);
4250
4250
  this.pathSegmentSeparator = this._getValue(this._options.pathSegmentSeparator, path12.sep);
4251
4251
  this.stats = this._getValue(this._options.stats, false);
4252
4252
  this.throwErrorOnBrokenSymbolicLink = this._getValue(this._options.throwErrorOnBrokenSymbolicLink, true);
@@ -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 fs9 = __require("fs");
5631
+ var fs10 = __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: fs9.lstat,
5636
- lstatSync: fs9.lstatSync,
5637
- stat: fs9.stat,
5638
- statSync: fs9.statSync,
5639
- readdir: fs9.readdir,
5640
- readdirSync: fs9.readdirSync
5635
+ lstat: fs10.lstat,
5636
+ lstatSync: fs10.lstatSync,
5637
+ stat: fs10.stat,
5638
+ statSync: fs10.statSync,
5639
+ readdir: fs10.readdir,
5640
+ readdirSync: fs10.readdirSync
5641
5641
  };
5642
5642
  var Settings = class {
5643
5643
  constructor(_options = {}) {
@@ -5784,114 +5784,41 @@ var require_out4 = __commonJS({
5784
5784
  });
5785
5785
 
5786
5786
  // src/main.ts
5787
- import { Command as Command6 } from "commander";
5787
+ import { Command as Command6, CommanderError } from "commander";
5788
5788
 
5789
5789
  // package.json
5790
- var version = "0.2.0";
5790
+ var version = "0.3.0";
5791
5791
 
5792
5792
  // src/commands/auth/index.ts
5793
5793
  import { Command } from "commander";
5794
5794
  import { z as z2 } from "zod/v4";
5795
5795
 
5796
- // src/utils/validate-command.ts
5797
- import { program } from "commander";
5798
- function validateOrExit(schema, value) {
5799
- const res = schema.safeParse(value);
5800
- if (!res.success) {
5801
- const msg = res.error.issues.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`).join("\n");
5802
- program.error(`Invalid arguments:
5803
- ${msg}`);
5804
- }
5805
- return res.data;
5806
- }
5807
-
5808
- // src/commands/auth/login.tsx
5809
- import http from "http";
5810
- import crypto from "crypto";
5811
- import { Spinner } from "@inkjs/ui";
5812
- import { Box, render, Text } from "ink";
5813
- import open from "open";
5814
-
5815
- // src/lib/telemetry.ts
5816
- import { randomUUID } from "crypto";
5817
- import fs2 from "fs/promises";
5818
- import os2 from "os";
5819
- import path2 from "path";
5820
-
5821
- // ../shared/src/analytics/index.ts
5822
- var ORGANIZATION_GROUP = "organization";
5823
- var analyticsEventNames = [
5824
- "trial_started",
5825
- "project_created",
5826
- "project_page_viewed",
5827
- "github_connect_started",
5828
- "github_install_initiated",
5829
- "plan_change_initiated",
5830
- "credits_purchase_initiated",
5831
- "glossary_created",
5832
- "glossary_deleted",
5833
- "glossary_imported",
5834
- "translation_job_viewed",
5835
- "translation_logs_viewed",
5836
- "cli_login_completed",
5837
- "cli_command_completed",
5838
- "cli_init_completed",
5839
- "cli_translate_completed",
5840
- "cli_validate_completed",
5841
- "translation_job_started",
5842
- "translation_job_completed",
5843
- "translation_job_failed",
5844
- "pull_request_opened",
5845
- "pull_request_merged",
5846
- "pull_request_closed_unmerged",
5847
- "github_app_installed",
5848
- "github_app_uninstalled",
5849
- "repository_connected",
5850
- "subscription_started",
5851
- "subscription_plan_changed",
5852
- "subscription_canceled",
5853
- "trial_ended",
5854
- "credits_purchased",
5855
- "credits_exhausted",
5856
- "usage_limit_reached",
5857
- "cta_clicked",
5858
- "waitlist_joined",
5859
- "signed_up",
5860
- "signed_in",
5861
- "onboarding_step_completed",
5862
- "onboarding_completed",
5863
- "language_added",
5864
- "translation_edited",
5865
- "translation_bulk_retranslated",
5866
- "dashboard_translation_started",
5867
- "project_settings_changed",
5868
- "member_invited",
5869
- "feedback_submitted",
5870
- "analytics_smoke_tested"
5871
- ];
5872
- var registeredNames = new Set(analyticsEventNames);
5873
-
5874
- // src/lib/telemetry.ts
5875
- import { PostHog } from "posthog-node";
5876
-
5877
5796
  // src/utils/errors.ts
5878
5797
  var CliError = class extends Error {
5879
5798
  hint;
5880
5799
  /** The message without the trailing hint. */
5881
5800
  baseMessage;
5882
- constructor(message, hint) {
5801
+ code;
5802
+ constructor(message, hint, code = "failed") {
5883
5803
  super(hint ? `${message} ${hint}` : message);
5884
5804
  this.name = "CliError";
5885
5805
  this.baseMessage = message;
5886
5806
  this.hint = hint;
5807
+ this.code = code;
5887
5808
  }
5888
5809
  };
5889
5810
  var AuthError = class extends CliError {
5890
- constructor(message, hint) {
5891
- super(message, hint);
5811
+ constructor(message, hint, code = "not_authenticated") {
5812
+ super(message, hint, code);
5892
5813
  this.name = "AuthError";
5893
5814
  }
5894
5815
  };
5816
+ var UsageError = class extends CliError {
5817
+ constructor(message, hint) {
5818
+ super(message, hint, "usage");
5819
+ this.name = "UsageError";
5820
+ }
5821
+ };
5895
5822
  function describeError(error62) {
5896
5823
  if (error62 instanceof Error) return error62.message;
5897
5824
  if (typeof error62 === "string") return error62;
@@ -5912,30 +5839,114 @@ function isUnauthenticatedError(error62) {
5912
5839
  "Could not parse JWT"
5913
5840
  ].some((marker) => message.includes(marker));
5914
5841
  }
5915
- function wantsJson(argv = process.argv) {
5916
- return argv.includes("--json");
5842
+ var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
5843
+ "ECONNREFUSED",
5844
+ "ECONNRESET",
5845
+ "ENOTFOUND",
5846
+ "EAI_AGAIN",
5847
+ "ETIMEDOUT",
5848
+ "ENETUNREACH",
5849
+ "EHOSTUNREACH",
5850
+ "UND_ERR_CONNECT_TIMEOUT",
5851
+ "UND_ERR_SOCKET"
5852
+ ]);
5853
+ function isNetworkError(error62) {
5854
+ for (let current = error62, depth = 0; current instanceof Error && depth < 4; current = current.cause, depth++) {
5855
+ const code = current.code;
5856
+ if (typeof code === "string" && NETWORK_ERROR_CODES.has(code)) {
5857
+ return true;
5858
+ }
5859
+ if (current.name === "TypeError" && current.message === "fetch failed")
5860
+ return true;
5861
+ }
5862
+ return false;
5917
5863
  }
5918
- function reportFatal(error62) {
5919
- const isKnown = error62 instanceof CliError;
5920
- const message = isKnown ? error62.baseMessage : describeError(error62);
5921
- const hint = isKnown ? error62.hint : void 0;
5922
- if (wantsJson()) {
5923
- console.error(JSON.stringify({ error: message, hint }, null, 2));
5924
- process.exit(1);
5864
+ function noConfigError() {
5865
+ return new CliError(
5866
+ "No tolkbot.json found.",
5867
+ "Run 'tolkbot init' to create one.",
5868
+ "no_config"
5869
+ );
5870
+ }
5871
+ function projectNotFoundError() {
5872
+ return new CliError(
5873
+ "Project not found.",
5874
+ "Run 'tolkbot init' again to link this repository to a project.",
5875
+ "project_not_found"
5876
+ );
5877
+ }
5878
+
5879
+ // src/utils/validate-command.ts
5880
+ function validateOrExit(schema, value) {
5881
+ const res = schema.safeParse(value);
5882
+ if (!res.success) {
5883
+ const msg = res.error.issues.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`).join("; ");
5884
+ throw new UsageError(`Invalid arguments: ${msg}`);
5885
+ }
5886
+ return res.data;
5887
+ }
5888
+
5889
+ // src/commands/auth/login.tsx
5890
+ import http from "http";
5891
+ import crypto from "crypto";
5892
+ import { Spinner } from "@inkjs/ui";
5893
+ import { Box, render, Text } from "ink";
5894
+ import open from "open";
5895
+
5896
+ // src/utils/env.ts
5897
+ import path from "path";
5898
+ import { fileURLToPath } from "url";
5899
+ import { createEnv } from "@t3-oss/env-core";
5900
+ import * as dotenv from "dotenv";
5901
+ import { z } from "zod/v4";
5902
+ var RELEASE_CONVEX_URL = "https://uncommon-monitor-462.convex.cloud"?.trim();
5903
+ var RELEASE_WEB_URL = "https://www.tolkbot.ai"?.trim();
5904
+ if (!RELEASE_CONVEX_URL) {
5905
+ try {
5906
+ const dir = path.dirname(fileURLToPath(import.meta.url));
5907
+ dotenv.config({
5908
+ path: path.resolve(dir, "../../../.env"),
5909
+ quiet: true
5910
+ });
5911
+ } catch {
5912
+ }
5913
+ }
5914
+ var env = createEnv({
5915
+ server: {
5916
+ // Optional here, required at the point of use: commands that never
5917
+ // reach the backend (help, logout) must work without deployment config.
5918
+ CONVEX_URL: z.url().optional(),
5919
+ TOLKBOT_WEB_URL: z.url().default("http://localhost:3000")
5920
+ },
5921
+ runtimeEnv: {
5922
+ CONVEX_URL: RELEASE_CONVEX_URL || process.env.CONVEX_URL,
5923
+ TOLKBOT_WEB_URL: RELEASE_WEB_URL || process.env.TOLKBOT_WEB_URL
5924
+ },
5925
+ skipValidation: !!process.env.CI || process.env.npm_lifecycle_event === "lint",
5926
+ onValidationError: (issues) => {
5927
+ const details = issues.map((issue2) => {
5928
+ const name = issue2.path?.join(".") ?? "(unknown)";
5929
+ return `${name}: ${issue2.message}`;
5930
+ }).join("\n ");
5931
+ throw new CliError(`Invalid configuration:
5932
+ ${details}`);
5925
5933
  }
5926
- console.error(`\u2717 ${message}`);
5927
- if (hint) console.error(` ${hint}`);
5928
- if (!isKnown && error62 instanceof Error && error62.stack) {
5929
- console.error("");
5930
- console.error(error62.stack);
5934
+ });
5935
+ function requireConvexUrl() {
5936
+ if (!env.CONVEX_URL) {
5937
+ throw new CliError(
5938
+ "CONVEX_URL is not set, so the CLI cannot reach the TolkBot backend.",
5939
+ "Set CONVEX_URL to your TolkBot deployment URL.",
5940
+ "network"
5941
+ );
5931
5942
  }
5932
- process.exit(1);
5943
+ return env.CONVEX_URL;
5933
5944
  }
5934
5945
 
5935
5946
  // src/utils/token.ts
5936
5947
  import fs from "fs/promises";
5937
5948
  import os from "os";
5938
- import path from "path";
5949
+ import path2 from "path";
5939
5950
  var TOKEN_ENV_VAR = "TOLKBOT_TOKEN";
5940
5951
  var EXPIRY_SKEW_SECONDS = 30;
5941
5952
  var tokenOverride = null;
@@ -5947,11 +5958,11 @@ function setTokenOverride(token) {
5947
5958
  tokenOverride = token?.trim() ? token.trim() : null;
5948
5959
  }
5949
5960
  function getTokenPath() {
5950
- return path.join(os.homedir(), ".tolkbot", "token");
5961
+ return path2.join(os.homedir(), ".tolkbot", "token");
5951
5962
  }
5952
5963
  async function saveToken(token) {
5953
5964
  const file2 = getTokenPath();
5954
- await fs.mkdir(path.dirname(file2), { recursive: true, mode: 448 });
5965
+ await fs.mkdir(path2.dirname(file2), { recursive: true, mode: 448 });
5955
5966
  await fs.writeFile(file2, token, { mode: 384 });
5956
5967
  }
5957
5968
  async function loadToken() {
@@ -6043,274 +6054,48 @@ function reauthHint(source) {
6043
6054
  }
6044
6055
  }
6045
6056
 
6046
- // src/lib/telemetry.ts
6047
- var TELEMETRY_ENV_VAR = "TOLKBOT_TELEMETRY";
6048
- var DO_NOT_TRACK_ENV_VAR = "DO_NOT_TRACK";
6049
- var POSTHOG_HOST = "https://eu.i.posthog.com";
6050
- var FLUSH_TIMEOUT_MS = 1e3;
6051
- var OFF_VALUES = /* @__PURE__ */ new Set(["0", "false", "off", "no"]);
6052
- var ON_VALUES = /* @__PURE__ */ new Set(["1", "true", "on", "yes"]);
6053
- function readEnv(name) {
6054
- return process.env[name]?.trim();
6055
- }
6056
- function projectKey() {
6057
- const key = "phc_uSMLUC9D7bHeaHFAosnoDzBj6DY5uzLcCQkXZxzj0mQ"?.trim();
6058
- if (!key) return void 0;
6059
- return key;
6060
- }
6061
- function environment() {
6062
- const nodeEnv = process.env.NODE_ENV;
6063
- return nodeEnv === "development" || nodeEnv === "test" ? nodeEnv : "production";
6064
- }
6065
- function inCi() {
6066
- return Boolean(process.env.CI);
6067
- }
6068
- function getTelemetryDir() {
6069
- return path2.join(os2.homedir(), ".tolkbot");
6070
- }
6071
- function getPreferencePath() {
6072
- return path2.join(getTelemetryDir(), "telemetry");
6073
- }
6074
- function getAnonymousIdPath() {
6075
- return path2.join(getTelemetryDir(), "anonymous-id");
6076
- }
6077
- async function readFileOrNull(file2) {
6078
- try {
6079
- return (await fs2.readFile(file2, "utf-8")).trim() || null;
6080
- } catch {
6081
- return null;
6082
- }
6083
- }
6084
- async function writePrivateFile(file2, content) {
6085
- await fs2.mkdir(path2.dirname(file2), { recursive: true, mode: 448 });
6086
- await fs2.writeFile(file2, content, { mode: 384 });
6087
- }
6088
- async function telemetryState() {
6089
- const envValue = readEnv(TELEMETRY_ENV_VAR)?.toLowerCase();
6090
- if (envValue !== void 0 && OFF_VALUES.has(envValue)) {
6091
- return { enabled: false, reason: "env" };
6092
- }
6093
- const doNotTrack = readEnv(DO_NOT_TRACK_ENV_VAR)?.toLowerCase();
6094
- if (doNotTrack !== void 0 && ON_VALUES.has(doNotTrack)) {
6095
- return { enabled: false, reason: "do-not-track" };
6096
- }
6097
- const preference = (await readFileOrNull(getPreferencePath()))?.toLowerCase();
6098
- if (preference !== void 0 && OFF_VALUES.has(preference)) {
6099
- return { enabled: false, reason: "preference" };
6100
- }
6101
- if (!projectKey()) {
6102
- return { enabled: false, reason: "no-key" };
6057
+ // src/commands/auth/login.tsx
6058
+ import { jsx, jsxs } from "react/jsx-runtime";
6059
+ var CALLBACK_PORTS = [3003, 3013, 3023, 3033, 3043];
6060
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
6061
+ function generateVerificationCode() {
6062
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
6063
+ let code = "";
6064
+ for (let i = 0; i < 8; i++) {
6065
+ code += chars.charAt(crypto.randomInt(chars.length));
6103
6066
  }
6104
- return { enabled: true, reason: "default" };
6067
+ return code;
6105
6068
  }
6106
- async function setTelemetryPreference(enabled) {
6107
- await writePrivateFile(getPreferencePath(), enabled ? "on\n" : "off\n");
6069
+ function respond(res, status, body) {
6070
+ res.writeHead(status, { "Content-Type": "text/html" });
6071
+ res.end(`
6072
+ <!DOCTYPE html>
6073
+ <html>
6074
+ <head><title>TolkBot CLI</title></head>
6075
+ <body style="font-family: system-ui; text-align: center; padding: 50px;">
6076
+ ${body}
6077
+ </body>
6078
+ </html>
6079
+ `);
6108
6080
  }
6109
- function decodeTokenSubject(token) {
6110
- const payload = token.split(".")[1];
6111
- if (!payload) return null;
6112
- try {
6113
- const json2 = Buffer.from(payload, "base64url").toString("utf-8");
6114
- const claims = JSON.parse(json2);
6115
- if (typeof claims === "object" && claims !== null && "sub" in claims && typeof claims.sub === "string") {
6116
- return claims.sub;
6117
- }
6118
- } catch {
6119
- }
6120
- return null;
6121
- }
6122
- async function resolveDistinctId() {
6123
- const resolved = await resolveToken();
6124
- const subject = resolved ? decodeTokenSubject(resolved.token) : null;
6125
- if (subject) return subject;
6126
- const file2 = getAnonymousIdPath();
6127
- const existing = await readFileOrNull(file2);
6128
- if (existing) return existing;
6129
- const generated = randomUUID();
6130
- try {
6131
- await writePrivateFile(file2, `${generated}
6132
- `);
6133
- } catch {
6134
- }
6135
- return generated;
6136
- }
6137
- var organizationId = null;
6138
- function setTelemetryOrganization(id) {
6139
- organizationId = id;
6140
- }
6141
- var client = null;
6142
- function getClient(key) {
6143
- client ??= new PostHog(key, {
6144
- host: POSTHOG_HOST,
6145
- // A CLI process is short-lived: send each event as it happens
6146
- // rather than batching for a flush that may never come.
6147
- flushAt: 1,
6148
- flushInterval: 0,
6149
- requestTimeout: FLUSH_TIMEOUT_MS,
6150
- // No IP-derived location on CLI events. The privacy policy lists
6151
- // exactly what the CLI reports, and location is not on it.
6152
- disableGeoip: true
6153
- });
6154
- return client;
6155
- }
6156
- var inFlight = /* @__PURE__ */ new Set();
6157
- async function send(event, properties) {
6158
- const state = await telemetryState();
6159
- if (!state.enabled) return;
6160
- const key = projectKey();
6161
- if (!key) return;
6162
- const distinctId = await resolveDistinctId();
6163
- await getClient(key).captureImmediate({
6164
- distinctId,
6165
- event,
6166
- properties: { ...properties, environment: environment() },
6167
- groups: organizationId ? { [ORGANIZATION_GROUP]: organizationId } : void 0
6168
- });
6169
- }
6170
- function track(event, properties) {
6171
- const attempt = send(event, properties).catch(() => void 0);
6172
- inFlight.add(attempt);
6173
- void attempt.finally(() => inFlight.delete(attempt));
6174
- }
6175
- async function flushTelemetry(timeoutMs = FLUSH_TIMEOUT_MS) {
6176
- if (inFlight.size === 0 && !client) return;
6177
- const deadline = new Promise((resolve) => {
6178
- const timer = setTimeout(resolve, timeoutMs);
6179
- timer.unref();
6180
- });
6181
- await Promise.race([Promise.allSettled([...inFlight]), deadline]);
6182
- if (client) {
6183
- const closing = client.shutdown(timeoutMs).catch(() => void 0);
6184
- client = null;
6185
- await Promise.race([closing, deadline]);
6186
- }
6187
- }
6188
- function runtimeProperties(cliVersion) {
6189
- return {
6190
- cli_version: cliVersion,
6191
- node_version: process.version,
6192
- platform: process.platform,
6193
- ci: inCi()
6194
- };
6195
- }
6196
- var activeCommand = null;
6197
- function beginCommand(path12, cliVersion) {
6198
- activeCommand = {
6199
- path: path12,
6200
- startedAt: Date.now(),
6201
- cliVersion,
6202
- reported: false
6203
- };
6204
- }
6205
- function completeCommand(exitCode) {
6206
- if (!activeCommand || activeCommand.reported) return;
6207
- activeCommand.reported = true;
6208
- track("cli_command_completed", {
6209
- command: activeCommand.path,
6210
- success: exitCode === 0,
6211
- exit_code: exitCode,
6212
- duration_ms: Date.now() - activeCommand.startedAt,
6213
- json: wantsJson(),
6214
- ...runtimeProperties(activeCommand.cliVersion)
6215
- });
6216
- }
6217
- async function exitAfterFlush(code) {
6218
- completeCommand(code);
6219
- await flushTelemetry();
6220
- process.exit(code);
6221
- }
6222
-
6223
- // src/utils/env.ts
6224
- import path3 from "path";
6225
- import { fileURLToPath } from "url";
6226
- import { createEnv } from "@t3-oss/env-core";
6227
- import * as dotenv from "dotenv";
6228
- import { z } from "zod/v4";
6229
- var RELEASE_CONVEX_URL = "https://uncommon-monitor-462.convex.cloud"?.trim();
6230
- var RELEASE_WEB_URL = "https://www.tolkbot.ai"?.trim();
6231
- if (!RELEASE_CONVEX_URL) {
6232
- try {
6233
- const dir = path3.dirname(fileURLToPath(import.meta.url));
6234
- dotenv.config({
6235
- path: path3.resolve(dir, "../../../.env"),
6236
- quiet: true
6237
- });
6238
- } catch {
6239
- }
6240
- }
6241
- var env = createEnv({
6242
- server: {
6243
- // Optional here, required at the point of use: commands that never
6244
- // reach the backend (help, logout) must work without deployment config.
6245
- CONVEX_URL: z.url().optional(),
6246
- TOLKBOT_WEB_URL: z.url().default("http://localhost:3000")
6247
- },
6248
- runtimeEnv: {
6249
- CONVEX_URL: RELEASE_CONVEX_URL || process.env.CONVEX_URL,
6250
- TOLKBOT_WEB_URL: RELEASE_WEB_URL || process.env.TOLKBOT_WEB_URL
6251
- },
6252
- skipValidation: !!process.env.CI || process.env.npm_lifecycle_event === "lint",
6253
- onValidationError: (issues) => {
6254
- const details = issues.map((issue2) => {
6255
- const name = issue2.path?.join(".") ?? "(unknown)";
6256
- return `${name}: ${issue2.message}`;
6257
- }).join("\n ");
6258
- throw new CliError(`Invalid configuration:
6259
- ${details}`);
6260
- }
6261
- });
6262
- function requireConvexUrl() {
6263
- if (!env.CONVEX_URL) {
6264
- throw new CliError(
6265
- "CONVEX_URL is not set, so the CLI cannot reach the TolkBot backend.",
6266
- "Set CONVEX_URL to your TolkBot deployment URL."
6267
- );
6268
- }
6269
- return env.CONVEX_URL;
6270
- }
6271
-
6272
- // src/commands/auth/login.tsx
6273
- import { jsx, jsxs } from "react/jsx-runtime";
6274
- var CALLBACK_PORTS = [3003, 3013, 3023, 3033, 3043];
6275
- var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
6276
- function generateVerificationCode() {
6277
- const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
6278
- let code = "";
6279
- for (let i = 0; i < 8; i++) {
6280
- code += chars.charAt(crypto.randomInt(chars.length));
6281
- }
6282
- return code;
6283
- }
6284
- function respond(res, status, body) {
6285
- res.writeHead(status, { "Content-Type": "text/html" });
6286
- res.end(`
6287
- <!DOCTYPE html>
6288
- <html>
6289
- <head><title>TolkBot CLI</title></head>
6290
- <body style="font-family: system-ui; text-align: center; padding: 50px;">
6291
- ${body}
6292
- </body>
6293
- </html>
6294
- `);
6295
- }
6296
- async function startCallbackServer(expectedCode) {
6297
- let settle2;
6298
- const settled = new Promise((resolve) => {
6299
- settle2 = resolve;
6300
- });
6301
- const server = http.createServer((req, res) => {
6302
- if (!req.url?.startsWith("/callback")) return;
6303
- const url2 = new URL(req.url, "http://localhost");
6304
- const token2 = url2.searchParams.get("token");
6305
- const returnedCode = url2.searchParams.get("code");
6306
- if (!token2) {
6307
- respond(
6308
- res,
6309
- 400,
6310
- "<h1>Authentication failed</h1><p>No token received. Please try again.</p>"
6311
- );
6312
- settle2({ error: new Error("No token received from browser") });
6313
- return;
6081
+ async function startCallbackServer(expectedCode) {
6082
+ let settle2;
6083
+ const settled = new Promise((resolve) => {
6084
+ settle2 = resolve;
6085
+ });
6086
+ const server = http.createServer((req, res) => {
6087
+ if (!req.url?.startsWith("/callback")) return;
6088
+ const url2 = new URL(req.url, "http://localhost");
6089
+ const token2 = url2.searchParams.get("token");
6090
+ const returnedCode = url2.searchParams.get("code");
6091
+ if (!token2) {
6092
+ respond(
6093
+ res,
6094
+ 400,
6095
+ "<h1>Authentication failed</h1><p>No token received. Please try again.</p>"
6096
+ );
6097
+ settle2({ error: new Error("No token received from browser") });
6098
+ return;
6314
6099
  }
6315
6100
  if (returnedCode !== expectedCode) {
6316
6101
  respond(
@@ -6404,22 +6189,16 @@ function LoginUI({ state }) {
6404
6189
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Confirm this code matches the one shown in your browser" })
6405
6190
  ] });
6406
6191
  }
6407
- if (state.type === "success") {
6408
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
6409
- /* @__PURE__ */ jsxs(Box, { children: [
6410
- /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713" }),
6411
- /* @__PURE__ */ jsx(Text, { children: " Successfully authenticated!" })
6412
- ] }),
6413
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
6414
- "Token saved to ",
6415
- getTokenPath()
6416
- ] }) }),
6417
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: validityNote(state.expiresAt) })
6418
- ] });
6419
- }
6420
6192
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
6421
- /* @__PURE__ */ jsx(Text, { color: "red", children: "\u2717 Authentication failed" }),
6422
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: state.message })
6193
+ /* @__PURE__ */ jsxs(Box, { children: [
6194
+ /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713" }),
6195
+ /* @__PURE__ */ jsx(Text, { children: " Successfully authenticated!" })
6196
+ ] }),
6197
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
6198
+ "Token saved to ",
6199
+ getTokenPath()
6200
+ ] }) }),
6201
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: validityNote(state.expiresAt) })
6423
6202
  ] });
6424
6203
  }
6425
6204
  async function runPlainLogin(opts, verificationCode) {
@@ -6440,12 +6219,17 @@ async function runPlainLogin(opts, verificationCode) {
6440
6219
  console.log(validityNote(decodeTokenExpiry(token)));
6441
6220
  } catch (error62) {
6442
6221
  server?.close();
6443
- console.error(
6444
- `Authentication failed: ${error62 instanceof Error ? error62.message : String(error62)}`
6445
- );
6446
- return exitAfterFlush(1);
6222
+ throw loginFailed(error62);
6447
6223
  }
6448
6224
  }
6225
+ function loginFailed(error62) {
6226
+ if (error62 instanceof CliError) return error62;
6227
+ return new CliError(
6228
+ `Authentication failed: ${error62 instanceof Error ? error62.message : String(error62)}`,
6229
+ "Run 'tolkbot auth login' again.",
6230
+ "login_failed"
6231
+ );
6232
+ }
6449
6233
  function loginUrl(port, verificationCode) {
6450
6234
  return `${env.TOLKBOT_WEB_URL}/cli/login/confirm?port=${port}&code=${verificationCode}`;
6451
6235
  }
@@ -6493,19 +6277,7 @@ async function runAuthLogin(opts = {}) {
6493
6277
  } catch (error62) {
6494
6278
  server?.close();
6495
6279
  unmount();
6496
- const errorUi = render(
6497
- /* @__PURE__ */ jsx(
6498
- LoginUI,
6499
- {
6500
- state: {
6501
- type: "error",
6502
- message: error62 instanceof Error ? error62.message : String(error62)
6503
- }
6504
- }
6505
- )
6506
- );
6507
- await errorUi.waitUntilExit();
6508
- return exitAfterFlush(1);
6280
+ throw loginFailed(error62);
6509
6281
  }
6510
6282
  }
6511
6283
 
@@ -6541,10 +6313,10 @@ function readProperty(candidate, key) {
6541
6313
  // src/lib/convex-client.ts
6542
6314
  import { ConvexHttpClient } from "convex/browser";
6543
6315
  var BACKEND_METHODS = /* @__PURE__ */ new Set(["query", "mutation", "action"]);
6544
- var client2 = null;
6316
+ var client = null;
6545
6317
  function getConvexClient() {
6546
- client2 ??= new ConvexHttpClient(requireConvexUrl());
6547
- return client2;
6318
+ client ??= new ConvexHttpClient(requireConvexUrl());
6319
+ return client;
6548
6320
  }
6549
6321
  function translateAuthError(error62, resolved) {
6550
6322
  const signedOut = findCliSignedOut(error62);
@@ -6552,13 +6324,15 @@ function translateAuthError(error62, resolved) {
6552
6324
  const at = new Date(signedOut.signedOutAt).toLocaleString();
6553
6325
  return new AuthError(
6554
6326
  `These credentials were signed out on ${at} (from ${describeTokenSource(resolved.source)}).`,
6555
- reauthHint(resolved.source)
6327
+ reauthHint(resolved.source),
6328
+ "signed_out"
6556
6329
  );
6557
6330
  }
6558
6331
  if (!isUnauthenticatedError(error62)) return error62;
6559
6332
  return new AuthError(
6560
6333
  `Your token was rejected (from ${describeTokenSource(resolved.source)}). It may be revoked, malformed, or issued for a different deployment.`,
6561
- reauthHint(resolved.source)
6334
+ reauthHint(resolved.source),
6335
+ "token_rejected"
6562
6336
  );
6563
6337
  }
6564
6338
  function withAuthErrors(target, resolved) {
@@ -6590,7 +6364,6 @@ function warnOnceIfExpiringSoon(resolved) {
6590
6364
  console.error(warning);
6591
6365
  }
6592
6366
  async function getAuthenticatedConvexClient() {
6593
- const convexClient = getConvexClient();
6594
6367
  const resolved = await resolveToken();
6595
6368
  if (!resolved) {
6596
6369
  throw new AuthError(
@@ -6602,9 +6375,11 @@ async function getAuthenticatedConvexClient() {
6602
6375
  const expiredAt = resolved.expiresAt ? new Date(resolved.expiresAt * 1e3).toLocaleString() : "an unknown time";
6603
6376
  throw new AuthError(
6604
6377
  `Your token expired at ${expiredAt} (from ${describeTokenSource(resolved.source)}).`,
6605
- reauthHint(resolved.source)
6378
+ reauthHint(resolved.source),
6379
+ "token_expired"
6606
6380
  );
6607
6381
  }
6382
+ const convexClient = getConvexClient();
6608
6383
  warnOnceIfExpiringSoon(resolved);
6609
6384
  convexClient.setAuth(resolved.token);
6610
6385
  return withAuthErrors(convexClient, resolved);
@@ -6637,11 +6412,6 @@ function jsonSuccess(data) {
6637
6412
  console.log(JSON.stringify(data, null, 2));
6638
6413
  });
6639
6414
  }
6640
- function jsonError(message) {
6641
- throw requestExit(1, () => {
6642
- console.error(JSON.stringify({ error: message }, null, 2));
6643
- });
6644
- }
6645
6415
  function jsonResult(data, exitCode) {
6646
6416
  console.log(JSON.stringify(data, null, 2));
6647
6417
  process.exitCode = exitCode;
@@ -6657,12 +6427,6 @@ function stillValidNote(expiresAt) {
6657
6427
  return `The token stays valid anywhere else it was copied ${until}. Run 'tolkbot auth logout --all' to sign out every CLI session.`;
6658
6428
  }
6659
6429
  function LogoutUI({ state }) {
6660
- if (state.type === "error") {
6661
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
6662
- /* @__PURE__ */ jsx2(Text2, { color: "red", children: "\u2717 Logout failed" }),
6663
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: state.message })
6664
- ] });
6665
- }
6666
6430
  const signedOutAll = state.signedOutAt !== null;
6667
6431
  let headline = state.hadToken ? " Successfully logged out!" : " Already logged out.";
6668
6432
  if (signedOutAll) headline = " Signed out every CLI session.";
@@ -6697,58 +6461,77 @@ async function signOutAllCliSessions() {
6697
6461
  return signedOutAt;
6698
6462
  }
6699
6463
  async function runAuthLogout(opts = {}) {
6700
- try {
6701
- const signedOutAt = opts.all ? await signOutAllCliSessions() : null;
6702
- const stored = await loadToken();
6703
- const hadToken = stored !== null;
6704
- const removedTokenExpiresAt = stored ? decodeTokenExpiry(stored) : null;
6705
- await deleteToken();
6706
- const envTokenStillSet = hasEnvToken();
6707
- if (opts.json) {
6708
- const warnings = [
6709
- ...signedOutAt === null && hadToken ? [stillValidNote(removedTokenExpiresAt)] : [],
6710
- ...envTokenStillSet ? [
6711
- signedOutAt === null ? `$${TOKEN_ENV_VAR} is still set and takes precedence over ${getTokenPath()}.` : `$${TOKEN_ENV_VAR} is still set; it was signed out too, so remove it.`
6712
- ] : []
6713
- ];
6714
- jsonSuccess({
6715
- loggedOut: true,
6716
- hadStoredToken: hadToken,
6717
- envTokenStillSet,
6718
- signedOutAllCliSessions: signedOutAt !== null,
6719
- ...signedOutAt !== null ? { signedOutAt: new Date(signedOutAt).toISOString() } : {},
6720
- ...warnings.length > 0 ? { warning: warnings.join(" ") } : {}
6721
- });
6722
- }
6723
- const ui = render2(
6724
- /* @__PURE__ */ jsx2(
6725
- LogoutUI,
6726
- {
6727
- state: {
6728
- type: "success",
6729
- hadToken,
6730
- envTokenStillSet,
6731
- removedTokenExpiresAt,
6732
- signedOutAt
6733
- }
6734
- }
6735
- )
6736
- );
6737
- await ui.waitUntilExit();
6738
- } catch (error62) {
6739
- const message = error62 instanceof Error ? error62.message : String(error62);
6740
- if (opts.json) {
6741
- jsonError(message);
6742
- }
6743
- const ui = render2(/* @__PURE__ */ jsx2(LogoutUI, { state: { type: "error", message } }));
6744
- await ui.waitUntilExit();
6745
- return exitAfterFlush(1);
6464
+ const signedOutAt = opts.all ? await signOutAllCliSessions() : null;
6465
+ const stored = await loadToken();
6466
+ const hadToken = stored !== null;
6467
+ const removedTokenExpiresAt = stored ? decodeTokenExpiry(stored) : null;
6468
+ await deleteToken();
6469
+ const envTokenStillSet = hasEnvToken();
6470
+ if (opts.json) {
6471
+ const warnings = [
6472
+ ...signedOutAt === null && hadToken ? [stillValidNote(removedTokenExpiresAt)] : [],
6473
+ ...envTokenStillSet ? [
6474
+ signedOutAt === null ? `$${TOKEN_ENV_VAR} is still set and takes precedence over ${getTokenPath()}.` : `$${TOKEN_ENV_VAR} is still set; it was signed out too, so remove it.`
6475
+ ] : []
6476
+ ];
6477
+ jsonSuccess({
6478
+ loggedOut: true,
6479
+ hadStoredToken: hadToken,
6480
+ envTokenStillSet,
6481
+ signedOutAllCliSessions: signedOutAt !== null,
6482
+ ...signedOutAt !== null ? { signedOutAt: new Date(signedOutAt).toISOString() } : {},
6483
+ ...warnings.length > 0 ? { warning: warnings.join(" ") } : {}
6484
+ });
6746
6485
  }
6486
+ render2(
6487
+ /* @__PURE__ */ jsx2(
6488
+ LogoutUI,
6489
+ {
6490
+ state: {
6491
+ type: "success",
6492
+ hadToken,
6493
+ envTokenStillSet,
6494
+ removedTokenExpiresAt,
6495
+ signedOutAt
6496
+ }
6497
+ }
6498
+ )
6499
+ ).unmount();
6747
6500
  }
6748
6501
 
6749
6502
  // src/commands/auth/whoami.tsx
6750
6503
  import { Spinner as Spinner2 } from "@inkjs/ui";
6751
- import { Box as Box3, render as render3, Text as Text3 } from "ink";
6504
+ import { Box as Box3, render as render4, Text as Text3 } from "ink";
6505
+
6506
+ // src/utils/terminal.ts
6507
+ import { render as render3 } from "ink";
6508
+ function isInteractive() {
6509
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
6510
+ }
6511
+ var jsonOutput = null;
6512
+ function setJsonOutput(value) {
6513
+ jsonOutput = value;
6514
+ }
6515
+ function wantsJson(argv = process.argv) {
6516
+ if (jsonOutput !== null) return jsonOutput;
6517
+ if (argv.includes("--no-json")) return false;
6518
+ if (argv.includes("--json")) return true;
6519
+ return !isInteractive();
6520
+ }
6521
+ function resolveJson(flag) {
6522
+ return flag ?? !isInteractive();
6523
+ }
6524
+ var NOTHING = {
6525
+ unmount: () => void 0,
6526
+ rerender: (_node) => void 0,
6527
+ waitUntilExit: () => Promise.resolve()
6528
+ };
6529
+ function renderTransient(node2) {
6530
+ if (!isInteractive()) return NOTHING;
6531
+ return render3(node2);
6532
+ }
6533
+
6534
+ // src/commands/auth/whoami.tsx
6752
6535
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
6753
6536
  function formatExpiry(expiresAt) {
6754
6537
  if (expiresAt === null) return "unknown";
@@ -6766,118 +6549,73 @@ function WhoamiUI({ state }) {
6766
6549
  /* @__PURE__ */ jsx3(Text3, { children: " Checking authentication\u2026" })
6767
6550
  ] });
6768
6551
  }
6769
- if (state.type === "success") {
6770
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
6771
- /* @__PURE__ */ jsxs3(Box3, { children: [
6772
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u250C " }),
6773
- /* @__PURE__ */ jsx3(Text3, { children: " Credentials " }),
6774
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: describeTokenSource(state.resolved.source) })
6775
- ] }),
6776
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2502" }),
6777
- /* @__PURE__ */ jsxs3(Box3, { children: [
6778
- /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u25CF " }),
6779
- /* @__PURE__ */ jsx3(Text3, { children: " Logged in as " }),
6780
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: state.email })
6781
- ] }),
6782
- /* @__PURE__ */ jsxs3(Box3, { children: [
6783
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2502 " }),
6784
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
6785
- " ",
6786
- describeExpiry(state.resolved)
6787
- ] })
6788
- ] }),
6789
- /* @__PURE__ */ jsxs3(Box3, { children: [
6790
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2514 " }),
6791
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2713" })
6792
- ] })
6793
- ] });
6794
- }
6795
- if (state.type === "not-logged-in") {
6796
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
6797
- /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u26A0 Not logged in" }),
6552
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
6553
+ /* @__PURE__ */ jsxs3(Box3, { children: [
6554
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u250C " }),
6555
+ /* @__PURE__ */ jsx3(Text3, { children: " Credentials " }),
6556
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: describeTokenSource(state.resolved.source) })
6557
+ ] }),
6558
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2502" }),
6559
+ /* @__PURE__ */ jsxs3(Box3, { children: [
6560
+ /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u25CF " }),
6561
+ /* @__PURE__ */ jsx3(Text3, { children: " Logged in as " }),
6562
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: state.email })
6563
+ ] }),
6564
+ /* @__PURE__ */ jsxs3(Box3, { children: [
6565
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2502 " }),
6798
6566
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
6799
- "Run ",
6800
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "tolkbot auth login" }),
6801
- ", or set $",
6802
- TOKEN_ENV_VAR,
6803
- " for CI."
6567
+ " ",
6568
+ describeExpiry(state.resolved)
6804
6569
  ] })
6805
- ] });
6806
- }
6807
- if (state.type === "expired") {
6808
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
6809
- /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u26A0 Token expired" }),
6810
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
6811
- "From ",
6812
- describeTokenSource(state.resolved.source),
6813
- ", expired",
6814
- " ",
6815
- formatExpiry(state.resolved.expiresAt)
6816
- ] }),
6817
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: reauthHint(state.resolved.source) })
6818
- ] });
6819
- }
6820
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
6821
- /* @__PURE__ */ jsx3(Text3, { color: "red", children: "\u2717 Failed to read authentication" }),
6822
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: state.message })
6570
+ ] }),
6571
+ /* @__PURE__ */ jsxs3(Box3, { children: [
6572
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2514 " }),
6573
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2713" })
6574
+ ] })
6823
6575
  ] });
6824
6576
  }
6825
- async function renderAndExit(state, exitCode) {
6826
- const ui = render3(/* @__PURE__ */ jsx3(WhoamiUI, { state }));
6827
- await ui.waitUntilExit();
6828
- return exitAfterFlush(exitCode);
6829
- }
6830
6577
  async function runAuthWhoami(opts = {}) {
6831
6578
  const resolved = await resolveToken();
6832
6579
  if (!resolved) {
6833
- if (opts.json) {
6834
- jsonError("Not authenticated.");
6835
- }
6836
- return renderAndExit({ type: "not-logged-in" }, 1);
6580
+ throw new AuthError(
6581
+ "Not logged in.",
6582
+ `Run 'tolkbot auth login', or set $${TOKEN_ENV_VAR} for CI.`
6583
+ );
6837
6584
  }
6838
6585
  if (isTokenExpired(resolved)) {
6839
- if (opts.json) {
6840
- jsonError(
6841
- `Token from ${describeTokenSource(resolved.source)} expired at ${formatExpiry(resolved.expiresAt)}.`
6842
- );
6843
- }
6844
- return renderAndExit({ type: "expired", resolved }, 1);
6586
+ throw new AuthError(
6587
+ `Your token (from ${describeTokenSource(resolved.source)}) expired at ${formatExpiry(resolved.expiresAt)}.`,
6588
+ reauthHint(resolved.source),
6589
+ "token_expired"
6590
+ );
6845
6591
  }
6846
- const spinner = opts.json ? null : render3(/* @__PURE__ */ jsx3(WhoamiUI, { state: { type: "loading" } }));
6847
- const unmount = () => spinner?.unmount();
6592
+ const spinner = opts.json ? null : renderTransient(/* @__PURE__ */ jsx3(WhoamiUI, { state: { type: "loading" } }));
6593
+ let email3;
6848
6594
  try {
6849
6595
  const convexClient = await getAuthenticatedConvexClient();
6850
6596
  const user = await convexClient.query(api.users.current, {});
6851
6597
  if (!user) {
6852
- unmount();
6853
- if (opts.json) {
6854
- jsonError("Token is valid but no matching user was found.");
6855
- }
6856
- return renderAndExit({ type: "not-logged-in" }, 1);
6857
- }
6858
- unmount();
6859
- if (opts.json) {
6860
- jsonSuccess({
6861
- authenticated: true,
6862
- email: user.email,
6863
- tokenSource: resolved.source,
6864
- tokenPath: getTokenPath(),
6865
- expiresAt: resolved.expiresAt ? new Date(resolved.expiresAt * 1e3).toISOString() : null,
6866
- expiresInSeconds: expiresInSeconds(resolved)
6867
- });
6868
- }
6869
- return renderAndExit(
6870
- { type: "success", email: user.email, resolved },
6871
- 0
6872
- );
6873
- } catch (error62) {
6874
- unmount();
6875
- const message = error62 instanceof Error ? error62.message : String(error62);
6876
- if (opts.json) {
6877
- jsonError(message);
6598
+ throw new AuthError(
6599
+ "Your token is valid, but TolkBot has no user for it.",
6600
+ reauthHint(resolved.source),
6601
+ "token_rejected"
6602
+ );
6878
6603
  }
6879
- return renderAndExit({ type: "error", message }, 1);
6604
+ email3 = user.email;
6605
+ } finally {
6606
+ spinner?.unmount();
6607
+ }
6608
+ if (opts.json) {
6609
+ jsonSuccess({
6610
+ authenticated: true,
6611
+ email: email3,
6612
+ tokenSource: resolved.source,
6613
+ tokenPath: getTokenPath(),
6614
+ expiresAt: resolved.expiresAt ? new Date(resolved.expiresAt * 1e3).toISOString() : null,
6615
+ expiresInSeconds: expiresInSeconds(resolved)
6616
+ });
6880
6617
  }
6618
+ render4(/* @__PURE__ */ jsx3(WhoamiUI, { state: { type: "success", email: email3, resolved } })).unmount();
6881
6619
  }
6882
6620
 
6883
6621
  // src/commands/auth/index.ts
@@ -6918,15 +6656,15 @@ import { z as z4 } from "zod/v4";
6918
6656
 
6919
6657
  // src/commands/glossary/create.tsx
6920
6658
  import { Spinner as Spinner3 } from "@inkjs/ui";
6921
- import { Box as Box4, render as render4, Text as Text4 } from "ink";
6659
+ import { Box as Box4, render as render5, Text as Text4 } from "ink";
6922
6660
 
6923
6661
  // ../config/dist/index.js
6924
- import * as fs3 from "fs";
6925
- import * as path4 from "path";
6662
+ import * as fs2 from "fs";
6663
+ import * as path3 from "path";
6926
6664
  import * as fs22 from "fs";
6927
6665
  import * as path22 from "path";
6928
6666
  import { execSync } from "child_process";
6929
- import * as fs32 from "fs/promises";
6667
+ import * as fs3 from "fs/promises";
6930
6668
  import * as path32 from "path";
6931
6669
  var __defProp2 = Object.defineProperty;
6932
6670
  var __export = (target, all) => {
@@ -6934,12 +6672,12 @@ var __export = (target, all) => {
6934
6672
  __defProp2(target, name, { get: all[name], enumerable: true });
6935
6673
  };
6936
6674
  function readJSON(p) {
6937
- return JSON.parse(fs3.readFileSync(p, "utf-8"));
6675
+ return JSON.parse(fs2.readFileSync(p, "utf-8"));
6938
6676
  }
6939
6677
  function readPackageDeps(cwd) {
6940
6678
  try {
6941
- const pkgPath = path4.join(cwd, "package.json");
6942
- if (!fs3.existsSync(pkgPath)) return {};
6679
+ const pkgPath = path3.join(cwd, "package.json");
6680
+ if (!fs2.existsSync(pkgPath)) return {};
6943
6681
  const pkg = readJSON(pkgPath);
6944
6682
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
6945
6683
  } catch {
@@ -6950,8 +6688,8 @@ function hasPackage(deps, name) {
6950
6688
  return name in deps;
6951
6689
  }
6952
6690
  function detectPackageManager(cwd) {
6953
- if (fs3.existsSync(path4.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
6954
- if (fs3.existsSync(path4.join(cwd, "yarn.lock"))) return "yarn";
6691
+ if (fs2.existsSync(path3.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
6692
+ if (fs2.existsSync(path3.join(cwd, "yarn.lock"))) return "yarn";
6955
6693
  return "npm";
6956
6694
  }
6957
6695
  function tryReadNextI18n(cwd) {
@@ -6961,12 +6699,12 @@ function tryReadNextI18n(cwd) {
6961
6699
  "next.config.cjs",
6962
6700
  "next.config.ts",
6963
6701
  "next.config.mts"
6964
- ].map((f) => path4.join(cwd, f));
6702
+ ].map((f) => path3.join(cwd, f));
6965
6703
  const res = {};
6966
6704
  for (const f of files) {
6967
- if (!fs3.existsSync(f)) continue;
6705
+ if (!fs2.existsSync(f)) continue;
6968
6706
  try {
6969
- const txt = fs3.readFileSync(f, "utf-8");
6707
+ const txt = fs2.readFileSync(f, "utf-8");
6970
6708
  const locMatch = txt.match(/locales\s*:\s*\[([\s\S]*?)\]/m);
6971
6709
  if (locMatch) {
6972
6710
  const arr = Array.from(
@@ -6986,14 +6724,14 @@ function tryReadNextI18n(cwd) {
6986
6724
  }
6987
6725
  function listSubdirs(p) {
6988
6726
  try {
6989
- return fs3.readdirSync(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6727
+ return fs2.readdirSync(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6990
6728
  } catch {
6991
6729
  return [];
6992
6730
  }
6993
6731
  }
6994
6732
  function listJSONFiles(p) {
6995
6733
  try {
6996
- return fs3.readdirSync(p, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name);
6734
+ return fs2.readdirSync(p, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name);
6997
6735
  } catch {
6998
6736
  return [];
6999
6737
  }
@@ -7025,13 +6763,13 @@ function detectProject(cwd) {
7025
6763
  const hasVue = hasPackage(deps, "vue");
7026
6764
  const hasExpo = hasPackage(deps, "expo");
7027
6765
  if (hasNext) {
7028
- const hasAppDir = fs3.existsSync(path4.join(cwd, "app"));
7029
- const hasPagesDir = fs3.existsSync(path4.join(cwd, "pages"));
6766
+ const hasAppDir = fs2.existsSync(path3.join(cwd, "app"));
6767
+ const hasPagesDir = fs2.existsSync(path3.join(cwd, "pages"));
7030
6768
  projectType = hasAppDir ? "next-app" : hasPagesDir ? "next-pages" : "next-app";
7031
6769
  } else if (hasExpo) {
7032
6770
  projectType = "expo";
7033
6771
  } else if (hasReact) {
7034
- const hasViteConfig = fs3.existsSync(path4.join(cwd, "vite.config.js")) || fs3.existsSync(path4.join(cwd, "vite.config.ts"));
6772
+ const hasViteConfig = fs2.existsSync(path3.join(cwd, "vite.config.js")) || fs2.existsSync(path3.join(cwd, "vite.config.ts"));
7035
6773
  projectType = hasViteConfig ? "react-vite" : "react-cra";
7036
6774
  } else if (hasVue) {
7037
6775
  projectType = "vue";
@@ -7063,36 +6801,36 @@ function detectFromRepo(cwd) {
7063
6801
  out.framework = "i18next";
7064
6802
  }
7065
6803
  const candidates = [
7066
- path4.join(cwd, "messages"),
6804
+ path3.join(cwd, "messages"),
7067
6805
  // next-intl pattern
7068
- path4.join(cwd, "locales"),
7069
- path4.join(cwd, "public", "locales"),
6806
+ path3.join(cwd, "locales"),
6807
+ path3.join(cwd, "public", "locales"),
7070
6808
  // next-i18next / i18next pattern
7071
- path4.join(cwd, "app", "locales"),
7072
- path4.join(cwd, "app", "messages"),
7073
- path4.join(cwd, "src", "locales"),
7074
- path4.join(cwd, "src", "messages")
7075
- ].filter((p) => fs3.existsSync(p) && fs3.statSync(p).isDirectory());
6809
+ path3.join(cwd, "app", "locales"),
6810
+ path3.join(cwd, "app", "messages"),
6811
+ path3.join(cwd, "src", "locales"),
6812
+ path3.join(cwd, "src", "messages")
6813
+ ].filter((p) => fs2.existsSync(p) && fs2.statSync(p).isDirectory());
7076
6814
  if (candidates.length) {
7077
6815
  const dir = candidates[0];
7078
6816
  out.outDir = dir;
7079
- if (dir?.includes(`${path4.sep}public${path4.sep}locales`)) {
6817
+ if (dir?.includes(`${path3.sep}public${path3.sep}locales`)) {
7080
6818
  out.include = ["public/locales/*/*.json"];
7081
- } else if (dir?.endsWith(`${path4.sep}messages`)) {
6819
+ } else if (dir?.endsWith(`${path3.sep}messages`)) {
7082
6820
  out.include = ["messages/*.json"];
7083
6821
  } else {
7084
6822
  out.include = ["**/messages/*.json", "**/locales/*.json"];
7085
6823
  }
7086
6824
  let locales;
7087
- if (dir?.includes(`${path4.sep}public${path4.sep}locales`)) {
6825
+ if (dir?.includes(`${path3.sep}public${path3.sep}locales`)) {
7088
6826
  const subdirs = listSubdirs(dir);
7089
6827
  locales = subdirs.filter((name) => {
7090
- const hasFiles = listJSONFiles(path4.join(dir, name)).length > 0;
6828
+ const hasFiles = listJSONFiles(path3.join(dir, name)).length > 0;
7091
6829
  return hasFiles;
7092
6830
  });
7093
6831
  } else {
7094
6832
  const files = listJSONFiles(dir ?? "");
7095
- locales = files.map((f) => path4.parse(f).name);
6833
+ locales = files.map((f) => path3.parse(f).name);
7096
6834
  }
7097
6835
  const nextI18n = tryReadNextI18n(cwd);
7098
6836
  const preferred = nextI18n.defaultLocale;
@@ -26858,7 +26596,7 @@ function getFrameworkSetup(framework) {
26858
26596
  function installDependencies(cwd, packageManager, deps) {
26859
26597
  if (deps.length === 0) return;
26860
26598
  const cmd = packageManager === "npm" ? `npm install ${deps.join(" ")}` : packageManager === "yarn" ? `yarn add ${deps.join(" ")}` : `pnpm add ${deps.join(" ")}`;
26861
- execSync(cmd, { stdio: "inherit", cwd });
26599
+ execSync(cmd, { stdio: ["ignore", 2, 2], cwd });
26862
26600
  }
26863
26601
  async function createLocaleFiles(cwd, framework, baseLocale, targetLocales) {
26864
26602
  const setup = FRAMEWORK_SETUPS[framework];
@@ -26866,22 +26604,22 @@ async function createLocaleFiles(cwd, framework, baseLocale, targetLocales) {
26866
26604
  for (const file2 of setup.files) {
26867
26605
  if (!file2.path.includes("{{lng}}")) {
26868
26606
  const filePath = path32.join(cwd, file2.path);
26869
- await fs32.mkdir(path32.dirname(filePath), { recursive: true });
26870
- await fs32.writeFile(filePath, file2.content);
26607
+ await fs3.mkdir(path32.dirname(filePath), { recursive: true });
26608
+ await fs3.writeFile(filePath, file2.content);
26871
26609
  createdFiles.push(file2.path);
26872
26610
  continue;
26873
26611
  }
26874
26612
  const basePath = file2.path.replace("{{lng}}", baseLocale);
26875
26613
  const baseFilePath = path32.join(cwd, basePath);
26876
- await fs32.mkdir(path32.dirname(baseFilePath), { recursive: true });
26877
- await fs32.writeFile(baseFilePath, file2.content);
26614
+ await fs3.mkdir(path32.dirname(baseFilePath), { recursive: true });
26615
+ await fs3.writeFile(baseFilePath, file2.content);
26878
26616
  createdFiles.push(basePath);
26879
26617
  for (const locale of targetLocales) {
26880
26618
  const targetPath = file2.path.replace("{{lng}}", locale);
26881
26619
  const targetFilePath = path32.join(cwd, targetPath);
26882
- await fs32.mkdir(path32.dirname(targetFilePath), { recursive: true });
26620
+ await fs3.mkdir(path32.dirname(targetFilePath), { recursive: true });
26883
26621
  const emptyContent = file2.path.endsWith(".json") ? "{}" : file2.content;
26884
- await fs32.writeFile(targetFilePath, emptyContent);
26622
+ await fs3.writeFile(targetFilePath, emptyContent);
26885
26623
  createdFiles.push(targetPath);
26886
26624
  }
26887
26625
  }
@@ -26906,18 +26644,12 @@ async function setupFramework(cwd, framework, baseLocale, targetLocales, package
26906
26644
  async function getProjectContext(convexClient) {
26907
26645
  const baseDir = process.cwd();
26908
26646
  const config3 = readConfig(baseDir);
26909
- if (!config3) {
26910
- throw new Error(
26911
- "No tolkbot.json found. Run 'tolkbot init' to create one."
26912
- );
26913
- }
26647
+ if (!config3) throw noConfigError();
26914
26648
  const projectId = config3.config.projectId;
26915
26649
  const project = await convexClient.query(api.projects.getProjectForCli, {
26916
26650
  projectId
26917
26651
  });
26918
- if (!project) {
26919
- throw new Error("Project not found. Please run 'tolkbot init' again.");
26920
- }
26652
+ if (!project) throw projectNotFoundError();
26921
26653
  return {
26922
26654
  projectId,
26923
26655
  organizationId: project.organizationId,
@@ -26928,15 +26660,17 @@ function parseTranslations(raw) {
26928
26660
  return raw.map((entry) => {
26929
26661
  const colonIdx = entry.indexOf(":");
26930
26662
  if (colonIdx === -1) {
26931
- throw new Error(
26932
- `Invalid translation format: "${entry}". Expected "locale:text" (e.g. "no:Hei")`
26663
+ throw new UsageError(
26664
+ `Invalid translation format: "${entry}".`,
26665
+ 'Expected "locale:text", such as "no:Hei".'
26933
26666
  );
26934
26667
  }
26935
26668
  const locale = entry.slice(0, colonIdx);
26936
26669
  const text = entry.slice(colonIdx + 1);
26937
26670
  if (!locale || !text) {
26938
- throw new Error(
26939
- `Invalid translation format: "${entry}". Both locale and text are required.`
26671
+ throw new UsageError(
26672
+ `Invalid translation format: "${entry}".`,
26673
+ "Both locale and text are required."
26940
26674
  );
26941
26675
  }
26942
26676
  return { locale, text };
@@ -26952,56 +26686,27 @@ function CreateUI({ state }) {
26952
26686
  /* @__PURE__ */ jsx4(Text4, { children: " Creating glossary term\u2026" })
26953
26687
  ] });
26954
26688
  }
26955
- if (state.type === "success") {
26956
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
26957
- /* @__PURE__ */ jsxs4(Box4, { children: [
26958
- /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2713" }),
26959
- /* @__PURE__ */ jsxs4(Text4, { children: [
26960
- " ",
26961
- "Created glossary term: ",
26962
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: state.term })
26963
- ] })
26964
- ] }),
26965
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
26966
- "ID: ",
26967
- state.id
26968
- ] }) })
26969
- ] });
26970
- }
26971
26689
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
26972
- /* @__PURE__ */ jsx4(Text4, { color: "red", children: "\u2717 Failed to create glossary term" }),
26973
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: state.message })
26690
+ /* @__PURE__ */ jsxs4(Box4, { children: [
26691
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2713" }),
26692
+ /* @__PURE__ */ jsxs4(Text4, { children: [
26693
+ " ",
26694
+ "Created glossary term: ",
26695
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: state.term })
26696
+ ] })
26697
+ ] }),
26698
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
26699
+ "ID: ",
26700
+ state.id
26701
+ ] }) })
26974
26702
  ] });
26975
26703
  }
26976
26704
  async function runGlossaryCreate(opts) {
26977
- if (opts.json) {
26978
- try {
26979
- const translations = opts.translation ? parseTranslations(opts.translation) : void 0;
26980
- const convexClient = await getAuthenticatedConvexClient();
26981
- const { projectId, organizationId: organizationId2 } = await getProjectContext(convexClient);
26982
- const glossaryId = await convexClient.mutation(
26983
- api.glossaries.create,
26984
- {
26985
- organizationId: organizationId2,
26986
- projectId,
26987
- src: opts.term,
26988
- srcLocale: opts.locale,
26989
- aiContext: opts.context,
26990
- doNotTranslate: opts.doNotTranslate,
26991
- translations
26992
- }
26993
- );
26994
- jsonSuccess({ id: glossaryId, term: opts.term });
26995
- } catch (error62) {
26996
- jsonError(error62 instanceof Error ? error62.message : String(error62));
26997
- }
26998
- }
26999
- let { unmount } = render4(/* @__PURE__ */ jsx4(CreateUI, { state: { type: "creating" } }));
27000
- try {
26705
+ const create = async () => {
27001
26706
  const translations = opts.translation ? parseTranslations(opts.translation) : void 0;
27002
26707
  const convexClient = await getAuthenticatedConvexClient();
27003
26708
  const { projectId, organizationId: organizationId2 } = await getProjectContext(convexClient);
27004
- const glossaryId = await convexClient.mutation(api.glossaries.create, {
26709
+ return convexClient.mutation(api.glossaries.create, {
27005
26710
  organizationId: organizationId2,
27006
26711
  projectId,
27007
26712
  src: opts.term,
@@ -27010,38 +26715,32 @@ async function runGlossaryCreate(opts) {
27010
26715
  doNotTranslate: opts.doNotTranslate,
27011
26716
  translations
27012
26717
  });
26718
+ };
26719
+ if (opts.json) {
26720
+ jsonSuccess({ id: await create(), term: opts.term });
26721
+ }
26722
+ const { unmount } = renderTransient(
26723
+ /* @__PURE__ */ jsx4(CreateUI, { state: { type: "creating" } })
26724
+ );
26725
+ let glossaryId;
26726
+ try {
26727
+ glossaryId = await create();
26728
+ } finally {
27013
26729
  unmount();
27014
- const ui = render4(
27015
- /* @__PURE__ */ jsx4(
27016
- CreateUI,
27017
- {
27018
- state: { type: "success", id: glossaryId, term: opts.term }
27019
- }
27020
- )
27021
- );
27022
- unmount = ui.unmount;
27023
- await ui.waitUntilExit();
27024
- } catch (error62) {
27025
- unmount();
27026
- const ui = render4(
27027
- /* @__PURE__ */ jsx4(
27028
- CreateUI,
27029
- {
27030
- state: {
27031
- type: "error",
27032
- message: error62 instanceof Error ? error62.message : String(error62)
27033
- }
27034
- }
27035
- )
27036
- );
27037
- await ui.waitUntilExit();
27038
- return exitAfterFlush(1);
27039
26730
  }
26731
+ render5(
26732
+ /* @__PURE__ */ jsx4(
26733
+ CreateUI,
26734
+ {
26735
+ state: { type: "success", id: glossaryId, term: opts.term }
26736
+ }
26737
+ )
26738
+ ).unmount();
27040
26739
  }
27041
26740
 
27042
26741
  // src/commands/glossary/list.tsx
27043
26742
  import { Spinner as Spinner4 } from "@inkjs/ui";
27044
- import { Box as Box5, render as render5, Text as Text5 } from "ink";
26743
+ import { Box as Box5, render as render6, Text as Text5 } from "ink";
27045
26744
 
27046
26745
  // ../shared/src/types/settings.ts
27047
26746
  var SUPPORTED_LOCALES = [
@@ -27266,12 +26965,6 @@ function GlossaryListUI({ state }) {
27266
26965
  /* @__PURE__ */ jsx5(Text5, { children: " Loading glossary terms\u2026" })
27267
26966
  ] });
27268
26967
  }
27269
- if (state.type === "error") {
27270
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
27271
- /* @__PURE__ */ jsx5(Text5, { color: "red", children: "\u2717 Error" }),
27272
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: state.message })
27273
- ] });
27274
- }
27275
26968
  const { listing } = state;
27276
26969
  const glossaries = listing.entries;
27277
26970
  if (glossaries.length === 0) {
@@ -27372,43 +27065,23 @@ function jsonListing(listing) {
27372
27065
  }
27373
27066
  async function runGlossaryList(opts) {
27374
27067
  if (opts.json) {
27375
- try {
27376
- const listing = await loadListing(opts);
27377
- jsonSuccess(jsonListing(listing));
27378
- } catch (error62) {
27379
- jsonError(error62 instanceof Error ? error62.message : String(error62));
27380
- }
27068
+ jsonSuccess(jsonListing(await loadListing(opts)));
27381
27069
  }
27382
- let { unmount } = render5(/* @__PURE__ */ jsx5(GlossaryListUI, { state: { type: "loading" } }));
27070
+ const { unmount } = renderTransient(
27071
+ /* @__PURE__ */ jsx5(GlossaryListUI, { state: { type: "loading" } })
27072
+ );
27073
+ let listing;
27383
27074
  try {
27384
- const listing = await loadListing(opts);
27385
- unmount();
27386
- const ui = render5(
27387
- /* @__PURE__ */ jsx5(GlossaryListUI, { state: { type: "success", listing } })
27388
- );
27389
- unmount = ui.unmount;
27390
- await ui.waitUntilExit();
27391
- } catch (error62) {
27075
+ listing = await loadListing(opts);
27076
+ } finally {
27392
27077
  unmount();
27393
- const ui = render5(
27394
- /* @__PURE__ */ jsx5(
27395
- GlossaryListUI,
27396
- {
27397
- state: {
27398
- type: "error",
27399
- message: error62 instanceof Error ? error62.message : String(error62)
27400
- }
27401
- }
27402
- )
27403
- );
27404
- await ui.waitUntilExit();
27405
- return exitAfterFlush(1);
27406
27078
  }
27079
+ render6(/* @__PURE__ */ jsx5(GlossaryListUI, { state: { type: "success", listing } })).unmount();
27407
27080
  }
27408
27081
 
27409
27082
  // src/commands/glossary/remove.tsx
27410
27083
  import { Spinner as Spinner5 } from "@inkjs/ui";
27411
- import { Box as Box6, render as render6, Text as Text6 } from "ink";
27084
+ import { Box as Box6, render as render7, Text as Text6 } from "ink";
27412
27085
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
27413
27086
  function RemoveUI({ state }) {
27414
27087
  if (state.type === "removing") {
@@ -27417,66 +27090,38 @@ function RemoveUI({ state }) {
27417
27090
  /* @__PURE__ */ jsx6(Text6, { children: " Removing glossary term\u2026" })
27418
27091
  ] });
27419
27092
  }
27420
- if (state.type === "success") {
27421
- return /* @__PURE__ */ jsxs6(Box6, { children: [
27422
- /* @__PURE__ */ jsx6(Text6, { color: "green", children: "\u2713" }),
27423
- /* @__PURE__ */ jsx6(Text6, { children: " Glossary term removed" })
27424
- ] });
27425
- }
27426
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
27427
- /* @__PURE__ */ jsx6(Text6, { color: "red", children: "\u2717 Failed to remove glossary term" }),
27428
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: state.message })
27093
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
27094
+ /* @__PURE__ */ jsx6(Text6, { color: "green", children: "\u2713" }),
27095
+ /* @__PURE__ */ jsx6(Text6, { children: " Glossary term removed" })
27429
27096
  ] });
27430
27097
  }
27431
27098
  async function runGlossaryRemove(opts) {
27432
- if (opts.json) {
27433
- try {
27434
- const convexClient = await getAuthenticatedConvexClient();
27435
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
27436
- await convexClient.mutation(api.glossaries.remove, {
27437
- organizationId: organizationId2,
27438
- glossaryId: opts.id
27439
- });
27440
- jsonSuccess({ id: opts.id, ok: true });
27441
- } catch (error62) {
27442
- jsonError(error62 instanceof Error ? error62.message : String(error62));
27443
- }
27444
- }
27445
- let { unmount } = render6(/* @__PURE__ */ jsx6(RemoveUI, { state: { type: "removing" } }));
27446
- try {
27099
+ const run = async () => {
27447
27100
  const convexClient = await getAuthenticatedConvexClient();
27448
27101
  const { organizationId: organizationId2 } = await getProjectContext(convexClient);
27449
27102
  await convexClient.mutation(api.glossaries.remove, {
27450
27103
  organizationId: organizationId2,
27451
27104
  glossaryId: opts.id
27452
27105
  });
27106
+ };
27107
+ if (opts.json) {
27108
+ await run();
27109
+ jsonSuccess({ id: opts.id, ok: true });
27110
+ }
27111
+ const { unmount } = renderTransient(
27112
+ /* @__PURE__ */ jsx6(RemoveUI, { state: { type: "removing" } })
27113
+ );
27114
+ try {
27115
+ await run();
27116
+ } finally {
27453
27117
  unmount();
27454
- const ui = render6(
27455
- /* @__PURE__ */ jsx6(RemoveUI, { state: { type: "success", id: opts.id } })
27456
- );
27457
- unmount = ui.unmount;
27458
- await ui.waitUntilExit();
27459
- } catch (error62) {
27460
- unmount();
27461
- const ui = render6(
27462
- /* @__PURE__ */ jsx6(
27463
- RemoveUI,
27464
- {
27465
- state: {
27466
- type: "error",
27467
- message: error62 instanceof Error ? error62.message : String(error62)
27468
- }
27469
- }
27470
- )
27471
- );
27472
- await ui.waitUntilExit();
27473
- return exitAfterFlush(1);
27474
27118
  }
27119
+ render7(/* @__PURE__ */ jsx6(RemoveUI, { state: { type: "success", id: opts.id } })).unmount();
27475
27120
  }
27476
27121
 
27477
27122
  // src/commands/glossary/show.tsx
27478
27123
  import { Spinner as Spinner6 } from "@inkjs/ui";
27479
- import { Box as Box7, render as render7, Text as Text7 } from "ink";
27124
+ import { Box as Box7, render as render8, Text as Text7 } from "ink";
27480
27125
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
27481
27126
  function GlossaryShowUI({ state }) {
27482
27127
  if (state.type === "loading") {
@@ -27485,15 +27130,6 @@ function GlossaryShowUI({ state }) {
27485
27130
  /* @__PURE__ */ jsx7(Text7, { children: " Loading glossary term\u2026" })
27486
27131
  ] });
27487
27132
  }
27488
- if (state.type === "not-found") {
27489
- return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717 Glossary term not found" }) });
27490
- }
27491
- if (state.type === "error") {
27492
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
27493
- /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2717 Error" }),
27494
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: state.message })
27495
- ] });
27496
- }
27497
27133
  const { glossary } = state;
27498
27134
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
27499
27135
  /* @__PURE__ */ jsxs7(Box7, { gap: 1, children: [
@@ -27528,73 +27164,49 @@ function GlossaryShowUI({ state }) {
27528
27164
  ] }) })
27529
27165
  ] });
27530
27166
  }
27167
+ async function loadGlossary(id) {
27168
+ const convexClient = await getAuthenticatedConvexClient();
27169
+ const { organizationId: organizationId2 } = await getProjectContext(convexClient);
27170
+ const glossary = await convexClient.query(api.glossaries.getById, {
27171
+ organizationId: organizationId2,
27172
+ glossaryId: id
27173
+ });
27174
+ if (!glossary) {
27175
+ throw new CliError(
27176
+ `Glossary term ${id} was not found.`,
27177
+ "List the terms with 'tolkbot glossary list'.",
27178
+ "not_found"
27179
+ );
27180
+ }
27181
+ return glossary;
27182
+ }
27531
27183
  async function runGlossaryShow(opts) {
27532
27184
  if (opts.json) {
27533
- try {
27534
- const convexClient = await getAuthenticatedConvexClient();
27535
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
27536
- const glossary = await convexClient.query(api.glossaries.getById, {
27537
- organizationId: organizationId2,
27538
- glossaryId: opts.id
27539
- });
27540
- if (!glossary) {
27541
- jsonError("Glossary term not found");
27542
- }
27543
- jsonSuccess({
27544
- id: glossary._id,
27545
- term: glossary.src,
27546
- locale: glossary.srcLocale,
27547
- doNotTranslate: glossary.doNotTranslate ?? false,
27548
- translations: glossary.translations ?? [],
27549
- context: glossary.aiContext ?? null
27550
- });
27551
- } catch (error62) {
27552
- jsonError(error62 instanceof Error ? error62.message : String(error62));
27553
- }
27185
+ const glossary2 = await loadGlossary(opts.id);
27186
+ jsonSuccess({
27187
+ id: glossary2._id,
27188
+ term: glossary2.src,
27189
+ locale: glossary2.srcLocale,
27190
+ doNotTranslate: glossary2.doNotTranslate ?? false,
27191
+ translations: glossary2.translations ?? [],
27192
+ context: glossary2.aiContext ?? null
27193
+ });
27554
27194
  }
27555
- let { unmount } = render7(/* @__PURE__ */ jsx7(GlossaryShowUI, { state: { type: "loading" } }));
27195
+ const { unmount } = renderTransient(
27196
+ /* @__PURE__ */ jsx7(GlossaryShowUI, { state: { type: "loading" } })
27197
+ );
27198
+ let glossary;
27556
27199
  try {
27557
- const convexClient = await getAuthenticatedConvexClient();
27558
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
27559
- const glossary = await convexClient.query(api.glossaries.getById, {
27560
- organizationId: organizationId2,
27561
- glossaryId: opts.id
27562
- });
27563
- if (!glossary) {
27564
- unmount();
27565
- const ui2 = render7(/* @__PURE__ */ jsx7(GlossaryShowUI, { state: { type: "not-found" } }));
27566
- unmount = ui2.unmount;
27567
- await ui2.waitUntilExit();
27568
- return exitAfterFlush(1);
27569
- return;
27570
- }
27571
- unmount();
27572
- const ui = render7(
27573
- /* @__PURE__ */ jsx7(GlossaryShowUI, { state: { type: "success", glossary } })
27574
- );
27575
- unmount = ui.unmount;
27576
- await ui.waitUntilExit();
27577
- } catch (error62) {
27200
+ glossary = await loadGlossary(opts.id);
27201
+ } finally {
27578
27202
  unmount();
27579
- const ui = render7(
27580
- /* @__PURE__ */ jsx7(
27581
- GlossaryShowUI,
27582
- {
27583
- state: {
27584
- type: "error",
27585
- message: error62 instanceof Error ? error62.message : String(error62)
27586
- }
27587
- }
27588
- )
27589
- );
27590
- await ui.waitUntilExit();
27591
- return exitAfterFlush(1);
27592
27203
  }
27204
+ render8(/* @__PURE__ */ jsx7(GlossaryShowUI, { state: { type: "success", glossary } })).unmount();
27593
27205
  }
27594
27206
 
27595
27207
  // src/commands/glossary/update.tsx
27596
27208
  import { Spinner as Spinner7 } from "@inkjs/ui";
27597
- import { Box as Box8, render as render8, Text as Text8 } from "ink";
27209
+ import { Box as Box8, render as render9, Text as Text8 } from "ink";
27598
27210
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
27599
27211
  function UpdateUI({ state }) {
27600
27212
  if (state.type === "updating") {
@@ -27603,21 +27215,15 @@ function UpdateUI({ state }) {
27603
27215
  /* @__PURE__ */ jsx8(Text8, { children: " Updating glossary term\u2026" })
27604
27216
  ] });
27605
27217
  }
27606
- if (state.type === "success") {
27607
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
27608
- /* @__PURE__ */ jsxs8(Box8, { children: [
27609
- /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2713" }),
27610
- /* @__PURE__ */ jsx8(Text8, { children: " Glossary term updated" })
27611
- ] }),
27612
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
27613
- "ID: ",
27614
- state.id
27615
- ] }) })
27616
- ] });
27617
- }
27618
27218
  return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
27619
- /* @__PURE__ */ jsx8(Text8, { color: "red", children: "\u2717 Failed to update glossary term" }),
27620
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: state.message })
27219
+ /* @__PURE__ */ jsxs8(Box8, { children: [
27220
+ /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2713" }),
27221
+ /* @__PURE__ */ jsx8(Text8, { children: " Glossary term updated" })
27222
+ ] }),
27223
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
27224
+ "ID: ",
27225
+ state.id
27226
+ ] }) })
27621
27227
  ] });
27622
27228
  }
27623
27229
  async function runGlossaryUpdate(opts) {
@@ -27631,124 +27237,333 @@ async function runGlossaryUpdate(opts) {
27631
27237
  patch.doNotTranslate = opts.doNotTranslate;
27632
27238
  if (translations !== void 0) patch.translations = translations;
27633
27239
  if (Object.keys(patch).length === 0) {
27634
- throw new Error(
27635
- "No fields to update. Provide at least one of: --term, --locale, --context, --do-not-translate, --translation"
27240
+ throw new UsageError(
27241
+ "No fields to update.",
27242
+ "Pass at least one of --term, --locale, --context, --do-not-translate, --translation."
27636
27243
  );
27637
27244
  }
27638
27245
  return patch;
27639
27246
  };
27247
+ const update = async () => {
27248
+ const patch = buildPatch();
27249
+ const convexClient = await getAuthenticatedConvexClient();
27250
+ const { organizationId: organizationId2 } = await getProjectContext(convexClient);
27251
+ await convexClient.mutation(api.glossaries.update, {
27252
+ organizationId: organizationId2,
27253
+ glossaryId: opts.id,
27254
+ patch
27255
+ });
27256
+ };
27640
27257
  if (opts.json) {
27641
- try {
27642
- const patch = buildPatch();
27643
- const convexClient = await getAuthenticatedConvexClient();
27644
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
27645
- await convexClient.mutation(api.glossaries.update, {
27646
- organizationId: organizationId2,
27647
- glossaryId: opts.id,
27648
- patch
27649
- });
27650
- jsonSuccess({ id: opts.id, ok: true });
27651
- } catch (error62) {
27652
- jsonError(error62 instanceof Error ? error62.message : String(error62));
27258
+ await update();
27259
+ jsonSuccess({ id: opts.id, ok: true });
27260
+ }
27261
+ const { unmount } = renderTransient(
27262
+ /* @__PURE__ */ jsx8(UpdateUI, { state: { type: "updating" } })
27263
+ );
27264
+ try {
27265
+ await update();
27266
+ } finally {
27267
+ unmount();
27268
+ }
27269
+ render9(/* @__PURE__ */ jsx8(UpdateUI, { state: { type: "success", id: opts.id } })).unmount();
27270
+ }
27271
+
27272
+ // src/commands/glossary/index.ts
27273
+ var glossaryCommand = new Command2("glossary").description(
27274
+ "Manage glossary terms"
27275
+ );
27276
+ glossaryCommand.command("list").description("List glossary terms for the current project").option("--locale <locale>", "Filter by source locale").option("--limit <n>", "Max number of results", (v) => Number(v), 100).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
27277
+ const schema = z4.object({
27278
+ locale: z4.string().optional(),
27279
+ limit: z4.coerce.number().int().positive().default(100),
27280
+ json: z4.boolean().optional()
27281
+ });
27282
+ const parsed = validateOrExit(schema, opts);
27283
+ await runGlossaryList(parsed);
27284
+ });
27285
+ glossaryCommand.command("show").description("Show details of a glossary term").requiredOption("--id <glossaryId>", "Glossary term ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
27286
+ const schema = z4.object({
27287
+ id: z4.string().min(1),
27288
+ json: z4.boolean().optional()
27289
+ });
27290
+ const parsed = validateOrExit(schema, opts);
27291
+ await runGlossaryShow(parsed);
27292
+ });
27293
+ glossaryCommand.command("create").description("Create a new glossary term").requiredOption("--term <text>", "Source term").requiredOption("--locale <locale>", "Source locale (e.g. en)").option("--context <text>", "AI context for translation guidance").option("--do-not-translate", "Mark term as do-not-translate").option(
27294
+ "--translation <locale:text...>",
27295
+ "Translations in locale:text format (e.g. --translation no:Hei de:Hallo)"
27296
+ ).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
27297
+ const schema = z4.object({
27298
+ term: z4.string().min(1),
27299
+ locale: z4.string().min(1),
27300
+ context: z4.string().optional(),
27301
+ doNotTranslate: z4.boolean().optional(),
27302
+ translation: z4.array(z4.string()).optional(),
27303
+ json: z4.boolean().optional()
27304
+ });
27305
+ const parsed = validateOrExit(schema, opts);
27306
+ await runGlossaryCreate(parsed);
27307
+ });
27308
+ glossaryCommand.command("update").description("Update an existing glossary term").requiredOption("--id <glossaryId>", "Glossary term ID").option("--term <text>", "New source term").option("--locale <locale>", "New source locale").option("--context <text>", "AI context for translation guidance").option("--do-not-translate", "Mark term as do-not-translate").option("--no-do-not-translate", "Unmark do-not-translate").option(
27309
+ "--translation <locale:text...>",
27310
+ "Translations in locale:text format (replaces all translations)"
27311
+ ).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
27312
+ const schema = z4.object({
27313
+ id: z4.string().min(1),
27314
+ term: z4.string().optional(),
27315
+ locale: z4.string().optional(),
27316
+ context: z4.string().optional(),
27317
+ doNotTranslate: z4.boolean().optional(),
27318
+ translation: z4.array(z4.string()).optional(),
27319
+ json: z4.boolean().optional()
27320
+ });
27321
+ const parsed = validateOrExit(schema, opts);
27322
+ await runGlossaryUpdate(parsed);
27323
+ });
27324
+ glossaryCommand.command("remove").description("Remove a glossary term").requiredOption("--id <glossaryId>", "Glossary term ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
27325
+ const schema = z4.object({
27326
+ id: z4.string().min(1),
27327
+ json: z4.boolean().optional()
27328
+ });
27329
+ const parsed = validateOrExit(schema, opts);
27330
+ await runGlossaryRemove(parsed);
27331
+ });
27332
+
27333
+ // src/commands/init/index.tsx
27334
+ import { Box as Box10, render as render10, Text as Text10 } from "ink";
27335
+
27336
+ // src/lib/telemetry.ts
27337
+ import { randomUUID } from "crypto";
27338
+ import fs4 from "fs/promises";
27339
+ import os2 from "os";
27340
+ import path4 from "path";
27341
+
27342
+ // ../shared/src/analytics/index.ts
27343
+ var ORGANIZATION_GROUP = "organization";
27344
+ var analyticsEventNames = [
27345
+ "trial_started",
27346
+ "project_created",
27347
+ "project_page_viewed",
27348
+ "github_connect_started",
27349
+ "github_install_initiated",
27350
+ "plan_change_initiated",
27351
+ "credits_purchase_initiated",
27352
+ "glossary_created",
27353
+ "glossary_deleted",
27354
+ "glossary_imported",
27355
+ "translation_job_viewed",
27356
+ "translation_logs_viewed",
27357
+ "cli_login_completed",
27358
+ "cli_command_completed",
27359
+ "cli_init_completed",
27360
+ "cli_translate_completed",
27361
+ "cli_validate_completed",
27362
+ "translation_job_started",
27363
+ "translation_job_completed",
27364
+ "translation_job_failed",
27365
+ "pull_request_opened",
27366
+ "pull_request_merged",
27367
+ "pull_request_closed_unmerged",
27368
+ "github_app_installed",
27369
+ "github_app_uninstalled",
27370
+ "repository_connected",
27371
+ "subscription_started",
27372
+ "subscription_plan_changed",
27373
+ "subscription_canceled",
27374
+ "trial_ended",
27375
+ "credits_purchased",
27376
+ "credits_exhausted",
27377
+ "usage_limit_reached",
27378
+ "cta_clicked",
27379
+ "waitlist_joined",
27380
+ "signed_up",
27381
+ "signed_in",
27382
+ "onboarding_step_completed",
27383
+ "onboarding_completed",
27384
+ "language_added",
27385
+ "translation_edited",
27386
+ "translation_bulk_retranslated",
27387
+ "dashboard_translation_started",
27388
+ "project_settings_changed",
27389
+ "member_invited",
27390
+ "feedback_submitted",
27391
+ "analytics_smoke_tested"
27392
+ ];
27393
+ var registeredNames = new Set(analyticsEventNames);
27394
+
27395
+ // src/lib/telemetry.ts
27396
+ import { PostHog } from "posthog-node";
27397
+ var TELEMETRY_ENV_VAR = "TOLKBOT_TELEMETRY";
27398
+ var DO_NOT_TRACK_ENV_VAR = "DO_NOT_TRACK";
27399
+ var POSTHOG_HOST = "https://eu.i.posthog.com";
27400
+ var FLUSH_TIMEOUT_MS = 1e3;
27401
+ var OFF_VALUES = /* @__PURE__ */ new Set(["0", "false", "off", "no"]);
27402
+ var ON_VALUES = /* @__PURE__ */ new Set(["1", "true", "on", "yes"]);
27403
+ function readEnv(name) {
27404
+ return process.env[name]?.trim();
27405
+ }
27406
+ function projectKey() {
27407
+ const key = "phc_uSMLUC9D7bHeaHFAosnoDzBj6DY5uzLcCQkXZxzj0mQ"?.trim();
27408
+ if (!key) return void 0;
27409
+ return key;
27410
+ }
27411
+ function environment() {
27412
+ const nodeEnv = process.env.NODE_ENV;
27413
+ return nodeEnv === "development" || nodeEnv === "test" ? nodeEnv : "production";
27414
+ }
27415
+ function inCi() {
27416
+ return Boolean(process.env.CI);
27417
+ }
27418
+ function getTelemetryDir() {
27419
+ return path4.join(os2.homedir(), ".tolkbot");
27420
+ }
27421
+ function getPreferencePath() {
27422
+ return path4.join(getTelemetryDir(), "telemetry");
27423
+ }
27424
+ function getAnonymousIdPath() {
27425
+ return path4.join(getTelemetryDir(), "anonymous-id");
27426
+ }
27427
+ async function readFileOrNull(file2) {
27428
+ try {
27429
+ return (await fs4.readFile(file2, "utf-8")).trim() || null;
27430
+ } catch {
27431
+ return null;
27432
+ }
27433
+ }
27434
+ async function writePrivateFile(file2, content) {
27435
+ await fs4.mkdir(path4.dirname(file2), { recursive: true, mode: 448 });
27436
+ await fs4.writeFile(file2, content, { mode: 384 });
27437
+ }
27438
+ async function telemetryState() {
27439
+ const envValue = readEnv(TELEMETRY_ENV_VAR)?.toLowerCase();
27440
+ if (envValue !== void 0 && OFF_VALUES.has(envValue)) {
27441
+ return { enabled: false, reason: "env" };
27442
+ }
27443
+ const doNotTrack = readEnv(DO_NOT_TRACK_ENV_VAR)?.toLowerCase();
27444
+ if (doNotTrack !== void 0 && ON_VALUES.has(doNotTrack)) {
27445
+ return { enabled: false, reason: "do-not-track" };
27446
+ }
27447
+ const preference = (await readFileOrNull(getPreferencePath()))?.toLowerCase();
27448
+ if (preference !== void 0 && OFF_VALUES.has(preference)) {
27449
+ return { enabled: false, reason: "preference" };
27450
+ }
27451
+ if (!projectKey()) {
27452
+ return { enabled: false, reason: "no-key" };
27453
+ }
27454
+ return { enabled: true, reason: "default" };
27455
+ }
27456
+ async function setTelemetryPreference(enabled) {
27457
+ await writePrivateFile(getPreferencePath(), enabled ? "on\n" : "off\n");
27458
+ }
27459
+ function decodeTokenSubject(token) {
27460
+ const payload = token.split(".")[1];
27461
+ if (!payload) return null;
27462
+ try {
27463
+ const json2 = Buffer.from(payload, "base64url").toString("utf-8");
27464
+ const claims = JSON.parse(json2);
27465
+ if (typeof claims === "object" && claims !== null && "sub" in claims && typeof claims.sub === "string") {
27466
+ return claims.sub;
27653
27467
  }
27468
+ } catch {
27654
27469
  }
27655
- let { unmount } = render8(/* @__PURE__ */ jsx8(UpdateUI, { state: { type: "updating" } }));
27470
+ return null;
27471
+ }
27472
+ async function resolveDistinctId() {
27473
+ const resolved = await resolveToken();
27474
+ const subject = resolved ? decodeTokenSubject(resolved.token) : null;
27475
+ if (subject) return subject;
27476
+ const file2 = getAnonymousIdPath();
27477
+ const existing = await readFileOrNull(file2);
27478
+ if (existing) return existing;
27479
+ const generated = randomUUID();
27656
27480
  try {
27657
- const patch = buildPatch();
27658
- const convexClient = await getAuthenticatedConvexClient();
27659
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
27660
- await convexClient.mutation(api.glossaries.update, {
27661
- organizationId: organizationId2,
27662
- glossaryId: opts.id,
27663
- patch
27664
- });
27665
- unmount();
27666
- const ui = render8(
27667
- /* @__PURE__ */ jsx8(UpdateUI, { state: { type: "success", id: opts.id } })
27668
- );
27669
- unmount = ui.unmount;
27670
- await ui.waitUntilExit();
27671
- } catch (error62) {
27672
- unmount();
27673
- const ui = render8(
27674
- /* @__PURE__ */ jsx8(
27675
- UpdateUI,
27676
- {
27677
- state: {
27678
- type: "error",
27679
- message: error62 instanceof Error ? error62.message : String(error62)
27680
- }
27681
- }
27682
- )
27683
- );
27684
- await ui.waitUntilExit();
27685
- return exitAfterFlush(1);
27481
+ await writePrivateFile(file2, `${generated}
27482
+ `);
27483
+ } catch {
27686
27484
  }
27485
+ return generated;
27687
27486
  }
27688
-
27689
- // src/commands/glossary/index.ts
27690
- var glossaryCommand = new Command2("glossary").description(
27691
- "Manage glossary terms"
27692
- );
27693
- glossaryCommand.command("list").description("List glossary terms for the current project").option("--locale <locale>", "Filter by source locale").option("--limit <n>", "Max number of results", (v) => Number(v), 100).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
27694
- const schema = z4.object({
27695
- locale: z4.string().optional(),
27696
- limit: z4.coerce.number().int().positive().default(100),
27697
- json: z4.boolean().optional()
27698
- });
27699
- const parsed = validateOrExit(schema, opts);
27700
- await runGlossaryList(parsed);
27701
- });
27702
- glossaryCommand.command("show").description("Show details of a glossary term").requiredOption("--id <glossaryId>", "Glossary term ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
27703
- const schema = z4.object({
27704
- id: z4.string().min(1),
27705
- json: z4.boolean().optional()
27487
+ var organizationId = null;
27488
+ function setTelemetryOrganization(id) {
27489
+ organizationId = id;
27490
+ }
27491
+ var client2 = null;
27492
+ function getClient(key) {
27493
+ client2 ??= new PostHog(key, {
27494
+ host: POSTHOG_HOST,
27495
+ // A CLI process is short-lived: send each event as it happens
27496
+ // rather than batching for a flush that may never come.
27497
+ flushAt: 1,
27498
+ flushInterval: 0,
27499
+ requestTimeout: FLUSH_TIMEOUT_MS,
27500
+ // No IP-derived location on CLI events. The privacy policy lists
27501
+ // exactly what the CLI reports, and location is not on it.
27502
+ disableGeoip: true
27706
27503
  });
27707
- const parsed = validateOrExit(schema, opts);
27708
- await runGlossaryShow(parsed);
27709
- });
27710
- glossaryCommand.command("create").description("Create a new glossary term").requiredOption("--term <text>", "Source term").requiredOption("--locale <locale>", "Source locale (e.g. en)").option("--context <text>", "AI context for translation guidance").option("--do-not-translate", "Mark term as do-not-translate").option(
27711
- "--translation <locale:text...>",
27712
- "Translations in locale:text format (e.g. --translation no:Hei de:Hallo)"
27713
- ).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
27714
- const schema = z4.object({
27715
- term: z4.string().min(1),
27716
- locale: z4.string().min(1),
27717
- context: z4.string().optional(),
27718
- doNotTranslate: z4.boolean().optional(),
27719
- translation: z4.array(z4.string()).optional(),
27720
- json: z4.boolean().optional()
27504
+ return client2;
27505
+ }
27506
+ var inFlight = /* @__PURE__ */ new Set();
27507
+ async function send(event, properties) {
27508
+ const state = await telemetryState();
27509
+ if (!state.enabled) return;
27510
+ const key = projectKey();
27511
+ if (!key) return;
27512
+ const distinctId = await resolveDistinctId();
27513
+ await getClient(key).captureImmediate({
27514
+ distinctId,
27515
+ event,
27516
+ properties: { ...properties, environment: environment() },
27517
+ groups: organizationId ? { [ORGANIZATION_GROUP]: organizationId } : void 0
27721
27518
  });
27722
- const parsed = validateOrExit(schema, opts);
27723
- await runGlossaryCreate(parsed);
27724
- });
27725
- glossaryCommand.command("update").description("Update an existing glossary term").requiredOption("--id <glossaryId>", "Glossary term ID").option("--term <text>", "New source term").option("--locale <locale>", "New source locale").option("--context <text>", "AI context for translation guidance").option("--do-not-translate", "Mark term as do-not-translate").option("--no-do-not-translate", "Unmark do-not-translate").option(
27726
- "--translation <locale:text...>",
27727
- "Translations in locale:text format (replaces all translations)"
27728
- ).option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
27729
- const schema = z4.object({
27730
- id: z4.string().min(1),
27731
- term: z4.string().optional(),
27732
- locale: z4.string().optional(),
27733
- context: z4.string().optional(),
27734
- doNotTranslate: z4.boolean().optional(),
27735
- translation: z4.array(z4.string()).optional(),
27736
- json: z4.boolean().optional()
27519
+ }
27520
+ function track(event, properties) {
27521
+ const attempt = send(event, properties).catch(() => void 0);
27522
+ inFlight.add(attempt);
27523
+ void attempt.finally(() => inFlight.delete(attempt));
27524
+ }
27525
+ async function flushTelemetry(timeoutMs = FLUSH_TIMEOUT_MS) {
27526
+ if (inFlight.size === 0 && !client2) return;
27527
+ const deadline = new Promise((resolve) => {
27528
+ const timer = setTimeout(resolve, timeoutMs);
27529
+ timer.unref();
27737
27530
  });
27738
- const parsed = validateOrExit(schema, opts);
27739
- await runGlossaryUpdate(parsed);
27740
- });
27741
- glossaryCommand.command("remove").description("Remove a glossary term").requiredOption("--id <glossaryId>", "Glossary term ID").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
27742
- const schema = z4.object({
27743
- id: z4.string().min(1),
27744
- json: z4.boolean().optional()
27531
+ await Promise.race([Promise.allSettled([...inFlight]), deadline]);
27532
+ if (client2) {
27533
+ const closing = client2.shutdown(timeoutMs).catch(() => void 0);
27534
+ client2 = null;
27535
+ await Promise.race([closing, deadline]);
27536
+ }
27537
+ }
27538
+ function runtimeProperties(cliVersion) {
27539
+ return {
27540
+ cli_version: cliVersion,
27541
+ node_version: process.version,
27542
+ platform: process.platform,
27543
+ ci: inCi()
27544
+ };
27545
+ }
27546
+ var activeCommand = null;
27547
+ function beginCommand(path12, cliVersion) {
27548
+ activeCommand = {
27549
+ path: path12,
27550
+ startedAt: Date.now(),
27551
+ cliVersion,
27552
+ reported: false
27553
+ };
27554
+ }
27555
+ function completeCommand(exitCode) {
27556
+ if (!activeCommand || activeCommand.reported) return;
27557
+ activeCommand.reported = true;
27558
+ track("cli_command_completed", {
27559
+ command: activeCommand.path,
27560
+ success: exitCode === 0,
27561
+ exit_code: exitCode,
27562
+ duration_ms: Date.now() - activeCommand.startedAt,
27563
+ json: wantsJson(),
27564
+ ...runtimeProperties(activeCommand.cliVersion)
27745
27565
  });
27746
- const parsed = validateOrExit(schema, opts);
27747
- await runGlossaryRemove(parsed);
27748
- });
27749
-
27750
- // src/commands/init/index.tsx
27751
- import { Box as Box10, render as render9, Text as Text10 } from "ink";
27566
+ }
27752
27567
 
27753
27568
  // src/commands/init/core.ts
27754
27569
  import { execSync as execSync2 } from "child_process";
@@ -27805,7 +27620,8 @@ function assertSupportedLocales(locales, label) {
27805
27620
  if (unsupported.length > 0) {
27806
27621
  throw new CliError(
27807
27622
  `Unsupported ${label}: ${unsupported.join(", ")}.`,
27808
- `TolkBot supports ${SUPPORTED.size} locales. Run 'tolkbot init' without --yes to pick from the list.`
27623
+ `TolkBot supports ${SUPPORTED.size} locales. Run 'tolkbot init' without --yes in a terminal to pick from the list.`,
27624
+ "usage"
27809
27625
  );
27810
27626
  }
27811
27627
  return normalized.filter((l) => isSupportedLocale(l));
@@ -27831,7 +27647,8 @@ async function linkRepository(cwd, plan) {
27831
27647
  if (!repository) {
27832
27648
  throw new CliError(
27833
27649
  `Repository ${owner}/${name} is not connected to TolkBot.`,
27834
- "Install the GitHub App at https://github.com/apps/tolkbot, grant it access to this repository, then run 'tolkbot init' again."
27650
+ "Install the GitHub App at https://github.com/apps/tolkbot, grant it access to this repository, then run 'tolkbot init' again.",
27651
+ "repository_not_connected"
27835
27652
  );
27836
27653
  }
27837
27654
  const existingProjects = await convexClient.query(
@@ -27927,7 +27744,8 @@ function requireSourcePattern(project, repository) {
27927
27744
  const reason = project.initializationStatus === "failed" && project.initializationError ? ` Detection failed: ${project.initializationError}` : "";
27928
27745
  throw new CliError(
27929
27746
  `TolkBot found no translation files in ${repository}.${reason}`,
27930
- `Set the file pattern in the project's settings at ${patternSettingsUrl(project.slug)}, then run 'tolkbot init' again.`
27747
+ `Set the file pattern in the project's settings at ${patternSettingsUrl(project.slug)}, then run 'tolkbot init' again.`,
27748
+ "no_translation_files"
27931
27749
  );
27932
27750
  }
27933
27751
 
@@ -28311,7 +28129,8 @@ function resolvePlan(cwd, opts) {
28311
28129
  if (!FRAMEWORKS.includes(opts.framework)) {
28312
28130
  throw new CliError(
28313
28131
  `Unknown framework "${opts.framework}".`,
28314
- `Expected one of: ${FRAMEWORKS.join(", ")}.`
28132
+ `Expected one of: ${FRAMEWORKS.join(", ")}.`,
28133
+ "usage"
28315
28134
  );
28316
28135
  }
28317
28136
  framework = opts.framework;
@@ -28421,37 +28240,36 @@ async function runHeadless(cwd, opts) {
28421
28240
  );
28422
28241
  return;
28423
28242
  }
28424
- render9(/* @__PURE__ */ jsx10(SummaryUI, { plan, result })).unmount();
28243
+ render10(/* @__PURE__ */ jsx10(SummaryUI, { plan, result })).unmount();
28425
28244
  }
28426
- function canPrompt() {
28427
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
28245
+ async function requireLogin() {
28246
+ const resolved = await resolveToken();
28247
+ if (!resolved) {
28248
+ throw new AuthError(
28249
+ "tolkbot init needs a TolkBot account, and this machine is not logged in.",
28250
+ `Run 'tolkbot auth login', which opens tolkbot.ai in a browser to sign in with GitHub, or set $${TOKEN_ENV_VAR} in CI. Init also needs the TolkBot GitHub App on this repository, and translating needs an organization with a subscription.`
28251
+ );
28252
+ }
28253
+ if (isTokenExpired(resolved)) {
28254
+ throw new AuthError(
28255
+ `Your token (from ${describeTokenSource(resolved.source)}) has expired.`,
28256
+ reauthHint(resolved.source),
28257
+ "token_expired"
28258
+ );
28259
+ }
28428
28260
  }
28429
28261
  async function runInit(opts = {}) {
28430
28262
  const cwd = process.cwd();
28431
- const asked = Boolean(opts.yes) || Boolean(opts.json);
28432
- const headless = asked || !canPrompt();
28433
- if (!headless) {
28263
+ await requireLogin();
28264
+ if (!opts.yes && !opts.json && isInteractive()) {
28434
28265
  await runInteractive(opts);
28435
28266
  return;
28436
28267
  }
28437
- if (!asked) {
28438
- throw new CliError(
28439
- "tolkbot init needs a terminal to prompt, and this session has none.",
28440
- "Re-run with --yes to accept detected settings, overriding any of them with --base, --targets or --framework."
28441
- );
28442
- }
28443
- try {
28444
- await runHeadless(cwd, opts);
28445
- } catch (error62) {
28446
- if (opts.json && !(error62 instanceof CliError)) {
28447
- throw new CliError(describeError(error62));
28448
- }
28449
- throw error62;
28450
- }
28268
+ await runHeadless(cwd, opts);
28451
28269
  }
28452
28270
  async function runInteractive(opts) {
28453
28271
  const outcome = { failed: false };
28454
- const { unmount, waitUntilExit } = render9(
28272
+ const { unmount, waitUntilExit } = render10(
28455
28273
  /* @__PURE__ */ jsx10(
28456
28274
  InitWizard,
28457
28275
  {
@@ -28487,7 +28305,7 @@ import { z as z5 } from "zod/v4";
28487
28305
 
28488
28306
  // src/commands/learnings/archive.tsx
28489
28307
  import { Spinner as Spinner9 } from "@inkjs/ui";
28490
- import { Box as Box11, render as render10, Text as Text11 } from "ink";
28308
+ import { Box as Box11, render as render11, Text as Text11 } from "ink";
28491
28309
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
28492
28310
  function ArchiveUI({ state }) {
28493
28311
  if (state.type === "archiving") {
@@ -28496,64 +28314,38 @@ function ArchiveUI({ state }) {
28496
28314
  /* @__PURE__ */ jsx11(Text11, { children: " Archiving learning\u2026" })
28497
28315
  ] });
28498
28316
  }
28499
- if (state.type === "success") {
28500
- return /* @__PURE__ */ jsxs11(Box11, { children: [
28501
- /* @__PURE__ */ jsx11(Text11, { color: "green", children: "\u2713" }),
28502
- /* @__PURE__ */ jsx11(Text11, { children: " Learning archived" })
28503
- ] });
28504
- }
28505
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
28506
- /* @__PURE__ */ jsx11(Text11, { color: "red", children: "\u2717 Failed to archive learning" }),
28507
- /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: state.message })
28317
+ return /* @__PURE__ */ jsxs11(Box11, { children: [
28318
+ /* @__PURE__ */ jsx11(Text11, { color: "green", children: "\u2713" }),
28319
+ /* @__PURE__ */ jsx11(Text11, { children: " Learning archived" })
28508
28320
  ] });
28509
28321
  }
28510
28322
  async function runLearningsArchive(opts) {
28511
- if (opts.json) {
28512
- try {
28513
- const convexClient = await getAuthenticatedConvexClient();
28514
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
28515
- await convexClient.mutation(api.learnings.archive, {
28516
- organizationId: organizationId2,
28517
- learningId: opts.id
28518
- });
28519
- jsonSuccess({ id: opts.id, ok: true });
28520
- } catch (error62) {
28521
- jsonError(error62 instanceof Error ? error62.message : String(error62));
28522
- }
28523
- }
28524
- let { unmount } = render10(/* @__PURE__ */ jsx11(ArchiveUI, { state: { type: "archiving" } }));
28525
- try {
28323
+ const run = async () => {
28526
28324
  const convexClient = await getAuthenticatedConvexClient();
28527
28325
  const { organizationId: organizationId2 } = await getProjectContext(convexClient);
28528
28326
  await convexClient.mutation(api.learnings.archive, {
28529
28327
  organizationId: organizationId2,
28530
28328
  learningId: opts.id
28531
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 {
28532
28341
  unmount();
28533
- const ui = render10(/* @__PURE__ */ jsx11(ArchiveUI, { state: { type: "success" } }));
28534
- unmount = ui.unmount;
28535
- await ui.waitUntilExit();
28536
- } catch (error62) {
28537
- unmount();
28538
- const ui = render10(
28539
- /* @__PURE__ */ jsx11(
28540
- ArchiveUI,
28541
- {
28542
- state: {
28543
- type: "error",
28544
- message: error62 instanceof Error ? error62.message : String(error62)
28545
- }
28546
- }
28547
- )
28548
- );
28549
- await ui.waitUntilExit();
28550
- return exitAfterFlush(1);
28551
28342
  }
28343
+ render11(/* @__PURE__ */ jsx11(ArchiveUI, { state: { type: "success" } })).unmount();
28552
28344
  }
28553
28345
 
28554
28346
  // src/commands/learnings/list.tsx
28555
28347
  import { Spinner as Spinner10 } from "@inkjs/ui";
28556
- import { Box as Box12, render as render11, Text as Text12 } from "ink";
28348
+ import { Box as Box12, render as render12, Text as Text12 } from "ink";
28557
28349
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
28558
28350
  var feedbackLabels = {
28559
28351
  "incorrect-term": "Incorrect term",
@@ -28569,12 +28361,6 @@ function LearningsListUI({ state }) {
28569
28361
  /* @__PURE__ */ jsx12(Text12, { children: " Loading learnings\u2026" })
28570
28362
  ] });
28571
28363
  }
28572
- if (state.type === "error") {
28573
- return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
28574
- /* @__PURE__ */ jsx12(Text12, { color: "red", children: "\u2717 Error" }),
28575
- /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: state.message })
28576
- ] });
28577
- }
28578
28364
  const { learnings } = state;
28579
28365
  if (learnings.length === 0) {
28580
28366
  return /* @__PURE__ */ jsx12(Box12, { children: /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "No learnings found." }) });
@@ -28617,143 +28403,90 @@ function LearningsListUI({ state }) {
28617
28403
  ] }, l._id))
28618
28404
  ] });
28619
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
28414
+ });
28415
+ }
28620
28416
  async function runLearningsList(opts) {
28621
28417
  if (opts.json) {
28622
- try {
28623
- const convexClient = await getAuthenticatedConvexClient();
28624
- const { projectId, organizationId: organizationId2 } = await getProjectContext(convexClient);
28625
- const learnings = await convexClient.query(
28626
- api.learnings.listByProject,
28627
- {
28628
- organizationId: organizationId2,
28629
- projectId,
28630
- status: opts.status,
28631
- limit: opts.limit
28632
- }
28633
- );
28634
- jsonSuccess({
28635
- learnings: learnings.map((l) => ({
28636
- id: l._id,
28637
- sourceText: l.sourceText,
28638
- suggestedTranslation: l.suggestedTranslation,
28639
- srcLocale: l.srcLocale,
28640
- dstLocale: l.dstLocale,
28641
- feedbackType: l.feedbackType,
28642
- reasoning: l.reasoning,
28643
- status: l.status,
28644
- source: l.source
28645
- }))
28646
- });
28647
- } catch (error62) {
28648
- jsonError(error62 instanceof Error ? error62.message : String(error62));
28649
- }
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
+ });
28650
28432
  }
28651
- let { unmount } = render11(/* @__PURE__ */ jsx12(LearningsListUI, { state: { type: "loading" } }));
28433
+ const { unmount } = renderTransient(
28434
+ /* @__PURE__ */ jsx12(LearningsListUI, { state: { type: "loading" } })
28435
+ );
28436
+ let learnings;
28652
28437
  try {
28653
- const convexClient = await getAuthenticatedConvexClient();
28654
- const { projectId, organizationId: organizationId2 } = await getProjectContext(convexClient);
28655
- const learnings = await convexClient.query(
28656
- api.learnings.listByProject,
28657
- {
28658
- organizationId: organizationId2,
28659
- projectId,
28660
- status: opts.status,
28661
- limit: opts.limit
28662
- }
28663
- );
28664
- unmount();
28665
- const ui = render11(
28666
- /* @__PURE__ */ jsx12(LearningsListUI, { state: { type: "success", learnings } })
28667
- );
28668
- unmount = ui.unmount;
28669
- await ui.waitUntilExit();
28670
- } catch (error62) {
28438
+ learnings = await loadLearnings(opts);
28439
+ } finally {
28671
28440
  unmount();
28672
- const ui = render11(
28673
- /* @__PURE__ */ jsx12(
28674
- LearningsListUI,
28675
- {
28676
- state: {
28677
- type: "error",
28678
- message: error62 instanceof Error ? error62.message : String(error62)
28679
- }
28680
- }
28681
- )
28682
- );
28683
- await ui.waitUntilExit();
28684
- return exitAfterFlush(1);
28685
28441
  }
28442
+ render12(
28443
+ /* @__PURE__ */ jsx12(LearningsListUI, { state: { type: "success", learnings } })
28444
+ ).unmount();
28686
28445
  }
28687
28446
 
28688
28447
  // src/commands/learnings/remove.tsx
28689
28448
  import { Spinner as Spinner11 } from "@inkjs/ui";
28690
- import { Box as Box13, render as render12, Text as Text13 } from "ink";
28449
+ import { Box as Box13, render as render13, Text as Text13 } from "ink";
28691
28450
  import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
28692
28451
  function RemoveUI2({ state }) {
28693
28452
  if (state.type === "removing") {
28694
28453
  return /* @__PURE__ */ jsxs13(Box13, { children: [
28695
28454
  /* @__PURE__ */ jsx13(Spinner11, {}),
28696
- /* @__PURE__ */ jsx13(Text13, { children: " Removing learning\u2026" })
28697
- ] });
28698
- }
28699
- if (state.type === "success") {
28700
- return /* @__PURE__ */ jsxs13(Box13, { children: [
28701
- /* @__PURE__ */ jsx13(Text13, { color: "green", children: "\u2713" }),
28702
- /* @__PURE__ */ jsx13(Text13, { children: " Learning removed" })
28455
+ /* @__PURE__ */ jsx13(Text13, { children: " Removing learning\u2026" })
28703
28456
  ] });
28704
28457
  }
28705
- return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
28706
- /* @__PURE__ */ jsx13(Text13, { color: "red", children: "\u2717 Failed to remove learning" }),
28707
- /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: state.message })
28458
+ return /* @__PURE__ */ jsxs13(Box13, { children: [
28459
+ /* @__PURE__ */ jsx13(Text13, { color: "green", children: "\u2713" }),
28460
+ /* @__PURE__ */ jsx13(Text13, { children: " Learning removed" })
28708
28461
  ] });
28709
28462
  }
28710
28463
  async function runLearningsRemove(opts) {
28711
- if (opts.json) {
28712
- try {
28713
- const convexClient = await getAuthenticatedConvexClient();
28714
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
28715
- await convexClient.mutation(api.learnings.remove, {
28716
- organizationId: organizationId2,
28717
- learningId: opts.id
28718
- });
28719
- jsonSuccess({ id: opts.id, ok: true });
28720
- } catch (error62) {
28721
- jsonError(error62 instanceof Error ? error62.message : String(error62));
28722
- }
28723
- }
28724
- let { unmount } = render12(/* @__PURE__ */ jsx13(RemoveUI2, { state: { type: "removing" } }));
28725
- try {
28464
+ const run = async () => {
28726
28465
  const convexClient = await getAuthenticatedConvexClient();
28727
28466
  const { organizationId: organizationId2 } = await getProjectContext(convexClient);
28728
28467
  await convexClient.mutation(api.learnings.remove, {
28729
28468
  organizationId: organizationId2,
28730
28469
  learningId: opts.id
28731
28470
  });
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 {
28732
28482
  unmount();
28733
- const ui = render12(/* @__PURE__ */ jsx13(RemoveUI2, { state: { type: "success" } }));
28734
- unmount = ui.unmount;
28735
- await ui.waitUntilExit();
28736
- } catch (error62) {
28737
- unmount();
28738
- const ui = render12(
28739
- /* @__PURE__ */ jsx13(
28740
- RemoveUI2,
28741
- {
28742
- state: {
28743
- type: "error",
28744
- message: error62 instanceof Error ? error62.message : String(error62)
28745
- }
28746
- }
28747
- )
28748
- );
28749
- await ui.waitUntilExit();
28750
- return exitAfterFlush(1);
28751
28483
  }
28484
+ render13(/* @__PURE__ */ jsx13(RemoveUI2, { state: { type: "success" } })).unmount();
28752
28485
  }
28753
28486
 
28754
28487
  // src/commands/learnings/show.tsx
28755
28488
  import { Spinner as Spinner12 } from "@inkjs/ui";
28756
- import { Box as Box14, render as render13, Text as Text14 } from "ink";
28489
+ import { Box as Box14, render as render14, Text as Text14 } from "ink";
28757
28490
  import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
28758
28491
  function LearningShowUI({ state }) {
28759
28492
  if (state.type === "loading") {
@@ -28762,15 +28495,6 @@ function LearningShowUI({ state }) {
28762
28495
  /* @__PURE__ */ jsx14(Text14, { children: " Loading learning\u2026" })
28763
28496
  ] });
28764
28497
  }
28765
- if (state.type === "not-found") {
28766
- return /* @__PURE__ */ jsx14(Box14, { children: /* @__PURE__ */ jsx14(Text14, { color: "red", children: "\u2717 Learning not found" }) });
28767
- }
28768
- if (state.type === "error") {
28769
- return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
28770
- /* @__PURE__ */ jsx14(Text14, { color: "red", children: "\u2717 Error" }),
28771
- /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: state.message })
28772
- ] });
28773
- }
28774
28498
  const { learning } = state;
28775
28499
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
28776
28500
  /* @__PURE__ */ jsxs14(Box14, { gap: 1, children: [
@@ -28811,70 +28535,47 @@ function LearningShowUI({ state }) {
28811
28535
  ] }) })
28812
28536
  ] });
28813
28537
  }
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"
28550
+ );
28551
+ }
28552
+ return learning;
28553
+ }
28814
28554
  async function runLearningsShow(opts) {
28815
28555
  if (opts.json) {
28816
- try {
28817
- const convexClient = await getAuthenticatedConvexClient();
28818
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
28819
- const learning = await convexClient.query(api.learnings.getById, {
28820
- organizationId: organizationId2,
28821
- learningId: opts.id
28822
- });
28823
- if (!learning) {
28824
- jsonError("Learning not found");
28825
- }
28826
- jsonSuccess({
28827
- id: learning._id,
28828
- sourceText: learning.sourceText,
28829
- suggestedTranslation: learning.suggestedTranslation,
28830
- srcLocale: learning.srcLocale,
28831
- dstLocale: learning.dstLocale,
28832
- feedbackType: learning.feedbackType,
28833
- reasoning: learning.reasoning,
28834
- status: learning.status,
28835
- source: learning.source
28836
- });
28837
- } catch (error62) {
28838
- jsonError(error62 instanceof Error ? error62.message : String(error62));
28839
- }
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
+ });
28840
28568
  }
28841
- let { unmount } = render13(/* @__PURE__ */ jsx14(LearningShowUI, { state: { type: "loading" } }));
28569
+ const { unmount } = renderTransient(
28570
+ /* @__PURE__ */ jsx14(LearningShowUI, { state: { type: "loading" } })
28571
+ );
28572
+ let learning;
28842
28573
  try {
28843
- const convexClient = await getAuthenticatedConvexClient();
28844
- const { organizationId: organizationId2 } = await getProjectContext(convexClient);
28845
- const learning = await convexClient.query(api.learnings.getById, {
28846
- organizationId: organizationId2,
28847
- learningId: opts.id
28848
- });
28849
- if (!learning) {
28850
- unmount();
28851
- const ui2 = render13(/* @__PURE__ */ jsx14(LearningShowUI, { state: { type: "not-found" } }));
28852
- unmount = ui2.unmount;
28853
- await ui2.waitUntilExit();
28854
- return exitAfterFlush(1);
28855
- }
28856
- unmount();
28857
- const ui = render13(
28858
- /* @__PURE__ */ jsx14(LearningShowUI, { state: { type: "success", learning } })
28859
- );
28860
- unmount = ui.unmount;
28861
- await ui.waitUntilExit();
28862
- } catch (error62) {
28574
+ learning = await loadLearning(opts.id);
28575
+ } finally {
28863
28576
  unmount();
28864
- const ui = render13(
28865
- /* @__PURE__ */ jsx14(
28866
- LearningShowUI,
28867
- {
28868
- state: {
28869
- type: "error",
28870
- message: error62 instanceof Error ? error62.message : String(error62)
28871
- }
28872
- }
28873
- )
28874
- );
28875
- await ui.waitUntilExit();
28876
- return exitAfterFlush(1);
28877
28577
  }
28578
+ render14(/* @__PURE__ */ jsx14(LearningShowUI, { state: { type: "success", learning } })).unmount();
28878
28579
  }
28879
28580
 
28880
28581
  // src/commands/learnings/index.ts
@@ -28964,13 +28665,13 @@ import { Command as Command5 } from "commander";
28964
28665
  import { z as z7 } from "zod/v4";
28965
28666
 
28966
28667
  // src/commands/translate/plan.tsx
28967
- import fs7 from "fs/promises";
28668
+ import fs8 from "fs/promises";
28968
28669
  import path8 from "path";
28969
28670
  import { Spinner as Spinner13 } from "@inkjs/ui";
28970
- import { Box as Box15, render as render14, Text as Text15 } from "ink";
28671
+ import { Box as Box15, render as render15, Text as Text15 } from "ink";
28971
28672
 
28972
28673
  // src/commands/translate/task-generator.ts
28973
- import fs6 from "fs/promises";
28674
+ import fs7 from "fs/promises";
28974
28675
  import path7 from "path";
28975
28676
 
28976
28677
  // ../../node_modules/.pnpm/emoji-regex@10.6.0/node_modules/emoji-regex/index.mjs
@@ -29066,7 +28767,7 @@ function eastAsianWidth(codePoint, { ambiguousAsWide = false } = {}) {
29066
28767
  var import_fast_glob = __toESM(require_out4(), 1);
29067
28768
  var import_fast_glob2 = __toESM(require_out4(), 1);
29068
28769
  import path5 from "path";
29069
- import fs4 from "fs/promises";
28770
+ import fs5 from "fs/promises";
29070
28771
  import path23 from "path";
29071
28772
  import fs23 from "fs/promises";
29072
28773
  import path33 from "path";
@@ -29174,11 +28875,11 @@ function flattenValues(data) {
29174
28875
  const values = /* @__PURE__ */ new Map();
29175
28876
  function walk(obj, prefix) {
29176
28877
  for (const [key, value] of Object.entries(obj)) {
29177
- const fullKey3 = prefix ? `${prefix}.${key}` : key;
28878
+ const fullKey4 = prefix ? `${prefix}.${key}` : key;
29178
28879
  if (typeof value === "string") {
29179
- values.set(fullKey3, value);
28880
+ values.set(fullKey4, value);
29180
28881
  } else if (typeof value === "object" && value !== null) {
29181
- walk(value, fullKey3);
28882
+ walk(value, fullKey4);
29182
28883
  }
29183
28884
  }
29184
28885
  }
@@ -29280,23 +28981,23 @@ function parseTsContent(content) {
29280
28981
  let parsed;
29281
28982
  try {
29282
28983
  parsed = JSON.parse(jsonString);
29283
- } catch (jsonError2) {
29284
- if (jsonError2 instanceof SyntaxError) {
29285
- const posMatch = jsonError2.message.match(/position\s+(\d+)/);
28984
+ } catch (jsonError) {
28985
+ if (jsonError instanceof SyntaxError) {
28986
+ const posMatch = jsonError.message.match(/position\s+(\d+)/);
29286
28987
  const pos = posMatch ? Number(posMatch[1]) : -1;
29287
28988
  const start = Math.max(0, pos - 40);
29288
28989
  const end = Math.min(jsonString.length, pos + 40);
29289
28990
  const snippet = jsonString.slice(start, end);
29290
28991
  const pointer = pos >= 0 ? " ".repeat(Math.min(pos - start, 40)) + "^" : "";
29291
28992
  throw new SyntaxError(
29292
- `${jsonError2.message}
28993
+ `${jsonError.message}
29293
28994
  --- context around position ${pos} ---
29294
28995
  ${snippet}
29295
28996
  ${pointer}`,
29296
- { cause: jsonError2 }
28997
+ { cause: jsonError }
29297
28998
  );
29298
28999
  }
29299
- throw jsonError2;
29000
+ throw jsonError;
29300
29001
  }
29301
29002
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
29302
29003
  return {
@@ -30316,7 +30017,7 @@ var JsonAdapter = class extends BaseAdapter {
30316
30017
  for (const dir of COMMON_LOCALE_DIRS) {
30317
30018
  const dirPath = path23.join(baseDir, dir);
30318
30019
  try {
30319
- await fs4.access(dirPath);
30020
+ await fs5.access(dirPath);
30320
30021
  const files = await (0, import_fast_glob.default)("**/*.json", {
30321
30022
  cwd: dirPath,
30322
30023
  absolute: true,
@@ -30348,7 +30049,7 @@ var JsonAdapter = class extends BaseAdapter {
30348
30049
  async parse(filePath) {
30349
30050
  let content;
30350
30051
  try {
30351
- content = await fs4.readFile(filePath, "utf-8");
30052
+ content = await fs5.readFile(filePath, "utf-8");
30352
30053
  } catch (error62) {
30353
30054
  if (error62 instanceof Error && "code" in error62 && error62.code === "ENOENT") {
30354
30055
  throw new FileNotFoundError(filePath, error62);
@@ -30389,16 +30090,16 @@ var JsonAdapter = class extends BaseAdapter {
30389
30090
  let finalData = data;
30390
30091
  if (shouldMerge) {
30391
30092
  try {
30392
- const existingContent = await fs4.readFile(filePath, "utf-8");
30093
+ const existingContent = await fs5.readFile(filePath, "utf-8");
30393
30094
  const existing = parseJsonContent(existingContent);
30394
30095
  finalData = merge2(existing.data, data);
30395
30096
  } catch {
30396
30097
  }
30397
30098
  }
30398
30099
  const dir = path23.dirname(filePath);
30399
- await fs4.mkdir(dir, { recursive: true });
30100
+ await fs5.mkdir(dir, { recursive: true });
30400
30101
  const content = serializeJsonContent(finalData, { indent });
30401
- await fs4.writeFile(filePath, content, "utf-8");
30102
+ await fs5.writeFile(filePath, content, "utf-8");
30402
30103
  }
30403
30104
  };
30404
30105
  var IGNORED = [
@@ -31298,7 +30999,7 @@ function planLocale(input2) {
31298
30999
  }
31299
31000
 
31300
31001
  // src/commands/translate/project-files.ts
31301
- import fs5 from "fs/promises";
31002
+ import fs6 from "fs/promises";
31302
31003
  import path6 from "path";
31303
31004
  function fullPattern(scope) {
31304
31005
  return scope.subpath ? path6.posix.join(scope.subpath, scope.sourcePattern) : scope.sourcePattern;
@@ -31346,7 +31047,7 @@ function targetFileFor(source, targetLocale, files, scope) {
31346
31047
  }
31347
31048
  async function readTranslationFile(absolutePath, format) {
31348
31049
  try {
31349
- const content = await fs5.readFile(absolutePath, "utf-8");
31050
+ const content = await fs6.readFile(absolutePath, "utf-8");
31350
31051
  const file2 = await createAdapter(format).parse(absolutePath);
31351
31052
  return { data: file2.data, content };
31352
31053
  } catch (error62) {
@@ -31360,7 +31061,7 @@ async function readTranslationFile(absolutePath, format) {
31360
31061
  var HOLD_CHUNK = 2e3;
31361
31062
  async function loadTasksFromFile(tasksPath) {
31362
31063
  const abs = path7.resolve(process.cwd(), tasksPath);
31363
- const raw = await fs6.readFile(abs, "utf8");
31064
+ const raw = await fs7.readFile(abs, "utf8");
31364
31065
  const parsed = JSON.parse(raw);
31365
31066
  if (!Array.isArray(parsed)) throw new Error("tasks file must be an array");
31366
31067
  return parsed;
@@ -31453,22 +31154,12 @@ async function generateTasks(ctx, srcLocale, dstLocale, opts = {}) {
31453
31154
  import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
31454
31155
  async function loadPlanContext(cwd) {
31455
31156
  const config3 = readConfig(cwd);
31456
- if (!config3) {
31457
- throw new CliError(
31458
- "No tolkbot.json found.",
31459
- "Run 'tolkbot init' to create one."
31460
- );
31461
- }
31157
+ if (!config3) throw noConfigError();
31462
31158
  const client3 = await getAuthenticatedConvexClient();
31463
31159
  const project = await client3.query(api.projects.getProjectForCli, {
31464
31160
  projectId: config3.config.projectId
31465
31161
  });
31466
- if (!project) {
31467
- throw new CliError(
31468
- "Project not found.",
31469
- "Run 'tolkbot init' again to link this repository to a project."
31470
- );
31471
- }
31162
+ if (!project) throw projectNotFoundError();
31472
31163
  setTelemetryOrganization(project.organizationId);
31473
31164
  return {
31474
31165
  root: path8.dirname(config3.path),
@@ -31525,32 +31216,26 @@ function PlanUI({
31525
31216
  ] }) })
31526
31217
  ] });
31527
31218
  }
31528
- if (state.type === "success") {
31529
- const { plan } = state;
31530
- return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
31531
- /* @__PURE__ */ jsxs15(Box15, { children: [
31532
- /* @__PURE__ */ jsx15(Text15, { color: "green", children: "\u2713" }),
31533
- /* @__PURE__ */ jsx15(Text15, { children: " Translation plan generated" })
31534
- ] }),
31535
- /* @__PURE__ */ jsx15(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text15, { dimColor: true, children: [
31536
- src,
31537
- " \u2192 ",
31538
- dst,
31539
- ": ",
31540
- plan.tasks.length,
31541
- " task(s)"
31542
- ] }) }),
31543
- describePlan(plan).map((line) => /* @__PURE__ */ jsx15(Box15, { children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: line }) }, line)),
31544
- plan.tasks.length === 0 && /* @__PURE__ */ jsx15(Box15, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text15, { color: "yellow", children: [
31545
- "No translations needed - ",
31546
- dst,
31547
- " is up to date."
31548
- ] }) })
31549
- ] });
31550
- }
31219
+ const { plan } = state;
31551
31220
  return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
31552
- /* @__PURE__ */ jsx15(Text15, { color: "red", children: "\u2717 Planning failed" }),
31553
- /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: state.message })
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
+ ] }) })
31554
31239
  ] });
31555
31240
  }
31556
31241
  function planOutput(plan) {
@@ -31565,88 +31250,68 @@ function planOutput(plan) {
31565
31250
  async function runPlan(opts) {
31566
31251
  const cwd = process.cwd();
31567
31252
  if (opts.json) {
31568
- try {
31569
- const ctx = await loadPlanContext(cwd);
31570
- const src2 = opts.src ?? ctx.project.baseLanguage;
31571
- const plan = await planForLocale(ctx, src2, opts.dst, {
31572
- retranslateUntracked: opts.retranslateUntracked
31573
- });
31574
- track("cli_translate_completed", {
31575
- mode: "plan",
31576
- key_count: plan.tasks.length,
31577
- locale_count: 1,
31578
- job_count: 0,
31579
- json: true
31580
- });
31581
- jsonSuccess(planOutput(plan));
31582
- } catch (error62) {
31583
- jsonError(error62 instanceof Error ? error62.message : String(error62));
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 });
31584
31268
  }
31269
+ jsonSuccess(planOutput(plan2));
31585
31270
  }
31586
31271
  let src = opts.src ?? "";
31587
- let { unmount } = render14(
31272
+ const progress = renderTransient(
31588
31273
  /* @__PURE__ */ jsx15(PlanUI, { state: { type: "loading" }, src, dst: opts.dst })
31589
31274
  );
31275
+ let plan;
31590
31276
  try {
31591
31277
  const ctx = await loadPlanContext(cwd);
31592
31278
  src = opts.src ?? ctx.project.baseLanguage;
31593
- unmount();
31594
- const planningUi = render14(
31279
+ progress.rerender(
31595
31280
  /* @__PURE__ */ jsx15(PlanUI, { state: { type: "planning" }, src, dst: opts.dst })
31596
31281
  );
31597
- unmount = planningUi.unmount;
31598
- const plan = await planForLocale(ctx, src, opts.dst, {
31282
+ plan = await planForLocale(ctx, src, opts.dst, {
31599
31283
  retranslateUntracked: opts.retranslateUntracked
31600
31284
  });
31601
- unmount();
31602
- const successUi = render14(
31603
- /* @__PURE__ */ jsx15(
31604
- PlanUI,
31605
- {
31606
- state: { type: "success", plan },
31607
- src,
31608
- dst: opts.dst
31609
- }
31610
- )
31611
- );
31612
- unmount = successUi.unmount;
31613
- track("cli_translate_completed", {
31614
- mode: "plan",
31615
- key_count: plan.tasks.length,
31616
- locale_count: 1,
31617
- job_count: 0,
31618
- json: false
31619
- });
31620
- const tasksJson = JSON.stringify(plan.tasks, null, 2);
31621
- if (opts.write) {
31622
- const outputPath = path8.resolve(cwd, opts.write);
31623
- await fs7.writeFile(outputPath, tasksJson + "\n", "utf-8");
31624
- await successUi.waitUntilExit();
31625
- console.log(`
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(`
31626
31301
  Tasks written to: ${outputPath}`);
31627
- } else {
31628
- await successUi.waitUntilExit();
31629
- console.log("\n" + tasksJson);
31630
- }
31631
- } catch (error62) {
31632
- unmount();
31633
- const errorUi = render14(
31634
- /* @__PURE__ */ jsx15(
31635
- PlanUI,
31636
- {
31637
- state: {
31638
- type: "error",
31639
- message: error62 instanceof Error ? error62.message : String(error62)
31640
- },
31641
- src,
31642
- dst: opts.dst
31643
- }
31644
- )
31645
- );
31646
- await errorUi.waitUntilExit();
31647
- return exitAfterFlush(1);
31302
+ } else {
31303
+ console.log("\n" + JSON.stringify(plan.tasks, null, 2));
31648
31304
  }
31649
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
+ }
31650
31315
 
31651
31316
  // src/commands/translate/adopt.tsx
31652
31317
  var STAMP_CHUNK = 200;
@@ -31694,51 +31359,44 @@ async function runAdopt(opts) {
31694
31359
  const src = opts.src ?? ctx.project.baseLanguage;
31695
31360
  const locales = opts.all ? ctx.project.targetLanguages : opts.dst ? [opts.dst] : [];
31696
31361
  if (locales.length === 0) {
31697
- throw new CliError(
31698
- opts.all ? "No target languages configured for this project." : "Either --dst <locale> or --all must be provided."
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."
31699
31368
  );
31700
31369
  }
31701
- const outcomes = [];
31370
+ const outcomes2 = [];
31702
31371
  for (const dst of locales)
31703
- outcomes.push(await adoptLocale(ctx, src, dst));
31372
+ outcomes2.push(await adoptLocale(ctx, src, dst));
31704
31373
  track("cli_translate_completed", {
31705
31374
  mode: "adopt",
31706
- key_count: outcomes.reduce((n, o) => n + o.adopted, 0),
31707
- locale_count: outcomes.length,
31375
+ key_count: outcomes2.reduce((n, o) => n + o.adopted, 0),
31376
+ locale_count: outcomes2.length,
31708
31377
  job_count: 0,
31709
31378
  json: opts.json === true
31710
31379
  });
31711
- return outcomes;
31380
+ return outcomes2;
31712
31381
  };
31713
31382
  if (opts.json) {
31714
- try {
31715
- jsonSuccess({ adopted: await run() });
31716
- } catch (error62) {
31717
- jsonError(error62 instanceof Error ? error62.message : String(error62));
31718
- }
31383
+ jsonSuccess({ adopted: await run() });
31719
31384
  }
31720
- try {
31721
- const outcomes = await run();
31722
- for (const o of outcomes) {
31723
- console.log(
31724
- `\u2713 ${o.dstLocale}: adopted ${o.adopted} untracked translation(s)` + (o.skipped > 0 ? `, ${o.skipped} already tracked` : "")
31725
- );
31726
- }
31727
- } catch (error62) {
31728
- console.error(
31729
- `\u2717 ${error62 instanceof Error ? error62.message : String(error62)}`
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` : "")
31730
31389
  );
31731
- return exitAfterFlush(1);
31732
31390
  }
31733
31391
  }
31734
31392
 
31735
31393
  // src/commands/translate/apply.tsx
31736
31394
  import { useState as useState2 } from "react";
31737
- import { MultiSelect, Spinner as Spinner14 } from "@inkjs/ui";
31738
- import { Box as Box16, render as render15, Text as Text16 } from "ink";
31395
+ import { MultiSelect } from "@inkjs/ui";
31396
+ import { Box as Box16, render as render16, Text as Text16 } from "ink";
31739
31397
 
31740
31398
  // src/commands/translate/apply-core.ts
31741
- import fs8 from "fs/promises";
31399
+ import fs9 from "fs/promises";
31742
31400
  import path9 from "path";
31743
31401
  function fullKey2(namespace, key) {
31744
31402
  return namespace ? `${namespace}.${key}` : key;
@@ -31785,26 +31443,32 @@ async function applyTasks(input2) {
31785
31443
  for (const task of tasks) {
31786
31444
  const sourceKey = sourceKeyFor(task.key, baseKeys);
31787
31445
  const currentSource = sourceKey === null ? void 0 : getByPath(source.data, sourceKey);
31788
- if (typeof currentSource !== "string" || computeValueHash(currentSource) !== (task.hash ?? computeValueHash(task.src))) {
31446
+ if (typeof currentSource !== "string" || computeValueHash(currentSource) !== computeValueHash(task.src)) {
31789
31447
  refuse(task, "source_changed");
31790
31448
  continue;
31791
31449
  }
31450
+ const translatedValue = task.dst ?? "";
31792
31451
  const current = getByPath(data, task.key);
31793
31452
  const currentHash = typeof current === "string" ? computeValueHash(current) : null;
31794
31453
  if (task.expectedTargetHash !== void 0 && task.expectedTargetHash !== currentHash) {
31795
- refuse(task, "target_changed");
31454
+ refuse(
31455
+ task,
31456
+ current === translatedValue ? "already_applied" : "target_changed"
31457
+ );
31796
31458
  continue;
31797
31459
  }
31798
- const translatedValue = task.dst ?? "";
31799
31460
  data = setByPath(data, task.key, translatedValue);
31800
31461
  written.entries.push({
31801
31462
  key: task.key,
31802
31463
  namespace: task.namespace,
31803
31464
  baseValue: task.src,
31804
- translatedValue
31465
+ translatedValue,
31466
+ changesFile: current !== translatedValue
31805
31467
  });
31806
31468
  }
31807
31469
  if (written.entries.length === 0) continue;
31470
+ result.written.push(written);
31471
+ if (input2.dryRun) continue;
31808
31472
  await writeTranslationFile({
31809
31473
  format,
31810
31474
  targetPath,
@@ -31813,16 +31477,15 @@ async function applyTasks(input2) {
31813
31477
  existingContent: existing?.content,
31814
31478
  sourceContent: source.content
31815
31479
  });
31816
- result.written.push(written);
31817
31480
  }
31818
31481
  return result;
31819
31482
  }
31820
31483
  async function writeTranslationFile(input2) {
31821
31484
  const { targetPath, data } = input2;
31822
31485
  const model = input2.existingContent ?? input2.sourceContent;
31823
- await fs8.mkdir(path9.dirname(targetPath), { recursive: true });
31486
+ await fs9.mkdir(path9.dirname(targetPath), { recursive: true });
31824
31487
  if (input2.format === "json") {
31825
- await fs8.writeFile(
31488
+ await fs9.writeFile(
31826
31489
  targetPath,
31827
31490
  serializeJsonContent(data, detectJsonStyle(model)),
31828
31491
  "utf-8"
@@ -31837,63 +31500,18 @@ async function writeTranslationFile(input2) {
31837
31500
  exportName: input2.existingContent ? shape?.exportName ?? void 0 : localeExportIdentifier(input2.dstLocale),
31838
31501
  useDefaultExport: shape?.useDefaultExport ?? false
31839
31502
  });
31840
- await fs8.writeFile(targetPath, content, "utf-8");
31503
+ await fs9.writeFile(targetPath, content, "utf-8");
31841
31504
  }
31842
31505
 
31843
31506
  // src/commands/translate/apply.tsx
31844
31507
  import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
31845
- function ApplyUI({
31846
- jobs,
31847
- state,
31848
- onSubmit
31849
- }) {
31850
- const [selectedValues, setSelectedValues] = useState2([]);
31851
- if (state.status === "selecting") {
31852
- return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
31853
- /* @__PURE__ */ jsx16(Text16, { bold: true, children: "Select translation jobs to apply:" }),
31854
- /* @__PURE__ */ jsx16(Text16, { dimColor: true, children: "Space to select, Enter to confirm, Ctrl+C to cancel" }),
31855
- /* @__PURE__ */ jsx16(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx16(
31856
- MultiSelect,
31857
- {
31858
- options: jobs,
31859
- onChange: setSelectedValues,
31860
- onSubmit: () => onSubmit(selectedValues)
31861
- }
31862
- ) })
31863
- ] });
31864
- }
31865
- if (state.status === "applying") {
31866
- return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
31867
- /* @__PURE__ */ jsx16(Spinner14, {}),
31868
- /* @__PURE__ */ jsxs16(Text16, { children: [
31869
- "Applying job ",
31870
- state.currentJobIndex,
31871
- "/",
31872
- state.totalJobs
31873
- ] }),
31874
- state.currentJob && /* @__PURE__ */ jsx16(Text16, { dimColor: true, children: state.currentJob })
31875
- ] });
31876
- }
31877
- if (state.status === "success") {
31878
- return /* @__PURE__ */ jsx16(Box16, { flexDirection: "column", children: /* @__PURE__ */ jsxs16(Text16, { color: "green", children: [
31879
- "\u2713 Applied ",
31880
- state.keys,
31881
- " keys to ",
31882
- state.files,
31883
- " files from",
31884
- " ",
31885
- state.totalJobs,
31886
- " job(s)",
31887
- state.refused ? `, ${state.refused} not applied` : ""
31888
- ] }) });
31889
- }
31890
- return /* @__PURE__ */ jsx16(Box16, { flexDirection: "column", children: /* @__PURE__ */ jsxs16(Text16, { color: "red", children: [
31891
- "\u2717 Error: ",
31892
- state.message
31893
- ] }) });
31894
- }
31895
31508
  var STAMP_CHUNK2 = 200;
31896
- async function applyJob(ctx, jobId) {
31509
+ var RECENT_JOBS = 50;
31510
+ var APPLICABLE_STATUSES = /* @__PURE__ */ new Set(["succeeded", "partial", "failed"]);
31511
+ function fullKey3(namespace, key) {
31512
+ return namespace ? `${namespace}.${key}` : key;
31513
+ }
31514
+ async function readJob(ctx, jobId) {
31897
31515
  const { client: client3, project } = ctx;
31898
31516
  const organizationId2 = project.organizationId;
31899
31517
  const job = await client3.query(api.translations.getJobById, {
@@ -31901,17 +31519,28 @@ async function applyJob(ctx, jobId) {
31901
31519
  jobId
31902
31520
  });
31903
31521
  if (!job) {
31904
- throw new Error(`Job ${jobId} not found`);
31522
+ throw new CliError(
31523
+ `Translation job ${jobId} was not found in this project.`,
31524
+ void 0,
31525
+ "job_not_found"
31526
+ );
31905
31527
  }
31906
- if (job.status !== "succeeded" && job.status !== "partial" && job.status !== "failed") {
31907
- throw new Error(
31908
- `Job status is "${job.status}". Only completed jobs can be applied.`
31528
+ if (!APPLICABLE_STATUSES.has(job.status)) {
31529
+ throw new CliError(
31530
+ `Translation job ${jobId} is ${job.status}. Only a finished job can be applied.`,
31531
+ `Check it with 'tolkbot translate status --job-id ${jobId}'.`,
31532
+ "job_not_finished"
31909
31533
  );
31910
31534
  }
31911
31535
  const tasks = await client3.query(api.translations.listTasksByJobId, {
31912
31536
  organizationId: organizationId2,
31913
31537
  jobId
31914
31538
  });
31539
+ return { job, tasks };
31540
+ }
31541
+ async function applyJob(ctx, jobId) {
31542
+ const { client: client3, project } = ctx;
31543
+ const { job, tasks } = await readJob(ctx, jobId);
31915
31544
  const result = await applyTasks({
31916
31545
  root: ctx.root,
31917
31546
  scope: scopeOf(project),
@@ -31921,24 +31550,36 @@ async function applyJob(ctx, jobId) {
31921
31550
  });
31922
31551
  const locale = governanceLocale(project, job.dstLocale);
31923
31552
  for (const file2 of result.written) {
31924
- for (let i = 0; i < file2.entries.length; i += STAMP_CHUNK2) {
31553
+ const entries = file2.entries.map(
31554
+ ({ key, namespace, baseValue, translatedValue }) => ({
31555
+ key,
31556
+ namespace,
31557
+ baseValue,
31558
+ translatedValue
31559
+ })
31560
+ );
31561
+ for (let i = 0; i < entries.length; i += STAMP_CHUNK2) {
31925
31562
  await client3.mutation(api.localeKeyStates.stampAppliedForCli, {
31926
- organizationId: organizationId2,
31563
+ organizationId: project.organizationId,
31927
31564
  projectId: project._id,
31928
31565
  locale,
31929
31566
  pathTemplate: file2.pathTemplate,
31930
- entries: file2.entries.slice(i, i + STAMP_CHUNK2)
31567
+ entries: entries.slice(i, i + STAMP_CHUNK2)
31931
31568
  });
31932
31569
  }
31933
31570
  }
31934
31571
  return {
31572
+ dstLocale: job.dstLocale,
31935
31573
  files: result.written.length,
31936
31574
  keys: result.written.reduce((n, f) => n + f.entries.length, 0),
31937
- refused: result.refused.filter((r) => r.reason !== "not_translated")
31575
+ refused: result.refused.filter(
31576
+ (r) => r.reason !== "not_translated" && r.reason !== "already_applied"
31577
+ )
31938
31578
  };
31939
31579
  }
31940
31580
  var REFUSAL_TEXT = {
31941
31581
  not_translated: "the task did not finish",
31582
+ already_applied: "the file already holds it",
31942
31583
  no_source_file: "its source file is gone or unreadable",
31943
31584
  source_changed: "the source text changed after it was planned",
31944
31585
  target_unreadable: "the target file cannot be read",
@@ -31949,296 +31590,251 @@ function describeRefusals(refused) {
31949
31590
  (r) => ` ${r.key}: not applied, ${REFUSAL_TEXT[r.reason]}`
31950
31591
  );
31951
31592
  }
31952
- async function runApply(opts) {
31953
- const ctx = await loadPlanContext(process.cwd());
31954
- const { project, client: convexClient } = ctx;
31955
- const projectId = project._id;
31956
- const organizationId2 = project.organizationId;
31957
- if (opts.json) {
31958
- if (!opts.jobId || opts.jobId.length === 0) {
31959
- jsonError(
31960
- "--job-id is required when using --json (no interactive selection)"
31593
+ async function findUnappliedJobs(ctx) {
31594
+ 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
+ const wanted = /* @__PURE__ */ new Map();
31601
+ const plannedKeys = (srcLocale, dstLocale) => {
31602
+ const id = `${srcLocale}\0${dstLocale}`;
31603
+ let keys = wanted.get(id);
31604
+ if (!keys) {
31605
+ keys = planForLocale(ctx, srcLocale, dstLocale).then(
31606
+ (plan) => new Set(
31607
+ plan.tasks.map(
31608
+ (task) => fullKey3(task.namespace, task.key)
31609
+ )
31610
+ )
31961
31611
  );
31612
+ wanted.set(id, keys);
31962
31613
  }
31963
- try {
31964
- const applied = [];
31965
- for (const jobId of opts.jobId) {
31966
- applied.push({ jobId, ...await applyJob(ctx, jobId) });
31967
- }
31968
- track("cli_translate_completed", {
31969
- mode: "apply",
31970
- key_count: applied.reduce((n, a) => n + a.keys, 0),
31971
- locale_count: applied.length,
31972
- job_count: applied.length,
31973
- json: true
31974
- });
31975
- jsonSuccess({ applied });
31976
- } catch (error62) {
31977
- jsonError(error62 instanceof Error ? error62.message : String(error62));
31978
- }
31979
- }
31980
- if (opts.jobId && opts.jobId.length > 0) {
31981
- console.log(`Applying ${opts.jobId.length} job(s)...`);
31982
- let totalFiles = 0;
31983
- let totalKeys = 0;
31984
- let totalRefused = 0;
31985
- for (let i = 0; i < opts.jobId.length; i++) {
31986
- const jobId = opts.jobId[i];
31987
- if (!jobId) continue;
31988
- console.log(` [${i + 1}/${opts.jobId.length}] ${jobId}`);
31989
- const { files, keys, refused } = await applyJob(ctx, jobId);
31990
- totalFiles += files;
31991
- totalKeys += keys;
31992
- totalRefused += refused.length;
31993
- for (const line of describeRefusals(refused)) console.log(line);
31994
- }
31995
- track("cli_translate_completed", {
31996
- mode: "apply",
31997
- key_count: totalKeys,
31998
- locale_count: opts.jobId.length,
31999
- job_count: opts.jobId.length,
32000
- json: false
31614
+ return keys;
31615
+ };
31616
+ const unapplied = [];
31617
+ for (const job of jobs) {
31618
+ if (!APPLICABLE_STATUSES.has(job.status)) continue;
31619
+ const tasks = await client3.query(api.translations.listTasksByJobId, {
31620
+ organizationId: project.organizationId,
31621
+ jobId: job._id
31622
+ });
31623
+ const preview2 = await applyTasks({
31624
+ root: ctx.root,
31625
+ scope: scopeOf(project),
31626
+ srcLocale: job.srcLocale,
31627
+ dstLocale: job.dstLocale,
31628
+ tasks,
31629
+ dryRun: true
31630
+ });
31631
+ let pending2 = 0;
31632
+ for (const file2 of preview2.written) {
31633
+ for (const entry of file2.entries) {
31634
+ if (entry.changesFile || (await plannedKeys(job.srcLocale, job.dstLocale)).has(
31635
+ fullKey3(entry.namespace, entry.key)
31636
+ )) {
31637
+ pending2++;
31638
+ }
31639
+ }
31640
+ }
31641
+ if (pending2 === 0) continue;
31642
+ unapplied.push({
31643
+ jobId: job._id,
31644
+ srcLocale: job.srcLocale,
31645
+ dstLocale: job.dstLocale,
31646
+ createdAt: job._creationTime,
31647
+ pending: pending2
32001
31648
  });
32002
- console.log(
32003
- `\u2713 Applied ${totalKeys} keys to ${totalFiles} files from ${opts.jobId.length} job(s)` + (totalRefused > 0 ? `, ${totalRefused} not applied` : "")
32004
- );
32005
- return;
32006
31649
  }
32007
- const jobs = await convexClient.query(api.translations.listJobsByProject, {
32008
- organizationId: organizationId2,
32009
- projectId,
32010
- limit: 10
32011
- });
32012
- if (jobs.length === 0) {
32013
- console.log("No translation jobs found for this project.");
32014
- console.log('Run "tolkbot translate run" to create translation jobs.');
32015
- return;
31650
+ return unapplied;
31651
+ }
31652
+ function chooseJobs(unapplied) {
31653
+ if (unapplied.length === 0) return { kind: "none" };
31654
+ const byLocale = /* @__PURE__ */ new Map();
31655
+ for (const job of unapplied) {
31656
+ const group = byLocale.get(job.dstLocale) ?? [];
31657
+ group.push(job);
31658
+ byLocale.set(job.dstLocale, group);
32016
31659
  }
32017
- const applicableJobs = jobs.filter(
32018
- (job) => job.status === "succeeded" || job.status === "partial"
31660
+ const locales = [...byLocale].filter(([, group]) => group.length > 1).map(([locale]) => locale);
31661
+ if (locales.length > 0) return { kind: "ambiguous", locales };
31662
+ return {
31663
+ kind: "apply",
31664
+ jobs: [...unapplied].sort((a, b) => a.createdAt - b.createdAt)
31665
+ };
31666
+ }
31667
+ function describeJob(job) {
31668
+ return `${job.jobId} (${job.srcLocale} \u2192 ${job.dstLocale}, ${job.pending} key(s), ${new Date(job.createdAt).toLocaleString()})`;
31669
+ }
31670
+ function nothingToApply() {
31671
+ return new CliError(
31672
+ "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.",
31674
+ "nothing_to_apply"
32019
31675
  );
32020
- if (applicableJobs.length === 0) {
32021
- console.log("No successful translation jobs found.");
32022
- console.log(
32023
- "Recent jobs:\n" + jobs.map(
32024
- (j) => ` ${j.dstLocale} - ${j.status} (${new Date(j._creationTime).toLocaleString()})`
32025
- ).join("\n")
32026
- );
32027
- return;
32028
- }
32029
- const jobOptions = applicableJobs.map((job) => ({
32030
- label: `${job.srcLocale} \u2192 ${job.dstLocale} (${job.completed}/${job.total} tasks) - ${new Date(job._creationTime).toLocaleString()}`,
32031
- value: job._id
32032
- }));
32033
- let state = { status: "selecting" };
32034
- let selectedJobIds = [];
32035
- const { rerender, unmount } = render15(
31676
+ }
31677
+ function ambiguousJobs(unapplied, locales) {
31678
+ const waiting = unapplied.filter((job) => locales.includes(job.dstLocale));
31679
+ const newest = locales.map(
31680
+ (locale) => waiting.filter((job) => job.dstLocale === locale).reduce((a, b) => b.createdAt > a.createdAt ? b : a)
31681
+ );
31682
+ return new UsageError(
31683
+ `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(" ")}.`
31685
+ );
31686
+ }
31687
+ function JobPicker({
31688
+ jobs,
31689
+ onSubmit
31690
+ }) {
31691
+ 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(
31696
+ MultiSelect,
31697
+ {
31698
+ options: jobs.map((job) => ({
31699
+ label: describeJob(job),
31700
+ value: job.jobId
31701
+ })),
31702
+ onChange: setSelected,
31703
+ onSubmit: () => onSubmit(selected)
31704
+ }
31705
+ ) })
31706
+ ] });
31707
+ }
31708
+ async function pickJobs(jobs) {
31709
+ let chosen = [];
31710
+ const ui = render16(
32036
31711
  /* @__PURE__ */ jsx16(
32037
- ApplyUI,
31712
+ JobPicker,
32038
31713
  {
32039
- jobs: jobOptions,
32040
- state,
31714
+ jobs,
32041
31715
  onSubmit: (selected) => {
32042
- selectedJobIds = selected;
31716
+ chosen = selected;
31717
+ ui.unmount();
32043
31718
  }
32044
31719
  }
32045
31720
  )
32046
31721
  );
32047
- const updateState = (newState) => {
32048
- state = { ...state, ...newState };
32049
- rerender(
32050
- /* @__PURE__ */ jsx16(
32051
- ApplyUI,
32052
- {
32053
- jobs: jobOptions,
32054
- state,
32055
- onSubmit: (selected) => {
32056
- selectedJobIds = selected;
32057
- }
32058
- }
32059
- )
31722
+ await ui.waitUntilExit();
31723
+ return chosen;
31724
+ }
31725
+ async function resolveJobIds(ctx, json2) {
31726
+ const unapplied = await findUnappliedJobs(ctx);
31727
+ const choice = chooseJobs(unapplied);
31728
+ if (choice.kind === "none") throw nothingToApply();
31729
+ if (choice.kind === "apply") return choice.jobs.map((job) => job.jobId);
31730
+ if (json2 || !isInteractive()) {
31731
+ throw ambiguousJobs(unapplied, choice.locales);
31732
+ }
31733
+ const picked = await pickJobs(unapplied);
31734
+ if (picked.length === 0) {
31735
+ throw new UsageError(
31736
+ "No job selected.",
31737
+ "Pass --job-id with the job(s) to apply."
32060
31738
  );
32061
- };
32062
- await new Promise((resolve) => {
32063
- const checkInterval = setInterval(() => {
32064
- if (selectedJobIds.length > 0 && state.status === "selecting") {
32065
- clearInterval(checkInterval);
32066
- resolve();
32067
- }
32068
- }, 100);
32069
- });
32070
- if (selectedJobIds.length === 0) {
32071
- unmount();
32072
- console.log("No jobs selected.");
32073
- return;
32074
31739
  }
32075
- updateState({
32076
- status: "applying",
32077
- totalJobs: selectedJobIds.length
32078
- });
32079
- try {
32080
- let totalFiles = 0;
32081
- let totalKeys = 0;
32082
- let totalRefused = 0;
32083
- const refusals = [];
32084
- for (let i = 0; i < selectedJobIds.length; i++) {
32085
- const jobId = selectedJobIds[i];
32086
- if (!jobId) continue;
32087
- updateState({
32088
- currentJob: jobId,
32089
- currentJobIndex: i + 1
32090
- });
32091
- const { files, keys, refused } = await applyJob(ctx, jobId);
32092
- totalFiles += files;
32093
- totalKeys += keys;
32094
- totalRefused += refused.length;
32095
- refusals.push(...describeRefusals(refused));
32096
- }
32097
- updateState({
32098
- status: "success",
32099
- files: totalFiles,
32100
- keys: totalKeys,
32101
- refused: totalRefused
32102
- });
31740
+ return picked;
31741
+ }
31742
+ async function runApply(opts) {
31743
+ const ctx = await loadPlanContext(process.cwd());
31744
+ const json2 = opts.json ?? false;
31745
+ const jobIds = opts.jobId && opts.jobId.length > 0 ? opts.jobId : await resolveJobIds(ctx, json2);
31746
+ if (json2) {
31747
+ const applied = [];
31748
+ for (const jobId of jobIds) {
31749
+ applied.push({ jobId, ...await applyJob(ctx, jobId) });
31750
+ }
32103
31751
  track("cli_translate_completed", {
32104
31752
  mode: "apply",
32105
- key_count: totalKeys,
32106
- locale_count: selectedJobIds.length,
32107
- job_count: selectedJobIds.length,
32108
- json: false
32109
- });
32110
- await new Promise((resolve) => setTimeout(resolve, 2e3));
32111
- unmount();
32112
- for (const line of refusals) console.log(line);
32113
- } catch (err) {
32114
- updateState({
32115
- status: "error",
32116
- message: err instanceof Error ? err.message : String(err)
31753
+ key_count: applied.reduce((n, a) => n + a.keys, 0),
31754
+ locale_count: new Set(applied.map((a) => a.dstLocale)).size,
31755
+ job_count: applied.length,
31756
+ json: true
32117
31757
  });
32118
- await new Promise((resolve) => setTimeout(resolve, 2e3));
32119
- unmount();
32120
- return exitAfterFlush(1);
32121
- }
31758
+ jsonSuccess({ applied });
31759
+ }
31760
+ console.log(`Applying ${jobIds.length} job(s)...`);
31761
+ let totalFiles = 0;
31762
+ let totalKeys = 0;
31763
+ let totalRefused = 0;
31764
+ const locales = /* @__PURE__ */ new Set();
31765
+ for (const [i, jobId] of jobIds.entries()) {
31766
+ console.log(` [${i + 1}/${jobIds.length}] ${jobId}`);
31767
+ const { dstLocale, files, keys, refused } = await applyJob(ctx, jobId);
31768
+ locales.add(dstLocale);
31769
+ totalFiles += files;
31770
+ totalKeys += keys;
31771
+ totalRefused += refused.length;
31772
+ for (const line of describeRefusals(refused)) console.log(line);
31773
+ }
31774
+ track("cli_translate_completed", {
31775
+ mode: "apply",
31776
+ key_count: totalKeys,
31777
+ locale_count: locales.size,
31778
+ job_count: jobIds.length,
31779
+ json: false
31780
+ });
31781
+ console.log(
31782
+ `\u2713 Applied ${totalKeys} keys to ${totalFiles} files from ${jobIds.length} job(s)` + (totalRefused > 0 ? `, ${totalRefused} not applied` : "")
31783
+ );
32122
31784
  }
32123
31785
 
32124
31786
  // src/commands/translate/cancel.tsx
32125
- import { Spinner as Spinner15 } from "@inkjs/ui";
32126
- import { Box as Box17, render as render16, Text as Text17 } from "ink";
31787
+ import { Spinner as Spinner14 } from "@inkjs/ui";
31788
+ import { Box as Box17, render as render17, Text as Text17 } from "ink";
32127
31789
  import { jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
32128
31790
  function CancelUI({ state }) {
32129
31791
  if (state.type === "cancelling") {
32130
31792
  return /* @__PURE__ */ jsxs17(Box17, { children: [
32131
- /* @__PURE__ */ jsx17(Spinner15, {}),
31793
+ /* @__PURE__ */ jsx17(Spinner14, {}),
32132
31794
  /* @__PURE__ */ jsx17(Text17, { children: " Cancelling job\u2026" })
32133
31795
  ] });
32134
31796
  }
32135
- if (state.type === "success") {
32136
- return /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
32137
- /* @__PURE__ */ jsxs17(Box17, { children: [
32138
- /* @__PURE__ */ jsx17(Text17, { color: "green", children: "\u2713" }),
32139
- /* @__PURE__ */ jsx17(Text17, { children: " Job cancelled successfully" })
32140
- ] }),
32141
- /* @__PURE__ */ jsx17(Box17, { marginTop: 1, children: /* @__PURE__ */ jsxs17(Text17, { dimColor: true, children: [
32142
- "Job ID: ",
32143
- state.jobId
32144
- ] }) })
32145
- ] });
32146
- }
32147
31797
  return /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
32148
- /* @__PURE__ */ jsx17(Text17, { color: "red", children: "\u2717 Failed to cancel job" }),
32149
- /* @__PURE__ */ jsx17(Text17, { dimColor: true, children: state.message })
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
+ ] }) })
32150
31806
  ] });
32151
31807
  }
31808
+ async function cancelJob(jobId) {
31809
+ const { project } = await loadPlanContext(process.cwd());
31810
+ const client3 = await getAuthenticatedConvexClient();
31811
+ await client3.action(api.translations.cancelJob, {
31812
+ organizationId: project.organizationId,
31813
+ jobId
31814
+ });
31815
+ }
32152
31816
  async function runCancel(opts) {
32153
31817
  if (opts.json) {
32154
- try {
32155
- if (!opts.jobId) {
32156
- jsonError("--job-id is required");
32157
- }
32158
- const convexClient = await getAuthenticatedConvexClient();
32159
- const baseDir = process.cwd();
32160
- const config3 = readConfig(baseDir);
32161
- if (!config3) {
32162
- jsonError(
32163
- "No tolkbot.json found. Run 'tolkbot init' to create one."
32164
- );
32165
- }
32166
- const project = await convexClient.query(
32167
- api.projects.getProjectForCli,
32168
- { projectId: config3.config.projectId }
32169
- );
32170
- if (!project) {
32171
- jsonError(
32172
- "Project not found. Please run 'tolkbot init' again."
32173
- );
32174
- }
32175
- setTelemetryOrganization(project.organizationId);
32176
- await convexClient.action(api.translations.cancelJob, {
32177
- organizationId: project.organizationId,
32178
- jobId: opts.jobId
32179
- });
32180
- jsonSuccess({ jobId: opts.jobId, ok: true });
32181
- } catch (error62) {
32182
- jsonError(error62 instanceof Error ? error62.message : String(error62));
32183
- }
31818
+ await cancelJob(opts.jobId);
31819
+ jsonSuccess({ jobId: opts.jobId, ok: true });
32184
31820
  }
32185
- let { unmount } = render16(/* @__PURE__ */ jsx17(CancelUI, { state: { type: "cancelling" } }));
31821
+ const { unmount } = renderTransient(
31822
+ /* @__PURE__ */ jsx17(CancelUI, { state: { type: "cancelling" } })
31823
+ );
32186
31824
  try {
32187
- if (!opts.jobId) {
32188
- throw new Error("--job-id is required");
32189
- }
32190
- const convexClient = await getAuthenticatedConvexClient();
32191
- const baseDir = process.cwd();
32192
- const config3 = readConfig(baseDir);
32193
- if (!config3) {
32194
- throw new Error(
32195
- "No tolkbot.json found. Run 'tolkbot init' to create one."
32196
- );
32197
- }
32198
- const project = await convexClient.query(
32199
- api.projects.getProjectForCli,
32200
- {
32201
- projectId: config3.config.projectId
32202
- }
32203
- );
32204
- if (!project) {
32205
- throw new Error(
32206
- "Project not found. Please run 'tolkbot init' again."
32207
- );
32208
- }
32209
- setTelemetryOrganization(project.organizationId);
32210
- await convexClient.action(api.translations.cancelJob, {
32211
- organizationId: project.organizationId,
32212
- jobId: opts.jobId
32213
- });
32214
- unmount();
32215
- const successUi = render16(
32216
- /* @__PURE__ */ jsx17(CancelUI, { state: { type: "success", jobId: opts.jobId } })
32217
- );
32218
- unmount = successUi.unmount;
32219
- await successUi.waitUntilExit();
32220
- } catch (error62) {
31825
+ await cancelJob(opts.jobId);
31826
+ } finally {
32221
31827
  unmount();
32222
- const errorUi = render16(
32223
- /* @__PURE__ */ jsx17(
32224
- CancelUI,
32225
- {
32226
- state: {
32227
- type: "error",
32228
- message: error62 instanceof Error ? error62.message : String(error62)
32229
- }
32230
- }
32231
- )
32232
- );
32233
- await errorUi.waitUntilExit();
32234
- return exitAfterFlush(1);
32235
31828
  }
31829
+ render17(
31830
+ /* @__PURE__ */ jsx17(CancelUI, { state: { type: "success", jobId: opts.jobId } })
31831
+ ).unmount();
32236
31832
  }
32237
31833
 
32238
31834
  // src/commands/translate/edit.tsx
32239
31835
  import path11 from "path";
32240
- import { Spinner as Spinner16 } from "@inkjs/ui";
32241
- import { Box as Box18, render as render17, Text as Text18 } from "ink";
31836
+ import { Spinner as Spinner15 } from "@inkjs/ui";
31837
+ import { Box as Box18, render as render18, Text as Text18 } from "ink";
32242
31838
 
32243
31839
  // src/utils/locale-files.ts
32244
31840
  import path10 from "path";
@@ -32280,51 +31876,44 @@ import { jsx as jsx18, jsxs as jsxs18 } from "react/jsx-runtime";
32280
31876
  function EditUI({ state }) {
32281
31877
  if (state.type === "editing") {
32282
31878
  return /* @__PURE__ */ jsxs18(Box18, { children: [
32283
- /* @__PURE__ */ jsx18(Spinner16, {}),
31879
+ /* @__PURE__ */ jsx18(Spinner15, {}),
32284
31880
  /* @__PURE__ */ jsx18(Text18, { children: " Editing translation\u2026" })
32285
31881
  ] });
32286
31882
  }
32287
- if (state.type === "success") {
32288
- return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
32289
- /* @__PURE__ */ jsxs18(Box18, { children: [
32290
- /* @__PURE__ */ jsx18(Text18, { color: "green", children: "\u2713" }),
32291
- /* @__PURE__ */ jsxs18(Text18, { children: [
32292
- " ",
32293
- "Updated ",
32294
- /* @__PURE__ */ jsx18(Text18, { bold: true, children: state.key })
32295
- ] })
32296
- ] }),
32297
- /* @__PURE__ */ jsx18(Box18, { paddingLeft: 2, children: /* @__PURE__ */ jsx18(Text18, { dimColor: true, children: state.value }) }),
32298
- /* @__PURE__ */ jsx18(Box18, { marginTop: 1, children: /* @__PURE__ */ jsxs18(Text18, { dimColor: true, children: [
32299
- "File: ",
32300
- state.filePath
32301
- ] }) })
32302
- ] });
32303
- }
32304
31883
  return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
32305
- /* @__PURE__ */ jsx18(Text18, { color: "red", children: "\u2717 Failed to edit translation" }),
32306
- /* @__PURE__ */ jsx18(Text18, { dimColor: true, children: state.message })
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
+ ] }) })
32307
31897
  ] });
32308
31898
  }
32309
31899
  async function performEdit(opts) {
32310
31900
  const baseDir = process.cwd();
32311
31901
  const config3 = readConfig(baseDir);
32312
- if (!config3) {
32313
- throw new Error(
32314
- "No tolkbot.json found. Run 'tolkbot init' to create one."
32315
- );
32316
- }
31902
+ if (!config3) throw noConfigError();
32317
31903
  const localeFiles = await findLocaleFiles(baseDir);
32318
31904
  const files = localeFiles.get(opts.locale);
32319
31905
  if (!files || files.length === 0) {
32320
- throw new Error(
32321
- `No translation files found for locale "${opts.locale}". Available locales: ${[...localeFiles.keys()].join(", ") || "none"}`
31906
+ throw new CliError(
31907
+ `No translation files found for locale "${opts.locale}".`,
31908
+ `Available locales: ${[...localeFiles.keys()].join(", ") || "none"}.`,
31909
+ "no_translation_files"
32322
31910
  );
32323
31911
  }
32324
31912
  const targetFile = resolveFileForKey(files, opts.key, opts.namespace);
32325
31913
  if (!targetFile) {
32326
- throw new Error(
32327
- `Could not determine target file for key "${opts.key}" in locale "${opts.locale}".`
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."
32328
31917
  );
32329
31918
  }
32330
31919
  const keyInFile = resolveKeyInFile(
@@ -32351,47 +31940,199 @@ async function performEdit(opts) {
32351
31940
  }
32352
31941
  async function runEdit(opts) {
32353
31942
  if (opts.json) {
32354
- try {
32355
- const result = await performEdit(opts);
32356
- jsonSuccess({
32357
- file: result.filePath,
32358
- key: result.key,
32359
- value: result.value
32360
- });
32361
- } catch (error62) {
32362
- jsonError(error62 instanceof Error ? error62.message : String(error62));
32363
- }
31943
+ const result2 = await performEdit(opts);
31944
+ jsonSuccess({
31945
+ file: result2.filePath,
31946
+ key: result2.key,
31947
+ value: result2.value
31948
+ });
32364
31949
  }
32365
- let { unmount } = render17(/* @__PURE__ */ jsx18(EditUI, { state: { type: "editing" } }));
31950
+ const { unmount } = renderTransient(/* @__PURE__ */ jsx18(EditUI, { state: { type: "editing" } }));
31951
+ let result;
32366
31952
  try {
32367
- const result = await performEdit(opts);
32368
- unmount();
32369
- const ui = render17(/* @__PURE__ */ jsx18(EditUI, { state: { type: "success", ...result } }));
32370
- unmount = ui.unmount;
32371
- await ui.waitUntilExit();
32372
- } catch (error62) {
31953
+ result = await performEdit(opts);
31954
+ } finally {
32373
31955
  unmount();
32374
- const ui = render17(
32375
- /* @__PURE__ */ jsx18(
32376
- EditUI,
32377
- {
32378
- state: {
32379
- type: "error",
32380
- message: error62 instanceof Error ? error62.message : String(error62)
32381
- }
32382
- }
32383
- )
32384
- );
32385
- await ui.waitUntilExit();
32386
- return exitAfterFlush(1);
32387
31956
  }
31957
+ render18(/* @__PURE__ */ jsx18(EditUI, { state: { type: "success", ...result } })).unmount();
32388
31958
  }
32389
31959
 
32390
31960
  // src/commands/translate/run.tsx
32391
- import { render as render18 } from "ink";
31961
+ import { render as render19 } from "ink";
31962
+
31963
+ // ../shared/src/conditions/index.ts
31964
+ var BILLING_BLOCKED = "BILLING_BLOCKED";
31965
+
31966
+ // ../shared/src/projects/localeSetup.ts
31967
+ var LOCALE_SETUP_STALE_MS = 15 * 60 * 1e3;
31968
+
31969
+ // ../shared/src/projects/index.ts
31970
+ var PROJECT_ARCHIVED = "PROJECT_ARCHIVED";
31971
+
31972
+ // src/utils/error-codes.ts
31973
+ var EXIT_CODES = {
31974
+ success: 0,
31975
+ failure: 1,
31976
+ usage: 2,
31977
+ auth: 3,
31978
+ billing: 4,
31979
+ network: 5
31980
+ };
31981
+ var ERROR_CODES = {
31982
+ // 1: the command ran and failed
31983
+ failed: EXIT_CODES.failure,
31984
+ internal: EXIT_CODES.failure,
31985
+ no_config: EXIT_CODES.failure,
31986
+ project_not_found: EXIT_CODES.failure,
31987
+ repository_not_connected: EXIT_CODES.failure,
31988
+ no_translation_files: EXIT_CODES.failure,
31989
+ no_target_languages: EXIT_CODES.failure,
31990
+ not_found: EXIT_CODES.failure,
31991
+ job_not_found: EXIT_CODES.failure,
31992
+ job_not_finished: EXIT_CODES.failure,
31993
+ job_failed: EXIT_CODES.failure,
31994
+ job_cancelled: EXIT_CODES.failure,
31995
+ job_timed_out: EXIT_CODES.failure,
31996
+ nothing_to_apply: EXIT_CODES.failure,
31997
+ validation_failed: EXIT_CODES.failure,
31998
+ login_failed: EXIT_CODES.failure,
31999
+ // 2: usage
32000
+ usage: EXIT_CODES.usage,
32001
+ // 3: not authenticated, or not allowed
32002
+ not_authenticated: EXIT_CODES.auth,
32003
+ token_expired: EXIT_CODES.auth,
32004
+ token_rejected: EXIT_CODES.auth,
32005
+ signed_out: EXIT_CODES.auth,
32006
+ project_archived: EXIT_CODES.auth,
32007
+ // 4: billing
32008
+ no_subscription: EXIT_CODES.billing,
32009
+ out_of_credits: EXIT_CODES.billing,
32010
+ payment_failed: EXIT_CODES.billing,
32011
+ // 5: could not reach TolkBot
32012
+ network: EXIT_CODES.network
32013
+ };
32014
+ function exitCodeFor(code) {
32015
+ return ERROR_CODES[code];
32016
+ }
32017
+
32018
+ // src/utils/report.ts
32019
+ function convexErrorData(error62) {
32020
+ if (error62 instanceof Error && error62.name === "ConvexError" && "data" in error62) {
32021
+ return { data: error62.data };
32022
+ }
32023
+ return null;
32024
+ }
32025
+ function billingHint() {
32026
+ return `Manage billing at ${env.TOLKBOT_WEB_URL}/subscription.`;
32027
+ }
32028
+ var BILLING_MESSAGES = [
32029
+ { startsWith: "No billing account", code: "no_subscription" },
32030
+ { startsWith: "Insufficient credits", code: "out_of_credits" },
32031
+ { startsWith: "Overage limit", code: "out_of_credits" }
32032
+ ];
32033
+ function fromConvexError(data) {
32034
+ if (typeof data === "string") {
32035
+ const billing = BILLING_MESSAGES.find(
32036
+ (m) => data.startsWith(m.startsWith)
32037
+ );
32038
+ if (billing) return new CliError(data, billingHint(), billing.code);
32039
+ return new CliError(data);
32040
+ }
32041
+ if (typeof data !== "object" || data === null) return null;
32042
+ const { code, message } = data;
32043
+ const text = typeof message === "string" ? message : describeError(data);
32044
+ if (code === PROJECT_ARCHIVED) {
32045
+ return new CliError(text, void 0, "project_archived");
32046
+ }
32047
+ if (code === BILLING_BLOCKED) {
32048
+ return new CliError(text, billingHint(), "payment_failed");
32049
+ }
32050
+ return typeof message === "string" ? new CliError(message) : null;
32051
+ }
32052
+ function serverMessage(message) {
32053
+ const unwrapped = message.replace(
32054
+ /^\[Request ID: [^\]]*\] Server Error\s*/,
32055
+ ""
32056
+ );
32057
+ const uncaught = /Uncaught (?:\w*Error: )?([\s\S]*?)(?:\n\s+at |$)/.exec(
32058
+ unwrapped
32059
+ );
32060
+ return (uncaught?.[1] ?? unwrapped).trim();
32061
+ }
32062
+ function argumentError(message) {
32063
+ if (!message.includes("ArgumentValidationError")) return null;
32064
+ const path12 = /Path: \.?(\S+)/.exec(message)?.[1];
32065
+ const value = /Value: (.+)/.exec(message)?.[1];
32066
+ return new UsageError(
32067
+ path12 && value ? `TolkBot refused ${value} as ${path12}.` : "TolkBot refused the arguments.",
32068
+ "Check the id or value passed; list ids with the command's list subcommand."
32069
+ );
32070
+ }
32071
+ function toCliError(error62) {
32072
+ if (error62 instanceof CliError) return error62;
32073
+ const convex = convexErrorData(error62);
32074
+ if (convex) {
32075
+ const known = fromConvexError(convex.data);
32076
+ if (known) return known;
32077
+ }
32078
+ if (isUnauthenticatedError(error62)) {
32079
+ return new CliError(
32080
+ "Not authenticated.",
32081
+ "Run 'tolkbot auth login', or set $TOLKBOT_TOKEN for CI and headless environments.",
32082
+ "not_authenticated"
32083
+ );
32084
+ }
32085
+ if (isNetworkError(error62)) {
32086
+ return new CliError(
32087
+ "Could not reach TolkBot.",
32088
+ "Check the network connection and try again.",
32089
+ "network"
32090
+ );
32091
+ }
32092
+ const message = describeError(error62);
32093
+ const refused = argumentError(message);
32094
+ if (refused) return refused;
32095
+ if (message.includes("Server Error")) {
32096
+ return new CliError(serverMessage(message));
32097
+ }
32098
+ const internal = new CliError(message, void 0, "internal");
32099
+ if (error62 instanceof Error) internal.stack = error62.stack;
32100
+ return internal;
32101
+ }
32102
+ function errorPayload(error62) {
32103
+ return {
32104
+ error: {
32105
+ code: error62.code,
32106
+ message: error62.baseMessage,
32107
+ hint: error62.hint ?? null
32108
+ }
32109
+ };
32110
+ }
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);
32121
+ }
32122
+ }
32123
+ const code = exitCodeFor(cli.code);
32124
+ process.exitCode = code;
32125
+ return code;
32126
+ }
32127
+ function fail(error62) {
32128
+ const cli = toCliError(error62);
32129
+ throw requestExit(exitCodeFor(cli.code), () => {
32130
+ reportError(cli);
32131
+ });
32132
+ }
32392
32133
 
32393
32134
  // src/commands/translate/run-ui.tsx
32394
- import { Spinner as Spinner17 } from "@inkjs/ui";
32135
+ import { Spinner as Spinner16 } from "@inkjs/ui";
32395
32136
  import { Box as Box19, Text as Text19 } from "ink";
32396
32137
  import { jsx as jsx19, jsxs as jsxs19 } from "react/jsx-runtime";
32397
32138
  function JobLink({ jobId }) {
@@ -32421,7 +32162,7 @@ function JobRow({ srcLocale, job }) {
32421
32162
  }
32422
32163
  if (status === "generating") {
32423
32164
  return /* @__PURE__ */ jsxs19(Box19, { children: [
32424
- /* @__PURE__ */ jsx19(Spinner17, {}),
32165
+ /* @__PURE__ */ jsx19(Spinner16, {}),
32425
32166
  /* @__PURE__ */ jsxs19(Text19, { children: [
32426
32167
  " ",
32427
32168
  srcLocale,
@@ -32436,7 +32177,7 @@ function JobRow({ srcLocale, job }) {
32436
32177
  const percent = progress.total > 0 ? Math.round(processed / progress.total * 100) : 0;
32437
32178
  return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
32438
32179
  /* @__PURE__ */ jsxs19(Box19, { children: [
32439
- /* @__PURE__ */ jsx19(Spinner17, {}),
32180
+ /* @__PURE__ */ jsx19(Spinner16, {}),
32440
32181
  /* @__PURE__ */ jsxs19(Text19, { children: [
32441
32182
  " ",
32442
32183
  srcLocale,
@@ -32561,13 +32302,31 @@ function isJobFinished(status) {
32561
32302
  function isJobUnsuccessful(status) {
32562
32303
  return status === "failed" || status === "cancelled";
32563
32304
  }
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"
32311
+ );
32312
+ }
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
+ );
32319
+ }
32320
+ return null;
32321
+ }
32564
32322
  var JOB_WAIT_TIMEOUT_MS = 60 * 6e4;
32565
32323
  var JOB_POLL_MS = 500;
32566
32324
  var JobWaitTimeoutError = class extends CliError {
32567
32325
  constructor(jobId, timeoutMs) {
32568
32326
  super(
32569
32327
  `Translation job ${jobId} was still running after ${Math.round(timeoutMs / 6e4)} minutes, so the CLI stopped waiting.`,
32570
- `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.`
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"
32571
32330
  );
32572
32331
  this.jobId = jobId;
32573
32332
  this.name = "JobWaitTimeoutError";
@@ -32583,7 +32342,11 @@ async function waitForJob(jobId, read, opts = {}) {
32583
32342
  for (; ; ) {
32584
32343
  const summary = await read();
32585
32344
  if (!summary) {
32586
- throw new CliError(`Translation job ${jobId} was not found.`);
32345
+ throw new CliError(
32346
+ `Translation job ${jobId} was not found.`,
32347
+ void 0,
32348
+ "job_not_found"
32349
+ );
32587
32350
  }
32588
32351
  opts.onProgress?.(summary);
32589
32352
  if (isJobFinished(summary.status)) return summary;
@@ -32641,67 +32404,68 @@ async function runTranslate(opts) {
32641
32404
  if (opts.all) {
32642
32405
  dstLocales = project.targetLanguages;
32643
32406
  if (dstLocales.length === 0) {
32644
- throw new Error(
32645
- `No target languages configured.
32646
- Please configure them in the project settings at:
32647
- ${env.TOLKBOT_WEB_URL}/projects/${project.slug}/settings`
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"
32648
32411
  );
32649
32412
  }
32650
32413
  } else if (opts.dst) {
32651
32414
  dstLocales = [opts.dst];
32652
32415
  } else {
32653
- throw new Error("Either --dst or --all must be provided");
32416
+ throw new UsageError(
32417
+ "Either --dst <locale> or --all must be provided."
32418
+ );
32654
32419
  }
32655
32420
  const planOpts = { retranslateUntracked: opts.retranslateUntracked };
32656
32421
  const tasksFor = async (dstLocale) => opts.tasks ? { tasks: await loadTasksFromFile(opts.tasks), plan: void 0 } : await generateTasks(ctx, srcLocale, dstLocale, planOpts);
32657
32422
  if (opts.json) {
32658
32423
  const results = [];
32659
- try {
32660
- for (const dstLocale of dstLocales) {
32661
- const { tasks, plan } = await tasksFor(dstLocale);
32662
- const summary = plan ? summarize(plan) : void 0;
32663
- if (tasks.length === 0) {
32664
- results.push({
32665
- dstLocale,
32666
- status: "up-to-date",
32667
- plan: summary
32668
- });
32669
- continue;
32670
- }
32671
- try {
32672
- const { jobId, summary: job } = await runTranslationJob({
32673
- projectId,
32674
- organizationId: project.organizationId,
32675
- framework,
32676
- srcLocale,
32677
- dstLocale,
32678
- tasks
32679
- });
32680
- results.push({
32681
- dstLocale,
32682
- jobId,
32683
- status: job.status,
32684
- completed: job.completed,
32685
- failed: job.failed,
32686
- total: job.total,
32687
- error: job.status === "cancelled" ? "The job was cancelled." : void 0,
32688
- plan: summary
32689
- });
32690
- } catch (error62) {
32691
- if (!(error62 instanceof JobWaitTimeoutError)) throw error62;
32692
- results.push({
32693
- dstLocale,
32694
- jobId: error62.jobId,
32695
- status: "timed-out",
32696
- error: error62.message,
32697
- plan: summary
32698
- });
32699
- break;
32700
- }
32701
- if (results.at(-1)?.status === "cancelled") break;
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;
32428
+ if (tasks.length === 0) {
32429
+ results.push({
32430
+ dstLocale,
32431
+ status: "up-to-date",
32432
+ plan: summary
32433
+ });
32434
+ continue;
32702
32435
  }
32703
- } catch (error62) {
32704
- jsonError(error62 instanceof Error ? error62.message : String(error62));
32436
+ try {
32437
+ const { jobId, summary: job } = await runTranslationJob({
32438
+ projectId,
32439
+ organizationId: project.organizationId,
32440
+ framework,
32441
+ srcLocale,
32442
+ dstLocale,
32443
+ tasks
32444
+ });
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
+ } 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
32464
+ });
32465
+ failure3 ??= error62;
32466
+ break;
32467
+ }
32468
+ if (results.at(-1)?.status === "cancelled") break;
32705
32469
  }
32706
32470
  track("cli_translate_completed", {
32707
32471
  mode: "run",
@@ -32710,18 +32474,15 @@ ${env.TOLKBOT_WEB_URL}/projects/${project.slug}/settings`
32710
32474
  job_count: results.filter((r) => r.jobId).length,
32711
32475
  json: true
32712
32476
  });
32713
- const ok = results.every(
32714
- (r) => r.status !== "timed-out" && !isJobUnsuccessful(r.status)
32715
- );
32716
- if (ok) jsonSuccess({ jobs: results });
32477
+ if (!failure3) jsonSuccess({ jobs: results });
32717
32478
  jsonResult({ jobs: results }, 1);
32718
- return;
32479
+ fail(failure3);
32719
32480
  }
32720
32481
  const jobs = dstLocales.map((locale) => ({
32721
32482
  dstLocale: locale,
32722
32483
  status: "waiting"
32723
32484
  }));
32724
- const { rerender, unmount } = render18(
32485
+ const { rerender, unmount } = render19(
32725
32486
  /* @__PURE__ */ jsx20(RunUI, { srcLocale, jobs })
32726
32487
  );
32727
32488
  const updateJob = (locale, update) => {
@@ -32745,6 +32506,7 @@ ${env.TOLKBOT_WEB_URL}/projects/${project.slug}/settings`
32745
32506
  });
32746
32507
  }
32747
32508
  };
32509
+ let failure2 = null;
32748
32510
  try {
32749
32511
  for (const dstLocale of dstLocales) {
32750
32512
  updateJob(dstLocale, { status: "generating" });
@@ -32783,7 +32545,7 @@ ${env.TOLKBOT_WEB_URL}/projects/${project.slug}/settings`
32783
32545
  jobId: error62.jobId,
32784
32546
  error: error62.message
32785
32547
  });
32786
- process.exitCode = 1;
32548
+ failure2 ??= error62;
32787
32549
  skipRemaining(dstLocale, "the CLI stopped waiting for");
32788
32550
  break;
32789
32551
  }
@@ -32795,7 +32557,7 @@ ${env.TOLKBOT_WEB_URL}/projects/${project.slug}/settings`
32795
32557
  jobId,
32796
32558
  error: "Translation failed"
32797
32559
  });
32798
- process.exitCode = 1;
32560
+ failure2 ??= unsuccessfulJobError(jobId, summary.status);
32799
32561
  } else if (summary.status === "cancelled") {
32800
32562
  updateJob(dstLocale, {
32801
32563
  status: "error",
@@ -32803,7 +32565,7 @@ ${env.TOLKBOT_WEB_URL}/projects/${project.slug}/settings`
32803
32565
  jobId,
32804
32566
  error: "The job was cancelled."
32805
32567
  });
32806
- process.exitCode = 1;
32568
+ failure2 ??= unsuccessfulJobError(jobId, summary.status);
32807
32569
  skipRemaining(dstLocale, "someone cancelled");
32808
32570
  break;
32809
32571
  } else {
@@ -32821,10 +32583,10 @@ ${env.TOLKBOT_WEB_URL}/projects/${project.slug}/settings`
32821
32583
  job_count: jobs.filter((j) => j.jobId).length,
32822
32584
  json: false
32823
32585
  });
32824
- } catch (err) {
32586
+ } finally {
32825
32587
  unmount();
32826
- throw err;
32827
32588
  }
32589
+ if (failure2) fail(failure2);
32828
32590
  }
32829
32591
  function summarize(plan) {
32830
32592
  return {
@@ -32836,32 +32598,23 @@ function summarize(plan) {
32836
32598
  }
32837
32599
 
32838
32600
  // src/commands/translate/status.tsx
32839
- import { Spinner as Spinner18 } from "@inkjs/ui";
32840
- import { Box as Box20, render as render19, Text as Text20 } from "ink";
32601
+ import { Spinner as Spinner17 } from "@inkjs/ui";
32602
+ import { Box as Box20, render as render20, Text as Text20 } from "ink";
32841
32603
  import { jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
32842
32604
  function StatusUI({ state }) {
32843
32605
  if (state.type === "loading") {
32844
32606
  return /* @__PURE__ */ jsxs20(Box20, { children: [
32845
- /* @__PURE__ */ jsx21(Spinner18, {}),
32607
+ /* @__PURE__ */ jsx21(Spinner17, {}),
32846
32608
  /* @__PURE__ */ jsx21(Text20, { children: " Loading job status\u2026" })
32847
32609
  ] });
32848
32610
  }
32849
- if (state.type === "not-found") {
32850
- return /* @__PURE__ */ jsx21(Box20, { children: /* @__PURE__ */ jsx21(Text20, { color: "red", children: "\u2717 Job not found" }) });
32851
- }
32852
- if (state.type === "error") {
32853
- return /* @__PURE__ */ jsxs20(Box20, { flexDirection: "column", children: [
32854
- /* @__PURE__ */ jsx21(Text20, { color: "red", children: "\u2717 Error fetching job status" }),
32855
- /* @__PURE__ */ jsx21(Text20, { dimColor: true, children: state.message })
32856
- ] });
32857
- }
32858
32611
  const { summary } = state;
32859
32612
  const processed = summary.completed + summary.failed;
32860
32613
  const progress = summary.total > 0 ? Math.round(processed / summary.total * 100) : 0;
32861
32614
  if (state.type === "running") {
32862
32615
  return /* @__PURE__ */ jsxs20(Box20, { flexDirection: "column", children: [
32863
32616
  /* @__PURE__ */ jsxs20(Box20, { children: [
32864
- /* @__PURE__ */ jsx21(Spinner18, {}),
32617
+ /* @__PURE__ */ jsx21(Spinner17, {}),
32865
32618
  /* @__PURE__ */ jsx21(Text20, { children: " Job is running\u2026" })
32866
32619
  ] }),
32867
32620
  /* @__PURE__ */ jsx21(Box20, { marginTop: 1, children: /* @__PURE__ */ jsxs20(Text20, { children: [
@@ -32912,96 +32665,59 @@ function StatusUI({ state }) {
32912
32665
  ] });
32913
32666
  }
32914
32667
  async function fetchJobSummary(jobId) {
32915
- const convexClient = await getAuthenticatedConvexClient();
32916
- const baseDir = process.cwd();
32917
- const config3 = readConfig(baseDir);
32918
- if (!config3) {
32919
- throw new Error(
32920
- "No tolkbot.json found. Run 'tolkbot init' to create one."
32921
- );
32922
- }
32923
- const project = await convexClient.query(api.projects.getProjectForCli, {
32924
- projectId: config3.config.projectId
32925
- });
32926
- if (!project) {
32927
- throw new Error("Project not found. Please run 'tolkbot init' again.");
32928
- }
32929
- setTelemetryOrganization(project.organizationId);
32930
- const summary = await convexClient.query(api.translations.getJobSummary, {
32668
+ const { client: client3, project } = await loadPlanContext(process.cwd());
32669
+ const summary = await client3.query(api.translations.getJobSummary, {
32931
32670
  organizationId: project.organizationId,
32932
32671
  jobId
32933
32672
  });
32673
+ if (!summary) {
32674
+ throw new CliError(
32675
+ `Translation job ${jobId} was not found in this project.`,
32676
+ void 0,
32677
+ "job_not_found"
32678
+ );
32679
+ }
32934
32680
  return summary;
32935
32681
  }
32936
32682
  async function runStatus(opts) {
32937
32683
  if (opts.json) {
32938
- try {
32939
- if (!opts.jobId) {
32940
- jsonError("--job-id is required");
32941
- }
32942
- const summary = await fetchJobSummary(opts.jobId);
32943
- if (!summary) {
32944
- jsonError("Job not found");
32945
- }
32946
- jsonSuccess({
32947
- jobId: opts.jobId,
32948
- status: summary.status,
32949
- completed: summary.completed,
32950
- failed: summary.failed,
32951
- total: summary.total
32952
- });
32953
- } catch (error62) {
32954
- jsonError(error62 instanceof Error ? error62.message : String(error62));
32955
- }
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);
32956
32696
  }
32957
- let { unmount } = render19(/* @__PURE__ */ jsx21(StatusUI, { state: { type: "loading" } }));
32697
+ const { unmount } = renderTransient(
32698
+ /* @__PURE__ */ jsx21(StatusUI, { state: { type: "loading" } })
32699
+ );
32700
+ let summary;
32958
32701
  try {
32959
- if (!opts.jobId) {
32960
- throw new Error("--job-id is required");
32961
- }
32962
- const summary = await fetchJobSummary(opts.jobId);
32963
- if (!summary) {
32964
- unmount();
32965
- const notFoundUi = render19(
32966
- /* @__PURE__ */ jsx21(StatusUI, { state: { type: "not-found" } })
32967
- );
32968
- unmount = notFoundUi.unmount;
32969
- await notFoundUi.waitUntilExit();
32970
- return exitAfterFlush(1);
32971
- }
32972
- const isCompleted = isJobFinished(summary.status);
32973
- unmount();
32974
- const finalUi = render19(
32975
- /* @__PURE__ */ jsx21(
32976
- StatusUI,
32977
- {
32978
- state: {
32979
- type: isCompleted ? "completed" : "running",
32980
- summary
32981
- }
32982
- }
32983
- )
32984
- );
32985
- unmount = finalUi.unmount;
32986
- await finalUi.waitUntilExit();
32987
- if (isJobUnsuccessful(summary.status)) {
32988
- return exitAfterFlush(1);
32989
- }
32990
- } catch (error62) {
32702
+ summary = await fetchJobSummary(opts.jobId);
32703
+ } finally {
32991
32704
  unmount();
32992
- const errorUi = render19(
32993
- /* @__PURE__ */ jsx21(
32994
- StatusUI,
32995
- {
32996
- state: {
32997
- type: "error",
32998
- message: error62 instanceof Error ? error62.message : String(error62)
32999
- }
32705
+ }
32706
+ const finalUi = render20(
32707
+ /* @__PURE__ */ jsx21(
32708
+ StatusUI,
32709
+ {
32710
+ state: {
32711
+ type: isJobFinished(summary.status) ? "completed" : "running",
32712
+ summary
33000
32713
  }
33001
- )
33002
- );
33003
- await errorUi.waitUntilExit();
33004
- return exitAfterFlush(1);
32714
+ }
32715
+ )
32716
+ );
32717
+ finalUi.unmount();
32718
+ if (isJobUnsuccessful(summary.status)) {
32719
+ const failure2 = unsuccessfulJobError(opts.jobId, summary.status);
32720
+ if (failure2) fail(failure2);
33005
32721
  }
33006
32722
  }
33007
32723
 
@@ -33040,14 +32756,12 @@ translateCommand.command("run").description("Run translation job (auto-generates
33040
32756
  });
33041
32757
  const parsed = validateOrExit(schema, opts);
33042
32758
  if (!parsed.dst && !parsed.all) {
33043
- console.error(
33044
- "Error: Either --dst <locale> or --all must be provided"
32759
+ throw new UsageError(
32760
+ "Either --dst <locale> or --all must be provided."
33045
32761
  );
33046
- return exitAfterFlush(1);
33047
32762
  }
33048
32763
  if (parsed.dst && parsed.all) {
33049
- console.error("Error: Cannot use both --dst and --all");
33050
- return exitAfterFlush(1);
32764
+ throw new UsageError("Cannot use both --dst and --all.");
33051
32765
  }
33052
32766
  await runTranslate(parsed);
33053
32767
  });
@@ -33065,8 +32779,7 @@ translateCommand.command("adopt").description(
33065
32779
  });
33066
32780
  const parsed = validateOrExit(schema, opts);
33067
32781
  if (parsed.dst && parsed.all) {
33068
- console.error("Error: Cannot use both --dst and --all");
33069
- return exitAfterFlush(1);
32782
+ throw new UsageError("Cannot use both --dst and --all.");
33070
32783
  }
33071
32784
  await runAdopt(parsed);
33072
32785
  });
@@ -33079,7 +32792,7 @@ translateCommand.command("cancel").description("Cancel translation job by job ID
33079
32792
  await runCancel(parsed);
33080
32793
  });
33081
32794
  translateCommand.command("apply").description(
33082
- "Apply translation job results (interactive select or specify --job-id)"
32795
+ "Apply finished translation jobs to the working tree. Without --job-id: the one unapplied job per language, or a picker in a terminal"
33083
32796
  ).option("--job-id <id...>", "Job ID(s) to apply").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
33084
32797
  const schema = z7.object({
33085
32798
  jobId: z7.array(z7.string()).optional(),
@@ -33109,7 +32822,7 @@ translateCommand.command("status").description("Check status of translation job
33109
32822
  });
33110
32823
 
33111
32824
  // src/commands/validate.tsx
33112
- import { Box as Box21, render as render20, Text as Text21 } from "ink";
32825
+ import { Box as Box21, render as render21, Text as Text21 } from "ink";
33113
32826
 
33114
32827
  // ../ai/src/validators/placeholders.ts
33115
32828
  var IDENTIFIER = /^[A-Za-z0-9_$][A-Za-z0-9_$.[\]-]*$/;
@@ -34275,11 +33988,24 @@ function emit(report, json2) {
34275
33988
  });
34276
33989
  if (json2) {
34277
33990
  jsonResult(report, exitCode);
33991
+ if (!report.valid) fail(validationFailure(report));
34278
33992
  return;
34279
33993
  }
34280
- render20(/* @__PURE__ */ jsx22(ReportUI, { report })).unmount();
33994
+ render21(/* @__PURE__ */ jsx22(ReportUI, { report })).unmount();
34281
33995
  process.exitCode = exitCode;
34282
33996
  }
33997
+ function validationFailure(report) {
33998
+ if (report.issues.some((issue2) => issue2.rule === "no-config")) {
33999
+ return noConfigError();
34000
+ }
34001
+ const { error: error62, warning } = report.counts;
34002
+ const found = report.strict ? `${error62} error(s) and ${warning} warning(s)` : `${error62} error(s)`;
34003
+ return new CliError(
34004
+ `Validation found ${found}.`,
34005
+ "The report on stdout lists each one.",
34006
+ "validation_failed"
34007
+ );
34008
+ }
34283
34009
  async function runValidate(opts = {}) {
34284
34010
  const json2 = opts.json ?? false;
34285
34011
  const strict = opts.strict ?? false;
@@ -34324,16 +34050,17 @@ function commandPath(command) {
34324
34050
  return names.join(" ");
34325
34051
  }
34326
34052
  function buildProgram() {
34327
- const program2 = 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(
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(
34328
34054
  "--token <token>",
34329
34055
  `Auth token to use for this invocation (overrides $${TOKEN_ENV_VAR} and the stored token)`
34330
34056
  );
34331
- program2.hook("preAction", (thisCommand, actionCommand) => {
34057
+ program.hook("preAction", (thisCommand, actionCommand) => {
34332
34058
  const { token } = thisCommand.opts();
34333
34059
  setTokenOverride(token);
34060
+ resolveJsonOption(actionCommand);
34334
34061
  beginCommand(commandPath(actionCommand), version);
34335
34062
  });
34336
- program2.command("init").description("Initialize TolkBot in the current repo").option("--force", "Overwrite existing config").option(
34063
+ program.command("init").description("Initialize TolkBot in the current repo").option("--force", "Overwrite existing config").option(
34337
34064
  "-y, --yes",
34338
34065
  "Skip all prompts, accepting detected settings. Required outside a terminal."
34339
34066
  ).option("--json", "Output the result as JSON (implies --yes)").option("--base <locale>", "Base locale (default: detected, else en)").option(
@@ -34346,28 +34073,75 @@ function buildProgram() {
34346
34073
  "--install",
34347
34074
  "Also install i18n dependencies and scaffold files. Off by default when non-interactive."
34348
34075
  ).action((opts) => runInit(opts));
34349
- program2.command("validate").description("Validate translation files and configuration").option("--json", "Output the report as JSON").option(
34076
+ program.command("validate").description("Validate translation files and configuration").option("--json", "Output the report as JSON").option(
34350
34077
  "--strict",
34351
34078
  "Treat warnings (incomplete locales) as errors. Off by default, since a locale is normally behind the base while a translation job is in flight."
34352
34079
  ).action(
34353
34080
  (opts) => runValidate(opts)
34354
34081
  );
34355
- program2.addCommand(authCommand);
34356
- program2.addCommand(glossaryCommand);
34357
- program2.addCommand(learningsCommand);
34358
- program2.addCommand(telemetryCommand);
34359
- program2.addCommand(translateCommand);
34360
- program2.addHelpText("after", PROGRAM_HELP_FOOTER);
34361
- return program2;
34082
+ program.addCommand(authCommand);
34083
+ program.addCommand(glossaryCommand);
34084
+ program.addCommand(learningsCommand);
34085
+ program.addCommand(telemetryCommand);
34086
+ program.addCommand(translateCommand);
34087
+ program.addHelpText("after", PROGRAM_HELP_FOOTER);
34088
+ forEachCommand(program, (command) => {
34089
+ addNoJsonOption(command);
34090
+ reportUsageErrors(command);
34091
+ });
34092
+ return program;
34093
+ }
34094
+ function forEachCommand(command, visit2) {
34095
+ visit2(command);
34096
+ for (const sub of command.commands) forEachCommand(sub, visit2);
34097
+ }
34098
+ function jsonOptionOf(command) {
34099
+ return command.options.find((option) => option.long === "--json");
34100
+ }
34101
+ function addNoJsonOption(command) {
34102
+ const json2 = jsonOptionOf(command);
34103
+ if (!json2 || command.options.some((o) => o.long === "--no-json")) return;
34104
+ json2.description = `${json2.description.replace(" (machine-readable)", "")}, the default without a terminal`;
34105
+ command.option("--no-json", "Output text, even without a terminal");
34106
+ }
34107
+ function resolveJsonOption(command) {
34108
+ if (!jsonOptionOf(command)) {
34109
+ setJsonOutput(null);
34110
+ return;
34111
+ }
34112
+ const json2 = resolveJson(command.opts().json);
34113
+ command.setOptionValue("json", json2);
34114
+ setJsonOutput(json2);
34115
+ }
34116
+ function reportUsageErrors(command) {
34117
+ command.exitOverride().configureOutput({
34118
+ outputError: (message, write) => {
34119
+ if (wantsJson()) {
34120
+ const help = ["tolkbot", commandPath(command), "--help"].filter(Boolean).join(" ");
34121
+ pendingUsageError = new UsageError(
34122
+ message.replace(/^error:\s*/, "").trim(),
34123
+ `Run '${help}'.`
34124
+ );
34125
+ return;
34126
+ }
34127
+ write(message);
34128
+ }
34129
+ });
34362
34130
  }
34131
+ var pendingUsageError = null;
34363
34132
  var PROGRAM_HELP_FOOTER = `
34364
34133
  Examples:
34365
34134
  $ tolkbot auth login
34366
- $ tolkbot init --yes
34367
- $ tolkbot translate plan --dst nb --json
34135
+ $ tolkbot init
34368
34136
  $ tolkbot translate run --all
34369
- $ tolkbot translate apply --job-id <id>
34370
- $ tolkbot validate --strict
34137
+ $ tolkbot translate apply
34138
+ $ tolkbot validate
34139
+
34140
+ Without a terminal (an agent, CI), nothing prompts: commands take their
34141
+ defaults and print JSON (--no-json for text). A failure prints
34142
+ {"error": {"code", "message", "hint"}} on stderr and exits with
34143
+ 1 failed 2 usage 3 not logged in or not allowed
34144
+ 4 billing 5 could not reach TolkBot
34371
34145
 
34372
34146
  Environment variables:
34373
34147
  ${TOKEN_ENV_VAR} Auth token for CI and headless environments (--token overrides it)
@@ -34388,8 +34162,14 @@ async function main() {
34388
34162
  exitCode = error62.code;
34389
34163
  return;
34390
34164
  }
34391
- exitCode = 1;
34392
- throw error62;
34165
+ if (error62 instanceof CommanderError) {
34166
+ if (error62.exitCode === 0) return;
34167
+ exitCode = EXIT_CODES.usage;
34168
+ process.exitCode = exitCode;
34169
+ if (pendingUsageError) reportError(pendingUsageError);
34170
+ return;
34171
+ }
34172
+ exitCode = reportError(error62);
34393
34173
  } finally {
34394
34174
  completeCommand(exitCode);
34395
34175
  await flushTelemetry();
@@ -34397,7 +34177,9 @@ async function main() {
34397
34177
  }
34398
34178
 
34399
34179
  // src/index.ts
34400
- main().catch(reportFatal);
34180
+ main().catch((error62) => {
34181
+ process.exitCode = reportError(error62);
34182
+ });
34401
34183
  /*! Bundled license information:
34402
34184
 
34403
34185
  is-extglob/index.js: