@vocoder/cli 0.1.19 → 0.1.21

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/README.md CHANGED
@@ -11,7 +11,7 @@ npm install -g @vocoder/cli
11
11
  Or use without installing:
12
12
 
13
13
  ```bash
14
- npx vocoder <command>
14
+ npx @vocoder/cli <command>
15
15
  ```
16
16
 
17
17
  ## Commands
@@ -56,14 +56,19 @@ The CLI opens the Vocoder GitHub App installation page. Authorizing the App crea
56
56
 
57
57
  ◒ Creating project...
58
58
 
59
- Step 1: Add the plugin to vite.config.ts
60
- ◆ Step 2: Wrap your app with VocoderProvider
61
- ◆ Step 3: Mark strings for translation with <T>
59
+ Finish setup in your code
62
60
 
63
- Use Vocoder with Claude Code
64
- claude mcp add --scope project --transport stdio \
65
- │ --env VOCODER_API_KEY=vc_xxxx \
66
- │ vocoder -- npx -y @vocoder/mcp
61
+ vite.config.ts — register the build plugin so Vocoder can extract your strings
62
+ your root layout or App component — wrap your app so translations load at runtime
63
+ ◆ wrap translatable text — mark strings for extraction — Vocoder picks these up on push
64
+
65
+ ✓ Push to main to trigger your first translation run.
66
+
67
+ ◆ Your API Key
68
+ │ ┌─────────────────────────────────────────┐
69
+ │ │ VOCODER_API_KEY=vcp_xxxx │
70
+ │ └─────────────────────────────────────────┘
71
+ ✓ Saved to .env
67
72
 
68
73
  ◇ You're all set.
69
74
  ```
@@ -121,10 +126,14 @@ Reads `VOCODER_API_KEY` from environment or `.env`. Detects `<T>` and `t()` usag
121
126
 
122
127
  | Flag | Description |
123
128
  |---|---|
129
+ | `--include <glob>` | Glob pattern for files to scan (repeatable). Default: `**/*.{tsx,jsx,ts,js}` |
130
+ | `--exclude <glob>` | Glob pattern to skip (repeatable). Merged with built-in excludes |
124
131
  | `--locale <code>` | Sync only this target locale |
125
132
  | `--dry-run` | Show what would be synced without submitting |
126
133
  | `--verbose` | Show extraction and sync details |
127
134
 
135
+ Patterns can also be set via env vars: `VOCODER_INCLUDE_PATTERN` and `VOCODER_EXCLUDE_PATTERN` (comma-separated).
136
+
128
137
  ---
129
138
 
130
139
  ### `vocoder logout`
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";
@@ -1955,62 +1957,74 @@ function runScaffold(params) {
1955
1957
  sourceLocale,
1956
1958
  targetBranches
1957
1959
  });
1958
- let stepNum = 1;
1960
+ const steps = [];
1959
1961
  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++;
1962
+ steps.push({
1963
+ label: snippets.pluginStep.file,
1964
+ hint: "register the build plugin so Vocoder can extract your strings",
1965
+ code: snippets.pluginStep.code
1966
+ });
1966
1967
  }
1967
1968
  if (snippets.providerStep) {
1969
+ steps.push({
1970
+ label: snippets.providerStep.file,
1971
+ hint: "wrap your app so translations load at runtime",
1972
+ code: snippets.providerStep.code
1973
+ });
1974
+ }
1975
+ steps.push({
1976
+ label: "wrap translatable text",
1977
+ hint: "mark strings for extraction \u2014 Vocoder picks these up on push",
1978
+ code: snippets.wrapStep.code
1979
+ });
1980
+ p5.log.message("");
1981
+ p5.log.message(chalk6.bold("Finish setup in your code"));
1982
+ p5.log.message("");
1983
+ for (let i = 0; i < steps.length; i++) {
1984
+ const step = steps[i];
1968
1985
  p5.log.step(
1969
- `${chalk6.bold(`Step ${stepNum}:`)} Add the provider to ${chalk6.cyan(snippets.providerStep.file)}`
1986
+ `${chalk6.bold(step.label)} ${chalk6.dim(`\u2014 ${step.hint}`)}`
1970
1987
  );
1971
- printCodeBlock(snippets.providerStep.code);
1972
- stepNum++;
1988
+ printCodeBlock(step.code);
1989
+ if (i < steps.length - 1) p5.log.message("");
1973
1990
  }
1974
- p5.log.step(`${chalk6.bold(`Step ${stepNum}:`)} Wrap translatable strings`);
1975
- printCodeBlock(snippets.wrapStep.code);
1976
1991
  p5.log.message("");
1977
- for (const line of snippets.whatsNext.split("\n")) {
1978
- p5.log.success(line);
1992
+ const branchList = targetBranches.length > 0 ? targetBranches.map((b) => chalk6.cyan(b)).join(" or ") : chalk6.cyan("your target branch");
1993
+ p5.log.success(
1994
+ `Push to ${branchList} to trigger your first translation run.`
1995
+ );
1996
+ p5.log.message(chalk6.gray(" Docs: https://vocoder.app/docs/getting-started"));
1997
+ }
1998
+ function writeApiKeyToEnv(apiKey) {
1999
+ const envPath = join2(process.cwd(), ".env");
2000
+ if (!existsSync(envPath)) return false;
2001
+ try {
2002
+ const content = readFileSync2(envPath, "utf-8");
2003
+ const keyLine = `VOCODER_API_KEY=${apiKey}`;
2004
+ let updated;
2005
+ if (/^VOCODER_API_KEY=/m.test(content)) {
2006
+ updated = content.replace(/^VOCODER_API_KEY=.*/m, keyLine);
2007
+ } else {
2008
+ const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
2009
+ updated = `${content}${sep}${keyLine}
2010
+ `;
2011
+ }
2012
+ writeFileSync2(envPath, updated);
2013
+ return true;
2014
+ } catch {
2015
+ return false;
1979
2016
  }
