@vocoder/cli 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -11,8 +11,9 @@ import {
11
11
  getSetupSnippets,
12
12
  loadVocoderConfig,
13
13
  readAuthData,
14
+ verifyStoredAuth,
14
15
  writeAuthData
15
- } from "./chunk-XF3KGGYQ.mjs";
16
+ } from "./chunk-XUCVAFBG.mjs";
16
17
 
17
18
  // src/bin.ts
18
19
  import { Command } from "commander";
@@ -1447,17 +1448,6 @@ function printCodeBlock(code) {
1447
1448
  `
1448
1449
  );
1449
1450
  }
1450
- async function verifyStoredToken(api, token) {
1451
- try {
1452
- return await api.getCliUserInfo(token);
1453
- } catch (err) {
1454
- clearAuthData();
1455
- if (err instanceof VocoderAPIError && err.status === 404) {
1456
- return { userGone: true };
1457
- }
1458
- return null;
1459
- }
1460
- }
1461
1451
  async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1462
1452
  let server = null;
1463
1453
  if (!options.ci) {
@@ -1638,17 +1628,17 @@ async function init(options = {}) {
1638
1628
  true
1639
1629
  );
1640
1630
  if (!authResult) return 1;
1641
- const spinner4 = p5.spinner();
1642
- spinner4.start("Generating new API key...");
1631
+ const spinner7 = p5.spinner();
1632
+ spinner7.start("Generating new API key...");
1643
1633
  try {
1644
1634
  const { apiKey } = await anonApi2.regenerateProjectApiKey(
1645
1635
  authResult.token,
1646
1636
  exactMatch.projectId
1647
1637
  );
1648
- spinner4.stop("New API key generated");
1638
+ spinner7.stop("New API key generated");
1649
1639
  printApiKey(apiKey);
1650
1640
  } catch (err) {
1651
- spinner4.stop("Failed to generate key");
1641
+ spinner7.stop("Failed to generate key");
1652
1642
  const msg = err instanceof Error ? err.message : String(err);
1653
1643
  p5.log.error(`Could not generate API key: ${msg}`);
1654
1644
  p5.log.info("Try again or generate one from the dashboard.");
@@ -1683,46 +1673,23 @@ async function init(options = {}) {
1683
1673
  let userEmail;
1684
1674
  let userName;
1685
1675
  let authOrganizationId;
1686
- const stored = readAuthData();
1687
- if (stored) {
1688
- const verified = await verifyStoredToken(api, stored.token);
1689
- if (verified && !("userGone" in verified)) {
1690
- p5.log.success(`Authenticated as ${chalk6.bold(verified.email)}`);
1691
- userToken = stored.token;
1692
- userEmail = verified.email;
1693
- userName = verified.name;
1694
- } else {
1695
- const isFirstTime = verified !== null && "userGone" in verified;
1696
- if (isFirstTime) {
1697
- p5.log.warn("Account not found \u2014 starting fresh setup");
1698
- } else {
1699
- p5.log.warn("Stored credentials expired \u2014 signing in again");
1700
- }
1701
- const authResult = await runAuthFlow(
1702
- api,
1703
- options,
1704
- /* reauth */
1705
- !isFirstTime,
1706
- identity?.repoCanonical
1707
- );
1708
- if (!authResult) return 1;
1709
- userToken = authResult.token;
1710
- userEmail = authResult.email;
1711
- userName = authResult.name;
1712
- authOrganizationId = authResult.organizationId;
1713
- writeAuthData({
1714
- token: userToken,
1715
- userId: authResult.userId,
1716
- email: userEmail,
1717
- name: userName,
1718
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1719
- });
1720
- }
1676
+ const storedAuth = await verifyStoredAuth(api);
1677
+ if (storedAuth.status === "valid") {
1678
+ p5.log.success(`Authenticated as ${chalk6.bold(storedAuth.email)}`);
1679
+ userToken = storedAuth.token;
1680
+ userEmail = storedAuth.email;
1681
+ userName = storedAuth.name;
1721
1682
  } else {
1683
+ const reauth = storedAuth.status === "expired";
1684
+ if (reauth) {
1685
+ p5.log.warn("Stored credentials expired \u2014 signing in again");
1686
+ } else if (storedAuth.status === "gone") {
1687
+ p5.log.warn("Account not found \u2014 starting fresh setup");
1688
+ }
1722
1689
  const authResult = await runAuthFlow(
1723
1690
  api,
1724
1691
  options,
1725
- false,
1692
+ reauth,
1726
1693
  identity?.repoCanonical
1727
1694
  );
1728
1695
  if (!authResult) return 1;
@@ -2142,30 +2109,16 @@ Translations won't run automatically until you grant access.
2142
2109
  }
2143
2110
  }
2144
2111
 
2145
- // src/commands/logout.ts
2146
- import * as p6 from "@clack/prompts";
2147
- async function logout(options = {}) {
2148
- const stored = readAuthData();
2149
- if (!stored) {
2150
- p6.log.info("Not currently authenticated.");
2151
- return 0;
2152
- }
2153
- const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
2154
- const api = new VocoderAPI({ apiUrl, apiKey: "" });
2155
- try {
2156
- await api.revokeCliToken(stored.token);
2157
- } catch {
2158
- }
2159
- clearAuthData();
2160
- p6.log.success(`Logged out (was ${stored.email})`);
2161
- return 0;
2162
- }
2112
+ // src/commands/locales.ts
2113
+ import * as p8 from "@clack/prompts";
2114
+ import chalk9 from "chalk";
2115
+ import { config as loadEnv3 } from "dotenv";
2163
2116
 
2164
2117
  // src/commands/sync.ts
2165
2118
  import { createHash, randomUUID } from "crypto";
2166
2119
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
2167
2120
  import { join as join3 } from "path";
2168
- import * as p8 from "@clack/prompts";
2121
+ import * as p7 from "@clack/prompts";
2169
2122
  import chalk8 from "chalk";
2170
2123
 
2171
2124
  // src/utils/branch.ts
@@ -2235,7 +2188,7 @@ function matchBranchPattern(branch, pattern) {
2235
2188
  }
2236
2189
 
2237
2190
  // src/utils/config.ts
2238
- import * as p7 from "@clack/prompts";
2191
+ import * as p6 from "@clack/prompts";
2239
2192
  import chalk7 from "chalk";
2240
2193
  import { config as loadEnv2 } from "dotenv";
2241
2194
  loadEnv2();
@@ -2294,7 +2247,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2294
2247
  };
2295
2248
  const fileConfig = loadVocoderConfig(process.cwd());
2296
2249
  if (!fileConfig) {
2297
- p7.log.warn(
2250
+ p6.log.warn(
2298
2251
  `No ${chalk7.cyan("vocoder.config.ts")} found \u2014 run ${chalk7.cyan("npx @vocoder/cli init")} to generate one.`
2299
2252
  );
2300
2253
  }
@@ -2325,7 +2278,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2325
2278
  excludePattern = fileConfig.exclude;
2326
2279
  configSources.excludePattern = "vocoder.config";
2327
2280
  } else if (envExcludePattern) {
2328
- excludePattern = envExcludePattern.split(",").map((p10) => p10.trim()).filter(Boolean);
2281
+ excludePattern = envExcludePattern.split(",").map((p14) => p14.trim()).filter(Boolean);
2329
2282
  configSources.excludePattern = "environment";
2330
2283
  } else {
2331
2284
  excludePattern = defaults.excludePattern;
@@ -2382,7 +2335,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2382
2335
  ...maxWaitMs ? [`Max wait: ${chalk7.cyan(String(configSources.maxWaitMs))}`] : [],
2383
2336
  `No fallback: ${chalk7.cyan(String(configSources.noFallback))}`
2384
2337
  ];
2385
- p7.note(lines.join("\n"), "Configuration sources");
2338
+ p6.note(lines.join("\n"), "Configuration sources");
2386
2339
  }
2387
2340
  return {
2388
2341
  includePattern,
@@ -2560,6 +2513,13 @@ function getLimitErrorGuidance(limitError) {
2560
2513
  `Upgrade plan: ${limitError.upgradeUrl}`
2561
2514
  ];
2562
2515
  }
2516
+ if (limitError.limitType === "target_locales") {
2517
+ return [
2518
+ `Current target locales: ${limitError.current}`,
2519
+ `Plan limit: ${limitError.current} (${limitError.planId})`,
2520
+ `Upgrade plan: ${limitError.upgradeUrl}`
2521
+ ];
2522
+ }
2563
2523
  return [
2564
2524
  `Plan: ${limitError.planId}`,
2565
2525
  `Current: ${limitError.current}`,
@@ -2644,22 +2604,22 @@ async function fetchApiSnapshot(api, params) {
2644
2604
  async function sync(options = {}) {
2645
2605
  const startTime = Date.now();
2646
2606
  const projectRoot = process.cwd();
2647
- p8.intro("Vocoder Sync");
2607
+ p7.intro("Vocoder Sync");
2648
2608
  const mergedConfig = await getMergedConfig(options, options.verbose);
2649
2609
  if (!mergedConfig.apiKey) {
2650
- p8.log.warn("No API key found. Run init to get started:");
2651
- p8.log.info(" npx @vocoder/cli init");
2652
- p8.log.info("");
2653
- p8.log.info(
2610
+ p7.log.warn("No API key found. Run init to get started:");
2611
+ p7.log.info(" npx @vocoder/cli init");
2612
+ p7.log.info("");
2613
+ p7.log.info(
2654
2614
  " Or add your key to .env: VOCODER_API_KEY=vcp_..."
2655
2615
  );
2656
- p8.outro("Run `npx @vocoder/cli init` to set up your project.");
2616
+ p7.outro("Run `npx @vocoder/cli init` to set up your project.");
2657
2617
  return 1;
2658
2618
  }
2659
- const spinner4 = p8.spinner();
2619
+ const spinner7 = p7.spinner();
2660
2620
  try {
2661
2621
  const branch = detectBranch(options.branch);
2662
- spinner4.start("Loading project configuration");
2622
+ spinner7.start("Loading project configuration");
2663
2623
  const localConfig = {
2664
2624
  apiKey: mergedConfig.apiKey,
2665
2625
  apiUrl: mergedConfig.apiUrl || "https://vocoder.app"
@@ -2683,18 +2643,18 @@ async function sync(options = {}) {
2683
2643
  ...fileConfig?.appIndustry ? { appIndustry: fileConfig.appIndustry } : {},
2684
2644
  ...fileConfig?.formality ? { formality: fileConfig.formality } : {}
2685
2645
  };
2686
- spinner4.stop(`Branch: ${chalk8.cyan(branch)}`);
2646
+ spinner7.stop(`Branch: ${chalk8.cyan(branch)}`);
2687
2647
  if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
2688
- p8.log.warn(
2648
+ p7.log.warn(
2689
2649
  `Skipping translations (${chalk8.cyan(branch)} is not a target branch)`
2690
2650
  );
2691
- p8.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
2692
- p8.log.info("Use --force to translate anyway");
2693
- p8.outro("");
2651
+ p7.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
2652
+ p7.log.info("Use --force to translate anyway");
2653
+ p7.outro("");
2694
2654
  return 0;
2695
2655
  }
2696
2656
  const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
2697
- spinner4.start(`Extracting strings from ${patternsDisplay}`);
2657
+ spinner7.start(`Extracting strings from ${patternsDisplay}`);
2698
2658
  const extractor = new StringExtractor();
2699
2659
  const extractedStrings = await extractor.extractFromProject(
2700
2660
  config.includePattern,
@@ -2702,14 +2662,14 @@ async function sync(options = {}) {
2702
2662
  config.excludePattern
2703
2663
  );
2704
2664
  if (extractedStrings.length === 0) {
2705
- spinner4.stop("No translatable strings found");
2706
- p8.log.warn(
2665
+ spinner7.stop("No translatable strings found");
2666
+ p7.log.warn(
2707
2667
  "Make sure you are wrapping translatable strings with Vocoder"
2708
2668
  );
2709
- p8.outro("");
2669
+ p7.outro("");
2710
2670
  return 0;
2711
2671
  }
2712
- spinner4.stop(
2672
+ spinner7.stop(
2713
2673
  `Extracted ${chalk8.cyan(extractedStrings.length)} strings from ${chalk8.cyan(patternsDisplay)}`
2714
2674
  );
2715
2675
  if (options.verbose) {
@@ -2717,10 +2677,10 @@ async function sync(options = {}) {
2717
2677
  if (extractedStrings.length > 5) {
2718
2678
  sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
2719
2679
  }
2720
- p8.note(sampleLines.join("\n"), "Sample strings");
2680
+ p7.note(sampleLines.join("\n"), "Sample strings");
2721
2681
  }
2722
2682
  if (options.dryRun) {
2723
- p8.note(
2683
+ p7.note(
2724
2684
  [
2725
2685
  `Strings: ${extractedStrings.length}`,
2726
2686
  `Branch: ${branch}`,
@@ -2731,12 +2691,12 @@ async function sync(options = {}) {
2731
2691
  ].join("\n"),
2732
2692
  "Dry run - would translate"
2733
2693
  );
2734
- p8.outro("No API calls made.");
2694
+ p7.outro("No API calls made.");
2735
2695
  return 0;
2736
2696
  }
2737
2697
  const repoIdentity = resolveGitRepositoryIdentity();
2738
2698
  if (!repoIdentity && options.verbose) {
2739
- p8.log.warn(
2699
+ p7.log.warn(
2740
2700
  "Could not detect git remote origin. Sync will continue without repo metadata."
2741
2701
  );
2742
2702
  }
@@ -2744,7 +2704,7 @@ async function sync(options = {}) {
2744
2704
  const stringEntries = buildStringEntries(extractedStrings);
2745
2705
  const sourceStrings = stringEntries.map((entry) => entry.text);
2746
2706
  if (options.verbose && stringEntries.length !== extractedStrings.length) {
2747
- p8.log.info(
2707
+ p7.log.info(
2748
2708
  `Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
2749
2709
  );
