@vocoder/cli 0.1.16 → 0.1.17

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,7 +5,7 @@ import {
5
5
  detectLocalEcosystem,
6
6
  getPackagesToInstall,
7
7
  getSetupSnippets
8
- } from "./chunk-KPIT5ETY.mjs";
8
+ } from "./chunk-3QBORM6T.mjs";
9
9
 
10
10
  // src/bin.ts
11
11
  import { Command } from "commander";
@@ -420,7 +420,7 @@ var VocoderAPI = class {
420
420
  organizationName: data.organizationName,
421
421
  sourceLocale: data.sourceLocale,
422
422
  targetLocales: data.targetLocales,
423
- branchTriggers: data.branchTriggers ?? [],
423
+ targetBranches: data.targetBranches ?? ["main"],
424
424
  primaryBranch: data.primaryBranch,
425
425
  syncPolicy: {
426
426
  blockingBranches: data.syncPolicy?.blockingBranches ?? ["main", "master"],
@@ -480,7 +480,8 @@ var VocoderAPI = class {
480
480
  ...typeof options?.requestedMaxWaitMs === "number" ? { requestedMaxWaitMs: options.requestedMaxWaitMs } : {},
481
481
  ...options?.clientRunId ? { clientRunId: options.clientRunId } : {},
482
482
  ...repoIdentity?.repoCanonical ? { repoCanonical: repoIdentity.repoCanonical } : {},
483
- ...repoIdentity?.repoScopePath !== void 0 ? { repoScopePath: repoIdentity.repoScopePath } : {}
483
+ ...repoIdentity?.repoAppDir !== void 0 ? { repoAppDir: repoIdentity.repoAppDir } : {},
484
+ ...repoIdentity?.commitSha ? { commitSha: repoIdentity.commitSha } : {}
484
485
  })
485
486
  }, "Translation submission failed");
486
487
  }
@@ -700,6 +701,25 @@ var VocoderAPI = class {
700
701
  const result = payload;
701
702
  return result.projects;
702
703
  }
704
+ async regenerateProjectApiKey(userToken, projectId) {
705
+ const response = await fetch(`${this.apiUrl}/api/cli/project/regenerate-key`, {
706
+ method: "POST",
707
+ headers: {
708
+ "Content-Type": "application/json",
709
+ Authorization: `Bearer ${userToken}`
710
+ },
711
+ body: JSON.stringify({ projectId })
712
+ });
713
+ const payload = await readPayload(response);
714
+ if (!response.ok) {
715
+ throw new VocoderAPIError({
716
+ message: extractErrorMessage(payload, `Failed to regenerate API key (${response.status})`),
717
+ status: response.status,
718
+ payload
719
+ });
720
+ }
721
+ return payload;
722
+ }
703
723
  // ── CLI GitHub endpoints ──────────────────────────────────────────────────────