1980
2017
  }
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);
2018
+ function printApiKey(apiKey) {
2019
+ const saved = writeApiKeyToEnv(apiKey);
2005
2020
  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"));
2021
+ p5.log.message(chalk6.bold("Your API Key"));
2022
+ printCodeBlock(`VOCODER_API_KEY=${apiKey}`);
2023
+ if (saved) {
2024
+ p5.log.success(chalk6.dim("Saved to .env"));
2025
+ } else {
2026
+ p5.log.message(chalk6.dim(" Add the above to your .env file"));
2027
+ }
2014
2028
  }
2015
2029
  function printCodeBlock(code) {
2016
2030
  const lines = code.split("\n");
@@ -2074,9 +2088,8 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
2074
2088
  return null;
2075
2089
  }
2076
2090
  if (!shouldOpen) {
2077
- p5.log.info(
2078
- "Open the URL above manually in your browser to continue."
2079
- );
2091
+ p5.note(browserUrl, "Sign In");
2092
+ p5.log.info("Open the URL above manually in your browser to continue.");
2080
2093
  } else {
2081
2094
  const opened = await tryOpenBrowser2(browserUrl);
2082
2095
  if (!opened) {
@@ -2235,7 +2248,7 @@ async function init(options = {}) {
2235
2248
  exactMatch.projectId
2236
2249
  );
2237
2250
  spinner4.stop("New API key generated");
2238
- printMcpSetup(apiKey);
2251
+ printApiKey(apiKey);
2239
2252
  } catch (err) {
2240
2253
  spinner4.stop("Failed to generate key");
2241
2254
  const msg = err instanceof Error ? err.message : String(err);
@@ -2710,7 +2723,7 @@ Translations won't run automatically until you grant access.
2710
2723
  sourceLocale: projectResult.sourceLocale,
2711
2724
  targetBranches: projectResult.targetBranches
2712
2725
  });