2750
2710
  }
@@ -2753,17 +2713,17 @@ async function sync(options = {}) {
2753
2713
  const cacheFile = getCacheFilePath(projectRoot, fingerprint);
2754
2714
  if (existsSync3(cacheFile)) {
2755
2715
  if (options.verbose) {
2756
- p8.log.info(`Cache hit: ${chalk8.dim(cacheFile)} (fingerprint ${chalk8.cyan(fingerprint)})`);
2716
+ p7.log.info(`Cache hit: ${chalk8.dim(cacheFile)} (fingerprint ${chalk8.cyan(fingerprint)})`);
2757
2717
  }
2758
2718
  const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
2759
- p8.outro(`Up to date (${duration2}s)`);
2719
+ p7.outro(`Up to date (${duration2}s)`);
2760
2720
  return 0;
2761
2721
  }
2762
2722
  if (options.verbose) {
2763
- p8.log.info(`No cache for fingerprint ${chalk8.cyan(fingerprint)} \u2014 will submit to API`);
2723
+ p7.log.info(`No cache for fingerprint ${chalk8.cyan(fingerprint)} \u2014 will submit to API`);
2764
2724
  }
2765
2725
  }
2766
- spinner4.start("Submitting strings to Vocoder API");
2726
+ spinner7.start("Submitting strings to Vocoder API");
2767
2727
  const batchResponse = await api.submitTranslation(
2768
2728
  branch,
2769
2729
  stringEntries,
@@ -2778,33 +2738,33 @@ async function sync(options = {}) {
2778
2738
  },
2779
2739
  repoIdentity ? { ...repoIdentity, commitSha } : { commitSha }
2780
2740
  );
