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