2713
- printMcpSetup(projectResult.apiKey);
2726
+ printApiKey(projectResult.apiKey);
2714
2727
  p5.outro("You're all set.");
2715
2728
  return 0;
2716
2729
  } catch (error) {
@@ -2748,9 +2761,9 @@ async function logout(options = {}) {
2748
2761
 
2749
2762
  // src/commands/sync.ts
2750
2763
  import { createHash, randomUUID } from "crypto";
2751
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2752
- import { join as join2 } from "path";
2753
- import * as p7 from "@clack/prompts";
2764
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
2765
+ import { join as join3 } from "path";
2766
+ import * as p8 from "@clack/prompts";
2754
2767
  import chalk8 from "chalk";
2755
2768
 
2756
2769
  // src/utils/branch.ts
@@ -2820,6 +2833,7 @@ function matchBranchPattern(branch, pattern) {
2820
2833
  }
2821
2834
 
2822
2835
  // src/utils/config.ts
2836
+ import * as p7 from "@clack/prompts";
2823
2837
  import chalk7 from "chalk";
2824
2838
  import { config as loadEnv2 } from "dotenv";
2825
2839
  loadEnv2();
@@ -2897,7 +2911,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2897
2911
  excludePattern = cliOptions.exclude;
2898
2912
  configSources.excludePattern = "CLI flag";
2899
2913
  } else if (envExcludePattern) {
2900
- excludePattern = envExcludePattern.split(",").map((p9) => p9.trim()).filter(Boolean);
2914
+ excludePattern = envExcludePattern.split(",").map((p10) => p10.trim()).filter(Boolean);
2901
2915
  configSources.excludePattern = "environment";
2902
2916
  } else {
2903
2917
  excludePattern = defaults.excludePattern;
@@ -2945,24 +2959,16 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2945
2959
  configSources.noFallback = "environment";
2946
2960
  }
2947
2961
  if (verbose) {
2948
- console.log(chalk7.dim("\n Configuration sources:"));
2949
- console.log(
2950
- chalk7.dim(` Include patterns: ${configSources.includePattern}`)
2951
- );
2952
- if (excludePattern.length > 0) {
2953
- console.log(
2954
- chalk7.dim(` Exclude patterns: ${configSources.excludePattern}`)
2955
- );
2956
- }
2957
- console.log(chalk7.dim(` API key: ${configSources.apiKey}`));
2958
- console.log(chalk7.dim(` API URL: ${configSources.apiUrl}
2959
- `));
2960
- console.log(chalk7.dim(` Sync mode: ${configSources.mode}`));
2961
- if (maxWaitMs) {
2962
- console.log(chalk7.dim(` Max wait: ${configSources.maxWaitMs}`));
2963
- }
2964
- console.log(chalk7.dim(` No fallback: ${configSources.noFallback}
2965
- `));
2962
+ const lines = [
2963
+ `Include patterns: ${chalk7.cyan(configSources.includePattern)}`,
2964
+ ...excludePattern.length > 0 ? [`Exclude patterns: ${chalk7.cyan(configSources.excludePattern)}`] : [],
2965
+ `API key: ${chalk7.cyan(configSources.apiKey)}`,
2966
+ `API URL: ${chalk7.cyan(configSources.apiUrl)}`,
2967
+ `Sync mode: ${chalk7.cyan(configSources.mode)}`,
2968
+ ...maxWaitMs ? [`Max wait: ${chalk7.cyan(String(configSources.maxWaitMs))}`] : [],
2969
+ `No fallback: ${chalk7.cyan(String(configSources.noFallback))}`
2970
+ ];
2971
+ p7.note(lines.join("\n"), "Configuration sources");
2966
2972
  }
2967
2973
  return {
2968
2974
  includePattern,
@@ -2983,9 +2989,9 @@ function computeStringsHash(texts) {
2983
2989
  }
2984
2990
  function readCachedStringsHash(projectRoot, branch) {
2985
2991
  const filePath = getCacheFilePath(projectRoot, branch);
2986
- if (!existsSync(filePath)) return null;
2992
+ if (!existsSync2(filePath)) return null;
2987
2993
  try {
2988
- const raw = JSON.parse(readFileSync2(filePath, "utf-8"));
2994
+ const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
2989
2995
  if (isRecord(raw) && typeof raw.stringsHash === "string")
2990
2996
  return raw.stringsHash;
2991
2997
  } catch {
@@ -3037,7 +3043,7 @@ function parseTranslations(value) {
3037
3043
  }
3038
3044
  function getCacheFilePath(projectRoot, branch) {
3039
3045
  const branchHash = createHash("sha1").update(branch).digest("hex").slice(0, 12);
3040
- return join2(
3046
+ return join3(
3041
3047
  projectRoot,
3042
3048
  "node_modules",
3043
3049
  ".vocoder",
@@ -3050,11 +3056,11 @@ function readLocalSnapshotCache(params) {
3050
3056
  const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
3051
3057
  for (const candidateBranch of candidateBranches) {
3052
3058
  const cacheFilePath = getCacheFilePath(params.projectRoot, candidateBranch);
3053
- if (!existsSync(cacheFilePath)) {
3059
+ if (!existsSync2(cacheFilePath)) {
3054
3060
  continue;
3055
3061
  }
3056
3062
  try {
3057
- const raw = readFileSync2(cacheFilePath, "utf-8");
3063
+ const raw = readFileSync3(cacheFilePath, "utf-8");
3058
3064
  const parsed = JSON.parse(raw);
3059
3065
  if (!isRecord(parsed)) {
3060
3066
  continue;
@@ -3080,7 +3086,7 @@ function readLocalSnapshotCache(params) {
3080
3086
  function writeLocalSnapshotCache(params) {
3081
3087
  const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
3082
3088
  mkdirSync2(
3083
- join2(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"),
3089
+ join3(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"),
3084
3090
  {
3085
3091
  recursive: true
3086
3092
  }
@@ -3097,7 +3103,7 @@ function writeLocalSnapshotCache(params) {
3097
3103
  ...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
3098
3104
  translations: params.translations
3099
3105
  };
3100
- writeFileSync2(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
3106
+ writeFileSync3(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
3101
3107
  return cacheFilePath;
3102
3108
  }
3103
3109
  function resolveEffectiveModeFromPolicy(params) {
@@ -3251,8 +3257,8 @@ async function fetchApiSnapshot(api, params) {
3251
3257
  async function sync(options = {}) {
3252
3258
  const startTime = Date.now();
3253
3259
  const projectRoot = process.cwd();
3254
- p7.intro("Vocoder Sync");
3255
- const spinner4 = p7.spinner();
3260
+ p8.intro("Vocoder Sync");
3261
+ const spinner4 = p8.spinner();
3256
3262
  try {
3257
3263
  spinner4.start("Detecting branch");
3258
3264
  const branch = detectBranch(options.branch);
@@ -3281,12 +3287,12 @@ async function sync(options = {}) {
3281
3287
  };
3282
3288
  spinner4.stop("Project configuration loaded");
3283
3289
  if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
3284
- p7.log.warn(
3290
+ p8.log.warn(
3285
3291
  `Skipping translations (${chalk8.cyan(branch)} is not a target branch)`
3286
3292
  );
3287
- p7.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
3288
- p7.log.info("Use --force to translate anyway");
3289
- p7.outro("");
3293
+ p8.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
3294
+ p8.log.info("Use --force to translate anyway");
3295
+ p8.outro("");
3290
3296
  return 0;
3291
3297
  }
3292
3298
  const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
@@ -3299,10 +3305,10 @@ async function sync(options = {}) {
3299
3305
  );
3300
3306
  if (extractedStrings.length === 0) {
3301
3307
  spinner4.stop("No translatable strings found");
3302
- p7.log.warn(
3308
+ p8.log.warn(
3303
3309
  "Make sure you are wrapping translatable strings with Vocoder"
3304
3310
  );
3305
- p7.outro("");
3311
+ p8.outro("");
3306
3312
  return 0;
3307
3313
  }
3308
3314
  spinner4.stop(
@@ -3313,10 +3319,10 @@ async function sync(options = {}) {
3313
3319
  if (extractedStrings.length > 5) {
3314
3320
  sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
3315
3321
  }
3316
- p7.note(sampleLines.join("\n"), "Sample strings");
3322
+ p8.note(sampleLines.join("\n"), "Sample strings");
3317
3323
  }
3318
3324
  if (options.dryRun) {
3319
- p7.note(
3325
+ p8.note(
3320
3326
  [
3321
3327
  `Strings: ${extractedStrings.length}`,
3322
3328
  `Branch: ${branch}`,
@@ -3327,12 +3333,12 @@ async function sync(options = {}) {
3327
3333
  ].join("\n"),
3328
3334
  "Dry run - would translate"
3329
3335
  );
3330
- p7.outro("No API calls made.");
3336
+ p8.outro("No API calls made.");
3331
3337
  return 0;
3332
3338
  }
3333
3339
  const repoIdentity = resolveGitRepositoryIdentity();
3334
3340
  if (!repoIdentity && options.verbose) {
3335
- p7.log.warn(
3341
+ p8.log.warn(
3336
3342
  "Could not detect git remote origin. Sync will continue without repo metadata."
3337
3343
  );
3338
3344
  }
@@ -3340,16 +3346,32 @@ async function sync(options = {}) {
3340
3346
  const stringEntries = buildStringEntries(extractedStrings);
3341
3347
  const sourceStrings = stringEntries.map((entry) => entry.text);
3342
3348
  if (options.verbose && stringEntries.length !== extractedStrings.length) {
3343
- p7.log.info(
3349
+ p8.log.info(
3344
3350
  `Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
3345
3351
  );
3346
3352
  }
3347
3353
  const currentHash = computeStringsHash(sourceStrings);
3348
3354
  if (!options.force) {
3349
3355
  const cachedHash = readCachedStringsHash(projectRoot, branch);
3356
+ if (options.verbose) {
3357
+ const cacheFile = getCacheFilePath(projectRoot, branch);
3358
+ if (cachedHash) {
3359
+ p8.log.info(
3360
+ `Local cache: ${chalk8.dim(cacheFile)}
3361
+ 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")}`
3362
+ );
3363
+ } else {
3364
+ p8.log.info(`No local cache found at ${chalk8.dim(cacheFile)} \u2014 will submit to API`);
3365
+ }
3366
+ }
3350
3367
  if (cachedHash && cachedHash === currentHash) {
3368
+ if (options.verbose) {
3369
+ p8.log.info(
3370
+ "Skipping API submission \u2014 delete node_modules/.vocoder to force a fresh sync"
3371
+ );
3372
+ }
3351
3373
  const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
3352
- p7.outro(`Up to date (${duration2}s)`);
3374
+ p8.outro(`Up to date (${duration2}s)`);
3353
3375
  return 0;
3354
3376
  }
3355
3377
  }
@@ -3374,31 +3396,31 @@ async function sync(options = {}) {
3374
3396
  policy: config.syncPolicy
3375
3397
  });
3376
3398
  if (options.verbose) {
3377
- p7.log.info(`Requested mode: ${requestedMode}`);
3378
- p7.log.info(`Effective mode: ${effectiveMode}`);
3379
- p7.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
3399
+ p8.log.info(`Requested mode: ${requestedMode}`);
3400
+ p8.log.info(`Effective mode: ${effectiveMode}`);
3401
+ p8.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
3380
3402
  if (batchResponse.queueStatus) {
3381
- p7.log.info(`Queue status: ${batchResponse.queueStatus}`);
3403
+ p8.log.info(`Queue status: ${batchResponse.queueStatus}`);
3382
3404
  }
3383
3405
  }
3384
3406
  if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
3385
- p7.log.success("No changes detected - strings are up to date");
3407
+ p8.log.success("No changes detected - strings are up to date");
3386
3408
  }
3387
- p7.log.info(`New strings: ${chalk8.cyan(batchResponse.newStrings)}`);
3409
+ p8.log.info(`New strings: ${chalk8.cyan(batchResponse.newStrings)}`);
3388
3410
  if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
3389
- p7.log.info(
3411
+ p8.log.info(
3390
3412
  `Deleted strings: ${chalk8.yellow(batchResponse.deletedStrings)} (archived)`
3391
3413
  );
3392
3414
  }
3393
- p7.log.info(`Total strings: ${chalk8.cyan(batchResponse.totalStrings)}`);
3415
+ p8.log.info(`Total strings: ${chalk8.cyan(batchResponse.totalStrings)}`);
3394
3416
  if (batchResponse.newStrings === 0) {
3395
- p7.log.success("No new strings - using existing translations");
3417
+ p8.log.success("No new strings - using existing translations");
3396
3418
  } else {
3397
- p7.log.info(
3419
+ p8.log.info(
3398
3420
  `Syncing to ${config.targetLocales.length} locales (${config.targetLocales.join(", ")})`
3399
3421
  );
3400
3422
  if (batchResponse.estimatedTime) {
3401
- p7.log.info(`Estimated time: ~${batchResponse.estimatedTime}s`);
3423
+ p8.log.info(`Estimated time: ~${batchResponse.estimatedTime}s`);
3402
3424
  }
3403
3425
  }
3404
3426
  let artifacts = null;
@@ -3436,7 +3458,7 @@ async function sync(options = {}) {
3436
3458
  if (effectiveMode === "required") {
3437
3459
  throw waitError;
3438
3460
  }
3439
- p7.log.warn(`Best-effort wait ended early: ${waitError.message}`);
3461
+ p8.log.warn(`Best-effort wait ended early: ${waitError.message}`);
3440
3462
  }
3441
3463
  }
3442
3464
  if (!artifacts) {
@@ -3470,7 +3492,7 @@ async function sync(options = {}) {
3470
3492
  spinner4.stop("Failed to fetch API snapshot");
3471
3493
  if (options.verbose) {
3472
3494
  const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
3473
- p7.log.warn(`Snapshot fetch error: ${message}`);
3495
+ p8.log.warn(`Snapshot fetch error: ${message}`);
3474
3496
  }
3475
3497
  }
3476
3498
  }
@@ -3504,61 +3526,61 @@ async function sync(options = {}) {
3504
3526
  completedAt: artifacts.completedAt ?? (artifacts.source === "fresh" ? (/* @__PURE__ */ new Date()).toISOString() : null)
3505
3527
  });
3506
3528
  if (options.verbose) {
3507
- p7.log.info(`Cached snapshot: ${cachePath}`);
3529
+ p8.log.info(`Cached snapshot: ${cachePath}`);
3508
3530
  }
3509
3531
  } catch (error) {
3510
3532
  if (options.verbose) {
3511
3533
  const message = error instanceof Error ? error.message : "Unknown cache write error";
3512
- p7.log.warn(`Failed to write local snapshot cache: ${message}`);
3534
+ p8.log.warn(`Failed to write local snapshot cache: ${message}`);
3513
3535
  }
3514
3536
  }
3515
3537
  if (artifacts.source !== "fresh") {
3516
3538
  const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
3517
- p7.log.warn(
3539
+ p8.log.warn(
3518
3540
  `Using ${sourceLabel}. New strings may appear after the background sync completes.`
3519
3541
  );
3520
3542
  }
3521
3543
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
3522
- p7.outro(`Sync complete! (${duration}s)`);
3544
+ p8.outro(`Sync complete! (${duration}s)`);
3523
3545
  return 0;
3524
3546
  } catch (error) {
3525
3547
  spinner4.stop();
3526
3548
  if (error instanceof VocoderAPIError && error.syncPolicyError) {
3527
- p7.log.error(error.syncPolicyError.message);
3549
+ p8.log.error(error.syncPolicyError.message);
3528
3550
  const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
3529
3551
  for (const line of guidance) {
3530
- p7.log.info(line);
3552
+ p8.log.info(line);
3531
3553
  }
3532
3554
  return 1;
3533
3555
  }
3534
3556
  if (error instanceof VocoderAPIError && error.limitError) {
3535
3557
  const { limitError } = error;
3536
- p7.log.error(limitError.message);
3558
+ p8.log.error(limitError.message);
3537
3559
  const guidance = getLimitErrorGuidance(limitError);
3538
3560
  for (const line of guidance) {
3539
- p7.log.info(line);
3561
+ p8.log.info(line);
3540
3562
  }
3541
3563
  return 1;
3542
3564
  }
3543
3565
  if (error instanceof Error) {
3544
- p7.log.error(error.message);
3566
+ p8.log.error(error.message);
3545
3567
  if (error.message.includes("VOCODER_API_KEY")) {
3546
- p7.log.warn(
3568
+ p8.log.warn(
3547
3569
  "VOCODER_API_KEY is only needed for `vocoder sync` (CLI push)."
3548
3570
  );
3549
- p7.log.info(" Create one at: https://vocoder.app/dashboard");
3550
- p7.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
3551
- p7.log.info("");
3552
- p7.log.info(
3571
+ p8.log.info(" Create one at: https://vocoder.app/dashboard");
3572
+ p8.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
3573
+ p8.log.info("");
3574
+ p8.log.info(
3553
3575
  " Note: If you use @vocoder/unplugin, `vocoder sync` is optional."
3554
3576
  );
3555
- p7.log.info(" Translations are fetched automatically at build time.");
3577
+ p8.log.info(" Translations are fetched automatically at build time.");
3556
3578
  } else if (error.message.includes("git branch")) {
3557
- p7.log.warn("Run from a git repository, or use:");
3558
- p7.log.info(" vocoder sync --branch main");
3579
+ p8.log.warn("Run from a git repository, or use:");
3580
+ p8.log.info(" vocoder sync --branch main");
3559
3581
  }
3560
3582
  if (options.verbose) {
3561
- p7.log.info(`Full error: ${error.stack ?? error}`);
3583
+ p8.log.info(`Full error: ${error.stack ?? error}`);
3562
3584
  }
3563
3585
  }
3564
3586
  return 1;
@@ -3566,26 +3588,26 @@ async function sync(options = {}) {
3566
3588
  }
3567
3589
 
3568
3590
  // src/commands/whoami.ts
3569
- import * as p8 from "@clack/prompts";
3591
+ import * as p9 from "@clack/prompts";
3570
3592
  import chalk9 from "chalk";
3571
3593
  async function whoami(options = {}) {
3572
3594
  const stored = readAuthData();
3573
3595
  if (!stored) {
3574
- p8.log.info("Not logged in. Run `vocoder init` to authenticate.");
3596
+ p9.log.info("Not logged in. Run `vocoder init` to authenticate.");
3575
3597
  return 1;
3576
3598
  }
3577
3599
  const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
3578
3600
  const api = new VocoderAPI({ apiUrl, apiKey: "" });
3579
3601
  try {
3580
3602
  const info = await api.getCliUserInfo(stored.token);
3581
- p8.log.info(`Logged in as ${chalk9.bold(info.email)}`);
3603
+ p9.log.info(`Logged in as ${chalk9.bold(info.email)}`);
3582
3604
  if (info.name) {
3583
- p8.log.info(`Name: ${info.name}`);
3605
+ p9.log.info(`Name: ${info.name}`);
3584
3606
  }
3585
- p8.log.info(`API: ${apiUrl}`);
3607
+ p9.log.info(`API: ${apiUrl}`);
3586
3608
  return 0;
3587
3609
  } catch {
3588
- p8.log.error(
3610
+ p9.log.error(
3589
3611
  "Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
3590
3612
  );
3591
3613
  return 1;