2781
- spinner4.stop("Strings submitted");
2741
+ spinner7.stop("Strings submitted");
2782
2742
  const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
2783
2743
  branch,
2784
2744
  requestedMode,
2785
2745
  policy: config.syncPolicy
2786
2746
  });
2787
2747
  if (options.verbose) {
2788
- p8.log.info(`Batch: ${chalk8.dim(batchResponse.batchId)}`);
2789
- p8.log.info(`Requested mode: ${requestedMode}`);
2790
- p8.log.info(`Effective mode: ${effectiveMode}`);
2791
- p8.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
2748
+ p7.log.info(`Batch: ${chalk8.dim(batchResponse.batchId)}`);
2749
+ p7.log.info(`Requested mode: ${requestedMode}`);
2750
+ p7.log.info(`Effective mode: ${effectiveMode}`);
2751
+ p7.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
2792
2752
  if (batchResponse.queueStatus) {
2793
- p8.log.info(`Queue status: ${batchResponse.queueStatus}`);
2753
+ p7.log.info(`Queue status: ${batchResponse.queueStatus}`);
2794
2754
  }
2795
2755
  }
2796
2756
  if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
2797
- p8.log.success(`Up to date \u2014 ${chalk8.cyan(batchResponse.totalStrings)} strings, no changes`);
2757
+ p7.log.success(`Up to date \u2014 ${chalk8.cyan(batchResponse.totalStrings)} strings, no changes`);
2798
2758
  } else if (batchResponse.newStrings === 0) {
2799
2759
  const archivedNote = batchResponse.deletedStrings && batchResponse.deletedStrings > 0 ? `, ${chalk8.yellow(batchResponse.deletedStrings)} archived` : "";
2800
- p8.log.success(`No new strings \u2014 ${chalk8.cyan(batchResponse.totalStrings)} total${archivedNote}, using existing translations`);
2760
+ p7.log.success(`No new strings \u2014 ${chalk8.cyan(batchResponse.totalStrings)} total${archivedNote}, using existing translations`);
2801
2761
  } else {
2802
2762
  const statParts = [`${chalk8.cyan(batchResponse.newStrings)} new, ${chalk8.cyan(batchResponse.totalStrings)} total`];
2803
2763
  if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
2804
2764
  statParts.push(`${chalk8.yellow(batchResponse.deletedStrings)} archived`);
2805
2765
  }
