@intlpullhq/cli 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ ContributorsApi,
4
+ SnapshotsApi,
5
+ TMApi,
6
+ WebhooksApi,
3
7
  createApiClient
4
- } from "./chunk-2MCKJWPD.js";
8
+ } from "./chunk-WVCVQFBI.js";
9
+ import {
10
+ ProjectsApi
11
+ } from "./chunk-KCZQUMQP.js";
12
+ import "./chunk-WSY27J6N.js";
13
+ import {
14
+ KeysApi
15
+ } from "./chunk-BULIQM4U.js";
16
+ import "./chunk-KIDP7N6D.js";
5
17
  import {
6
18
  detectFramework,
7
19
  getAuthErrorMessage,
@@ -15,10 +27,10 @@ import {
15
27
  saveAuthConfig,
16
28
  saveProjectConfig,
17
29
  setCustomEnvFile
18
- } from "./chunk-GQADIM7O.js";
30
+ } from "./chunk-IWYURZV2.js";
19
31
 
20
32
  // src/index.tsx
21
- import { Command as Command2 } from "commander";
33
+ import { Command as Command7 } from "commander";
22
34
 
23
35
  // src/commands/init.tsx
24
36
  import { useState, useEffect } from "react";
@@ -1059,10 +1071,10 @@ function runStatus() {
1059
1071
  render3(/* @__PURE__ */ jsx8(StatusCommand, {}));
1060
1072
  }
1061
1073
 
1062
- // src/commands/pull.tsx
1074
+ // src/commands/download.tsx
1063
1075
  import { render as render4 } from "ink";
1064
1076
 
1065
- // src/commands/pull/components/interactive-pull.tsx
1077
+ // src/commands/download/components/interactive-pull.tsx
1066
1078
  import { useState as useState6, useEffect as useEffect5, useCallback as useCallback2 } from "react";
1067
1079
  import { Box as Box9, Text as Text10, useApp as useApp2 } from "ink";
1068
1080
  import SelectInput2 from "ink-select-input";
@@ -1070,10 +1082,13 @@ import Spinner3 from "ink-spinner";
1070
1082
  import { mkdirSync as mkdirSync3, existsSync as existsSync5 } from "fs";
1071
1083
  import { join as join5, dirname as dirname3 } from "path";
1072
1084
 
1073
- // src/commands/pull/api.ts
1085
+ // src/lib/api/fetch-utils.ts
1074
1086
  var DEFAULT_TIMEOUT = 3e4;
1075
1087
  var MAX_RETRIES = 3;
1076
1088
  var RETRY_DELAYS = [1e3, 2e3, 4e3];
1089
+ function sleep(ms) {
1090
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1091
+ }
1077
1092
  async function fetchWithRetry(url, options = {}, retries = MAX_RETRIES) {
1078
1093
  const { timeout = DEFAULT_TIMEOUT, ...fetchOptions } = options;
1079
1094
  for (let attempt = 0; attempt <= retries; attempt++) {
@@ -1085,6 +1100,24 @@ async function fetchWithRetry(url, options = {}, retries = MAX_RETRIES) {
1085
1100
  signal: controller.signal
1086
1101
  });
1087
1102
  clearTimeout(timeoutId);
1103
+ if (response.status === 429 && attempt < retries) {
1104
+ const retryAfter = response.headers.get("Retry-After");
1105
+ let delayMs = RETRY_DELAYS[attempt] || 4e3;
1106
+ if (retryAfter) {
1107
+ const retryAfterSeconds = parseInt(retryAfter, 10);
1108
+ if (!isNaN(retryAfterSeconds)) {
1109
+ delayMs = retryAfterSeconds * 1e3;
1110
+ } else {
1111
+ const retryDate = new Date(retryAfter);
1112
+ if (!isNaN(retryDate.getTime())) {
1113
+ delayMs = Math.max(0, retryDate.getTime() - Date.now());
1114
+ }
1115
+ }
1116
+ }
1117
+ delayMs = Math.min(delayMs, 3e4);
1118
+ await sleep(delayMs);
1119
+ continue;
1120
+ }
1088
1121
  if (response.status >= 500 && attempt < retries) {
1089
1122
  await sleep(RETRY_DELAYS[attempt] || 4e3);
1090
1123
  continue;
@@ -1108,9 +1141,8 @@ async function fetchWithRetry(url, options = {}, retries = MAX_RETRIES) {
1108
1141
  }
1109
1142
  throw new Error("Max retries exceeded");
1110
1143
  }
1111
- function sleep(ms) {
1112
- return new Promise((resolve) => setTimeout(resolve, ms));
1113
- }
1144
+
1145
+ // src/commands/download/api.ts
1114
1146
  async function fetchProjects2(apiUrl, apiKey) {
1115
1147
  const headers = { Accept: "application/json" };
1116
1148
  if (apiKey) headers["X-API-Key"] = apiKey;
@@ -1264,7 +1296,14 @@ async function fetchTranslationsParallel(projectId, apiUrl, apiKey, options) {
1264
1296
  const availableLanguages = projectData.languages?.map((l) => l.code) || ["en"];
1265
1297
  const targetLanguages = options?.languages?.length ? options.languages : availableLanguages;
1266
1298
  const namespacesResult = await fetchNamespaces(projectId, apiUrl, apiKey);
1267
- const namespaces = namespacesResult.namespaces;
1299
+ let namespaces = namespacesResult.namespaces;
1300
+ if (options?.namespaces?.length) {
1301
+ const requestedNamespaces = new Set(options.namespaces);
1302
+ namespaces = namespaces.filter((ns) => requestedNamespaces.has(ns));
1303
+ const missingNamespaces = options.namespaces.filter((ns) => !namespacesResult.namespaces.includes(ns));
1304
+ if (missingNamespaces.length > 0) {
1305
+ }
1306
+ }
1268
1307
  const taskParams = [];
1269
1308
  for (const lang of targetLanguages) {
1270
1309
  for (const ns of namespaces) {
@@ -1327,7 +1366,7 @@ async function fetchProjectInfo(projectId, apiUrl, apiKey) {
1327
1366
  };
1328
1367
  }
1329
1368
 
1330
- // src/commands/pull/formatters.ts
1369
+ // src/commands/download/formatters.ts
1331
1370
  function formatAsJson(translations) {
1332
1371
  return JSON.stringify(translations, null, 2);
1333
1372
  }
@@ -1377,7 +1416,7 @@ function getFileExtension(format) {
1377
1416
  }
1378
1417
  }
1379
1418
 
1380
- // src/commands/pull/components/multi-select.tsx
1419
+ // src/commands/download/components/multi-select.tsx
1381
1420
  import { useState as useState5 } from "react";
1382
1421
  import { Box as Box8, Text as Text9, useInput as useInput2 } from "ink";
1383
1422
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
@@ -1411,7 +1450,7 @@ function MultiSelect({ items, selected, onToggle, onSubmit, isActive = true }) {
1411
1450
  ] });
1412
1451
  }
1413
1452
 
1414
- // src/commands/pull/hooks/useInteractivePull.ts
1453
+ // src/commands/download/hooks/useInteractivePull.ts
1415
1454
  import { useReducer, useCallback } from "react";
