@vocoder/cli 0.1.3 → 0.1.4

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
@@ -5,6 +5,7 @@ import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
7
  import * as p from "@clack/prompts";
8
+ import chalk from "chalk";
8
9
 
9
10
  // src/utils/api.ts
10
11
  function isLimitErrorResponse(value) {
@@ -254,12 +255,29 @@ var VocoderAPI = class {
254
255
  }
255
256
  return payload;
256
257
  }
258
+ /**
259
+ * Look up whether a project already exists for a given repo + scope.
260
+ * Returns { projectId, projectName, organizationName } or null if not found.
261
+ */
262
+ async lookupProjectByRepo(params) {
263
+ try {
264
+ const response = await fetch(`${this.apiUrl}/api/cli/init/lookup`, {
265
+ method: "POST",
266
+ headers: { "Content-Type": "application/json" },
267
+ body: JSON.stringify({
268
+ repo: params.repoCanonical,
269
+ scopePath: params.scopePath
270
+ })
271
+ });
272
+ if (response.status === 404) return null;
273
+ if (!response.ok) return null;
274
+ return await response.json();
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
257
279
  };
258
280
 
259
- // src/commands/init.ts
260
- import { existsSync, readFileSync, writeFileSync } from "fs";
261
- import { join } from "path";
262
-
263
281
  // src/utils/git-identity.ts
264
282
  import { execSync } from "child_process";
265
283
  import { relative, resolve } from "path";
@@ -359,55 +377,11 @@ function resolveGitContext() {
359
377
  // src/commands/init.ts
360
378
  import { spawn } from "child_process";
361
379
  var SUBSCRIPTION_SETTINGS_PATH = "/dashboard/workspace/settings?tab=subscription";
362
- function escapeRegExp(value) {
363
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
364
- }
365
380
  function parseTargetLocales(value) {
366
381
  if (!value) return void 0;
367
382
  const locales = value.split(",").map((locale) => locale.trim()).filter(Boolean);
368
383
  return locales.length > 0 ? locales : void 0;
369
384
  }
370
- function getEnvLine(filePath, key) {
371
- if (!existsSync(filePath)) {
372
- return null;
373
- }
374
- const current = readFileSync(filePath, "utf-8");
375
- const pattern = new RegExp(`^${escapeRegExp(key)}=.*$`, "m");
376
- const existingMatch = current.match(pattern);
377
- return existingMatch?.[0] ?? null;
378
- }
379
- function getEnvValue(filePath, key) {
380
- const line = getEnvLine(filePath, key);
381
- if (!line) return null;
382
- const eqIndex = line.indexOf("=");
383
- if (eqIndex === -1) return null;
384
- return line.slice(eqIndex + 1);
385
- }
386
- function upsertEnvValue(params) {
387
- const lineValue = `${params.key}=${params.value}`;
388
- if (!existsSync(params.filePath)) {
389
- writeFileSync(params.filePath, `${lineValue}
390
- `, "utf-8");
391
- return;
392
- }
393
- const current = readFileSync(params.filePath, "utf-8");
394
- const pattern = new RegExp(`^${escapeRegExp(params.key)}=.*$`, "m");
395
- const existingMatch = current.match(pattern);
396
- if (existingMatch && existingMatch[0] !== lineValue && !params.allowOverwrite) {
397
- throw new Error(
398
- `${params.key} already exists in ${params.filePath}. Re-run with --yes to overwrite.`
399
- );
400
- }
401
- if (existingMatch) {
402
- const updated = current.replace(pattern, lineValue);
403
- writeFileSync(params.filePath, updated.endsWith("\n") ? updated : `${updated}
404
- `, "utf-8");
405
- return;
406
- }
407
- const prefix = current.endsWith("\n") || current.length === 0 ? "" : "\n";
408
- writeFileSync(params.filePath, `${current}${prefix}${lineValue}
409
- `, "utf-8");
410
- }
411
385
  async function sleep(ms) {
412
386
  await new Promise((resolve2) => setTimeout(resolve2, ms));
413
387
  }
@@ -468,76 +442,44 @@ function printPlanLimitMessage(apiUrl, message) {
468
442
  ${message}`);
469
443
  p.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
470
444
  }
471
- function maskApiKey(key) {
472
- if (key.length <= 8) return key;
473
- return `${key.slice(0, 4)}...${key.slice(-4)}`;
445
+ function printNextSteps(projectName, organizationName) {
446
+ p.log.info(`Project: ${chalk.bold(projectName)}`);
447
+ p.log.info(`Workspace: ${chalk.bold(organizationName)}`);
448
+ p.log.info("");
449
+ p.log.info("Next steps:");
450
+ p.log.info(` 1. Add ${chalk.cyan("@vocoder/unplugin")} to your build config`);
451
+ p.log.info(` 2. Wrap translatable strings with ${chalk.green("<T>")}...${chalk.green("</T>")}`);
452
+ p.log.info(" 3. Push to trigger extraction + translation");
474
453
  }
475
454
  async function init(options = {}) {
476
- const projectRoot = process.cwd();
477
455
  const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
478
- const envPath = join(projectRoot, ".env");
479
456
  p.intro("Vocoder Setup");
480
457
  const spinner4 = p.spinner();
481
458
  try {
482
- const existingKey = getEnvValue(envPath, "VOCODER_API_KEY");
483
- if (existingKey && existingKey.startsWith("vc_")) {
484
- const existingApi = new VocoderAPI({ apiUrl, apiKey: existingKey });
485
- try {
486
- const config = await existingApi.getProjectConfig();
487
- p.log.info("Existing configuration found:");
488
- p.note(
489
- [
490
- `Project: ${config.projectName}`,
491
- `Workspace: ${config.organizationName}`,
492
- `Source: ${config.sourceLocale}`,
493
- `Targets: ${config.targetLocales.join(", ")}`,
494
- `Key: ${maskApiKey(existingKey)}`
495
- ].join("\n")
496
- );
497
- if (options.yes) {
498
- p.outro("Configuration unchanged. You're all set!");
499
- return 0;
500
- }
501
- const action = await p.select({
502
- message: "What would you like to do?",
503
- options: [
504
- { value: "keep", label: "Keep current configuration" },
505
- { value: "reconfigure", label: "Reconfigure (new browser setup)" }
506
- ]
507
- });
508
- if (p.isCancel(action)) {
509
- p.cancel("Setup cancelled.");
510
- return 1;
511
- }
512
- if (action === "keep") {
513
- p.outro("Configuration unchanged. You're all set!");
514
- return 0;
515
- }
516
- } catch {
517
- p.log.warn("Found VOCODER_API_KEY in .env but it appears to be invalid or expired.");
518
- if (!options.yes) {
519
- const action = await p.select({
520
- message: "What would you like to do?",
521
- options: [
522
- { value: "reconfigure", label: "Reconfigure (new browser setup)" },
523
- { value: "keep", label: "Keep current key anyway" }
524
- ]
525
- });
526
- if (p.isCancel(action)) {
527
- p.cancel("Setup cancelled.");
528
- return 1;
529
- }
530
- if (action === "keep") {
531
- p.outro("Keeping existing key. You may encounter errors if the key is invalid.");
532
- return 0;
533
- }
534
- }
459
+ const gitContext = resolveGitContext();
460
+ const identity = gitContext.identity;
461
+ if (gitContext.warnings.length > 0) {
462
+ for (const warning of gitContext.warnings) {
463
+ p.log.warn(warning);
535
464
  }
536
465
  }
466
+ if (identity) {
467
+ spinner4.start("Checking for existing project...");
468
+ const api2 = new VocoderAPI({ apiUrl, apiKey: "" });
469
+ const existing = await api2.lookupProjectByRepo({
470
+ repoCanonical: identity.repoCanonical,
471
+ scopePath: identity.repoScopePath
472
+ });
473
+ if (existing) {
474
+ spinner4.stop("Found existing project!");
475
+ p.outro("Vocoder is already set up for this repository.");
476
+ printNextSteps(existing.projectName, existing.organizationName);
477
+ return 0;
478
+ }
479
+ spinner4.stop("No existing project found for this repo.");
480
+ }
537
481
  spinner4.start("Creating setup session");
538
482
  const api = new VocoderAPI({ apiUrl, apiKey: "" });
539
- const gitContext = resolveGitContext();
540
- const identity = gitContext.identity;
541
483
  const start = await api.startInitSession({
542
484
  projectName: options.projectName,
543
485
  sourceLocale: options.sourceLocale,
@@ -547,12 +489,8 @@ async function init(options = {}) {
547
489
  });
548
490
  spinner4.stop("Setup session created");
549
491
  const verificationUrlString = start.verificationUrl;
550
- if (gitContext.warnings.length > 0) {
551
- for (const warning of gitContext.warnings) {
552
- p.log.warn(warning);
553
- }
554
- }
555
- p.note(verificationUrlString, "Authorize in your browser");
492
+ p.log.info("Create a project in your browser to continue.");
493
+ p.note(verificationUrlString, "Setup URL");
556
494
  if (process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== "true") {
557
495
  const shouldOpen = options.yes ? true : await p.confirm({ message: "Open this URL in your browser?" });
558
496
  if (p.isCancel(shouldOpen)) {
@@ -562,14 +500,14 @@ async function init(options = {}) {
562
500
  if (shouldOpen) {
563
501
  const opened = await tryOpenBrowser(verificationUrlString);
564
502
  if (opened) {
565
- p.log.info("Opened your browser for verification.");
503
+ p.log.info("Opened your browser.");
566
504
  } else {
567
505
  p.log.info("Could not open a browser automatically. Use the URL above.");
568
506
  }
569
507
  }
570
508
  }
571
509
  const expiresAt = new Date(start.expiresAt).getTime();
572
- spinner4.start("Waiting for browser authorization...");
510
+ spinner4.start("Waiting for setup to complete...");
573
511
  while (Date.now() < expiresAt) {
574
512
  const status = await api.getInitSessionStatus({
575
513
  sessionId: start.sessionId,
@@ -578,7 +516,7 @@ async function init(options = {}) {
578
516
  if (status.status === "pending") {
579
517
  const pendingMessage = status.message?.trim();
580
518
  if (pendingMessage) {
581
- spinner4.message(`Waiting for browser authorization... (${pendingMessage})`);
519
+ spinner4.message(`Waiting for setup to complete... (${pendingMessage})`);
582
520
  }
583
521
  await sleep((status.pollIntervalSeconds || start.poll.intervalSeconds) * 1e3);
584
522
  continue;
@@ -594,70 +532,19 @@ async function init(options = {}) {
594
532
  return 1;
595
533
  }
596
534
  if (status.status === "completed") {
597
- spinner4.stop("Authorization complete!");
598
- const key = "VOCODER_API_KEY";
599
- const desiredLine = `${key}=${status.credentials.apiKey}`;
600
- const existingLine = getEnvLine(envPath, key);
601
- const isAlreadyCurrent = existingLine === desiredLine;
602
- let didOverwrite = false;
603
- if (!isAlreadyCurrent) {
604
- try {
605
- upsertEnvValue({
606
- filePath: envPath,
607
- key,
608
- value: status.credentials.apiKey,
609
- allowOverwrite: Boolean(options.yes)
610
- });
611
- didOverwrite = Boolean(existingLine);
612
- } catch (error) {
613
- const overwriteConflict = error instanceof Error && error.message.includes(`${key} already exists in ${envPath}`);
614
- if (!overwriteConflict) {
615
- throw error;
616
- }
617
- const shouldOverwrite = await p.confirm({
618
- message: `${key} already exists in ${envPath}. Overwrite it?`
619
- });
620
- if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {
621
- p.log.warn("Existing VOCODER_API_KEY was not changed.");
622
- p.log.info("Re-run with --yes to overwrite it without prompting.");
623
- p.cancel("Setup cancelled.");
624
- return 1;
625
- }
626
- upsertEnvValue({
627
- filePath: envPath,
628
- key,
629
- value: status.credentials.apiKey,
630
- allowOverwrite: true
631
- });
632
- didOverwrite = true;
633
- }
634
- }
635
- if (isAlreadyCurrent) {
636
- p.log.info(`VOCODER_API_KEY already matches your .env file`);
637
- } else if (didOverwrite) {
638
- p.log.success(`Updated VOCODER_API_KEY in .env`);
639
- } else {
640
- p.log.success(`Wrote VOCODER_API_KEY to .env`);
641
- }
535
+ spinner4.stop("Setup complete!");
536
+ const { credentials } = status;
642
537
  p.outro("Vocoder initialized successfully!");
643
- p.log.info(`Project: ${status.credentials.projectName}`);
644
- p.log.info(`Workspace: ${status.credentials.organizationName}`);
538
+ printNextSteps(credentials.projectName, credentials.organizationName);
645
539
  return 0;
646
540
  }
647
541
  }
648
- spinner4.stop("Authorization timed out");
649
- p.log.error("Authorization timed out. Run `vocoder init` again.");
542
+ spinner4.stop("Setup timed out");
543
+ p.log.error("Setup timed out. Run `vocoder init` again.");
650
544
  p.cancel("Setup could not be completed.");
651
545
  return 1;
652
546
  } catch (error) {
653
547
  spinner4.stop();
654
- if (error instanceof VocoderAPIError && error.limitError) {
655
- printPlanLimitMessage(apiUrl, error.limitError.message);
656
- p.log.info(`Current: ${error.limitError.current}`);
657
- p.log.info(`Required: ${error.limitError.required}`);
658
- p.log.info(`Upgrade: ${error.limitError.upgradeUrl}`);
659
- return 1;
660
- }
661
548
  if (error instanceof Error) {
662
549
  if (isPlanLimitFailure(error.message)) {
663
550
  printPlanLimitMessage(apiUrl, error.message);
@@ -742,10 +629,10 @@ function matchBranchPattern(branch, pattern) {
742
629
  }
743
630
 
744
631
  // src/commands/sync.ts
745
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
632
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
746
633
 
747
634
  // src/utils/config.ts
748
- import chalk from "chalk";
635
+ import chalk2 from "chalk";
749
636
  import { config as loadEnv } from "dotenv";
750
637
  loadEnv();
751
638
  function validateLocalConfig(config) {
@@ -837,19 +724,19 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
837
724
  configSources.noFallback = "environment";
838
725
  }
839
726
  if (verbose) {
840
- console.log(chalk.dim("\n Configuration sources:"));
841
- console.log(chalk.dim(` Include patterns: ${configSources.extractionPattern}`));
727
+ console.log(chalk2.dim("\n Configuration sources:"));
728
+ console.log(chalk2.dim(` Include patterns: ${configSources.extractionPattern}`));
842
729
  if (excludePattern.length > 0) {
843
- console.log(chalk.dim(` Exclude patterns: ${configSources.excludePattern}`));
730
+ console.log(chalk2.dim(` Exclude patterns: ${configSources.excludePattern}`));
844
731
  }
845
- console.log(chalk.dim(` API key: ${configSources.apiKey}`));
846
- console.log(chalk.dim(` API URL: ${configSources.apiUrl}
732
+ console.log(chalk2.dim(` API key: ${configSources.apiKey}`));
733
+ console.log(chalk2.dim(` API URL: ${configSources.apiUrl}
847
734
  `));
848
- console.log(chalk.dim(` Sync mode: ${configSources.mode}`));
735
+ console.log(chalk2.dim(` Sync mode: ${configSources.mode}`));
849
736
  if (maxWaitMs) {
850
- console.log(chalk.dim(` Max wait: ${configSources.maxWaitMs}`));
737
+ console.log(chalk2.dim(` Max wait: ${configSources.maxWaitMs}`));
851
738
  }
852
- console.log(chalk.dim(` No fallback: ${configSources.noFallback}
739
+ console.log(chalk2.dim(` No fallback: ${configSources.noFallback}
853
740
  `));
854
741
  }
855
742
  return {
@@ -866,7 +753,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
866
753
 
867
754
  // src/utils/extract.ts
868
755
  import { createHash } from "crypto";
869
- import { readFileSync as readFileSync2 } from "fs";
756
+ import { readFileSync } from "fs";
870
757
  import { parse } from "@babel/parser";
871
758
  import babelTraverse from "@babel/traverse";
872
759
  import { glob } from "glob";
@@ -910,7 +797,7 @@ var StringExtractor = class {
910
797
  * Extract strings from a single file
911
798
  */
912
799
  async extractFromFile(filePath, projectRoot) {
913
- const code = readFileSync2(filePath, "utf-8");
800
+ const code = readFileSync(filePath, "utf-8");
914
801
  const strings = [];
915
802
  const relativeFilePath = pathRelative(projectRoot, filePath).split("\\").join("/");
916
803
  try {
@@ -1137,8 +1024,8 @@ var StringExtractor = class {
1137
1024
  };
1138
1025
 
1139
1026
  // src/commands/sync.ts
1140
- import chalk2 from "chalk";
1141
- import { join as join2 } from "path";
1027
+ import chalk3 from "chalk";
1028
+ import { join } from "path";
1142
1029
  function isRecord(value) {
1143
1030
  return typeof value === "object" && value !== null && !Array.isArray(value);
1144
1031
  }
@@ -1186,17 +1073,17 @@ function getCacheFilePath(projectRoot, branch) {
1186
1073
  const slug = branch.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
1187
1074
  const branchHash = createHash2("sha1").update(branch).digest("hex").slice(0, 12);
1188
1075
  const filename = `${slug || "branch"}-${branchHash}.json`;
1189
- return join2(projectRoot, ".vocoder", "cache", "sync", filename);
1076
+ return join(projectRoot, ".vocoder", "cache", "sync", filename);
1190
1077
  }
1191
1078
  function readLocalSnapshotCache(params) {
1192
1079
  const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
1193
1080
  for (const candidateBranch of candidateBranches) {
1194
1081
  const cacheFilePath = getCacheFilePath(params.projectRoot, candidateBranch);
1195
- if (!existsSync2(cacheFilePath)) {
1082
+ if (!existsSync(cacheFilePath)) {
1196
1083
  continue;
1197
1084
  }
1198
1085
  try {
1199
- const raw = readFileSync3(cacheFilePath, "utf-8");
1086
+ const raw = readFileSync2(cacheFilePath, "utf-8");
1200
1087
  const parsed = JSON.parse(raw);
1201
1088
  if (!isRecord(parsed)) {
1202
1089
  continue;
@@ -1222,7 +1109,7 @@ function readLocalSnapshotCache(params) {
1222
1109
  }
1223
1110
  function writeLocalSnapshotCache(params) {
1224
1111
  const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
1225
- mkdirSync(join2(params.projectRoot, ".vocoder", "cache", "sync"), {
1112
+ mkdirSync(join(params.projectRoot, ".vocoder", "cache", "sync"), {
1226
1113
  recursive: true
1227
1114
  });
1228
1115
  const payload = {
@@ -1236,7 +1123,7 @@ function writeLocalSnapshotCache(params) {
1236
1123
  ...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
1237
1124
  translations: params.translations
1238
1125
  };
1239
- writeFileSync2(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
1126
+ writeFileSync(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
1240
1127
  return cacheFilePath;
1241
1128
  }
1242
1129
  function resolveEffectiveModeFromPolicy(params) {
@@ -1396,7 +1283,7 @@ async function sync(options = {}) {
1396
1283
  try {
1397
1284
  spinner4.start("Detecting branch");
1398
1285
  const branch = detectBranch(options.branch);
1399
- spinner4.stop(`Branch: ${chalk2.cyan(branch)}`);
1286
+ spinner4.stop(`Branch: ${chalk3.cyan(branch)}`);
1400
1287
  spinner4.start("Loading project configuration");
1401
1288
  const mergedConfig = await getMergedConfig(options, options.verbose);
1402
1289
  const localConfig = {
@@ -1422,7 +1309,7 @@ async function sync(options = {}) {
1422
1309
  spinner4.stop("Project configuration loaded");
1423
1310
  if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
1424
1311
  p2.log.warn(
1425
- `Skipping translations (${chalk2.cyan(branch)} is not a target branch)`
1312
+ `Skipping translations (${chalk3.cyan(branch)} is not a target branch)`
1426
1313
  );
1427
1314
  p2.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
1428
1315
  p2.log.info("Use --force to translate anyway");
@@ -1444,7 +1331,7 @@ async function sync(options = {}) {
1444
1331
  return 0;
1445
1332
  }
1446
1333
  spinner4.stop(
1447
- `Extracted ${chalk2.cyan(extractedStrings.length)} strings from ${chalk2.cyan(patternsDisplay)}`
1334
+ `Extracted ${chalk3.cyan(extractedStrings.length)} strings from ${chalk3.cyan(patternsDisplay)}`
1448
1335
  );
1449
1336
  if (options.verbose) {
1450
1337
  const sampleLines = extractedStrings.slice(0, 5).map((s) => ` "${s.text}" (${s.file}:${s.line})`);
@@ -1493,7 +1380,7 @@ async function sync(options = {}) {
1493
1380
  },
1494
1381
  repoIdentity ?? void 0
1495
1382
  );
1496
- spinner4.stop(`Submitted to API - Batch ${chalk2.cyan(batchResponse.batchId)}`);
1383
+ spinner4.stop(`Submitted to API - Batch ${chalk3.cyan(batchResponse.batchId)}`);
1497
1384
  const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
1498
1385
  branch,
1499
1386
  requestedMode,
@@ -1510,13 +1397,13 @@ async function sync(options = {}) {
1510
1397
  if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
1511
1398
  p2.log.success("No changes detected - strings are up to date");
1512
1399
  }
1513
- p2.log.info(`New strings: ${chalk2.cyan(batchResponse.newStrings)}`);
1400
+ p2.log.info(`New strings: ${chalk3.cyan(batchResponse.newStrings)}`);
1514
1401
  if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
1515
1402
  p2.log.info(
1516
- `Deleted strings: ${chalk2.yellow(batchResponse.deletedStrings)} (archived)`
1403
+ `Deleted strings: ${chalk3.yellow(batchResponse.deletedStrings)} (archived)`
1517
1404
  );
1518
1405
  }
1519
- p2.log.info(`Total strings: ${chalk2.cyan(batchResponse.totalStrings)}`);
1406
+ p2.log.info(`Total strings: ${chalk3.cyan(batchResponse.totalStrings)}`);
1520
1407
  if (batchResponse.newStrings === 0) {
1521
1408
  p2.log.success("No new strings - using existing translations");
1522
1409
  } else {
@@ -1670,9 +1557,12 @@ async function sync(options = {}) {
1670
1557
  if (error instanceof Error) {
1671
1558
  p2.log.error(error.message);
1672
1559
  if (error.message.includes("VOCODER_API_KEY")) {
1673
- p2.log.warn("Set your API key:");
1674
- p2.log.info(' export VOCODER_API_KEY="your-api-key"');
1675
- p2.log.info(" or add it to your .env file");
1560
+ p2.log.warn("VOCODER_API_KEY is only needed for `vocoder sync` (CLI push).");
1561
+ p2.log.info(" Create one at: https://vocoder.app/dashboard");
1562
+ p2.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
1563
+ p2.log.info("");
1564
+ p2.log.info(" Note: If you use @vocoder/unplugin, `vocoder sync` is optional.");
1565
+ p2.log.info(" Translations are fetched automatically at build time.");
1676
1566
  } else if (error.message.includes("git branch")) {
1677
1567
  p2.log.warn("Run from a git repository, or use:");
1678
1568
  p2.log.info(" vocoder sync --branch main");
@@ -1686,13 +1576,13 @@ async function sync(options = {}) {
1686
1576
  }
1687
1577
 
1688
1578
  // src/commands/wrap.ts
1689
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1579
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1690
1580
  import { relative as relative2 } from "path";
1691
1581
  import * as p3 from "@clack/prompts";
1692
- import chalk3 from "chalk";
1582
+ import chalk4 from "chalk";
1693
1583
 
1694
1584
  // src/utils/wrap/analyzer.ts
1695
- import { readFileSync as readFileSync4 } from "fs";
1585
+ import { readFileSync as readFileSync3 } from "fs";
1696
1586
  import { parse as parse2 } from "@babel/parser";
1697
1587
  import babelTraverse2 from "@babel/traverse";
1698
1588
  import { glob as glob2 } from "glob";
@@ -2045,7 +1935,7 @@ var StringAnalyzer = class {
2045
1935
  * Analyze a single file and return wrap candidates.
2046
1936
  */
2047
1937
  analyzeFile(filePath) {
2048
- const code = readFileSync4(filePath, "utf-8");
1938
+ const code = readFileSync3(filePath, "utf-8");
2049
1939
  return this.analyzeCode(code, filePath);
2050
1940
  }
2051
1941
  /**
@@ -2672,21 +2562,21 @@ async function wrap(options = {}) {
2672
2562
  return 0;
2673
2563
  }
2674
2564
  spinner4.stop(
2675
- `Found ${chalk3.cyan(allCandidates.length)} candidate strings`
2565
+ `Found ${chalk4.cyan(allCandidates.length)} candidate strings`
2676
2566
  );
2677
2567
  const filtered = allCandidates.filter(
2678
2568
  (c) => meetsConfidenceThreshold(c.confidence, confidenceThreshold)
2679
2569
  );
2680
2570
  if (filtered.length === 0) {
2681
2571
  p3.log.warn(
2682
- `No strings meet the ${chalk3.bold(confidenceThreshold)} confidence threshold.`
2572
+ `No strings meet the ${chalk4.bold(confidenceThreshold)} confidence threshold.`
2683
2573
  );
2684
2574
  p3.log.info("Try --confidence medium or --confidence low to see more candidates.");
2685
2575
  p3.outro("");
2686
2576
  return 0;
2687
2577
  }
2688
2578
  p3.log.info(
2689
- `${filtered.length} strings meet ${chalk3.bold(confidenceThreshold)} confidence threshold`
2579
+ `${filtered.length} strings meet ${chalk4.bold(confidenceThreshold)} confidence threshold`
2690
2580
  );
2691
2581
  const byFile = /* @__PURE__ */ new Map();
2692
2582
  for (const c of filtered) {
@@ -2698,15 +2588,15 @@ async function wrap(options = {}) {
2698
2588
  const lines = [];
2699
2589
  for (const [file, candidates] of byFile) {
2700
2590
  const relPath = relative2(projectRoot, file);
2701
- lines.push(chalk3.bold(relPath));
2591
+ lines.push(chalk4.bold(relPath));
2702
2592
  for (const c of candidates) {
2703
- const confidenceColor = c.confidence === "high" ? chalk3.green : c.confidence === "medium" ? chalk3.yellow : chalk3.red;
2593
+ const confidenceColor = c.confidence === "high" ? chalk4.green : c.confidence === "medium" ? chalk4.yellow : chalk4.red;
2704
2594
  const strategyLabel = c.strategy === "T-component" ? "<T>" : "t()";
2705
2595
  lines.push(
2706
- ` ${chalk3.dim(`L${c.line}`)} ${confidenceColor(`[${c.confidence}]`)} ${chalk3.cyan(strategyLabel)} "${truncate(c.text, 50)}"`
2596
+ ` ${chalk4.dim(`L${c.line}`)} ${confidenceColor(`[${c.confidence}]`)} ${chalk4.cyan(strategyLabel)} "${truncate(c.text, 50)}"`
2707
2597
  );
2708
2598
  if (options.verbose) {
2709
- lines.push(chalk3.dim(` ${c.reason}`));
2599
+ lines.push(chalk4.dim(` ${c.reason}`));
2710
2600
  }
2711
2601
  }
2712
2602
  lines.push("");
@@ -2738,10 +2628,10 @@ async function wrap(options = {}) {
2738
2628
  acceptedByFile.set(c.file, existing);
2739
2629
  }
2740
2630
  for (const [file, candidates] of acceptedByFile) {
2741
- const code = readFileSync5(file, "utf-8");
2631
+ const code = readFileSync4(file, "utf-8");
2742
2632
  const result = transformer.transform(code, candidates, file);
2743
2633
  if (result.wrappedCount > 0) {
2744
- writeFileSync3(file, result.output, "utf-8");
2634
+ writeFileSync2(file, result.output, "utf-8");
2745
2635
  totalWrapped += result.wrappedCount;
2746
2636
  filesModified++;
2747
2637
  }
@@ -2751,7 +2641,7 @@ async function wrap(options = {}) {
2751
2641
  }
2752
2642
  }
2753
2643
  spinner4.stop(
2754
- `Wrapped ${chalk3.cyan(totalWrapped)} strings across ${chalk3.cyan(filesModified)} files`
2644
+ `Wrapped ${chalk4.cyan(totalWrapped)} strings across ${chalk4.cyan(filesModified)} files`
2755
2645
  );
2756
2646
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
2757
2647
  p3.outro(`Done! (${duration}s)`);
@@ -2776,7 +2666,7 @@ async function interactiveConfirm(byFile, projectRoot) {
2776
2666
  p3.log.info("Interactive mode \u2014 confirm each string:");
2777
2667
  for (const [file, candidates] of byFile) {
2778
2668
  const relPath = relative2(projectRoot, file);
2779
- p3.log.step(chalk3.bold(relPath));
2669
+ p3.log.step(chalk4.bold(relPath));
2780
2670
  let skipFile = false;
2781
2671
  for (const c of candidates) {
2782
2672
  if (skipFile) break;
@@ -2832,9 +2722,9 @@ function summarizeCandidates(candidates) {
2832
2722
  else tFunction++;
2833
2723
  }
2834
2724
  const parts = [];
2835
- if (high > 0) parts.push(chalk3.green(`${high} high`));
2836
- if (medium > 0) parts.push(chalk3.yellow(`${medium} medium`));
2837
- if (low > 0) parts.push(chalk3.red(`${low} low`));
2725
+ if (high > 0) parts.push(chalk4.green(`${high} high`));
2726
+ if (medium > 0) parts.push(chalk4.yellow(`${medium} medium`));
2727
+ if (low > 0) parts.push(chalk4.red(`${low} low`));
2838
2728
  return `${candidates.length} total (${parts.join(", ")}) | ${tComponent} <T>, ${tFunction} t()`;
2839
2729
  }
2840
2730