2806
2766
  const estTime = batchResponse.estimatedTime ? ` (~${batchResponse.estimatedTime}s)` : "";
2807
- p8.log.info(`${statParts.join(", ")} \u2192 syncing to ${config.targetLocales.join(", ")}${estTime}`);
2767
+ p7.log.info(`${statParts.join(", ")} \u2192 syncing to ${config.targetLocales.join(", ")}${estTime}`);
2808
2768
  }
2809
2769
  let artifacts = null;
2810
2770
  if (batchResponse.translations) {
@@ -2816,7 +2776,7 @@ async function sync(options = {}) {
2816
2776
  let waitError = null;
2817
2777
  if (!artifacts && (effectiveMode === "required" || effectiveMode === "best-effort")) {
2818
2778
  const waitTimeoutSecs = Math.round(waitTimeoutMs / 1e3);
2819
- spinner4.start(`Waiting for translations (max ${waitTimeoutSecs}s)`);
2779
+ spinner7.start(`Waiting for translations (max ${waitTimeoutSecs}s)`);
2820
2780
  let lastProgress = 0;
2821
2781
  try {
2822
2782
  const completion = await api.waitForCompletion(
@@ -2825,7 +2785,7 @@ async function sync(options = {}) {
2825
2785
  (progress) => {
2826
2786
  const percent = Math.round(progress * 100);
2827
2787
  if (percent > lastProgress) {
2828
- spinner4.message(`Translating... ${percent}%`);
2788
+ spinner7.message(`Translating... ${percent}%`);
2829
2789
  lastProgress = percent;
2830
2790
  }
2831
2791
  }
@@ -2835,14 +2795,14 @@ async function sync(options = {}) {
2835
2795
  translations: completion.translations,
2836
2796
  localeMetadata: completion.localeMetadata
2837
2797
  };
2838
- spinner4.stop("Translations complete");
2798
+ spinner7.stop("Translations complete");
2839
2799
  } catch (error) {
2840
- spinner4.stop("Translation wait incomplete");
2800
+ spinner7.stop("Translation wait incomplete");
2841
2801
  waitError = error instanceof Error ? error : new Error(String(error));
2842
2802
  if (effectiveMode === "required") {
2843
2803
  throw waitError;
2844
2804
  }
2845
- p8.log.warn(`Best-effort wait ended early: ${waitError.message}`);
2805
+ p7.log.warn(`Best-effort wait ended early: ${waitError.message}`);
2846
2806
  }
2847
2807
  }
2848
2808
  if (!artifacts) {
@@ -2851,14 +2811,14 @@ async function sync(options = {}) {
2851
2811
  "Fresh translations are not available and fallback is disabled (--no-fallback)."
2852
2812
  );
2853
2813
  }
2854
- spinner4.start("Loading fallback translations");
2814
+ spinner7.start("Loading fallback translations");
2855
2815
  const localFallback = readLocalCache({
2856
2816
  projectRoot,
2857
2817
  fingerprint
2858
2818
  });
2859
2819
  if (localFallback) {
2860
2820
  artifacts = localFallback;
2861
- spinner4.stop(`Using local cached snapshot (${fingerprint})`);
2821
+ spinner7.stop(`Using local cached snapshot (${fingerprint})`);
2862
2822
  } else {
2863
2823
  try {
2864
2824
  const apiSnapshot = await fetchApiSnapshot(api, {
@@ -2867,15 +2827,15 @@ async function sync(options = {}) {
2867
2827
  });
2868
2828
  if (apiSnapshot) {
2869
2829
  artifacts = apiSnapshot;
2870
- spinner4.stop("Using latest completed API snapshot");
2830
+ spinner7.stop("Using latest completed API snapshot");
2871
2831
  } else {
2872
- spinner4.stop("No completed API snapshot available");
2832
+ spinner7.stop("No completed API snapshot available");
2873
2833
  }
2874
2834
  } catch (error) {
2875
- spinner4.stop("Failed to fetch API snapshot");
2835
+ spinner7.stop("Failed to fetch API snapshot");
2876
2836
  if (options.verbose) {
2877
2837
  const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
2878
- p8.log.warn(`Snapshot fetch error: ${message}`);
2838
+ p7.log.warn(`Snapshot fetch error: ${message}`);
2879
2839
  }
2880
2840
  }
2881
2841
  }
@@ -2907,85 +2867,433 @@ async function sync(options = {}) {
2907
2867
  });
2908
2868
  const cachePath = writeCache({ projectRoot, fingerprint, data });
2909
2869
  if (options.verbose) {
2910
- p8.log.info(`Cache written: ${cachePath}`);
2870
+ p7.log.info(`Cache written: ${cachePath}`);
2911
2871
  }
2912
2872
  } catch (error) {
2913
2873
  if (options.verbose) {
2914
2874
  const message = error instanceof Error ? error.message : "Unknown cache write error";
2915
- p8.log.warn(`Failed to write cache: ${message}`);
2875
+ p7.log.warn(`Failed to write cache: ${message}`);
2916
2876
  }
2917
2877
  }
2918
2878
  if (artifacts.source !== "fresh") {
2919
2879
  const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
2920
- p8.log.warn(
2880
+ p7.log.warn(
2921
2881
  `Using ${sourceLabel}. New strings may appear after the background sync completes.`
2922
2882
  );
2923
2883
  }
2924
2884
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
2925
- p8.outro(`Sync complete! (${duration}s)`);
2885
+ p7.outro(`Sync complete! (${duration}s)`);
2926
2886
  return 0;
2927
2887
  } catch (error) {
2928
- spinner4.stop();
2888
+ spinner7.stop();
2929
2889
  if (error instanceof VocoderAPIError && error.syncPolicyError) {
2930
- p8.log.error(error.syncPolicyError.message);
2890
+ p7.log.error(error.syncPolicyError.message);
2931
2891
  const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
2932
2892
  for (const line of guidance) {
2933
- p8.log.info(line);
2893
+ p7.log.info(line);
2934
2894
  }
2935
2895
  return 1;
2936
2896
  }
2937
2897
  if (error instanceof VocoderAPIError && error.limitError) {
2938
2898
  const { limitError } = error;
2939
- p8.log.error(limitError.message);
2899
+ p7.log.error(limitError.message);
2940
2900
  const guidance = getLimitErrorGuidance(limitError);
2941
2901
  for (const line of guidance) {
2942
- p8.log.info(line);
2902
+ p7.log.info(line);
2943
2903
  }
2944
2904
  return 1;
2945
2905
  }
2946
2906
  if (error instanceof Error) {
2947
- p8.log.error(error.message);
2907
+ p7.log.error(error.message);
2948
2908
  const isInvalidKey = error.message.toLowerCase().includes("invalid api key") || error instanceof VocoderAPIError && error.status === 401;
2949
2909
  if (isInvalidKey) {
2950
- p8.log.warn(
2910
+ p7.log.warn(
2951
2911
  "API key rejected \u2014 the project may have been deleted or the key revoked."
2952
2912
  );
2953
- p8.log.info(
2913
+ p7.log.info(
2954
2914
  " Run `npx @vocoder/cli init` to create a new project and key."
2955
2915
  );
2956
2916
  } else if (error.message.includes("git branch")) {
2957
- p8.log.warn("Run from a git repository, or use:");
2958
- p8.log.info(" vocoder sync --branch main");
2917
+ p7.log.warn("Run from a git repository, or use:");
2918
+ p7.log.info(" vocoder sync --branch main");
2959
2919
  }
2960
2920
  if (options.verbose) {
2961
- p8.log.info(`Full error: ${error.stack ?? error}`);
2921
+ p7.log.info(`Full error: ${error.stack ?? error}`);
2962
2922
  }
2963
2923
  }
2964
2924
  return 1;
2965
2925
  }
2966
2926
  }
2967
2927
 
2968
- // src/commands/whoami.ts
2928
+ // src/commands/locales.ts
2929
+ loadEnv3();
2930
+ function getApiConfig(options) {
2931
+ const apiKey = process.env.VOCODER_API_KEY;
2932
+ if (!apiKey) {
2933
+ p8.log.error(
2934
+ "VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
2935
+ );
2936
+ return null;
2937
+ }
2938
+ return {
2939
+ apiKey,
2940
+ apiUrl: options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app"
2941
+ };
2942
+ }
2943
+ async function listProjectLocales(options = {}) {
2944
+ const config = getApiConfig(options);
2945
+ if (!config) return 1;
2946
+ const api = new VocoderAPI(config);
2947
+ try {
2948
+ const projectConfig2 = await api.getProjectConfig();
2949
+ p8.log.info(
2950
+ `Source locale: ${chalk9.cyan(projectConfig2.sourceLocale)}`
2951
+ );
2952
+ if (projectConfig2.targetLocales.length === 0) {
2953
+ p8.log.info("Target locales: (none configured)");
2954
+ } else {
2955
+ p8.log.info(
2956
+ `Target locales: ${projectConfig2.targetLocales.map((l) => chalk9.cyan(l)).join(", ")}`
2957
+ );
2958
+ }
2959
+ return 0;
2960
+ } catch (error) {
2961
+ p8.log.error(
2962
+ error instanceof Error ? error.message : "Failed to fetch project locales."
2963
+ );
2964
+ return 1;
2965
+ }
2966
+ }
2967
+ async function addLocales(locales, options = {}) {
2968
+ if (locales.length === 0) {
2969
+ p8.log.error("No locale codes provided.");
2970
+ return 1;
2971
+ }
2972
+ const config = getApiConfig(options);
2973
+ if (!config) return 1;
2974
+ const api = new VocoderAPI(config);
2975
+ let lastTargetLocales = [];
2976
+ let hadError = false;
2977
+ for (const locale of locales) {
2978
+ const spinner7 = p8.spinner();
2979
+ spinner7.start(`Adding ${locale}\u2026`);
2980
+ try {
2981
+ const result = await api.addLocale(locale);
2982
+ lastTargetLocales = result.targetLocales;
2983
+ spinner7.stop(`Added ${chalk9.cyan(locale)}`);
2984
+ } catch (error) {
2985
+ spinner7.stop(`Failed to add ${chalk9.red(locale)}`);
2986
+ hadError = true;
2987
+ if (error instanceof VocoderAPIError && error.limitError) {
2988
+ const { limitError } = error;
2989
+ p8.log.error(limitError.message);
2990
+ for (const line of getLimitErrorGuidance(limitError)) {
2991
+ p8.log.info(line);
2992
+ }
2993
+ break;
2994
+ }
2995
+ p8.log.error(
2996
+ error instanceof Error ? error.message : "Unknown error"
2997
+ );
2998
+ }
2999
+ }
3000
+ if (lastTargetLocales.length > 0) {
3001
+ p8.log.info(
3002
+ `Target locales now: ${lastTargetLocales.map((l) => chalk9.cyan(l)).join(", ")}`
3003
+ );
3004
+ }
3005
+ return hadError ? 1 : 0;
3006
+ }
3007
+ async function removeLocales(locales, options = {}) {
3008
+ if (locales.length === 0) {
3009
+ p8.log.error("No locale codes provided.");
3010
+ return 1;
3011
+ }
3012
+ const config = getApiConfig(options);
3013
+ if (!config) return 1;
3014
+ const api = new VocoderAPI(config);
3015
+ let lastTargetLocales = [];
3016
+ let hadError = false;
3017
+ for (const locale of locales) {
3018
+ const spinner7 = p8.spinner();
3019
+ spinner7.start(`Removing ${locale}\u2026`);
3020
+ try {
3021
+ const result = await api.removeLocale(locale);
3022
+ lastTargetLocales = result.targetLocales;
3023
+ spinner7.stop(`Removed ${chalk9.cyan(locale)}`);
3024
+ } catch (error) {
3025
+ spinner7.stop(`Failed to remove ${chalk9.red(locale)}`);
3026
+ hadError = true;
3027
+ p8.log.error(
3028
+ error instanceof Error ? error.message : "Unknown error"
3029
+ );
3030
+ }
3031
+ }
3032
+ if (lastTargetLocales.length > 0) {
3033
+ p8.log.info(
3034
+ `Target locales now: ${lastTargetLocales.map((l) => chalk9.cyan(l)).join(", ")}`
3035
+ );
3036
+ } else if (!hadError) {
3037
+ p8.log.info("Target locales now: (none configured)");
3038
+ }
3039
+ return hadError ? 1 : 0;
3040
+ }
3041
+ async function listSupportedLocales(options = {}) {
3042
+ const config = getApiConfig(options);
3043
+ if (!config) return 1;
3044
+ const api = new VocoderAPI(config);
3045
+ try {
3046
+ const result = await api.listLocales(config.apiKey);
3047
+ p8.log.info(chalk9.bold("Source locales:"));
3048
+ printLocaleTable(result.sourceLocales);
3049
+ p8.log.info("");
3050
+ p8.log.info(chalk9.bold("Target locales:"));
3051
+ printLocaleTable(result.targetLocales);
3052
+ return 0;
3053
+ } catch (error) {
3054
+ p8.log.error(
3055
+ error instanceof Error ? error.message : "Failed to fetch supported locales."
3056
+ );
3057
+ return 1;
3058
+ }
3059
+ }
3060
+ function printLocaleTable(locales) {
3061
+ for (const locale of locales) {
3062
+ const native = locale.nativeName && locale.nativeName !== locale.name ? ` (${locale.nativeName})` : "";
3063
+ p8.log.info(` ${chalk9.cyan(locale.code.padEnd(10))} ${locale.name}${native}`);
3064
+ }
3065
+ }
3066
+
3067
+ // src/commands/logout.ts
2969
3068
  import * as p9 from "@clack/prompts";
2970
- import chalk9 from "chalk";
3069
+ async function logout(options = {}) {
3070
+ const stored = readAuthData();
3071
+ if (!stored) {
3072
+ p9.log.info("Not currently authenticated.");
3073
+ return 0;
3074
+ }
3075
+ const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
3076
+ const api = new VocoderAPI({ apiUrl, apiKey: "" });
3077
+ try {
3078
+ await api.revokeCliToken(stored.token);
3079
+ } catch {
3080
+ }
3081
+ clearAuthData();
3082
+ p9.log.success(`Logged out (was ${stored.email})`);
3083
+ return 0;
3084
+ }
3085
+
3086
+ // src/commands/project-config.ts
3087
+ import * as p10 from "@clack/prompts";
3088
+ import chalk10 from "chalk";
3089
+ import { config as loadEnv4 } from "dotenv";
3090
+ loadEnv4();
3091
+ async function projectConfig(options = {}) {
3092
+ const apiKey = process.env.VOCODER_API_KEY;
3093
+ if (!apiKey) {
3094
+ p10.log.error(
3095
+ "VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
3096
+ );
3097
+ return 1;
3098
+ }
3099
+ const apiUrl = options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app";
3100
+ const api = new VocoderAPI({ apiKey, apiUrl });
3101
+ try {
3102
+ const config = await api.getProjectConfig();
3103
+ const lines = [
3104
+ `Project: ${chalk10.bold(config.projectName)}`,
3105
+ `Organization: ${config.organizationName}`,
3106
+ `Source locale: ${chalk10.cyan(config.sourceLocale)}`,
3107
+ `Target locales: ${config.targetLocales.length > 0 ? config.targetLocales.map((l) => chalk10.cyan(l)).join(", ") : chalk10.dim("(none)")}`,
3108
+ `Target branches: ${config.targetBranches.map((b) => chalk10.cyan(b)).join(", ")}`,
3109
+ ...config.primaryBranch ? [`Primary branch: ${chalk10.cyan(config.primaryBranch)}`] : [],
3110
+ `Sync policy:`,
3111
+ ` Blocking branches: ${config.syncPolicy.blockingBranches.map((b) => chalk10.cyan(b)).join(", ")}`,
3112
+ ` Blocking mode: ${chalk10.cyan(config.syncPolicy.blockingMode)}`,
3113
+ ` Non-blocking mode: ${chalk10.cyan(config.syncPolicy.nonBlockingMode)}`,
3114
+ ` Max wait: ${chalk10.cyan(String(config.syncPolicy.defaultMaxWaitMs))} ms`
3115
+ ];
3116
+ p10.note(lines.join("\n"), `${config.projectName} \u2014 project config`);
3117
+ return 0;
3118
+ } catch (error) {
3119
+ p10.log.error(
3120
+ error instanceof Error ? error.message : "Failed to fetch project config."
3121
+ );
3122
+ return 1;
3123
+ }
3124
+ }
3125
+
3126
+ // src/commands/translations.ts
3127
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
3128
+ import { join as join4 } from "path";
3129
+ import * as p11 from "@clack/prompts";
3130
+ import chalk11 from "chalk";
3131
+ import { config as loadEnv5 } from "dotenv";
3132
+ loadEnv5();
3133
+ async function getTranslations(options = {}) {
3134
+ const apiKey = process.env.VOCODER_API_KEY;
3135
+ if (!apiKey) {
3136
+ p11.log.error(
3137
+ "VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
3138
+ );
3139
+ return 1;
3140
+ }
3141
+ const apiUrl = options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app";
3142
+ const api = new VocoderAPI({ apiKey, apiUrl });
3143
+ let branch;
3144
+ try {
3145
+ branch = detectBranch(options.branch);
3146
+ } catch (error) {
3147
+ p11.log.error(
3148
+ error instanceof Error ? error.message : "Failed to detect branch."
3149
+ );
3150
+ return 1;
3151
+ }
3152
+ const spinner7 = p11.spinner();
3153
+ spinner7.start(`Fetching translations for ${chalk11.cyan(branch)}\u2026`);
3154
+ try {
3155
+ const projectConfig2 = await api.getProjectConfig();
3156
+ const targetLocales = options.locale ? [options.locale] : projectConfig2.targetLocales;
3157
+ if (targetLocales.length === 0) {
3158
+ spinner7.stop("No target locales configured.");
3159
+ p11.log.info("Add target locales with `vocoder locales add <code>`.");
3160
+ return 1;
3161
+ }
3162
+ const snapshot = await api.getTranslationSnapshot({ branch, targetLocales });
3163
+ spinner7.stop(`Fetched translations for ${chalk11.cyan(branch)}`);
3164
+ if (snapshot.status === "NOT_FOUND") {
3165
+ p11.log.warn(
3166
+ `No translation snapshot found for branch "${branch}". Run \`vocoder sync\` to generate one.`
3167
+ );
3168
+ return 1;
3169
+ }
3170
+ const translations = snapshot.translations ?? {};
3171
+ if (options.output) {
3172
+ writeLocaleFiles(translations, options.output);
3173
+ } else {
3174
+ process.stdout.write(JSON.stringify(translations, null, 2));
3175
+ process.stdout.write("\n");
3176
+ }
3177
+ return 0;
3178
+ } catch (error) {
3179
+ spinner7.stop("Failed to fetch translations.");
3180
+ p11.log.error(
3181
+ error instanceof Error ? error.message : "Unknown error."
3182
+ );
3183
+ return 1;
3184
+ }
3185
+ }
3186
+ function writeLocaleFiles(translations, outputDir) {
3187
+ mkdirSync2(outputDir, { recursive: true });
3188
+ for (const [locale, strings] of Object.entries(translations)) {
3189
+ const filePath = join4(outputDir, `${locale}.json`);
3190
+ writeFileSync4(filePath, JSON.stringify(strings, null, 2) + "\n", "utf-8");
3191
+ p11.log.success(`Wrote ${chalk11.cyan(filePath)}`);
3192
+ }
3193
+ }
3194
+
3195
+ // src/commands/create-project.ts
3196
+ import * as p12 from "@clack/prompts";
3197
+ import chalk12 from "chalk";
3198
+ import { config as loadEnv6 } from "dotenv";
3199
+ loadEnv6();
3200
+ async function createProject(options) {
3201
+ const authData = readAuthData();
3202
+ if (!authData) {
3203
+ p12.log.error(
3204
+ "Not logged in. Run `npx @vocoder/cli init` to authenticate first."
3205
+ );
3206
+ return 1;
3207
+ }
3208
+ const apiUrl = options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app";
3209
+ const api = new VocoderAPI({ apiKey: "", apiUrl });
3210
+ let repoCanonical;
3211
+ let appDir = options.appDir ?? ".";
3212
+ if (options.repo) {
3213
+ repoCanonical = options.repo;
3214
+ } else {
3215
+ const identity = resolveGitRepositoryIdentity();
3216
+ if (identity) {
3217
+ repoCanonical = identity.repoCanonical;
3218
+ if (!options.appDir && identity.repoAppDir) {
3219
+ appDir = identity.repoAppDir;
3220
+ }
3221
+ } else {
3222
+ p12.log.warn(
3223
+ "Could not detect a git remote. The project will be created without repo binding \u2014 sync-on-push will not function until a repository is connected via the Vocoder dashboard."
3224
+ );
3225
+ }
3226
+ }
3227
+ const targetLocales = options.targetLocales ? options.targetLocales.split(",").map((l) => l.trim()).filter(Boolean) : [];
3228
+ const targetBranches = options.targetBranches ? options.targetBranches.split(",").map((b) => b.trim()).filter(Boolean) : ["main"];
3229
+ const spinner7 = p12.spinner();
3230
+ spinner7.start(`Creating project "${options.name}"\u2026`);
3231
+ try {
3232
+ const result = await api.createProject(authData.token, {
3233
+ organizationId: options.workspace,
3234
+ name: options.name,
3235
+ sourceLocale: options.sourceLocale,
3236
+ targetLocales,
3237
+ targetBranches,
3238
+ appDirs: [appDir],
3239
+ ...repoCanonical ? { repoCanonical } : {}
3240
+ });
3241
+ spinner7.stop(`Created project ${chalk12.bold(result.projectName)}`);
3242
+ const lines = [
3243
+ `Project ID: ${result.projectId}`,
3244
+ `Source locale: ${chalk12.cyan(result.sourceLocale)}`,
3245
+ `Target locales: ${result.targetLocales.length > 0 ? result.targetLocales.map((l) => chalk12.cyan(l)).join(", ") : chalk12.dim("(none)")}`,
3246
+ `Branches: ${result.targetBranches.map((b) => chalk12.cyan(b)).join(", ")}`,
3247
+ ...repoCanonical ? [`Repository: ${chalk12.cyan(repoCanonical)}${appDir !== "." ? ` (${appDir})` : ""}`] : [],
3248
+ "",
3249
+ `Add this to your .env file:`,
3250
+ ` ${chalk12.bold("VOCODER_API_KEY")}=${chalk12.cyan(result.apiKey)}`
3251
+ ];
3252
+ p12.note(lines.join("\n"), "Project created");
3253
+ if (!result.repositoryBound && repoCanonical) {
3254
+ p12.log.warn(
3255
+ `Repository "${repoCanonical}" was not automatically connected. Ensure your GitHub App installation covers this repository.`
3256
+ );
3257
+ }
3258
+ return 0;
3259
+ } catch (error) {
3260
+ spinner7.stop("Failed to create project.");
3261
+ if (error instanceof VocoderAPIError && error.limitError) {
3262
+ const { limitError } = error;
3263
+ p12.log.error(limitError.message);
3264
+ for (const line of getLimitErrorGuidance(limitError)) {
3265
+ p12.log.info(line);
3266
+ }
3267
+ return 1;
3268
+ }
3269
+ p12.log.error(
3270
+ error instanceof Error ? error.message : "Unknown error."
3271
+ );
3272
+ return 1;
3273
+ }
3274
+ }
3275
+
3276
+ // src/commands/whoami.ts
3277
+ import * as p13 from "@clack/prompts";
3278
+ import chalk13 from "chalk";
2971
3279
  async function whoami(options = {}) {
2972
3280
  const stored = readAuthData();
2973
3281
  if (!stored) {
2974
- p9.log.info("Not logged in. Run `vocoder init` to authenticate.");
3282
+ p13.log.info("Not logged in. Run `vocoder init` to authenticate.");
2975
3283
  return 1;
2976
3284
  }
2977
3285
  const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
2978
3286
  const api = new VocoderAPI({ apiUrl, apiKey: "" });
2979
3287
  try {
2980
3288
  const info = await api.getCliUserInfo(stored.token);
2981
- p9.log.info(`Logged in as ${chalk9.bold(info.email)}`);
3289
+ p13.log.info(`Logged in as ${chalk13.bold(info.email)}`);
2982
3290
  if (info.name) {
2983
- p9.log.info(`Name: ${info.name}`);
3291
+ p13.log.info(`Name: ${info.name}`);
2984
3292
  }
2985
- p9.log.info(`API: ${apiUrl}`);
3293
+ p13.log.info(`API: ${apiUrl}`);
2986
3294
  return 0;
2987
3295
  } catch {
2988
- p9.log.error(
3296
+ p13.log.error(
2989
3297
  "Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
2990
3298
  );
2991
3299
  return 1;
@@ -3017,5 +3325,38 @@ program.command("sync").description("Extract strings and sync translations").opt
3017
3325
  });
3018
3326
  program.command("logout").description("Log out and remove stored credentials").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(logout, options));
3019
3327
  program.command("whoami").description("Show the currently authenticated user").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(whoami, options));
3328
+ var localesCmd = program.command("locales").description("Manage project target locales").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(listProjectLocales, options));
3329
+ localesCmd.command("add <codes...>").description("Add one or more target locales by BCP 47 code (e.g. fr de pt-BR)").option("--api-url <url>", "Override Vocoder API URL").action(
3330
+ (codes, options) => runCommand((opts) => addLocales(codes, opts), options)
3331
+ );
3332
+ localesCmd.command("remove <codes...>").description("Remove one or more target locales by BCP 47 code").option("--api-url <url>", "Override Vocoder API URL").action(
3333
+ (codes, options) => runCommand((opts) => removeLocales(codes, opts), options)
3334
+ );
3335
+ localesCmd.command("supported").description("List all locales supported by Vocoder").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(listSupportedLocales, options));
3336
+ program.command("project").description("Show current project configuration").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(projectConfig, options));
3337
+ program.command("translations").description("Download the current translation snapshot").option("--branch <branch>", "Git branch (auto-detected if omitted)").option("--locale <locale>", "Fetch a specific locale only").option("--output <dir>", "Write locale JSON files to this directory").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(getTranslations, options));
3338
+ program.command("create-project").description("Create a new Vocoder project (requires prior `vocoder init`)").requiredOption("--name <name>", "Project display name").requiredOption("--source-locale <code>", "Source language BCP 47 code (e.g. en)").requiredOption("--workspace <org-id>", "Workspace organization ID").option(
3339
+ "--target-locales <codes>",
3340
+ "Comma-separated target locale codes (e.g. fr,de,pt-BR)"
3341
+ ).option(
3342
+ "--target-branches <branches>",
3343
+ "Comma-separated branch names to sync (default: main)"
3344
+ ).option(
3345
+ "--repo <canonical>",
3346
+ "Git repo canonical (e.g. github:owner/repo). Auto-detected from git remote if omitted."
3347
+ ).option(
3348
+ "--app-dir <path>",
3349
+ "App directory within the repo for monorepos (default: .)"
3350
+ ).option("--api-url <url>", "Override Vocoder API URL").action((options) => {
3351
+ const translated = {
3352
+ ...options,
3353
+ // Commander camelCases dashed options
3354
+ sourceLocale: options.sourceLocale,
3355
+ targetLocales: options.targetLocales,
3356
+ targetBranches: options.targetBranches,
3357
+ workspace: options.workspace
3358
+ };
3359
+ return runCommand(createProject, translated);
3360
+ });
3020
3361
  program.parse(process.argv);
3021
3362
  //# sourceMappingURL=bin.mjs.map