@vocoder/cli 0.1.20 → 0.1.22

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
@@ -12,6 +12,8 @@ import { Command } from "commander";
12
12
 
13
13
  // src/commands/init.ts
14
14
  import { execSync as execSync3, spawn as spawn2 } from "child_process";
15
+ import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
16
+ import { join as join2 } from "path";
15
17
  import * as p5 from "@clack/prompts";
16
18
  import chalk6 from "chalk";
17
19
  import { config as loadEnv } from "dotenv";
@@ -593,6 +595,25 @@ var VocoderAPI = class {
593
595
  });
594
596
  }
595
597
  const result = payload;
598
+ return result;
599
+ }
600
+ async listCompatibleLocales(userToken, sourceLocale) {
601
+ const url = `${this.apiUrl}/api/cli/locales/compatible?source=${encodeURIComponent(sourceLocale)}`;
602
+ const response = await fetch(url, {
603
+ headers: { Authorization: `Bearer ${userToken}` }
604
+ });
605
+ const payload = await readPayload(response);
606
+ if (!response.ok) {
607
+ throw new VocoderAPIError({
608
+ message: extractErrorMessage(
609
+ payload,
610
+ `Failed to list compatible locales (${response.status})`
611
+ ),
612
+ status: response.status,
613
+ payload
614
+ });
615
+ }
616
+ const result = payload;
596
617
  return result.locales;
597
618
  }
598
619
  // ── Project creation ──────────────────────────────────────────────────────────
