@vocoder/cli 0.11.0 → 0.12.1
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 +140 -4
- package/dist/bin.mjs +519 -207
- package/dist/bin.mjs.map +1 -1
- package/dist/{chunk-XF3KGGYQ.mjs → chunk-SR6SDGUI.mjs} +62 -4
- package/dist/{chunk-XF3KGGYQ.mjs.map → chunk-SR6SDGUI.mjs.map} +1 -1
- package/dist/lib.d.mts +57 -8
- package/dist/lib.mjs +3 -1
- package/dist/lib.mjs.map +1 -1
- package/package.json +3 -3
package/dist/bin.mjs
CHANGED
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
getSetupSnippets,
|
|
12
12
|
loadVocoderConfig,
|
|
13
13
|
readAuthData,
|
|
14
|
+
verifyStoredAuth,
|
|
14
15
|
writeAuthData
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-SR6SDGUI.mjs";
|
|
16
17
|
|
|
17
18
|
// src/bin.ts
|
|
18
19
|
import { Command } from "commander";
|
|
@@ -40,17 +41,21 @@ function writeVocoderConfig(options) {
|
|
|
40
41
|
const {
|
|
41
42
|
targetBranches = ["main"],
|
|
42
43
|
useTypeScript = true,
|
|
43
|
-
cwd = process.cwd()
|
|
44
|
+
cwd = process.cwd(),
|
|
45
|
+
appDir
|
|
44
46
|
} = options;
|
|
45
47
|
if (findExistingConfig(cwd)) return null;
|
|
46
48
|
const ext = useTypeScript ? "ts" : "js";
|
|
47
49
|
const configPath = join(cwd, `vocoder.config.${ext}`);
|
|
48
50
|
const branchesStr = targetBranches.map((b) => `'${b}'`).join(", ");
|
|
51
|
+
const defaultIncludes = ["**/*.{tsx,jsx,ts,js}"];
|
|
52
|
+
const includes = appDir ? defaultIncludes.map((p14) => `${appDir}/${p14}`) : defaultIncludes;
|
|
53
|
+
const includesStr = includes.map((p14) => `'${p14}'`).join(", ");
|
|
49
54
|
const content = `import { defineConfig } from '@vocoder/config'
|
|
50
55
|
|
|
51
56
|
export default defineConfig({
|
|
52
57
|
targetBranches: [${branchesStr}],
|
|
53
|
-
include: [
|
|
58
|
+
include: [${includesStr}],
|
|
54
59
|
exclude: [
|
|
55
60
|
'**/node_modules/**',
|
|
56
61
|
'**/.next/**',
|
|
@@ -877,25 +882,9 @@ async function runProjectCreate(params) {
|
|
|
877
882
|
return null;
|
|
878
883
|
}
|
|
879
884
|
const languageOptions = buildLanguageOptions(sourceLocales);
|
|
880
|
-
|
|
881
|
-
if (
|
|
882
|
-
appDir = params.defaultAppDir;
|
|
885
|
+
const appDir = params.defaultAppDir ?? "";
|
|
886
|
+
if (appDir) {
|
|
883
887
|
p3.log.success(`App directory: ${chalk4.bold(appDir)}`);
|
|
884
|
-
} else {
|
|
885
|
-
const rawScope = await p3.text({
|
|
886
|
-
message: "App directory (leave blank for the entire repo)",
|
|
887
|
-
placeholder: "e.g. apps/web, packages/frontend",
|
|
888
|
-
initialValue: "",
|
|
889
|
-
validate(value) {
|
|
890
|
-
const v = value.trim();
|
|
891
|
-
if (!v) return;
|
|
892
|
-
if (v.startsWith("/"))
|
|
893
|
-
return "Use a relative path, not an absolute path";
|
|
894
|
-
if (v.includes("..")) return 'Path must not contain ".."';
|
|
895
|
-
}
|
|
896
|
-
});
|
|
897
|
-
if (p3.isCancel(rawScope)) return null;
|
|
898
|
-
appDir = (rawScope ?? "").trim();
|
|
899
888
|
}
|
|
900
889
|
const sourceLocale = await searchSelectLocale(
|
|
901
890
|
languageOptions,
|
|
@@ -968,7 +957,7 @@ async function runProjectCreate(params) {
|
|
|
968
957
|
return null;
|
|
969
958
|
}
|
|
970
959
|
}
|
|
971
|
-
async function
|
|
960
|
+
async function runAppCreate(params) {
|
|
972
961
|
const { api, userToken, projectId, projectName, repoCanonical } = params;
|
|
973
962
|
const existingScopes = new Set(params.existingApps.map((a) => a.appDir));
|
|
974
963
|
let sourceLocales;
|
|
@@ -981,35 +970,13 @@ async function runProjectAppCreate(params) {
|
|
|
981
970
|
return null;
|
|
982
971
|
}
|
|
983
972
|
const languageOptions = buildLanguageOptions(sourceLocales);
|
|
984
|
-
|
|
985
|
-
if (
|
|
986
|
-
appDir
|
|
973
|
+
const appDir = params.defaultAppDir ?? "";
|
|
974
|
+
if (existingScopes.has(appDir)) {
|
|
975
|
+
p3.log.error(`App directory "${appDir}" is already configured for this project.`);
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
if (appDir) {
|
|
987
979
|
p3.log.success(`App directory: ${chalk4.bold(appDir)}`);
|
|
988
|
-
} else {
|
|
989
|
-
if (params.existingApps.length > 0) {
|
|
990
|
-
const configuredList = params.existingApps.map((a) => chalk4.dim(a.appDir || "(entire repo)")).join(", ");
|
|
991
|
-
p3.log.info(`Already configured: ${configuredList}`);
|
|
992
|
-
}
|
|
993
|
-
const hasWholeRepoApp = existingScopes.has("");
|
|
994
|
-
const rawScope = await p3.text({
|
|
995
|
-
message: "App directory for this new app",
|
|
996
|
-
placeholder: "e.g. apps/backend",
|
|
997
|
-
initialValue: params.defaultAppDir ?? "",
|
|
998
|
-
validate(value) {
|
|
999
|
-
const v = value.trim();
|
|
1000
|
-
if (!v && hasWholeRepoApp)
|
|
1001
|
-
return "This project already covers the entire repo.";
|
|
1002
|
-
if (!v)
|
|
1003
|
-
return "App directory is required when other apps already exist.";
|
|
1004
|
-
if (v.startsWith("/"))
|
|
1005
|
-
return "Use a relative path, not an absolute path.";
|
|
1006
|
-
if (v.includes("..")) return 'Path must not contain "..".';
|
|
1007
|
-
if (existingScopes.has(v))
|
|
1008
|
-
return `"${v}" is already configured. Choose a different directory.`;
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
1011
|
-
if (p3.isCancel(rawScope)) return null;
|
|
1012
|
-
appDir = (rawScope ?? "").trim();
|
|
1013
980
|
}
|
|
1014
981
|
const sourceLocale = await searchSelectLocale(
|
|
1015
982
|
languageOptions,
|
|
@@ -1061,7 +1028,7 @@ async function runProjectAppCreate(params) {
|
|
|
1061
1028
|
}
|
|
1062
1029
|
const targetBranches = appPushBranches;
|
|
1063
1030
|
try {
|
|
1064
|
-
const result = await api.
|
|
1031
|
+
const result = await api.createApp(userToken, {
|
|
1065
1032
|
projectId,
|
|
1066
1033
|
appDir,
|
|
1067
1034
|
sourceLocale,
|
|
@@ -1301,7 +1268,7 @@ function printPlanLimitMessage(apiUrl, message) {
|
|
|
1301
1268
|
p5.log.info(`Manage subscription: ${getSubscriptionSettingsUrl(apiUrl)}`);
|
|
1302
1269
|
}
|
|
1303
1270
|
function runScaffold(params) {
|
|
1304
|
-
const { sourceLocale, targetBranches } = params;
|
|
1271
|
+
const { sourceLocale, targetBranches, appDir } = params;
|
|
1305
1272
|
const detection = detectLocalEcosystem();
|
|
1306
1273
|
const useTypeScript = detection.isTypeScript;
|
|
1307
1274
|
if (detection.ecosystem) {
|
|
@@ -1344,7 +1311,8 @@ function runScaffold(params) {
|
|
|
1344
1311
|
framework: detection.framework,
|
|
1345
1312
|
ecosystem: detection.ecosystem,
|
|
1346
1313
|
sourceLocale,
|
|
1347
|
-
targetBranches
|
|
1314
|
+
targetBranches,
|
|
1315
|
+
appDir
|
|
1348
1316
|
});
|
|
1349
1317
|
const steps = [];
|
|
1350
1318
|
if (snippets.pluginStep) {
|
|
@@ -1377,7 +1345,7 @@ function runScaffold(params) {
|
|
|
1377
1345
|
printCodeBlock(step.code);
|
|
1378
1346
|
if (i < steps.length - 1) p5.log.message("");
|
|
1379
1347
|
}
|
|
1380
|
-
const written = writeVocoderConfig({ targetBranches, useTypeScript });
|
|
1348
|
+
const written = writeVocoderConfig({ targetBranches, useTypeScript, appDir });
|
|
1381
1349
|
if (written) {
|
|
1382
1350
|
p5.log.success(`Created ${chalk6.cyan(written)}`);
|
|
1383
1351
|
} else if (!findExistingConfig(process.cwd())) {
|
|
@@ -1447,17 +1415,6 @@ function printCodeBlock(code) {
|
|
|
1447
1415
|
`
|
|
1448
1416
|
);
|
|
1449
1417
|
}
|
|
1450
|
-
async function verifyStoredToken(api, token) {
|
|
1451
|
-
try {
|
|
1452
|
-
return await api.getCliUserInfo(token);
|
|
1453
|
-
} catch (err) {
|
|
1454
|
-
clearAuthData();
|
|
1455
|
-
if (err instanceof VocoderAPIError && err.status === 404) {
|
|
1456
|
-
return { userGone: true };
|
|
1457
|
-
}
|
|
1458
|
-
return null;
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
1418
|
async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
1462
1419
|
let server = null;
|
|
1463
1420
|
if (!options.ci) {
|
|
@@ -1486,8 +1443,9 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
1486
1443
|
return null;
|
|
1487
1444
|
}
|
|
1488
1445
|
if (!shouldOpen) {
|
|
1489
|
-
|
|
1490
|
-
p5.
|
|
1446
|
+
server?.close();
|
|
1447
|
+
p5.cancel("Setup cancelled.");
|
|
1448
|
+
return null;
|
|
1491
1449
|
} else {
|
|
1492
1450
|
const opened = await tryOpenBrowser2(browserUrl);
|
|
1493
1451
|
if (!opened) {
|
|
@@ -1638,17 +1596,17 @@ async function init(options = {}) {
|
|
|
1638
1596
|
true
|
|
1639
1597
|
);
|
|
1640
1598
|
if (!authResult) return 1;
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1599
|
+
const spinner7 = p5.spinner();
|
|
1600
|
+
spinner7.start("Generating new API key...");
|
|
1643
1601
|
try {
|
|
1644
1602
|
const { apiKey } = await anonApi2.regenerateProjectApiKey(
|
|
1645
1603
|
authResult.token,
|
|
1646
1604
|
exactMatch.projectId
|
|
1647
1605
|
);
|
|
1648
|
-
|
|
1606
|
+
spinner7.stop("New API key generated");
|
|
1649
1607
|
printApiKey(apiKey);
|
|
1650
1608
|
} catch (err) {
|
|
1651
|
-
|
|
1609
|
+
spinner7.stop("Failed to generate key");
|
|
1652
1610
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1653
1611
|
p5.log.error(`Could not generate API key: ${msg}`);
|
|
1654
1612
|
p5.log.info("Try again or generate one from the dashboard.");
|
|
@@ -1683,46 +1641,23 @@ async function init(options = {}) {
|
|
|
1683
1641
|
let userEmail;
|
|
1684
1642
|
let userName;
|
|
1685
1643
|
let authOrganizationId;
|
|
1686
|
-
const
|
|
1687
|
-
if (
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
userEmail = verified.email;
|
|
1693
|
-
userName = verified.name;
|
|
1694
|
-
} else {
|
|
1695
|
-
const isFirstTime = verified !== null && "userGone" in verified;
|
|
1696
|
-
if (isFirstTime) {
|
|
1697
|
-
p5.log.warn("Account not found \u2014 starting fresh setup");
|
|
1698
|
-
} else {
|
|
1699
|
-
p5.log.warn("Stored credentials expired \u2014 signing in again");
|
|
1700
|
-
}
|
|
1701
|
-
const authResult = await runAuthFlow(
|
|
1702
|
-
api,
|
|
1703
|
-
options,
|
|
1704
|
-
/* reauth */
|
|
1705
|
-
!isFirstTime,
|
|
1706
|
-
identity?.repoCanonical
|
|
1707
|
-
);
|
|
1708
|
-
if (!authResult) return 1;
|
|
1709
|
-
userToken = authResult.token;
|
|
1710
|
-
userEmail = authResult.email;
|
|
1711
|
-
userName = authResult.name;
|
|
1712
|
-
authOrganizationId = authResult.organizationId;
|
|
1713
|
-
writeAuthData({
|
|
1714
|
-
token: userToken,
|
|
1715
|
-
userId: authResult.userId,
|
|
1716
|
-
email: userEmail,
|
|
1717
|
-
name: userName,
|
|
1718
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1644
|
+
const storedAuth = await verifyStoredAuth(api);
|
|
1645
|
+
if (storedAuth.status === "valid") {
|
|
1646
|
+
p5.log.success(`Authenticated as ${chalk6.bold(storedAuth.email)}`);
|
|
1647
|
+
userToken = storedAuth.token;
|
|
1648
|
+
userEmail = storedAuth.email;
|
|
1649
|
+
userName = storedAuth.name;
|
|
1721
1650
|
} else {
|
|
1651
|
+
const reauth = storedAuth.status === "expired";
|
|
1652
|
+
if (reauth) {
|
|
1653
|
+
p5.log.warn("Stored credentials expired \u2014 signing in again");
|
|
1654
|
+
} else if (storedAuth.status === "gone") {
|
|
1655
|
+
p5.log.warn("Account not found \u2014 starting fresh setup");
|
|
1656
|
+
}
|
|
1722
1657
|
const authResult = await runAuthFlow(
|
|
1723
1658
|
api,
|
|
1724
1659
|
options,
|
|
1725
|
-
|
|
1660
|
+
reauth,
|
|
1726
1661
|
identity?.repoCanonical
|
|
1727
1662
|
);
|
|
1728
1663
|
if (!authResult) return 1;
|
|
@@ -1999,7 +1934,7 @@ async function init(options = {}) {
|
|
|
1999
1934
|
`${chalk6.bold(repoProjectName)} is already set up for this repo.
|
|
2000
1935
|
Configured apps: ${existingAppsForRepo.map((a) => chalk6.cyan(a.appDir || "(entire repo)")).join(", ")}`
|
|
2001
1936
|
);
|
|
2002
|
-
const appResult = await
|
|
1937
|
+
const appResult = await runAppCreate({
|
|
2003
1938
|
api,
|
|
2004
1939
|
userToken,
|
|
2005
1940
|
projectId: repoProjectId,
|
|
@@ -2015,7 +1950,8 @@ async function init(options = {}) {
|
|
|
2015
1950
|
}
|
|
2016
1951
|
runScaffold({
|
|
2017
1952
|
sourceLocale: appResult.sourceLocale,
|
|
2018
|
-
targetBranches: appResult.targetBranches
|
|
1953
|
+
targetBranches: appResult.targetBranches,
|
|
1954
|
+
appDir: identity?.repoAppDir
|
|
2019
1955
|
});
|
|
2020
1956
|
p5.outro("You're all set.");
|
|
2021
1957
|
return 0;
|
|
@@ -2072,7 +2008,7 @@ async function init(options = {}) {
|
|
|
2072
2008
|
return 1;
|
|
2073
2009
|
}
|
|
2074
2010
|
const chosen = existingProjects.find((proj) => proj.id === chosenId);
|
|
2075
|
-
const appResult = await
|
|
2011
|
+
const appResult = await runAppCreate({
|
|
2076
2012
|
api,
|
|
2077
2013
|
userToken,
|
|
2078
2014
|
projectId: chosen.id,
|
|
@@ -2088,7 +2024,8 @@ async function init(options = {}) {
|
|
|
2088
2024
|
}
|
|
2089
2025
|
runScaffold({
|
|
2090
2026
|
sourceLocale: appResult.sourceLocale,
|
|
2091
|
-
targetBranches: appResult.targetBranches
|
|
2027
|
+
targetBranches: appResult.targetBranches,
|
|
2028
|
+
appDir: identity?.repoAppDir
|
|
2092
2029
|
});
|
|
2093
2030
|
p5.outro("You're all set.");
|
|
2094
2031
|
return 0;
|
|
@@ -2123,7 +2060,8 @@ Translations won't run automatically until you grant access.
|
|
|
2123
2060
|
}
|
|
2124
2061
|
runScaffold({
|
|
2125
2062
|
sourceLocale: projectResult.sourceLocale,
|
|
2126
|
-
targetBranches: projectResult.targetBranches
|
|
2063
|
+
targetBranches: projectResult.targetBranches,
|
|
2064
|
+
appDir: identity?.repoAppDir
|
|
2127
2065
|
});
|
|
2128
2066
|
printApiKey(projectResult.apiKey);
|
|
2129
2067
|
p5.outro("You're all set.");
|
|
@@ -2142,30 +2080,16 @@ Translations won't run automatically until you grant access.
|
|
|
2142
2080
|
}
|
|
2143
2081
|
}
|
|
2144
2082
|
|
|
2145
|
-
// src/commands/
|
|
2146
|
-
import * as
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
if (!stored) {
|
|
2150
|
-
p6.log.info("Not currently authenticated.");
|
|
2151
|
-
return 0;
|
|
2152
|
-
}
|
|
2153
|
-
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
2154
|
-
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
2155
|
-
try {
|
|
2156
|
-
await api.revokeCliToken(stored.token);
|
|
2157
|
-
} catch {
|
|
2158
|
-
}
|
|
2159
|
-
clearAuthData();
|
|
2160
|
-
p6.log.success(`Logged out (was ${stored.email})`);
|
|
2161
|
-
return 0;
|
|
2162
|
-
}
|
|
2083
|
+
// src/commands/locales.ts
|
|
2084
|
+
import * as p8 from "@clack/prompts";
|
|
2085
|
+
import chalk9 from "chalk";
|
|
2086
|
+
import { config as loadEnv3 } from "dotenv";
|
|
2163
2087
|
|
|
2164
2088
|
// src/commands/sync.ts
|
|
2165
2089
|
import { createHash, randomUUID } from "crypto";
|
|
2166
2090
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2167
2091
|
import { join as join3 } from "path";
|
|
2168
|
-
import * as
|
|
2092
|
+
import * as p7 from "@clack/prompts";
|
|
2169
2093
|
import chalk8 from "chalk";
|
|
2170
2094
|
|
|
2171
2095
|
// src/utils/branch.ts
|
|
@@ -2235,7 +2159,7 @@ function matchBranchPattern(branch, pattern) {
|
|
|
2235
2159
|
}
|
|
2236
2160
|
|
|
2237
2161
|
// src/utils/config.ts
|
|
2238
|
-
import * as
|
|
2162
|
+
import * as p6 from "@clack/prompts";
|
|
2239
2163
|
import chalk7 from "chalk";
|
|
2240
2164
|
import { config as loadEnv2 } from "dotenv";
|
|
2241
2165
|
loadEnv2();
|
|
@@ -2294,7 +2218,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2294
2218
|
};
|
|
2295
2219
|
const fileConfig = loadVocoderConfig(process.cwd());
|
|
2296
2220
|
if (!fileConfig) {
|
|
2297
|
-
|
|
2221
|
+
p6.log.warn(
|
|
2298
2222
|
`No ${chalk7.cyan("vocoder.config.ts")} found \u2014 run ${chalk7.cyan("npx @vocoder/cli init")} to generate one.`
|
|
2299
2223
|
);
|
|
2300
2224
|
}
|
|
@@ -2325,7 +2249,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2325
2249
|
excludePattern = fileConfig.exclude;
|
|
2326
2250
|
configSources.excludePattern = "vocoder.config";
|
|
2327
2251
|
} else if (envExcludePattern) {
|
|
2328
|
-
excludePattern = envExcludePattern.split(",").map((
|
|
2252
|
+
excludePattern = envExcludePattern.split(",").map((p14) => p14.trim()).filter(Boolean);
|
|
2329
2253
|
configSources.excludePattern = "environment";
|
|
2330
2254
|
} else {
|
|
2331
2255
|
excludePattern = defaults.excludePattern;
|
|
@@ -2382,7 +2306,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2382
2306
|
...maxWaitMs ? [`Max wait: ${chalk7.cyan(String(configSources.maxWaitMs))}`] : [],
|
|
2383
2307
|
`No fallback: ${chalk7.cyan(String(configSources.noFallback))}`
|
|
2384
2308
|
];
|
|
2385
|
-
|
|
2309
|
+
p6.note(lines.join("\n"), "Configuration sources");
|
|
2386
2310
|
}
|
|
2387
2311
|
return {
|
|
2388
2312
|
includePattern,
|
|
@@ -2452,8 +2376,8 @@ function buildTranslationData(params) {
|
|
|
2452
2376
|
const hashKeyed = {};
|
|
2453
2377
|
for (const [locale, localeMap] of Object.entries(params.translations)) {
|
|
2454
2378
|
hashKeyed[locale] = {};
|
|
2455
|
-
for (const [
|
|
2456
|
-
const hash = textToHash.get(
|
|
2379
|
+
for (const [text, translation] of Object.entries(localeMap)) {
|
|
2380
|
+
const hash = textToHash.get(text);
|
|
2457
2381
|
if (hash) hashKeyed[locale][hash] = translation;
|
|
2458
2382
|
}
|
|
2459
2383
|
}
|
|
@@ -2560,6 +2484,13 @@ function getLimitErrorGuidance(limitError) {
|
|
|
2560
2484
|
`Upgrade plan: ${limitError.upgradeUrl}`
|
|
2561
2485
|
];
|
|
2562
2486
|
}
|
|
2487
|
+
if (limitError.limitType === "target_locales") {
|
|
2488
|
+
return [
|
|
2489
|
+
`Current target locales: ${limitError.current}`,
|
|
2490
|
+
`Plan limit: ${limitError.current} (${limitError.planId})`,
|
|
2491
|
+
`Upgrade plan: ${limitError.upgradeUrl}`
|
|
2492
|
+
];
|
|
2493
|
+
}
|
|
2563
2494
|
return [
|
|
2564
2495
|
`Plan: ${limitError.planId}`,
|
|
2565
2496
|
`Current: ${limitError.current}`,
|
|
@@ -2644,22 +2575,22 @@ async function fetchApiSnapshot(api, params) {
|
|
|
2644
2575
|
async function sync(options = {}) {
|
|
2645
2576
|
const startTime = Date.now();
|
|
2646
2577
|
const projectRoot = process.cwd();
|
|
2647
|
-
|
|
2578
|
+
p7.intro("Vocoder Sync");
|
|
2648
2579
|
const mergedConfig = await getMergedConfig(options, options.verbose);
|
|
2649
2580
|
if (!mergedConfig.apiKey) {
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2581
|
+
p7.log.warn("No API key found. Run init to get started:");
|
|
2582
|
+
p7.log.info(" npx @vocoder/cli init");
|
|
2583
|
+
p7.log.info("");
|
|
2584
|
+
p7.log.info(
|
|
2654
2585
|
" Or add your key to .env: VOCODER_API_KEY=vcp_..."
|
|
2655
2586
|
);
|
|
2656
|
-
|
|
2587
|
+
p7.outro("Run `npx @vocoder/cli init` to set up your project.");
|
|
2657
2588
|
return 1;
|
|
2658
2589
|
}
|
|
2659
|
-
const
|
|
2590
|
+
const spinner7 = p7.spinner();
|
|
2660
2591
|
try {
|
|
2661
2592
|
const branch = detectBranch(options.branch);
|
|
2662
|
-
|
|
2593
|
+
spinner7.start("Loading project configuration");
|
|
2663
2594
|
const localConfig = {
|
|
2664
2595
|
apiKey: mergedConfig.apiKey,
|
|
2665
2596
|
apiUrl: mergedConfig.apiUrl || "https://vocoder.app"
|
|
@@ -2683,18 +2614,18 @@ async function sync(options = {}) {
|
|
|
2683
2614
|
...fileConfig?.appIndustry ? { appIndustry: fileConfig.appIndustry } : {},
|
|
2684
2615
|
...fileConfig?.formality ? { formality: fileConfig.formality } : {}
|
|
2685
2616
|
};
|
|
2686
|
-
|
|
2617
|
+
spinner7.stop(`Branch: ${chalk8.cyan(branch)}`);
|
|
2687
2618
|
if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
|
|
2688
|
-
|
|
2619
|
+
p7.log.warn(
|
|
2689
2620
|
`Skipping translations (${chalk8.cyan(branch)} is not a target branch)`
|
|
2690
2621
|
);
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2622
|
+
p7.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
|
|
2623
|
+
p7.log.info("Use --force to translate anyway");
|
|
2624
|
+
p7.outro("");
|
|
2694
2625
|
return 0;
|
|
2695
2626
|
}
|
|
2696
2627
|
const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
|
|
2697
|
-
|
|
2628
|
+
spinner7.start(`Extracting strings from ${patternsDisplay}`);
|
|
2698
2629
|
const extractor = new StringExtractor();
|
|
2699
2630
|
const extractedStrings = await extractor.extractFromProject(
|
|
2700
2631
|
config.includePattern,
|
|
@@ -2702,14 +2633,14 @@ async function sync(options = {}) {
|
|
|
2702
2633
|
config.excludePattern
|
|
2703
2634
|
);
|
|
2704
2635
|
if (extractedStrings.length === 0) {
|
|
2705
|
-
|
|
2706
|
-
|
|
2636
|
+
spinner7.stop("No translatable strings found");
|
|
2637
|
+
p7.log.warn(
|
|
2707
2638
|
"Make sure you are wrapping translatable strings with Vocoder"
|
|
2708
2639
|
);
|
|
2709
|
-
|
|
2640
|
+
p7.outro("");
|
|
2710
2641
|
return 0;
|
|
2711
2642
|
}
|
|
2712
|
-
|
|
2643
|
+
spinner7.stop(
|
|
2713
2644
|
`Extracted ${chalk8.cyan(extractedStrings.length)} strings from ${chalk8.cyan(patternsDisplay)}`
|
|
2714
2645
|
);
|
|
2715
2646
|
if (options.verbose) {
|
|
@@ -2717,10 +2648,10 @@ async function sync(options = {}) {
|
|
|
2717
2648
|
if (extractedStrings.length > 5) {
|
|
2718
2649
|
sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
|
|
2719
2650
|
}
|
|
2720
|
-
|
|
2651
|
+
p7.note(sampleLines.join("\n"), "Sample strings");
|
|
2721
2652
|
}
|
|
2722
2653
|
if (options.dryRun) {
|
|
2723
|
-
|
|
2654
|
+
p7.note(
|
|
2724
2655
|
[
|
|
2725
2656
|
`Strings: ${extractedStrings.length}`,
|
|
2726
2657
|
`Branch: ${branch}`,
|
|
@@ -2731,12 +2662,12 @@ async function sync(options = {}) {
|
|
|
2731
2662
|
].join("\n"),
|
|
2732
2663
|
"Dry run - would translate"
|
|
2733
2664
|
);
|
|
2734
|
-
|
|
2665
|
+
p7.outro("No API calls made.");
|
|
2735
2666
|
return 0;
|
|
2736
2667
|
}
|
|
2737
2668
|
const repoIdentity = resolveGitRepositoryIdentity();
|
|
2738
2669
|
if (!repoIdentity && options.verbose) {
|
|
2739
|
-
|
|
2670
|
+
p7.log.warn(
|
|
2740
2671
|
"Could not detect git remote origin. Sync will continue without repo metadata."
|
|
2741
2672
|
);
|
|
2742
2673
|
}
|
|
@@ -2744,7 +2675,7 @@ async function sync(options = {}) {
|
|
|
2744
2675
|
const stringEntries = buildStringEntries(extractedStrings);
|
|
2745
2676
|
const sourceStrings = stringEntries.map((entry) => entry.text);
|
|
2746
2677
|
if (options.verbose && stringEntries.length !== extractedStrings.length) {
|
|
2747
|
-
|
|
2678
|
+
p7.log.info(
|
|
2748
2679
|
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
|
|
2749
2680
|
);
|
|
2750
2681
|
}
|
|
@@ -2753,17 +2684,17 @@ async function sync(options = {}) {
|
|
|
2753
2684
|
const cacheFile = getCacheFilePath(projectRoot, fingerprint);
|
|
2754
2685
|
if (existsSync3(cacheFile)) {
|
|
2755
2686
|
if (options.verbose) {
|
|
2756
|
-
|
|
2687
|
+
p7.log.info(`Cache hit: ${chalk8.dim(cacheFile)} (fingerprint ${chalk8.cyan(fingerprint)})`);
|
|
2757
2688
|
}
|
|
2758
2689
|
const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2759
|
-
|
|
2690
|
+
p7.outro(`Up to date (${duration2}s)`);
|
|
2760
2691
|
return 0;
|
|
2761
2692
|
}
|
|
2762
2693
|
if (options.verbose) {
|
|
2763
|
-
|
|
2694
|
+
p7.log.info(`No cache for fingerprint ${chalk8.cyan(fingerprint)} \u2014 will submit to API`);
|
|
2764
2695
|
}
|
|
2765
2696
|
}
|
|
2766
|
-
|
|
2697
|
+
spinner7.start("Submitting strings to Vocoder API");
|
|
2767
2698
|
const batchResponse = await api.submitTranslation(
|
|
2768
2699
|
branch,
|
|
2769
2700
|
stringEntries,
|
|
@@ -2773,38 +2704,38 @@ async function sync(options = {}) {
|
|
|
2773
2704
|
requestedMaxWaitMs: waitTimeoutMs,
|
|
2774
2705
|
clientRunId: randomUUID(),
|
|
2775
2706
|
force: options.force,
|
|
2776
|
-
// Sync appIndustry from vocoder.config.ts to
|
|
2707
|
+
// Sync appIndustry from vocoder.config.ts to App on every push
|
|
2777
2708
|
...config.appIndustry ? { appIndustry: config.appIndustry } : {}
|
|
2778
2709
|
},
|
|
2779
2710
|
repoIdentity ? { ...repoIdentity, commitSha } : { commitSha }
|
|
2780
2711
|
);
|
|
2781
|
-
|
|
2712
|
+
spinner7.stop("Strings submitted");
|
|
2782
2713
|
const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
|
|
2783
2714
|
branch,
|
|
2784
2715
|
requestedMode,
|
|
2785
2716
|
policy: config.syncPolicy
|
|
2786
2717
|
});
|
|
2787
2718
|
if (options.verbose) {
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2719
|
+
p7.log.info(`Batch: ${chalk8.dim(batchResponse.batchId)}`);
|
|
2720
|
+
p7.log.info(`Requested mode: ${requestedMode}`);
|
|
2721
|
+
p7.log.info(`Effective mode: ${effectiveMode}`);
|
|
2722
|
+
p7.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
|
|
2792
2723
|
if (batchResponse.queueStatus) {
|
|
2793
|
-
|
|
2724
|
+
p7.log.info(`Queue status: ${batchResponse.queueStatus}`);
|
|
2794
2725
|
}
|
|
2795
2726
|
}
|
|
2796
2727
|
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
2797
|
-
|
|
2728
|
+
p7.log.success(`Up to date \u2014 ${chalk8.cyan(batchResponse.totalStrings)} strings, no changes`);
|
|
2798
2729
|
} else if (batchResponse.newStrings === 0) {
|
|
2799
2730
|
const archivedNote = batchResponse.deletedStrings && batchResponse.deletedStrings > 0 ? `, ${chalk8.yellow(batchResponse.deletedStrings)} archived` : "";
|
|
2800
|
-
|
|
2731
|
+
p7.log.success(`No new strings \u2014 ${chalk8.cyan(batchResponse.totalStrings)} total${archivedNote}, using existing translations`);
|
|
2801
2732
|
} else {
|
|
2802
2733
|
const statParts = [`${chalk8.cyan(batchResponse.newStrings)} new, ${chalk8.cyan(batchResponse.totalStrings)} total`];
|
|
2803
2734
|
if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
|
|
2804
2735
|
statParts.push(`${chalk8.yellow(batchResponse.deletedStrings)} archived`);
|
|
2805
2736
|
}
|
|
2806
2737
|
const estTime = batchResponse.estimatedTime ? ` (~${batchResponse.estimatedTime}s)` : "";
|
|
2807
|
-
|
|
2738
|
+
p7.log.info(`${statParts.join(", ")} \u2192 syncing to ${config.targetLocales.join(", ")}${estTime}`);
|
|
2808
2739
|
}
|
|
2809
2740
|
let artifacts = null;
|
|
2810
2741
|
if (batchResponse.translations) {
|
|
@@ -2816,7 +2747,7 @@ async function sync(options = {}) {
|
|
|
2816
2747
|
let waitError = null;
|
|
2817
2748
|
if (!artifacts && (effectiveMode === "required" || effectiveMode === "best-effort")) {
|
|
2818
2749
|
const waitTimeoutSecs = Math.round(waitTimeoutMs / 1e3);
|
|
2819
|
-
|
|
2750
|
+
spinner7.start(`Waiting for translations (max ${waitTimeoutSecs}s)`);
|
|
2820
2751
|
let lastProgress = 0;
|
|
2821
2752
|
try {
|
|
2822
2753
|
const completion = await api.waitForCompletion(
|
|
@@ -2825,7 +2756,7 @@ async function sync(options = {}) {
|
|
|
2825
2756
|
(progress) => {
|
|
2826
2757
|
const percent = Math.round(progress * 100);
|
|
2827
2758
|
if (percent > lastProgress) {
|
|
2828
|
-
|
|
2759
|
+
spinner7.message(`Translating... ${percent}%`);
|
|
2829
2760
|
lastProgress = percent;
|
|
2830
2761
|
}
|
|
2831
2762
|
}
|
|
@@ -2835,14 +2766,14 @@ async function sync(options = {}) {
|
|
|
2835
2766
|
translations: completion.translations,
|
|
2836
2767
|
localeMetadata: completion.localeMetadata
|
|
2837
2768
|
};
|
|
2838
|
-
|
|
2769
|
+
spinner7.stop("Translations complete");
|
|
2839
2770
|
} catch (error) {
|
|
2840
|
-
|
|
2771
|
+
spinner7.stop("Translation wait incomplete");
|
|
2841
2772
|
waitError = error instanceof Error ? error : new Error(String(error));
|
|
2842
2773
|
if (effectiveMode === "required") {
|
|
2843
2774
|
throw waitError;
|
|
2844
2775
|
}
|
|
2845
|
-
|
|
2776
|
+
p7.log.warn(`Best-effort wait ended early: ${waitError.message}`);
|
|
2846
2777
|
}
|
|
2847
2778
|
}
|
|
2848
2779
|
if (!artifacts) {
|
|
@@ -2851,14 +2782,14 @@ async function sync(options = {}) {
|
|
|
2851
2782
|
"Fresh translations are not available and fallback is disabled (--no-fallback)."
|
|
2852
2783
|
);
|
|
2853
2784
|
}
|
|
2854
|
-
|
|
2785
|
+
spinner7.start("Loading fallback translations");
|
|
2855
2786
|
const localFallback = readLocalCache({
|
|
2856
2787
|
projectRoot,
|
|
2857
2788
|
fingerprint
|
|
2858
2789
|
});
|
|
2859
2790
|
if (localFallback) {
|
|
2860
2791
|
artifacts = localFallback;
|
|
2861
|
-
|
|
2792
|
+
spinner7.stop(`Using local cached snapshot (${fingerprint})`);
|
|
2862
2793
|
} else {
|
|
2863
2794
|
try {
|
|
2864
2795
|
const apiSnapshot = await fetchApiSnapshot(api, {
|
|
@@ -2867,15 +2798,15 @@ async function sync(options = {}) {
|
|
|
2867
2798
|
});
|
|
2868
2799
|
if (apiSnapshot) {
|
|
2869
2800
|
artifacts = apiSnapshot;
|
|
2870
|
-
|
|
2801
|
+
spinner7.stop("Using latest completed API snapshot");
|
|
2871
2802
|
} else {
|
|
2872
|
-
|
|
2803
|
+
spinner7.stop("No completed API snapshot available");
|
|
2873
2804
|
}
|
|
2874
2805
|
} catch (error) {
|
|
2875
|
-
|
|
2806
|
+
spinner7.stop("Failed to fetch API snapshot");
|
|
2876
2807
|
if (options.verbose) {
|
|
2877
2808
|
const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
|
|
2878
|
-
|
|
2809
|
+
p7.log.warn(`Snapshot fetch error: ${message}`);
|
|
2879
2810
|
}
|
|
2880
2811
|
}
|
|
2881
2812
|
}
|
|
@@ -2907,85 +2838,433 @@ async function sync(options = {}) {
|
|
|
2907
2838
|
});
|
|
2908
2839
|
const cachePath = writeCache({ projectRoot, fingerprint, data });
|
|
2909
2840
|
if (options.verbose) {
|
|
2910
|
-
|
|
2841
|
+
p7.log.info(`Cache written: ${cachePath}`);
|
|
2911
2842
|
}
|
|
2912
2843
|
} catch (error) {
|
|
2913
2844
|
if (options.verbose) {
|
|
2914
2845
|
const message = error instanceof Error ? error.message : "Unknown cache write error";
|
|
2915
|
-
|
|
2846
|
+
p7.log.warn(`Failed to write cache: ${message}`);
|
|
2916
2847
|
}
|
|
2917
2848
|
}
|
|
2918
2849
|
if (artifacts.source !== "fresh") {
|
|
2919
2850
|
const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
|
|
2920
|
-
|
|
2851
|
+
p7.log.warn(
|
|
2921
2852
|
`Using ${sourceLabel}. New strings may appear after the background sync completes.`
|
|
2922
2853
|
);
|
|
2923
2854
|
}
|
|
2924
2855
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2925
|
-
|
|
2856
|
+
p7.outro(`Sync complete! (${duration}s)`);
|
|
2926
2857
|
return 0;
|
|
2927
2858
|
} catch (error) {
|
|
2928
|
-
|
|
2859
|
+
spinner7.stop();
|
|
2929
2860
|
if (error instanceof VocoderAPIError && error.syncPolicyError) {
|
|
2930
|
-
|
|
2861
|
+
p7.log.error(error.syncPolicyError.message);
|
|
2931
2862
|
const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
|
|
2932
2863
|
for (const line of guidance) {
|
|
2933
|
-
|
|
2864
|
+
p7.log.info(line);
|
|
2934
2865
|
}
|
|
2935
2866
|
return 1;
|
|
2936
2867
|
}
|
|
2937
2868
|
if (error instanceof VocoderAPIError && error.limitError) {
|
|
2938
2869
|
const { limitError } = error;
|
|
2939
|
-
|
|
2870
|
+
p7.log.error(limitError.message);
|
|
2940
2871
|
const guidance = getLimitErrorGuidance(limitError);
|
|
2941
2872
|
for (const line of guidance) {
|
|
2942
|
-
|
|
2873
|
+
p7.log.info(line);
|
|
2943
2874
|
}
|
|
2944
2875
|
return 1;
|
|
2945
2876
|
}
|
|
2946
2877
|
if (error instanceof Error) {
|
|
2947
|
-
|
|
2878
|
+
p7.log.error(error.message);
|
|
2948
2879
|
const isInvalidKey = error.message.toLowerCase().includes("invalid api key") || error instanceof VocoderAPIError && error.status === 401;
|
|
2949
2880
|
if (isInvalidKey) {
|
|
2950
|
-
|
|
2881
|
+
p7.log.warn(
|
|
2951
2882
|
"API key rejected \u2014 the project may have been deleted or the key revoked."
|
|
2952
2883
|
);
|
|
2953
|
-
|
|
2884
|
+
p7.log.info(
|
|
2954
2885
|
" Run `npx @vocoder/cli init` to create a new project and key."
|
|
2955
2886
|
);
|
|
2956
2887
|
} else if (error.message.includes("git branch")) {
|
|
2957
|
-
|
|
2958
|
-
|
|
2888
|
+
p7.log.warn("Run from a git repository, or use:");
|
|
2889
|
+
p7.log.info(" vocoder sync --branch main");
|
|
2959
2890
|
}
|
|
2960
2891
|
if (options.verbose) {
|
|
2961
|
-
|
|
2892
|
+
p7.log.info(`Full error: ${error.stack ?? error}`);
|
|
2962
2893
|
}
|
|
2963
2894
|
}
|
|
2964
2895
|
return 1;
|
|
2965
2896
|
}
|
|
2966
2897
|
}
|
|
2967
2898
|
|
|
2968
|
-
// src/commands/
|
|
2899
|
+
// src/commands/locales.ts
|
|
2900
|
+
loadEnv3();
|
|
2901
|
+
function getApiConfig(options) {
|
|
2902
|
+
const apiKey = process.env.VOCODER_API_KEY;
|
|
2903
|
+
if (!apiKey) {
|
|
2904
|
+
p8.log.error(
|
|
2905
|
+
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
2906
|
+
);
|
|
2907
|
+
return null;
|
|
2908
|
+
}
|
|
2909
|
+
return {
|
|
2910
|
+
apiKey,
|
|
2911
|
+
apiUrl: options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app"
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
async function listProjectLocales(options = {}) {
|
|
2915
|
+
const config = getApiConfig(options);
|
|
2916
|
+
if (!config) return 1;
|
|
2917
|
+
const api = new VocoderAPI(config);
|
|
2918
|
+
try {
|
|
2919
|
+
const projectConfig2 = await api.getProjectConfig();
|
|
2920
|
+
p8.log.info(
|
|
2921
|
+
`Source locale: ${chalk9.cyan(projectConfig2.sourceLocale)}`
|
|
2922
|
+
);
|
|
2923
|
+
if (projectConfig2.targetLocales.length === 0) {
|
|
2924
|
+
p8.log.info("Target locales: (none configured)");
|
|
2925
|
+
} else {
|
|
2926
|
+
p8.log.info(
|
|
2927
|
+
`Target locales: ${projectConfig2.targetLocales.map((l) => chalk9.cyan(l)).join(", ")}`
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
return 0;
|
|
2931
|
+
} catch (error) {
|
|
2932
|
+
p8.log.error(
|
|
2933
|
+
error instanceof Error ? error.message : "Failed to fetch project locales."
|
|
2934
|
+
);
|
|
2935
|
+
return 1;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
async function addLocales(locales, options = {}) {
|
|
2939
|
+
if (locales.length === 0) {
|
|
2940
|
+
p8.log.error("No locale codes provided.");
|
|
2941
|
+
return 1;
|
|
2942
|
+
}
|
|
2943
|
+
const config = getApiConfig(options);
|
|
2944
|
+
if (!config) return 1;
|
|
2945
|
+
const api = new VocoderAPI(config);
|
|
2946
|
+
let lastTargetLocales = [];
|
|
2947
|
+
let hadError = false;
|
|
2948
|
+
for (const locale of locales) {
|
|
2949
|
+
const spinner7 = p8.spinner();
|
|
2950
|
+
spinner7.start(`Adding ${locale}\u2026`);
|
|
2951
|
+
try {
|
|
2952
|
+
const result = await api.addLocale(locale);
|
|
2953
|
+
lastTargetLocales = result.targetLocales;
|
|
2954
|
+
spinner7.stop(`Added ${chalk9.cyan(locale)}`);
|
|
2955
|
+
} catch (error) {
|
|
2956
|
+
spinner7.stop(`Failed to add ${chalk9.red(locale)}`);
|
|
2957
|
+
hadError = true;
|
|
2958
|
+
if (error instanceof VocoderAPIError && error.limitError) {
|
|
2959
|
+
const { limitError } = error;
|
|
2960
|
+
p8.log.error(limitError.message);
|
|
2961
|
+
for (const line of getLimitErrorGuidance(limitError)) {
|
|
2962
|
+
p8.log.info(line);
|
|
2963
|
+
}
|
|
2964
|
+
break;
|
|
2965
|
+
}
|
|
2966
|
+
p8.log.error(
|
|
2967
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
if (lastTargetLocales.length > 0) {
|
|
2972
|
+
p8.log.info(
|
|
2973
|
+
`Target locales now: ${lastTargetLocales.map((l) => chalk9.cyan(l)).join(", ")}`
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
return hadError ? 1 : 0;
|
|
2977
|
+
}
|
|
2978
|
+
async function removeLocales(locales, options = {}) {
|
|
2979
|
+
if (locales.length === 0) {
|
|
2980
|
+
p8.log.error("No locale codes provided.");
|
|
2981
|
+
return 1;
|
|
2982
|
+
}
|
|
2983
|
+
const config = getApiConfig(options);
|
|
2984
|
+
if (!config) return 1;
|
|
2985
|
+
const api = new VocoderAPI(config);
|
|
2986
|
+
let lastTargetLocales = [];
|
|
2987
|
+
let hadError = false;
|
|
2988
|
+
for (const locale of locales) {
|
|
2989
|
+
const spinner7 = p8.spinner();
|
|
2990
|
+
spinner7.start(`Removing ${locale}\u2026`);
|
|
2991
|
+
try {
|
|
2992
|
+
const result = await api.removeLocale(locale);
|
|
2993
|
+
lastTargetLocales = result.targetLocales;
|
|
2994
|
+
spinner7.stop(`Removed ${chalk9.cyan(locale)}`);
|
|
2995
|
+
} catch (error) {
|
|
2996
|
+
spinner7.stop(`Failed to remove ${chalk9.red(locale)}`);
|
|
2997
|
+
hadError = true;
|
|
2998
|
+
p8.log.error(
|
|
2999
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
if (lastTargetLocales.length > 0) {
|
|
3004
|
+
p8.log.info(
|
|
3005
|
+
`Target locales now: ${lastTargetLocales.map((l) => chalk9.cyan(l)).join(", ")}`
|
|
3006
|
+
);
|
|
3007
|
+
} else if (!hadError) {
|
|
3008
|
+
p8.log.info("Target locales now: (none configured)");
|
|
3009
|
+
}
|
|
3010
|
+
return hadError ? 1 : 0;
|
|
3011
|
+
}
|
|
3012
|
+
async function listSupportedLocales(options = {}) {
|
|
3013
|
+
const config = getApiConfig(options);
|
|
3014
|
+
if (!config) return 1;
|
|
3015
|
+
const api = new VocoderAPI(config);
|
|
3016
|
+
try {
|
|
3017
|
+
const result = await api.listLocales(config.apiKey);
|
|
3018
|
+
p8.log.info(chalk9.bold("Source locales:"));
|
|
3019
|
+
printLocaleTable(result.sourceLocales);
|
|
3020
|
+
p8.log.info("");
|
|
3021
|
+
p8.log.info(chalk9.bold("Target locales:"));
|
|
3022
|
+
printLocaleTable(result.targetLocales);
|
|
3023
|
+
return 0;
|
|
3024
|
+
} catch (error) {
|
|
3025
|
+
p8.log.error(
|
|
3026
|
+
error instanceof Error ? error.message : "Failed to fetch supported locales."
|
|
3027
|
+
);
|
|
3028
|
+
return 1;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
function printLocaleTable(locales) {
|
|
3032
|
+
for (const locale of locales) {
|
|
3033
|
+
const native = locale.nativeName && locale.nativeName !== locale.name ? ` (${locale.nativeName})` : "";
|
|
3034
|
+
p8.log.info(` ${chalk9.cyan(locale.code.padEnd(10))} ${locale.name}${native}`);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// src/commands/logout.ts
|
|
2969
3039
|
import * as p9 from "@clack/prompts";
|
|
2970
|
-
|
|
3040
|
+
async function logout(options = {}) {
|
|
3041
|
+
const stored = readAuthData();
|
|
3042
|
+
if (!stored) {
|
|
3043
|
+
p9.log.info("Not currently authenticated.");
|
|
3044
|
+
return 0;
|
|
3045
|
+
}
|
|
3046
|
+
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
3047
|
+
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
3048
|
+
try {
|
|
3049
|
+
await api.revokeCliToken(stored.token);
|
|
3050
|
+
} catch {
|
|
3051
|
+
}
|
|
3052
|
+
clearAuthData();
|
|
3053
|
+
p9.log.success(`Logged out (was ${stored.email})`);
|
|
3054
|
+
return 0;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// src/commands/project-config.ts
|
|
3058
|
+
import * as p10 from "@clack/prompts";
|
|
3059
|
+
import chalk10 from "chalk";
|
|
3060
|
+
import { config as loadEnv4 } from "dotenv";
|
|
3061
|
+
loadEnv4();
|
|
3062
|
+
async function projectConfig(options = {}) {
|
|
3063
|
+
const apiKey = process.env.VOCODER_API_KEY;
|
|
3064
|
+
if (!apiKey) {
|
|
3065
|
+
p10.log.error(
|
|
3066
|
+
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
3067
|
+
);
|
|
3068
|
+
return 1;
|
|
3069
|
+
}
|
|
3070
|
+
const apiUrl = options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app";
|
|
3071
|
+
const api = new VocoderAPI({ apiKey, apiUrl });
|
|
3072
|
+
try {
|
|
3073
|
+
const config = await api.getProjectConfig();
|
|
3074
|
+
const lines = [
|
|
3075
|
+
`Project: ${chalk10.bold(config.projectName)}`,
|
|
3076
|
+
`Organization: ${config.organizationName}`,
|
|
3077
|
+
`Source locale: ${chalk10.cyan(config.sourceLocale)}`,
|
|
3078
|
+
`Target locales: ${config.targetLocales.length > 0 ? config.targetLocales.map((l) => chalk10.cyan(l)).join(", ") : chalk10.dim("(none)")}`,
|
|
3079
|
+
`Target branches: ${config.targetBranches.map((b) => chalk10.cyan(b)).join(", ")}`,
|
|
3080
|
+
...config.primaryBranch ? [`Primary branch: ${chalk10.cyan(config.primaryBranch)}`] : [],
|
|
3081
|
+
`Sync policy:`,
|
|
3082
|
+
` Blocking branches: ${config.syncPolicy.blockingBranches.map((b) => chalk10.cyan(b)).join(", ")}`,
|
|
3083
|
+
` Blocking mode: ${chalk10.cyan(config.syncPolicy.blockingMode)}`,
|
|
3084
|
+
` Non-blocking mode: ${chalk10.cyan(config.syncPolicy.nonBlockingMode)}`,
|
|
3085
|
+
` Max wait: ${chalk10.cyan(String(config.syncPolicy.defaultMaxWaitMs))} ms`
|
|
3086
|
+
];
|
|
3087
|
+
p10.note(lines.join("\n"), `${config.projectName} \u2014 project config`);
|
|
3088
|
+
return 0;
|
|
3089
|
+
} catch (error) {
|
|
3090
|
+
p10.log.error(
|
|
3091
|
+
error instanceof Error ? error.message : "Failed to fetch project config."
|
|
3092
|
+
);
|
|
3093
|
+
return 1;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
// src/commands/translations.ts
|
|
3098
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
3099
|
+
import { join as join4 } from "path";
|
|
3100
|
+
import * as p11 from "@clack/prompts";
|
|
3101
|
+
import chalk11 from "chalk";
|
|
3102
|
+
import { config as loadEnv5 } from "dotenv";
|
|
3103
|
+
loadEnv5();
|
|
3104
|
+
async function getTranslations(options = {}) {
|
|
3105
|
+
const apiKey = process.env.VOCODER_API_KEY;
|
|
3106
|
+
if (!apiKey) {
|
|
3107
|
+
p11.log.error(
|
|
3108
|
+
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
3109
|
+
);
|
|
3110
|
+
return 1;
|
|
3111
|
+
}
|
|
3112
|
+
const apiUrl = options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app";
|
|
3113
|
+
const api = new VocoderAPI({ apiKey, apiUrl });
|
|
3114
|
+
let branch;
|
|
3115
|
+
try {
|
|
3116
|
+
branch = detectBranch(options.branch);
|
|
3117
|
+
} catch (error) {
|
|
3118
|
+
p11.log.error(
|
|
3119
|
+
error instanceof Error ? error.message : "Failed to detect branch."
|
|
3120
|
+
);
|
|
3121
|
+
return 1;
|
|
3122
|
+
}
|
|
3123
|
+
const spinner7 = p11.spinner();
|
|
3124
|
+
spinner7.start(`Fetching translations for ${chalk11.cyan(branch)}\u2026`);
|
|
3125
|
+
try {
|
|
3126
|
+
const projectConfig2 = await api.getProjectConfig();
|
|
3127
|
+
const targetLocales = options.locale ? [options.locale] : projectConfig2.targetLocales;
|
|
3128
|
+
if (targetLocales.length === 0) {
|
|
3129
|
+
spinner7.stop("No target locales configured.");
|
|
3130
|
+
p11.log.info("Add target locales with `vocoder locales add <code>`.");
|
|
3131
|
+
return 1;
|
|
3132
|
+
}
|
|
3133
|
+
const snapshot = await api.getTranslationSnapshot({ branch, targetLocales });
|
|
3134
|
+
spinner7.stop(`Fetched translations for ${chalk11.cyan(branch)}`);
|
|
3135
|
+
if (snapshot.status === "NOT_FOUND") {
|
|
3136
|
+
p11.log.warn(
|
|
3137
|
+
`No translation snapshot found for branch "${branch}". Run \`vocoder sync\` to generate one.`
|
|
3138
|
+
);
|
|
3139
|
+
return 1;
|
|
3140
|
+
}
|
|
3141
|
+
const translations = snapshot.translations ?? {};
|
|
3142
|
+
if (options.output) {
|
|
3143
|
+
writeLocaleFiles(translations, options.output);
|
|
3144
|
+
} else {
|
|
3145
|
+
process.stdout.write(JSON.stringify(translations, null, 2));
|
|
3146
|
+
process.stdout.write("\n");
|
|
3147
|
+
}
|
|
3148
|
+
return 0;
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
spinner7.stop("Failed to fetch translations.");
|
|
3151
|
+
p11.log.error(
|
|
3152
|
+
error instanceof Error ? error.message : "Unknown error."
|
|
3153
|
+
);
|
|
3154
|
+
return 1;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
function writeLocaleFiles(translations, outputDir) {
|
|
3158
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
3159
|
+
for (const [locale, strings] of Object.entries(translations)) {
|
|
3160
|
+
const filePath = join4(outputDir, `${locale}.json`);
|
|
3161
|
+
writeFileSync4(filePath, JSON.stringify(strings, null, 2) + "\n", "utf-8");
|
|
3162
|
+
p11.log.success(`Wrote ${chalk11.cyan(filePath)}`);
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// src/commands/create-project.ts
|
|
3167
|
+
import * as p12 from "@clack/prompts";
|
|
3168
|
+
import chalk12 from "chalk";
|
|
3169
|
+
import { config as loadEnv6 } from "dotenv";
|
|
3170
|
+
loadEnv6();
|
|
3171
|
+
async function createProject(options) {
|
|
3172
|
+
const authData = readAuthData();
|
|
3173
|
+
if (!authData) {
|
|
3174
|
+
p12.log.error(
|
|
3175
|
+
"Not logged in. Run `npx @vocoder/cli init` to authenticate first."
|
|
3176
|
+
);
|
|
3177
|
+
return 1;
|
|
3178
|
+
}
|
|
3179
|
+
const apiUrl = options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app";
|
|
3180
|
+
const api = new VocoderAPI({ apiKey: "", apiUrl });
|
|
3181
|
+
let repoCanonical;
|
|
3182
|
+
let appDir = options.appDir ?? ".";
|
|
3183
|
+
if (options.repo) {
|
|
3184
|
+
repoCanonical = options.repo;
|
|
3185
|
+
} else {
|
|
3186
|
+
const identity = resolveGitRepositoryIdentity();
|
|
3187
|
+
if (identity) {
|
|
3188
|
+
repoCanonical = identity.repoCanonical;
|
|
3189
|
+
if (!options.appDir && identity.repoAppDir) {
|
|
3190
|
+
appDir = identity.repoAppDir;
|
|
3191
|
+
}
|
|
3192
|
+
} else {
|
|
3193
|
+
p12.log.warn(
|
|
3194
|
+
"Could not detect a git remote. The project will be created without repo binding \u2014 sync-on-push will not function until a repository is connected via the Vocoder dashboard."
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
const targetLocales = options.targetLocales ? options.targetLocales.split(",").map((l) => l.trim()).filter(Boolean) : [];
|
|
3199
|
+
const targetBranches = options.targetBranches ? options.targetBranches.split(",").map((b) => b.trim()).filter(Boolean) : ["main"];
|
|
3200
|
+
const spinner7 = p12.spinner();
|
|
3201
|
+
spinner7.start(`Creating project "${options.name}"\u2026`);
|
|
3202
|
+
try {
|
|
3203
|
+
const result = await api.createProject(authData.token, {
|
|
3204
|
+
organizationId: options.workspace,
|
|
3205
|
+
name: options.name,
|
|
3206
|
+
sourceLocale: options.sourceLocale,
|
|
3207
|
+
targetLocales,
|
|
3208
|
+
targetBranches,
|
|
3209
|
+
appDirs: [appDir],
|
|
3210
|
+
...repoCanonical ? { repoCanonical } : {}
|
|
3211
|
+
});
|
|
3212
|
+
spinner7.stop(`Created project ${chalk12.bold(result.projectName)}`);
|
|
3213
|
+
const lines = [
|
|
3214
|
+
`Project ID: ${result.projectId}`,
|
|
3215
|
+
`Source locale: ${chalk12.cyan(result.sourceLocale)}`,
|
|
3216
|
+
`Target locales: ${result.targetLocales.length > 0 ? result.targetLocales.map((l) => chalk12.cyan(l)).join(", ") : chalk12.dim("(none)")}`,
|
|
3217
|
+
`Branches: ${result.targetBranches.map((b) => chalk12.cyan(b)).join(", ")}`,
|
|
3218
|
+
...repoCanonical ? [`Repository: ${chalk12.cyan(repoCanonical)}${appDir !== "." ? ` (${appDir})` : ""}`] : [],
|
|
3219
|
+
"",
|
|
3220
|
+
`Add this to your .env file:`,
|
|
3221
|
+
` ${chalk12.bold("VOCODER_API_KEY")}=${chalk12.cyan(result.apiKey)}`
|
|
3222
|
+
];
|
|
3223
|
+
p12.note(lines.join("\n"), "Project created");
|
|
3224
|
+
if (!result.repositoryBound && repoCanonical) {
|
|
3225
|
+
p12.log.warn(
|
|
3226
|
+
`Repository "${repoCanonical}" was not automatically connected. Ensure your GitHub App installation covers this repository.`
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
return 0;
|
|
3230
|
+
} catch (error) {
|
|
3231
|
+
spinner7.stop("Failed to create project.");
|
|
3232
|
+
if (error instanceof VocoderAPIError && error.limitError) {
|
|
3233
|
+
const { limitError } = error;
|
|
3234
|
+
p12.log.error(limitError.message);
|
|
3235
|
+
for (const line of getLimitErrorGuidance(limitError)) {
|
|
3236
|
+
p12.log.info(line);
|
|
3237
|
+
}
|
|
3238
|
+
return 1;
|
|
3239
|
+
}
|
|
3240
|
+
p12.log.error(
|
|
3241
|
+
error instanceof Error ? error.message : "Unknown error."
|
|
3242
|
+
);
|
|
3243
|
+
return 1;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
// src/commands/whoami.ts
|
|
3248
|
+
import * as p13 from "@clack/prompts";
|
|
3249
|
+
import chalk13 from "chalk";
|
|
2971
3250
|
async function whoami(options = {}) {
|
|
2972
3251
|
const stored = readAuthData();
|
|
2973
3252
|
if (!stored) {
|
|
2974
|
-
|
|
3253
|
+
p13.log.info("Not logged in. Run `vocoder init` to authenticate.");
|
|
2975
3254
|
return 1;
|
|
2976
3255
|
}
|
|
2977
3256
|
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
2978
3257
|
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
2979
3258
|
try {
|
|
2980
3259
|
const info = await api.getCliUserInfo(stored.token);
|
|
2981
|
-
|
|
3260
|
+
p13.log.info(`Logged in as ${chalk13.bold(info.email)}`);
|
|
2982
3261
|
if (info.name) {
|
|
2983
|
-
|
|
3262
|
+
p13.log.info(`Name: ${info.name}`);
|
|
2984
3263
|
}
|
|
2985
|
-
|
|
3264
|
+
p13.log.info(`API: ${apiUrl}`);
|
|
2986
3265
|
return 0;
|
|
2987
3266
|
} catch {
|
|
2988
|
-
|
|
3267
|
+
p13.log.error(
|
|
2989
3268
|
"Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
|
|
2990
3269
|
);
|
|
2991
3270
|
return 1;
|
|
@@ -3017,5 +3296,38 @@ program.command("sync").description("Extract strings and sync translations").opt
|
|
|
3017
3296
|
});
|
|
3018
3297
|
program.command("logout").description("Log out and remove stored credentials").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(logout, options));
|
|
3019
3298
|
program.command("whoami").description("Show the currently authenticated user").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(whoami, options));
|
|
3299
|
+
var localesCmd = program.command("locales").description("Manage project target locales").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(listProjectLocales, options));
|
|
3300
|
+
localesCmd.command("add <codes...>").description("Add one or more target locales by BCP 47 code (e.g. fr de pt-BR)").option("--api-url <url>", "Override Vocoder API URL").action(
|
|
3301
|
+
(codes, options) => runCommand((opts) => addLocales(codes, opts), options)
|
|
3302
|
+
);
|
|
3303
|
+
localesCmd.command("remove <codes...>").description("Remove one or more target locales by BCP 47 code").option("--api-url <url>", "Override Vocoder API URL").action(
|
|
3304
|
+
(codes, options) => runCommand((opts) => removeLocales(codes, opts), options)
|
|
3305
|
+
);
|
|
3306
|
+
localesCmd.command("supported").description("List all locales supported by Vocoder").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(listSupportedLocales, options));
|
|
3307
|
+
program.command("project").description("Show current project configuration").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(projectConfig, options));
|
|
3308
|
+
program.command("translations").description("Download the current translation snapshot").option("--branch <branch>", "Git branch (auto-detected if omitted)").option("--locale <locale>", "Fetch a specific locale only").option("--output <dir>", "Write locale JSON files to this directory").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(getTranslations, options));
|
|
3309
|
+
program.command("create-project").description("Create a new Vocoder project (requires prior `vocoder init`)").requiredOption("--name <name>", "Project display name").requiredOption("--source-locale <code>", "Source language BCP 47 code (e.g. en)").requiredOption("--workspace <org-id>", "Workspace organization ID").option(
|
|
3310
|
+
"--target-locales <codes>",
|
|
3311
|
+
"Comma-separated target locale codes (e.g. fr,de,pt-BR)"
|
|
3312
|
+
).option(
|
|
3313
|
+
"--target-branches <branches>",
|
|
3314
|
+
"Comma-separated branch names to sync (default: main)"
|
|
3315
|
+
).option(
|
|
3316
|
+
"--repo <canonical>",
|
|
3317
|
+
"Git repo canonical (e.g. github:owner/repo). Auto-detected from git remote if omitted."
|
|
3318
|
+
).option(
|
|
3319
|
+
"--app-dir <path>",
|
|
3320
|
+
"App directory within the repo for monorepos (default: .)"
|
|
3321
|
+
).option("--api-url <url>", "Override Vocoder API URL").action((options) => {
|
|
3322
|
+
const translated = {
|
|
3323
|
+
...options,
|
|
3324
|
+
// Commander camelCases dashed options
|
|
3325
|
+
sourceLocale: options.sourceLocale,
|
|
3326
|
+
targetLocales: options.targetLocales,
|
|
3327
|
+
targetBranches: options.targetBranches,
|
|
3328
|
+
workspace: options.workspace
|
|
3329
|
+
};
|
|
3330
|
+
return runCommand(createProject, translated);
|
|
3331
|
+
});
|
|
3020
3332
|
program.parse(process.argv);
|
|
3021
3333
|
//# sourceMappingURL=bin.mjs.map
|