@vocoder/cli 0.11.0 → 0.12.0
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 +137 -1
- package/dist/bin.mjs +485 -144
- package/dist/bin.mjs.map +1 -1
- package/dist/{chunk-XF3KGGYQ.mjs → chunk-XUCVAFBG.mjs} +59 -1
- package/dist/{chunk-XF3KGGYQ.mjs.map → chunk-XUCVAFBG.mjs.map} +1 -1
- package/dist/lib.d.mts +50 -2
- 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-XUCVAFBG.mjs";
|
|
16
17
|
|
|
17
18
|
// src/bin.ts
|
|
18
19
|
import { Command } from "commander";
|
|
@@ -1447,17 +1448,6 @@ function printCodeBlock(code) {
|
|
|
1447
1448
|
`
|
|
1448
1449
|
);
|
|
1449
1450
|
}
|
|
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
1451
|
async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
1462
1452
|
let server = null;
|
|
1463
1453
|
if (!options.ci) {
|
|
@@ -1638,17 +1628,17 @@ async function init(options = {}) {
|
|
|
1638
1628
|
true
|
|
1639
1629
|
);
|
|
1640
1630
|
if (!authResult) return 1;
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1631
|
+
const spinner7 = p5.spinner();
|
|
1632
|
+
spinner7.start("Generating new API key...");
|
|
1643
1633
|
try {
|
|
1644
1634
|
const { apiKey } = await anonApi2.regenerateProjectApiKey(
|
|
1645
1635
|
authResult.token,
|
|
1646
1636
|
exactMatch.projectId
|
|
1647
1637
|
);
|
|
1648
|
-
|
|
1638
|
+
spinner7.stop("New API key generated");
|
|
1649
1639
|
printApiKey(apiKey);
|
|
1650
1640
|
} catch (err) {
|
|
1651
|
-
|
|
1641
|
+
spinner7.stop("Failed to generate key");
|
|
1652
1642
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1653
1643
|
p5.log.error(`Could not generate API key: ${msg}`);
|
|
1654
1644
|
p5.log.info("Try again or generate one from the dashboard.");
|
|
@@ -1683,46 +1673,23 @@ async function init(options = {}) {
|
|
|
1683
1673
|
let userEmail;
|
|
1684
1674
|
let userName;
|
|
1685
1675
|
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
|
-
}
|
|
1676
|
+
const storedAuth = await verifyStoredAuth(api);
|
|
1677
|
+
if (storedAuth.status === "valid") {
|
|
1678
|
+
p5.log.success(`Authenticated as ${chalk6.bold(storedAuth.email)}`);
|
|
1679
|
+
userToken = storedAuth.token;
|
|
1680
|
+
userEmail = storedAuth.email;
|
|
1681
|
+
userName = storedAuth.name;
|
|
1721
1682
|
} else {
|
|
1683
|
+
const reauth = storedAuth.status === "expired";
|
|
1684
|
+
if (reauth) {
|
|
1685
|
+
p5.log.warn("Stored credentials expired \u2014 signing in again");
|
|
1686
|
+
} else if (storedAuth.status === "gone") {
|
|
1687
|
+
p5.log.warn("Account not found \u2014 starting fresh setup");
|
|
1688
|
+
}
|
|
1722
1689
|
const authResult = await runAuthFlow(
|
|
1723
1690
|
api,
|
|
1724
1691
|
options,
|
|
1725
|
-
|
|
1692
|
+
reauth,
|
|
1726
1693
|
identity?.repoCanonical
|
|
1727
1694
|
);
|
|
1728
1695
|
if (!authResult) return 1;
|
|
@@ -2142,30 +2109,16 @@ Translations won't run automatically until you grant access.
|
|
|
2142
2109
|
}
|
|
2143
2110
|
}
|
|
2144
2111
|
|
|
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
|
-
}
|
|
2112
|
+
// src/commands/locales.ts
|
|
2113
|
+
import * as p8 from "@clack/prompts";
|
|
2114
|
+
import chalk9 from "chalk";
|
|
2115
|
+
import { config as loadEnv3 } from "dotenv";
|
|
2163
2116
|
|
|
2164
2117
|
// src/commands/sync.ts
|
|
2165
2118
|
import { createHash, randomUUID } from "crypto";
|
|
2166
2119
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2167
2120
|
import { join as join3 } from "path";
|
|
2168
|
-
import * as
|
|
2121
|
+
import * as p7 from "@clack/prompts";
|
|
2169
2122
|
import chalk8 from "chalk";
|
|
2170
2123
|
|
|
2171
2124
|
// src/utils/branch.ts
|
|
@@ -2235,7 +2188,7 @@ function matchBranchPattern(branch, pattern) {
|
|
|
2235
2188
|
}
|
|
2236
2189
|
|
|
2237
2190
|
// src/utils/config.ts
|
|
2238
|
-
import * as
|
|
2191
|
+
import * as p6 from "@clack/prompts";
|
|
2239
2192
|
import chalk7 from "chalk";
|
|
2240
2193
|
import { config as loadEnv2 } from "dotenv";
|
|
2241
2194
|
loadEnv2();
|
|
@@ -2294,7 +2247,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2294
2247
|
};
|
|
2295
2248
|
const fileConfig = loadVocoderConfig(process.cwd());
|
|
2296
2249
|
if (!fileConfig) {
|
|
2297
|
-
|
|
2250
|
+
p6.log.warn(
|
|
2298
2251
|
`No ${chalk7.cyan("vocoder.config.ts")} found \u2014 run ${chalk7.cyan("npx @vocoder/cli init")} to generate one.`
|
|
2299
2252
|
);
|
|
2300
2253
|
}
|
|
@@ -2325,7 +2278,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2325
2278
|
excludePattern = fileConfig.exclude;
|
|
2326
2279
|
configSources.excludePattern = "vocoder.config";
|
|
2327
2280
|
} else if (envExcludePattern) {
|
|
2328
|
-
excludePattern = envExcludePattern.split(",").map((
|
|
2281
|
+
excludePattern = envExcludePattern.split(",").map((p14) => p14.trim()).filter(Boolean);
|
|
2329
2282
|
configSources.excludePattern = "environment";
|
|
2330
2283
|
} else {
|
|
2331
2284
|
excludePattern = defaults.excludePattern;
|
|
@@ -2382,7 +2335,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2382
2335
|
...maxWaitMs ? [`Max wait: ${chalk7.cyan(String(configSources.maxWaitMs))}`] : [],
|
|
2383
2336
|
`No fallback: ${chalk7.cyan(String(configSources.noFallback))}`
|
|
2384
2337
|
];
|
|
2385
|
-
|
|
2338
|
+
p6.note(lines.join("\n"), "Configuration sources");
|
|
2386
2339
|
}
|
|
2387
2340
|
return {
|
|
2388
2341
|
includePattern,
|
|
@@ -2560,6 +2513,13 @@ function getLimitErrorGuidance(limitError) {
|
|
|
2560
2513
|
`Upgrade plan: ${limitError.upgradeUrl}`
|
|
2561
2514
|
];
|
|
2562
2515
|
}
|
|
2516
|
+
if (limitError.limitType === "target_locales") {
|
|
2517
|
+
return [
|
|
2518
|
+
`Current target locales: ${limitError.current}`,
|
|
2519
|
+
`Plan limit: ${limitError.current} (${limitError.planId})`,
|
|
2520
|
+
`Upgrade plan: ${limitError.upgradeUrl}`
|
|
2521
|
+
];
|
|
2522
|
+
}
|
|
2563
2523
|
return [
|
|
2564
2524
|
`Plan: ${limitError.planId}`,
|
|
2565
2525
|
`Current: ${limitError.current}`,
|
|
@@ -2644,22 +2604,22 @@ async function fetchApiSnapshot(api, params) {
|
|
|
2644
2604
|
async function sync(options = {}) {
|
|
2645
2605
|
const startTime = Date.now();
|
|
2646
2606
|
const projectRoot = process.cwd();
|
|
2647
|
-
|
|
2607
|
+
p7.intro("Vocoder Sync");
|
|
2648
2608
|
const mergedConfig = await getMergedConfig(options, options.verbose);
|
|
2649
2609
|
if (!mergedConfig.apiKey) {
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2610
|
+
p7.log.warn("No API key found. Run init to get started:");
|
|
2611
|
+
p7.log.info(" npx @vocoder/cli init");
|
|
2612
|
+
p7.log.info("");
|
|
2613
|
+
p7.log.info(
|
|
2654
2614
|
" Or add your key to .env: VOCODER_API_KEY=vcp_..."
|
|
2655
2615
|
);
|
|
2656
|
-
|
|
2616
|
+
p7.outro("Run `npx @vocoder/cli init` to set up your project.");
|
|
2657
2617
|
return 1;
|
|
2658
2618
|
}
|
|
2659
|
-
const
|
|
2619
|
+
const spinner7 = p7.spinner();
|
|
2660
2620
|
try {
|
|
2661
2621
|
const branch = detectBranch(options.branch);
|
|
2662
|
-
|
|
2622
|
+
spinner7.start("Loading project configuration");
|
|
2663
2623
|
const localConfig = {
|
|
2664
2624
|
apiKey: mergedConfig.apiKey,
|
|
2665
2625
|
apiUrl: mergedConfig.apiUrl || "https://vocoder.app"
|
|
@@ -2683,18 +2643,18 @@ async function sync(options = {}) {
|
|
|
2683
2643
|
...fileConfig?.appIndustry ? { appIndustry: fileConfig.appIndustry } : {},
|
|
2684
2644
|
...fileConfig?.formality ? { formality: fileConfig.formality } : {}
|
|
2685
2645
|
};
|
|
2686
|
-
|
|
2646
|
+
spinner7.stop(`Branch: ${chalk8.cyan(branch)}`);
|
|
2687
2647
|
if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
|
|
2688
|
-
|
|
2648
|
+
p7.log.warn(
|
|
2689
2649
|
`Skipping translations (${chalk8.cyan(branch)} is not a target branch)`
|
|
2690
2650
|
);
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2651
|
+
p7.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
|
|
2652
|
+
p7.log.info("Use --force to translate anyway");
|
|
2653
|
+
p7.outro("");
|
|
2694
2654
|
return 0;
|
|
2695
2655
|
}
|
|
2696
2656
|
const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
|
|
2697
|
-
|
|
2657
|
+
spinner7.start(`Extracting strings from ${patternsDisplay}`);
|
|
2698
2658
|
const extractor = new StringExtractor();
|
|
2699
2659
|
const extractedStrings = await extractor.extractFromProject(
|
|
2700
2660
|
config.includePattern,
|
|
@@ -2702,14 +2662,14 @@ async function sync(options = {}) {
|
|
|
2702
2662
|
config.excludePattern
|
|
2703
2663
|
);
|
|
2704
2664
|
if (extractedStrings.length === 0) {
|
|
2705
|
-
|
|
2706
|
-
|
|
2665
|
+
spinner7.stop("No translatable strings found");
|
|
2666
|
+
p7.log.warn(
|
|
2707
2667
|
"Make sure you are wrapping translatable strings with Vocoder"
|
|
2708
2668
|
);
|
|
2709
|
-
|
|
2669
|
+
p7.outro("");
|
|
2710
2670
|
return 0;
|
|
2711
2671
|
}
|
|
2712
|
-
|
|
2672
|
+
spinner7.stop(
|
|
2713
2673
|
`Extracted ${chalk8.cyan(extractedStrings.length)} strings from ${chalk8.cyan(patternsDisplay)}`
|
|
2714
2674
|
);
|
|
2715
2675
|
if (options.verbose) {
|
|
@@ -2717,10 +2677,10 @@ async function sync(options = {}) {
|
|
|
2717
2677
|
if (extractedStrings.length > 5) {
|
|
2718
2678
|
sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
|
|
2719
2679
|
}
|
|
2720
|
-
|
|
2680
|
+
p7.note(sampleLines.join("\n"), "Sample strings");
|
|
2721
2681
|
}
|
|
2722
2682
|
if (options.dryRun) {
|
|
2723
|
-
|
|
2683
|
+
p7.note(
|
|
2724
2684
|
[
|
|
2725
2685
|
`Strings: ${extractedStrings.length}`,
|
|
2726
2686
|
`Branch: ${branch}`,
|
|
@@ -2731,12 +2691,12 @@ async function sync(options = {}) {
|
|
|
2731
2691
|
].join("\n"),
|
|
2732
2692
|
"Dry run - would translate"
|
|
2733
2693
|
);
|
|
2734
|
-
|
|
2694
|
+
p7.outro("No API calls made.");
|
|
2735
2695
|
return 0;
|
|
2736
2696
|
}
|
|
2737
2697
|
const repoIdentity = resolveGitRepositoryIdentity();
|
|
2738
2698
|
if (!repoIdentity && options.verbose) {
|
|
2739
|
-
|
|
2699
|
+
p7.log.warn(
|
|
2740
2700
|
"Could not detect git remote origin. Sync will continue without repo metadata."
|
|
2741
2701
|
);
|
|
2742
2702
|
}
|
|
@@ -2744,7 +2704,7 @@ async function sync(options = {}) {
|
|
|
2744
2704
|
const stringEntries = buildStringEntries(extractedStrings);
|
|
2745
2705
|
const sourceStrings = stringEntries.map((entry) => entry.text);
|
|
2746
2706
|
if (options.verbose && stringEntries.length !== extractedStrings.length) {
|
|
2747
|
-
|
|
2707
|
+
p7.log.info(
|
|
2748
2708
|
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
|
|
2749
2709
|
);
|
|
2750
2710
|
}
|
|
@@ -2753,17 +2713,17 @@ async function sync(options = {}) {
|
|
|
2753
2713
|
const cacheFile = getCacheFilePath(projectRoot, fingerprint);
|
|
2754
2714
|
if (existsSync3(cacheFile)) {
|
|
2755
2715
|
if (options.verbose) {
|
|
2756
|
-
|
|
2716
|
+
p7.log.info(`Cache hit: ${chalk8.dim(cacheFile)} (fingerprint ${chalk8.cyan(fingerprint)})`);
|
|
2757
2717
|
}
|
|
2758
2718
|
const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2759
|
-
|
|
2719
|
+
p7.outro(`Up to date (${duration2}s)`);
|
|
2760
2720
|
return 0;
|
|
2761
2721
|
}
|
|
2762
2722
|
if (options.verbose) {
|
|
2763
|
-
|
|
2723
|
+
p7.log.info(`No cache for fingerprint ${chalk8.cyan(fingerprint)} \u2014 will submit to API`);
|
|
2764
2724
|
}
|
|
2765
2725
|
}
|
|
2766
|
-
|
|
2726
|
+
spinner7.start("Submitting strings to Vocoder API");
|
|
2767
2727
|
const batchResponse = await api.submitTranslation(
|
|
2768
2728
|
branch,
|
|
2769
2729
|
stringEntries,
|
|
@@ -2778,33 +2738,33 @@ async function sync(options = {}) {
|
|
|
2778
2738
|
},
|
|
2779
2739
|
repoIdentity ? { ...repoIdentity, commitSha } : { commitSha }
|
|
2780
2740
|
);
|
|
2781
|
-
|
|
2741
|
+
spinner7.stop("Strings submitted");
|
|
2782
2742
|
const effectiveMode = batchResponse.effectiveMode ?? resolveEffectiveModeFromPolicy({
|
|
2783
2743
|
branch,
|
|
2784
2744
|
requestedMode,
|
|
2785
2745
|
policy: config.syncPolicy
|
|
2786
2746
|
});
|
|
2787
2747
|
if (options.verbose) {
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2748
|
+
p7.log.info(`Batch: ${chalk8.dim(batchResponse.batchId)}`);
|
|
2749
|
+
p7.log.info(`Requested mode: ${requestedMode}`);
|
|
2750
|
+
p7.log.info(`Effective mode: ${effectiveMode}`);
|
|
2751
|
+
p7.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
|
|
2792
2752
|
if (batchResponse.queueStatus) {
|
|
2793
|
-
|
|
2753
|
+
p7.log.info(`Queue status: ${batchResponse.queueStatus}`);
|
|
2794
2754
|
}
|
|
2795
2755
|
}
|
|
2796
2756
|
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
2797
|
-
|
|
2757
|
+
p7.log.success(`Up to date \u2014 ${chalk8.cyan(batchResponse.totalStrings)} strings, no changes`);
|
|
2798
2758
|
} else if (batchResponse.newStrings === 0) {
|
|
2799
2759
|
const archivedNote = batchResponse.deletedStrings && batchResponse.deletedStrings > 0 ? `, ${chalk8.yellow(batchResponse.deletedStrings)} archived` : "";
|
|
2800
|
-
|
|
2760
|
+
p7.log.success(`No new strings \u2014 ${chalk8.cyan(batchResponse.totalStrings)} total${archivedNote}, using existing translations`);
|
|
2801
2761
|
} else {
|
|
2802
2762
|
const statParts = [`${chalk8.cyan(batchResponse.newStrings)} new, ${chalk8.cyan(batchResponse.totalStrings)} total`];
|
|
2803
2763
|
if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
|
|
2804
2764
|
statParts.push(`${chalk8.yellow(batchResponse.deletedStrings)} archived`);
|
|
2805
2765
|
}
|
|
2806
2766
|
const estTime = batchResponse.estimatedTime ? ` (~${batchResponse.estimatedTime}s)` : "";
|
|
2807
|
-
|
|
2767
|
+
p7.log.info(`${statParts.join(", ")} \u2192 syncing to ${config.targetLocales.join(", ")}${estTime}`);
|
|
2808
2768
|
}
|
|
2809
2769
|
let artifacts = null;
|
|
2810
2770
|
if (batchResponse.translations) {
|
|
@@ -2816,7 +2776,7 @@ async function sync(options = {}) {
|
|
|
2816
2776
|
let waitError = null;
|
|
2817
2777
|
if (!artifacts && (effectiveMode === "required" || effectiveMode === "best-effort")) {
|
|
2818
2778
|
const waitTimeoutSecs = Math.round(waitTimeoutMs / 1e3);
|
|
2819
|
-
|
|
2779
|
+
spinner7.start(`Waiting for translations (max ${waitTimeoutSecs}s)`);
|
|
2820
2780
|
let lastProgress = 0;
|
|
2821
2781
|
try {
|
|
2822
2782
|
const completion = await api.waitForCompletion(
|
|
@@ -2825,7 +2785,7 @@ async function sync(options = {}) {
|
|
|
2825
2785
|
(progress) => {
|
|
2826
2786
|
const percent = Math.round(progress * 100);
|
|
2827
2787
|
if (percent > lastProgress) {
|
|
2828
|
-
|
|
2788
|
+
spinner7.message(`Translating... ${percent}%`);
|
|
2829
2789
|
lastProgress = percent;
|
|
2830
2790
|
}
|
|
2831
2791
|
}
|
|
@@ -2835,14 +2795,14 @@ async function sync(options = {}) {
|
|
|
2835
2795
|
translations: completion.translations,
|
|
2836
2796
|
localeMetadata: completion.localeMetadata
|
|
2837
2797
|
};
|
|
2838
|
-
|
|
2798
|
+
spinner7.stop("Translations complete");
|
|
2839
2799
|
} catch (error) {
|
|
2840
|
-
|
|
2800
|
+
spinner7.stop("Translation wait incomplete");
|
|
2841
2801
|
waitError = error instanceof Error ? error : new Error(String(error));
|
|
2842
2802
|
if (effectiveMode === "required") {
|
|
2843
2803
|
throw waitError;
|
|
2844
2804
|
}
|
|
2845
|
-
|
|
2805
|
+
p7.log.warn(`Best-effort wait ended early: ${waitError.message}`);
|
|
2846
2806
|
}
|
|
2847
2807
|
}
|
|
2848
2808
|
if (!artifacts) {
|
|
@@ -2851,14 +2811,14 @@ async function sync(options = {}) {
|
|
|
2851
2811
|
"Fresh translations are not available and fallback is disabled (--no-fallback)."
|
|
2852
2812
|
);
|
|
2853
2813
|
}
|
|
2854
|
-
|
|
2814
|
+
spinner7.start("Loading fallback translations");
|
|
2855
2815
|
const localFallback = readLocalCache({
|
|
2856
2816
|
projectRoot,
|
|
2857
2817
|
fingerprint
|
|
2858
2818
|
});
|
|
2859
2819
|
if (localFallback) {
|
|
2860
2820
|
artifacts = localFallback;
|
|
2861
|
-
|
|
2821
|
+
spinner7.stop(`Using local cached snapshot (${fingerprint})`);
|
|
2862
2822
|
} else {
|
|
2863
2823
|
try {
|
|
2864
2824
|
const apiSnapshot = await fetchApiSnapshot(api, {
|
|
@@ -2867,15 +2827,15 @@ async function sync(options = {}) {
|
|
|
2867
2827
|
});
|
|
2868
2828
|
if (apiSnapshot) {
|
|
2869
2829
|
artifacts = apiSnapshot;
|
|
2870
|
-
|
|
2830
|
+
spinner7.stop("Using latest completed API snapshot");
|
|
2871
2831
|
} else {
|
|
2872
|
-
|
|
2832
|
+
spinner7.stop("No completed API snapshot available");
|
|
2873
2833
|
}
|
|
2874
2834
|
} catch (error) {
|
|
2875
|
-
|
|
2835
|
+
spinner7.stop("Failed to fetch API snapshot");
|
|
2876
2836
|
if (options.verbose) {
|
|
2877
2837
|
const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
|
|
2878
|
-
|
|
2838
|
+
p7.log.warn(`Snapshot fetch error: ${message}`);
|
|
2879
2839
|
}
|
|
2880
2840
|
}
|
|
2881
2841
|
}
|
|
@@ -2907,85 +2867,433 @@ async function sync(options = {}) {
|
|
|
2907
2867
|
});
|
|
2908
2868
|
const cachePath = writeCache({ projectRoot, fingerprint, data });
|
|
2909
2869
|
if (options.verbose) {
|
|
2910
|
-
|
|
2870
|
+
p7.log.info(`Cache written: ${cachePath}`);
|
|
2911
2871
|
}
|
|
2912
2872
|
} catch (error) {
|
|
2913
2873
|
if (options.verbose) {
|
|
2914
2874
|
const message = error instanceof Error ? error.message : "Unknown cache write error";
|
|
2915
|
-
|
|
2875
|
+
p7.log.warn(`Failed to write cache: ${message}`);
|
|
2916
2876
|
}
|
|
2917
2877
|
}
|
|
2918
2878
|
if (artifacts.source !== "fresh") {
|
|
2919
2879
|
const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
|
|
2920
|
-
|
|
2880
|
+
p7.log.warn(
|
|
2921
2881
|
`Using ${sourceLabel}. New strings may appear after the background sync completes.`
|
|
2922
2882
|
);
|
|
2923
2883
|
}
|
|
2924
2884
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2925
|
-
|
|
2885
|
+
p7.outro(`Sync complete! (${duration}s)`);
|
|
2926
2886
|
return 0;
|
|
2927
2887
|
} catch (error) {
|
|
2928
|
-
|
|
2888
|
+
spinner7.stop();
|
|
2929
2889
|
if (error instanceof VocoderAPIError && error.syncPolicyError) {
|
|
2930
|
-
|
|
2890
|
+
p7.log.error(error.syncPolicyError.message);
|
|
2931
2891
|
const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
|
|
2932
2892
|
for (const line of guidance) {
|
|
2933
|
-
|
|
2893
|
+
p7.log.info(line);
|
|
2934
2894
|
}
|
|
2935
2895
|
return 1;
|
|
2936
2896
|
}
|
|
2937
2897
|
if (error instanceof VocoderAPIError && error.limitError) {
|
|
2938
2898
|
const { limitError } = error;
|
|
2939
|
-
|
|
2899
|
+
p7.log.error(limitError.message);
|
|
2940
2900
|
const guidance = getLimitErrorGuidance(limitError);
|
|
2941
2901
|
for (const line of guidance) {
|
|
2942
|
-
|
|
2902
|
+
p7.log.info(line);
|
|
2943
2903
|
}
|
|
2944
2904
|
return 1;
|
|
2945
2905
|
}
|
|
2946
2906
|
if (error instanceof Error) {
|
|
2947
|
-
|
|
2907
|
+
p7.log.error(error.message);
|
|
2948
2908
|
const isInvalidKey = error.message.toLowerCase().includes("invalid api key") || error instanceof VocoderAPIError && error.status === 401;
|
|
2949
2909
|
if (isInvalidKey) {
|
|
2950
|
-
|
|
2910
|
+
p7.log.warn(
|
|
2951
2911
|
"API key rejected \u2014 the project may have been deleted or the key revoked."
|
|
2952
2912
|
);
|
|
2953
|
-
|
|
2913
|
+
p7.log.info(
|
|
2954
2914
|
" Run `npx @vocoder/cli init` to create a new project and key."
|
|
2955
2915
|
);
|
|
2956
2916
|
} else if (error.message.includes("git branch")) {
|
|
2957
|
-
|
|
2958
|
-
|
|
2917
|
+
p7.log.warn("Run from a git repository, or use:");
|
|
2918
|
+
p7.log.info(" vocoder sync --branch main");
|
|
2959
2919
|
}
|
|
2960
2920
|
if (options.verbose) {
|
|
2961
|
-
|
|
2921
|
+
p7.log.info(`Full error: ${error.stack ?? error}`);
|
|
2962
2922
|
}
|
|
2963
2923
|
}
|
|
2964
2924
|
return 1;
|
|
2965
2925
|
}
|
|
2966
2926
|
}
|
|
2967
2927
|
|
|
2968
|
-
// src/commands/
|
|
2928
|
+
// src/commands/locales.ts
|
|
2929
|
+
loadEnv3();
|
|
2930
|
+
function getApiConfig(options) {
|
|
2931
|
+
const apiKey = process.env.VOCODER_API_KEY;
|
|
2932
|
+
if (!apiKey) {
|
|
2933
|
+
p8.log.error(
|
|
2934
|
+
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
2935
|
+
);
|
|
2936
|
+
return null;
|
|
2937
|
+
}
|
|
2938
|
+
return {
|
|
2939
|
+
apiKey,
|
|
2940
|
+
apiUrl: options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app"
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
async function listProjectLocales(options = {}) {
|
|
2944
|
+
const config = getApiConfig(options);
|
|
2945
|
+
if (!config) return 1;
|
|
2946
|
+
const api = new VocoderAPI(config);
|
|
2947
|
+
try {
|
|
2948
|
+
const projectConfig2 = await api.getProjectConfig();
|
|
2949
|
+
p8.log.info(
|
|
2950
|
+
`Source locale: ${chalk9.cyan(projectConfig2.sourceLocale)}`
|
|
2951
|
+
);
|
|
2952
|
+
if (projectConfig2.targetLocales.length === 0) {
|
|
2953
|
+
p8.log.info("Target locales: (none configured)");
|
|
2954
|
+
} else {
|
|
2955
|
+
p8.log.info(
|
|
2956
|
+
`Target locales: ${projectConfig2.targetLocales.map((l) => chalk9.cyan(l)).join(", ")}`
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
return 0;
|
|
2960
|
+
} catch (error) {
|
|
2961
|
+
p8.log.error(
|
|
2962
|
+
error instanceof Error ? error.message : "Failed to fetch project locales."
|
|
2963
|
+
);
|
|
2964
|
+
return 1;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
async function addLocales(locales, options = {}) {
|
|
2968
|
+
if (locales.length === 0) {
|
|
2969
|
+
p8.log.error("No locale codes provided.");
|
|
2970
|
+
return 1;
|
|
2971
|
+
}
|
|
2972
|
+
const config = getApiConfig(options);
|
|
2973
|
+
if (!config) return 1;
|
|
2974
|
+
const api = new VocoderAPI(config);
|
|
2975
|
+
let lastTargetLocales = [];
|
|
2976
|
+
let hadError = false;
|
|
2977
|
+
for (const locale of locales) {
|
|
2978
|
+
const spinner7 = p8.spinner();
|
|
2979
|
+
spinner7.start(`Adding ${locale}\u2026`);
|
|
2980
|
+
try {
|
|
2981
|
+
const result = await api.addLocale(locale);
|
|
2982
|
+
lastTargetLocales = result.targetLocales;
|
|
2983
|
+
spinner7.stop(`Added ${chalk9.cyan(locale)}`);
|
|
2984
|
+
} catch (error) {
|
|
2985
|
+
spinner7.stop(`Failed to add ${chalk9.red(locale)}`);
|
|
2986
|
+
hadError = true;
|
|
2987
|
+
if (error instanceof VocoderAPIError && error.limitError) {
|
|
2988
|
+
const { limitError } = error;
|
|
2989
|
+
p8.log.error(limitError.message);
|
|
2990
|
+
for (const line of getLimitErrorGuidance(limitError)) {
|
|
2991
|
+
p8.log.info(line);
|
|
2992
|
+
}
|
|
2993
|
+
break;
|
|
2994
|
+
}
|
|
2995
|
+
p8.log.error(
|
|
2996
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
2997
|
+
);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
if (lastTargetLocales.length > 0) {
|
|
3001
|
+
p8.log.info(
|
|
3002
|
+
`Target locales now: ${lastTargetLocales.map((l) => chalk9.cyan(l)).join(", ")}`
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
3005
|
+
return hadError ? 1 : 0;
|
|
3006
|
+
}
|
|
3007
|
+
async function removeLocales(locales, options = {}) {
|
|
3008
|
+
if (locales.length === 0) {
|
|
3009
|
+
p8.log.error("No locale codes provided.");
|
|
3010
|
+
return 1;
|
|
3011
|
+
}
|
|
3012
|
+
const config = getApiConfig(options);
|
|
3013
|
+
if (!config) return 1;
|
|
3014
|
+
const api = new VocoderAPI(config);
|
|
3015
|
+
let lastTargetLocales = [];
|
|
3016
|
+
let hadError = false;
|
|
3017
|
+
for (const locale of locales) {
|
|
3018
|
+
const spinner7 = p8.spinner();
|
|
3019
|
+
spinner7.start(`Removing ${locale}\u2026`);
|
|
3020
|
+
try {
|
|
3021
|
+
const result = await api.removeLocale(locale);
|
|
3022
|
+
lastTargetLocales = result.targetLocales;
|
|
3023
|
+
spinner7.stop(`Removed ${chalk9.cyan(locale)}`);
|
|
3024
|
+
} catch (error) {
|
|
3025
|
+
spinner7.stop(`Failed to remove ${chalk9.red(locale)}`);
|
|
3026
|
+
hadError = true;
|
|
3027
|
+
p8.log.error(
|
|
3028
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
if (lastTargetLocales.length > 0) {
|
|
3033
|
+
p8.log.info(
|
|
3034
|
+
`Target locales now: ${lastTargetLocales.map((l) => chalk9.cyan(l)).join(", ")}`
|
|
3035
|
+
);
|
|
3036
|
+
} else if (!hadError) {
|
|
3037
|
+
p8.log.info("Target locales now: (none configured)");
|
|
3038
|
+
}
|
|
3039
|
+
return hadError ? 1 : 0;
|
|
3040
|
+
}
|
|
3041
|
+
async function listSupportedLocales(options = {}) {
|
|
3042
|
+
const config = getApiConfig(options);
|
|
3043
|
+
if (!config) return 1;
|
|
3044
|
+
const api = new VocoderAPI(config);
|
|
3045
|
+
try {
|
|
3046
|
+
const result = await api.listLocales(config.apiKey);
|
|
3047
|
+
p8.log.info(chalk9.bold("Source locales:"));
|
|
3048
|
+
printLocaleTable(result.sourceLocales);
|
|
3049
|
+
p8.log.info("");
|
|
3050
|
+
p8.log.info(chalk9.bold("Target locales:"));
|
|
3051
|
+
printLocaleTable(result.targetLocales);
|
|
3052
|
+
return 0;
|
|
3053
|
+
} catch (error) {
|
|
3054
|
+
p8.log.error(
|
|
3055
|
+
error instanceof Error ? error.message : "Failed to fetch supported locales."
|
|
3056
|
+
);
|
|
3057
|
+
return 1;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
function printLocaleTable(locales) {
|
|
3061
|
+
for (const locale of locales) {
|
|
3062
|
+
const native = locale.nativeName && locale.nativeName !== locale.name ? ` (${locale.nativeName})` : "";
|
|
3063
|
+
p8.log.info(` ${chalk9.cyan(locale.code.padEnd(10))} ${locale.name}${native}`);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// src/commands/logout.ts
|
|
2969
3068
|
import * as p9 from "@clack/prompts";
|
|
2970
|
-
|
|
3069
|
+
async function logout(options = {}) {
|
|
3070
|
+
const stored = readAuthData();
|
|
3071
|
+
if (!stored) {
|
|
3072
|
+
p9.log.info("Not currently authenticated.");
|
|
3073
|
+
return 0;
|
|
3074
|
+
}
|
|
3075
|
+
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
3076
|
+
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
3077
|
+
try {
|
|
3078
|
+
await api.revokeCliToken(stored.token);
|
|
3079
|
+
} catch {
|
|
3080
|
+
}
|
|
3081
|
+
clearAuthData();
|
|
3082
|
+
p9.log.success(`Logged out (was ${stored.email})`);
|
|
3083
|
+
return 0;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// src/commands/project-config.ts
|
|
3087
|
+
import * as p10 from "@clack/prompts";
|
|
3088
|
+
import chalk10 from "chalk";
|
|
3089
|
+
import { config as loadEnv4 } from "dotenv";
|
|
3090
|
+
loadEnv4();
|
|
3091
|
+
async function projectConfig(options = {}) {
|
|
3092
|
+
const apiKey = process.env.VOCODER_API_KEY;
|
|
3093
|
+
if (!apiKey) {
|
|
3094
|
+
p10.log.error(
|
|
3095
|
+
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
3096
|
+
);
|
|
3097
|
+
return 1;
|
|
3098
|
+
}
|
|
3099
|
+
const apiUrl = options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app";
|
|
3100
|
+
const api = new VocoderAPI({ apiKey, apiUrl });
|
|
3101
|
+
try {
|
|
3102
|
+
const config = await api.getProjectConfig();
|
|
3103
|
+
const lines = [
|
|
3104
|
+
`Project: ${chalk10.bold(config.projectName)}`,
|
|
3105
|
+
`Organization: ${config.organizationName}`,
|
|
3106
|
+
`Source locale: ${chalk10.cyan(config.sourceLocale)}`,
|
|
3107
|
+
`Target locales: ${config.targetLocales.length > 0 ? config.targetLocales.map((l) => chalk10.cyan(l)).join(", ") : chalk10.dim("(none)")}`,
|
|
3108
|
+
`Target branches: ${config.targetBranches.map((b) => chalk10.cyan(b)).join(", ")}`,
|
|
3109
|
+
...config.primaryBranch ? [`Primary branch: ${chalk10.cyan(config.primaryBranch)}`] : [],
|
|
3110
|
+
`Sync policy:`,
|
|
3111
|
+
` Blocking branches: ${config.syncPolicy.blockingBranches.map((b) => chalk10.cyan(b)).join(", ")}`,
|
|
3112
|
+
` Blocking mode: ${chalk10.cyan(config.syncPolicy.blockingMode)}`,
|
|
3113
|
+
` Non-blocking mode: ${chalk10.cyan(config.syncPolicy.nonBlockingMode)}`,
|
|
3114
|
+
` Max wait: ${chalk10.cyan(String(config.syncPolicy.defaultMaxWaitMs))} ms`
|
|
3115
|
+
];
|
|
3116
|
+
p10.note(lines.join("\n"), `${config.projectName} \u2014 project config`);
|
|
3117
|
+
return 0;
|
|
3118
|
+
} catch (error) {
|
|
3119
|
+
p10.log.error(
|
|
3120
|
+
error instanceof Error ? error.message : "Failed to fetch project config."
|
|
3121
|
+
);
|
|
3122
|
+
return 1;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
// src/commands/translations.ts
|
|
3127
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
3128
|
+
import { join as join4 } from "path";
|
|
3129
|
+
import * as p11 from "@clack/prompts";
|
|
3130
|
+
import chalk11 from "chalk";
|
|
3131
|
+
import { config as loadEnv5 } from "dotenv";
|
|
3132
|
+
loadEnv5();
|
|
3133
|
+
async function getTranslations(options = {}) {
|
|
3134
|
+
const apiKey = process.env.VOCODER_API_KEY;
|
|
3135
|
+
if (!apiKey) {
|
|
3136
|
+
p11.log.error(
|
|
3137
|
+
"VOCODER_API_KEY is not set. Run `npx @vocoder/cli init` to set up your project."
|
|
3138
|
+
);
|
|
3139
|
+
return 1;
|
|
3140
|
+
}
|
|
3141
|
+
const apiUrl = options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app";
|
|
3142
|
+
const api = new VocoderAPI({ apiKey, apiUrl });
|
|
3143
|
+
let branch;
|
|
3144
|
+
try {
|
|
3145
|
+
branch = detectBranch(options.branch);
|
|
3146
|
+
} catch (error) {
|
|
3147
|
+
p11.log.error(
|
|
3148
|
+
error instanceof Error ? error.message : "Failed to detect branch."
|
|
3149
|
+
);
|
|
3150
|
+
return 1;
|
|
3151
|
+
}
|
|
3152
|
+
const spinner7 = p11.spinner();
|
|
3153
|
+
spinner7.start(`Fetching translations for ${chalk11.cyan(branch)}\u2026`);
|
|
3154
|
+
try {
|
|
3155
|
+
const projectConfig2 = await api.getProjectConfig();
|
|
3156
|
+
const targetLocales = options.locale ? [options.locale] : projectConfig2.targetLocales;
|
|
3157
|
+
if (targetLocales.length === 0) {
|
|
3158
|
+
spinner7.stop("No target locales configured.");
|
|
3159
|
+
p11.log.info("Add target locales with `vocoder locales add <code>`.");
|
|
3160
|
+
return 1;
|
|
3161
|
+
}
|
|
3162
|
+
const snapshot = await api.getTranslationSnapshot({ branch, targetLocales });
|
|
3163
|
+
spinner7.stop(`Fetched translations for ${chalk11.cyan(branch)}`);
|
|
3164
|
+
if (snapshot.status === "NOT_FOUND") {
|
|
3165
|
+
p11.log.warn(
|
|
3166
|
+
`No translation snapshot found for branch "${branch}". Run \`vocoder sync\` to generate one.`
|
|
3167
|
+
);
|
|
3168
|
+
return 1;
|
|
3169
|
+
}
|
|
3170
|
+
const translations = snapshot.translations ?? {};
|
|
3171
|
+
if (options.output) {
|
|
3172
|
+
writeLocaleFiles(translations, options.output);
|
|
3173
|
+
} else {
|
|
3174
|
+
process.stdout.write(JSON.stringify(translations, null, 2));
|
|
3175
|
+
process.stdout.write("\n");
|
|
3176
|
+
}
|
|
3177
|
+
return 0;
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
spinner7.stop("Failed to fetch translations.");
|
|
3180
|
+
p11.log.error(
|
|
3181
|
+
error instanceof Error ? error.message : "Unknown error."
|
|
3182
|
+
);
|
|
3183
|
+
return 1;
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
function writeLocaleFiles(translations, outputDir) {
|
|
3187
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
3188
|
+
for (const [locale, strings] of Object.entries(translations)) {
|
|
3189
|
+
const filePath = join4(outputDir, `${locale}.json`);
|
|
3190
|
+
writeFileSync4(filePath, JSON.stringify(strings, null, 2) + "\n", "utf-8");
|
|
3191
|
+
p11.log.success(`Wrote ${chalk11.cyan(filePath)}`);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// src/commands/create-project.ts
|
|
3196
|
+
import * as p12 from "@clack/prompts";
|
|
3197
|
+
import chalk12 from "chalk";
|
|
3198
|
+
import { config as loadEnv6 } from "dotenv";
|
|
3199
|
+
loadEnv6();
|
|
3200
|
+
async function createProject(options) {
|
|
3201
|
+
const authData = readAuthData();
|
|
3202
|
+
if (!authData) {
|
|
3203
|
+
p12.log.error(
|
|
3204
|
+
"Not logged in. Run `npx @vocoder/cli init` to authenticate first."
|
|
3205
|
+
);
|
|
3206
|
+
return 1;
|
|
3207
|
+
}
|
|
3208
|
+
const apiUrl = options.apiUrl ?? process.env.VOCODER_API_URL ?? "https://vocoder.app";
|
|
3209
|
+
const api = new VocoderAPI({ apiKey: "", apiUrl });
|
|
3210
|
+
let repoCanonical;
|
|
3211
|
+
let appDir = options.appDir ?? ".";
|
|
3212
|
+
if (options.repo) {
|
|
3213
|
+
repoCanonical = options.repo;
|
|
3214
|
+
} else {
|
|
3215
|
+
const identity = resolveGitRepositoryIdentity();
|
|
3216
|
+
if (identity) {
|
|
3217
|
+
repoCanonical = identity.repoCanonical;
|
|
3218
|
+
if (!options.appDir && identity.repoAppDir) {
|
|
3219
|
+
appDir = identity.repoAppDir;
|
|
3220
|
+
}
|
|
3221
|
+
} else {
|
|
3222
|
+
p12.log.warn(
|
|
3223
|
+
"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."
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
const targetLocales = options.targetLocales ? options.targetLocales.split(",").map((l) => l.trim()).filter(Boolean) : [];
|
|
3228
|
+
const targetBranches = options.targetBranches ? options.targetBranches.split(",").map((b) => b.trim()).filter(Boolean) : ["main"];
|
|
3229
|
+
const spinner7 = p12.spinner();
|
|
3230
|
+
spinner7.start(`Creating project "${options.name}"\u2026`);
|
|
3231
|
+
try {
|
|
3232
|
+
const result = await api.createProject(authData.token, {
|
|
3233
|
+
organizationId: options.workspace,
|
|
3234
|
+
name: options.name,
|
|
3235
|
+
sourceLocale: options.sourceLocale,
|
|
3236
|
+
targetLocales,
|
|
3237
|
+
targetBranches,
|
|
3238
|
+
appDirs: [appDir],
|
|
3239
|
+
...repoCanonical ? { repoCanonical } : {}
|
|
3240
|
+
});
|
|
3241
|
+
spinner7.stop(`Created project ${chalk12.bold(result.projectName)}`);
|
|
3242
|
+
const lines = [
|
|
3243
|
+
`Project ID: ${result.projectId}`,
|
|
3244
|
+
`Source locale: ${chalk12.cyan(result.sourceLocale)}`,
|
|
3245
|
+
`Target locales: ${result.targetLocales.length > 0 ? result.targetLocales.map((l) => chalk12.cyan(l)).join(", ") : chalk12.dim("(none)")}`,
|
|
3246
|
+
`Branches: ${result.targetBranches.map((b) => chalk12.cyan(b)).join(", ")}`,
|
|
3247
|
+
...repoCanonical ? [`Repository: ${chalk12.cyan(repoCanonical)}${appDir !== "." ? ` (${appDir})` : ""}`] : [],
|
|
3248
|
+
"",
|
|
3249
|
+
`Add this to your .env file:`,
|
|
3250
|
+
` ${chalk12.bold("VOCODER_API_KEY")}=${chalk12.cyan(result.apiKey)}`
|
|
3251
|
+
];
|
|
3252
|
+
p12.note(lines.join("\n"), "Project created");
|
|
3253
|
+
if (!result.repositoryBound && repoCanonical) {
|
|
3254
|
+
p12.log.warn(
|
|
3255
|
+
`Repository "${repoCanonical}" was not automatically connected. Ensure your GitHub App installation covers this repository.`
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
return 0;
|
|
3259
|
+
} catch (error) {
|
|
3260
|
+
spinner7.stop("Failed to create project.");
|
|
3261
|
+
if (error instanceof VocoderAPIError && error.limitError) {
|
|
3262
|
+
const { limitError } = error;
|
|
3263
|
+
p12.log.error(limitError.message);
|
|
3264
|
+
for (const line of getLimitErrorGuidance(limitError)) {
|
|
3265
|
+
p12.log.info(line);
|
|
3266
|
+
}
|
|
3267
|
+
return 1;
|
|
3268
|
+
}
|
|
3269
|
+
p12.log.error(
|
|
3270
|
+
error instanceof Error ? error.message : "Unknown error."
|
|
3271
|
+
);
|
|
3272
|
+
return 1;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
// src/commands/whoami.ts
|
|
3277
|
+
import * as p13 from "@clack/prompts";
|
|
3278
|
+
import chalk13 from "chalk";
|
|
2971
3279
|
async function whoami(options = {}) {
|
|
2972
3280
|
const stored = readAuthData();
|
|
2973
3281
|
if (!stored) {
|
|
2974
|
-
|
|
3282
|
+
p13.log.info("Not logged in. Run `vocoder init` to authenticate.");
|
|
2975
3283
|
return 1;
|
|
2976
3284
|
}
|
|
2977
3285
|
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
2978
3286
|
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
2979
3287
|
try {
|
|
2980
3288
|
const info = await api.getCliUserInfo(stored.token);
|
|
2981
|
-
|
|
3289
|
+
p13.log.info(`Logged in as ${chalk13.bold(info.email)}`);
|
|
2982
3290
|
if (info.name) {
|
|
2983
|
-
|
|
3291
|
+
p13.log.info(`Name: ${info.name}`);
|
|
2984
3292
|
}
|
|
2985
|
-
|
|
3293
|
+
p13.log.info(`API: ${apiUrl}`);
|
|
2986
3294
|
return 0;
|
|
2987
3295
|
} catch {
|
|
2988
|
-
|
|
3296
|
+
p13.log.error(
|
|
2989
3297
|
"Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
|
|
2990
3298
|
);
|
|
2991
3299
|
return 1;
|
|
@@ -3017,5 +3325,38 @@ program.command("sync").description("Extract strings and sync translations").opt
|
|
|
3017
3325
|
});
|
|
3018
3326
|
program.command("logout").description("Log out and remove stored credentials").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(logout, options));
|
|
3019
3327
|
program.command("whoami").description("Show the currently authenticated user").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(whoami, options));
|
|
3328
|
+
var localesCmd = program.command("locales").description("Manage project target locales").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(listProjectLocales, options));
|
|
3329
|
+
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(
|
|
3330
|
+
(codes, options) => runCommand((opts) => addLocales(codes, opts), options)
|
|
3331
|
+
);
|
|
3332
|
+
localesCmd.command("remove <codes...>").description("Remove one or more target locales by BCP 47 code").option("--api-url <url>", "Override Vocoder API URL").action(
|
|
3333
|
+
(codes, options) => runCommand((opts) => removeLocales(codes, opts), options)
|
|
3334
|
+
);
|
|
3335
|
+
localesCmd.command("supported").description("List all locales supported by Vocoder").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(listSupportedLocales, options));
|
|
3336
|
+
program.command("project").description("Show current project configuration").option("--api-url <url>", "Override Vocoder API URL").action((options) => runCommand(projectConfig, options));
|
|
3337
|
+
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));
|
|
3338
|
+
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(
|
|
3339
|
+
"--target-locales <codes>",
|
|
3340
|
+
"Comma-separated target locale codes (e.g. fr,de,pt-BR)"
|
|
3341
|
+
).option(
|
|
3342
|
+
"--target-branches <branches>",
|
|
3343
|
+
"Comma-separated branch names to sync (default: main)"
|
|
3344
|
+
).option(
|
|
3345
|
+
"--repo <canonical>",
|
|
3346
|
+
"Git repo canonical (e.g. github:owner/repo). Auto-detected from git remote if omitted."
|
|
3347
|
+
).option(
|
|
3348
|
+
"--app-dir <path>",
|
|
3349
|
+
"App directory within the repo for monorepos (default: .)"
|
|
3350
|
+
).option("--api-url <url>", "Override Vocoder API URL").action((options) => {
|
|
3351
|
+
const translated = {
|
|
3352
|
+
...options,
|
|
3353
|
+
// Commander camelCases dashed options
|
|
3354
|
+
sourceLocale: options.sourceLocale,
|
|
3355
|
+
targetLocales: options.targetLocales,
|
|
3356
|
+
targetBranches: options.targetBranches,
|
|
3357
|
+
workspace: options.workspace
|
|
3358
|
+
};
|
|
3359
|
+
return runCommand(createProject, translated);
|
|
3360
|
+
});
|
|
3020
3361
|
program.parse(process.argv);
|
|
3021
3362
|
//# sourceMappingURL=bin.mjs.map
|