@@ -1619,17 +1640,16 @@ async function runProjectCreate(params) {
1619
1640
  const { api, userToken, organizationId, repoCanonical } = params;
1620
1641
  const projectName = (params.defaultName ?? "my-project").trim();
1621
1642
  p3.log.success(`Project: ${chalk4.bold(projectName)}`);
1622
- let rawLocales;
1643
+ let sourceLocales;
1623
1644
  try {
1624
- rawLocales = await api.listLocales(userToken);
1645
+ ({ sourceLocales } = await api.listLocales(userToken));
1625
1646
  } catch {
1626
1647
  p3.log.error(
1627
1648
  "Failed to fetch supported locales. Check your connection and try again."
1628
1649
  );
1629
1650
  return null;
1630
1651
  }
1631
- const languageOptions = buildLanguageOptions(rawLocales);
1632
- const localeOptions = buildLocaleOptions(rawLocales);
1652
+ const languageOptions = buildLanguageOptions(sourceLocales);
1633
1653
  let appDir;
1634
1654
  if (params.defaultAppDir) {
1635
1655
  appDir = params.defaultAppDir;
@@ -1656,6 +1676,16 @@ async function runProjectCreate(params) {
1656
1676
  params.defaultSourceLocale ?? "en"
1657
1677
  );
1658
1678
  if (sourceLocale === null) return null;
1679
+ let compatibleTargets;
1680
+ try {
1681
+ compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
1682
+ } catch {
1683
+ p3.log.error(
1684
+ "Failed to fetch compatible target locales. Check your connection and try again."
1685
+ );
1686
+ return null;
1687
+ }
1688
+ const localeOptions = buildLocaleOptions(compatibleTargets);
1659
1689
  const targetOptions = localeOptions.filter(
1660
1690
  (opt) => opt.bcp47 !== sourceLocale
1661
1691
  );
@@ -1714,17 +1744,16 @@ async function runProjectCreate(params) {
1714
1744
  async function runProjectAppCreate(params) {
1715
1745
  const { api, userToken, projectId, projectName, repoCanonical } = params;
1716
1746
  const existingScopes = new Set(params.existingApps.map((a) => a.appDir));
1717
- let rawLocales;
1747
+ let sourceLocales;
1718
1748
  try {
1719
- rawLocales = await api.listLocales(userToken);
1749
+ ({ sourceLocales } = await api.listLocales(userToken));
1720
1750
  } catch {
1721
1751
  p3.log.error(
1722
1752
  "Failed to fetch supported locales. Check your connection and try again."
1723
1753
  );
1724
1754
  return null;
1725
1755
  }
1726
- const languageOptions = buildLanguageOptions(rawLocales);
1727
- const localeOptions = buildLocaleOptions(rawLocales);
1756
+ const languageOptions = buildLanguageOptions(sourceLocales);
1728
1757
  let appDir;
1729
1758
  if (params.defaultAppDir && !existingScopes.has(params.defaultAppDir)) {
1730
1759
  appDir = params.defaultAppDir;
@@ -1761,7 +1790,16 @@ async function runProjectAppCreate(params) {
1761
1790
  "en"
1762
1791
  );
1763
1792
  if (sourceLocale === null) return null;
1764
- const targetOptions = localeOptions.filter(
1793
+ let compatibleTargets;
1794
+ try {
1795
+ compatibleTargets = await api.listCompatibleLocales(userToken, sourceLocale);
1796
+ } catch {
1797
+ p3.log.error(
1798
+ "Failed to fetch compatible target locales. Check your connection and try again."
1799
+ );
1800
+ return null;
1801
+ }
1802
+ const targetOptions = buildLocaleOptions(compatibleTargets).filter(
1765
1803
  (opt) => opt.bcp47 !== sourceLocale
1766
1804
  );
1767
1805
  const targetLocales = await searchMultiSelectLocales(
@@ -1955,62 +1993,74 @@ function runScaffold(params) {
1955
1993
  sourceLocale,
1956
1994
  targetBranches
1957
1995
  });
1958
- let stepNum = 1;
1996
+ const steps = [];
1959
1997
  if (snippets.pluginStep) {
1960
- p5.log.message("");
1961
- p5.log.step(
1962
- `${chalk6.bold(`Step ${stepNum}:`)} Add the plugin to ${chalk6.cyan(snippets.pluginStep.file)}`
1963
- );
1964
- printCodeBlock(snippets.pluginStep.code);
1965
- stepNum++;
1998
+ steps.push({
1999
+ label: snippets.pluginStep.file,
2000
+ hint: "register the build plugin so Vocoder can extract your strings",
2001
+ code: snippets.pluginStep.code
2002
+ });
1966
2003
  }
1967
2004
  if (snippets.providerStep) {
2005
+ steps.push({
2006
+ label: snippets.providerStep.file,
2007
+ hint: "wrap your app so translations load at runtime",
2008
+ code: snippets.providerStep.code
2009
+ });
2010
+ }
2011
+ steps.push({
2012
+ label: "wrap translatable text",
2013
+ hint: "mark strings for extraction \u2014 Vocoder picks these up on push",
2014
+ code: snippets.wrapStep.code
2015
+ });
2016
+ p5.log.message("");
2017
+ p5.log.message(chalk6.bold("Finish setup in your code"));
2018
+ p5.log.message("");
2019
+ for (let i = 0; i < steps.length; i++) {
2020
+ const step = steps[i];
1968
2021
  p5.log.step(
1969
- `${chalk6.bold(`Step ${stepNum}:`)} Add the provider to ${chalk6.cyan(snippets.providerStep.file)}`
2022
+ `${chalk6.bold(step.label)} ${chalk6.dim(`\u2014 ${step.hint}`)}`
1970
2023
  );
1971
- printCodeBlock(snippets.providerStep.code);
1972
- stepNum++;
2024
+ printCodeBlock(step.code);
2025
+ if (i < steps.length - 1) p5.log.message("");
1973
2026
  }
1974
- p5.log.step(`${chalk6.bold(`Step ${stepNum}:`)} Wrap translatable strings`);
1975
- printCodeBlock(snippets.wrapStep.code);
1976
2027
  p5.log.message("");
1977
- for (const line of snippets.whatsNext.split("\n")) {
1978
- p5.log.success(line);
2028
+ const branchList = targetBranches.length > 0 ? targetBranches.map((b) => chalk6.cyan(b)).join(" or ") : chalk6.cyan("your target branch");
2029
+ p5.log.success(
2030
+ `Push to ${branchList} to trigger your first translation run.`
2031
+ );
2032
+ p5.log.message(chalk6.gray(" Docs: https://vocoder.app/docs/getting-started"));
2033
+ }
2034
+ function writeApiKeyToEnv(apiKey) {
2035
+ const envPath = join2(process.cwd(), ".env");
2036
+ if (!existsSync(envPath)) return false;
2037
+ try {
2038
+ const content = readFileSync2(envPath, "utf-8");
2039
+ const keyLine = `VOCODER_API_KEY=${apiKey}`;
2040
+ let updated;
2041
+ if (/^VOCODER_API_KEY=/m.test(content)) {
2042
+ updated = content.replace(/^VOCODER_API_KEY=.*/m, keyLine);
2043
+ } else {
2044
+ const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2045
+ updated = `${content}${sep}${keyLine}
2046
+ `;
2047
+ }
2048
+ writeFileSync2(envPath, updated);
2049
+ return true;
2050
+ } catch {
2051
+ return false;
1979
2052
  }
1980
2053
  }
1981
- function printMcpSetup(apiKey) {
1982
- const addCommand = `claude mcp add --scope project --transport stdio \\
1983
- --env VOCODER_API_KEY=${apiKey} \\
1984
- vocoder -- npx -y @vocoder/mcp`;
1985
- const teamConfig = JSON.stringify(
1986
- {
1987
- mcpServers: {
1988
- vocoder: {
1989
- type: "stdio",
1990
- command: "npx",
1991
- args: ["-y", "@vocoder/mcp"],
1992
- // biome-ignore lint/suspicious/noTemplateCurlyInString: MCP config template, not a JS template literal
1993
- env: { VOCODER_API_KEY: "${env:VOCODER_API_KEY}" }
1994
- }
1995
- }
1996
- },
1997
- null,
1998
- 2
1999
- );
2000
- p5.log.message("");
2001
- p5.log.message(chalk6.bold("Use Vocoder with Claude Code"));
2002
- p5.log.message("Run this to add the MCP server to your project:");
2003
- p5.log.message("");
2004
- printCodeBlock(addCommand);
2054
+ function printApiKey(apiKey) {
2055
+ const saved = writeApiKeyToEnv(apiKey);
2005
2056
  p5.log.message("");
2006
- p5.log.message(
2007
- "To share with your team, commit " + chalk6.cyan(".mcp.json") + " with an env var reference"
2008
- );
2009
- p5.log.message("so each developer supplies their own key:");
2010
- p5.log.message("");
2011
- printCodeBlock(teamConfig);
2012
- p5.log.message("");
2013
- p5.log.message(chalk6.gray("Setup instructions: https://vocoder.app/docs/mcp"));
2057
+ p5.log.message(chalk6.bold("Your API Key"));
2058
+ printCodeBlock(`VOCODER_API_KEY=${apiKey}`);
2059
+ if (saved) {
2060
+ p5.log.success(chalk6.dim("Saved to .env"));
2061
+ } else {
2062
+ p5.log.message(chalk6.dim(" Add the above to your .env file"));
2063
+ }
2014
2064
  }
2015
2065
  function printCodeBlock(code) {
2016
2066
  const lines = code.split("\n");
@@ -2234,7 +2284,7 @@ async function init(options = {}) {
2234
2284
  exactMatch.projectId
2235
2285
  );
2236
2286
  spinner4.stop("New API key generated");
2237
- printMcpSetup(apiKey);
2287
+ printApiKey(apiKey);
2238
2288
  } catch (err) {
2239
2289
  spinner4.stop("Failed to generate key");
2240
2290
  const msg = err instanceof Error ? err.message : String(err);
@@ -2709,7 +2759,7 @@ Translations won't run automatically until you grant access.
2709
2759
  sourceLocale: projectResult.sourceLocale,
2710
2760
  targetBranches: projectResult.targetBranches
2711
2761
  });
2712
- printMcpSetup(projectResult.apiKey);
2762
+ printApiKey(projectResult.apiKey);
2713
2763
  p5.outro("You're all set.");
2714
2764
  return 0;
2715
2765
  } catch (error) {
@@ -2747,9 +2797,9 @@ async function logout(options = {}) {
2747
2797
 
2748
2798
  // src/commands/sync.ts
2749
2799
  import { createHash, randomUUID } from "crypto";
2750
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2751
- import { join as join2 } from "path";
2752
- import * as p7 from "@clack/prompts";
2800
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
2801
+ import { join as join3 } from "path";
2802
+ import * as p8 from "@clack/prompts";
2753
2803
  import chalk8 from "chalk";
2754
2804
 
2755
2805
  // src/utils/branch.ts
@@ -2819,6 +2869,7 @@ function matchBranchPattern(branch, pattern) {
2819
2869
  }
2820
2870
 
2821
2871
  // src/utils/config.ts
2872
+ import * as p7 from "@clack/prompts";
2822
2873
  import chalk7 from "chalk";
2823
2874
  import { config as loadEnv2 } from "dotenv";
2824
2875
  loadEnv2();
@@ -2896,7 +2947,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2896
2947
  excludePattern = cliOptions.exclude;
2897
2948
  configSources.excludePattern = "CLI flag";
2898
2949
  } else if (envExcludePattern) {
2899
- excludePattern = envExcludePattern.split(",").map((p9) => p9.trim()).filter(Boolean);
2950
+ excludePattern = envExcludePattern.split(",").map((p10) => p10.trim()).filter(Boolean);
2900
2951
  configSources.excludePattern = "environment";
2901
2952
  } else {
2902
2953
  excludePattern = defaults.excludePattern;
@@ -2944,24 +2995,16 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2944
2995
  configSources.noFallback = "environment";
2945
2996
  }
2946
2997
  if (verbose) {
2947
- console.log(chalk7.dim("\n Configuration sources:"));
2948
- console.log(
2949
- chalk7.dim(` Include patterns: ${configSources.includePattern}`)
2950
- );
2951
- if (excludePattern.length > 0) {
2952
- console.log(
2953
- chalk7.dim(` Exclude patterns: ${configSources.excludePattern}`)
2954
- );
2955
- }
2956
- console.log(chalk7.dim(` API key: ${configSources.apiKey}`));
2957
- console.log(chalk7.dim(` API URL: ${configSources.apiUrl}
2958
- `));
2959
- console.log(chalk7.dim(` Sync mode: ${configSources.mode}`));
2960
- if (maxWaitMs) {
2961
- console.log(chalk7.dim(` Max wait: ${configSources.maxWaitMs}`));
2962
- }
2963
- console.log(chalk7.dim(` No fallback: ${configSources.noFallback}
2964
- `));
2998
+ const lines = [
2999
+ `Include patterns: ${chalk7.cyan(configSources.includePattern)}`,
3000
+ ...excludePattern.length > 0 ? [`Exclude patterns: ${chalk7.cyan(configSources.excludePattern)}`] : [],
3001
+ `API key: ${chalk7.cyan(configSources.apiKey)}`,
3002
+ `API URL: ${chalk7.cyan(configSources.apiUrl)}`,
3003
+ `Sync mode: ${chalk7.cyan(configSources.mode)}`,
3004
+ ...maxWaitMs ? [`Max wait: ${chalk7.cyan(String(configSources.maxWaitMs))}`] : [],
3005
+ `No fallback: ${chalk7.cyan(String(configSources.noFallback))}`
3006
+ ];
3007
+ p7.note(lines.join("\n"), "Configuration sources");
2965
3008
  }
2966
3009
  return {
2967
3010
  includePattern,
@@ -2982,9 +3025,9 @@ function computeStringsHash(texts) {
2982
3025
  }
2983
3026
  function readCachedStringsHash(projectRoot, branch) {
2984
3027
  const filePath = getCacheFilePath(projectRoot, branch);
2985
- if (!existsSync(filePath)) return null;
3028
+ if (!existsSync2(filePath)) return null;
2986
3029
  try {
2987
- const raw = JSON.parse(readFileSync2(filePath, "utf-8"));
3030
+ const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
2988
3031
  if (isRecord(raw) && typeof raw.stringsHash === "string")
2989
3032
  return raw.stringsHash;
2990
3033
  } catch {
@@ -3036,7 +3079,7 @@ function parseTranslations(value) {
3036
3079
  }
3037
3080
  function getCacheFilePath(projectRoot, branch) {
3038
3081
  const branchHash = createHash("sha1").update(branch).digest("hex").slice(0, 12);
3039
- return join2(
3082
+ return join3(
3040
3083
  projectRoot,
3041
3084
  "node_modules",
3042
3085
  ".vocoder",
@@ -3049,11 +3092,11 @@ function readLocalSnapshotCache(params) {
3049
3092
  const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
3050
3093
  for (const candidateBranch of candidateBranches) {
3051
3094
  const cacheFilePath = getCacheFilePath(params.projectRoot, candidateBranch);
3052
- if (!existsSync(cacheFilePath)) {
3095
+ if (!existsSync2(cacheFilePath)) {
3053
3096
  continue;
3054
3097
  }
3055
3098
  try {
3056
- const raw = readFileSync2(cacheFilePath, "utf-8");
3099
+ const raw = readFileSync3(cacheFilePath, "utf-8");
3057
3100
  const parsed = JSON.parse(raw);
3058
3101
  if (!isRecord(parsed)) {
3059
3102
  continue;
@@ -3079,7 +3122,7 @@ function readLocalSnapshotCache(params) {
3079
3122
  function writeLocalSnapshotCache(params) {
3080
3123
  const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
3081
3124
  mkdirSync2(
3082
- join2(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"),
3125
+ join3(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"),
3083
3126
  {
3084
3127
  recursive: true
3085
3128
  }
@@ -3096,7 +3139,7 @@ function writeLocalSnapshotCache(params) {
3096
3139
  ...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
3097
3140
  translations: params.translations
3098
3141
  };
3099
- writeFileSync2(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
3142
+ writeFileSync3(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
3100
3143
  return cacheFilePath;
3101
3144
  }
3102
3145
  function resolveEffectiveModeFromPolicy(params) {
@@ -3250,8 +3293,8 @@ async function fetchApiSnapshot(api, params) {
3250
3293
  async function sync(options = {}) {
3251
3294
  const startTime = Date.now();
3252
3295
  const projectRoot = process.cwd();
3253
- p7.intro("Vocoder Sync");
3254
- const spinner4 = p7.spinner();
3296
+ p8.intro("Vocoder Sync");
3297
+ const spinner4 = p8.spinner();
3255
3298
  try {
3256
3299
  spinner4.start("Detecting branch");
3257
3300
  const branch = detectBranch(options.branch);
@@ -3280,12 +3323,12 @@ async function sync(options = {}) {
3280
3323
  };
3281
3324
  spinner4.stop("Project configuration loaded");
3282
3325
  if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
3283
- p7.log.warn(
3326
+ p8.log.warn(
3284
3327
  `Skipping translations (${chalk8.cyan(branch)} is not a target branch)`
3285
3328
  );
3286
- p7.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
3287
- p7.log.info("Use --force to translate anyway");
3288
- p7.outro("");
3329
+ p8.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
3330
+ p8.log.info("Use --force to translate anyway");
3331
+ p8.outro("");
3289
3332
  return 0;
3290
3333
  }
3291
3334
  const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
@@ -3298,10 +3341,10 @@ async function sync(options = {}) {
3298
3341
  );
3299
3342
  if (extractedStrings.length === 0) {
3300
3343
  spinner4.stop("No translatable strings found");
3301
- p7.log.warn(
3344
+ p8.log.warn(
3302
3345
  "Make sure you are wrapping translatable strings with Vocoder"
3303
3346
  );
3304
- p7.outro("");
3347
+ p8.outro("");
3305
3348
  return 0;
3306
3349
  }
3307
3350
  spinner4.stop(
@@ -3312,10 +3355,10 @@ async function sync(options = {}) {
3312
3355
  if (extractedStrings.length > 5) {
3313
3356
  sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
3314
3357
  }
3315
- p7.note(sampleLines.join("\n"), "Sample strings");
3358
+ p8.note(sampleLines.join("\n"), "Sample strings");
3316
3359
  }
3317
3360
  if (options.dryRun) {
3318
- p7.note(
3361
+ p8.note(
3319
3362
  [
3320
3363
  `Strings: ${extractedStrings.length}`,
3321
3364
  `Branch: ${branch}`,
@@ -3326,12 +3369,12 @@ async function sync(options = {}) {
3326
3369
  ].join("\n"),
3327
3370
  "Dry run - would translate"
3328
3371
  );
3329
- p7.outro("No API calls made.");
3372
+ p8.outro("No API calls made.");
3330
3373
  return 0;
3331
3374
  }
3332
3375
  const repoIdentity = resolveGitRepositoryIdentity();
3333
3376
  if (!repoIdentity && options.verbose) {
3334
- p7.log.warn(
3377
+ p8.log.warn(
3335
3378
  "Could not detect git remote origin. Sync will continue without repo metadata."
3336
3379
  );
3337
3380
  }
@@ -3339,16 +3382,32 @@ async function sync(options = {}) {
3339
3382
  const stringEntries = buildStringEntries(extractedStrings);
3340
3383
  const sourceStrings = stringEntries.map((entry) => entry.text);
3341
3384
  if (options.verbose && stringEntries.length !== extractedStrings.length) {
3342
- p7.log.info(
3385
+ p8.log.info(
3343
3386
  `Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
3344
3387
  );
3345
3388
  }
3346
3389
  const currentHash = computeStringsHash(sourceStrings);
3347
3390
  if (!options.force) {
3348
3391
  const cachedHash = readCachedStringsHash(projectRoot, branch);
3392
+ if (options.verbose) {
3393
+ const cacheFile = getCacheFilePath(projectRoot, branch);
3394
+ if (cachedHash) {
3395
+ p8.log.info(
3396
+ `Local cache: ${chalk8.dim(cacheFile)}
3397
+ cached hash ${chalk8.cyan(cachedHash.slice(0, 8))}\u2026 vs current ${chalk8.cyan(currentHash.slice(0, 8))}\u2026 \u2014 ${cachedHash === currentHash ? chalk8.green("match") : chalk8.yellow("changed")}`
3398
+ );
3399
+ } else {
3400
+ p8.log.info(`No local cache found at ${chalk8.dim(cacheFile)} \u2014 will submit to API`);
3401
+ }
3402
+ }
3349
3403
  if (cachedHash && cachedHash === currentHash) {
3404
+ if (options.verbose) {
3405
+ p8.log.info(
3406
+ "Skipping API submission \u2014 delete node_modules/.vocoder to force a fresh sync"
3407
+ );
3408
+ }
3350
3409
  const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
3351
- p7.outro(`Up to date (${duration2}s)`);
3410
+ p8.outro(`Up to date (${duration2}s)`);
3352
3411
  return 0;
3353
3412
  }
3354
3413
  }
@@ -3373,31 +3432,31 @@ async function sync(options = {}) {
3373
3432
  policy: config.syncPolicy
3374
3433
  });
3375
3434
  if (options.verbose) {
3376
- p7.log.info(`Requested mode: ${requestedMode}`);
3377
- p7.log.info(`Effective mode: ${effectiveMode}`);
3378
- p7.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
3435
+ p8.log.info(`Requested mode: ${requestedMode}`);
3436
+ p8.log.info(`Effective mode: ${effectiveMode}`);
3437
+ p8.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
3379
3438
  if (batchResponse.queueStatus) {
3380
- p7.log.info(`Queue status: ${batchResponse.queueStatus}`);
3439
+ p8.log.info(`Queue status: ${batchResponse.queueStatus}`);
3381
3440
  }
3382
3441
  }
3383
3442
  if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
3384
- p7.log.success("No changes detected - strings are up to date");
3443
+ p8.log.success("No changes detected - strings are up to date");
3385
3444
  }
3386
- p7.log.info(`New strings: ${chalk8.cyan(batchResponse.newStrings)}`);
3445
+ p8.log.info(`New strings: ${chalk8.cyan(batchResponse.newStrings)}`);
3387
3446
  if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
3388
- p7.log.info(
3447
+ p8.log.info(
3389
3448
  `Deleted strings: ${chalk8.yellow(batchResponse.deletedStrings)} (archived)`
3390
3449
  );
3391
3450
  }
3392
- p7.log.info(`Total strings: ${chalk8.cyan(batchResponse.totalStrings)}`);
3451
+ p8.log.info(`Total strings: ${chalk8.cyan(batchResponse.totalStrings)}`);
3393
3452
  if (batchResponse.newStrings === 0) {
3394
- p7.log.success("No new strings - using existing translations");
3453
+ p8.log.success("No new strings - using existing translations");
3395
3454
  } else {
3396
- p7.log.info(
3455
+ p8.log.info(
3397
3456
  `Syncing to ${config.targetLocales.length} locales (${config.targetLocales.join(", ")})`
3398
3457
  );
3399
3458
  if (batchResponse.estimatedTime) {
3400
- p7.log.info(`Estimated time: ~${batchResponse.estimatedTime}s`);
3459
+ p8.log.info(`Estimated time: ~${batchResponse.estimatedTime}s`);
3401
3460
  }
3402
3461
  }
3403
3462
  let artifacts = null;
@@ -3435,7 +3494,7 @@ async function sync(options = {}) {
3435
3494
  if (effectiveMode === "required") {
3436
3495
  throw waitError;
3437
3496
  }
3438
- p7.log.warn(`Best-effort wait ended early: ${waitError.message}`);
3497
+ p8.log.warn(`Best-effort wait ended early: ${waitError.message}`);
3439
3498
  }
3440
3499
  }
3441
3500
  if (!artifacts) {
@@ -3469,7 +3528,7 @@ async function sync(options = {}) {
3469
3528
  spinner4.stop("Failed to fetch API snapshot");
3470
3529
  if (options.verbose) {
3471
3530
  const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
3472
- p7.log.warn(`Snapshot fetch error: ${message}`);
3531
+ p8.log.warn(`Snapshot fetch error: ${message}`);
3473
3532
  }
3474
3533
  }
3475
3534
  }
@@ -3503,61 +3562,61 @@ async function sync(options = {}) {
3503
3562
  completedAt: artifacts.completedAt ?? (artifacts.source === "fresh" ? (/* @__PURE__ */ new Date()).toISOString() : null)
3504
3563
  });
3505
3564
  if (options.verbose) {
3506
- p7.log.info(`Cached snapshot: ${cachePath}`);
3565
+ p8.log.info(`Cached snapshot: ${cachePath}`);
3507
3566
  }
3508
3567
  } catch (error) {
3509
3568
  if (options.verbose) {
3510
3569
  const message = error instanceof Error ? error.message : "Unknown cache write error";
3511
- p7.log.warn(`Failed to write local snapshot cache: ${message}`);
3570
+ p8.log.warn(`Failed to write local snapshot cache: ${message}`);
3512
3571
  }
3513
3572
  }
3514
3573
  if (artifacts.source !== "fresh") {
3515
3574
  const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
3516
- p7.log.warn(
3575
+ p8.log.warn(
3517
3576
  `Using ${sourceLabel}. New strings may appear after the background sync completes.`
3518
3577
  );
3519
3578
  }
3520
3579
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
3521
- p7.outro(`Sync complete! (${duration}s)`);
3580
+ p8.outro(`Sync complete! (${duration}s)`);
3522
3581
  return 0;
3523
3582
  } catch (error) {
3524
3583
  spinner4.stop();
3525
3584
  if (error instanceof VocoderAPIError && error.syncPolicyError) {
3526
- p7.log.error(error.syncPolicyError.message);
3585
+ p8.log.error(error.syncPolicyError.message);
3527
3586
  const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
3528
3587
  for (const line of guidance) {
3529
- p7.log.info(line);
3588
+ p8.log.info(line);
3530
3589
  }
3531
3590
  return 1;
3532
3591
  }
3533
3592
  if (error instanceof VocoderAPIError && error.limitError) {
3534
3593
  const { limitError } = error;
3535
- p7.log.error(limitError.message);
3594
+ p8.log.error(limitError.message);
3536
3595
  const guidance = getLimitErrorGuidance(limitError);
3537
3596
  for (const line of guidance) {
3538
- p7.log.info(line);
3597
+ p8.log.info(line);
3539
3598
  }
3540
3599
  return 1;
3541
3600
  }
3542
3601
  if (error instanceof Error) {
3543
- p7.log.error(error.message);
3602
+ p8.log.error(error.message);
3544
3603
  if (error.message.includes("VOCODER_API_KEY")) {
3545
- p7.log.warn(
3604
+ p8.log.warn(
3546
3605
  "VOCODER_API_KEY is only needed for `vocoder sync` (CLI push)."
3547
3606
  );
3548
- p7.log.info(" Create one at: https://vocoder.app/dashboard");
3549
- p7.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
3550
- p7.log.info("");
3551
- p7.log.info(
3607
+ p8.log.info(" Create one at: https://vocoder.app/dashboard");
3608
+ p8.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
3609
+ p8.log.info("");
3610
+ p8.log.info(
3552
3611
  " Note: If you use @vocoder/unplugin, `vocoder sync` is optional."
3553
3612
  );
3554
- p7.log.info(" Translations are fetched automatically at build time.");
3613
+ p8.log.info(" Translations are fetched automatically at build time.");
3555
3614
  } else if (error.message.includes("git branch")) {
3556
- p7.log.warn("Run from a git repository, or use:");
3557
- p7.log.info(" vocoder sync --branch main");
3615
+ p8.log.warn("Run from a git repository, or use:");
3616
+ p8.log.info(" vocoder sync --branch main");
3558
3617
  }
3559
3618
  if (options.verbose) {
3560
- p7.log.info(`Full error: ${error.stack ?? error}`);
3619
+ p8.log.info(`Full error: ${error.stack ?? error}`);
3561
3620
  }
3562
3621
  }
3563
3622
  return 1;
@@ -3565,26 +3624,26 @@ async function sync(options = {}) {
3565
3624
  }
3566
3625
 
3567
3626
  // src/commands/whoami.ts
3568
- import * as p8 from "@clack/prompts";
3627
+ import * as p9 from "@clack/prompts";
3569
3628
  import chalk9 from "chalk";
3570
3629
  async function whoami(options = {}) {
3571
3630
  const stored = readAuthData();
3572
3631
  if (!stored) {
3573
- p8.log.info("Not logged in. Run `vocoder init` to authenticate.");
3632
+ p9.log.info("Not logged in. Run `vocoder init` to authenticate.");
3574
3633
  return 1;
3575
3634
  }
3576
3635
  const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
3577
3636
  const api = new VocoderAPI({ apiUrl, apiKey: "" });
3578
3637
  try {
3579
3638
  const info = await api.getCliUserInfo(stored.token);
3580
- p8.log.info(`Logged in as ${chalk9.bold(info.email)}`);
3639
+ p9.log.info(`Logged in as ${chalk9.bold(info.email)}`);
3581
3640
  if (info.name) {
3582
- p8.log.info(`Name: ${info.name}`);
3641
+ p9.log.info(`Name: ${info.name}`);
3583
3642
  }
3584
- p8.log.info(`API: ${apiUrl}`);
3643
+ p9.log.info(`API: ${apiUrl}`);
3585
3644
  return 0;
3586
3645
  } catch {
3587
- p8.log.error(
3646
+ p9.log.error(
3588
3647
  "Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
3589
3648
  );
3590
3649
  return 1;