704
724
  async startCliGitHubInstall(userToken, params) {
705
725
  const response = await fetch(`${this.apiUrl}/api/cli/github/install/start`, {
@@ -842,7 +862,7 @@ var VocoderAPI = class {
842
862
  headers: { "Content-Type": "application/json" },
843
863
  body: JSON.stringify({
844
864
  repo: params.repoCanonical,
845
- scopePath: params.scopePath
865
+ appDir: params.appDir
846
866
  })
847
867
  });
848
868
  if (!response.ok) {
@@ -886,6 +906,15 @@ import { config as loadEnv } from "dotenv";
886
906
  // src/utils/git-identity.ts
887
907
  import { execSync } from "child_process";
888
908
  import { relative, resolve } from "path";
909
+ var SHA_REGEX = /^[0-9a-f]{40}$/i;
910
+ function detectCommitSha() {
911
+ if (process.env.VOCODER_COMMIT_SHA && SHA_REGEX.test(process.env.VOCODER_COMMIT_SHA)) {
912
+ return process.env.VOCODER_COMMIT_SHA;
913
+ }
914
+ const knownSha = process.env.GITHUB_SHA || process.env.VERCEL_GIT_COMMIT_SHA || process.env.CI_COMMIT_SHA || process.env.BITBUCKET_COMMIT || process.env.CIRCLE_SHA1 || process.env.RENDER_GIT_COMMIT;
915
+ if (knownSha && SHA_REGEX.test(knownSha)) return knownSha;
916
+ return safeExec("git rev-parse HEAD");
917
+ }
889
918
  function safeExec(command) {
890
919
  try {
891
920
  const output = execSync(command, {
@@ -956,16 +985,16 @@ function resolveGitRepositoryIdentity() {
956
985
  }
957
986
  const repositoryRoot = safeExec("git rev-parse --show-toplevel");
958
987
  const currentDirectory = process.cwd();
959
- let repoScopePath = "";
988
+ let repoAppDir = "";
960
989
  if (repositoryRoot) {
961
990
  const relativePath = relative(resolve(repositoryRoot), resolve(currentDirectory)).replace(/\\/g, "/").trim();
962
991
  if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
963
- repoScopePath = relativePath;
992
+ repoAppDir = relativePath;
964
993
  }
965
994
  }
966
995
  return {
967
996
  repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
968
- repoScopePath
997
+ repoAppDir
969
998
  };
970
999
  }
971
1000
  function resolveGitContext() {
@@ -1249,7 +1278,7 @@ function filterItems(items, query) {
1249
1278
  const lower = query.toLowerCase();
1250
1279
  return items.filter((i) => i.value.toLowerCase().includes(lower));
1251
1280
  }
1252
- function buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional = false) {
1281
+ function buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional = false, excludedPatterns = /* @__PURE__ */ new Set()) {
1253
1282
  const lines = [];
1254
1283
  const end = Math.min(filtered.length, scrollOffset + MAX_VISIBLE2);
1255
1284
  for (let i = scrollOffset; i < end; i++) {
@@ -1265,7 +1294,7 @@ function buildList2(filtered, cursor, scrollOffset, selected, filter, customPatt
1265
1294
  const allItems = [...filtered];
1266
1295
  const isNewPattern = trimmed.length > 0 && !allItems.some((i) => i.value === trimmed) && !customPatterns.includes(trimmed);
1267
1296
  if (isNewPattern) {
1268
- const err = validateBranchPattern(trimmed);
1297
+ const err = validateBranchPattern(trimmed) ?? (excludedPatterns.has(trimmed) ? "Already used for automatic translation" : null);
1269
1298
  const icon = addCursor ? grn2("\u25FB") : dim2("\u25FB");
1270
1299
  const label = err ? `${ylw2("+")} ${dim2(`"${trimmed}" \u2014 ${err}`)}` : `${grn2("+")} Add "${trimmed}" as branch pattern`;
1271
1300
  lines.push(`${cyan2(S_BAR2)} ${icon} ${label}`);
@@ -1284,6 +1313,7 @@ function buildList2(filtered, cursor, scrollOffset, selected, filter, customPatt
1284
1313
  async function filterableBranchSelect(params) {
1285
1314
  const { message, branches, defaultBranch } = params;
1286
1315
  const optional = params.optional ?? false;
1316
+ const excludedSet = new Set(params.excludedPatterns ?? []);
1287
1317
  let filter = "";
1288
1318
  let cursor = 0;
1289
1319
  let scrollOffset = 0;
@@ -1331,7 +1361,7 @@ ${symbol2(this.state)} ${message}
1331
1361
  return [
1332
1362
  hdr.trimEnd(),
1333
1363
  `${ylw2(S_BAR2)} ${dim2("/")} ${hint}`,
1334
- buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional),
1364
+ buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional, excludedSet),
1335
1365
  `${ylw2(S_BAR_END2)} ${ylw2(this.error)}`,
1336
1366
  ""
1337
1367
  ].join("\n");
@@ -1339,7 +1369,7 @@ ${symbol2(this.state)} ${message}
1339
1369
  return [
1340
1370
  hdr.trimEnd(),
1341
1371
  `${cyan2(S_BAR2)} ${dim2("/")} ${hint}`,
1342
- buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional),
1372
+ buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional, excludedSet),
1343
1373
  `${cyan2(S_BAR_END2)}`,
1344
1374
  ""
1345
1375
  ].join("\n");
@@ -1380,7 +1410,7 @@ ${symbol2(this.state)} ${message}
1380
1410
  case "space":
1381
1411
  if (addCursor) {
1382
1412
  const t = filter.trim();
1383
- const err = validateBranchPattern(t);
1413
+ const err = validateBranchPattern(t) ?? (excludedSet.has(t) ? "Already used for automatic translation" : null);
1384
1414
  if (!err) {
1385
1415
  customPatterns.push(t);
1386
1416
  selected.add(t);
@@ -1441,10 +1471,10 @@ async function runProjectCreate(params) {
1441
1471
  }
1442
1472
  const languageOptions = buildLanguageOptions(rawLocales);
1443
1473
  const localeOptions = buildLocaleOptions(rawLocales);
1444
- let scopePath;
1445
- if (params.defaultScopePath) {
1446
- scopePath = params.defaultScopePath;
1447
- p3.log.success(`App directory: ${chalk4.bold(scopePath)}`);
1474
+ let appDir;
1475
+ if (params.defaultAppDir) {
1476
+ appDir = params.defaultAppDir;
1477
+ p3.log.success(`App directory: ${chalk4.bold(appDir)}`);
1448
1478
  } else {
1449
1479
  const rawScope = await p3.text({
1450
1480
  message: "App directory (leave blank for the entire repo)",
@@ -1458,7 +1488,7 @@ async function runProjectCreate(params) {
1458
1488
  }
1459
1489
  });
1460
1490
  if (p3.isCancel(rawScope)) return null;
1461
- scopePath = (rawScope ?? "").trim();
1491
+ appDir = (rawScope ?? "").trim();
1462
1492
  }
1463
1493
  const sourceLocale = await searchSelectLocale(
1464
1494
  languageOptions,
@@ -1482,7 +1512,7 @@ async function runProjectCreate(params) {
1482
1512
  let initial = initialBranches;
1483
1513
  while (pushBranches.length === 0) {
1484
1514
  const result = await filterableBranchSelect({
1485
- message: "Translate on push \u2014 which branches?",
1515
+ message: "Which branches should trigger translations?",
1486
1516
  branches: detected.branches,
1487
1517
  defaultBranch: detected.defaultBranch,
1488
1518
  initialValues: initial
@@ -1496,59 +1526,15 @@ async function runProjectCreate(params) {
1496
1526
  }
1497
1527
  }
1498
1528
  }
1499
- const prResult = await filterableBranchSelect({
1500
- message: "Translate on pull requests \u2014 which branches? (optional)",
1501
- branches: detected.branches,
1502
- defaultBranch: detected.defaultBranch,
1503
- initialValues: [],
1504
- optional: true
1505
- });
1506
- if (prResult === null) return null;
1507
- const prBranches = prResult;
1508
- const autoSet = /* @__PURE__ */ new Set([...pushBranches, ...prBranches]);
1509
- const manualResult = await filterableBranchSelect({
1510
- message: `Manual-only branches \u2014 translate via \`vocoder sync\` only (optional)`,
1511
- branches: detected.branches.filter((b) => !autoSet.has(b)),
1512
- defaultBranch: detected.defaultBranch,
1513
- initialValues: [],
1514
- optional: true
1515
- });
1516
- if (manualResult === null) return null;
1517
- const manualBranches = manualResult.filter((b) => {
1518
- if (autoSet.has(b)) {
1519
- p3.log.warn(`"${b}" is already configured for automatic translation \u2014 skipping from manual.`);
1520
- return false;
1521
- }
1522
- return true;
1523
- });
1524
- if (pushBranches.length === 0 && prBranches.length === 0 && manualBranches.length === 0) {
1525
- p3.log.error("At least one branch must be configured.");
1526
- return null;
1527
- }
1528
- const triggerMap = /* @__PURE__ */ new Map();
1529
- for (const b of pushBranches) {
1530
- if (!triggerMap.has(b)) triggerMap.set(b, /* @__PURE__ */ new Set());
1531
- triggerMap.get(b).add("push");
1532
- }
1533
- for (const b of prBranches) {
1534
- if (!triggerMap.has(b)) triggerMap.set(b, /* @__PURE__ */ new Set());
1535
- triggerMap.get(b).add("pull_request");
1536
- }
1537
- for (const b of manualBranches) {
1538
- triggerMap.set(b, /* @__PURE__ */ new Set(["manual"]));
1539
- }
1540
- const branchTriggers = Array.from(triggerMap.entries()).map(([pattern, triggers]) => ({
1541
- pattern,
1542
- triggers: Array.from(triggers)
1543
- }));
1529
+ const targetBranches = pushBranches;
1544
1530
  try {
1545
1531
  const result = await api.createProject(userToken, {
1546
1532
  organizationId,
1547
1533
  name: projectName,
1548
1534
  sourceLocale,
1549
1535
  targetLocales,
1550
- branchTriggers,
1551
- scopePaths: scopePath ? [scopePath] : [],
1536
+ targetBranches,
1537
+ appDirs: appDir ? [appDir] : [],
1552
1538
  repoCanonical
1553
1539
  });
1554
1540
  p3.log.success(`Project ${chalk4.bold(result.projectName)} created!`);
@@ -1561,7 +1547,7 @@ async function runProjectCreate(params) {
1561
1547
  }
1562
1548
  async function runProjectAppCreate(params) {
1563
1549
  const { api, userToken, projectId, projectName, repoCanonical } = params;
1564
- const existingScopes = new Set(params.existingApps.map((a) => a.scopePath));
1550
+ const existingScopes = new Set(params.existingApps.map((a) => a.appDir));
1565
1551
  let rawLocales;
1566
1552
  try {
1567
1553
  rawLocales = await api.listLocales(userToken);
@@ -1571,20 +1557,20 @@ async function runProjectAppCreate(params) {
1571
1557
  }
1572
1558
  const languageOptions = buildLanguageOptions(rawLocales);
1573
1559
  const localeOptions = buildLocaleOptions(rawLocales);
1574
- let scopePath;
1575
- if (params.defaultScopePath && !existingScopes.has(params.defaultScopePath)) {
1576
- scopePath = params.defaultScopePath;
1577
- p3.log.success(`App directory: ${chalk4.bold(scopePath)}`);
1560
+ let appDir;
1561
+ if (params.defaultAppDir && !existingScopes.has(params.defaultAppDir)) {
1562
+ appDir = params.defaultAppDir;
1563
+ p3.log.success(`App directory: ${chalk4.bold(appDir)}`);
1578
1564
  } else {
1579
1565
  if (params.existingApps.length > 0) {
1580
- const configuredList = params.existingApps.map((a) => chalk4.dim(a.scopePath || "(entire repo)")).join(", ");
1566
+ const configuredList = params.existingApps.map((a) => chalk4.dim(a.appDir || "(entire repo)")).join(", ");
1581
1567
  p3.log.info(`Already configured: ${configuredList}`);
1582
1568
  }
1583
1569
  const hasWholeRepoApp = existingScopes.has("");
1584
1570
  const rawScope = await p3.text({
1585
1571
  message: "App directory for this new app",
1586
1572
  placeholder: "e.g. apps/backend",
1587
- initialValue: params.defaultScopePath ?? "",
1573
+ initialValue: params.defaultAppDir ?? "",
1588
1574
  validate(value) {
1589
1575
  const v = value.trim();
1590
1576
  if (!v && hasWholeRepoApp) return "This project already covers the entire repo.";
@@ -1595,7 +1581,7 @@ async function runProjectAppCreate(params) {
1595
1581
  }
1596
1582
  });
1597
1583
  if (p3.isCancel(rawScope)) return null;
1598
- scopePath = (rawScope ?? "").trim();
1584
+ appDir = (rawScope ?? "").trim();
1599
1585
  }
1600
1586
  const sourceLocale = await searchSelectLocale(
1601
1587
  languageOptions,
@@ -1618,7 +1604,7 @@ async function runProjectAppCreate(params) {
1618
1604
  let initial = [detectedApp.defaultBranch];
1619
1605
  while (appPushBranches.length === 0) {
1620
1606
  const result = await filterableBranchSelect({
1621
- message: "Translate on push \u2014 which branches?",
1607
+ message: "Which branches should trigger translations?",
1622
1608
  branches: detectedApp.branches,
1623
1609
  defaultBranch: detectedApp.defaultBranch,
1624
1610
  initialValues: initial
@@ -1632,64 +1618,25 @@ async function runProjectAppCreate(params) {
1632
1618
  }
1633
1619
  }
1634
1620
  }
1635
- const appPrResult = await filterableBranchSelect({
1636
- message: "Translate on pull requests \u2014 which branches? (optional)",
1637
- branches: detectedApp.branches,
1638
- defaultBranch: detectedApp.defaultBranch,
1639
- initialValues: [],
1640
- optional: true
1641
- });
1642
- if (appPrResult === null) return null;
1643
- const appAutoSet = /* @__PURE__ */ new Set([...appPushBranches, ...appPrResult]);
1644
- const appManualResult = await filterableBranchSelect({
1645
- message: "Manual-only branches (optional)",
1646
- branches: detectedApp.branches.filter((b) => !appAutoSet.has(b)),
1647
- defaultBranch: detectedApp.defaultBranch,
1648
- initialValues: [],
1649
- optional: true
1650
- });
1651
- if (appManualResult === null) return null;
1652
- const appManualBranches = appManualResult.filter((b) => {
1653
- if (appAutoSet.has(b)) {
1654
- p3.log.warn(`"${b}" is already configured for automatic translation \u2014 skipping from manual.`);
1655
- return false;
1656
- }
1657
- return true;
1658
- });
1659
- const appTriggerMap = /* @__PURE__ */ new Map();
1660
- for (const b of appPushBranches) {
1661
- if (!appTriggerMap.has(b)) appTriggerMap.set(b, /* @__PURE__ */ new Set());
1662
- appTriggerMap.get(b).add("push");
1663
- }
1664
- for (const b of appPrResult) {
1665
- if (!appTriggerMap.has(b)) appTriggerMap.set(b, /* @__PURE__ */ new Set());
1666
- appTriggerMap.get(b).add("pull_request");
1667
- }
1668
- for (const b of appManualBranches) {
1669
- appTriggerMap.set(b, /* @__PURE__ */ new Set(["manual"]));
1670
- }
1671
- const branchTriggers = Array.from(appTriggerMap.entries()).map(([pattern, triggers]) => ({
1672
- pattern,
1673
- triggers: Array.from(triggers)
1674
- }));
1621
+ const targetBranches = appPushBranches;
1675
1622
  try {
1676
1623
  const result = await api.createProjectApp(userToken, {
1677
1624
  projectId,
1678
- scopePath,
1625
+ appDir,
1679
1626
  sourceLocale,
1680
1627
  targetLocales,
1681
- branchTriggers,
1628
+ targetBranches,
1682
1629
  repoCanonical: repoCanonical ?? ""
1683
1630
  });
1684
- p3.log.success(`App ${chalk4.bold(scopePath)} added to ${chalk4.bold(projectName)}!`);
1631
+ p3.log.success(`App ${chalk4.bold(appDir)} added to ${chalk4.bold(projectName)}!`);
1685
1632
  return {
1686
1633
  projectId: result.projectId,
1687
1634
  projectName: result.projectName,
1688
1635
  apiKey: result.apiKey,
1689
- scopePath: result.scopePath,
1636
+ appDir: result.appDir,
1690
1637
  sourceLocale,
1691
1638
  targetLocales,
1692
- branchTriggers
1639
+ targetBranches
1693
1640
  };
1694
1641
  } catch (error) {
1695
1642
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -1799,9 +1746,7 @@ function printPlanLimitMessage(apiUrl, message) {
1799
1746
  p5.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
1800
1747
  }
1801
1748
  function runScaffold(params) {
1802
- const { projectName, organizationName, sourceLocale, branchTriggers } = params;
1803
- p5.log.info(`Project: ${chalk6.bold(projectName)}`);
1804
- p5.log.info(`Workspace: ${chalk6.bold(organizationName)}`);
1749
+ const { sourceLocale, targetBranches } = params;
1805
1750
  const detection = detectLocalEcosystem();
1806
1751
  if (detection.ecosystem) {
1807
1752
  const frameworkLabel = detection.framework ?? detection.ecosystem;
@@ -1828,7 +1773,7 @@ function runScaffold(params) {
1828
1773
  framework: detection.framework,
1829
1774
  ecosystem: detection.ecosystem,
1830
1775
  sourceLocale,
1831
- branchTriggers
1776
+ targetBranches
1832
1777
  });
1833
1778
  let stepNum = 1;
1834
1779
  if (snippets.pluginStep) {
@@ -2037,7 +1982,7 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
2037
1982
  }
2038
1983
  async function init(options = {}) {
2039
1984
  const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
2040
- p5.intro("Vocoder Setup");
1985
+ p5.intro(chalk6.bold("Vocoder Setup"));
2041
1986
  try {
2042
1987
  const gitContext = resolveGitContext();
2043
1988
  const identity = gitContext.identity;
@@ -2053,28 +1998,43 @@ async function init(options = {}) {
2053
1998
  const anonApi = new VocoderAPI({ apiUrl, apiKey: "" });
2054
1999
  const lookup = await anonApi.lookupProjectByRepo({
2055
2000
  repoCanonical: identity.repoCanonical,
2056
- scopePath: identity.repoScopePath
2001
+ appDir: identity.repoAppDir
2057
2002
  });
2058
2003
  if (lookup.exactMatch) {
2059
2004
  const { exactMatch } = lookup;
2060
- runScaffold({
2061
- projectName: exactMatch.projectName,
2062
- organizationName: exactMatch.organizationName,
2063
- sourceLocale: exactMatch.sourceLocale ?? "en",
2064
- branchTriggers: exactMatch.branchTriggers ?? [{ pattern: "main", triggers: ["push"] }]
2005
+ p5.log.success(`Project: ${chalk6.bold(exactMatch.projectName)}`);
2006
+ p5.log.info(`Branches: ${chalk6.cyan((exactMatch.targetBranches ?? ["main"]).join(", "))}`);
2007
+ const needsKey = await p5.confirm({
2008
+ message: "Need to regenerate your API key?"
2065
2009
  });
2010
+ if (!p5.isCancel(needsKey) && needsKey) {
2011
+ const anonApi2 = new VocoderAPI({ apiUrl, apiKey: "" });
2012
+ const authResult = await runAuthFlow(
2013
+ anonApi2,
2014
+ options,
2015
+ /* reauth */
2016
+ true
2017
+ );
2018
+ if (!authResult) return 1;
2019
+ const spinner4 = p5.spinner();
2020
+ spinner4.start("Generating new API key...");
2021
+ try {
2022
+ const { apiKey } = await anonApi2.regenerateProjectApiKey(authResult.token, exactMatch.projectId);
2023
+ spinner4.stop("New API key generated");
2024
+ printMcpSetup(apiKey);
2025
+ } catch {
2026
+ spinner4.stop("Failed to generate key");
2027
+ p5.log.error("Could not generate API key. Try again or generate one from the dashboard.");
2028
+ return 1;
2029
+ }
2030
+ }
2066
2031
  p5.outro("Vocoder is already set up for this repository.");
2067
2032
  return 0;
2068
2033
  }
2069
2034
  if (lookup.hasWholeRepoApp) {
2070
- const wholeRepo = lookup.existingApps.find((a) => a.scopePath === "");
2035
+ const wholeRepo = lookup.existingApps.find((a) => a.appDir === "");
2071
2036
  if (wholeRepo) {
2072
- runScaffold({
2073
- projectName: wholeRepo.projectName,
2074
- organizationName: wholeRepo.organizationName,
2075
- sourceLocale: "en",
2076
- branchTriggers: [{ pattern: "main", triggers: ["push"] }]
2077
- });
2037
+ p5.log.success(`Project: ${chalk6.bold(wholeRepo.projectName)}`);
2078
2038
  p5.outro("Vocoder is already set up for this repository.");
2079
2039
  return 0;
2080
2040
  }
@@ -2090,7 +2050,6 @@ async function init(options = {}) {
2090
2050
  let userEmail;
2091
2051
  let userName;
2092
2052
  let authOrganizationId;
2093
- let authDiscoveryReady = false;
2094
2053
  const stored = readAuthData();
2095
2054
  if (stored && stored.apiUrl === apiUrl) {
2096
2055
  const verified = await verifyStoredToken(api, stored.token);
@@ -2118,7 +2077,6 @@ async function init(options = {}) {
2118
2077
  userEmail = authResult.email;
2119
2078
  userName = authResult.name;
2120
2079
  authOrganizationId = authResult.organizationId;
2121
- authDiscoveryReady = authResult.discoveryReady ?? false;
2122
2080
  writeAuthData({
2123
2081
  token: userToken,
2124
2082
  apiUrl,
@@ -2360,7 +2318,7 @@ async function init(options = {}) {
2360
2318
  if (repoProjectId && repoProjectName && existingAppsForRepo.length > 0) {
2361
2319
  p5.log.info(
2362
2320
  `${chalk6.bold(repoProjectName)} is already set up for this repo.
2363
- Configured apps: ${existingAppsForRepo.map((a) => chalk6.cyan(a.scopePath || "(entire repo)")).join(", ")}`
2321
+ Configured apps: ${existingAppsForRepo.map((a) => chalk6.cyan(a.appDir || "(entire repo)")).join(", ")}`
2364
2322
  );
2365
2323
  const appResult = await runProjectAppCreate({
2366
2324
  api,
@@ -2369,7 +2327,7 @@ async function init(options = {}) {
2369
2327
  projectName: repoProjectName,
2370
2328
  organizationName: selectedWorkspaceName,
2371
2329
  repoCanonical: identity?.repoCanonical,
2372
- defaultScopePath: identity?.repoScopePath,
2330
+ defaultAppDir: identity?.repoAppDir,
2373
2331
  existingApps: existingAppsForRepo
2374
2332
  });
2375
2333
  if (!appResult) {
@@ -2377,10 +2335,8 @@ async function init(options = {}) {
2377
2335
  return 1;
2378
2336
  }
2379
2337
  runScaffold({
2380
- projectName: appResult.projectName,
2381
- organizationName: selectedWorkspaceName,
2382
2338
  sourceLocale: appResult.sourceLocale,
2383
- branchTriggers: appResult.branchTriggers
2339
+ targetBranches: appResult.targetBranches
2384
2340
  });
2385
2341
  p5.outro("You're all set.");
2386
2342
  return 0;
@@ -2439,7 +2395,7 @@ async function init(options = {}) {
2439
2395
  projectName: chosen.name,
2440
2396
  organizationName: selectedWorkspaceName,
2441
2397
  repoCanonical: identity?.repoCanonical,
2442
- defaultScopePath: identity?.repoScopePath,
2398
+ defaultAppDir: identity?.repoAppDir,
2443
2399
  existingApps: []
2444
2400
  });
2445
2401
  if (!appResult) {
@@ -2447,10 +2403,8 @@ async function init(options = {}) {
2447
2403
  return 1;
2448
2404
  }
2449
2405
  runScaffold({
2450
- projectName: appResult.projectName,
2451
- organizationName: selectedWorkspaceName,
2452
2406
  sourceLocale: appResult.sourceLocale,
2453
- branchTriggers: appResult.branchTriggers
2407
+ targetBranches: appResult.targetBranches
2454
2408
  });
2455
2409
  p5.outro("You're all set.");
2456
2410
  return 0;
@@ -2465,7 +2419,7 @@ async function init(options = {}) {
2465
2419
  defaultSourceLocale: "en",
2466
2420
  repoCanonical: identity?.repoCanonical,
2467
2421
  defaultBranches: ["main"],
2468
- defaultScopePath: identity?.repoScopePath
2422
+ defaultAppDir: identity?.repoAppDir
2469
2423
  });
2470
2424
  if (!projectResult) {
2471
2425
  p5.log.error("Project creation failed. Run `vocoder init` again.");
@@ -2484,10 +2438,8 @@ Translations won't run automatically until you grant access.
2484
2438
  );
2485
2439
  }
2486
2440
  runScaffold({
2487
- projectName: projectResult.projectName,
2488
- organizationName: selectedWorkspaceName,
2489
2441
  sourceLocale: projectResult.sourceLocale,
2490
- branchTriggers: projectResult.branchTriggers
2442
+ targetBranches: projectResult.targetBranches
2491
2443
  });
2492
2444
  printMcpSetup(projectResult.apiKey);
2493
2445
  p5.outro("You're all set.");
@@ -2606,8 +2558,11 @@ function validateLocalConfig(config) {
2606
2558
  if (!config.apiKey || config.apiKey.length === 0) {
2607
2559
  throw new Error("VOCODER_API_KEY is required. Set it in your .env file.");
2608
2560
  }
2609
- if (!config.apiKey.startsWith("vc_")) {
2610
- throw new Error("Invalid API key format. Expected format: vc_...");
2561
+ if (!config.apiKey.startsWith("vcp_")) {
2562
+ if (config.apiKey.startsWith("vco_") || config.apiKey.startsWith("vcu_")) {
2563
+ throw new Error("VOCODER_API_KEY must be a project-scoped key (starts with vcp_). Got an org or user key.");
2564
+ }
2565
+ throw new Error("Invalid API key format. Expected a project API key starting with vcp_.");
2611
2566
  }
2612
2567
  if (!config.apiUrl || !config.apiUrl.startsWith("http")) {
2613
2568
  throw new Error("Invalid API URL");
@@ -2615,7 +2570,7 @@ function validateLocalConfig(config) {
2615
2570
  }
2616
2571
  async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2617
2572
  const configSources = {
2618
- extractionPattern: "default",
2573
+ includePattern: "default",
2619
2574
  excludePattern: "default",
2620
2575
  apiKey: "environment",
2621
2576
  apiUrl: "default",
@@ -2624,29 +2579,53 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2624
2579
  noFallback: "default"
2625
2580
  };
2626
2581
  const defaults = {
2627
- extractionPattern: ["src/**/*.{tsx,jsx,ts,js}"],
2628
- excludePattern: [],
2582
+ includePattern: ["**/*.{tsx,jsx,ts,js}"],
2583
+ excludePattern: [
2584
+ "**/node_modules/**",
2585
+ "**/.next/**",
2586
+ "**/.nuxt/**",
2587
+ "**/.svelte-kit/**",
2588
+ "**/.output/**",
2589
+ "**/dist/**",
2590
+ "**/build/**",
2591
+ "**/out/**",
2592
+ "**/.vite/**",
2593
+ "**/.turbo/**",
2594
+ "**/coverage/**",
2595
+ "**/.cache/**",
2596
+ "**/*.min.js",
2597
+ "**/*.min.ts",
2598
+ "**/__generated__/**",
2599
+ "**/*.test.*",
2600
+ "**/*.spec.*",
2601
+ "**/*.stories.*",
2602
+ "**/__tests__/**"
2603
+ ],
2629
2604
  apiUrl: "https://vocoder.app"
2630
2605
  };
2631
- const envExtractionPattern = process.env.VOCODER_EXTRACTION_PATTERN;
2606
+ const envExtractionPattern = process.env.VOCODER_INCLUDE_PATTERN;
2607
+ const envExcludePattern = process.env.VOCODER_EXCLUDE_PATTERN;
2632
2608
  const envApiUrl = process.env.VOCODER_API_URL;
2633
2609
  const envSyncMode = process.env.VOCODER_SYNC_MODE;
2634
2610
  const envSyncMaxWaitMs = process.env.VOCODER_SYNC_MAX_WAIT_MS;
2635
2611
  const envSyncNoFallback = process.env.VOCODER_SYNC_NO_FALLBACK;
2636
- let extractionPattern;
2612
+ let includePattern;
2637
2613
  if (cliOptions.include && cliOptions.include.length > 0) {
2638
- extractionPattern = cliOptions.include;
2639
- configSources.extractionPattern = "CLI flag";
2614
+ includePattern = cliOptions.include;
2615
+ configSources.includePattern = "CLI flag";
2640
2616
  } else if (envExtractionPattern) {
2641
- extractionPattern = [envExtractionPattern];
2642
- configSources.extractionPattern = "environment";
2617
+ includePattern = [envExtractionPattern];
2618
+ configSources.includePattern = "environment";
2643
2619
  } else {
2644
- extractionPattern = defaults.extractionPattern;
2620
+ includePattern = defaults.includePattern;
2645
2621
  }
2646
2622
  let excludePattern;
2647
2623
  if (cliOptions.exclude && cliOptions.exclude.length > 0) {
2648
2624
  excludePattern = cliOptions.exclude;
2649
2625
  configSources.excludePattern = "CLI flag";
2626
+ } else if (envExcludePattern) {
2627
+ excludePattern = envExcludePattern.split(",").map((p9) => p9.trim()).filter(Boolean);
2628
+ configSources.excludePattern = "environment";
2650
2629
  } else {
2651
2630
  excludePattern = defaults.excludePattern;
2652
2631
  }
@@ -2692,7 +2671,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2692
2671
  }
2693
2672
  if (verbose) {
2694
2673
  console.log(chalk7.dim("\n Configuration sources:"));
2695
- console.log(chalk7.dim(` Include patterns: ${configSources.extractionPattern}`));
2674
+ console.log(chalk7.dim(` Include patterns: ${configSources.includePattern}`));
2696
2675
  if (excludePattern.length > 0) {
2697
2676
  console.log(chalk7.dim(` Exclude patterns: ${configSources.excludePattern}`));
2698
2677
  }
@@ -2707,7 +2686,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2707
2686
  `));
2708
2687
  }
2709
2688
  return {
2710
- extractionPattern,
2689
+ includePattern,
2711
2690
  excludePattern,
2712
2691
  apiKey,
2713
2692
  apiUrl,
@@ -2721,6 +2700,20 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2721
2700
  // src/commands/sync.ts
2722
2701
  import chalk8 from "chalk";
2723
2702
  import { join as join2 } from "path";
2703
+ function computeStringsHash(texts) {
2704
+ const sorted = [...texts].sort();
2705
+ return createHash("sha256").update(sorted.join("\0")).digest("hex").slice(0, 16);
2706
+ }
2707
+ function readCachedStringsHash(projectRoot, branch) {
2708
+ const filePath = getCacheFilePath(projectRoot, branch);
2709
+ if (!existsSync(filePath)) return null;
2710
+ try {
2711
+ const raw = JSON.parse(readFileSync2(filePath, "utf-8"));
2712
+ if (isRecord(raw) && typeof raw.stringsHash === "string") return raw.stringsHash;
2713
+ } catch {
2714
+ }
2715
+ return null;
2716
+ }
2724
2717
  function isRecord(value) {
2725
2718
  return typeof value === "object" && value !== null && !Array.isArray(value);
2726
2719
  }
@@ -2765,10 +2758,8 @@ function parseTranslations(value) {
2765
2758
  return Object.keys(translations).length > 0 ? translations : null;
2766
2759
  }
2767
2760
  function getCacheFilePath(projectRoot, branch) {
2768
- const slug = branch.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
2769
2761
  const branchHash = createHash("sha1").update(branch).digest("hex").slice(0, 12);
2770
- const filename = `${slug || "branch"}-${branchHash}.json`;
2771
- return join2(projectRoot, "node_modules", ".vocoder", "cache", "sync", filename);
2762
+ return join2(projectRoot, "node_modules", ".vocoder", "cache", "sync", `${branchHash}.json`);
2772
2763
  }
2773
2764
  function readLocalSnapshotCache(params) {
2774
2765
  const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
@@ -2813,6 +2804,7 @@ function writeLocalSnapshotCache(params) {
2813
2804
  sourceLocale: params.sourceLocale,
2814
2805
  targetLocales: params.targetLocales,
2815
2806
  savedAt: (/* @__PURE__ */ new Date()).toISOString(),
2807
+ ...params.stringsHash ? { stringsHash: params.stringsHash } : {},
2816
2808
  ...params.snapshotBatchId ? { snapshotBatchId: params.snapshotBatchId } : {},
2817
2809
  ...params.completedAt ? { completedAt: params.completedAt } : {},
2818
2810
  ...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
@@ -2994,7 +2986,7 @@ async function sync(options = {}) {
2994
2986
  const config = {
2995
2987
  ...localConfig,
2996
2988
  ...apiConfig,
2997
- extractionPattern: mergedConfig.extractionPattern,
2989
+ includePattern: mergedConfig.includePattern,
2998
2990
  excludePattern: mergedConfig.excludePattern,
2999
2991
  timeout: waitTimeoutMs
3000
2992
  };
@@ -3008,11 +3000,11 @@ async function sync(options = {}) {
3008
3000
  p7.outro("");
3009
3001
  return 0;
3010
3002
  }
3011
- const patternsDisplay = Array.isArray(config.extractionPattern) ? config.extractionPattern.join(", ") : config.extractionPattern;
3003
+ const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
3012
3004
  spinner4.start(`Extracting strings from ${patternsDisplay}`);
3013
3005
  const extractor = new StringExtractor();
3014
3006
  const extractedStrings = await extractor.extractFromProject(
3015
- config.extractionPattern,
3007
+ config.includePattern,
3016
3008
  projectRoot,
3017
3009
  config.excludePattern
3018
3010
  );
@@ -3053,6 +3045,7 @@ async function sync(options = {}) {
3053
3045
  "Could not detect git remote origin. Sync will continue without repo metadata."
3054
3046
  );
3055
3047
  }
3048
+ const commitSha = detectCommitSha() ?? void 0;
3056
3049
  const stringEntries = buildStringEntries(extractedStrings);
3057
3050
  const sourceStrings = stringEntries.map((entry) => entry.text);
3058
3051
  if (options.verbose && stringEntries.length !== extractedStrings.length) {
@@ -3060,6 +3053,15 @@ async function sync(options = {}) {
3060
3053
  `Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
3061
3054
  );
3062
3055
  }
3056
+ const currentHash = computeStringsHash(sourceStrings);
3057
+ if (!options.force) {
3058
+ const cachedHash = readCachedStringsHash(projectRoot, branch);
3059
+ if (cachedHash && cachedHash === currentHash) {
3060
+ const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
3061
+ p7.outro(`Up to date (${duration2}s)`);
3062
+ return 0;
3063
+ }
3064
+ }
3063
3065
  spinner4.start("Submitting strings to Vocoder API");
3064
3066
  const batchResponse = await api.submitTranslation(
3065
3067
  branch,
@@ -3070,7 +3072,7 @@ async function sync(options = {}) {
3070
3072
  requestedMaxWaitMs: waitTimeoutMs,
3071
3073
  clientRunId: randomUUID()
3072
3074
  },
3073
- repoIdentity ?? void 0
3075
+ repoIdentity ? { ...repoIdentity, commitSha } : { commitSha }
3074
3076
  );
3075
3077
  spinner4.stop(`Submitted to API - Batch ${chalk8.cyan(batchResponse.batchId)}`);
3076
3078
  const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
@@ -3204,6 +3206,7 @@ async function sync(options = {}) {
3204
3206
  targetLocales: config.targetLocales,
3205
3207
  translations: finalTranslations,
3206
3208
  localeMetadata: artifacts.localeMetadata,
3209
+ stringsHash: currentHash,
3207
3210
  snapshotBatchId: artifacts.snapshotBatchId ?? (artifacts.source === "fresh" ? batchResponse.batchId : batchResponse.latestCompletedBatchId),
3208
3211
  completedAt: artifacts.completedAt ?? (artifacts.source === "fresh" ? (/* @__PURE__ */ new Date()).toISOString() : null)
3209
3212
  });
@@ -3224,8 +3227,6 @@ async function sync(options = {}) {
3224
3227
  }
3225
3228
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
3226
3229
  p7.outro(`Sync complete! (${duration}s)`);
3227
- p7.log.info("Translations will be injected at build time by @vocoder/unplugin.");
3228
- p7.log.info("Just use <VocoderProvider> and <T> \u2014 no manual imports needed.");
3229
3230
  return 0;
3230
3231
  } catch (error) {
3231
3232
  spinner4.stop();