@vocoder/cli 0.1.15 → 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) {
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}`);
@@ -1274,11 +1303,17 @@ function buildList2(filtered, cursor, scrollOffset, selected, filter, customPatt
1274
1303
  }
1275
1304
  const hidden = filtered.length - (end - scrollOffset);
1276
1305
  if (hidden > 0) lines.push(dim2(`${S_BAR2} ${hidden} more`));
1277
- if (selected.size > 0) lines.push(dim2(`${S_BAR2} ${selected.size} selected \u2014 Enter to confirm`));
1306
+ if (selected.size > 0) {
1307
+ lines.push(dim2(`${S_BAR2} ${selected.size} selected \u2014 Enter to confirm`));
1308
+ } else if (optional) {
1309
+ lines.push(dim2(`${S_BAR2} Enter to skip`));
1310
+ }
1278
1311
  return lines.join("\n");
1279
1312
  }
1280
1313
  async function filterableBranchSelect(params) {
1281
1314
  const { message, branches, defaultBranch } = params;
1315
+ const optional = params.optional ?? false;
1316
+ const excludedSet = new Set(params.excludedPatterns ?? []);
1282
1317
  let filter = "";
1283
1318
  let cursor = 0;
1284
1319
  let scrollOffset = 0;
@@ -1305,7 +1340,7 @@ async function filterableBranchSelect(params) {
1305
1340
  const prompt = new Prompt2(
1306
1341
  {
1307
1342
  validate() {
1308
- if (selected.size === 0) return "At least one branch is required.";
1343
+ if (!optional && selected.size === 0) return "At least one branch is required.";
1309
1344
  return void 0;
1310
1345
  },
1311
1346
  render() {
@@ -1326,7 +1361,7 @@ ${symbol2(this.state)} ${message}
1326
1361
  return [
1327
1362
  hdr.trimEnd(),
1328
1363
  `${ylw2(S_BAR2)} ${dim2("/")} ${hint}`,
1329
- buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor),
1364
+ buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional, excludedSet),
1330
1365
  `${ylw2(S_BAR_END2)} ${ylw2(this.error)}`,
1331
1366
  ""
1332
1367
  ].join("\n");
@@ -1334,7 +1369,7 @@ ${symbol2(this.state)} ${message}
1334
1369
  return [
1335
1370
  hdr.trimEnd(),
1336
1371
  `${cyan2(S_BAR2)} ${dim2("/")} ${hint}`,
1337
- buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor),
1372
+ buildList2(filtered, cursor, scrollOffset, selected, filter, customPatterns, addCursor, optional, excludedSet),
1338
1373
  `${cyan2(S_BAR_END2)}`,
1339
1374
  ""
1340
1375
  ].join("\n");
@@ -1375,7 +1410,7 @@ ${symbol2(this.state)} ${message}
1375
1410
  case "space":
1376
1411
  if (addCursor) {
1377
1412
  const t = filter.trim();
1378
- const err = validateBranchPattern(t);
1413
+ const err = validateBranchPattern(t) ?? (excludedSet.has(t) ? "Already used for automatic translation" : null);
1379
1414
  if (!err) {
1380
1415
  customPatterns.push(t);
1381
1416
  selected.add(t);
@@ -1436,10 +1471,10 @@ async function runProjectCreate(params) {
1436
1471
  }
1437
1472
  const languageOptions = buildLanguageOptions(rawLocales);
1438
1473
  const localeOptions = buildLocaleOptions(rawLocales);
1439
- let scopePath;
1440
- if (params.defaultScopePath) {
1441
- scopePath = params.defaultScopePath;
1442
- 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)}`);
1443
1478
  } else {
1444
1479
  const rawScope = await p3.text({
1445
1480
  message: "App directory (leave blank for the entire repo)",
@@ -1453,7 +1488,7 @@ async function runProjectCreate(params) {
1453
1488
  }
1454
1489
  });
1455
1490
  if (p3.isCancel(rawScope)) return null;
1456
- scopePath = (rawScope ?? "").trim();
1491
+ appDir = (rawScope ?? "").trim();
1457
1492
  }