1416
1455
  var initialState = {
1417
1456
  step: "project",
@@ -1732,7 +1771,7 @@ function cleanupStaleTempFiles(dir, maxAgeMs = TEMP_FILE_MAX_AGE_MS) {
1732
1771
  return result;
1733
1772
  }
1734
1773
 
1735
- // src/commands/pull/components/interactive-pull.tsx
1774
+ // src/commands/download/components/interactive-pull.tsx
1736
1775
  import { Fragment, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1737
1776
  function InteractivePull({ initialOptions }) {
1738
1777
  const { exit } = useApp2();
@@ -2206,7 +2245,7 @@ ${writeErrors.join("\n")}`);
2206
2245
  ] });
2207
2246
  }
2208
2247
 
2209
- // src/commands/pull/components/smart-pull.tsx
2248
+ // src/commands/download/components/smart-pull.tsx
2210
2249
  import { useState as useState7, useEffect as useEffect6 } from "react";
2211
2250
  import { Box as Box10, Text as Text11 } from "ink";
2212
2251
  import Spinner4 from "ink-spinner";
@@ -2342,7 +2381,140 @@ var LIBRARIES2 = {
2342
2381
  }
2343
2382
  };
2344
2383
 
2345
- // src/commands/pull/components/smart-pull.tsx
2384
+ // src/lib/key-scanner.ts
2385
+ import { glob } from "glob";
2386
+ import { readFile } from "fs/promises";
2387
+ var DEFAULT_EXTRACTORS = [
2388
+ // i18next: t('key.name') or t("key.name")
2389
+ {
2390
+ name: "i18next",
2391
+ pattern: /\bt\s*\(\s*['"]([^'"]+)['"]/g,
2392
+ extract: (m) => m[1]
2393
+ },
2394
+ // react-i18next: t('key.name')
2395
+ {
2396
+ name: "react-i18next",
2397
+ pattern: /\bt\s*\(\s*['"]([^'"]+)['"]/g,
2398
+ extract: (m) => m[1]
2399
+ },
2400
+ // vue-i18n: $t('key.name') or {{ $t('key.name') }}
2401
+ {
2402
+ name: "vue-i18n",
2403
+ pattern: /\$t\s*\(\s*['"]([^'"]+)['"]/g,
2404
+ extract: (m) => m[1]
2405
+ },
2406
+ // react-intl: formatMessage({ id: 'key.name' })
2407
+ {
2408
+ name: "react-intl-formatMessage",
2409
+ pattern: /formatMessage\s*\(\s*\{\s*id:\s*['"]([^'"]+)['"]/g,
2410
+ extract: (m) => m[1]
2411
+ },
2412
+ // react-intl: <FormattedMessage id="key.name" />
2413
+ {
2414
+ name: "react-intl-component",
2415
+ pattern: /<FormattedMessage\s+id=["']([^"']+)["']/g,
2416
+ extract: (m) => m[1]
2417
+ },
2418
+ // next-intl: t('key.name')
2419
+ {
2420
+ name: "next-intl",
2421
+ pattern: /\bt\s*\(\s*['"]([^'"]+)['"]/g,
2422
+ extract: (m) => m[1]
2423
+ },
2424
+ // lingui: t`key.name` or t({ id: 'key.name' })
2425
+ {
2426
+ name: "lingui-macro",
2427
+ pattern: /\bt\s*`([^`]+)`/g,
2428
+ extract: (m) => m[1]
2429
+ },
2430
+ {
2431
+ name: "lingui-object",
2432
+ pattern: /\bt\s*\(\s*\{\s*id:\s*['"]([^'"]+)['"]/g,
2433
+ extract: (m) => m[1]
2434
+ },
2435
+ // svelte-i18n: $t('key.name') or {$t('key.name')}
2436
+ {
2437
+ name: "svelte-i18n",
2438
+ pattern: /\$t\s*\(\s*['"]([^'"]+)['"]/g,
2439
+ extract: (m) => m[1]
2440
+ },
2441
+ // astro-i18n: t('key.name')
2442
+ {
2443
+ name: "astro-i18n",
2444
+ pattern: /\bt\s*\(\s*['"]([^'"]+)['"]/g,
2445
+ extract: (m) => m[1]
2446
+ }
2447
+ ];
2448
+ async function scanForKeys(rootDir, options = {}) {
2449
+ const patterns = options.patterns || ["**/*.{ts,tsx,js,jsx,vue,svelte,astro}"];
2450
+ const exclude = options.exclude || ["node_modules/**", "dist/**", ".next/**", "build/**", ".svelte-kit/**", "out/**"];
2451
+ const extractors = options.extractors || DEFAULT_EXTRACTORS;
2452
+ const usedKeys = /* @__PURE__ */ new Set();
2453
+ try {
2454
+ const files = await glob(patterns, { cwd: rootDir, ignore: exclude });
2455
+ const batchSize = 50;
2456
+ for (let i = 0; i < files.length; i += batchSize) {
2457
+ const batch = files.slice(i, i + batchSize);
2458
+ await Promise.all(
2459
+ batch.map(async (file) => {
2460
+ try {
2461
+ const content = await readFile(`${rootDir}/${file}`, "utf-8");
2462
+ for (const extractor of extractors) {
2463
+ const matches = content.matchAll(extractor.pattern);
2464
+ for (const match of matches) {
2465
+ const key = extractor.extract(match);
2466
+ if (key) {
2467
+ usedKeys.add(key);
2468
+ }
2469
+ }
2470
+ }
2471
+ } catch (err) {
2472
+ if (err instanceof Error && !err.message.includes("ENOENT")) {
2473
+ }
2474
+ }
2475
+ })
2476
+ );
2477
+ }
2478
+ } catch (err) {
2479
+ throw new Error(`Failed to scan files: ${err instanceof Error ? err.message : "Unknown error"}`);
2480
+ }
2481
+ return usedKeys;
2482
+ }
2483
+ function validatePrefix(prefix) {
2484
+ return /^[a-zA-Z0-9.]+$/.test(prefix);
2485
+ }
2486
+ function applyPrefix(keys, prefix) {
2487
+ const result = {};
2488
+ const prefixWithDot = `${prefix}.`;
2489
+ for (const [key, value] of Object.entries(keys)) {
2490
+ result[`${prefixWithDot}${key}`] = value;
2491
+ }
2492
+ return result;
2493
+ }
2494
+ function stripPrefix(keys, prefix) {
2495
+ const result = {};
2496
+ const prefixWithDot = `${prefix}.`;
2497
+ for (const [key, value] of Object.entries(keys)) {
2498
+ if (key.startsWith(prefixWithDot)) {
2499
+ result[key.substring(prefixWithDot.length)] = value;
2500
+ } else {
2501
+ result[key] = value;
2502
+ }
2503
+ }
2504
+ return result;
2505
+ }
2506
+ function filterByPrefix(keys, prefix) {
2507
+ const result = {};
2508
+ const prefixWithDot = `${prefix}.`;
2509
+ for (const [key, value] of Object.entries(keys)) {
2510
+ if (key.startsWith(prefixWithDot) || key === prefix) {
2511
+ result[key] = value;
2512
+ }
2513
+ }
2514
+ return result;
2515
+ }
2516
+
2517
+ // src/commands/download/components/smart-pull.tsx
2346
2518
  import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2347
2519
  function SmartPull({ options }) {
2348
2520
  const [state, setState] = useState7({
@@ -2361,6 +2533,12 @@ function SmartPull({ options }) {
2361
2533
  if (!apiKey) {
2362
2534
  throw new Error("Not authenticated. Run `npx @intlpullhq/cli login` or set INTLPULL_API_KEY.");
2363
2535
  }
2536
+ if (options.translationKeyPrefix && !validatePrefix(options.translationKeyPrefix)) {
2537
+ throw new Error('Invalid prefix format. Use alphanumeric characters and dots only (e.g., "checkout" or "app.checkout")');
2538
+ }
2539
+ if (options.filterByPrefix && !validatePrefix(options.filterByPrefix)) {
2540
+ throw new Error("Invalid filter prefix format. Use alphanumeric characters and dots only");
2541
+ }
2364
2542
  setState((s) => ({ ...s, status: "detecting", message: "Fetching project info..." }));
2365
2543
  let projectId = options.project || projectConfig?.projectId;
2366
2544
  let projectInfo = null;
@@ -2428,6 +2606,7 @@ Or use a project-scoped API key for automatic selection.`
2428
2606
  let version;
2429
2607
  let namespaceCount = 0;
2430
2608
  let namespaces = [];
2609
+ const targetNamespaces = options.namespaces ? options.namespaces.split(",").map((n) => n.trim()).filter(Boolean) : void 0;
2431
2610
  if (useParallel) {
2432
2611
  const result = await fetchTranslationsParallel(
2433
2612
  projectId,
@@ -2437,6 +2616,7 @@ Or use a project-scoped API key for automatic selection.`
2437
2616
  languages: targetLanguages,
2438
2617
  platform,
2439
2618
  branch,
2619
+ namespaces: targetNamespaces,
2440
2620
  onProgress: (completed, total) => {
2441
2621
  setState((s) => ({
2442
2622
  ...s,
@@ -2474,6 +2654,38 @@ Or use a project-scoped API key for automatic selection.`
2474
2654
  bundle = result.bundle;
2475
2655
  version = result.version;
2476
2656
  }
2657
+ if (options.filterByPrefix) {
2658
+ setState((s) => ({ ...s, message: `Filtering keys by prefix "${options.filterByPrefix}"...` }));
2659
+ for (const locale in bundle) {
2660
+ bundle[locale] = filterByPrefix(bundle[locale], options.filterByPrefix);
2661
+ }
2662
+ if (namespacedBundle) {
2663
+ for (const locale in namespacedBundle) {
2664
+ for (const namespace in namespacedBundle[locale]) {
2665
+ namespacedBundle[locale][namespace] = filterByPrefix(
2666
+ namespacedBundle[locale][namespace],
2667
+ options.filterByPrefix
2668
+ );
2669
+ }
2670
+ }
2671
+ }
2672
+ }
2673
+ if (options.translationKeyPrefix) {
2674
+ setState((s) => ({ ...s, message: `Stripping prefix "${options.translationKeyPrefix}"...` }));
2675
+ for (const locale in bundle) {
2676
+ bundle[locale] = stripPrefix(bundle[locale], options.translationKeyPrefix);
2677
+ }
2678
+ if (namespacedBundle) {
2679
+ for (const locale in namespacedBundle) {
2680
+ for (const namespace in namespacedBundle[locale]) {
2681
+ namespacedBundle[locale][namespace] = stripPrefix(
2682
+ namespacedBundle[locale][namespace],
2683
+ options.translationKeyPrefix
2684
+ );
2685
+ }
2686
+ }
2687
+ }
2688
+ }
2477
2689
  setState((s) => ({ ...s, message: "Writing translation files..." }));
2478
2690
  const ext = getFileExtension(format);
2479
2691
  const writtenFiles = [];
@@ -2662,9 +2874,9 @@ ${writeErrors.join("\n")}`);
2662
2874
  ] });
2663
2875
  }
2664
2876
 
2665
- // src/commands/pull.tsx
2877
+ // src/commands/download.tsx
2666
2878
  import { jsx as jsx12 } from "react/jsx-runtime";
2667
- function runPull(options) {
2879
+ function runDownload(options) {
2668
2880
  if (options.interactive) {
2669
2881
  render4(/* @__PURE__ */ jsx12(InteractivePull, { initialOptions: options }));
2670
2882
  } else {
@@ -2672,16 +2884,16 @@ function runPull(options) {
2672
2884
  }
2673
2885
  }
2674
2886
 
2675
- // src/commands/push.tsx
2887
+ // src/commands/upload.tsx
2676
2888
  import { render as render5 } from "ink";
2677
2889
 
2678
- // src/commands/push/components/PushComponent.tsx
2890
+ // src/commands/upload/components/PushComponent.tsx
2679
2891
  import { useEffect as useEffect7, useCallback as useCallback5 } from "react";
2680
2892
  import { Box as Box14, Text as Text15 } from "ink";
2681
2893
  import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
2682
- import { join as join8, basename as basename3, extname as extname3 } from "path";
2894
+ import { basename as basename3, extname as extname3, isAbsolute, resolve } from "path";
2683
2895
 
2684
- // src/commands/push/hooks/usePushState.ts
2896
+ // src/commands/upload/hooks/usePushState.ts
2685
2897
  import { useReducer as useReducer2, useCallback as useCallback3 } from "react";
2686
2898
  var initialState2 = {
2687
2899
  status: "checking",
@@ -2836,50 +3048,10 @@ function usePushState() {
2836
3048
  };
2837
3049
  }
2838
3050
 
2839
- // src/commands/push/hooks/useLimitCheck.ts
3051
+ // src/commands/upload/hooks/useLimitCheck.ts
2840
3052
  import { useState as useState8, useCallback as useCallback4 } from "react";
2841
3053
 
2842
- // src/commands/push/api.ts
2843
- var DEFAULT_TIMEOUT2 = 3e4;
2844
- var MAX_RETRIES2 = 3;
2845
- var RETRY_DELAYS2 = [1e3, 2e3, 4e3];
2846
- async function fetchWithRetry2(url, options = {}, retries = MAX_RETRIES2) {
2847
- const { timeout = DEFAULT_TIMEOUT2, ...fetchOptions } = options;
2848
- for (let attempt = 0; attempt <= retries; attempt++) {
2849
- const controller = new AbortController();
2850
- const timeoutId = setTimeout(() => controller.abort(), timeout);
2851
- try {
2852
- const response = await fetch(url, {
2853
- ...fetchOptions,
2854
- signal: controller.signal
2855
- });
2856
- clearTimeout(timeoutId);
2857
- if (response.status >= 500 && attempt < retries) {
2858
- await sleep2(RETRY_DELAYS2[attempt] || 4e3);
2859
- continue;
2860
- }
2861
- return response;
2862
- } catch (error) {
2863
- clearTimeout(timeoutId);
2864
- if (error instanceof Error && error.name === "AbortError") {
2865
- if (attempt < retries) {
2866
- await sleep2(RETRY_DELAYS2[attempt] || 4e3);
2867
- continue;
2868
- }
2869
- throw new Error(`Request timed out after ${timeout}ms`);
2870
- }
2871
- if (attempt < retries) {
2872
- await sleep2(RETRY_DELAYS2[attempt] || 4e3);
2873
- continue;
2874
- }
2875
- throw error;
2876
- }
2877
- }
2878
- throw new Error("Max retries exceeded");
2879
- }
2880
- function sleep2(ms) {
2881
- return new Promise((resolve) => setTimeout(resolve, ms));
2882
- }
3054
+ // src/commands/upload/api.ts
2883
3055
  function getApiHeaders() {
2884
3056
  const resolved = getResolvedApiKey();
2885
3057
  const headers = {
@@ -2898,7 +3070,7 @@ function getApiUrl() {
2898
3070
  async function fetchProjects3() {
2899
3071
  const apiUrl = getApiUrl();
2900
3072
  const headers = getApiHeaders();
2901
- const response = await fetchWithRetry2(`${apiUrl}/api/v1/projects`, { headers });
3073
+ const response = await fetchWithRetry(`${apiUrl}/api/v1/projects`, { headers });
2902
3074
  if (!response.ok) {
2903
3075
  if (response.status === 401) {
2904
3076
  throw new Error("Authentication failed. Run `npx @intlpullhq/cli login`");
@@ -2916,7 +3088,7 @@ async function fetchProjects3() {
2916
3088
  async function fetchProject(projectId) {
2917
3089
  const apiUrl = getApiUrl();
2918
3090
  const headers = getApiHeaders();
2919
- const response = await fetchWithRetry2(`${apiUrl}/api/v1/projects/${projectId}`, { headers });
3091
+ const response = await fetchWithRetry(`${apiUrl}/api/v1/projects/${projectId}`, { headers });
2920
3092
  if (!response.ok) {
2921
3093
  if (response.status === 404) {
2922
3094
  throw new Error("Project not found. Check your project ID.");
@@ -2928,7 +3100,7 @@ async function fetchProject(projectId) {
2928
3100
  async function fetchBranches(projectId) {
2929
3101
  const apiUrl = getApiUrl();
2930
3102
  const headers = getApiHeaders();
2931
- const response = await fetchWithRetry2(`${apiUrl}/api/v1/projects/${projectId}/branches`, {
3103
+ const response = await fetchWithRetry(`${apiUrl}/api/v1/projects/${projectId}/branches`, {
2932
3104
  headers
2933
3105
  });
2934
3106
  if (!response.ok) {
@@ -2940,7 +3112,7 @@ async function fetchBranches(projectId) {
2940
3112
  async function checkUsageLimit(keyCount) {
2941
3113
  const apiUrl = getApiUrl();
2942
3114
  const headers = getApiHeaders();
2943
- const response = await fetchWithRetry2(`${apiUrl}/api/v1/billing/usage/check`, {
3115
+ const response = await fetchWithRetry(`${apiUrl}/api/v1/billing/usage/check`, {
2944
3116
  method: "POST",
2945
3117
  headers,
2946
3118
  body: JSON.stringify({ key_count: keyCount })
@@ -2958,28 +3130,40 @@ async function checkUsageLimit(keyCount) {
2958
3130
  }
2959
3131
  async function pushKeys(projectId, keys, language, namespace, options) {
2960
3132
  const apiUrl = getApiUrl();
2961
- const headers = getApiHeaders();
2962
- const body = {
3133
+ const resolved = getResolvedApiKey();
3134
+ const requestBody = {
2963
3135
  keys,
2964
3136
  language,
2965
3137
  namespace
2966
3138
  };
2967
3139
  if (options?.branchId) {
2968
- body.branch_id = options.branchId;
3140
+ requestBody.branch_id = options.branchId;
2969
3141
  }
2970
3142
  if (options?.platforms?.length) {
2971
- body.platforms = options.platforms;
3143
+ requestBody.platforms = options.platforms;
3144
+ }
3145
+ const headers = {
3146
+ "Accept": "application/json",
3147
+ "Content-Type": "application/json"
3148
+ };
3149
+ if (resolved?.key) {
3150
+ headers["X-API-Key"] = resolved.key;
2972
3151
  }
2973
- const response = await fetchWithRetry2(`${apiUrl}/api/v1/projects/${projectId}/keys/bulk`, {
3152
+ const response = await fetchWithRetry(`${apiUrl}/api/v1/projects/${projectId}/keys/bulk`, {
2974
3153
  method: "POST",
2975
3154
  headers,
2976
- body: JSON.stringify(body)
3155
+ body: JSON.stringify(requestBody)
2977
3156
  });
2978
3157
  if (!response.ok) {
2979
3158
  const error = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
2980
3159
  throw new Error(error.error || `Push failed: ${response.status}`);
2981
3160
  }
2982
- return response.json();
3161
+ const result = await response.json();
3162
+ return {
3163
+ keys_inserted: result.keys_inserted || 0,
3164
+ keys_updated: result.keys_updated || 0,
3165
+ keys_skipped: result.keys_skipped || 0
3166
+ };
2983
3167
  }
2984
3168
  async function pushKeysParallel(projectId, namespaces, language, options) {
2985
3169
  const concurrency = options?.concurrency || 3;
@@ -3020,7 +3204,7 @@ async function pushKeysParallel(projectId, namespaces, language, options) {
3020
3204
  async function addLanguagesBulk(projectId, languages, autoTranslate = false) {
3021
3205
  const apiUrl = getApiUrl();
3022
3206
  const headers = getApiHeaders();
3023
- const response = await fetchWithRetry2(
3207
+ const response = await fetchWithRetry(
3024
3208
  `${apiUrl}/api/v1/projects/${projectId}/language-settings/bulk`,
3025
3209
  {
3026
3210
  method: "POST",
@@ -3037,7 +3221,7 @@ async function addLanguagesBulk(projectId, languages, autoTranslate = false) {
3037
3221
  }
3038
3222
  }
3039
3223
 
3040
- // src/commands/push/hooks/useLimitCheck.ts
3224
+ // src/commands/upload/hooks/useLimitCheck.ts
3041
3225
  function useLimitCheck(verbose = false) {
3042
3226
  const [state, setState] = useState8({
3043
3227
  checking: false
@@ -3143,7 +3327,7 @@ function calculateUploadableKeys(namespaces, availableSlots) {
3143
3327
  return { limited, totalKeys };
3144
3328
  }
3145
3329
 
3146
- // src/commands/push/components/LimitConfirmation.tsx
3330
+ // src/commands/upload/components/LimitConfirmation.tsx
3147
3331
  import { Box as Box11, Text as Text12, useInput as useInput3 } from "ink";
3148
3332
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
3149
3333
  function LimitConfirmation({ limitExceeded, onConfirm, onCancel }) {
@@ -3211,7 +3395,7 @@ function LimitConfirmation({ limitExceeded, onConfirm, onCancel }) {
3211
3395
  ] });
3212
3396
  }
3213
3397
 
3214
- // src/commands/push/components/PushProgress.tsx
3398
+ // src/commands/upload/components/PushProgress.tsx
3215
3399
  import { Box as Box12, Text as Text13 } from "ink";
3216
3400
  import Spinner5 from "ink-spinner";
3217
3401
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
@@ -3284,7 +3468,7 @@ function PushProgress({
3284
3468
  ] });
3285
3469
  }
3286
3470
 
3287
- // src/commands/push/components/PushResult.tsx
3471
+ // src/commands/upload/components/PushResult.tsx
3288
3472
  import { Box as Box13, Text as Text14 } from "ink";
3289
3473
  import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
3290
3474
  function calculateTotals(allLanguages, languageResults, namespaceResults) {
@@ -4068,7 +4252,7 @@ function readAllTranslationsByLanguage(projectRoot = process.cwd()) {
4068
4252
  };
4069
4253
  }
4070
4254
 
4071
- // src/commands/push/components/PushComponent.tsx
4255
+ // src/commands/upload/components/PushComponent.tsx
4072
4256
  import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
4073
4257
  var isInteractiveTerminal2 = process.stdin.isTTY && process.stdout.isTTY;
4074
4258
  function PushComponent({ options }) {
@@ -4098,12 +4282,16 @@ function PushComponent({ options }) {
4098
4282
  cancel: cancelLimit,
4099
4283
  userConfirmed
4100
4284
  } = useLimitCheck(options.verbose);
4101
- const runPush2 = useCallback5(async () => {
4285
+ const runPush = useCallback5(async () => {
4102
4286
  const resolved = getResolvedApiKey();
4103
4287
  if (!resolved?.key) {
4104
4288
  setError("Not authenticated. Run `npx @intlpullhq/cli login` or set INTLPULL_API_KEY in .env");
4105
4289
  return;
4106
4290
  }
4291
+ if (options.translationKeyPrefix && !validatePrefix(options.translationKeyPrefix)) {
4292
+ setError('Invalid prefix format. Use alphanumeric characters and dots only (e.g., "checkout" or "app.checkout")');
4293
+ return;
4294
+ }
4107
4295
  const config = getProjectConfig();
4108
4296
  let projectId = options.project || config?.projectId;
4109
4297
  if (!projectId) {
@@ -4179,9 +4367,9 @@ To create this branch, go to the IntlPull dashboard.`
4179
4367
  let sourceLanguage = config?.sourceLanguage || "en";
4180
4368
  if (options.file) {
4181
4369
  setStatus("reading", `Reading ${options.file}...`);
4182
- const filePath = join8(cwd, options.file);
4370
+ const filePath = isAbsolute(options.file) ? options.file : resolve(cwd, options.file);
4183
4371
  if (!existsSync8(filePath)) {
4184
- setError(`File not found: ${options.file}`);
4372
+ setError(`File not found: ${filePath}`);
4185
4373
  return;
4186
4374
  }
4187
4375
  try {
@@ -4278,9 +4466,16 @@ To create this branch, go to the IntlPull dashboard.`
4278
4466
  for (let langIdx = 0; langIdx < allLangResult.languages.length; langIdx++) {
4279
4467
  const langData = allLangResult.languages[langIdx];
4280
4468
  updateLanguageResult(langIdx, { status: "pushing" });
4469
+ let namespacesToPush2 = langData.namespaces;
4470
+ if (options.translationKeyPrefix) {
4471
+ namespacesToPush2 = langData.namespaces.map((ns) => ({
4472
+ ...ns,
4473
+ keys: applyPrefix(ns.keys, options.translationKeyPrefix)
4474
+ }));
4475
+ }
4281
4476
  const { results: results2, totals: totals2 } = await pushKeysParallel(
4282
4477
  projectId,
4283
- langData.namespaces,
4478
+ namespacesToPush2,
4284
4479
  langData.language,
4285
4480
  {
4286
4481
  branchId,
@@ -4377,7 +4572,14 @@ To create this branch, go to the IntlPull dashboard.`
4377
4572
  return;
4378
4573
  }
4379
4574
  setStatus("pushing", `Pushing ${totalKeys} keys to IntlPull...`);
4380
- const { results, totals } = await pushKeysParallel(projectId, namespaces, sourceLanguage, {
4575
+ let namespacesToPush = namespaces;
4576
+ if (options.translationKeyPrefix) {
4577
+ namespacesToPush = namespaces.map((ns) => ({
4578
+ ...ns,
4579
+ keys: applyPrefix(ns.keys, options.translationKeyPrefix)
4580
+ }));
4581
+ }
4582
+ const { results, totals } = await pushKeysParallel(projectId, namespacesToPush, sourceLanguage, {
4381
4583
  branchId,
4382
4584
  platforms: options.platform && options.platform !== "all" ? [options.platform] : void 0
4383
4585
  });
@@ -4405,7 +4607,7 @@ To create this branch, go to the IntlPull dashboard.`
4405
4607
  complete(results.some((r) => r.error));
4406
4608
  }, [options, limitState]);
4407
4609
  useEffect7(() => {
4408
- runPush2();
4610
+ runPush();
4409
4611
  }, []);
4410
4612
  useEffect7(() => {
4411
4613
  if (userConfirmed && state.status === "waiting_confirmation" && state.pushContext) {
@@ -4427,13 +4629,27 @@ To create this branch, go to the IntlPull dashboard.`
4427
4629
  const sourceLanguageData = allLangResult.languages.find((l) => l.language === sourceLanguage);
4428
4630
  if (sourceLanguageData) {
4429
4631
  const { limited } = calculateUploadableKeys(sourceLanguageData.namespaces, availableSlots);
4430
- const { results, totals } = await pushKeysParallel(projectId, limited, sourceLanguage, {
4632
+ let limitedToPost = limited;
4633
+ if (options.translationKeyPrefix) {
4634
+ limitedToPost = limited.map((ns) => ({
4635
+ ...ns,
4636
+ keys: applyPrefix(ns.keys, options.translationKeyPrefix)
4637
+ }));
4638
+ }
4639
+ const { results, totals } = await pushKeysParallel(projectId, limitedToPost, sourceLanguage, {
4431
4640
  branchId,
4432
4641
  platforms: platform && platform !== "all" ? [platform] : void 0
4433
4642
  });
4434
4643
  for (const langData of allLangResult.languages) {
4435
4644
  if (langData.language === sourceLanguage) continue;
4436
- await pushKeysParallel(projectId, langData.namespaces, langData.language, {
4645
+ let namespacesToPush = langData.namespaces;
4646
+ if (options.translationKeyPrefix) {
4647
+ namespacesToPush = langData.namespaces.map((ns) => ({
4648
+ ...ns,
4649
+ keys: applyPrefix(ns.keys, options.translationKeyPrefix)
4650
+ }));
4651
+ }
4652
+ await pushKeysParallel(projectId, namespacesToPush, langData.language, {
4437
4653
  branchId,
4438
4654
  platforms: platform && platform !== "all" ? [platform] : void 0
4439
4655
  });
@@ -4442,7 +4658,14 @@ To create this branch, go to the IntlPull dashboard.`
4442
4658
  }
4443
4659
  } else if (namespaces) {
4444
4660
  const { limited, totalKeys } = calculateUploadableKeys(namespaces, availableSlots);
4445
- const { results } = await pushKeysParallel(projectId, limited, sourceLanguage, {
4661
+ let limitedToPost = limited;
4662
+ if (options.translationKeyPrefix) {
4663
+ limitedToPost = limited.map((ns) => ({
4664
+ ...ns,
4665
+ keys: applyPrefix(ns.keys, options.translationKeyPrefix)
4666
+ }));
4667
+ }
4668
+ const { results } = await pushKeysParallel(projectId, limitedToPost, sourceLanguage, {
4446
4669
  branchId,
4447
4670
  platforms: platform && platform !== "all" ? [platform] : void 0
4448
4671
  });
@@ -4510,9 +4733,9 @@ To create this branch, go to the IntlPull dashboard.`
4510
4733
  );
4511
4734
  }
4512
4735
 
4513
- // src/commands/push.tsx
4736
+ // src/commands/upload.tsx
4514
4737
  import { jsx as jsx17 } from "react/jsx-runtime";
4515
- function runPush(options) {
4738
+ function runUpload(options) {
4516
4739
  render5(/* @__PURE__ */ jsx17(PushComponent, { options }));
4517
4740
  }
4518
4741
 
@@ -4589,7 +4812,20 @@ function ExportComponent({ options }) {
4589
4812
  platform: options.platform
4590
4813
  };
4591
4814
  const blob = await api.exportTranslations(projectId, exportOptions);
4592
- const extension = format === "android" ? "zip" : format === "ios" ? "zip" : format;
4815
+ const extensionMap = {
4816
+ json: "json",
4817
+ yaml: "yaml",
4818
+ ts: "ts",
4819
+ android: "zip",
4820
+ ios: "zip",
4821
+ xliff: "xliff",
4822
+ po: "po",
4823
+ arb: "arb",
4824
+ stringsdict: "stringsdict",
4825
+ xcstrings: "xcstrings",
4826
+ csv: "csv"
4827
+ };
4828
+ const extension = extensionMap[format] || format;
4593
4829
  const outputFile = options.output || `translations.${extension}`;
4594
4830
  setState({
4595
4831
  status: "saving",
@@ -4684,11 +4920,44 @@ function detectFormatFromExtension(filename) {
4684
4920
  case ".yaml":
4685
4921
  case ".yml":
4686
4922
  return "yaml";
4923
+ case ".xliff":
4924
+ case ".xlf":
4925
+ return "xliff";
4926
+ case ".po":
4927
+ case ".pot":
4928
+ return "po";
4929
+ case ".arb":
4930
+ return "arb";
4931
+ case ".stringsdict":
4932
+ return "stringsdict";
4933
+ case ".xcstrings":
4934
+ return "xcstrings";
4935
+ case ".csv":
4936
+ return "csv";
4937
+ case ".strings":
4938
+ return "ios";
4939
+ case ".xml":
4940
+ return "android";
4687
4941
  case ".json":
4688
4942
  default:
4689
4943
  return "json";
4690
4944
  }
4691
4945
  }
4946
+ var SUPPORTED_EXTENSIONS = [
4947
+ ".json",
4948
+ ".yaml",
4949
+ ".yml",
4950
+ ".xliff",
4951
+ ".xlf",
4952
+ ".po",
4953
+ ".pot",
4954
+ ".arb",
4955
+ ".stringsdict",
4956
+ ".xcstrings",
4957
+ ".csv",
4958
+ ".strings",
4959
+ ".xml"
4960
+ ];
4692
4961
  function findTranslationFiles(dir, pattern) {
4693
4962
  const files = [];
4694
4963
  try {
@@ -4696,7 +4965,7 @@ function findTranslationFiles(dir, pattern) {
4696
4965
  for (const entry of entries) {
4697
4966
  if (entry.isFile()) {
4698
4967
  const ext = extname4(entry.name).toLowerCase();
4699
- if ([".json", ".yaml", ".yml"].includes(ext)) {
4968
+ if (SUPPORTED_EXTENSIONS.includes(ext)) {
4700
4969
  if (pattern) {
4701
4970
  const regex = new RegExp(pattern.replace(/\*/g, ".*").replace(/\?/g, "."));
4702
4971
  if (!regex.test(entry.name)) continue;
@@ -5812,7 +6081,7 @@ var LokaliseMigrator = class {
5812
6081
  hasMore = keys.length === limit;
5813
6082
  page++;
5814
6083
  if (hasMore) {
5815
- await new Promise((resolve) => setTimeout(resolve, 200));
6084
+ await new Promise((resolve2) => setTimeout(resolve2, 200));
5816
6085
  }
5817
6086
  }
5818
6087
  return allKeys;
@@ -5881,7 +6150,7 @@ var LokaliseMigrator = class {
5881
6150
  hasMore = entries.length === limit;
5882
6151
  page++;
5883
6152
  if (hasMore) {
5884
- await new Promise((resolve) => setTimeout(resolve, 200));
6153
+ await new Promise((resolve2) => setTimeout(resolve2, 200));
5885
6154
  }
5886
6155
  }
5887
6156
  } catch {
@@ -5929,7 +6198,7 @@ var LokaliseMigrator = class {
5929
6198
  hasMore = terms.length === limit;
5930
6199
  page++;
5931
6200
  if (hasMore) {
5932
- await new Promise((resolve) => setTimeout(resolve, 200));
6201
+ await new Promise((resolve2) => setTimeout(resolve2, 200));
5933
6202
  }
5934
6203
  }
5935
6204
  }
@@ -6148,7 +6417,7 @@ var CrowdinMigrator = class {
6148
6417
  hasMore = strings.length === limit;
6149
6418
  offset += limit;
6150
6419
  if (hasMore) {
6151
- await new Promise((resolve) => setTimeout(resolve, 100));
6420
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
6152
6421
  }
6153
6422
  }
6154
6423
  return allKeys;
@@ -6246,7 +6515,7 @@ var CrowdinMigrator = class {
6246
6515
  hasMore = segments.length === limit;
6247
6516
  offset += limit;
6248
6517
  if (hasMore) {
6249
- await new Promise((resolve) => setTimeout(resolve, 100));
6518
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
6250
6519
  }
6251
6520
  } catch {
6252
6521
  hasMore = false;
@@ -6294,7 +6563,7 @@ var CrowdinMigrator = class {
6294
6563
  hasMore = terms.length === limit;
6295
6564
  offset += limit;
6296
6565
  if (hasMore) {
6297
- await new Promise((resolve) => setTimeout(resolve, 100));
6566
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
6298
6567
  }
6299
6568
  }
6300
6569
  }
@@ -6482,7 +6751,7 @@ var PhraseMigrator = class {
6482
6751
  hasMore = projects2.length === perPage;
6483
6752
  page++;
6484
6753
  if (hasMore) {
6485
- await new Promise((resolve) => setTimeout(resolve, 200));
6754
+ await new Promise((resolve2) => setTimeout(resolve2, 200));
6486
6755
  }
6487
6756
  }
6488
6757
  return allProjects;
@@ -6538,7 +6807,7 @@ var PhraseMigrator = class {
6538
6807
  hasMore = keys.length === perPage;
6539
6808
  page++;
6540
6809
  if (hasMore) {
6541
- await new Promise((resolve) => setTimeout(resolve, 300));
6810
+ await new Promise((resolve2) => setTimeout(resolve2, 300));
6542
6811
  }
6543
6812
  }
6544
6813
  return allKeys;
@@ -6624,7 +6893,7 @@ var PhraseMigrator = class {
6624
6893
  hasMore = terms.length === perPage;
6625
6894
  page++;
6626
6895
  if (hasMore) {
6627
- await new Promise((resolve) => setTimeout(resolve, 200));
6896
+ await new Promise((resolve2) => setTimeout(resolve2, 200));
6628
6897
  }
6629
6898
  }
6630
6899
  }
@@ -7747,7 +8016,7 @@ Make sure your files follow naming conventions like:
7747
8016
  });
7748
8017
  try {
7749
8018
  if (optionsDryRun) {
7750
- await new Promise((resolve) => setTimeout(resolve, 500));
8019
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
7751
8020
  setUploadStatuses((prev) => {
7752
8021
  const updated = [...prev];
7753
8022
  updated[index] = {
@@ -8039,7 +8308,7 @@ function runMigrateFiles(path4, options) {
8039
8308
  // src/commands/check.tsx
8040
8309
  import { useState as useState15, useEffect as useEffect12 } from "react";
8041
8310
  import { render as render10, Box as Box27, Text as Text28 } from "ink";
8042
- import { glob } from "glob";
8311
+ import { glob as glob2 } from "glob";
8043
8312
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
8044
8313
 
8045
8314
  // src/components/TaskList.tsx
@@ -8180,7 +8449,7 @@ function CheckCommand({ options }) {
8180
8449
  ];
8181
8450
  let localeFiles = [];
8182
8451
  for (const pattern of patterns) {
8183
- const files = await glob(pattern, { absolute: true });
8452
+ const files = await glob2(pattern, { absolute: true });
8184
8453
  if (files.length > 0) {
8185
8454
  localeFiles = files;
8186
8455
  break;
@@ -8776,7 +9045,7 @@ function runDiff(options) {
8776
9045
  import { useState as useState17, useEffect as useEffect14 } from "react";
8777
9046
  import { render as render12, Box as Box29, Text as Text30 } from "ink";
8778
9047
  import { readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
8779
- import { glob as glob2 } from "glob";
9048
+ import { glob as glob3 } from "glob";
8780
9049
  import { jsx as jsx33, jsxs as jsxs30 } from "react/jsx-runtime";
8781
9050
  function FixCommand({ options }) {
8782
9051
  const [tasks, setTasks] = useState17([
@@ -8816,7 +9085,7 @@ function FixCommand({ options }) {
8816
9085
  ];
8817
9086
  let localeFiles = [];
8818
9087
  for (const pattern of patterns) {
8819
- const files = await glob2(pattern, { absolute: true });
9088
+ const files = await glob3(pattern, { absolute: true });
8820
9089
  if (files.length > 0) {
8821
9090
  localeFiles = files;
8822
9091
  break;
@@ -11319,101 +11588,2767 @@ documentsCommand.command("download").description("Download a translated document
11319
11588
  });
11320
11589
  });
11321
11590
 
11322
- // src/index.tsx
11323
- var program = new Command2();
11324
- program.name("intlpull").description("Intelligent i18n CLI for modern apps").version("0.1.5").option("--env-file <path>", "Path to custom env file (e.g., .env.production)").hook("preAction", (thisCommand) => {
11325
- const envFile = thisCommand.opts().envFile;
11326
- if (envFile) {
11327
- setCustomEnvFile(envFile);
11591
+ // src/commands/keys/index.tsx
11592
+ import { Command as Command2 } from "commander";
11593
+
11594
+ // src/commands/keys/list.tsx
11595
+ import { useState as useState26, useEffect as useEffect23 } from "react";
11596
+ import { render as render21, Box as Box36, Text as Text39, Newline as Newline2 } from "ink";
11597
+ import Spinner18 from "ink-spinner";
11598
+ import { jsx as jsx42, jsxs as jsxs39 } from "react/jsx-runtime";
11599
+ function SimpleTable2({ data }) {
11600
+ if (data.length === 0) return null;
11601
+ const headers = Object.keys(data[0]);
11602
+ const colWidths = headers.map(
11603
+ (h) => Math.max(h.length, ...data.map((row) => String(row[h] || "").length))
11604
+ );
11605
+ const separator = "\u2500".repeat(colWidths.reduce((a, b) => a + b + 3, 1));
11606
+ return /* @__PURE__ */ jsxs39(Box36, { flexDirection: "column", children: [
11607
+ /* @__PURE__ */ jsx42(Text39, { children: separator }),
11608
+ /* @__PURE__ */ jsxs39(Text39, { children: [
11609
+ "\u2502 ",
11610
+ headers.map((h, i) => h.padEnd(colWidths[i])).join(" \u2502 "),
11611
+ " \u2502"
11612
+ ] }),
11613
+ /* @__PURE__ */ jsx42(Text39, { children: separator }),
11614
+ data.map((row, rowIdx) => /* @__PURE__ */ jsxs39(Text39, { children: [
11615
+ "\u2502 ",
11616
+ headers.map((h, i) => String(row[h] || "").padEnd(colWidths[i])).join(" \u2502 "),
11617
+ " \u2502"
11618
+ ] }, rowIdx)),
11619
+ /* @__PURE__ */ jsx42(Text39, { children: separator })
11620
+ ] });
11621
+ }
11622
+ var KeysList = ({ options }) => {
11623
+ const [keys, setKeys] = useState26([]);
11624
+ const [isLoading, setIsLoading] = useState26(true);
11625
+ const [error, setError] = useState26(null);
11626
+ useEffect23(() => {
11627
+ const fetchKeys = async () => {
11628
+ try {
11629
+ const config = getProjectConfig();
11630
+ const projectId = options.project || config?.projectId;
11631
+ if (!projectId) {
11632
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
11633
+ setIsLoading(false);
11634
+ return;
11635
+ }
11636
+ const api = new KeysApi();
11637
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : void 0;
11638
+ const limit = options.limit ? parseInt(options.limit, 10) : void 0;
11639
+ const result = await api.listKeys(projectId, {
11640
+ namespace: options.namespace,
11641
+ search: options.search,
11642
+ tags,
11643
+ limit
11644
+ });
11645
+ setKeys(result.keys);
11646
+ } catch (err) {
11647
+ setError(err instanceof Error ? err.message : "Failed to fetch keys");
11648
+ } finally {
11649
+ setIsLoading(false);
11650
+ }
11651
+ };
11652
+ fetchKeys();
11653
+ }, [options]);
11654
+ if (error) {
11655
+ return /* @__PURE__ */ jsxs39(Text39, { color: "red", children: [
11656
+ "Error: ",
11657
+ error
11658
+ ] });
11328
11659
  }
11329
- const { loaded } = loadEnvFiles();
11330
- if (loaded.length > 0 && process.env.INTLPULL_DEBUG) {
11331
- console.log(`Loaded env from: ${loaded.join(", ")}`);
11660
+ if (isLoading) {
11661
+ return /* @__PURE__ */ jsxs39(Text39, { children: [
11662
+ /* @__PURE__ */ jsx42(Text39, { color: "green", children: /* @__PURE__ */ jsx42(Spinner18, { type: "dots" }) }),
11663
+ " Loading keys..."
11664
+ ] });
11332
11665
  }
11333
- });
11334
- program.command("init").description("Initialize IntlPull in your project").option("--framework <framework>", "Framework (next|react|vue|svelte|astro)").option("--library <library>", "i18n library (next-intl|react-i18next|vue-i18n)").option("--output <dir>", "Output directory for translation files").option("--project <id>", "Project ID to link").option("-y, --yes", "Auto-detect and initialize without prompts (non-interactive mode)").action((options) => {
11335
- runInit({
11336
- framework: options.framework,
11337
- library: options.library,
11338
- output: options.output,
11339
- project: options.project,
11340
- yes: options.yes
11341
- });
11342
- });
11343
- program.command("status").description("Show project status and translation progress").action(() => {
11344
- runStatus();
11345
- });
11346
- program.command("login").description("Authenticate with IntlPull").option("--token <token>", "Use API token (prefer INTLPULL_API_KEY env var for security)").action((options) => {
11347
- runLogin({ token: options.token });
11348
- });
11349
- program.command("logout").description("Clear stored credentials").action(async () => {
11350
- const { clearAuthConfig } = await import("./config-DX34FMWV.js");
11351
- clearAuthConfig();
11352
- console.log("\u2713 Logged out successfully");
11353
- });
11354
- program.command("whoami").description("Show current authenticated user").action(async () => {
11355
- const { getResolvedApiKey: getResolvedApiKey2, getAuthErrorMessage: getAuthErrorMessage2 } = await import("./config-DX34FMWV.js");
11356
- const { createApiClient: createApiClient2 } = await import("./api-K3AY4VAI.js");
11357
- const resolved = getResolvedApiKey2();
11358
- if (!resolved) {
11359
- console.log(getAuthErrorMessage2());
11360
- return;
11666
+ if (keys.length === 0) {
11667
+ return /* @__PURE__ */ jsx42(Text39, { children: "No keys found." });
11361
11668
  }
11362
- try {
11363
- const api = createApiClient2();
11364
- const { projects: projects2 } = await api.listProjects();
11365
- const sourceLabel = resolved.source === "env" ? "INTLPULL_API_KEY env var" : "~/.intlpull/auth.json";
11366
- console.log("\n\u2713 Authenticated successfully\n");
11367
- console.log(" API Key:", resolved.key.substring(0, 20) + "...");
11368
- console.log(" Source:", sourceLabel);
11369
- console.log(" Projects:", projects2.length);
11370
- if (projects2.length > 0) {
11371
- console.log("\n Your projects:");
11372
- for (const p of projects2.slice(0, 5)) {
11373
- console.log(` - ${p.name} (${p.slug})`);
11374
- }
11375
- if (projects2.length > 5) {
11376
- console.log(` ... and ${projects2.length - 5} more`);
11377
- }
11378
- }
11379
- console.log("");
11380
- } catch (err) {
11381
- console.log("API Key:", resolved.key.substring(0, 15) + "...");
11382
- console.error("Error verifying API key:", err instanceof Error ? err.message : "Unknown error");
11669
+ if (options.format === "json") {
11670
+ console.log(JSON.stringify(keys, null, 2));
11671
+ return null;
11383
11672
  }
11384
- });
11385
- program.command("upload").alias("push").description("Upload translation keys to IntlPull (auto-detects project and files)").option("--project <id>", "Project ID (auto-detected with project-scoped API key)").option("--file <path>", "Specific file to upload (e.g., messages/admin.json)").option("--branch <name>", "Branch name (auto-detected from git, pushes to IntlPull branch)").option("--platform <platform>", "Platform to tag keys with (ios, android, web)").option("--all-languages", "Upload translations for ALL languages (default: true)", true).option("--source-only", "Upload only source language keys", false).option("--dry-run", "Preview without uploading", false).option("-v, --verbose", "Show detailed detection info", false).option("-q, --quiet", "Suppress output except errors (CI mode)").action((options) => {
11386
- runPush({
11387
- project: options.project,
11388
- file: options.file,
11389
- branch: options.branch,
11390
- platform: options.platform,
11391
- // --source-only overrides --all-languages
11392
- allLanguages: options.sourceOnly ? false : options.allLanguages,
11393
- dryRun: options.dryRun,
11394
- verbose: options.verbose,
11395
- quiet: options.quiet
11396
- });
11397
- });
11398
- program.command("download").alias("pull").description("Download translations from IntlPull (auto-detects project, framework, and languages)").option("--project <id>", "Project ID (auto-detected with project-scoped API key)").option("--format <format>", "Output format (json|yaml|ts)", "json").option("--output <dir>", "Output directory (auto-detected from framework)").option("--languages <langs>", "Languages to download (comma-separated, defaults to all)").option("--branch <branch>", "Translation branch to download from").option("--platform <platform>", "Platform variant (default|ios|android|web)").option("--no-parallel", "Disable parallel fetching (use single request)").option("-i, --interactive", "Run in interactive mode with prompts").option("-q, --quiet", "Suppress output except errors").option("--clean-orphans [mode]", 'Remove orphaned files (files not in remote). Use "dry-run" to preview').option("-f, --force", "Skip confirmation prompts for destructive operations").action((options) => {
11399
- let cleanOrphans;
11400
- if (options.cleanOrphans === true || options.cleanOrphans === "") {
11401
- cleanOrphans = true;
11402
- } else if (options.cleanOrphans === "dry-run") {
11673
+ const data = keys.map((key) => ({
11674
+ Key: key.key,
11675
+ Namespace: key.namespace || "-",
11676
+ Description: key.description ? key.description.length > 40 ? key.description.substring(0, 37) + "..." : key.description : "-",
11677
+ Tags: key.tags && key.tags.length > 0 ? key.tags.join(", ") : "-"
11678
+ }));
11679
+ return /* @__PURE__ */ jsxs39(Box36, { flexDirection: "column", children: [
11680
+ /* @__PURE__ */ jsxs39(Text39, { children: [
11681
+ "Found ",
11682
+ keys.length,
11683
+ " key(s)"
11684
+ ] }),
11685
+ /* @__PURE__ */ jsx42(Newline2, {}),
11686
+ /* @__PURE__ */ jsx42(SimpleTable2, { data })
11687
+ ] });
11688
+ };
11689
+ function runKeysList(options) {
11690
+ render21(/* @__PURE__ */ jsx42(KeysList, { options }));
11691
+ }
11692
+
11693
+ // src/commands/keys/create.tsx
11694
+ import { useState as useState27, useEffect as useEffect24 } from "react";
11695
+ import { render as render22, Box as Box37, Text as Text40 } from "ink";
11696
+ import Spinner19 from "ink-spinner";
11697
+ import { jsx as jsx43, jsxs as jsxs40 } from "react/jsx-runtime";
11698
+ var KeysCreate = ({ keyName, options }) => {
11699
+ const [result, setResult] = useState27(null);
11700
+ const [isLoading, setIsLoading] = useState27(true);
11701
+ const [error, setError] = useState27(null);
11702
+ useEffect24(() => {
11703
+ const createKey = async () => {
11704
+ try {
11705
+ const config = getProjectConfig();
11706
+ const projectId = options.project || config?.projectId;
11707
+ if (!projectId) {
11708
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
11709
+ setIsLoading(false);
11710
+ return;
11711
+ }
11712
+ if (!options.value) {
11713
+ setError("Value is required. Use --value <translation>");
11714
+ setIsLoading(false);
11715
+ return;
11716
+ }
11717
+ const api = new KeysApi();
11718
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : void 0;
11719
+ const { ProjectsApi: ProjectsApi2 } = await import("./projects-AMQMORAR.js");
11720
+ const projectsApi = new ProjectsApi2();
11721
+ const project = await projectsApi.getProject(projectId);
11722
+ const created = await api.createKey(projectId, {
11723
+ namespace: options.namespace || "default",
11724
+ key: keyName,
11725
+ value: options.value,
11726
+ language: project.source_language,
11727
+ context: options.context,
11728
+ tags
11729
+ });
11730
+ setResult(created);
11731
+ } catch (err) {
11732
+ setError(err instanceof Error ? err.message : "Failed to create key");
11733
+ } finally {
11734
+ setIsLoading(false);
11735
+ }
11736
+ };
11737
+ createKey();
11738
+ }, [keyName, options]);
11739
+ if (error) {
11740
+ return /* @__PURE__ */ jsxs40(Text40, { color: "red", children: [
11741
+ "Error: ",
11742
+ error
11743
+ ] });
11744
+ }
11745
+ if (isLoading) {
11746
+ return /* @__PURE__ */ jsxs40(Text40, { children: [
11747
+ /* @__PURE__ */ jsx43(Text40, { color: "green", children: /* @__PURE__ */ jsx43(Spinner19, { type: "dots" }) }),
11748
+ " Creating key..."
11749
+ ] });
11750
+ }
11751
+ if (!result) {
11752
+ return /* @__PURE__ */ jsx43(Text40, { color: "red", children: "Failed to create key" });
11753
+ }
11754
+ return /* @__PURE__ */ jsxs40(Box37, { flexDirection: "column", children: [
11755
+ /* @__PURE__ */ jsx43(Text40, { color: "green", children: "\u2713 Key created successfully" }),
11756
+ /* @__PURE__ */ jsxs40(Text40, { children: [
11757
+ "Key: ",
11758
+ result.key
11759
+ ] }),
11760
+ /* @__PURE__ */ jsxs40(Text40, { children: [
11761
+ "Namespace: ",
11762
+ result.namespace || "-"
11763
+ ] }),
11764
+ result.description && /* @__PURE__ */ jsxs40(Text40, { children: [
11765
+ "Description: ",
11766
+ result.description
11767
+ ] }),
11768
+ result.tags && result.tags.length > 0 && /* @__PURE__ */ jsxs40(Text40, { children: [
11769
+ "Tags: ",
11770
+ result.tags.join(", ")
11771
+ ] })
11772
+ ] });
11773
+ };
11774
+ function runKeysCreate(keyName, options) {
11775
+ render22(/* @__PURE__ */ jsx43(KeysCreate, { keyName, options }));
11776
+ }
11777
+
11778
+ // src/commands/keys/update.tsx
11779
+ import { useState as useState28, useEffect as useEffect25 } from "react";
11780
+ import { render as render23, Box as Box38, Text as Text41 } from "ink";
11781
+ import Spinner20 from "ink-spinner";
11782
+ import { jsx as jsx44, jsxs as jsxs41 } from "react/jsx-runtime";
11783
+ var KeysUpdate = ({ keyName, options }) => {
11784
+ const [result, setResult] = useState28(null);
11785
+ const [isLoading, setIsLoading] = useState28(true);
11786
+ const [error, setError] = useState28(null);
11787
+ useEffect25(() => {
11788
+ const updateKey = async () => {
11789
+ try {
11790
+ const config = getProjectConfig();
11791
+ const projectId = options.project || config?.projectId;
11792
+ if (!projectId) {
11793
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
11794
+ setIsLoading(false);
11795
+ return;
11796
+ }
11797
+ const api = new KeysApi();
11798
+ const keysResult = await api.listKeys(projectId, { search: keyName });
11799
+ const existingKey = keysResult.keys.find((k) => k.key === keyName);
11800
+ if (!existingKey) {
11801
+ setError(`Key "${keyName}" not found`);
11802
+ setIsLoading(false);
11803
+ return;
11804
+ }
11805
+ const updateData = {};
11806
+ if (options.context !== void 0) {
11807
+ updateData.context = options.context;
11808
+ }
11809
+ if (options.tags !== void 0) {
11810
+ updateData.tags = options.tags.split(",").map((t) => t.trim());
11811
+ }
11812
+ if (Object.keys(updateData).length === 0) {
11813
+ setError("No update fields provided. Use --context or --tags");
11814
+ setIsLoading(false);
11815
+ return;
11816
+ }
11817
+ const updated = await api.updateKey(projectId, existingKey.id, updateData);
11818
+ setResult(updated);
11819
+ } catch (err) {
11820
+ setError(err instanceof Error ? err.message : "Failed to update key");
11821
+ } finally {
11822
+ setIsLoading(false);
11823
+ }
11824
+ };
11825
+ updateKey();
11826
+ }, [keyName, options]);
11827
+ if (error) {
11828
+ return /* @__PURE__ */ jsxs41(Text41, { color: "red", children: [
11829
+ "Error: ",
11830
+ error
11831
+ ] });
11832
+ }
11833
+ if (isLoading) {
11834
+ return /* @__PURE__ */ jsxs41(Text41, { children: [
11835
+ /* @__PURE__ */ jsx44(Text41, { color: "green", children: /* @__PURE__ */ jsx44(Spinner20, { type: "dots" }) }),
11836
+ " Updating key..."
11837
+ ] });
11838
+ }
11839
+ if (!result) {
11840
+ return /* @__PURE__ */ jsx44(Text41, { color: "red", children: "Failed to update key" });
11841
+ }
11842
+ return /* @__PURE__ */ jsxs41(Box38, { flexDirection: "column", children: [
11843
+ /* @__PURE__ */ jsx44(Text41, { color: "green", children: "\u2713 Key updated successfully" }),
11844
+ /* @__PURE__ */ jsxs41(Text41, { children: [
11845
+ "Key: ",
11846
+ result.key
11847
+ ] }),
11848
+ /* @__PURE__ */ jsxs41(Text41, { children: [
11849
+ "Namespace: ",
11850
+ result.namespace || "-"
11851
+ ] }),
11852
+ result.description && /* @__PURE__ */ jsxs41(Text41, { children: [
11853
+ "Description: ",
11854
+ result.description
11855
+ ] }),
11856
+ result.tags && result.tags.length > 0 && /* @__PURE__ */ jsxs41(Text41, { children: [
11857
+ "Tags: ",
11858
+ result.tags.join(", ")
11859
+ ] })
11860
+ ] });
11861
+ };
11862
+ function runKeysUpdate(keyName, options) {
11863
+ render23(/* @__PURE__ */ jsx44(KeysUpdate, { keyName, options }));
11864
+ }
11865
+
11866
+ // src/commands/keys/delete.tsx
11867
+ import { useState as useState29, useEffect as useEffect26 } from "react";
11868
+ import { render as render24, Box as Box39, Text as Text42, useInput as useInput8 } from "ink";
11869
+ import Spinner21 from "ink-spinner";
11870
+ import { jsx as jsx45, jsxs as jsxs42 } from "react/jsx-runtime";
11871
+ var KeysDelete = ({ keyName, options }) => {
11872
+ const [key, setKey] = useState29(null);
11873
+ const [isLoading, setIsLoading] = useState29(true);
11874
+ const [error, setError] = useState29(null);
11875
+ const [needsConfirmation, setNeedsConfirmation] = useState29(!options.force);
11876
+ const [isDeleting, setIsDeleting] = useState29(false);
11877
+ const [deleted, setDeleted] = useState29(false);
11878
+ useEffect26(() => {
11879
+ const findKey = async () => {
11880
+ try {
11881
+ const config = getProjectConfig();
11882
+ const projectId = options.project || config?.projectId;
11883
+ if (!projectId) {
11884
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
11885
+ setIsLoading(false);
11886
+ return;
11887
+ }
11888
+ const api = new KeysApi();
11889
+ const keysResult = await api.listKeys(projectId, { search: keyName });
11890
+ const existingKey = keysResult.keys.find((k) => k.key === keyName);
11891
+ if (!existingKey) {
11892
+ setError(`Key "${keyName}" not found`);
11893
+ setIsLoading(false);
11894
+ return;
11895
+ }
11896
+ setKey(existingKey);
11897
+ setIsLoading(false);
11898
+ if (options.force) {
11899
+ await performDelete(projectId, existingKey.id, api);
11900
+ }
11901
+ } catch (err) {
11902
+ setError(err instanceof Error ? err.message : "Failed to find key");
11903
+ setIsLoading(false);
11904
+ }
11905
+ };
11906
+ findKey();
11907
+ }, [keyName, options.force, options.project]);
11908
+ const performDelete = async (projectId, keyId, api) => {
11909
+ try {
11910
+ setIsDeleting(true);
11911
+ await api.deleteKey(projectId, keyId);
11912
+ setDeleted(true);
11913
+ setNeedsConfirmation(false);
11914
+ } catch (err) {
11915
+ setError(err instanceof Error ? err.message : "Failed to delete key");
11916
+ } finally {
11917
+ setIsDeleting(false);
11918
+ }
11919
+ };
11920
+ useInput8((input, inputKey) => {
11921
+ if (!needsConfirmation || isDeleting) return;
11922
+ if (input.toLowerCase() === "y") {
11923
+ const config = getProjectConfig();
11924
+ const projectId = options.project || config?.projectId;
11925
+ if (projectId && key) {
11926
+ const api = new KeysApi();
11927
+ performDelete(projectId, key.id, api);
11928
+ }
11929
+ } else if (input.toLowerCase() === "n" || inputKey.escape) {
11930
+ process.exit(0);
11931
+ }
11932
+ });
11933
+ if (error) {
11934
+ return /* @__PURE__ */ jsxs42(Text42, { color: "red", children: [
11935
+ "Error: ",
11936
+ error
11937
+ ] });
11938
+ }
11939
+ if (isLoading) {
11940
+ return /* @__PURE__ */ jsxs42(Text42, { children: [
11941
+ /* @__PURE__ */ jsx45(Text42, { color: "green", children: /* @__PURE__ */ jsx45(Spinner21, { type: "dots" }) }),
11942
+ " Finding key..."
11943
+ ] });
11944
+ }
11945
+ if (isDeleting) {
11946
+ return /* @__PURE__ */ jsxs42(Text42, { children: [
11947
+ /* @__PURE__ */ jsx45(Text42, { color: "green", children: /* @__PURE__ */ jsx45(Spinner21, { type: "dots" }) }),
11948
+ " Deleting key..."
11949
+ ] });
11950
+ }
11951
+ if (deleted) {
11952
+ return /* @__PURE__ */ jsxs42(Box39, { flexDirection: "column", children: [
11953
+ /* @__PURE__ */ jsx45(Text42, { color: "green", children: "\u2713 Key deleted successfully" }),
11954
+ /* @__PURE__ */ jsxs42(Text42, { children: [
11955
+ "Key: ",
11956
+ keyName
11957
+ ] })
11958
+ ] });
11959
+ }
11960
+ if (needsConfirmation && key) {
11961
+ return /* @__PURE__ */ jsxs42(Box39, { flexDirection: "column", children: [
11962
+ /* @__PURE__ */ jsx45(Box39, { marginBottom: 1, children: /* @__PURE__ */ jsx45(Text42, { color: "yellow", children: "Warning: Delete Key" }) }),
11963
+ /* @__PURE__ */ jsxs42(Box39, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
11964
+ /* @__PURE__ */ jsx45(Text42, { children: "You are about to delete the following key:" }),
11965
+ /* @__PURE__ */ jsx45(Text42, { dimColor: true }),
11966
+ /* @__PURE__ */ jsxs42(Text42, { children: [
11967
+ " Key: ",
11968
+ /* @__PURE__ */ jsx45(Text42, { bold: true, children: key.key })
11969
+ ] }),
11970
+ /* @__PURE__ */ jsxs42(Text42, { children: [
11971
+ " Namespace: ",
11972
+ /* @__PURE__ */ jsx45(Text42, { bold: true, children: key.namespace || "-" })
11973
+ ] }),
11974
+ key.description && /* @__PURE__ */ jsxs42(Text42, { children: [
11975
+ " Description: ",
11976
+ key.description
11977
+ ] }),
11978
+ key.tags && key.tags.length > 0 && /* @__PURE__ */ jsxs42(Text42, { children: [
11979
+ " Tags: ",
11980
+ key.tags.join(", ")
11981
+ ] })
11982
+ ] }),
11983
+ /* @__PURE__ */ jsxs42(Box39, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
11984
+ /* @__PURE__ */ jsx45(Text42, { color: "red", children: "This will delete the key and all its translations." }),
11985
+ /* @__PURE__ */ jsx45(Text42, { dimColor: true, children: "This action cannot be undone." })
11986
+ ] }),
11987
+ /* @__PURE__ */ jsx45(Box39, { marginLeft: 2, children: /* @__PURE__ */ jsxs42(Text42, { children: [
11988
+ "Press",
11989
+ " ",
11990
+ /* @__PURE__ */ jsx45(Text42, { bold: true, color: "green", children: "Y" }),
11991
+ " ",
11992
+ "to confirm,",
11993
+ " ",
11994
+ /* @__PURE__ */ jsx45(Text42, { bold: true, color: "red", children: "N" }),
11995
+ " ",
11996
+ "to cancel"
11997
+ ] }) })
11998
+ ] });
11999
+ }
12000
+ return null;
12001
+ };
12002
+ function runKeysDelete(keyName, options) {
12003
+ render24(/* @__PURE__ */ jsx45(KeysDelete, { keyName, options }));
12004
+ }
12005
+
12006
+ // src/commands/keys/bulk-delete.tsx
12007
+ import { useState as useState30, useEffect as useEffect27 } from "react";
12008
+ import { render as render25, Box as Box40, Text as Text43, useInput as useInput9 } from "ink";
12009
+ import Spinner22 from "ink-spinner";
12010
+ import { jsx as jsx46, jsxs as jsxs43 } from "react/jsx-runtime";
12011
+ var KeysBulkDelete = ({ options }) => {
12012
+ const [keys, setKeys] = useState30([]);
12013
+ const [isLoading, setIsLoading] = useState30(true);
12014
+ const [error, setError] = useState30(null);
12015
+ const [needsConfirmation, setNeedsConfirmation] = useState30(!options.force);
12016
+ const [isDeleting, setIsDeleting] = useState30(false);
12017
+ const [deleted, setDeleted] = useState30(false);
12018
+ const [deletedCount, setDeletedCount] = useState30(0);
12019
+ useEffect27(() => {
12020
+ const findKeys = async () => {
12021
+ try {
12022
+ const config = getProjectConfig();
12023
+ const projectId = options.project || config?.projectId;
12024
+ if (!projectId) {
12025
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
12026
+ setIsLoading(false);
12027
+ return;
12028
+ }
12029
+ if (!options.query) {
12030
+ setError("Query is required. Use --query <search-term>");
12031
+ setIsLoading(false);
12032
+ return;
12033
+ }
12034
+ const api = new KeysApi();
12035
+ const keysResult = await api.listKeys(projectId, { search: options.query });
12036
+ if (keysResult.keys.length === 0) {
12037
+ setError(`No keys found matching query "${options.query}"`);
12038
+ setIsLoading(false);
12039
+ return;
12040
+ }
12041
+ setKeys(keysResult.keys);
12042
+ setIsLoading(false);
12043
+ if (options.dryRun) {
12044
+ return;
12045
+ }
12046
+ if (options.force) {
12047
+ await performBulkDelete(projectId, keysResult.keys.map((k) => k.id), api);
12048
+ }
12049
+ } catch (err) {
12050
+ setError(err instanceof Error ? err.message : "Failed to find keys");
12051
+ setIsLoading(false);
12052
+ }
12053
+ };
12054
+ findKeys();
12055
+ }, [options]);
12056
+ const performBulkDelete = async (projectId, keyIds, api) => {
12057
+ try {
12058
+ setIsDeleting(true);
12059
+ const result = await api.bulkDeleteKeys(projectId, keyIds);
12060
+ setDeletedCount(result.deleted);
12061
+ setDeleted(true);
12062
+ setNeedsConfirmation(false);
12063
+ } catch (err) {
12064
+ setError(err instanceof Error ? err.message : "Failed to delete keys");
12065
+ } finally {
12066
+ setIsDeleting(false);
12067
+ }
12068
+ };
12069
+ useInput9((input, inputKey) => {
12070
+ if (!needsConfirmation || isDeleting || options.dryRun) return;
12071
+ if (input.toLowerCase() === "y") {
12072
+ const config = getProjectConfig();
12073
+ const projectId = options.project || config?.projectId;
12074
+ if (projectId && keys.length > 0) {
12075
+ const api = new KeysApi();
12076
+ performBulkDelete(projectId, keys.map((k) => k.id), api);
12077
+ }
12078
+ } else if (input.toLowerCase() === "n" || inputKey.escape) {
12079
+ process.exit(0);
12080
+ }
12081
+ });
12082
+ if (error) {
12083
+ return /* @__PURE__ */ jsxs43(Text43, { color: "red", children: [
12084
+ "Error: ",
12085
+ error
12086
+ ] });
12087
+ }
12088
+ if (isLoading) {
12089
+ return /* @__PURE__ */ jsxs43(Text43, { children: [
12090
+ /* @__PURE__ */ jsx46(Text43, { color: "green", children: /* @__PURE__ */ jsx46(Spinner22, { type: "dots" }) }),
12091
+ " Searching for keys..."
12092
+ ] });
12093
+ }
12094
+ if (isDeleting) {
12095
+ return /* @__PURE__ */ jsxs43(Text43, { children: [
12096
+ /* @__PURE__ */ jsx46(Text43, { color: "green", children: /* @__PURE__ */ jsx46(Spinner22, { type: "dots" }) }),
12097
+ " Deleting ",
12098
+ keys.length,
12099
+ " key(s)..."
12100
+ ] });
12101
+ }
12102
+ if (deleted) {
12103
+ return /* @__PURE__ */ jsxs43(Box40, { flexDirection: "column", children: [
12104
+ /* @__PURE__ */ jsx46(Text43, { color: "green", children: "\u2713 Bulk delete completed successfully" }),
12105
+ /* @__PURE__ */ jsxs43(Text43, { children: [
12106
+ "Deleted ",
12107
+ deletedCount,
12108
+ " key(s)"
12109
+ ] })
12110
+ ] });
12111
+ }
12112
+ if (options.dryRun) {
12113
+ return /* @__PURE__ */ jsxs43(Box40, { flexDirection: "column", children: [
12114
+ /* @__PURE__ */ jsx46(Box40, { marginBottom: 1, children: /* @__PURE__ */ jsx46(Text43, { color: "cyan", children: "Dry Run Mode" }) }),
12115
+ /* @__PURE__ */ jsxs43(Box40, { flexDirection: "column", marginLeft: 2, children: [
12116
+ /* @__PURE__ */ jsxs43(Text43, { children: [
12117
+ "Found ",
12118
+ keys.length,
12119
+ ' key(s) matching query "',
12120
+ options.query,
12121
+ '":'
12122
+ ] }),
12123
+ /* @__PURE__ */ jsx46(Text43, { dimColor: true }),
12124
+ keys.slice(0, 10).map((key) => /* @__PURE__ */ jsxs43(Text43, { children: [
12125
+ " \u2022 ",
12126
+ key.key,
12127
+ " (",
12128
+ key.namespace || "default",
12129
+ ")"
12130
+ ] }, key.id)),
12131
+ keys.length > 10 && /* @__PURE__ */ jsxs43(Text43, { dimColor: true, children: [
12132
+ " ... and ",
12133
+ keys.length - 10,
12134
+ " more"
12135
+ ] })
12136
+ ] }),
12137
+ /* @__PURE__ */ jsx46(Box40, { marginTop: 1, marginLeft: 2, children: /* @__PURE__ */ jsx46(Text43, { dimColor: true, children: "Run without --dry-run to delete these keys." }) })
12138
+ ] });
12139
+ }
12140
+ if (needsConfirmation && keys.length > 0) {
12141
+ return /* @__PURE__ */ jsxs43(Box40, { flexDirection: "column", children: [
12142
+ /* @__PURE__ */ jsx46(Box40, { marginBottom: 1, children: /* @__PURE__ */ jsx46(Text43, { color: "yellow", children: "Warning: Bulk Delete" }) }),
12143
+ /* @__PURE__ */ jsxs43(Box40, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
12144
+ /* @__PURE__ */ jsxs43(Text43, { children: [
12145
+ "You are about to delete ",
12146
+ keys.length,
12147
+ ' key(s) matching query "',
12148
+ options.query,
12149
+ '":'
12150
+ ] }),
12151
+ /* @__PURE__ */ jsx46(Text43, { dimColor: true }),
12152
+ keys.slice(0, 5).map((key) => /* @__PURE__ */ jsxs43(Text43, { children: [
12153
+ " \u2022 ",
12154
+ key.key,
12155
+ " (",
12156
+ key.namespace || "default",
12157
+ ")"
12158
+ ] }, key.id)),
12159
+ keys.length > 5 && /* @__PURE__ */ jsxs43(Text43, { dimColor: true, children: [
12160
+ " ... and ",
12161
+ keys.length - 5,
12162
+ " more"
12163
+ ] })
12164
+ ] }),
12165
+ /* @__PURE__ */ jsxs43(Box40, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
12166
+ /* @__PURE__ */ jsx46(Text43, { color: "red", children: "This will delete all matching keys and their translations." }),
12167
+ /* @__PURE__ */ jsx46(Text43, { dimColor: true, children: "This action cannot be undone." })
12168
+ ] }),
12169
+ /* @__PURE__ */ jsx46(Box40, { marginLeft: 2, children: /* @__PURE__ */ jsxs43(Text43, { children: [
12170
+ "Press",
12171
+ " ",
12172
+ /* @__PURE__ */ jsx46(Text43, { bold: true, color: "green", children: "Y" }),
12173
+ " ",
12174
+ "to confirm,",
12175
+ " ",
12176
+ /* @__PURE__ */ jsx46(Text43, { bold: true, color: "red", children: "N" }),
12177
+ " ",
12178
+ "to cancel"
12179
+ ] }) })
12180
+ ] });
12181
+ }
12182
+ return null;
12183
+ };
12184
+ function runKeysBulkDelete(options) {
12185
+ render25(/* @__PURE__ */ jsx46(KeysBulkDelete, { options }));
12186
+ }
12187
+
12188
+ // src/commands/keys/index.tsx
12189
+ var keysCommand = new Command2("keys").description("Manage translation keys");
12190
+ keysCommand.command("list").description("List translation keys").option("--project <id>", "Project ID").option("--namespace <namespace>", "Filter by namespace").option("--search <query>", "Search keys by name").option("--tags <tags>", "Filter by tags (comma-separated)").option("--limit <n>", "Limit results").option("--format <format>", "Output format (table|json)", "table").action((options) => {
12191
+ runKeysList({
12192
+ project: options.project,
12193
+ namespace: options.namespace,
12194
+ search: options.search,
12195
+ tags: options.tags,
12196
+ limit: options.limit,
12197
+ format: options.format
12198
+ });
12199
+ });
12200
+ keysCommand.command("create <key>").description("Create a new translation key").requiredOption("--value <text>", "Translation value").option("--project <id>", "Project ID").option("--namespace <namespace>", "Namespace (default: default)").option("--context <text>", "Context description").option("--tags <tags>", "Tags (comma-separated)").action((key, options) => {
12201
+ runKeysCreate(key, {
12202
+ value: options.value,
12203
+ project: options.project,
12204
+ namespace: options.namespace,
12205
+ context: options.context,
12206
+ tags: options.tags
12207
+ });
12208
+ });
12209
+ keysCommand.command("update <key>").description("Update an existing translation key").option("--project <id>", "Project ID").option("--context <text>", "Update context description").option("--tags <tags>", "Update tags (comma-separated)").action((key, options) => {
12210
+ runKeysUpdate(key, {
12211
+ project: options.project,
12212
+ context: options.context,
12213
+ tags: options.tags
12214
+ });
12215
+ });
12216
+ keysCommand.command("delete <key>").description("Delete a translation key").option("--project <id>", "Project ID").option("--force", "Skip confirmation prompt").action((key, options) => {
12217
+ runKeysDelete(key, {
12218
+ project: options.project,
12219
+ force: options.force
12220
+ });
12221
+ });
12222
+ keysCommand.command("bulk-delete").description("Delete multiple keys matching a query").requiredOption("--query <search>", "Search query to match keys").option("--project <id>", "Project ID").option("--dry-run", "Show what would be deleted without deleting").option("--force", "Skip confirmation prompt").action((options) => {
12223
+ runKeysBulkDelete({
12224
+ query: options.query,
12225
+ project: options.project,
12226
+ dryRun: options.dryRun,
12227
+ force: options.force
12228
+ });
12229
+ });
12230
+
12231
+ // src/commands/tm/index.tsx
12232
+ import { Command as Command3 } from "commander";
12233
+
12234
+ // src/commands/tm/apply.tsx
12235
+ import { useState as useState31, useEffect as useEffect28 } from "react";
12236
+ import { render as render26, Box as Box41, Text as Text44, Newline as Newline3 } from "ink";
12237
+ import Spinner23 from "ink-spinner";
12238
+ import { Fragment as Fragment7, jsx as jsx47, jsxs as jsxs44 } from "react/jsx-runtime";
12239
+ var TMApply = ({ options }) => {
12240
+ const [result, setResult] = useState31(null);
12241
+ const [isLoading, setIsLoading] = useState31(true);
12242
+ const [error, setError] = useState31(null);
12243
+ useEffect28(() => {
12244
+ const applyTM = async () => {
12245
+ try {
12246
+ const config = getProjectConfig();
12247
+ const projectId = options.project || config?.projectId;
12248
+ if (!projectId) {
12249
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
12250
+ setIsLoading(false);
12251
+ return;
12252
+ }
12253
+ if (!options.language) {
12254
+ setError("Target language is required. Use --language <code>");
12255
+ setIsLoading(false);
12256
+ return;
12257
+ }
12258
+ const threshold2 = options.threshold ? parseInt(options.threshold, 10) : 85;
12259
+ const api = new TMApi();
12260
+ const tmResult = await api.applyTMMatches(projectId, {
12261
+ target_language: options.language,
12262
+ namespace: options.namespace,
12263
+ min_threshold: threshold2,
12264
+ dry_run: options.dryRun
12265
+ });
12266
+ setResult(tmResult);
12267
+ } catch (err) {
12268
+ setError(err instanceof Error ? err.message : "Failed to apply TM");
12269
+ } finally {
12270
+ setIsLoading(false);
12271
+ }
12272
+ };
12273
+ applyTM();
12274
+ }, [options]);
12275
+ if (error) {
12276
+ return /* @__PURE__ */ jsxs44(Text44, { color: "red", children: [
12277
+ "Error: ",
12278
+ error
12279
+ ] });
12280
+ }
12281
+ if (isLoading) {
12282
+ return /* @__PURE__ */ jsxs44(Text44, { children: [
12283
+ /* @__PURE__ */ jsx47(Text44, { color: "green", children: /* @__PURE__ */ jsx47(Spinner23, { type: "dots" }) }),
12284
+ " ",
12285
+ options.dryRun ? "Previewing TM matches..." : "Applying TM matches..."
12286
+ ] });
12287
+ }
12288
+ if (!result) {
12289
+ return /* @__PURE__ */ jsx47(Text44, { children: "No results" });
12290
+ }
12291
+ const threshold = options.threshold ? parseInt(options.threshold, 10) : 85;
12292
+ const qualifyingMatches = result.matches.filter((m) => m.score >= threshold);
12293
+ return /* @__PURE__ */ jsxs44(Box41, { flexDirection: "column", children: [
12294
+ /* @__PURE__ */ jsx47(Text44, { bold: true, children: options.dryRun ? "TM Match Preview" : "TM Applied" }),
12295
+ /* @__PURE__ */ jsx47(Newline3, {}),
12296
+ /* @__PURE__ */ jsxs44(Text44, { children: [
12297
+ "Threshold: ",
12298
+ threshold,
12299
+ "%"
12300
+ ] }),
12301
+ /* @__PURE__ */ jsxs44(Text44, { children: [
12302
+ "Language: ",
12303
+ options.language
12304
+ ] }),
12305
+ options.namespace && /* @__PURE__ */ jsxs44(Text44, { children: [
12306
+ "Namespace: ",
12307
+ options.namespace
12308
+ ] }),
12309
+ /* @__PURE__ */ jsx47(Newline3, {}),
12310
+ /* @__PURE__ */ jsxs44(Text44, { children: [
12311
+ "Qualifying matches: ",
12312
+ qualifyingMatches.length
12313
+ ] }),
12314
+ !options.dryRun && /* @__PURE__ */ jsxs44(Fragment7, { children: [
12315
+ /* @__PURE__ */ jsxs44(Text44, { color: "green", children: [
12316
+ "Applied: ",
12317
+ result.applied
12318
+ ] }),
12319
+ /* @__PURE__ */ jsxs44(Text44, { color: "yellow", children: [
12320
+ "Skipped: ",
12321
+ result.skipped
12322
+ ] })
12323
+ ] }),
12324
+ /* @__PURE__ */ jsx47(Newline3, {}),
12325
+ qualifyingMatches.length > 0 && /* @__PURE__ */ jsxs44(Fragment7, { children: [
12326
+ /* @__PURE__ */ jsx47(Text44, { bold: true, children: "Matches:" }),
12327
+ /* @__PURE__ */ jsx47(Newline3, {}),
12328
+ qualifyingMatches.slice(0, 20).map((match, idx) => /* @__PURE__ */ jsxs44(Box41, { flexDirection: "column", marginBottom: 1, children: [
12329
+ /* @__PURE__ */ jsxs44(Text44, { children: [
12330
+ /* @__PURE__ */ jsx47(Text44, { color: "cyan", children: match.key }),
12331
+ " (",
12332
+ match.score,
12333
+ "%)"
12334
+ ] }),
12335
+ /* @__PURE__ */ jsxs44(Text44, { dimColor: true, children: [
12336
+ " Source: ",
12337
+ match.source
12338
+ ] }),
12339
+ /* @__PURE__ */ jsxs44(Text44, { dimColor: true, children: [
12340
+ " Translation: ",
12341
+ match.translation
12342
+ ] }),
12343
+ match.applied && /* @__PURE__ */ jsx47(Text44, { color: "green", children: " \u2713 Applied" })
12344
+ ] }, idx)),
12345
+ qualifyingMatches.length > 20 && /* @__PURE__ */ jsxs44(Text44, { dimColor: true, children: [
12346
+ "... and ",
12347
+ qualifyingMatches.length - 20,
12348
+ " more matches"
12349
+ ] })
12350
+ ] }),
12351
+ qualifyingMatches.length === 0 && /* @__PURE__ */ jsx47(Text44, { dimColor: true, children: "No matches found above threshold." })
12352
+ ] });
12353
+ };
12354
+ function runTMApply(options) {
12355
+ render26(/* @__PURE__ */ jsx47(TMApply, { options }));
12356
+ }
12357
+
12358
+ // src/commands/tm/search.tsx
12359
+ import { useState as useState32, useEffect as useEffect29 } from "react";
12360
+ import { render as render27, Box as Box42, Text as Text45, Newline as Newline4 } from "ink";
12361
+ import Spinner24 from "ink-spinner";
12362
+ import { jsx as jsx48, jsxs as jsxs45 } from "react/jsx-runtime";
12363
+ var TMSearch = ({ options }) => {
12364
+ const [matches, setMatches] = useState32([]);
12365
+ const [isLoading, setIsLoading] = useState32(true);
12366
+ const [error, setError] = useState32(null);
12367
+ useEffect29(() => {
12368
+ const searchTM = async () => {
12369
+ try {
12370
+ if (!options.text) {
12371
+ setError("Search text is required");
12372
+ setIsLoading(false);
12373
+ return;
12374
+ }
12375
+ if (!options.source) {
12376
+ setError("Source language is required. Use --source <lang>");
12377
+ setIsLoading(false);
12378
+ return;
12379
+ }
12380
+ if (!options.target) {
12381
+ setError("Target language is required. Use --target <lang>");
12382
+ setIsLoading(false);
12383
+ return;
12384
+ }
12385
+ const api = new TMApi();
12386
+ const minQuality = options.minQuality ? parseInt(options.minQuality, 10) : 0;
12387
+ const result = await api.searchTM({
12388
+ text: options.text,
12389
+ source_lang: options.source,
12390
+ target_lang: options.target,
12391
+ min_quality: minQuality
12392
+ });
12393
+ setMatches(result.matches);
12394
+ } catch (err) {
12395
+ setError(err instanceof Error ? err.message : "Failed to search TM");
12396
+ } finally {
12397
+ setIsLoading(false);
12398
+ }
12399
+ };
12400
+ searchTM();
12401
+ }, [options]);
12402
+ if (error) {
12403
+ return /* @__PURE__ */ jsxs45(Text45, { color: "red", children: [
12404
+ "Error: ",
12405
+ error
12406
+ ] });
12407
+ }
12408
+ if (isLoading) {
12409
+ return /* @__PURE__ */ jsxs45(Text45, { children: [
12410
+ /* @__PURE__ */ jsx48(Text45, { color: "green", children: /* @__PURE__ */ jsx48(Spinner24, { type: "dots" }) }),
12411
+ " ",
12412
+ "Searching Translation Memory..."
12413
+ ] });
12414
+ }
12415
+ if (matches.length === 0) {
12416
+ return /* @__PURE__ */ jsxs45(Box42, { flexDirection: "column", children: [
12417
+ /* @__PURE__ */ jsxs45(Text45, { children: [
12418
+ "No matches found for: ",
12419
+ /* @__PURE__ */ jsx48(Text45, { bold: true, children: options.text })
12420
+ ] }),
12421
+ /* @__PURE__ */ jsxs45(Text45, { dimColor: true, children: [
12422
+ "Source: ",
12423
+ options.source,
12424
+ " \u2192 Target: ",
12425
+ options.target
12426
+ ] })
12427
+ ] });
12428
+ }
12429
+ const sortedMatches = [...matches].sort((a, b) => b.quality - a.quality);
12430
+ return /* @__PURE__ */ jsxs45(Box42, { flexDirection: "column", children: [
12431
+ /* @__PURE__ */ jsx48(Text45, { bold: true, children: "Translation Memory Matches" }),
12432
+ /* @__PURE__ */ jsx48(Newline4, {}),
12433
+ /* @__PURE__ */ jsxs45(Text45, { children: [
12434
+ "Query: ",
12435
+ /* @__PURE__ */ jsx48(Text45, { bold: true, children: options.text })
12436
+ ] }),
12437
+ /* @__PURE__ */ jsxs45(Text45, { dimColor: true, children: [
12438
+ options.source,
12439
+ " \u2192 ",
12440
+ options.target
12441
+ ] }),
12442
+ /* @__PURE__ */ jsx48(Newline4, {}),
12443
+ /* @__PURE__ */ jsxs45(Box42, { flexDirection: "column", children: [
12444
+ /* @__PURE__ */ jsx48(Text45, { bold: true, children: "Score Translation" }),
12445
+ /* @__PURE__ */ jsx48(Text45, { children: "\u2500".repeat(60) }),
12446
+ sortedMatches.map((match, idx) => /* @__PURE__ */ jsxs45(Box42, { flexDirection: "column", marginBottom: 1, children: [
12447
+ /* @__PURE__ */ jsxs45(Text45, { children: [
12448
+ /* @__PURE__ */ jsxs45(Text45, { color: match.quality >= 90 ? "green" : match.quality >= 75 ? "yellow" : "white", children: [
12449
+ match.quality,
12450
+ "%"
12451
+ ] }),
12452
+ " ",
12453
+ match.target_text
12454
+ ] }),
12455
+ match.context && /* @__PURE__ */ jsxs45(Text45, { dimColor: true, children: [
12456
+ " Context: ",
12457
+ match.context
12458
+ ] })
12459
+ ] }, idx))
12460
+ ] }),
12461
+ /* @__PURE__ */ jsx48(Newline4, {}),
12462
+ /* @__PURE__ */ jsxs45(Text45, { dimColor: true, children: [
12463
+ "Found ",
12464
+ matches.length,
12465
+ " match(es)"
12466
+ ] })
12467
+ ] });
12468
+ };
12469
+ function runTMSearch(options) {
12470
+ render27(/* @__PURE__ */ jsx48(TMSearch, { options }));
12471
+ }
12472
+
12473
+ // src/commands/tm/add.tsx
12474
+ import { useState as useState33, useEffect as useEffect30 } from "react";
12475
+ import { render as render28, Box as Box43, Text as Text46, Newline as Newline5 } from "ink";
12476
+ import Spinner25 from "ink-spinner";
12477
+ import { jsx as jsx49, jsxs as jsxs46 } from "react/jsx-runtime";
12478
+ var TMAdd = ({ options }) => {
12479
+ const [entry, setEntry] = useState33(null);
12480
+ const [isLoading, setIsLoading] = useState33(true);
12481
+ const [error, setError] = useState33(null);
12482
+ useEffect30(() => {
12483
+ const addEntry = async () => {
12484
+ try {
12485
+ if (!options.sourceText) {
12486
+ setError("Source text is required");
12487
+ setIsLoading(false);
12488
+ return;
12489
+ }
12490
+ if (!options.targetText) {
12491
+ setError("Target text is required");
12492
+ setIsLoading(false);
12493
+ return;
12494
+ }
12495
+ if (!options.sourceLang) {
12496
+ setError("Source language is required. Use --source-lang <lang>");
12497
+ setIsLoading(false);
12498
+ return;
12499
+ }
12500
+ if (!options.targetLang) {
12501
+ setError("Target language is required. Use --target-lang <lang>");
12502
+ setIsLoading(false);
12503
+ return;
12504
+ }
12505
+ const api = new TMApi();
12506
+ const quality = options.quality ? parseInt(options.quality, 10) : 100;
12507
+ const result = await api.addTMEntry({
12508
+ source_text: options.sourceText,
12509
+ target_text: options.targetText,
12510
+ source_lang: options.sourceLang,
12511
+ target_lang: options.targetLang,
12512
+ quality,
12513
+ context: options.context
12514
+ });
12515
+ setEntry(result);
12516
+ } catch (err) {
12517
+ setError(err instanceof Error ? err.message : "Failed to add TM entry");
12518
+ } finally {
12519
+ setIsLoading(false);
12520
+ }
12521
+ };
12522
+ addEntry();
12523
+ }, [options]);
12524
+ if (error) {
12525
+ return /* @__PURE__ */ jsxs46(Text46, { color: "red", children: [
12526
+ "Error: ",
12527
+ error
12528
+ ] });
12529
+ }
12530
+ if (isLoading) {
12531
+ return /* @__PURE__ */ jsxs46(Text46, { children: [
12532
+ /* @__PURE__ */ jsx49(Text46, { color: "green", children: /* @__PURE__ */ jsx49(Spinner25, { type: "dots" }) }),
12533
+ " ",
12534
+ "Adding TM entry..."
12535
+ ] });
12536
+ }
12537
+ if (!entry) {
12538
+ return /* @__PURE__ */ jsx49(Text46, { children: "No entry created" });
12539
+ }
12540
+ return /* @__PURE__ */ jsxs46(Box43, { flexDirection: "column", children: [
12541
+ /* @__PURE__ */ jsx49(Text46, { color: "green", children: "\u2713 Translation Memory entry added successfully" }),
12542
+ /* @__PURE__ */ jsx49(Newline5, {}),
12543
+ /* @__PURE__ */ jsxs46(Text46, { children: [
12544
+ /* @__PURE__ */ jsxs46(Text46, { bold: true, children: [
12545
+ "Source (",
12546
+ entry.source_lang,
12547
+ "):"
12548
+ ] }),
12549
+ " ",
12550
+ entry.source_text
12551
+ ] }),
12552
+ /* @__PURE__ */ jsxs46(Text46, { children: [
12553
+ /* @__PURE__ */ jsxs46(Text46, { bold: true, children: [
12554
+ "Target (",
12555
+ entry.target_lang,
12556
+ "):"
12557
+ ] }),
12558
+ " ",
12559
+ entry.target_text
12560
+ ] }),
12561
+ /* @__PURE__ */ jsxs46(Text46, { children: [
12562
+ /* @__PURE__ */ jsx49(Text46, { bold: true, children: "Quality:" }),
12563
+ " ",
12564
+ entry.quality,
12565
+ "%"
12566
+ ] }),
12567
+ entry.context && /* @__PURE__ */ jsxs46(Text46, { children: [
12568
+ /* @__PURE__ */ jsx49(Text46, { bold: true, children: "Context:" }),
12569
+ " ",
12570
+ entry.context
12571
+ ] }),
12572
+ /* @__PURE__ */ jsx49(Newline5, {}),
12573
+ /* @__PURE__ */ jsxs46(Text46, { dimColor: true, children: [
12574
+ "ID: ",
12575
+ entry.id
12576
+ ] })
12577
+ ] });
12578
+ };
12579
+ function runTMAdd(options) {
12580
+ render28(/* @__PURE__ */ jsx49(TMAdd, { options }));
12581
+ }
12582
+
12583
+ // src/commands/tm/stats.tsx
12584
+ import { useState as useState34, useEffect as useEffect31 } from "react";
12585
+ import { render as render29, Box as Box44, Text as Text47, Newline as Newline6 } from "ink";
12586
+ import Spinner26 from "ink-spinner";
12587
+ import { Fragment as Fragment8, jsx as jsx50, jsxs as jsxs47 } from "react/jsx-runtime";
12588
+ var TMStatsComponent = ({ options }) => {
12589
+ const [stats, setStats] = useState34(null);
12590
+ const [isLoading, setIsLoading] = useState34(true);
12591
+ const [error, setError] = useState34(null);
12592
+ useEffect31(() => {
12593
+ const fetchStats = async () => {
12594
+ try {
12595
+ const api = new TMApi();
12596
+ const result = await api.getTMStats();
12597
+ setStats(result);
12598
+ } catch (err) {
12599
+ setError(err instanceof Error ? err.message : "Failed to fetch TM stats");
12600
+ } finally {
12601
+ setIsLoading(false);
12602
+ }
12603
+ };
12604
+ fetchStats();
12605
+ }, [options]);
12606
+ if (error) {
12607
+ return /* @__PURE__ */ jsxs47(Text47, { color: "red", children: [
12608
+ "Error: ",
12609
+ error
12610
+ ] });
12611
+ }
12612
+ if (isLoading) {
12613
+ return /* @__PURE__ */ jsxs47(Text47, { children: [
12614
+ /* @__PURE__ */ jsx50(Text47, { color: "green", children: /* @__PURE__ */ jsx50(Spinner26, { type: "dots" }) }),
12615
+ " ",
12616
+ "Loading Translation Memory statistics..."
12617
+ ] });
12618
+ }
12619
+ if (!stats) {
12620
+ return /* @__PURE__ */ jsx50(Text47, { children: "No statistics available" });
12621
+ }
12622
+ return /* @__PURE__ */ jsxs47(Box44, { flexDirection: "column", children: [
12623
+ /* @__PURE__ */ jsx50(Text47, { bold: true, children: "Translation Memory Statistics" }),
12624
+ /* @__PURE__ */ jsx50(Newline6, {}),
12625
+ /* @__PURE__ */ jsxs47(Text47, { children: [
12626
+ /* @__PURE__ */ jsx50(Text47, { bold: true, children: "Total Entries:" }),
12627
+ " ",
12628
+ stats.total_entries.toLocaleString()
12629
+ ] }),
12630
+ /* @__PURE__ */ jsxs47(Text47, { children: [
12631
+ /* @__PURE__ */ jsx50(Text47, { bold: true, children: "Average Quality:" }),
12632
+ " ",
12633
+ stats.avg_quality.toFixed(1),
12634
+ "%"
12635
+ ] }),
12636
+ /* @__PURE__ */ jsx50(Newline6, {}),
12637
+ stats.language_pairs.length > 0 && /* @__PURE__ */ jsxs47(Fragment8, { children: [
12638
+ /* @__PURE__ */ jsx50(Text47, { bold: true, children: "Language Pairs:" }),
12639
+ /* @__PURE__ */ jsx50(Newline6, {}),
12640
+ /* @__PURE__ */ jsx50(Box44, { flexDirection: "column", children: stats.language_pairs.map((pair, idx) => /* @__PURE__ */ jsxs47(Text47, { children: [
12641
+ /* @__PURE__ */ jsxs47(Text47, { color: "cyan", children: [
12642
+ pair.source,
12643
+ " \u2192 ",
12644
+ pair.target
12645
+ ] }),
12646
+ ": ",
12647
+ pair.count.toLocaleString(),
12648
+ " ",
12649
+ pair.count === 1 ? "entry" : "entries"
12650
+ ] }, idx)) })
12651
+ ] }),
12652
+ stats.language_pairs.length === 0 && /* @__PURE__ */ jsx50(Text47, { dimColor: true, children: "No language pairs found. Add entries with `intlpull tm add`" })
12653
+ ] });
12654
+ };
12655
+ function runTMStats(options) {
12656
+ render29(/* @__PURE__ */ jsx50(TMStatsComponent, { options }));
12657
+ }
12658
+
12659
+ // src/commands/tm/index.tsx
12660
+ var tmCommand = new Command3("tm").description("Manage Translation Memory");
12661
+ tmCommand.command("apply").description("Apply TM matches to project translations").option("--project <id>", "Project ID").option("--language <code>", "Target language code").option("--namespace <namespace>", "Filter by namespace").option("--threshold <n>", "Minimum match score (default: 85)", "85").option("--dry-run", "Preview matches without applying").action((options) => {
12662
+ runTMApply({
12663
+ project: options.project,
12664
+ language: options.language,
12665
+ namespace: options.namespace,
12666
+ threshold: options.threshold,
12667
+ dryRun: options.dryRun
12668
+ });
12669
+ });
12670
+ tmCommand.command("search <text>").description("Search Translation Memory for a specific text").requiredOption("--source <lang>", "Source language code").requiredOption("--target <lang>", "Target language code").option("--min-quality <n>", "Minimum quality score (0-100)", "0").action((text, options) => {
12671
+ runTMSearch({
12672
+ text,
12673
+ source: options.source,
12674
+ target: options.target,
12675
+ minQuality: options.minQuality
12676
+ });
12677
+ });
12678
+ tmCommand.command("add <source> <target>").description("Add a new Translation Memory entry").requiredOption("--source-lang <lang>", "Source language code").requiredOption("--target-lang <lang>", "Target language code").option("--quality <n>", "Quality score (0-100, default: 100)", "100").option("--context <text>", "Context description").action((source, target, options) => {
12679
+ runTMAdd({
12680
+ sourceText: source,
12681
+ targetText: target,
12682
+ sourceLang: options.sourceLang,
12683
+ targetLang: options.targetLang,
12684
+ quality: options.quality,
12685
+ context: options.context
12686
+ });
12687
+ });
12688
+ tmCommand.command("stats").description("Show Translation Memory statistics").action(() => {
12689
+ runTMStats({});
12690
+ });
12691
+
12692
+ // src/commands/cleanup.tsx
12693
+ import { useState as useState35, useEffect as useEffect32 } from "react";
12694
+ import { render as render30, Box as Box45, Text as Text48 } from "ink";
12695
+ import Spinner27 from "ink-spinner";
12696
+ import { jsx as jsx51, jsxs as jsxs48 } from "react/jsx-runtime";
12697
+ var CleanupComponent = ({ options }) => {
12698
+ const [state, setState] = useState35("scanning");
12699
+ const [orphanKeys, setOrphanKeys] = useState35([]);
12700
+ const [totalKeys, setTotalKeys] = useState35(0);
12701
+ const [deleted, setDeleted] = useState35(0);
12702
+ const [error, setError] = useState35(null);
12703
+ useEffect32(() => {
12704
+ async function run() {
12705
+ try {
12706
+ const resolved = getResolvedApiKey();
12707
+ if (!resolved?.key) {
12708
+ setError("Not authenticated. Run `intlpull login` or set INTLPULL_API_KEY");
12709
+ setState("error");
12710
+ return;
12711
+ }
12712
+ const config = getProjectConfig();
12713
+ let projectId = options.project || config?.projectId;
12714
+ if (!projectId) {
12715
+ const projectsApi = new ProjectsApi();
12716
+ const { projects: projects2 } = await projectsApi.listProjects();
12717
+ if (projects2.length === 0) {
12718
+ setError("No projects found. Create one first.");
12719
+ setState("error");
12720
+ return;
12721
+ }
12722
+ if (projects2.length === 1) {
12723
+ projectId = projects2[0].id;
12724
+ } else {
12725
+ setError(
12726
+ `Multiple projects found. Specify with --project:
12727
+ ` + projects2.map((p) => ` --project ${p.id} (${p.name})`).join("\n")
12728
+ );
12729
+ setState("error");
12730
+ return;
12731
+ }
12732
+ }
12733
+ if (!projectId) {
12734
+ setError("Could not determine project ID");
12735
+ setState("error");
12736
+ return;
12737
+ }
12738
+ const api = new KeysApi();
12739
+ setState("scanning");
12740
+ const { keys: serverKeys } = await api.listKeys(projectId);
12741
+ setTotalKeys(serverKeys.length);
12742
+ if (serverKeys.length === 0) {
12743
+ setState("done");
12744
+ return;
12745
+ }
12746
+ let usedKeys = null;
12747
+ if (options.scanCodebase) {
12748
+ setState("comparing");
12749
+ usedKeys = await scanForKeys(process.cwd());
12750
+ }
12751
+ setState("comparing");
12752
+ const orphans = serverKeys.filter((k) => {
12753
+ if (usedKeys && !usedKeys.has(k.key)) {
12754
+ return true;
12755
+ }
12756
+ if (options.days !== void 0 && k.updated_at) {
12757
+ const daysSinceUpdate = (Date.now() - new Date(k.updated_at).getTime()) / (1e3 * 60 * 60 * 24);
12758
+ return daysSinceUpdate > options.days;
12759
+ }
12760
+ return false;
12761
+ });
12762
+ setOrphanKeys(orphans);
12763
+ if (options.dryRun || orphans.length === 0) {
12764
+ setState("done");
12765
+ return;
12766
+ }
12767
+ if (!options.force) {
12768
+ setState("confirming");
12769
+ if (!process.stdin.isTTY) {
12770
+ setError("Refusing to delete keys in non-interactive mode without --force flag");
12771
+ setState("error");
12772
+ return;
12773
+ }
12774
+ }
12775
+ setState("deleting");
12776
+ const keyIds = orphans.map((k) => k.id);
12777
+ const batchSize = 100;
12778
+ let deletedCount = 0;
12779
+ for (let i = 0; i < keyIds.length; i += batchSize) {
12780
+ const batch = keyIds.slice(i, i + batchSize);
12781
+ await api.bulkDeleteKeys(projectId, batch);
12782
+ deletedCount += batch.length;
12783
+ setDeleted(deletedCount);
12784
+ }
12785
+ setState("done");
12786
+ } catch (err) {
12787
+ setError(err instanceof Error ? err.message : "Failed to cleanup keys");
12788
+ setState("error");
12789
+ }
12790
+ }
12791
+ run();
12792
+ }, [options]);
12793
+ if (state === "error") {
12794
+ return /* @__PURE__ */ jsx51(Box45, { flexDirection: "column", children: /* @__PURE__ */ jsxs48(Text48, { color: "red", children: [
12795
+ "Error: ",
12796
+ error
12797
+ ] }) });
12798
+ }
12799
+ if (state === "scanning") {
12800
+ return /* @__PURE__ */ jsxs48(Box45, { children: [
12801
+ /* @__PURE__ */ jsx51(Text48, { color: "green", children: /* @__PURE__ */ jsx51(Spinner27, { type: "dots" }) }),
12802
+ /* @__PURE__ */ jsx51(Text48, { children: " Fetching keys from server..." })
12803
+ ] });
12804
+ }
12805
+ if (state === "comparing") {
12806
+ return /* @__PURE__ */ jsxs48(Box45, { flexDirection: "column", children: [
12807
+ /* @__PURE__ */ jsxs48(Box45, { children: [
12808
+ /* @__PURE__ */ jsx51(Text48, { color: "green", children: /* @__PURE__ */ jsx51(Spinner27, { type: "dots" }) }),
12809
+ /* @__PURE__ */ jsxs48(Text48, { children: [
12810
+ " ",
12811
+ options.scanCodebase ? "Scanning codebase for key usage..." : "Analyzing keys..."
12812
+ ] })
12813
+ ] }),
12814
+ /* @__PURE__ */ jsxs48(Text48, { dimColor: true, children: [
12815
+ " Found ",
12816
+ totalKeys,
12817
+ " total keys"
12818
+ ] })
12819
+ ] });
12820
+ }
12821
+ if (state === "confirming") {
12822
+ return /* @__PURE__ */ jsxs48(Box45, { flexDirection: "column", children: [
12823
+ /* @__PURE__ */ jsxs48(Text48, { color: "yellow", children: [
12824
+ "Warning: About to delete ",
12825
+ orphanKeys.length,
12826
+ " keys"
12827
+ ] }),
12828
+ /* @__PURE__ */ jsx51(Text48, { dimColor: true, children: "Press Ctrl+C to cancel or add --force flag to skip confirmation" })
12829
+ ] });
12830
+ }
12831
+ if (state === "deleting") {
12832
+ return /* @__PURE__ */ jsx51(Box45, { flexDirection: "column", children: /* @__PURE__ */ jsxs48(Box45, { children: [
12833
+ /* @__PURE__ */ jsx51(Text48, { color: "green", children: /* @__PURE__ */ jsx51(Spinner27, { type: "dots" }) }),
12834
+ /* @__PURE__ */ jsxs48(Text48, { children: [
12835
+ " Deleting orphaned keys... (",
12836
+ deleted,
12837
+ "/",
12838
+ orphanKeys.length,
12839
+ ")"
12840
+ ] })
12841
+ ] }) });
12842
+ }
12843
+ if (state === "done") {
12844
+ if (orphanKeys.length === 0) {
12845
+ return /* @__PURE__ */ jsxs48(Box45, { flexDirection: "column", children: [
12846
+ /* @__PURE__ */ jsx51(Text48, { color: "green", children: "\u2713 No orphaned keys found" }),
12847
+ /* @__PURE__ */ jsxs48(Text48, { dimColor: true, children: [
12848
+ " Total keys: ",
12849
+ totalKeys
12850
+ ] }),
12851
+ options.scanCodebase && /* @__PURE__ */ jsx51(Text48, { dimColor: true, children: " All keys are used in codebase" })
12852
+ ] });
12853
+ }
12854
+ if (options.dryRun) {
12855
+ return /* @__PURE__ */ jsxs48(Box45, { flexDirection: "column", children: [
12856
+ /* @__PURE__ */ jsxs48(Text48, { color: "yellow", children: [
12857
+ "Dry run: Found ",
12858
+ orphanKeys.length,
12859
+ " orphaned keys"
12860
+ ] }),
12861
+ /* @__PURE__ */ jsxs48(Box45, { flexDirection: "column", marginTop: 1, children: [
12862
+ /* @__PURE__ */ jsx51(Text48, { bold: true, children: "Orphaned keys:" }),
12863
+ orphanKeys.slice(0, 20).map((k) => /* @__PURE__ */ jsxs48(Text48, { dimColor: true, children: [
12864
+ k.key,
12865
+ options.days !== void 0 && k.updated_at && /* @__PURE__ */ jsxs48(Text48, { dimColor: true, children: [
12866
+ " ",
12867
+ "(last updated:",
12868
+ " ",
12869
+ Math.floor((Date.now() - new Date(k.updated_at).getTime()) / (1e3 * 60 * 60 * 24)),
12870
+ " ",
12871
+ "days ago)"
12872
+ ] })
12873
+ ] }, k.id)),
12874
+ orphanKeys.length > 20 && /* @__PURE__ */ jsxs48(Text48, { dimColor: true, children: [
12875
+ " ... and ",
12876
+ orphanKeys.length - 20,
12877
+ " more"
12878
+ ] })
12879
+ ] }),
12880
+ /* @__PURE__ */ jsx51(Box45, { marginTop: 1, children: /* @__PURE__ */ jsx51(Text48, { dimColor: true, children: "Remove --dry-run to delete these keys" }) })
12881
+ ] });
12882
+ }
12883
+ return /* @__PURE__ */ jsxs48(Box45, { flexDirection: "column", children: [
12884
+ /* @__PURE__ */ jsx51(Text48, { color: "green", children: "\u2713 Cleanup complete" }),
12885
+ /* @__PURE__ */ jsxs48(Text48, { children: [
12886
+ " Deleted: ",
12887
+ deleted,
12888
+ " keys"
12889
+ ] }),
12890
+ /* @__PURE__ */ jsxs48(Text48, { dimColor: true, children: [
12891
+ " Remaining: ",
12892
+ totalKeys - deleted,
12893
+ " keys"
12894
+ ] })
12895
+ ] });
12896
+ }
12897
+ return null;
12898
+ };
12899
+ function runCleanup(options) {
12900
+ render30(/* @__PURE__ */ jsx51(CleanupComponent, { options }));
12901
+ }
12902
+
12903
+ // src/commands/snapshots/index.tsx
12904
+ import { Command as Command4 } from "commander";
12905
+
12906
+ // src/commands/snapshots/create.tsx
12907
+ import { useState as useState36, useEffect as useEffect33 } from "react";
12908
+ import { render as render31, Box as Box46, Text as Text49 } from "ink";
12909
+ import Spinner28 from "ink-spinner";
12910
+ import { jsx as jsx52, jsxs as jsxs49 } from "react/jsx-runtime";
12911
+ var SnapshotsCreate = ({ options }) => {
12912
+ const [result, setResult] = useState36(null);
12913
+ const [isLoading, setIsLoading] = useState36(true);
12914
+ const [error, setError] = useState36(null);
12915
+ useEffect33(() => {
12916
+ const createSnapshot = async () => {
12917
+ try {
12918
+ const resolved = getResolvedApiKey();
12919
+ if (!resolved?.key) {
12920
+ setError("Not authenticated. Run `intlpull login` or set INTLPULL_API_KEY");
12921
+ setIsLoading(false);
12922
+ return;
12923
+ }
12924
+ const config = getProjectConfig();
12925
+ let projectId = options.project || config?.projectId;
12926
+ if (!projectId) {
12927
+ const projectsApi = new ProjectsApi();
12928
+ const { projects: projects2 } = await projectsApi.listProjects();
12929
+ if (projects2.length === 0) {
12930
+ setError("No projects found. Create one first.");
12931
+ setIsLoading(false);
12932
+ return;
12933
+ }
12934
+ if (projects2.length === 1) {
12935
+ projectId = projects2[0].id;
12936
+ } else {
12937
+ setError(
12938
+ `Multiple projects found. Specify with --project:
12939
+ ` + projects2.map((p) => ` --project ${p.id} (${p.name})`).join("\n")
12940
+ );
12941
+ setIsLoading(false);
12942
+ return;
12943
+ }
12944
+ }
12945
+ if (!projectId) {
12946
+ setError("Could not determine project ID");
12947
+ setIsLoading(false);
12948
+ return;
12949
+ }
12950
+ const api = new SnapshotsApi();
12951
+ const snapshot = await api.createSnapshot(projectId, {
12952
+ name: options.name,
12953
+ description: options.description
12954
+ });
12955
+ setResult(snapshot);
12956
+ } catch (err) {
12957
+ setError(err instanceof Error ? err.message : "Failed to create snapshot");
12958
+ } finally {
12959
+ setIsLoading(false);
12960
+ }
12961
+ };
12962
+ createSnapshot();
12963
+ }, [options]);
12964
+ if (error) {
12965
+ return /* @__PURE__ */ jsxs49(Text49, { color: "red", children: [
12966
+ "Error: ",
12967
+ error
12968
+ ] });
12969
+ }
12970
+ if (isLoading) {
12971
+ return /* @__PURE__ */ jsxs49(Box46, { children: [
12972
+ /* @__PURE__ */ jsx52(Text49, { color: "green", children: /* @__PURE__ */ jsx52(Spinner28, { type: "dots" }) }),
12973
+ /* @__PURE__ */ jsx52(Text49, { children: " Creating snapshot..." })
12974
+ ] });
12975
+ }
12976
+ if (!result) {
12977
+ return /* @__PURE__ */ jsx52(Text49, { color: "red", children: "Failed to create snapshot" });
12978
+ }
12979
+ return /* @__PURE__ */ jsxs49(Box46, { flexDirection: "column", children: [
12980
+ /* @__PURE__ */ jsx52(Text49, { color: "green", children: "\u2713 Snapshot created successfully" }),
12981
+ /* @__PURE__ */ jsxs49(Text49, { children: [
12982
+ " ID: ",
12983
+ result.id
12984
+ ] }),
12985
+ /* @__PURE__ */ jsxs49(Text49, { children: [
12986
+ " Name: ",
12987
+ result.name
12988
+ ] }),
12989
+ result.description && /* @__PURE__ */ jsxs49(Text49, { children: [
12990
+ " Description: ",
12991
+ result.description
12992
+ ] }),
12993
+ /* @__PURE__ */ jsxs49(Text49, { children: [
12994
+ " Keys: ",
12995
+ result.keys_count.toLocaleString()
12996
+ ] }),
12997
+ /* @__PURE__ */ jsxs49(Text49, { children: [
12998
+ " Languages: ",
12999
+ result.languages_count
13000
+ ] }),
13001
+ /* @__PURE__ */ jsxs49(Text49, { dimColor: true, children: [
13002
+ " Created: ",
13003
+ new Date(result.created_at).toLocaleString()
13004
+ ] })
13005
+ ] });
13006
+ };
13007
+ function runSnapshotsCreate(options) {
13008
+ render31(/* @__PURE__ */ jsx52(SnapshotsCreate, { options }));
13009
+ }
13010
+
13011
+ // src/commands/snapshots/list.tsx
13012
+ import { useState as useState37, useEffect as useEffect34 } from "react";
13013
+ import { render as render32, Box as Box47, Text as Text50 } from "ink";
13014
+ import Spinner29 from "ink-spinner";
13015
+ import { jsx as jsx53, jsxs as jsxs50 } from "react/jsx-runtime";
13016
+ var SnapshotsList = ({ options }) => {
13017
+ const [snapshots, setSnapshots] = useState37([]);
13018
+ const [isLoading, setIsLoading] = useState37(true);
13019
+ const [error, setError] = useState37(null);
13020
+ useEffect34(() => {
13021
+ const listSnapshots = async () => {
13022
+ try {
13023
+ const resolved = getResolvedApiKey();
13024
+ if (!resolved?.key) {
13025
+ setError("Not authenticated. Run `intlpull login` or set INTLPULL_API_KEY");
13026
+ setIsLoading(false);
13027
+ return;
13028
+ }
13029
+ const config = getProjectConfig();
13030
+ let projectId = options.project || config?.projectId;
13031
+ if (!projectId) {
13032
+ const projectsApi = new ProjectsApi();
13033
+ const { projects: projects2 } = await projectsApi.listProjects();
13034
+ if (projects2.length === 0) {
13035
+ setError("No projects found");
13036
+ setIsLoading(false);
13037
+ return;
13038
+ }
13039
+ if (projects2.length === 1) {
13040
+ projectId = projects2[0].id;
13041
+ } else {
13042
+ setError(
13043
+ `Multiple projects found. Specify with --project:
13044
+ ` + projects2.map((p) => ` --project ${p.id} (${p.name})`).join("\n")
13045
+ );
13046
+ setIsLoading(false);
13047
+ return;
13048
+ }
13049
+ }
13050
+ if (!projectId) {
13051
+ setError("Could not determine project ID");
13052
+ setIsLoading(false);
13053
+ return;
13054
+ }
13055
+ const api = new SnapshotsApi();
13056
+ const result = await api.listSnapshots(projectId);
13057
+ setSnapshots(result);
13058
+ } catch (err) {
13059
+ setError(err instanceof Error ? err.message : "Failed to list snapshots");
13060
+ } finally {
13061
+ setIsLoading(false);
13062
+ }
13063
+ };
13064
+ listSnapshots();
13065
+ }, [options]);
13066
+ if (error) {
13067
+ return /* @__PURE__ */ jsxs50(Text50, { color: "red", children: [
13068
+ "Error: ",
13069
+ error
13070
+ ] });
13071
+ }
13072
+ if (isLoading) {
13073
+ return /* @__PURE__ */ jsxs50(Box47, { children: [
13074
+ /* @__PURE__ */ jsx53(Text50, { color: "green", children: /* @__PURE__ */ jsx53(Spinner29, { type: "dots" }) }),
13075
+ /* @__PURE__ */ jsx53(Text50, { children: " Loading snapshots..." })
13076
+ ] });
13077
+ }
13078
+ if (snapshots.length === 0) {
13079
+ return /* @__PURE__ */ jsxs50(Box47, { flexDirection: "column", children: [
13080
+ /* @__PURE__ */ jsx53(Text50, { children: "No snapshots found" }),
13081
+ /* @__PURE__ */ jsx53(Text50, { dimColor: true, children: 'Create one with: intlpull snapshots create --name="my-snapshot"' })
13082
+ ] });
13083
+ }
13084
+ const formatDate2 = (date) => {
13085
+ const d = new Date(date);
13086
+ const now = /* @__PURE__ */ new Date();
13087
+ const diffMs = now.getTime() - d.getTime();
13088
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
13089
+ if (diffDays === 0) {
13090
+ const diffHours = Math.floor(diffMs / (1e3 * 60 * 60));
13091
+ if (diffHours === 0) {
13092
+ const diffMins = Math.floor(diffMs / (1e3 * 60));
13093
+ return `${diffMins} min${diffMins === 1 ? "" : "s"} ago`;
13094
+ }
13095
+ return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
13096
+ }
13097
+ if (diffDays < 7) {
13098
+ return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
13099
+ }
13100
+ return d.toLocaleDateString();
13101
+ };
13102
+ return /* @__PURE__ */ jsxs50(Box47, { flexDirection: "column", children: [
13103
+ /* @__PURE__ */ jsxs50(Text50, { bold: true, children: [
13104
+ "Snapshots (",
13105
+ snapshots.length,
13106
+ ")"
13107
+ ] }),
13108
+ /* @__PURE__ */ jsxs50(Text50, { dimColor: true, children: [
13109
+ "ID",
13110
+ " ".repeat(34),
13111
+ "Name",
13112
+ " ".repeat(26),
13113
+ "Created",
13114
+ " ".repeat(10),
13115
+ "Keys",
13116
+ " ".repeat(5),
13117
+ "Languages"
13118
+ ] }),
13119
+ snapshots.map((s) => /* @__PURE__ */ jsxs50(Text50, { children: [
13120
+ s.id.substring(0, 8),
13121
+ "...",
13122
+ " ".repeat(25),
13123
+ s.name.substring(0, 25).padEnd(30),
13124
+ formatDate2(s.created_at).padEnd(17),
13125
+ s.keys_count.toLocaleString().padEnd(9),
13126
+ s.languages_count
13127
+ ] }, s.id))
13128
+ ] });
13129
+ };
13130
+ function runSnapshotsList(options) {
13131
+ render32(/* @__PURE__ */ jsx53(SnapshotsList, { options }));
13132
+ }
13133
+
13134
+ // src/commands/snapshots/restore.tsx
13135
+ import { useState as useState38, useEffect as useEffect35 } from "react";
13136
+ import { render as render33, Box as Box48, Text as Text51, useInput as useInput10 } from "ink";
13137
+ import Spinner30 from "ink-spinner";
13138
+ import { jsx as jsx54, jsxs as jsxs51 } from "react/jsx-runtime";
13139
+ var SnapshotsRestore = ({ options }) => {
13140
+ const [result, setResult] = useState38(null);
13141
+ const [isLoading, setIsLoading] = useState38(true);
13142
+ const [error, setError] = useState38(null);
13143
+ const [awaitingConfirmation, setAwaitingConfirmation] = useState38(false);
13144
+ const [projectId, setProjectId] = useState38(null);
13145
+ useInput10((input, key) => {
13146
+ if (!awaitingConfirmation) return;
13147
+ if (input.toLowerCase() === "y") {
13148
+ setAwaitingConfirmation(false);
13149
+ setIsLoading(true);
13150
+ performRestore();
13151
+ } else if (input.toLowerCase() === "n" || key.escape) {
13152
+ setError("Restore cancelled by user");
13153
+ setAwaitingConfirmation(false);
13154
+ }
13155
+ });
13156
+ const performRestore = async () => {
13157
+ if (!projectId) return;
13158
+ try {
13159
+ const api = new SnapshotsApi();
13160
+ const response = await api.restoreSnapshot(projectId, options.snapshotId);
13161
+ setResult(response);
13162
+ } catch (err) {
13163
+ setError(err instanceof Error ? err.message : "Failed to restore snapshot");
13164
+ } finally {
13165
+ setIsLoading(false);
13166
+ }
13167
+ };
13168
+ useEffect35(() => {
13169
+ const initRestore = async () => {
13170
+ try {
13171
+ const resolved = getResolvedApiKey();
13172
+ if (!resolved?.key) {
13173
+ setError("Not authenticated. Run `intlpull login` or set INTLPULL_API_KEY");
13174
+ setIsLoading(false);
13175
+ return;
13176
+ }
13177
+ const config = getProjectConfig();
13178
+ let resolvedProjectId = options.project || config?.projectId;
13179
+ if (!resolvedProjectId) {
13180
+ const projectsApi = new ProjectsApi();
13181
+ const { projects: projects2 } = await projectsApi.listProjects();
13182
+ if (projects2.length === 0) {
13183
+ setError("No projects found");
13184
+ setIsLoading(false);
13185
+ return;
13186
+ }
13187
+ if (projects2.length === 1) {
13188
+ resolvedProjectId = projects2[0].id;
13189
+ } else {
13190
+ setError(
13191
+ `Multiple projects found. Specify with --project:
13192
+ ` + projects2.map((p) => ` --project ${p.id} (${p.name})`).join("\n")
13193
+ );
13194
+ setIsLoading(false);
13195
+ return;
13196
+ }
13197
+ }
13198
+ if (!resolvedProjectId) {
13199
+ setError("Could not determine project ID");
13200
+ setIsLoading(false);
13201
+ return;
13202
+ }
13203
+ setProjectId(resolvedProjectId);
13204
+ if (!options.force && !process.stdin.isTTY) {
13205
+ setError("Refusing to restore in non-interactive mode without --force flag");
13206
+ setIsLoading(false);
13207
+ return;
13208
+ }
13209
+ if (options.force) {
13210
+ const api = new SnapshotsApi();
13211
+ const response = await api.restoreSnapshot(resolvedProjectId, options.snapshotId);
13212
+ setResult(response);
13213
+ setIsLoading(false);
13214
+ } else {
13215
+ setIsLoading(false);
13216
+ setAwaitingConfirmation(true);
13217
+ }
13218
+ } catch (err) {
13219
+ setError(err instanceof Error ? err.message : "Failed to restore snapshot");
13220
+ setIsLoading(false);
13221
+ }
13222
+ };
13223
+ initRestore();
13224
+ }, [options]);
13225
+ if (error) {
13226
+ return /* @__PURE__ */ jsxs51(Text51, { color: "red", children: [
13227
+ "Error: ",
13228
+ error
13229
+ ] });
13230
+ }
13231
+ if (awaitingConfirmation) {
13232
+ return /* @__PURE__ */ jsxs51(Box48, { flexDirection: "column", children: [
13233
+ /* @__PURE__ */ jsx54(Text51, { color: "yellow", children: "\u26A0 WARNING: This will overwrite the current project state!" }),
13234
+ /* @__PURE__ */ jsxs51(Text51, { children: [
13235
+ "Snapshot ID: ",
13236
+ options.snapshotId
13237
+ ] }),
13238
+ /* @__PURE__ */ jsx54(Box48, { marginTop: 1, children: /* @__PURE__ */ jsx54(Text51, { bold: true, children: "Are you sure you want to restore this snapshot? (Y/N): " }) })
13239
+ ] });
13240
+ }
13241
+ if (isLoading) {
13242
+ return /* @__PURE__ */ jsxs51(Box48, { children: [
13243
+ /* @__PURE__ */ jsx54(Text51, { color: "green", children: /* @__PURE__ */ jsx54(Spinner30, { type: "dots" }) }),
13244
+ /* @__PURE__ */ jsx54(Text51, { children: " Restoring snapshot..." })
13245
+ ] });
13246
+ }
13247
+ if (!result) {
13248
+ return /* @__PURE__ */ jsx54(Text51, { color: "red", children: "Failed to restore snapshot" });
13249
+ }
13250
+ return /* @__PURE__ */ jsxs51(Box48, { flexDirection: "column", children: [
13251
+ /* @__PURE__ */ jsx54(Text51, { color: "green", children: "\u2713 Snapshot restored successfully" }),
13252
+ /* @__PURE__ */ jsxs51(Text51, { children: [
13253
+ " Restored keys: ",
13254
+ result.restored_keys.toLocaleString()
13255
+ ] }),
13256
+ /* @__PURE__ */ jsxs51(Text51, { children: [
13257
+ " Restored translations: ",
13258
+ result.restored_translations.toLocaleString()
13259
+ ] }),
13260
+ /* @__PURE__ */ jsx54(Box48, { marginTop: 1, children: /* @__PURE__ */ jsx54(Text51, { dimColor: true, children: "Note: This operation overwrites current project state" }) })
13261
+ ] });
13262
+ };
13263
+ function runSnapshotsRestore(options) {
13264
+ render33(/* @__PURE__ */ jsx54(SnapshotsRestore, { options }));
13265
+ }
13266
+
13267
+ // src/commands/snapshots/delete.tsx
13268
+ import { useState as useState39, useEffect as useEffect36 } from "react";
13269
+ import { render as render34, Box as Box49, Text as Text52 } from "ink";
13270
+ import Spinner31 from "ink-spinner";
13271
+ import { jsx as jsx55, jsxs as jsxs52 } from "react/jsx-runtime";
13272
+ var SnapshotsDelete = ({ options }) => {
13273
+ const [isLoading, setIsLoading] = useState39(true);
13274
+ const [error, setError] = useState39(null);
13275
+ const [success, setSuccess] = useState39(false);
13276
+ useEffect36(() => {
13277
+ const deleteSnapshot = async () => {
13278
+ try {
13279
+ const resolved = getResolvedApiKey();
13280
+ if (!resolved?.key) {
13281
+ setError("Not authenticated. Run `intlpull login` or set INTLPULL_API_KEY");
13282
+ setIsLoading(false);
13283
+ return;
13284
+ }
13285
+ const config = getProjectConfig();
13286
+ let projectId = options.project || config?.projectId;
13287
+ if (!projectId) {
13288
+ const projectsApi = new ProjectsApi();
13289
+ const { projects: projects2 } = await projectsApi.listProjects();
13290
+ if (projects2.length === 0) {
13291
+ setError("No projects found");
13292
+ setIsLoading(false);
13293
+ return;
13294
+ }
13295
+ if (projects2.length === 1) {
13296
+ projectId = projects2[0].id;
13297
+ } else {
13298
+ setError(
13299
+ `Multiple projects found. Specify with --project:
13300
+ ` + projects2.map((p) => ` --project ${p.id} (${p.name})`).join("\n")
13301
+ );
13302
+ setIsLoading(false);
13303
+ return;
13304
+ }
13305
+ }
13306
+ if (!projectId) {
13307
+ setError("Could not determine project ID");
13308
+ setIsLoading(false);
13309
+ return;
13310
+ }
13311
+ if (!options.force && !process.stdin.isTTY) {
13312
+ setError("Refusing to delete in non-interactive mode without --force flag");
13313
+ setIsLoading(false);
13314
+ return;
13315
+ }
13316
+ const api = new SnapshotsApi();
13317
+ await api.deleteSnapshot(projectId, options.snapshotId);
13318
+ setSuccess(true);
13319
+ } catch (err) {
13320
+ setError(err instanceof Error ? err.message : "Failed to delete snapshot");
13321
+ } finally {
13322
+ setIsLoading(false);
13323
+ }
13324
+ };
13325
+ deleteSnapshot();
13326
+ }, [options]);
13327
+ if (error) {
13328
+ return /* @__PURE__ */ jsxs52(Text52, { color: "red", children: [
13329
+ "Error: ",
13330
+ error
13331
+ ] });
13332
+ }
13333
+ if (isLoading) {
13334
+ return /* @__PURE__ */ jsxs52(Box49, { children: [
13335
+ /* @__PURE__ */ jsx55(Text52, { color: "green", children: /* @__PURE__ */ jsx55(Spinner31, { type: "dots" }) }),
13336
+ /* @__PURE__ */ jsx55(Text52, { children: " Deleting snapshot..." })
13337
+ ] });
13338
+ }
13339
+ if (success) {
13340
+ return /* @__PURE__ */ jsx55(Text52, { color: "green", children: "\u2713 Snapshot deleted successfully" });
13341
+ }
13342
+ return /* @__PURE__ */ jsx55(Text52, { color: "red", children: "Failed to delete snapshot" });
13343
+ };
13344
+ function runSnapshotsDelete(options) {
13345
+ render34(/* @__PURE__ */ jsx55(SnapshotsDelete, { options }));
13346
+ }
13347
+
13348
+ // src/commands/snapshots/index.tsx
13349
+ var snapshotsCommand = new Command4("snapshots").description("Manage project snapshots (capture and restore project state)");
13350
+ snapshotsCommand.command("create").description("Create a snapshot of current project state").requiredOption("--name <name>", "Snapshot name").option("--description <desc>", "Snapshot description").option("--project <id>", "Project ID (auto-detected with project-scoped API key)").action((options) => {
13351
+ runSnapshotsCreate({
13352
+ name: options.name,
13353
+ description: options.description,
13354
+ project: options.project
13355
+ });
13356
+ });
13357
+ snapshotsCommand.command("list").description("List all snapshots for a project").option("--project <id>", "Project ID (auto-detected with project-scoped API key)").action((options) => {
13358
+ runSnapshotsList({
13359
+ project: options.project
13360
+ });
13361
+ });
13362
+ snapshotsCommand.command("restore <snapshotId>").description("Restore project to snapshot state").option("--project <id>", "Project ID (auto-detected with project-scoped API key)").option("-f, --force", "Skip confirmation prompt").action((snapshotId, options) => {
13363
+ runSnapshotsRestore({
13364
+ snapshotId,
13365
+ project: options.project,
13366
+ force: options.force
13367
+ });
13368
+ });
13369
+ snapshotsCommand.command("delete <snapshotId>").description("Delete a snapshot").option("--project <id>", "Project ID (auto-detected with project-scoped API key)").option("-f, --force", "Skip confirmation prompt").action((snapshotId, options) => {
13370
+ runSnapshotsDelete({
13371
+ snapshotId,
13372
+ project: options.project,
13373
+ force: options.force
13374
+ });
13375
+ });
13376
+
13377
+ // src/commands/webhooks/index.tsx
13378
+ import { Command as Command5 } from "commander";
13379
+
13380
+ // src/commands/webhooks/list.tsx
13381
+ import { useState as useState40, useEffect as useEffect37 } from "react";
13382
+ import { render as render35, Box as Box50, Text as Text53, Newline as Newline7 } from "ink";
13383
+ import Spinner32 from "ink-spinner";
13384
+ import { jsx as jsx56, jsxs as jsxs53 } from "react/jsx-runtime";
13385
+ function SimpleTable3({ data }) {
13386
+ if (data.length === 0) return null;
13387
+ const headers = Object.keys(data[0]);
13388
+ const colWidths = headers.map(
13389
+ (h) => Math.max(h.length, ...data.map((row) => String(row[h] || "").length))
13390
+ );
13391
+ const separator = "\u2500".repeat(colWidths.reduce((a, b) => a + b + 3, 1));
13392
+ return /* @__PURE__ */ jsxs53(Box50, { flexDirection: "column", children: [
13393
+ /* @__PURE__ */ jsx56(Text53, { children: separator }),
13394
+ /* @__PURE__ */ jsxs53(Text53, { children: [
13395
+ "\u2502 ",
13396
+ headers.map((h, i) => h.padEnd(colWidths[i])).join(" \u2502 "),
13397
+ " \u2502"
13398
+ ] }),
13399
+ /* @__PURE__ */ jsx56(Text53, { children: separator }),
13400
+ data.map((row, rowIdx) => /* @__PURE__ */ jsxs53(Text53, { children: [
13401
+ "\u2502 ",
13402
+ headers.map((h, i) => String(row[h] || "").padEnd(colWidths[i])).join(" \u2502 "),
13403
+ " \u2502"
13404
+ ] }, rowIdx)),
13405
+ /* @__PURE__ */ jsx56(Text53, { children: separator })
13406
+ ] });
13407
+ }
13408
+ var WebhooksList = ({ options }) => {
13409
+ const [webhooks, setWebhooks] = useState40([]);
13410
+ const [isLoading, setIsLoading] = useState40(true);
13411
+ const [error, setError] = useState40(null);
13412
+ useEffect37(() => {
13413
+ const fetchWebhooks = async () => {
13414
+ try {
13415
+ const config = getProjectConfig();
13416
+ const projectId = options.project || config?.projectId;
13417
+ if (!projectId) {
13418
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
13419
+ setIsLoading(false);
13420
+ return;
13421
+ }
13422
+ const api = new WebhooksApi();
13423
+ const result = await api.listWebhooks(projectId);
13424
+ setWebhooks(result.webhooks);
13425
+ } catch (err) {
13426
+ setError(err instanceof Error ? err.message : "Failed to fetch webhooks");
13427
+ } finally {
13428
+ setIsLoading(false);
13429
+ }
13430
+ };
13431
+ fetchWebhooks();
13432
+ }, [options]);
13433
+ if (error) {
13434
+ return /* @__PURE__ */ jsxs53(Text53, { color: "red", children: [
13435
+ "Error: ",
13436
+ error
13437
+ ] });
13438
+ }
13439
+ if (isLoading) {
13440
+ return /* @__PURE__ */ jsxs53(Text53, { children: [
13441
+ /* @__PURE__ */ jsx56(Text53, { color: "green", children: /* @__PURE__ */ jsx56(Spinner32, { type: "dots" }) }),
13442
+ " Loading webhooks..."
13443
+ ] });
13444
+ }
13445
+ if (webhooks.length === 0) {
13446
+ return /* @__PURE__ */ jsx56(Text53, { children: "No webhooks found." });
13447
+ }
13448
+ if (options.format === "json") {
13449
+ console.log(JSON.stringify(webhooks, null, 2));
13450
+ return null;
13451
+ }
13452
+ const data = webhooks.map((webhook) => {
13453
+ const eventsDisplay = webhook.events.length > 2 ? webhook.events.slice(0, 2).join(", ") + ` +${webhook.events.length - 2}` : webhook.events.join(", ");
13454
+ return {
13455
+ ID: webhook.id.substring(0, 12) + "...",
13456
+ URL: webhook.url.length > 35 ? webhook.url.substring(0, 32) + "..." : webhook.url,
13457
+ Events: eventsDisplay,
13458
+ Status: webhook.enabled ? "Active" : "Disabled",
13459
+ "Last Triggered": webhook.last_triggered_at || "Never"
13460
+ };
13461
+ });
13462
+ return /* @__PURE__ */ jsxs53(Box50, { flexDirection: "column", children: [
13463
+ /* @__PURE__ */ jsxs53(Text53, { children: [
13464
+ "Found ",
13465
+ webhooks.length,
13466
+ " webhook(s)"
13467
+ ] }),
13468
+ /* @__PURE__ */ jsx56(Newline7, {}),
13469
+ /* @__PURE__ */ jsx56(SimpleTable3, { data })
13470
+ ] });
13471
+ };
13472
+ function runWebhooksList(options) {
13473
+ render35(/* @__PURE__ */ jsx56(WebhooksList, { options }));
13474
+ }
13475
+
13476
+ // src/commands/webhooks/create.tsx
13477
+ import { useState as useState41, useEffect as useEffect38 } from "react";
13478
+ import { render as render36, Box as Box51, Text as Text54 } from "ink";
13479
+ import Spinner33 from "ink-spinner";
13480
+ import { jsx as jsx57, jsxs as jsxs54 } from "react/jsx-runtime";
13481
+ var WebhooksCreate = ({ options }) => {
13482
+ const [result, setResult] = useState41(null);
13483
+ const [isLoading, setIsLoading] = useState41(true);
13484
+ const [error, setError] = useState41(null);
13485
+ useEffect38(() => {
13486
+ const createWebhook = async () => {
13487
+ try {
13488
+ const config = getProjectConfig();
13489
+ const projectId = options.project || config?.projectId;
13490
+ if (!projectId) {
13491
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
13492
+ setIsLoading(false);
13493
+ return;
13494
+ }
13495
+ if (!options.url) {
13496
+ setError("Webhook URL is required. Use --url <url>");
13497
+ setIsLoading(false);
13498
+ return;
13499
+ }
13500
+ if (!options.events) {
13501
+ setError("Events are required. Use --events <event1,event2,...>");
13502
+ setIsLoading(false);
13503
+ return;
13504
+ }
13505
+ const api = new WebhooksApi();
13506
+ const events = options.events.split(",").map((e) => e.trim());
13507
+ const created = await api.createWebhook(projectId, {
13508
+ url: options.url,
13509
+ events,
13510
+ secret: options.secret,
13511
+ enabled: options.enabled !== false
13512
+ });
13513
+ setResult(created);
13514
+ } catch (err) {
13515
+ setError(err instanceof Error ? err.message : "Failed to create webhook");
13516
+ } finally {
13517
+ setIsLoading(false);
13518
+ }
13519
+ };
13520
+ createWebhook();
13521
+ }, [options]);
13522
+ if (error) {
13523
+ return /* @__PURE__ */ jsxs54(Text54, { color: "red", children: [
13524
+ "Error: ",
13525
+ error
13526
+ ] });
13527
+ }
13528
+ if (isLoading) {
13529
+ return /* @__PURE__ */ jsxs54(Text54, { children: [
13530
+ /* @__PURE__ */ jsx57(Text54, { color: "green", children: /* @__PURE__ */ jsx57(Spinner33, { type: "dots" }) }),
13531
+ " Creating webhook..."
13532
+ ] });
13533
+ }
13534
+ if (!result) {
13535
+ return /* @__PURE__ */ jsx57(Text54, { color: "red", children: "Failed to create webhook" });
13536
+ }
13537
+ const { webhook, secret } = result;
13538
+ return /* @__PURE__ */ jsxs54(Box51, { flexDirection: "column", children: [
13539
+ /* @__PURE__ */ jsx57(Text54, { color: "green", children: "\u2713 Webhook created successfully" }),
13540
+ /* @__PURE__ */ jsxs54(Text54, { children: [
13541
+ "ID: ",
13542
+ webhook.id
13543
+ ] }),
13544
+ /* @__PURE__ */ jsxs54(Text54, { children: [
13545
+ "URL: ",
13546
+ webhook.url
13547
+ ] }),
13548
+ /* @__PURE__ */ jsxs54(Text54, { children: [
13549
+ "Events: ",
13550
+ webhook.events.join(", ")
13551
+ ] }),
13552
+ /* @__PURE__ */ jsxs54(Text54, { children: [
13553
+ "Status: ",
13554
+ webhook.enabled ? "Active" : "Disabled"
13555
+ ] }),
13556
+ secret && /* @__PURE__ */ jsxs54(Box51, { flexDirection: "column", marginTop: 1, children: [
13557
+ /* @__PURE__ */ jsx57(Text54, { color: "yellow", children: "\u26A0 SECURITY WARNING" }),
13558
+ /* @__PURE__ */ jsx57(Text54, { color: "yellow", children: "Webhook secret shown below. Save it securely - it will NOT be shown again." }),
13559
+ /* @__PURE__ */ jsx57(Text54, { color: "yellow", children: "Clear your terminal history after copying." }),
13560
+ /* @__PURE__ */ jsx57(Box51, { marginTop: 1, borderStyle: "single", borderColor: "yellow", paddingX: 1, children: /* @__PURE__ */ jsx57(Text54, { bold: true, children: secret }) })
13561
+ ] })
13562
+ ] });
13563
+ };
13564
+ function runWebhooksCreate(options) {
13565
+ render36(/* @__PURE__ */ jsx57(WebhooksCreate, { options }));
13566
+ }
13567
+
13568
+ // src/commands/webhooks/test.tsx
13569
+ import { useState as useState42, useEffect as useEffect39 } from "react";
13570
+ import { render as render37, Box as Box52, Text as Text55 } from "ink";
13571
+ import Spinner34 from "ink-spinner";
13572
+ import { jsx as jsx58, jsxs as jsxs55 } from "react/jsx-runtime";
13573
+ var WebhooksTest = ({ webhookId, options }) => {
13574
+ const [result, setResult] = useState42(null);
13575
+ const [isLoading, setIsLoading] = useState42(true);
13576
+ const [error, setError] = useState42(null);
13577
+ useEffect39(() => {
13578
+ const testWebhook = async () => {
13579
+ try {
13580
+ const config = getProjectConfig();
13581
+ const projectId = options.project || config?.projectId;
13582
+ if (!projectId) {
13583
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
13584
+ setIsLoading(false);
13585
+ return;
13586
+ }
13587
+ const api = new WebhooksApi();
13588
+ const testResult = await api.testWebhook(projectId, webhookId);
13589
+ setResult(testResult);
13590
+ } catch (err) {
13591
+ setError(err instanceof Error ? err.message : "Failed to test webhook");
13592
+ } finally {
13593
+ setIsLoading(false);
13594
+ }
13595
+ };
13596
+ testWebhook();
13597
+ }, [webhookId, options]);
13598
+ if (error) {
13599
+ return /* @__PURE__ */ jsxs55(Text55, { color: "red", children: [
13600
+ "Error: ",
13601
+ error
13602
+ ] });
13603
+ }
13604
+ if (isLoading) {
13605
+ return /* @__PURE__ */ jsxs55(Text55, { children: [
13606
+ /* @__PURE__ */ jsx58(Text55, { color: "green", children: /* @__PURE__ */ jsx58(Spinner34, { type: "dots" }) }),
13607
+ " Testing webhook..."
13608
+ ] });
13609
+ }
13610
+ if (!result) {
13611
+ return /* @__PURE__ */ jsx58(Text55, { color: "red", children: "Failed to test webhook" });
13612
+ }
13613
+ if (result.success) {
13614
+ return /* @__PURE__ */ jsxs55(Box52, { flexDirection: "column", children: [
13615
+ /* @__PURE__ */ jsx58(Text55, { color: "green", children: "\u2713 Webhook test successful" }),
13616
+ result.status_code && /* @__PURE__ */ jsxs55(Text55, { children: [
13617
+ "Status Code: ",
13618
+ result.status_code
13619
+ ] }),
13620
+ result.response_time_ms && /* @__PURE__ */ jsxs55(Text55, { children: [
13621
+ "Response Time: ",
13622
+ result.response_time_ms,
13623
+ "ms"
13624
+ ] })
13625
+ ] });
13626
+ }
13627
+ return /* @__PURE__ */ jsxs55(Box52, { flexDirection: "column", children: [
13628
+ /* @__PURE__ */ jsx58(Text55, { color: "red", children: "\u2717 Webhook test failed" }),
13629
+ result.status_code && /* @__PURE__ */ jsxs55(Text55, { children: [
13630
+ "Status Code: ",
13631
+ result.status_code
13632
+ ] }),
13633
+ result.response_time_ms && /* @__PURE__ */ jsxs55(Text55, { children: [
13634
+ "Response Time: ",
13635
+ result.response_time_ms,
13636
+ "ms"
13637
+ ] }),
13638
+ result.error && /* @__PURE__ */ jsxs55(Text55, { children: [
13639
+ "Error: ",
13640
+ result.error
13641
+ ] })
13642
+ ] });
13643
+ };
13644
+ function runWebhooksTest(webhookId, options) {
13645
+ render37(/* @__PURE__ */ jsx58(WebhooksTest, { webhookId, options }));
13646
+ }
13647
+
13648
+ // src/commands/webhooks/delete.tsx
13649
+ import { useState as useState43, useEffect as useEffect40 } from "react";
13650
+ import { render as render38, Box as Box53, Text as Text56, useInput as useInput11 } from "ink";
13651
+ import Spinner35 from "ink-spinner";
13652
+ import { jsx as jsx59, jsxs as jsxs56 } from "react/jsx-runtime";
13653
+ var WebhooksDelete = ({ webhookId, options }) => {
13654
+ const [webhook, setWebhook] = useState43(null);
13655
+ const [isLoading, setIsLoading] = useState43(true);
13656
+ const [error, setError] = useState43(null);
13657
+ const [needsConfirmation, setNeedsConfirmation] = useState43(!options.force);
13658
+ const [isDeleting, setIsDeleting] = useState43(false);
13659
+ const [deleted, setDeleted] = useState43(false);
13660
+ useEffect40(() => {
13661
+ const findWebhook = async () => {
13662
+ try {
13663
+ const config = getProjectConfig();
13664
+ const projectId = options.project || config?.projectId;
13665
+ if (!projectId) {
13666
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
13667
+ setIsLoading(false);
13668
+ return;
13669
+ }
13670
+ const api = new WebhooksApi();
13671
+ const webhookData = await api.getWebhook(projectId, webhookId);
13672
+ setWebhook(webhookData);
13673
+ setIsLoading(false);
13674
+ if (options.force) {
13675
+ await performDelete(projectId, webhookId, api);
13676
+ }
13677
+ } catch (err) {
13678
+ setError(err instanceof Error ? err.message : "Failed to find webhook");
13679
+ setIsLoading(false);
13680
+ }
13681
+ };
13682
+ findWebhook();
13683
+ }, [webhookId, options.force, options.project]);
13684
+ const performDelete = async (projectId, id, api) => {
13685
+ try {
13686
+ setIsDeleting(true);
13687
+ await api.deleteWebhook(projectId, id);
13688
+ setDeleted(true);
13689
+ setNeedsConfirmation(false);
13690
+ } catch (err) {
13691
+ setError(err instanceof Error ? err.message : "Failed to delete webhook");
13692
+ } finally {
13693
+ setIsDeleting(false);
13694
+ }
13695
+ };
13696
+ useInput11((input, inputKey) => {
13697
+ if (!needsConfirmation || isDeleting) return;
13698
+ if (input.toLowerCase() === "y") {
13699
+ const config = getProjectConfig();
13700
+ const projectId = options.project || config?.projectId;
13701
+ if (projectId) {
13702
+ const api = new WebhooksApi();
13703
+ performDelete(projectId, webhookId, api);
13704
+ }
13705
+ } else if (input.toLowerCase() === "n" || inputKey.escape) {
13706
+ process.exit(0);
13707
+ }
13708
+ });
13709
+ if (error) {
13710
+ return /* @__PURE__ */ jsxs56(Text56, { color: "red", children: [
13711
+ "Error: ",
13712
+ error
13713
+ ] });
13714
+ }
13715
+ if (isLoading) {
13716
+ return /* @__PURE__ */ jsxs56(Text56, { children: [
13717
+ /* @__PURE__ */ jsx59(Text56, { color: "green", children: /* @__PURE__ */ jsx59(Spinner35, { type: "dots" }) }),
13718
+ " Finding webhook..."
13719
+ ] });
13720
+ }
13721
+ if (isDeleting) {
13722
+ return /* @__PURE__ */ jsxs56(Text56, { children: [
13723
+ /* @__PURE__ */ jsx59(Text56, { color: "green", children: /* @__PURE__ */ jsx59(Spinner35, { type: "dots" }) }),
13724
+ " Deleting webhook..."
13725
+ ] });
13726
+ }
13727
+ if (deleted) {
13728
+ return /* @__PURE__ */ jsxs56(Box53, { flexDirection: "column", children: [
13729
+ /* @__PURE__ */ jsx59(Text56, { color: "green", children: "\u2713 Webhook deleted successfully" }),
13730
+ /* @__PURE__ */ jsxs56(Text56, { children: [
13731
+ "ID: ",
13732
+ webhookId
13733
+ ] })
13734
+ ] });
13735
+ }
13736
+ if (needsConfirmation && webhook) {
13737
+ return /* @__PURE__ */ jsxs56(Box53, { flexDirection: "column", children: [
13738
+ /* @__PURE__ */ jsx59(Box53, { marginBottom: 1, children: /* @__PURE__ */ jsx59(Text56, { color: "yellow", children: "Warning: Delete Webhook" }) }),
13739
+ /* @__PURE__ */ jsxs56(Box53, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
13740
+ /* @__PURE__ */ jsx59(Text56, { children: "You are about to delete the following webhook:" }),
13741
+ /* @__PURE__ */ jsx59(Text56, { dimColor: true }),
13742
+ /* @__PURE__ */ jsxs56(Text56, { children: [
13743
+ " ID: ",
13744
+ /* @__PURE__ */ jsx59(Text56, { bold: true, children: webhook.id })
13745
+ ] }),
13746
+ /* @__PURE__ */ jsxs56(Text56, { children: [
13747
+ " URL: ",
13748
+ /* @__PURE__ */ jsx59(Text56, { bold: true, children: webhook.url })
13749
+ ] }),
13750
+ /* @__PURE__ */ jsxs56(Text56, { children: [
13751
+ " Events: ",
13752
+ webhook.events.join(", ")
13753
+ ] })
13754
+ ] }),
13755
+ /* @__PURE__ */ jsxs56(Box53, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
13756
+ /* @__PURE__ */ jsx59(Text56, { color: "red", children: "This will permanently remove the webhook." }),
13757
+ /* @__PURE__ */ jsx59(Text56, { dimColor: true, children: "This action cannot be undone." })
13758
+ ] }),
13759
+ /* @__PURE__ */ jsx59(Box53, { marginLeft: 2, children: /* @__PURE__ */ jsxs56(Text56, { children: [
13760
+ "Press",
13761
+ " ",
13762
+ /* @__PURE__ */ jsx59(Text56, { bold: true, color: "green", children: "Y" }),
13763
+ " ",
13764
+ "to confirm,",
13765
+ " ",
13766
+ /* @__PURE__ */ jsx59(Text56, { bold: true, color: "red", children: "N" }),
13767
+ " ",
13768
+ "to cancel"
13769
+ ] }) })
13770
+ ] });
13771
+ }
13772
+ return null;
13773
+ };
13774
+ function runWebhooksDelete(webhookId, options) {
13775
+ render38(/* @__PURE__ */ jsx59(WebhooksDelete, { webhookId, options }));
13776
+ }
13777
+
13778
+ // src/commands/webhooks/index.tsx
13779
+ var webhooksCommand = new Command5("webhooks").description("Manage webhooks");
13780
+ webhooksCommand.command("list").description("List all webhooks").option("--project <id>", "Project ID").option("--format <format>", "Output format (table|json)", "table").action((options) => {
13781
+ runWebhooksList({
13782
+ project: options.project,
13783
+ format: options.format
13784
+ });
13785
+ });
13786
+ webhooksCommand.command("create").description("Create a new webhook").requiredOption("--url <url>", "Webhook URL endpoint").requiredOption("--events <events>", "Comma-separated list of events (e.g., translation.created,key.updated)").option("--project <id>", "Project ID").option("--secret <secret>", "Webhook secret for payload signing").option("--enabled", "Enable webhook immediately (default: true)", true).action((options) => {
13787
+ runWebhooksCreate({
13788
+ project: options.project,
13789
+ url: options.url,
13790
+ events: options.events,
13791
+ secret: options.secret,
13792
+ enabled: options.enabled
13793
+ });
13794
+ });
13795
+ webhooksCommand.command("test <id>").description("Test a webhook by sending a test event").option("--project <id>", "Project ID").action((id, options) => {
13796
+ runWebhooksTest(id, {
13797
+ project: options.project
13798
+ });
13799
+ });
13800
+ webhooksCommand.command("delete <id>").description("Delete a webhook").option("--project <id>", "Project ID").option("--force", "Skip confirmation prompt").action((id, options) => {
13801
+ runWebhooksDelete(id, {
13802
+ project: options.project,
13803
+ force: options.force
13804
+ });
13805
+ });
13806
+
13807
+ // src/commands/contributors/index.tsx
13808
+ import { Command as Command6 } from "commander";
13809
+
13810
+ // src/commands/contributors/list.tsx
13811
+ import { useState as useState44, useEffect as useEffect41 } from "react";
13812
+ import { render as render39, Box as Box54, Text as Text57, Newline as Newline8 } from "ink";
13813
+ import Spinner36 from "ink-spinner";
13814
+ import { jsx as jsx60, jsxs as jsxs57 } from "react/jsx-runtime";
13815
+ function SimpleTable4({ data }) {
13816
+ if (data.length === 0) return null;
13817
+ const headers = Object.keys(data[0]);
13818
+ const colWidths = headers.map(
13819
+ (h) => Math.max(h.length, ...data.map((row) => String(row[h] || "").length))
13820
+ );
13821
+ const separator = "\u2500".repeat(colWidths.reduce((a, b) => a + b + 3, 1));
13822
+ return /* @__PURE__ */ jsxs57(Box54, { flexDirection: "column", children: [
13823
+ /* @__PURE__ */ jsx60(Text57, { children: separator }),
13824
+ /* @__PURE__ */ jsxs57(Text57, { children: [
13825
+ "\u2502 ",
13826
+ headers.map((h, i) => h.padEnd(colWidths[i])).join(" \u2502 "),
13827
+ " \u2502"
13828
+ ] }),
13829
+ /* @__PURE__ */ jsx60(Text57, { children: separator }),
13830
+ data.map((row, rowIdx) => /* @__PURE__ */ jsxs57(Text57, { children: [
13831
+ "\u2502 ",
13832
+ headers.map((h, i) => String(row[h] || "").padEnd(colWidths[i])).join(" \u2502 "),
13833
+ " \u2502"
13834
+ ] }, rowIdx)),
13835
+ /* @__PURE__ */ jsx60(Text57, { children: separator })
13836
+ ] });
13837
+ }
13838
+ var ContributorsList = ({ options }) => {
13839
+ const [members, setMembers] = useState44([]);
13840
+ const [isLoading, setIsLoading] = useState44(true);
13841
+ const [error, setError] = useState44(null);
13842
+ useEffect41(() => {
13843
+ const fetchMembers = async () => {
13844
+ try {
13845
+ const config = getProjectConfig();
13846
+ const projectId = options.project || config?.projectId;
13847
+ if (!projectId) {
13848
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
13849
+ setIsLoading(false);
13850
+ return;
13851
+ }
13852
+ const api = new ContributorsApi();
13853
+ const result = await api.listTeamMembers(projectId);
13854
+ setMembers(result.members);
13855
+ } catch (err) {
13856
+ setError(err instanceof Error ? err.message : "Failed to fetch team members");
13857
+ } finally {
13858
+ setIsLoading(false);
13859
+ }
13860
+ };
13861
+ fetchMembers();
13862
+ }, [options]);
13863
+ if (error) {
13864
+ return /* @__PURE__ */ jsxs57(Text57, { color: "red", children: [
13865
+ "Error: ",
13866
+ error
13867
+ ] });
13868
+ }
13869
+ if (isLoading) {
13870
+ return /* @__PURE__ */ jsxs57(Text57, { children: [
13871
+ /* @__PURE__ */ jsx60(Text57, { color: "green", children: /* @__PURE__ */ jsx60(Spinner36, { type: "dots" }) }),
13872
+ " Loading team members..."
13873
+ ] });
13874
+ }
13875
+ if (members.length === 0) {
13876
+ return /* @__PURE__ */ jsx60(Text57, { children: "No team members found." });
13877
+ }
13878
+ if (options.format === "json") {
13879
+ console.log(JSON.stringify(members, null, 2));
13880
+ return null;
13881
+ }
13882
+ const data = members.map((member) => {
13883
+ const emailDisplay = member.email.length > 25 ? member.email.substring(0, 22) + "..." : member.email;
13884
+ const languagesDisplay = member.languages && member.languages.length > 0 ? member.languages.length > 3 ? member.languages.slice(0, 3).join(",") + ` +${member.languages.length - 3}` : member.languages.join(",") : "All";
13885
+ const joinedDate = new Date(member.joined_at).toLocaleDateString();
13886
+ return {
13887
+ Email: emailDisplay,
13888
+ Name: member.name || "-",
13889
+ Role: member.role.charAt(0).toUpperCase() + member.role.slice(1),
13890
+ Languages: languagesDisplay,
13891
+ Joined: joinedDate
13892
+ };
13893
+ });
13894
+ return /* @__PURE__ */ jsxs57(Box54, { flexDirection: "column", children: [
13895
+ /* @__PURE__ */ jsxs57(Text57, { children: [
13896
+ "Found ",
13897
+ members.length,
13898
+ " team member(s)"
13899
+ ] }),
13900
+ /* @__PURE__ */ jsx60(Newline8, {}),
13901
+ /* @__PURE__ */ jsx60(SimpleTable4, { data })
13902
+ ] });
13903
+ };
13904
+ function runContributorsList(options) {
13905
+ render39(/* @__PURE__ */ jsx60(ContributorsList, { options }));
13906
+ }
13907
+
13908
+ // src/commands/contributors/add.tsx
13909
+ import { useState as useState45, useEffect as useEffect42 } from "react";
13910
+ import { render as render40, Box as Box55, Text as Text58 } from "ink";
13911
+ import Spinner37 from "ink-spinner";
13912
+ import { jsx as jsx61, jsxs as jsxs58 } from "react/jsx-runtime";
13913
+ var ContributorsAdd = ({ options }) => {
13914
+ const [result, setResult] = useState45(null);
13915
+ const [isLoading, setIsLoading] = useState45(true);
13916
+ const [error, setError] = useState45(null);
13917
+ useEffect42(() => {
13918
+ const addContributor = async () => {
13919
+ try {
13920
+ const config = getProjectConfig();
13921
+ const projectId = options.project || config?.projectId;
13922
+ if (!projectId) {
13923
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
13924
+ setIsLoading(false);
13925
+ return;
13926
+ }
13927
+ if (!options.email) {
13928
+ setError("Email is required. Use --email <email>");
13929
+ setIsLoading(false);
13930
+ return;
13931
+ }
13932
+ if (!options.role) {
13933
+ setError("Role is required. Use --role <owner|admin|member|viewer>");
13934
+ setIsLoading(false);
13935
+ return;
13936
+ }
13937
+ const validRoles = ["owner", "admin", "member", "viewer"];
13938
+ if (!validRoles.includes(options.role)) {
13939
+ setError("Invalid role. Must be one of: owner, admin, member, viewer");
13940
+ setIsLoading(false);
13941
+ return;
13942
+ }
13943
+ const api = new ContributorsApi();
13944
+ const languages = options.languages ? options.languages.split(",").map((l) => l.trim()) : void 0;
13945
+ const invited = await api.inviteTeamMember(projectId, {
13946
+ email: options.email,
13947
+ role: options.role,
13948
+ languages,
13949
+ message: options.message
13950
+ });
13951
+ setResult(invited.member);
13952
+ } catch (err) {
13953
+ setError(err instanceof Error ? err.message : "Failed to invite team member");
13954
+ } finally {
13955
+ setIsLoading(false);
13956
+ }
13957
+ };
13958
+ addContributor();
13959
+ }, [options]);
13960
+ if (error) {
13961
+ return /* @__PURE__ */ jsxs58(Text58, { color: "red", children: [
13962
+ "Error: ",
13963
+ error
13964
+ ] });
13965
+ }
13966
+ if (isLoading) {
13967
+ return /* @__PURE__ */ jsxs58(Text58, { children: [
13968
+ /* @__PURE__ */ jsx61(Text58, { color: "green", children: /* @__PURE__ */ jsx61(Spinner37, { type: "dots" }) }),
13969
+ " Inviting team member..."
13970
+ ] });
13971
+ }
13972
+ if (!result) {
13973
+ return /* @__PURE__ */ jsx61(Text58, { color: "red", children: "Failed to invite team member" });
13974
+ }
13975
+ return /* @__PURE__ */ jsxs58(Box55, { flexDirection: "column", children: [
13976
+ /* @__PURE__ */ jsx61(Text58, { color: "green", children: "\u2713 Team member invited successfully" }),
13977
+ /* @__PURE__ */ jsxs58(Text58, { children: [
13978
+ "Email: ",
13979
+ result.email
13980
+ ] }),
13981
+ /* @__PURE__ */ jsxs58(Text58, { children: [
13982
+ "Role: ",
13983
+ result.role
13984
+ ] }),
13985
+ result.languages && result.languages.length > 0 && /* @__PURE__ */ jsxs58(Text58, { children: [
13986
+ "Languages: ",
13987
+ result.languages.join(", ")
13988
+ ] }),
13989
+ /* @__PURE__ */ jsx61(Box55, { marginTop: 1, children: /* @__PURE__ */ jsxs58(Text58, { dimColor: true, children: [
13990
+ "An invitation email has been sent to ",
13991
+ result.email
13992
+ ] }) })
13993
+ ] });
13994
+ };
13995
+ function runContributorsAdd(options) {
13996
+ render40(/* @__PURE__ */ jsx61(ContributorsAdd, { options }));
13997
+ }
13998
+
13999
+ // src/commands/contributors/update.tsx
14000
+ import { useState as useState46, useEffect as useEffect43 } from "react";
14001
+ import { render as render41, Box as Box56, Text as Text59 } from "ink";
14002
+ import Spinner38 from "ink-spinner";
14003
+ import { jsx as jsx62, jsxs as jsxs59 } from "react/jsx-runtime";
14004
+ var ContributorsUpdate = ({ userId, options }) => {
14005
+ const [result, setResult] = useState46(null);
14006
+ const [isLoading, setIsLoading] = useState46(true);
14007
+ const [error, setError] = useState46(null);
14008
+ useEffect43(() => {
14009
+ const updateContributor = async () => {
14010
+ try {
14011
+ const config = getProjectConfig();
14012
+ const projectId = options.project || config?.projectId;
14013
+ if (!projectId) {
14014
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
14015
+ setIsLoading(false);
14016
+ return;
14017
+ }
14018
+ if (!options.role && !options.languages) {
14019
+ setError("At least one of --role or --languages is required");
14020
+ setIsLoading(false);
14021
+ return;
14022
+ }
14023
+ const validRoles = ["owner", "admin", "member", "viewer"];
14024
+ if (options.role && !validRoles.includes(options.role)) {
14025
+ setError("Invalid role. Must be one of: owner, admin, member, viewer");
14026
+ setIsLoading(false);
14027
+ return;
14028
+ }
14029
+ const api = new ContributorsApi();
14030
+ const languages = options.languages ? options.languages.split(",").map((l) => l.trim()) : void 0;
14031
+ const updated = await api.updateTeamMember(projectId, userId, {
14032
+ role: options.role,
14033
+ languages
14034
+ });
14035
+ setResult(updated.member);
14036
+ } catch (err) {
14037
+ setError(err instanceof Error ? err.message : "Failed to update team member");
14038
+ } finally {
14039
+ setIsLoading(false);
14040
+ }
14041
+ };
14042
+ updateContributor();
14043
+ }, [userId, options]);
14044
+ if (error) {
14045
+ return /* @__PURE__ */ jsxs59(Text59, { color: "red", children: [
14046
+ "Error: ",
14047
+ error
14048
+ ] });
14049
+ }
14050
+ if (isLoading) {
14051
+ return /* @__PURE__ */ jsxs59(Text59, { children: [
14052
+ /* @__PURE__ */ jsx62(Text59, { color: "green", children: /* @__PURE__ */ jsx62(Spinner38, { type: "dots" }) }),
14053
+ " Updating team member..."
14054
+ ] });
14055
+ }
14056
+ if (!result) {
14057
+ return /* @__PURE__ */ jsx62(Text59, { color: "red", children: "Failed to update team member" });
14058
+ }
14059
+ return /* @__PURE__ */ jsxs59(Box56, { flexDirection: "column", children: [
14060
+ /* @__PURE__ */ jsx62(Text59, { color: "green", children: "\u2713 Team member updated successfully" }),
14061
+ /* @__PURE__ */ jsxs59(Text59, { children: [
14062
+ "Email: ",
14063
+ result.email
14064
+ ] }),
14065
+ /* @__PURE__ */ jsxs59(Text59, { children: [
14066
+ "Role: ",
14067
+ result.role
14068
+ ] }),
14069
+ result.languages && result.languages.length > 0 && /* @__PURE__ */ jsxs59(Text59, { children: [
14070
+ "Languages: ",
14071
+ result.languages.join(", ")
14072
+ ] })
14073
+ ] });
14074
+ };
14075
+ function runContributorsUpdate(userId, options) {
14076
+ render41(/* @__PURE__ */ jsx62(ContributorsUpdate, { userId, options }));
14077
+ }
14078
+
14079
+ // src/commands/contributors/remove.tsx
14080
+ import { useState as useState47, useEffect as useEffect44 } from "react";
14081
+ import { render as render42, Box as Box57, Text as Text60, useInput as useInput12 } from "ink";
14082
+ import Spinner39 from "ink-spinner";
14083
+ import { jsx as jsx63, jsxs as jsxs60 } from "react/jsx-runtime";
14084
+ var ContributorsRemove = ({ userId, options }) => {
14085
+ const [member, setMember] = useState47(null);
14086
+ const [isLoading, setIsLoading] = useState47(true);
14087
+ const [error, setError] = useState47(null);
14088
+ const [needsConfirmation, setNeedsConfirmation] = useState47(!options.force);
14089
+ const [isRemoving, setIsRemoving] = useState47(false);
14090
+ const [removed, setRemoved] = useState47(false);
14091
+ useEffect44(() => {
14092
+ const findMember = async () => {
14093
+ try {
14094
+ const config = getProjectConfig();
14095
+ const projectId = options.project || config?.projectId;
14096
+ if (!projectId) {
14097
+ setError("Project ID is required. Use --project <id> or run from a linked directory.");
14098
+ setIsLoading(false);
14099
+ return;
14100
+ }
14101
+ const api = new ContributorsApi();
14102
+ const result = await api.listTeamMembers(projectId);
14103
+ const existingMember = result.members.find((m) => m.id === userId || m.email === userId);
14104
+ if (!existingMember) {
14105
+ setError(`Team member "${userId}" not found`);
14106
+ setIsLoading(false);
14107
+ return;
14108
+ }
14109
+ setMember(existingMember);
14110
+ setIsLoading(false);
14111
+ if (options.force) {
14112
+ await performRemove(projectId, existingMember.id, api);
14113
+ }
14114
+ } catch (err) {
14115
+ setError(err instanceof Error ? err.message : "Failed to find team member");
14116
+ setIsLoading(false);
14117
+ }
14118
+ };
14119
+ findMember();
14120
+ }, [userId, options.force, options.project]);
14121
+ const performRemove = async (projectId, memberId, api) => {
14122
+ try {
14123
+ setIsRemoving(true);
14124
+ await api.removeTeamMember(projectId, memberId);
14125
+ setRemoved(true);
14126
+ setNeedsConfirmation(false);
14127
+ } catch (err) {
14128
+ setError(err instanceof Error ? err.message : "Failed to remove team member");
14129
+ } finally {
14130
+ setIsRemoving(false);
14131
+ }
14132
+ };
14133
+ useInput12((input, inputKey) => {
14134
+ if (!needsConfirmation || isRemoving) return;
14135
+ if (input.toLowerCase() === "y") {
14136
+ const config = getProjectConfig();
14137
+ const projectId = options.project || config?.projectId;
14138
+ if (projectId && member) {
14139
+ const api = new ContributorsApi();
14140
+ performRemove(projectId, member.id, api);
14141
+ }
14142
+ } else if (input.toLowerCase() === "n" || inputKey.escape) {
14143
+ process.exit(0);
14144
+ }
14145
+ });
14146
+ if (error) {
14147
+ return /* @__PURE__ */ jsxs60(Text60, { color: "red", children: [
14148
+ "Error: ",
14149
+ error
14150
+ ] });
14151
+ }
14152
+ if (isLoading) {
14153
+ return /* @__PURE__ */ jsxs60(Text60, { children: [
14154
+ /* @__PURE__ */ jsx63(Text60, { color: "green", children: /* @__PURE__ */ jsx63(Spinner39, { type: "dots" }) }),
14155
+ " Finding team member..."
14156
+ ] });
14157
+ }
14158
+ if (isRemoving) {
14159
+ return /* @__PURE__ */ jsxs60(Text60, { children: [
14160
+ /* @__PURE__ */ jsx63(Text60, { color: "green", children: /* @__PURE__ */ jsx63(Spinner39, { type: "dots" }) }),
14161
+ " Removing team member..."
14162
+ ] });
14163
+ }
14164
+ if (removed) {
14165
+ return /* @__PURE__ */ jsxs60(Box57, { flexDirection: "column", children: [
14166
+ /* @__PURE__ */ jsx63(Text60, { color: "green", children: "\u2713 Team member removed successfully" }),
14167
+ /* @__PURE__ */ jsxs60(Text60, { children: [
14168
+ "Email: ",
14169
+ member?.email
14170
+ ] })
14171
+ ] });
14172
+ }
14173
+ if (needsConfirmation && member) {
14174
+ return /* @__PURE__ */ jsxs60(Box57, { flexDirection: "column", children: [
14175
+ /* @__PURE__ */ jsx63(Box57, { marginBottom: 1, children: /* @__PURE__ */ jsx63(Text60, { color: "yellow", children: "Warning: Remove Team Member" }) }),
14176
+ /* @__PURE__ */ jsxs60(Box57, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
14177
+ /* @__PURE__ */ jsx63(Text60, { children: "You are about to remove the following team member:" }),
14178
+ /* @__PURE__ */ jsx63(Text60, { dimColor: true }),
14179
+ /* @__PURE__ */ jsxs60(Text60, { children: [
14180
+ " Email: ",
14181
+ /* @__PURE__ */ jsx63(Text60, { bold: true, children: member.email })
14182
+ ] }),
14183
+ /* @__PURE__ */ jsxs60(Text60, { children: [
14184
+ " Name: ",
14185
+ /* @__PURE__ */ jsx63(Text60, { bold: true, children: member.name || "-" })
14186
+ ] }),
14187
+ /* @__PURE__ */ jsxs60(Text60, { children: [
14188
+ " Role: ",
14189
+ member.role
14190
+ ] }),
14191
+ member.languages && member.languages.length > 0 && /* @__PURE__ */ jsxs60(Text60, { children: [
14192
+ " Languages: ",
14193
+ member.languages.join(", ")
14194
+ ] })
14195
+ ] }),
14196
+ /* @__PURE__ */ jsxs60(Box57, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
14197
+ /* @__PURE__ */ jsx63(Text60, { color: "red", children: "This will revoke their access to the project." }),
14198
+ /* @__PURE__ */ jsx63(Text60, { dimColor: true, children: "This action cannot be undone." })
14199
+ ] }),
14200
+ /* @__PURE__ */ jsx63(Box57, { marginLeft: 2, children: /* @__PURE__ */ jsxs60(Text60, { children: [
14201
+ "Press",
14202
+ " ",
14203
+ /* @__PURE__ */ jsx63(Text60, { bold: true, color: "green", children: "Y" }),
14204
+ " ",
14205
+ "to confirm,",
14206
+ " ",
14207
+ /* @__PURE__ */ jsx63(Text60, { bold: true, color: "red", children: "N" }),
14208
+ " ",
14209
+ "to cancel"
14210
+ ] }) })
14211
+ ] });
14212
+ }
14213
+ return null;
14214
+ };
14215
+ function runContributorsRemove(userId, options) {
14216
+ render42(/* @__PURE__ */ jsx63(ContributorsRemove, { userId, options }));
14217
+ }
14218
+
14219
+ // src/commands/contributors/index.tsx
14220
+ var contributorsCommand = new Command6("contributors").description("Manage team contributors");
14221
+ contributorsCommand.command("list").description("List all team members").option("--project <id>", "Project ID").option("--format <format>", "Output format (table|json)", "table").action((options) => {
14222
+ runContributorsList({
14223
+ project: options.project,
14224
+ format: options.format
14225
+ });
14226
+ });
14227
+ contributorsCommand.command("add").description("Invite a new team member").requiredOption("--email <email>", "Email address of the person to invite").requiredOption("--role <role>", "Role: owner, admin, member, or viewer").option("--project <id>", "Project ID").option("--languages <langs>", "Languages the member can access (comma-separated, default: all)").option("--message <message>", "Custom invitation message").action((options) => {
14228
+ runContributorsAdd({
14229
+ project: options.project,
14230
+ email: options.email,
14231
+ role: options.role,
14232
+ languages: options.languages,
14233
+ message: options.message
14234
+ });
14235
+ });
14236
+ contributorsCommand.command("update <userId>").description("Update a team member's role or permissions").option("--project <id>", "Project ID").option("--role <role>", "New role: owner, admin, member, or viewer").option("--languages <langs>", "Languages the member can access (comma-separated)").action((userId, options) => {
14237
+ runContributorsUpdate(userId, {
14238
+ project: options.project,
14239
+ role: options.role,
14240
+ languages: options.languages
14241
+ });
14242
+ });
14243
+ contributorsCommand.command("remove <userId>").description("Remove a team member").option("--project <id>", "Project ID").option("--force", "Skip confirmation prompt").action((userId, options) => {
14244
+ runContributorsRemove(userId, {
14245
+ project: options.project,
14246
+ force: options.force
14247
+ });
14248
+ });
14249
+
14250
+ // src/index.tsx
14251
+ var program = new Command7();
14252
+ program.name("intlpull").description("Intelligent i18n CLI for modern apps").version("0.1.5").option("--env-file <path>", "Path to custom env file (e.g., .env.production)").hook("preAction", (thisCommand) => {
14253
+ const envFile = thisCommand.opts().envFile;
14254
+ if (envFile) {
14255
+ setCustomEnvFile(envFile);
14256
+ }
14257
+ const { loaded } = loadEnvFiles();
14258
+ if (loaded.length > 0 && process.env.INTLPULL_DEBUG) {
14259
+ console.log(`Loaded env from: ${loaded.join(", ")}`);
14260
+ }
14261
+ });
14262
+ program.command("init").description("Initialize IntlPull in your project").option("--framework <framework>", "Framework (next|react|vue|svelte|astro)").option("--library <library>", "i18n library (next-intl|react-i18next|vue-i18n)").option("--output <dir>", "Output directory for translation files").option("--project <id>", "Project ID to link").option("-y, --yes", "Auto-detect and initialize without prompts (non-interactive mode)").action((options) => {
14263
+ runInit({
14264
+ framework: options.framework,
14265
+ library: options.library,
14266
+ output: options.output,
14267
+ project: options.project,
14268
+ yes: options.yes
14269
+ });
14270
+ });
14271
+ program.command("status").description("Show project status and translation progress").action(() => {
14272
+ runStatus();
14273
+ });
14274
+ program.command("login").description("Authenticate with IntlPull").option("--token <token>", "Use API token (prefer INTLPULL_API_KEY env var for security)").action((options) => {
14275
+ runLogin({ token: options.token });
14276
+ });
14277
+ program.command("logout").description("Clear stored credentials").action(async () => {
14278
+ const { clearAuthConfig } = await import("./config-F3O3XWYX.js");
14279
+ clearAuthConfig();
14280
+ console.log("\u2713 Logged out successfully");
14281
+ });
14282
+ program.command("whoami").description("Show current authenticated user").action(async () => {
14283
+ const { getResolvedApiKey: getResolvedApiKey2, getAuthErrorMessage: getAuthErrorMessage2 } = await import("./config-F3O3XWYX.js");
14284
+ const { createApiClient: createApiClient2 } = await import("./api-77ZWU4IB.js");
14285
+ const resolved = getResolvedApiKey2();
14286
+ if (!resolved) {
14287
+ console.log(getAuthErrorMessage2());
14288
+ return;
14289
+ }
14290
+ try {
14291
+ const api = createApiClient2();
14292
+ const { projects: projects2 } = await api.listProjects();
14293
+ const sourceLabel = resolved.source === "env" ? "INTLPULL_API_KEY env var" : "~/.intlpull/auth.json";
14294
+ console.log("\n\u2713 Authenticated successfully\n");
14295
+ console.log(" API Key:", resolved.key.substring(0, 20) + "...");
14296
+ console.log(" Source:", sourceLabel);
14297
+ console.log(" Projects:", projects2.length);
14298
+ if (projects2.length > 0) {
14299
+ console.log("\n Your projects:");
14300
+ for (const p of projects2.slice(0, 5)) {
14301
+ console.log(` - ${p.name} (${p.slug})`);
14302
+ }
14303
+ if (projects2.length > 5) {
14304
+ console.log(` ... and ${projects2.length - 5} more`);
14305
+ }
14306
+ }
14307
+ console.log("");
14308
+ } catch (err) {
14309
+ console.log("API Key:", resolved.key.substring(0, 15) + "...");
14310
+ console.error("Error verifying API key:", err instanceof Error ? err.message : "Unknown error");
14311
+ }
14312
+ });
14313
+ program.command("upload").description("Upload translation keys to IntlPull (auto-detects project and files)").option("--project <id>", "Project ID (auto-detected with project-scoped API key)").option("--file <path>", "Specific file to upload (e.g., messages/admin.json)").option("--branch <name>", "Branch name (auto-detected from git, pushes to IntlPull branch)").option("--platform <platform>", "Platform to tag keys with (ios, android, web)").option("--all-languages", "Upload translations for ALL languages (default: true)", true).option("--source-only", "Upload only source language keys", false).option("--dry-run", "Preview without uploading", false).option("-v, --verbose", "Show detailed detection info", false).option("-q, --quiet", "Suppress output except errors (CI mode)").option("--translation-key-prefix <prefix>", 'Prepend prefix to all keys (e.g., "checkout" makes "submit" \u2192 "checkout.submit")').option("--apply-tm", "Apply Translation Memory matches after pushing", false).option("--tm-threshold <n>", "Minimum TM match score (default: 85)", "85").option("--tm-dry-run", "Preview TM matches without applying", false).action((options) => {
14314
+ runUpload({
14315
+ project: options.project,
14316
+ file: options.file,
14317
+ branch: options.branch,
14318
+ platform: options.platform,
14319
+ // --source-only overrides --all-languages
14320
+ allLanguages: options.sourceOnly ? false : options.allLanguages,
14321
+ dryRun: options.dryRun,
14322
+ verbose: options.verbose,
14323
+ quiet: options.quiet,
14324
+ translationKeyPrefix: options.translationKeyPrefix,
14325
+ applyTm: options.applyTm,
14326
+ tmThreshold: options.tmThreshold,
14327
+ tmDryRun: options.tmDryRun
14328
+ });
14329
+ });
14330
+ program.command("download").description("Download translations from IntlPull (auto-detects project, framework, and languages)").option("--project <id>", "Project ID (auto-detected with project-scoped API key)").option("--format <format>", "Output format (json|yaml|ts)", "json").option("--output <dir>", "Output directory (auto-detected from framework)").option("--languages <langs>", "Languages to download (comma-separated, defaults to all)").option("--namespace <namespaces>", "Namespaces to download (comma-separated, defaults to all)").option("--branch <branch>", "Translation branch to download from").option("--platform <platform>", "Platform variant (default|ios|android|web)").option("--no-parallel", "Disable parallel fetching (use single request)").option("-i, --interactive", "Run in interactive mode with prompts").option("-q, --quiet", "Suppress output except errors").option("--clean-orphans [mode]", 'Remove orphaned files (files not in remote). Use "dry-run" to preview').option("-f, --force", "Skip confirmation prompts for destructive operations").option("--translation-key-prefix <prefix>", 'Strip this prefix from keys (e.g., "checkout" makes "checkout.submit" \u2192 "submit")').option("--filter-by-prefix <prefix>", "Only download keys starting with this prefix").action((options) => {
14331
+ let cleanOrphans;
14332
+ if (options.cleanOrphans === true || options.cleanOrphans === "") {
14333
+ cleanOrphans = true;
14334
+ } else if (options.cleanOrphans === "dry-run") {
11403
14335
  cleanOrphans = "dry-run";
11404
14336
  }
11405
- runPull({
14337
+ runDownload({
11406
14338
  project: options.project,
11407
14339
  format: options.format,
11408
14340
  output: options.output,
11409
14341
  languages: options.languages,
14342
+ namespaces: options.namespace,
11410
14343
  branch: options.branch,
11411
14344
  platform: options.platform,
11412
14345
  parallel: options.parallel,
11413
14346
  interactive: options.interactive,
11414
14347
  quiet: options.quiet,
11415
14348
  cleanOrphans,
11416
- force: options.force
14349
+ force: options.force,
14350
+ translationKeyPrefix: options.translationKeyPrefix,
14351
+ filterByPrefix: options.filterByPrefix
11417
14352
  });
11418
14353
  });
11419
14354
  program.command("listen").alias("watch").description("Watch for translation updates and auto-sync to local files").option("--project <id>", "Project ID").option("--output <dir>", "Output directory (auto-detected from .intlpull.json)").option("--format <format>", "Output format (json|yaml|ts)", "json").option("--languages <langs>", "Languages to sync (comma-separated)").option("--branch <branch>", "Translation branch").option("--platform <platform>", "Platform variant").option("--interval <seconds>", "Polling interval in seconds", "5").option("--timeout <seconds>", "Auto-stop after N seconds (default: 300 = 5 min)").option("--once", "Sync once and exit (no watching)").option("--quiet", "Minimal output").option("--no-parallel", "Disable parallel fetching (use single request)").action((options) => {
@@ -11461,7 +14396,7 @@ program.command("releases [action] [releaseId]").description("Manage Over-The-Ai
11461
14396
  quiet: options.quiet
11462
14397
  });
11463
14398
  });
11464
- program.command("export").description("Export translations as ZIP").option("--format <format>", "Export format (json|yaml|ts|android|ios)", "json").option("--languages <langs>", "Languages to export (comma-separated)").option("--output <file>", "Output file path").option("--version <version>", "Version to export").option("--branch <branch>", "Translation branch").option("--platform <platform>", "Platform variant").option("--project <id>", "Project ID").action((options) => {
14399
+ program.command("export").description("Export translations in various formats").option("--format <format>", "Export format (json|yaml|ts|android|ios|xliff|po|arb|stringsdict|xcstrings|csv)", "json").option("--languages <langs>", "Languages to export (comma-separated)").option("--output <file>", "Output file path").option("--version <version>", "Version to export").option("--branch <branch>", "Translation branch").option("--platform <platform>", "Platform variant").option("--project <id>", "Project ID").action((options) => {
11465
14400
  runExport({
11466
14401
  format: options.format,
11467
14402
  languages: options.languages,
@@ -11472,7 +14407,7 @@ program.command("export").description("Export translations as ZIP").option("--fo
11472
14407
  project: options.project
11473
14408
  });
11474
14409
  });
11475
- program.command("import").description("Import existing translation files").option("--path <path>", "Path to file or directory to import").option("--pattern <pattern>", 'Glob pattern for multi-file import (e.g., "*.json")').option("--format <format>", "File format (json|yaml)", "json").option("--project <id>", "Project ID").option("--language <code>", "Target language code (auto-detect if omitted)").option("--namespace <name>", "Target namespace", "common").option("--branch <name>", "Branch to import translations to").option("--platform <platform>", "Platform to tag imported keys with (ios, android, web)").option("--dry-run", "Preview without importing", false).option("--update-existing", "Update existing translations", true).option("--skip-existing", "Skip if key already exists", false).option("--detect-icu-plurals", "Detect ICU plural syntax", true).action((options) => {
14410
+ program.command("import").description("Import existing translation files").option("--path <path>", "Path to file or directory to import").option("--pattern <pattern>", 'Glob pattern for multi-file import (e.g., "*.json")').option("--format <format>", "File format (json|yaml|xliff|po|arb|stringsdict|xcstrings|csv)", "json").option("--project <id>", "Project ID").option("--language <code>", "Target language code (auto-detect if omitted)").option("--namespace <name>", "Target namespace", "common").option("--branch <name>", "Branch to import translations to").option("--platform <platform>", "Platform to tag imported keys with (ios, android, web)").option("--dry-run", "Preview without importing", false).option("--update-existing", "Update existing translations", true).option("--skip-existing", "Skip if key already exists", false).option("--detect-icu-plurals", "Detect ICU plural syntax", true).action((options) => {
11476
14411
  runImport({
11477
14412
  path: options.path,
11478
14413
  pattern: options.pattern,
@@ -11526,7 +14461,7 @@ program.command("fix").description("Auto-fix missing translations").option("--so
11526
14461
  dryRun: options.dryRun
11527
14462
  });
11528
14463
  });
11529
- program.command("diff").description("Show what would change on push/pull").option("--source <lang>", "Source language").option("--target <lang>", "Target language to compare").action((options) => {
14464
+ program.command("diff").description("Show what would change on upload/download").option("--source <lang>", "Source language").option("--target <lang>", "Target language to compare").action((options) => {
11530
14465
  runDiff({
11531
14466
  source: options.source,
11532
14467
  target: options.target
@@ -11534,12 +14469,12 @@ program.command("diff").description("Show what would change on push/pull").optio
11534
14469
  });
11535
14470
  var projects = program.command("projects").description("Manage projects");
11536
14471
  projects.command("list").description("List all projects").action(async () => {
11537
- const { isAuthenticated } = await import("./config-DX34FMWV.js");
14472
+ const { isAuthenticated } = await import("./config-F3O3XWYX.js");
11538
14473
  if (!isAuthenticated()) {
11539
14474
  console.log("Not authenticated. Run `npx @intlpullhq/cli login` first.");
11540
14475
  return;
11541
14476
  }
11542
- const { createApiClient: createApiClient2 } = await import("./api-K3AY4VAI.js");
14477
+ const { createApiClient: createApiClient2 } = await import("./api-77ZWU4IB.js");
11543
14478
  try {
11544
14479
  const api = createApiClient2();
11545
14480
  const { projects: projects2 } = await api.listProjects();
@@ -11560,12 +14495,12 @@ projects.command("list").description("List all projects").action(async () => {
11560
14495
  }
11561
14496
  });
11562
14497
  projects.command("create <name>").description("Create a new project").option("--source-language <lang>", "Source language", "en").option("--languages <langs>", "Supported languages (comma-separated)", "en").action(async (name, options) => {
11563
- const { isAuthenticated } = await import("./config-DX34FMWV.js");
14498
+ const { isAuthenticated } = await import("./config-F3O3XWYX.js");
11564
14499
  if (!isAuthenticated()) {
11565
14500
  console.log("Not authenticated. Run `npx @intlpullhq/cli login` first.");
11566
14501
  return;
11567
14502
  }
11568
- const { createApiClient: createApiClient2 } = await import("./api-K3AY4VAI.js");
14503
+ const { createApiClient: createApiClient2 } = await import("./api-77ZWU4IB.js");
11569
14504
  try {
11570
14505
  const api = createApiClient2();
11571
14506
  const languages = options.languages.split(",").map((l) => l.trim());
@@ -11584,7 +14519,7 @@ projects.command("create <name>").description("Create a new project").option("--
11584
14519
  }
11585
14520
  });
11586
14521
  projects.command("link <id>").description("Link current directory to a project").action(async (id) => {
11587
- const { getProjectConfig: getProjectConfig2, saveProjectConfig: saveProjectConfig3, detectFramework: detectFramework2 } = await import("./config-DX34FMWV.js");
14522
+ const { getProjectConfig: getProjectConfig2, saveProjectConfig: saveProjectConfig3, detectFramework: detectFramework2 } = await import("./config-F3O3XWYX.js");
11588
14523
  const existing = getProjectConfig2();
11589
14524
  const detected = detectFramework2();
11590
14525
  const config = {
@@ -11666,4 +14601,18 @@ zendesk.command("disconnect").description("Disconnect Zendesk integration").acti
11666
14601
  runZendeskDisconnect();
11667
14602
  });
11668
14603
  program.addCommand(documentsCommand);
14604
+ program.addCommand(keysCommand);
14605
+ program.addCommand(tmCommand);
14606
+ program.addCommand(snapshotsCommand);
14607
+ program.addCommand(webhooksCommand);
14608
+ program.addCommand(contributorsCommand);
14609
+ program.command("cleanup").description("Detect and delete orphaned translation keys").option("--project <id>", "Project ID (auto-detected with project-scoped API key)").option("--scan-codebase", "Scan local files for key usage patterns", false).option("--dry-run", "Preview orphans without deleting", false).option("--days <n>", "Only consider keys not updated in N days", parseInt).option("-f, --force", "Skip confirmation prompt", false).action((options) => {
14610
+ runCleanup({
14611
+ project: options.project,
14612
+ scanCodebase: options.scanCodebase,
14613
+ dryRun: options.dryRun,
14614
+ days: options.days,
14615
+ force: options.force
14616
+ });
14617
+ });
11669
14618
  program.parse();