@vocoder/cli 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/dist/bin.mjs +153 -131
- package/dist/bin.mjs.map +1 -1
- package/dist/lib.d.mts +8 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install -g @vocoder/cli
|
|
|
11
11
|
Or use without installing:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx vocoder <command>
|
|
14
|
+
npx @vocoder/cli <command>
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Commands
|
|
@@ -56,14 +56,19 @@ The CLI opens the Vocoder GitHub App installation page. Authorizing the App crea
|
|
|
56
56
|
|
|
57
57
|
◒ Creating project...
|
|
58
58
|
|
|
59
|
-
◆
|
|
60
|
-
◆ Step 2: Wrap your app with VocoderProvider
|
|
61
|
-
◆ Step 3: Mark strings for translation with <T>
|
|
59
|
+
◆ Finish setup in your code
|
|
62
60
|
|
|
63
|
-
◆
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
◆ vite.config.ts — register the build plugin so Vocoder can extract your strings
|
|
62
|
+
◆ your root layout or App component — wrap your app so translations load at runtime
|
|
63
|
+
◆ wrap translatable text — mark strings for extraction — Vocoder picks these up on push
|
|
64
|
+
|
|
65
|
+
✓ Push to main to trigger your first translation run.
|
|
66
|
+
|
|
67
|
+
◆ Your API Key
|
|
68
|
+
│ ┌─────────────────────────────────────────┐
|
|
69
|
+
│ │ VOCODER_API_KEY=vcp_xxxx │
|
|
70
|
+
│ └─────────────────────────────────────────┘
|
|
71
|
+
✓ Saved to .env
|
|
67
72
|
|
|
68
73
|
◇ You're all set.
|
|
69
74
|
```
|
|
@@ -121,10 +126,14 @@ Reads `VOCODER_API_KEY` from environment or `.env`. Detects `<T>` and `t()` usag
|
|
|
121
126
|
|
|
122
127
|
| Flag | Description |
|
|
123
128
|
|---|---|
|
|
129
|
+
| `--include <glob>` | Glob pattern for files to scan (repeatable). Default: `**/*.{tsx,jsx,ts,js}` |
|
|
130
|
+
| `--exclude <glob>` | Glob pattern to skip (repeatable). Merged with built-in excludes |
|
|
124
131
|
| `--locale <code>` | Sync only this target locale |
|
|
125
132
|
| `--dry-run` | Show what would be synced without submitting |
|
|
126
133
|
| `--verbose` | Show extraction and sync details |
|
|
127
134
|
|
|
135
|
+
Patterns can also be set via env vars: `VOCODER_INCLUDE_PATTERN` and `VOCODER_EXCLUDE_PATTERN` (comma-separated).
|
|
136
|
+
|
|
128
137
|
---
|
|
129
138
|
|
|
130
139
|
### `vocoder logout`
|
package/dist/bin.mjs
CHANGED
|
@@ -12,6 +12,8 @@ import { Command } from "commander";
|
|
|
12
12
|
|
|
13
13
|
// src/commands/init.ts
|
|
14
14
|
import { execSync as execSync3, spawn as spawn2 } from "child_process";
|
|
15
|
+
import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
16
|
+
import { join as join2 } from "path";
|
|
15
17
|
import * as p5 from "@clack/prompts";
|
|
16
18
|
import chalk6 from "chalk";
|
|
17
19
|
import { config as loadEnv } from "dotenv";
|
|
@@ -1955,62 +1957,74 @@ function runScaffold(params) {
|
|
|
1955
1957
|
sourceLocale,
|
|
1956
1958
|
targetBranches
|
|
1957
1959
|
});
|
|
1958
|
-
|
|
1960
|
+
const steps = [];
|
|
1959
1961
|
if (snippets.pluginStep) {
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
stepNum++;
|
|
1962
|
+
steps.push({
|
|
1963
|
+
label: snippets.pluginStep.file,
|
|
1964
|
+
hint: "register the build plugin so Vocoder can extract your strings",
|
|
1965
|
+
code: snippets.pluginStep.code
|
|
1966
|
+
});
|
|
1966
1967
|
}
|
|
1967
1968
|
if (snippets.providerStep) {
|
|
1969
|
+
steps.push({
|
|
1970
|
+
label: snippets.providerStep.file,
|
|
1971
|
+
hint: "wrap your app so translations load at runtime",
|
|
1972
|
+
code: snippets.providerStep.code
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
steps.push({
|
|
1976
|
+
label: "wrap translatable text",
|
|
1977
|
+
hint: "mark strings for extraction \u2014 Vocoder picks these up on push",
|
|
1978
|
+
code: snippets.wrapStep.code
|
|
1979
|
+
});
|
|
1980
|
+
p5.log.message("");
|
|
1981
|
+
p5.log.message(chalk6.bold("Finish setup in your code"));
|
|
1982
|
+
p5.log.message("");
|
|
1983
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1984
|
+
const step = steps[i];
|
|
1968
1985
|
p5.log.step(
|
|
1969
|
-
`${chalk6.bold(
|
|
1986
|
+
`${chalk6.bold(step.label)} ${chalk6.dim(`\u2014 ${step.hint}`)}`
|
|
1970
1987
|
);
|
|
1971
|
-
printCodeBlock(
|
|
1972
|
-
|
|
1988
|
+
printCodeBlock(step.code);
|
|
1989
|
+
if (i < steps.length - 1) p5.log.message("");
|
|
1973
1990
|
}
|
|
1974
|
-
p5.log.step(`${chalk6.bold(`Step ${stepNum}:`)} Wrap translatable strings`);
|
|
1975
|
-
printCodeBlock(snippets.wrapStep.code);
|
|
1976
1991
|
p5.log.message("");
|
|
1977
|
-
|
|
1978
|
-
|
|
1992
|
+
const branchList = targetBranches.length > 0 ? targetBranches.map((b) => chalk6.cyan(b)).join(" or ") : chalk6.cyan("your target branch");
|
|
1993
|
+
p5.log.success(
|
|
1994
|
+
`Push to ${branchList} to trigger your first translation run.`
|
|
1995
|
+
);
|
|
1996
|
+
p5.log.message(chalk6.gray(" Docs: https://vocoder.app/docs/getting-started"));
|
|
1997
|
+
}
|
|
1998
|
+
function writeApiKeyToEnv(apiKey) {
|
|
1999
|
+
const envPath = join2(process.cwd(), ".env");
|
|
2000
|
+
if (!existsSync(envPath)) return false;
|
|
2001
|
+
try {
|
|
2002
|
+
const content = readFileSync2(envPath, "utf-8");
|
|
2003
|
+
const keyLine = `VOCODER_API_KEY=${apiKey}`;
|
|
2004
|
+
let updated;
|
|
2005
|
+
if (/^VOCODER_API_KEY=/m.test(content)) {
|
|
2006
|
+
updated = content.replace(/^VOCODER_API_KEY=.*/m, keyLine);
|
|
2007
|
+
} else {
|
|
2008
|
+
const sep = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
2009
|
+
updated = `${content}${sep}${keyLine}
|
|
2010
|
+
`;
|
|
2011
|
+
}
|
|
2012
|
+
writeFileSync2(envPath, updated);
|
|
2013
|
+
return true;
|
|
2014
|
+
} catch {
|
|
2015
|
+
return false;
|
|
1979
2016
|
}
|
|
1980
2017
|
}
|
|
1981
|
-
function
|
|
1982
|
-
const
|
|
1983
|
-
--env VOCODER_API_KEY=${apiKey} \\
|
|
1984
|
-
vocoder -- npx -y @vocoder/mcp`;
|
|
1985
|
-
const teamConfig = JSON.stringify(
|
|
1986
|
-
{
|
|
1987
|
-
mcpServers: {
|
|
1988
|
-
vocoder: {
|
|
1989
|
-
type: "stdio",
|
|
1990
|
-
command: "npx",
|
|
1991
|
-
args: ["-y", "@vocoder/mcp"],
|
|
1992
|
-
// biome-ignore lint/suspicious/noTemplateCurlyInString: MCP config template, not a JS template literal
|
|
1993
|
-
env: { VOCODER_API_KEY: "${env:VOCODER_API_KEY}" }
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
},
|
|
1997
|
-
null,
|
|
1998
|
-
2
|
|
1999
|
-
);
|
|
2000
|
-
p5.log.message("");
|
|
2001
|
-
p5.log.message(chalk6.bold("Use Vocoder with Claude Code"));
|
|
2002
|
-
p5.log.message("Run this to add the MCP server to your project:");
|
|
2003
|
-
p5.log.message("");
|
|
2004
|
-
printCodeBlock(addCommand);
|
|
2018
|
+
function printApiKey(apiKey) {
|
|
2019
|
+
const saved = writeApiKeyToEnv(apiKey);
|
|
2005
2020
|
p5.log.message("");
|
|
2006
|
-
p5.log.message(
|
|
2007
|
-
|
|
2008
|
-
)
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
p5.log.message(chalk6.gray("Setup instructions: https://vocoder.app/docs/mcp"));
|
|
2021
|
+
p5.log.message(chalk6.bold("Your API Key"));
|
|
2022
|
+
printCodeBlock(`VOCODER_API_KEY=${apiKey}`);
|
|
2023
|
+
if (saved) {
|
|
2024
|
+
p5.log.success(chalk6.dim("Saved to .env"));
|
|
2025
|
+
} else {
|
|
2026
|
+
p5.log.message(chalk6.dim(" Add the above to your .env file"));
|
|
2027
|
+
}
|
|
2014
2028
|
}
|
|
2015
2029
|
function printCodeBlock(code) {
|
|
2016
2030
|
const lines = code.split("\n");
|
|
@@ -2074,9 +2088,8 @@ async function runAuthFlow(api, options, reauth = false, repoCanonical) {
|
|
|
2074
2088
|
return null;
|
|
2075
2089
|
}
|
|
2076
2090
|
if (!shouldOpen) {
|
|
2077
|
-
p5.
|
|
2078
|
-
|
|
2079
|
-
);
|
|
2091
|
+
p5.note(browserUrl, "Sign In");
|
|
2092
|
+
p5.log.info("Open the URL above manually in your browser to continue.");
|
|
2080
2093
|
} else {
|
|
2081
2094
|
const opened = await tryOpenBrowser2(browserUrl);
|
|
2082
2095
|
if (!opened) {
|
|
@@ -2235,7 +2248,7 @@ async function init(options = {}) {
|
|
|
2235
2248
|
exactMatch.projectId
|
|
2236
2249
|
);
|
|
2237
2250
|
spinner4.stop("New API key generated");
|
|
2238
|
-
|
|
2251
|
+
printApiKey(apiKey);
|
|
2239
2252
|
} catch (err) {
|
|
2240
2253
|
spinner4.stop("Failed to generate key");
|
|
2241
2254
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2710,7 +2723,7 @@ Translations won't run automatically until you grant access.
|
|
|
2710
2723
|
sourceLocale: projectResult.sourceLocale,
|
|
2711
2724
|
targetBranches: projectResult.targetBranches
|
|
2712
2725
|
});
|
|
2713
|
-
|
|
2726
|
+
printApiKey(projectResult.apiKey);
|
|
2714
2727
|
p5.outro("You're all set.");
|
|
2715
2728
|
return 0;
|
|
2716
2729
|
} catch (error) {
|
|
@@ -2748,9 +2761,9 @@ async function logout(options = {}) {
|
|
|
2748
2761
|
|
|
2749
2762
|
// src/commands/sync.ts
|
|
2750
2763
|
import { createHash, randomUUID } from "crypto";
|
|
2751
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as
|
|
2752
|
-
import { join as
|
|
2753
|
-
import * as
|
|
2764
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
2765
|
+
import { join as join3 } from "path";
|
|
2766
|
+
import * as p8 from "@clack/prompts";
|
|
2754
2767
|
import chalk8 from "chalk";
|
|
2755
2768
|
|
|
2756
2769
|
// src/utils/branch.ts
|
|
@@ -2820,6 +2833,7 @@ function matchBranchPattern(branch, pattern) {
|
|
|
2820
2833
|
}
|
|
2821
2834
|
|
|
2822
2835
|
// src/utils/config.ts
|
|
2836
|
+
import * as p7 from "@clack/prompts";
|
|
2823
2837
|
import chalk7 from "chalk";
|
|
2824
2838
|
import { config as loadEnv2 } from "dotenv";
|
|
2825
2839
|
loadEnv2();
|
|
@@ -2897,7 +2911,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2897
2911
|
excludePattern = cliOptions.exclude;
|
|
2898
2912
|
configSources.excludePattern = "CLI flag";
|
|
2899
2913
|
} else if (envExcludePattern) {
|
|
2900
|
-
excludePattern = envExcludePattern.split(",").map((
|
|
2914
|
+
excludePattern = envExcludePattern.split(",").map((p10) => p10.trim()).filter(Boolean);
|
|
2901
2915
|
configSources.excludePattern = "environment";
|
|
2902
2916
|
} else {
|
|
2903
2917
|
excludePattern = defaults.excludePattern;
|
|
@@ -2945,24 +2959,16 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2945
2959
|
configSources.noFallback = "environment";
|
|
2946
2960
|
}
|
|
2947
2961
|
if (verbose) {
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
)
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
console.log(chalk7.dim(` API URL: ${configSources.apiUrl}
|
|
2959
|
-
`));
|
|
2960
|
-
console.log(chalk7.dim(` Sync mode: ${configSources.mode}`));
|
|
2961
|
-
if (maxWaitMs) {
|
|
2962
|
-
console.log(chalk7.dim(` Max wait: ${configSources.maxWaitMs}`));
|
|
2963
|
-
}
|
|
2964
|
-
console.log(chalk7.dim(` No fallback: ${configSources.noFallback}
|
|
2965
|
-
`));
|
|
2962
|
+
const lines = [
|
|
2963
|
+
`Include patterns: ${chalk7.cyan(configSources.includePattern)}`,
|
|
2964
|
+
...excludePattern.length > 0 ? [`Exclude patterns: ${chalk7.cyan(configSources.excludePattern)}`] : [],
|
|
2965
|
+
`API key: ${chalk7.cyan(configSources.apiKey)}`,
|
|
2966
|
+
`API URL: ${chalk7.cyan(configSources.apiUrl)}`,
|
|
2967
|
+
`Sync mode: ${chalk7.cyan(configSources.mode)}`,
|
|
2968
|
+
...maxWaitMs ? [`Max wait: ${chalk7.cyan(String(configSources.maxWaitMs))}`] : [],
|
|
2969
|
+
`No fallback: ${chalk7.cyan(String(configSources.noFallback))}`
|
|
2970
|
+
];
|
|
2971
|
+
p7.note(lines.join("\n"), "Configuration sources");
|
|
2966
2972
|
}
|
|
2967
2973
|
return {
|
|
2968
2974
|
includePattern,
|
|
@@ -2983,9 +2989,9 @@ function computeStringsHash(texts) {
|
|
|
2983
2989
|
}
|
|
2984
2990
|
function readCachedStringsHash(projectRoot, branch) {
|
|
2985
2991
|
const filePath = getCacheFilePath(projectRoot, branch);
|
|
2986
|
-
if (!
|
|
2992
|
+
if (!existsSync2(filePath)) return null;
|
|
2987
2993
|
try {
|
|
2988
|
-
const raw = JSON.parse(
|
|
2994
|
+
const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
2989
2995
|
if (isRecord(raw) && typeof raw.stringsHash === "string")
|
|
2990
2996
|
return raw.stringsHash;
|
|
2991
2997
|
} catch {
|
|
@@ -3037,7 +3043,7 @@ function parseTranslations(value) {
|
|
|
3037
3043
|
}
|
|
3038
3044
|
function getCacheFilePath(projectRoot, branch) {
|
|
3039
3045
|
const branchHash = createHash("sha1").update(branch).digest("hex").slice(0, 12);
|
|
3040
|
-
return
|
|
3046
|
+
return join3(
|
|
3041
3047
|
projectRoot,
|
|
3042
3048
|
"node_modules",
|
|
3043
3049
|
".vocoder",
|
|
@@ -3050,11 +3056,11 @@ function readLocalSnapshotCache(params) {
|
|
|
3050
3056
|
const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
|
|
3051
3057
|
for (const candidateBranch of candidateBranches) {
|
|
3052
3058
|
const cacheFilePath = getCacheFilePath(params.projectRoot, candidateBranch);
|
|
3053
|
-
if (!
|
|
3059
|
+
if (!existsSync2(cacheFilePath)) {
|
|
3054
3060
|
continue;
|
|
3055
3061
|
}
|
|
3056
3062
|
try {
|
|
3057
|
-
const raw =
|
|
3063
|
+
const raw = readFileSync3(cacheFilePath, "utf-8");
|
|
3058
3064
|
const parsed = JSON.parse(raw);
|
|
3059
3065
|
if (!isRecord(parsed)) {
|
|
3060
3066
|
continue;
|
|
@@ -3080,7 +3086,7 @@ function readLocalSnapshotCache(params) {
|
|
|
3080
3086
|
function writeLocalSnapshotCache(params) {
|
|
3081
3087
|
const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
|
|
3082
3088
|
mkdirSync2(
|
|
3083
|
-
|
|
3089
|
+
join3(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"),
|
|
3084
3090
|
{
|
|
3085
3091
|
recursive: true
|
|
3086
3092
|
}
|
|
@@ -3097,7 +3103,7 @@ function writeLocalSnapshotCache(params) {
|
|
|
3097
3103
|
...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
|
|
3098
3104
|
translations: params.translations
|
|
3099
3105
|
};
|
|
3100
|
-
|
|
3106
|
+
writeFileSync3(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
3101
3107
|
return cacheFilePath;
|
|
3102
3108
|
}
|
|
3103
3109
|
function resolveEffectiveModeFromPolicy(params) {
|
|
@@ -3251,8 +3257,8 @@ async function fetchApiSnapshot(api, params) {
|
|
|
3251
3257
|
async function sync(options = {}) {
|
|
3252
3258
|
const startTime = Date.now();
|
|
3253
3259
|
const projectRoot = process.cwd();
|
|
3254
|
-
|
|
3255
|
-
const spinner4 =
|
|
3260
|
+
p8.intro("Vocoder Sync");
|
|
3261
|
+
const spinner4 = p8.spinner();
|
|
3256
3262
|
try {
|
|
3257
3263
|
spinner4.start("Detecting branch");
|
|
3258
3264
|
const branch = detectBranch(options.branch);
|
|
@@ -3281,12 +3287,12 @@ async function sync(options = {}) {
|
|
|
3281
3287
|
};
|
|
3282
3288
|
spinner4.stop("Project configuration loaded");
|
|
3283
3289
|
if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
|
|
3284
|
-
|
|
3290
|
+
p8.log.warn(
|
|
3285
3291
|
`Skipping translations (${chalk8.cyan(branch)} is not a target branch)`
|
|
3286
3292
|
);
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3293
|
+
p8.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
|
|
3294
|
+
p8.log.info("Use --force to translate anyway");
|
|
3295
|
+
p8.outro("");
|
|
3290
3296
|
return 0;
|
|
3291
3297
|
}
|
|
3292
3298
|
const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
|
|
@@ -3299,10 +3305,10 @@ async function sync(options = {}) {
|
|
|
3299
3305
|
);
|
|
3300
3306
|
if (extractedStrings.length === 0) {
|
|
3301
3307
|
spinner4.stop("No translatable strings found");
|
|
3302
|
-
|
|
3308
|
+
p8.log.warn(
|
|
3303
3309
|
"Make sure you are wrapping translatable strings with Vocoder"
|
|
3304
3310
|
);
|
|
3305
|
-
|
|
3311
|
+
p8.outro("");
|
|
3306
3312
|
return 0;
|
|
3307
3313
|
}
|
|
3308
3314
|
spinner4.stop(
|
|
@@ -3313,10 +3319,10 @@ async function sync(options = {}) {
|
|
|
3313
3319
|
if (extractedStrings.length > 5) {
|
|
3314
3320
|
sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
|
|
3315
3321
|
}
|
|
3316
|
-
|
|
3322
|
+
p8.note(sampleLines.join("\n"), "Sample strings");
|
|
3317
3323
|
}
|
|
3318
3324
|
if (options.dryRun) {
|
|
3319
|
-
|
|
3325
|
+
p8.note(
|
|
3320
3326
|
[
|
|
3321
3327
|
`Strings: ${extractedStrings.length}`,
|
|
3322
3328
|
`Branch: ${branch}`,
|
|
@@ -3327,12 +3333,12 @@ async function sync(options = {}) {
|
|
|
3327
3333
|
].join("\n"),
|
|
3328
3334
|
"Dry run - would translate"
|
|
3329
3335
|
);
|
|
3330
|
-
|
|
3336
|
+
p8.outro("No API calls made.");
|
|
3331
3337
|
return 0;
|
|
3332
3338
|
}
|
|
3333
3339
|
const repoIdentity = resolveGitRepositoryIdentity();
|
|
3334
3340
|
if (!repoIdentity && options.verbose) {
|
|
3335
|
-
|
|
3341
|
+
p8.log.warn(
|
|
3336
3342
|
"Could not detect git remote origin. Sync will continue without repo metadata."
|
|
3337
3343
|
);
|
|
3338
3344
|
}
|
|
@@ -3340,16 +3346,32 @@ async function sync(options = {}) {
|
|
|
3340
3346
|
const stringEntries = buildStringEntries(extractedStrings);
|
|
3341
3347
|
const sourceStrings = stringEntries.map((entry) => entry.text);
|
|
3342
3348
|
if (options.verbose && stringEntries.length !== extractedStrings.length) {
|
|
3343
|
-
|
|
3349
|
+
p8.log.info(
|
|
3344
3350
|
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
|
|
3345
3351
|
);
|
|
3346
3352
|
}
|
|
3347
3353
|
const currentHash = computeStringsHash(sourceStrings);
|
|
3348
3354
|
if (!options.force) {
|
|
3349
3355
|
const cachedHash = readCachedStringsHash(projectRoot, branch);
|
|
3356
|
+
if (options.verbose) {
|
|
3357
|
+
const cacheFile = getCacheFilePath(projectRoot, branch);
|
|
3358
|
+
if (cachedHash) {
|
|
3359
|
+
p8.log.info(
|
|
3360
|
+
`Local cache: ${chalk8.dim(cacheFile)}
|
|
3361
|
+
cached hash ${chalk8.cyan(cachedHash.slice(0, 8))}\u2026 vs current ${chalk8.cyan(currentHash.slice(0, 8))}\u2026 \u2014 ${cachedHash === currentHash ? chalk8.green("match") : chalk8.yellow("changed")}`
|
|
3362
|
+
);
|
|
3363
|
+
} else {
|
|
3364
|
+
p8.log.info(`No local cache found at ${chalk8.dim(cacheFile)} \u2014 will submit to API`);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3350
3367
|
if (cachedHash && cachedHash === currentHash) {
|
|
3368
|
+
if (options.verbose) {
|
|
3369
|
+
p8.log.info(
|
|
3370
|
+
"Skipping API submission \u2014 delete node_modules/.vocoder to force a fresh sync"
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3351
3373
|
const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
3352
|
-
|
|
3374
|
+
p8.outro(`Up to date (${duration2}s)`);
|
|
3353
3375
|
return 0;
|
|
3354
3376
|
}
|
|
3355
3377
|
}
|
|
@@ -3374,31 +3396,31 @@ async function sync(options = {}) {
|
|
|
3374
3396
|
policy: config.syncPolicy
|
|
3375
3397
|
});
|
|
3376
3398
|
if (options.verbose) {
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3399
|
+
p8.log.info(`Requested mode: ${requestedMode}`);
|
|
3400
|
+
p8.log.info(`Effective mode: ${effectiveMode}`);
|
|
3401
|
+
p8.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
|
|
3380
3402
|
if (batchResponse.queueStatus) {
|
|
3381
|
-
|
|
3403
|
+
p8.log.info(`Queue status: ${batchResponse.queueStatus}`);
|
|
3382
3404
|
}
|
|
3383
3405
|
}
|
|
3384
3406
|
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
3385
|
-
|
|
3407
|
+
p8.log.success("No changes detected - strings are up to date");
|
|
3386
3408
|
}
|
|
3387
|
-
|
|
3409
|
+
p8.log.info(`New strings: ${chalk8.cyan(batchResponse.newStrings)}`);
|
|
3388
3410
|
if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
|
|
3389
|
-
|
|
3411
|
+
p8.log.info(
|
|
3390
3412
|
`Deleted strings: ${chalk8.yellow(batchResponse.deletedStrings)} (archived)`
|
|
3391
3413
|
);
|
|
3392
3414
|
}
|
|
3393
|
-
|
|
3415
|
+
p8.log.info(`Total strings: ${chalk8.cyan(batchResponse.totalStrings)}`);
|
|
3394
3416
|
if (batchResponse.newStrings === 0) {
|
|
3395
|
-
|
|
3417
|
+
p8.log.success("No new strings - using existing translations");
|
|
3396
3418
|
} else {
|
|
3397
|
-
|
|
3419
|
+
p8.log.info(
|
|
3398
3420
|
`Syncing to ${config.targetLocales.length} locales (${config.targetLocales.join(", ")})`
|
|
3399
3421
|
);
|
|
3400
3422
|
if (batchResponse.estimatedTime) {
|
|
3401
|
-
|
|
3423
|
+
p8.log.info(`Estimated time: ~${batchResponse.estimatedTime}s`);
|
|
3402
3424
|
}
|
|
3403
3425
|
}
|
|
3404
3426
|
let artifacts = null;
|
|
@@ -3436,7 +3458,7 @@ async function sync(options = {}) {
|
|
|
3436
3458
|
if (effectiveMode === "required") {
|
|
3437
3459
|
throw waitError;
|
|
3438
3460
|
}
|
|
3439
|
-
|
|
3461
|
+
p8.log.warn(`Best-effort wait ended early: ${waitError.message}`);
|
|
3440
3462
|
}
|
|
3441
3463
|
}
|
|
3442
3464
|
if (!artifacts) {
|
|
@@ -3470,7 +3492,7 @@ async function sync(options = {}) {
|
|
|
3470
3492
|
spinner4.stop("Failed to fetch API snapshot");
|
|
3471
3493
|
if (options.verbose) {
|
|
3472
3494
|
const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
|
|
3473
|
-
|
|
3495
|
+
p8.log.warn(`Snapshot fetch error: ${message}`);
|
|
3474
3496
|
}
|
|
3475
3497
|
}
|
|
3476
3498
|
}
|
|
@@ -3504,61 +3526,61 @@ async function sync(options = {}) {
|
|
|
3504
3526
|
completedAt: artifacts.completedAt ?? (artifacts.source === "fresh" ? (/* @__PURE__ */ new Date()).toISOString() : null)
|
|
3505
3527
|
});
|
|
3506
3528
|
if (options.verbose) {
|
|
3507
|
-
|
|
3529
|
+
p8.log.info(`Cached snapshot: ${cachePath}`);
|
|
3508
3530
|
}
|
|
3509
3531
|
} catch (error) {
|
|
3510
3532
|
if (options.verbose) {
|
|
3511
3533
|
const message = error instanceof Error ? error.message : "Unknown cache write error";
|
|
3512
|
-
|
|
3534
|
+
p8.log.warn(`Failed to write local snapshot cache: ${message}`);
|
|
3513
3535
|
}
|
|
3514
3536
|
}
|
|
3515
3537
|
if (artifacts.source !== "fresh") {
|
|
3516
3538
|
const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
|
|
3517
|
-
|
|
3539
|
+
p8.log.warn(
|
|
3518
3540
|
`Using ${sourceLabel}. New strings may appear after the background sync completes.`
|
|
3519
3541
|
);
|
|
3520
3542
|
}
|
|
3521
3543
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
3522
|
-
|
|
3544
|
+
p8.outro(`Sync complete! (${duration}s)`);
|
|
3523
3545
|
return 0;
|
|
3524
3546
|
} catch (error) {
|
|
3525
3547
|
spinner4.stop();
|
|
3526
3548
|
if (error instanceof VocoderAPIError && error.syncPolicyError) {
|
|
3527
|
-
|
|
3549
|
+
p8.log.error(error.syncPolicyError.message);
|
|
3528
3550
|
const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
|
|
3529
3551
|
for (const line of guidance) {
|
|
3530
|
-
|
|
3552
|
+
p8.log.info(line);
|
|
3531
3553
|
}
|
|
3532
3554
|
return 1;
|
|
3533
3555
|
}
|
|
3534
3556
|
if (error instanceof VocoderAPIError && error.limitError) {
|
|
3535
3557
|
const { limitError } = error;
|
|
3536
|
-
|
|
3558
|
+
p8.log.error(limitError.message);
|
|
3537
3559
|
const guidance = getLimitErrorGuidance(limitError);
|
|
3538
3560
|
for (const line of guidance) {
|
|
3539
|
-
|
|
3561
|
+
p8.log.info(line);
|
|
3540
3562
|
}
|
|
3541
3563
|
return 1;
|
|
3542
3564
|
}
|
|
3543
3565
|
if (error instanceof Error) {
|
|
3544
|
-
|
|
3566
|
+
p8.log.error(error.message);
|
|
3545
3567
|
if (error.message.includes("VOCODER_API_KEY")) {
|
|
3546
|
-
|
|
3568
|
+
p8.log.warn(
|
|
3547
3569
|
"VOCODER_API_KEY is only needed for `vocoder sync` (CLI push)."
|
|
3548
3570
|
);
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3571
|
+
p8.log.info(" Create one at: https://vocoder.app/dashboard");
|
|
3572
|
+
p8.log.info(' Then: export VOCODER_API_KEY="vc_..." or add it to .env');
|
|
3573
|
+
p8.log.info("");
|
|
3574
|
+
p8.log.info(
|
|
3553
3575
|
" Note: If you use @vocoder/unplugin, `vocoder sync` is optional."
|
|
3554
3576
|
);
|
|
3555
|
-
|
|
3577
|
+
p8.log.info(" Translations are fetched automatically at build time.");
|
|
3556
3578
|
} else if (error.message.includes("git branch")) {
|
|
3557
|
-
|
|
3558
|
-
|
|
3579
|
+
p8.log.warn("Run from a git repository, or use:");
|
|
3580
|
+
p8.log.info(" vocoder sync --branch main");
|
|
3559
3581
|
}
|
|
3560
3582
|
if (options.verbose) {
|
|
3561
|
-
|
|
3583
|
+
p8.log.info(`Full error: ${error.stack ?? error}`);
|
|
3562
3584
|
}
|
|
3563
3585
|
}
|
|
3564
3586
|
return 1;
|
|
@@ -3566,26 +3588,26 @@ async function sync(options = {}) {
|
|
|
3566
3588
|
}
|
|
3567
3589
|
|
|
3568
3590
|
// src/commands/whoami.ts
|
|
3569
|
-
import * as
|
|
3591
|
+
import * as p9 from "@clack/prompts";
|
|
3570
3592
|
import chalk9 from "chalk";
|
|
3571
3593
|
async function whoami(options = {}) {
|
|
3572
3594
|
const stored = readAuthData();
|
|
3573
3595
|
if (!stored) {
|
|
3574
|
-
|
|
3596
|
+
p9.log.info("Not logged in. Run `vocoder init` to authenticate.");
|
|
3575
3597
|
return 1;
|
|
3576
3598
|
}
|
|
3577
3599
|
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
3578
3600
|
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
3579
3601
|
try {
|
|
3580
3602
|
const info = await api.getCliUserInfo(stored.token);
|
|
3581
|
-
|
|
3603
|
+
p9.log.info(`Logged in as ${chalk9.bold(info.email)}`);
|
|
3582
3604
|
if (info.name) {
|
|
3583
|
-
|
|
3605
|
+
p9.log.info(`Name: ${info.name}`);
|
|
3584
3606
|
}
|
|
3585
|
-
|
|
3607
|
+
p9.log.info(`API: ${apiUrl}`);
|
|
3586
3608
|
return 0;
|
|
3587
3609
|
} catch {
|
|
3588
|
-
|
|
3610
|
+
p9.log.error(
|
|
3589
3611
|
"Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
|
|
3590
3612
|
);
|
|
3591
3613
|
return 1;
|