1458
1493
  const sourceLocale = await searchSelectLocale(
1459
1494
  languageOptions,
@@ -1472,12 +1507,12 @@ async function runProjectCreate(params) {
1472
1507
  }
1473
1508
  const detected = detectGitBranches();
1474
1509
  const initialBranches = params.defaultBranches?.length ? params.defaultBranches : [detected.defaultBranch];
1475
- let selectedBranches = [];
1510
+ let pushBranches = [];
1476
1511
  {
1477
1512
  let initial = initialBranches;
1478
- while (selectedBranches.length === 0) {
1513
+ while (pushBranches.length === 0) {
1479
1514
  const result = await filterableBranchSelect({
1480
- message: "Target branches",
1515
+ message: "Which branches should trigger translations?",
1481
1516
  branches: detected.branches,
1482
1517
  defaultBranch: detected.defaultBranch,
1483
1518
  initialValues: initial
@@ -1487,33 +1522,19 @@ async function runProjectCreate(params) {
1487
1522
  p3.log.warn("At least one branch is required. Please select at least one.");
1488
1523
  initial = [detected.defaultBranch];
1489
1524
  } else {
1490
- selectedBranches = result;
1525
+ pushBranches = result;
1491
1526
  }
1492
1527
  }
1493
1528
  }
1494
- const triggerChoice = await p3.select({
1495
- message: "When should translations run?",
1496
- options: [
1497
- { value: "push", label: "On push to target branches" },
1498
- { value: "pull_request", label: "On pull requests" },
1499
- { value: "push_and_pr", label: "On push and pull requests" },
1500
- { value: "manual", label: "Manual only", hint: "use vocoder sync or trigger from dashboard" }
1501
- ]
1502
- });
1503
- if (p3.isCancel(triggerChoice)) return null;
1504
- const triggersForBranch = triggerChoice === "push_and_pr" ? ["push", "pull_request"] : [triggerChoice];
1505
- const branchTriggers = selectedBranches.map((pattern) => ({
1506
- pattern,
1507
- triggers: triggersForBranch
1508
- }));
1529
+ const targetBranches = pushBranches;
1509
1530
  try {
1510
1531
  const result = await api.createProject(userToken, {
1511
1532
  organizationId,
1512
1533
  name: projectName,
1513
1534
  sourceLocale,
1514
1535
  targetLocales,
1515
- branchTriggers,
1516
- scopePaths: scopePath ? [scopePath] : [],
1536
+ targetBranches,
1537
+ appDirs: appDir ? [appDir] : [],
1517
1538
  repoCanonical
1518
1539
  });
1519
1540
  p3.log.success(`Project ${chalk4.bold(result.projectName)} created!`);
@@ -1526,7 +1547,7 @@ async function runProjectCreate(params) {
1526
1547
  }
1527
1548
  async function runProjectAppCreate(params) {
1528
1549
  const { api, userToken, projectId, projectName, repoCanonical } = params;
1529
- const existingScopes = new Set(params.existingApps.map((a) => a.scopePath));
1550
+ const existingScopes = new Set(params.existingApps.map((a) => a.appDir));
1530
1551
  let rawLocales;
1531
1552
  try {
1532
1553
  rawLocales = await api.listLocales(userToken);
@@ -1536,20 +1557,20 @@ async function runProjectAppCreate(params) {
1536
1557
  }
1537
1558
  const languageOptions = buildLanguageOptions(rawLocales);
1538
1559
  const localeOptions = buildLocaleOptions(rawLocales);
1539
- let scopePath;
1540
- if (params.defaultScopePath && !existingScopes.has(params.defaultScopePath)) {
1541
- scopePath = params.defaultScopePath;
1542
- 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)}`);
1543
1564
  } else {
1544
1565
  if (params.existingApps.length > 0) {
1545
- 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(", ");
1546
1567
  p3.log.info(`Already configured: ${configuredList}`);
1547
1568
  }
1548
1569
  const hasWholeRepoApp = existingScopes.has("");
1549
1570
  const rawScope = await p3.text({
1550
1571
  message: "App directory for this new app",
1551
1572
  placeholder: "e.g. apps/backend",
1552
- initialValue: params.defaultScopePath ?? "",
1573
+ initialValue: params.defaultAppDir ?? "",
1553
1574
  validate(value) {
1554
1575
  const v = value.trim();
1555
1576
  if (!v && hasWholeRepoApp) return "This project already covers the entire repo.";
@@ -1560,7 +1581,7 @@ async function runProjectAppCreate(params) {
1560
1581
  }
1561
1582
  });
1562
1583
  if (p3.isCancel(rawScope)) return null;
1563
- scopePath = (rawScope ?? "").trim();
1584
+ appDir = (rawScope ?? "").trim();
1564
1585
  }
1565
1586
  const sourceLocale = await searchSelectLocale(
1566
1587
  languageOptions,
@@ -1577,59 +1598,45 @@ async function runProjectAppCreate(params) {
1577
1598
  if (targetLocales.length === 0) {
1578
1599
  p3.log.warn("No target languages selected \u2014 you can add them later from the dashboard.");
1579
1600
  }
1580
- const triggerChoice = await p3.select({
1581
- message: "When should translations run?",
1582
- options: [
1583
- { value: "push", label: "On push to target branches" },
1584
- { value: "pull_request", label: "On pull requests" },
1585
- { value: "push_and_pr", label: "On push and pull requests" },
1586
- { value: "manual", label: "Manual only", hint: "use vocoder sync or trigger from dashboard" }
1587
- ]
1588
- });
1589
- if (p3.isCancel(triggerChoice)) return null;
1590
- const triggersForBranch = triggerChoice === "push_and_pr" ? ["push", "pull_request"] : [triggerChoice];
1591
- const detected = detectGitBranches();
1592
- let selectedBranches = [];
1601
+ const detectedApp = detectGitBranches();
1602
+ let appPushBranches = [];
1593
1603
  {
1594
- let initial = [detected.defaultBranch];
1595
- while (selectedBranches.length === 0) {
1604
+ let initial = [detectedApp.defaultBranch];
1605
+ while (appPushBranches.length === 0) {
1596
1606
  const result = await filterableBranchSelect({
1597
- message: "Target branches",
1598
- branches: detected.branches,
1599
- defaultBranch: detected.defaultBranch,
1607
+ message: "Which branches should trigger translations?",
1608
+ branches: detectedApp.branches,
1609
+ defaultBranch: detectedApp.defaultBranch,
1600
1610
  initialValues: initial
1601
1611
  });
1602
1612
  if (result === null) return null;
1603
1613
  if (result.length === 0) {
1604
1614
  p3.log.warn("At least one branch is required.");
1605
- initial = [detected.defaultBranch];
1615
+ initial = [detectedApp.defaultBranch];
1606
1616
  } else {
1607
- selectedBranches = result;
1617
+ appPushBranches = result;
1608
1618
  }
1609
1619
  }
1610
1620
  }
1611
- const branchTriggers = selectedBranches.map((pattern) => ({
1612
- pattern,
1613
- triggers: triggersForBranch
1614
- }));
1621
+ const targetBranches = appPushBranches;
1615
1622
  try {
1616
1623
  const result = await api.createProjectApp(userToken, {
1617
1624
  projectId,
1618
- scopePath,
1625
+ appDir,
1619
1626
  sourceLocale,
1620
1627
  targetLocales,
1621
- branchTriggers,
1628
+ targetBranches,
1622
1629
  repoCanonical: repoCanonical ?? ""
1623
1630
  });
1624
- 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)}!`);
1625
1632
  return {
1626
1633
  projectId: result.projectId,
1627
1634
  projectName: result.projectName,
1628
1635
  apiKey: result.apiKey,
1629
- scopePath: result.scopePath,
1636
+ appDir: result.appDir,
1630
1637
  sourceLocale,
1631
1638
  targetLocales,
1632
- branchTriggers
1639
+ targetBranches
1633
1640
  };
1634
1641
  } catch (error) {
1635
1642
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -1739,9 +1746,7 @@ function printPlanLimitMessage(apiUrl, message) {
1739
1746
  p5.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
1740
1747
  }
1741
1748
  function runScaffold(params) {
1742
- const { projectName, organizationName, sourceLocale, branchTriggers } = params;
1743
- p5.log.info(`Project: ${chalk6.bold(projectName)}`);
1744
- p5.log.info(`Workspace: ${chalk6.bold(organizationName)}`);
1749
+ const { sourceLocale, targetBranches } = params;
1745
1750
  const detection = detectLocalEcosystem();
1746
1751
  if (detection.ecosystem) {
1747
1752
  const frameworkLabel = detection.framework ?? detection.ecosystem;
@@ -1768,7 +1773,7 @@ function runScaffold(params) {
1768
1773
  framework: detection.framework,
1769
1774
  ecosystem: detection.ecosystem,
1770
1775
  sourceLocale,
1771
- branchTriggers
1776
+ targetBranches
1772
1777
  });
1773
1778
  let stepNum = 1;
1774
1779
  if (snippets.pluginStep) {
@@ -1977,7 +1982,7 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
1977
1982
  }
1978
1983
  async function init(options = {}) {
1979
1984
  const apiUrl = options.apiUrl || process.env.VOCODER_API_URL || "https://vocoder.app";
1980
- p5.intro("Vocoder Setup");
1985
+ p5.intro(chalk6.bold("Vocoder Setup"));
1981
1986
  try {
1982
1987
  const gitContext = resolveGitContext();
1983
1988
  const identity = gitContext.identity;
@@ -1993,28 +1998,43 @@ async function init(options = {}) {
1993
1998
  const anonApi = new VocoderAPI({ apiUrl, apiKey: "" });
1994
1999
  const lookup = await anonApi.lookupProjectByRepo({
1995
2000
  repoCanonical: identity.repoCanonical,
1996
- scopePath: identity.repoScopePath
2001
+ appDir: identity.repoAppDir
1997
2002
  });
1998
2003
  if (lookup.exactMatch) {
1999
2004
  const { exactMatch } = lookup;
2000
- runScaffold({
2001
- projectName: exactMatch.projectName,
2002
- organizationName: exactMatch.organizationName,
2003
- sourceLocale: exactMatch.sourceLocale ?? "en",
2004
- 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?"
2005
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
+ }
2006
2031
  p5.outro("Vocoder is already set up for this repository.");
2007
2032
  return 0;
2008
2033
  }
2009
2034
  if (lookup.hasWholeRepoApp) {
2010
- const wholeRepo = lookup.existingApps.find((a) => a.scopePath === "");
2035
+ const wholeRepo = lookup.existingApps.find((a) => a.appDir === "");
2011
2036
  if (wholeRepo) {
2012
- runScaffold({
2013
- projectName: wholeRepo.projectName,
2014
- organizationName: wholeRepo.organizationName,
2015
- sourceLocale: "en",
2016
- branchTriggers: [{ pattern: "main", triggers: ["push"] }]
2017
- });
2037
+ p5.log.success(`Project: ${chalk6.bold(wholeRepo.projectName)}`);
2018
2038
  p5.outro("Vocoder is already set up for this repository.");
2019
2039
  return 0;
2020
2040
  }
@@ -2030,7 +2050,6 @@ async function init(options = {}) {
2030
2050
  let userEmail;
2031
2051
  let userName;
2032
2052
  let authOrganizationId;
2033
- let authDiscoveryReady = false;
2034
2053
  const stored = readAuthData();
2035
2054
  if (stored && stored.apiUrl === apiUrl) {
2036
2055
  const verified = await verifyStoredToken(api, stored.token);
@@ -2058,7 +2077,6 @@ async function init(options = {}) {
2058
2077
  userEmail = authResult.email;
2059
2078
  userName = authResult.name;
2060
2079
  authOrganizationId = authResult.organizationId;
2061
- authDiscoveryReady = authResult.discoveryReady ?? false;
2062
2080
  writeAuthData({
2063
2081
  token: userToken,
2064
2082
  apiUrl,
@@ -2300,7 +2318,7 @@ async function init(options = {}) {
2300
2318
  if (repoProjectId && repoProjectName && existingAppsForRepo.length > 0) {
2301
2319
  p5.log.info(
2302
2320
  `${chalk6.bold(repoProjectName)} is already set up for this repo.
2303
- 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(", ")}`
2304
2322
  );
2305
2323
  const appResult = await runProjectAppCreate({
2306
2324
  api,
@@ -2309,7 +2327,7 @@ async function init(options = {}) {
2309
2327
  projectName: repoProjectName,
2310
2328
  organizationName: selectedWorkspaceName,
2311
2329
  repoCanonical: identity?.repoCanonical,
2312
- defaultScopePath: identity?.repoScopePath,
2330
+ defaultAppDir: identity?.repoAppDir,
2313
2331
  existingApps: existingAppsForRepo
2314
2332
  });
2315
2333
  if (!appResult) {
@@ -2317,10 +2335,8 @@ async function init(options = {}) {
2317
2335
  return 1;
2318
2336
  }
2319
2337
  runScaffold({
2320
- projectName: appResult.projectName,
2321
- organizationName: selectedWorkspaceName,
2322
2338
  sourceLocale: appResult.sourceLocale,
2323
- branchTriggers: appResult.branchTriggers
2339
+ targetBranches: appResult.targetBranches
2324
2340
  });
2325
2341
  p5.outro("You're all set.");
2326
2342
  return 0;
@@ -2379,7 +2395,7 @@ async function init(options = {}) {
2379
2395
  projectName: chosen.name,
2380
2396
  organizationName: selectedWorkspaceName,
2381
2397
  repoCanonical: identity?.repoCanonical,
2382
- defaultScopePath: identity?.repoScopePath,
2398
+ defaultAppDir: identity?.repoAppDir,
2383
2399
  existingApps: []
2384
2400
  });
2385
2401
  if (!appResult) {
@@ -2387,10 +2403,8 @@ async function init(options = {}) {
2387
2403
  return 1;
2388
2404
  }
2389
2405
  runScaffold({
2390
- projectName: appResult.projectName,
2391
- organizationName: selectedWorkspaceName,
2392
2406
  sourceLocale: appResult.sourceLocale,
2393
- branchTriggers: appResult.branchTriggers
2407
+ targetBranches: appResult.targetBranches
2394
2408
  });
2395
2409
  p5.outro("You're all set.");
2396
2410
  return 0;
@@ -2405,7 +2419,7 @@ async function init(options = {}) {
2405
2419
  defaultSourceLocale: "en",
2406
2420
  repoCanonical: identity?.repoCanonical,
2407
2421
  defaultBranches: ["main"],
2408
- defaultScopePath: identity?.repoScopePath
2422
+ defaultAppDir: identity?.repoAppDir
2409
2423
  });
2410
2424
  if (!projectResult) {
2411
2425
  p5.log.error("Project creation failed. Run `vocoder init` again.");
@@ -2424,10 +2438,8 @@ Translations won't run automatically until you grant access.
2424
2438
  );
2425
2439
  }
2426
2440
  runScaffold({
2427
- projectName: projectResult.projectName,
2428
- organizationName: selectedWorkspaceName,
2429
2441
  sourceLocale: projectResult.sourceLocale,
2430
- branchTriggers: projectResult.branchTriggers
2442
+ targetBranches: projectResult.targetBranches
2431
2443
  });
2432
2444
  printMcpSetup(projectResult.apiKey);
2433
2445
  p5.outro("You're all set.");
@@ -2546,8 +2558,11 @@ function validateLocalConfig(config) {
2546
2558
  if (!config.apiKey || config.apiKey.length === 0) {
2547
2559
  throw new Error("VOCODER_API_KEY is required. Set it in your .env file.");
2548
2560
  }
2549
- if (!config.apiKey.startsWith("vc_")) {
2550
- 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_.");
2551
2566
  }
2552
2567
  if (!config.apiUrl || !config.apiUrl.startsWith("http")) {
2553
2568
  throw new Error("Invalid API URL");
@@ -2555,7 +2570,7 @@ function validateLocalConfig(config) {
2555
2570
  }
2556
2571
  async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2557
2572
  const configSources = {
2558
- extractionPattern: "default",
2573
+ includePattern: "default",
2559
2574
  excludePattern: "default",
2560
2575
  apiKey: "environment",
2561
2576
  apiUrl: "default",
@@ -2564,29 +2579,53 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2564
2579
  noFallback: "default"
2565
2580
  };
2566
2581
  const defaults = {
2567
- extractionPattern: ["src/**/*.{tsx,jsx,ts,js}"],
2568
- 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
+ ],
2569
2604
  apiUrl: "https://vocoder.app"
2570
2605
  };
2571
- const envExtractionPattern = process.env.VOCODER_EXTRACTION_PATTERN;
2606
+ const envExtractionPattern = process.env.VOCODER_INCLUDE_PATTERN;
2607
+ const envExcludePattern = process.env.VOCODER_EXCLUDE_PATTERN;
2572
2608
  const envApiUrl = process.env.VOCODER_API_URL;
2573
2609
  const envSyncMode = process.env.VOCODER_SYNC_MODE;
2574
2610
  const envSyncMaxWaitMs = process.env.VOCODER_SYNC_MAX_WAIT_MS;
2575
2611
  const envSyncNoFallback = process.env.VOCODER_SYNC_NO_FALLBACK;
2576
- let extractionPattern;
2612
+ let includePattern;
2577
2613
  if (cliOptions.include && cliOptions.include.length > 0) {
2578
- extractionPattern = cliOptions.include;
2579
- configSources.extractionPattern = "CLI flag";
2614
+ includePattern = cliOptions.include;
2615
+ configSources.includePattern = "CLI flag";
2580
2616
  } else if (envExtractionPattern) {
2581
- extractionPattern = [envExtractionPattern];
2582
- configSources.extractionPattern = "environment";
2617
+ includePattern = [envExtractionPattern];
2618
+ configSources.includePattern = "environment";
2583
2619
  } else {
2584
- extractionPattern = defaults.extractionPattern;
2620
+ includePattern = defaults.includePattern;
2585
2621
  }
2586
2622
  let excludePattern;
2587
2623
  if (cliOptions.exclude && cliOptions.exclude.length > 0) {
2588
2624
  excludePattern = cliOptions.exclude;
2589
2625
  configSources.excludePattern = "CLI flag";
2626
+ } else if (envExcludePattern) {
2627
+ excludePattern = envExcludePattern.split(",").map((p9) => p9.trim()).filter(Boolean);
2628
+ configSources.excludePattern = "environment";
2590
2629
  } else {
2591
2630
  excludePattern = defaults.excludePattern;
2592
2631
  }
@@ -2632,7 +2671,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2632
2671
  }
2633
2672
  if (verbose) {
2634
2673
  console.log(chalk7.dim("\n Configuration sources:"));
2635
- console.log(chalk7.dim(` Include patterns: ${configSources.extractionPattern}`));
2674
+ console.log(chalk7.dim(` Include patterns: ${configSources.includePattern}`));
2636
2675
  if (excludePattern.length > 0) {
2637
2676
  console.log(chalk7.dim(` Exclude patterns: ${configSources.excludePattern}`));
2638
2677
  }
@@ -2647,7 +2686,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2647
2686
  `));
2648
2687
  }
2649
2688
  return {
2650
- extractionPattern,
2689
+ includePattern,
2651
2690
  excludePattern,
2652
2691
  apiKey,
2653
2692
  apiUrl,
@@ -2661,6 +2700,20 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
2661
2700
  // src/commands/sync.ts
2662
2701
  import chalk8 from "chalk";
2663
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
+ }
2664
2717
  function isRecord(value) {
2665
2718
  return typeof value === "object" && value !== null && !Array.isArray(value);
2666
2719
  }
@@ -2705,10 +2758,8 @@ function parseTranslations(value) {
2705
2758
  return Object.keys(translations).length > 0 ? translations : null;
2706
2759
  }
2707
2760
  function getCacheFilePath(projectRoot, branch) {
2708
- const slug = branch.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
2709
2761
  const branchHash = createHash("sha1").update(branch).digest("hex").slice(0, 12);
2710
- const filename = `${slug || "branch"}-${branchHash}.json`;
2711
- return join2(projectRoot, "node_modules", ".vocoder", "cache", "sync", filename);
2762
+ return join2(projectRoot, "node_modules", ".vocoder", "cache", "sync", `${branchHash}.json`);
2712
2763
  }
2713
2764
  function readLocalSnapshotCache(params) {
2714
2765
  const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
@@ -2753,6 +2804,7 @@ function writeLocalSnapshotCache(params) {
2753
2804
  sourceLocale: params.sourceLocale,
2754
2805
  targetLocales: params.targetLocales,
2755
2806
  savedAt: (/* @__PURE__ */ new Date()).toISOString(),
2807
+ ...params.stringsHash ? { stringsHash: params.stringsHash } : {},
2756
2808
  ...params.snapshotBatchId ? { snapshotBatchId: params.snapshotBatchId } : {},
2757
2809
  ...params.completedAt ? { completedAt: params.completedAt } : {},
2758
2810
  ...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
@@ -2934,7 +2986,7 @@ async function sync(options = {}) {
2934
2986
  const config = {
2935
2987
  ...localConfig,
2936
2988
  ...apiConfig,
2937
- extractionPattern: mergedConfig.extractionPattern,
2989
+ includePattern: mergedConfig.includePattern,
2938
2990
  excludePattern: mergedConfig.excludePattern,
2939
2991
  timeout: waitTimeoutMs
2940
2992
  };
@@ -2948,11 +3000,11 @@ async function sync(options = {}) {
2948
3000
  p7.outro("");
2949
3001
  return 0;
2950
3002
  }
2951
- const patternsDisplay = Array.isArray(config.extractionPattern) ? config.extractionPattern.join(", ") : config.extractionPattern;
3003
+ const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
2952
3004
  spinner4.start(`Extracting strings from ${patternsDisplay}`);
2953
3005
  const extractor = new StringExtractor();
2954
3006
  const extractedStrings = await extractor.extractFromProject(
2955
- config.extractionPattern,
3007
+ config.includePattern,
2956
3008
  projectRoot,
2957
3009
  config.excludePattern
2958
3010
  );
@@ -2993,6 +3045,7 @@ async function sync(options = {}) {
2993
3045
  "Could not detect git remote origin. Sync will continue without repo metadata."
2994
3046
  );
2995
3047
  }
3048
+ const commitSha = detectCommitSha() ?? void 0;
2996
3049
  const stringEntries = buildStringEntries(extractedStrings);
2997
3050
  const sourceStrings = stringEntries.map((entry) => entry.text);
2998
3051
  if (options.verbose && stringEntries.length !== extractedStrings.length) {
@@ -3000,6 +3053,15 @@ async function sync(options = {}) {
3000
3053
  `Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
3001
3054
  );
3002
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
+ }
3003
3065
  spinner4.start("Submitting strings to Vocoder API");
3004
3066
  const batchResponse = await api.submitTranslation(
3005
3067
  branch,
@@ -3010,7 +3072,7 @@ async function sync(options = {}) {
3010
3072
  requestedMaxWaitMs: waitTimeoutMs,
3011
3073
  clientRunId: randomUUID()
3012
3074
  },
3013
- repoIdentity ?? void 0
3075
+ repoIdentity ? { ...repoIdentity, commitSha } : { commitSha }
3014
3076
  );
3015
3077
  spinner4.stop(`Submitted to API - Batch ${chalk8.cyan(batchResponse.batchId)}`);
3016
3078
  const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
@@ -3144,6 +3206,7 @@ async function sync(options = {}) {
3144
3206
  targetLocales: config.targetLocales,
3145
3207
  translations: finalTranslations,
3146
3208
  localeMetadata: artifacts.localeMetadata,
3209
+ stringsHash: currentHash,
3147
3210
  snapshotBatchId: artifacts.snapshotBatchId ?? (artifacts.source === "fresh" ? batchResponse.batchId : batchResponse.latestCompletedBatchId),
3148
3211
  completedAt: artifacts.completedAt ?? (artifacts.source === "fresh" ? (/* @__PURE__ */ new Date()).toISOString() : null)
3149
3212
  });
@@ -3164,8 +3227,6 @@ async function sync(options = {}) {
3164
3227
  }
3165
3228
  const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
3166
3229
  p7.outro(`Sync complete! (${duration}s)`);
3167
- p7.log.info("Translations will be injected at build time by @vocoder/unplugin.");
3168
- p7.log.info("Just use <VocoderProvider> and <T> \u2014 no manual imports needed.");
3169
3230
  return 0;
3170
3231
  } catch (error) {
3171
3232
  spinner4.stop();