@vocoder/cli 0.1.20 → 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 +151 -128
- 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:");
|
|
2018
|
+
function printApiKey(apiKey) {
|
|
2019
|
+
const saved = writeApiKeyToEnv(apiKey);
|
|
2003
2020
|
p5.log.message("");
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
printCodeBlock(teamConfig);
|
|
2012
|
-
p5.log.message("");
|
|
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");
|
|
@@ -2234,7 +2248,7 @@ async function init(options = {}) {
|
|
|
2234
2248
|
exactMatch.projectId
|
|
2235
2249
|
);
|
|
2236
2250
|
spinner4.stop("New API key generated");
|
|
2237
|
-
|
|
2251
|
+
printApiKey(apiKey);
|
|
2238
2252
|
} catch (err) {
|
|
2239
2253
|
spinner4.stop("Failed to generate key");
|
|
2240
2254
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2709,7 +2723,7 @@ Translations won't run automatically until you grant access.
|
|
|
2709
2723
|
sourceLocale: projectResult.sourceLocale,
|
|
2710
2724
|
targetBranches: projectResult.targetBranches
|
|
2711
2725
|
});
|
|
2712
|
-
|
|
2726
|
+
printApiKey(projectResult.apiKey);
|
|
2713
2727
|
p5.outro("You're all set.");
|
|
2714
2728
|
return 0;
|
|
2715
2729
|
} catch (error) {
|
|
@@ -2747,9 +2761,9 @@ async function logout(options = {}) {
|
|
|
2747
2761
|
|
|
2748
2762
|
// src/commands/sync.ts
|
|
2749
2763
|
import { createHash, randomUUID } from "crypto";
|
|
2750
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as
|
|
2751
|
-
import { join as
|
|
2752
|
-
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";
|
|
2753
2767
|
import chalk8 from "chalk";
|
|
2754
2768
|
|
|
2755
2769
|
// src/utils/branch.ts
|
|
@@ -2819,6 +2833,7 @@ function matchBranchPattern(branch, pattern) {
|
|
|
2819
2833
|
}
|
|
2820
2834
|
|
|
2821
2835
|
// src/utils/config.ts
|
|
2836
|
+
import * as p7 from "@clack/prompts";
|
|
2822
2837
|
import chalk7 from "chalk";
|
|
2823
2838
|
import { config as loadEnv2 } from "dotenv";
|
|
2824
2839
|
loadEnv2();
|
|
@@ -2896,7 +2911,7 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2896
2911
|
excludePattern = cliOptions.exclude;
|
|
2897
2912
|
configSources.excludePattern = "CLI flag";
|
|
2898
2913
|
} else if (envExcludePattern) {
|
|
2899
|
-
excludePattern = envExcludePattern.split(",").map((
|
|
2914
|
+
excludePattern = envExcludePattern.split(",").map((p10) => p10.trim()).filter(Boolean);
|
|
2900
2915
|
configSources.excludePattern = "environment";
|
|
2901
2916
|
} else {
|
|
2902
2917
|
excludePattern = defaults.excludePattern;
|
|
@@ -2944,24 +2959,16 @@ async function getMergedConfig(cliOptions, verbose = false, _startDir) {
|
|
|
2944
2959
|
configSources.noFallback = "environment";
|
|
2945
2960
|
}
|
|
2946
2961
|
if (verbose) {
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
)
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
console.log(chalk7.dim(` API URL: ${configSources.apiUrl}
|
|
2958
|
-
`));
|
|
2959
|
-
console.log(chalk7.dim(` Sync mode: ${configSources.mode}`));
|
|
2960
|
-
if (maxWaitMs) {
|
|
2961
|
-
console.log(chalk7.dim(` Max wait: ${configSources.maxWaitMs}`));
|
|
2962
|
-
}
|
|
2963
|
-
console.log(chalk7.dim(` No fallback: ${configSources.noFallback}
|
|
2964
|
-
`));
|
|
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");
|
|
2965
2972
|
}
|
|
2966
2973
|
return {
|
|
2967
2974
|
includePattern,
|
|
@@ -2982,9 +2989,9 @@ function computeStringsHash(texts) {
|
|
|
2982
2989
|
}
|
|
2983
2990
|
function readCachedStringsHash(projectRoot, branch) {
|
|
2984
2991
|
const filePath = getCacheFilePath(projectRoot, branch);
|
|
2985
|
-
if (!
|
|
2992
|
+
if (!existsSync2(filePath)) return null;
|
|
2986
2993
|
try {
|
|
2987
|
-
const raw = JSON.parse(
|
|
2994
|
+
const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
2988
2995
|
if (isRecord(raw) && typeof raw.stringsHash === "string")
|
|
2989
2996
|
return raw.stringsHash;
|
|
2990
2997
|
} catch {
|
|
@@ -3036,7 +3043,7 @@ function parseTranslations(value) {
|
|
|
3036
3043
|
}
|
|
3037
3044
|
function getCacheFilePath(projectRoot, branch) {
|
|
3038
3045
|
const branchHash = createHash("sha1").update(branch).digest("hex").slice(0, 12);
|
|
3039
|
-
return
|
|
3046
|
+
return join3(
|
|
3040
3047
|
projectRoot,
|
|
3041
3048
|
"node_modules",
|
|
3042
3049
|
".vocoder",
|
|
@@ -3049,11 +3056,11 @@ function readLocalSnapshotCache(params) {
|
|
|
3049
3056
|
const candidateBranches = params.branch === "main" ? ["main"] : [params.branch, "main"];
|
|
3050
3057
|
for (const candidateBranch of candidateBranches) {
|
|
3051
3058
|
const cacheFilePath = getCacheFilePath(params.projectRoot, candidateBranch);
|
|
3052
|
-
if (!
|
|
3059
|
+
if (!existsSync2(cacheFilePath)) {
|
|
3053
3060
|
continue;
|
|
3054
3061
|
}
|
|
3055
3062
|
try {
|
|
3056
|
-
const raw =
|
|
3063
|
+
const raw = readFileSync3(cacheFilePath, "utf-8");
|
|
3057
3064
|
const parsed = JSON.parse(raw);
|
|
3058
3065
|
if (!isRecord(parsed)) {
|
|
3059
3066
|
continue;
|
|
@@ -3079,7 +3086,7 @@ function readLocalSnapshotCache(params) {
|
|
|
3079
3086
|
function writeLocalSnapshotCache(params) {
|
|
3080
3087
|
const cacheFilePath = getCacheFilePath(params.projectRoot, params.branch);
|
|
3081
3088
|
mkdirSync2(
|
|
3082
|
-
|
|
3089
|
+
join3(params.projectRoot, "node_modules", ".vocoder", "cache", "sync"),
|
|
3083
3090
|
{
|
|
3084
3091
|
recursive: true
|
|
3085
3092
|
}
|
|
@@ -3096,7 +3103,7 @@ function writeLocalSnapshotCache(params) {
|
|
|
3096
3103
|
...params.localeMetadata ? { localeMetadata: params.localeMetadata } : {},
|
|
3097
3104
|
translations: params.translations
|
|
3098
3105
|
};
|
|
3099
|
-
|
|
3106
|
+
writeFileSync3(cacheFilePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
3100
3107
|
return cacheFilePath;
|
|
3101
3108
|
}
|
|
3102
3109
|
function resolveEffectiveModeFromPolicy(params) {
|
|
@@ -3250,8 +3257,8 @@ async function fetchApiSnapshot(api, params) {
|
|
|
3250
3257
|
async function sync(options = {}) {
|
|
3251
3258
|
const startTime = Date.now();
|
|
3252
3259
|
const projectRoot = process.cwd();
|
|
3253
|
-
|
|
3254
|
-
const spinner4 =
|
|
3260
|
+
p8.intro("Vocoder Sync");
|
|
3261
|
+
const spinner4 = p8.spinner();
|
|
3255
3262
|
try {
|
|
3256
3263
|
spinner4.start("Detecting branch");
|
|
3257
3264
|
const branch = detectBranch(options.branch);
|
|
@@ -3280,12 +3287,12 @@ async function sync(options = {}) {
|
|
|
3280
3287
|
};
|
|
3281
3288
|
spinner4.stop("Project configuration loaded");
|
|
3282
3289
|
if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
|
|
3283
|
-
|
|
3290
|
+
p8.log.warn(
|
|
3284
3291
|
`Skipping translations (${chalk8.cyan(branch)} is not a target branch)`
|
|
3285
3292
|
);
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3293
|
+
p8.log.info(`Target branches: ${config.targetBranches.join(", ")}`);
|
|
3294
|
+
p8.log.info("Use --force to translate anyway");
|
|
3295
|
+
p8.outro("");
|
|
3289
3296
|
return 0;
|
|
3290
3297
|
}
|
|
3291
3298
|
const patternsDisplay = Array.isArray(config.includePattern) ? config.includePattern.join(", ") : config.includePattern;
|
|
@@ -3298,10 +3305,10 @@ async function sync(options = {}) {
|
|
|
3298
3305
|
);
|
|
3299
3306
|
if (extractedStrings.length === 0) {
|
|
3300
3307
|
spinner4.stop("No translatable strings found");
|
|
3301
|
-
|
|
3308
|
+
p8.log.warn(
|
|
3302
3309
|
"Make sure you are wrapping translatable strings with Vocoder"
|
|
3303
3310
|
);
|
|
3304
|
-
|
|
3311
|
+
p8.outro("");
|
|
3305
3312
|
return 0;
|
|
3306
3313
|
}
|
|
3307
3314
|
spinner4.stop(
|
|
@@ -3312,10 +3319,10 @@ async function sync(options = {}) {
|
|
|
3312
3319
|
if (extractedStrings.length > 5) {
|
|
3313
3320
|
sampleLines.push(` ... and ${extractedStrings.length - 5} more`);
|
|
3314
3321
|
}
|
|
3315
|
-
|
|
3322
|
+
p8.note(sampleLines.join("\n"), "Sample strings");
|
|
3316
3323
|
}
|
|
3317
3324
|
if (options.dryRun) {
|
|
3318
|
-
|
|
3325
|
+
p8.note(
|
|
3319
3326
|
[
|
|
3320
3327
|
`Strings: ${extractedStrings.length}`,
|
|
3321
3328
|
`Branch: ${branch}`,
|
|
@@ -3326,12 +3333,12 @@ async function sync(options = {}) {
|
|
|
3326
3333
|
].join("\n"),
|
|
3327
3334
|
"Dry run - would translate"
|
|
3328
3335
|
);
|
|
3329
|
-
|
|
3336
|
+
p8.outro("No API calls made.");
|
|
3330
3337
|
return 0;
|
|
3331
3338
|
}
|
|
3332
3339
|
const repoIdentity = resolveGitRepositoryIdentity();
|
|
3333
3340
|
if (!repoIdentity && options.verbose) {
|
|
3334
|
-
|
|
3341
|
+
p8.log.warn(
|
|
3335
3342
|
"Could not detect git remote origin. Sync will continue without repo metadata."
|
|
3336
3343
|
);
|
|
3337
3344
|
}
|
|
@@ -3339,16 +3346,32 @@ async function sync(options = {}) {
|
|
|
3339
3346
|
const stringEntries = buildStringEntries(extractedStrings);
|
|
3340
3347
|
const sourceStrings = stringEntries.map((entry) => entry.text);
|
|
3341
3348
|
if (options.verbose && stringEntries.length !== extractedStrings.length) {
|
|
3342
|
-
|
|
3349
|
+
p8.log.info(
|
|
3343
3350
|
`Deduped ${extractedStrings.length} extracted entries into ${stringEntries.length} unique source strings`
|
|
3344
3351
|
);
|
|
3345
3352
|
}
|
|
3346
3353
|
const currentHash = computeStringsHash(sourceStrings);
|
|
3347
3354
|
if (!options.force) {
|
|
3348
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
|
+
}
|
|
3349
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
|
+
}
|
|
3350
3373
|
const duration2 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
3351
|
-
|
|
3374
|
+
p8.outro(`Up to date (${duration2}s)`);
|
|
3352
3375
|
return 0;
|
|
3353
3376
|
}
|
|
3354
3377
|
}
|
|
@@ -3373,31 +3396,31 @@ async function sync(options = {}) {
|
|
|
3373
3396
|
policy: config.syncPolicy
|
|
3374
3397
|
});
|
|
3375
3398
|
if (options.verbose) {
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3399
|
+
p8.log.info(`Requested mode: ${requestedMode}`);
|
|
3400
|
+
p8.log.info(`Effective mode: ${effectiveMode}`);
|
|
3401
|
+
p8.log.info(`Wait timeout: ${waitTimeoutMs}ms`);
|
|
3379
3402
|
if (batchResponse.queueStatus) {
|
|
3380
|
-
|
|
3403
|
+
p8.log.info(`Queue status: ${batchResponse.queueStatus}`);
|
|
3381
3404
|
}
|
|
3382
3405
|
}
|
|
3383
3406
|
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
3384
|
-
|
|
3407
|
+
p8.log.success("No changes detected - strings are up to date");
|
|
3385
3408
|
}
|
|
3386
|
-
|
|
3409
|
+
p8.log.info(`New strings: ${chalk8.cyan(batchResponse.newStrings)}`);
|
|
3387
3410
|
if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
|
|
3388
|
-
|
|
3411
|
+
p8.log.info(
|
|
3389
3412
|
`Deleted strings: ${chalk8.yellow(batchResponse.deletedStrings)} (archived)`
|
|
3390
3413
|
);
|
|
3391
3414
|
}
|
|
3392
|
-
|
|
3415
|
+
p8.log.info(`Total strings: ${chalk8.cyan(batchResponse.totalStrings)}`);
|
|
3393
3416
|
if (batchResponse.newStrings === 0) {
|
|
3394
|
-
|
|
3417
|
+
p8.log.success("No new strings - using existing translations");
|
|
3395
3418
|
} else {
|
|
3396
|
-
|
|
3419
|
+
p8.log.info(
|
|
3397
3420
|
`Syncing to ${config.targetLocales.length} locales (${config.targetLocales.join(", ")})`
|
|
3398
3421
|
);
|
|
3399
3422
|
if (batchResponse.estimatedTime) {
|
|
3400
|
-
|
|
3423
|
+
p8.log.info(`Estimated time: ~${batchResponse.estimatedTime}s`);
|
|
3401
3424
|
}
|
|
3402
3425
|
}
|
|
3403
3426
|
let artifacts = null;
|
|
@@ -3435,7 +3458,7 @@ async function sync(options = {}) {
|
|
|
3435
3458
|
if (effectiveMode === "required") {
|
|
3436
3459
|
throw waitError;
|
|
3437
3460
|
}
|
|
3438
|
-
|
|
3461
|
+
p8.log.warn(`Best-effort wait ended early: ${waitError.message}`);
|
|
3439
3462
|
}
|
|
3440
3463
|
}
|
|
3441
3464
|
if (!artifacts) {
|
|
@@ -3469,7 +3492,7 @@ async function sync(options = {}) {
|
|
|
3469
3492
|
spinner4.stop("Failed to fetch API snapshot");
|
|
3470
3493
|
if (options.verbose) {
|
|
3471
3494
|
const message = error instanceof Error ? error.message : "Unknown snapshot fetch error";
|
|
3472
|
-
|
|
3495
|
+
p8.log.warn(`Snapshot fetch error: ${message}`);
|
|
3473
3496
|
}
|
|
3474
3497
|
}
|
|
3475
3498
|
}
|
|
@@ -3503,61 +3526,61 @@ async function sync(options = {}) {
|
|
|
3503
3526
|
completedAt: artifacts.completedAt ?? (artifacts.source === "fresh" ? (/* @__PURE__ */ new Date()).toISOString() : null)
|
|
3504
3527
|
});
|
|
3505
3528
|
if (options.verbose) {
|
|
3506
|
-
|
|
3529
|
+
p8.log.info(`Cached snapshot: ${cachePath}`);
|
|
3507
3530
|
}
|
|
3508
3531
|
} catch (error) {
|
|
3509
3532
|
if (options.verbose) {
|
|
3510
3533
|
const message = error instanceof Error ? error.message : "Unknown cache write error";
|
|
3511
|
-
|
|
3534
|
+
p8.log.warn(`Failed to write local snapshot cache: ${message}`);
|
|
3512
3535
|
}
|
|
3513
3536
|
}
|
|
3514
3537
|
if (artifacts.source !== "fresh") {
|
|
3515
3538
|
const sourceLabel = artifacts.source === "local-cache" ? "local cached snapshot" : "completed API snapshot";
|
|
3516
|
-
|
|
3539
|
+
p8.log.warn(
|
|
3517
3540
|
`Using ${sourceLabel}. New strings may appear after the background sync completes.`
|
|
3518
3541
|
);
|
|
3519
3542
|
}
|
|
3520
3543
|
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
3521
|
-
|
|
3544
|
+
p8.outro(`Sync complete! (${duration}s)`);
|
|
3522
3545
|
return 0;
|
|
3523
3546
|
} catch (error) {
|
|
3524
3547
|
spinner4.stop();
|
|
3525
3548
|
if (error instanceof VocoderAPIError && error.syncPolicyError) {
|
|
3526
|
-
|
|
3549
|
+
p8.log.error(error.syncPolicyError.message);
|
|
3527
3550
|
const guidance = getSyncPolicyErrorGuidance(error.syncPolicyError);
|
|
3528
3551
|
for (const line of guidance) {
|
|
3529
|
-
|
|
3552
|
+
p8.log.info(line);
|
|
3530
3553
|
}
|
|
3531
3554
|
return 1;
|
|
3532
3555
|
}
|
|
3533
3556
|
if (error instanceof VocoderAPIError && error.limitError) {
|
|
3534
3557
|
const { limitError } = error;
|
|
3535
|
-
|
|
3558
|
+
p8.log.error(limitError.message);
|
|
3536
3559
|
const guidance = getLimitErrorGuidance(limitError);
|
|
3537
3560
|
for (const line of guidance) {
|
|
3538
|
-
|
|
3561
|
+
p8.log.info(line);
|
|
3539
3562
|
}
|
|
3540
3563
|
return 1;
|
|
3541
3564
|
}
|
|
3542
3565
|
if (error instanceof Error) {
|
|
3543
|
-
|
|
3566
|
+
p8.log.error(error.message);
|
|
3544
3567
|
if (error.message.includes("VOCODER_API_KEY")) {
|
|
3545
|
-
|
|
3568
|
+
p8.log.warn(
|
|
3546
3569
|
"VOCODER_API_KEY is only needed for `vocoder sync` (CLI push)."
|
|
3547
3570
|
);
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
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(
|
|
3552
3575
|
" Note: If you use @vocoder/unplugin, `vocoder sync` is optional."
|
|
3553
3576
|
);
|
|
3554
|
-
|
|
3577
|
+
p8.log.info(" Translations are fetched automatically at build time.");
|
|
3555
3578
|
} else if (error.message.includes("git branch")) {
|
|
3556
|
-
|
|
3557
|
-
|
|
3579
|
+
p8.log.warn("Run from a git repository, or use:");
|
|
3580
|
+
p8.log.info(" vocoder sync --branch main");
|
|
3558
3581
|
}
|
|
3559
3582
|
if (options.verbose) {
|
|
3560
|
-
|
|
3583
|
+
p8.log.info(`Full error: ${error.stack ?? error}`);
|
|
3561
3584
|
}
|
|
3562
3585
|
}
|
|
3563
3586
|
return 1;
|
|
@@ -3565,26 +3588,26 @@ async function sync(options = {}) {
|
|
|
3565
3588
|
}
|
|
3566
3589
|
|
|
3567
3590
|
// src/commands/whoami.ts
|
|
3568
|
-
import * as
|
|
3591
|
+
import * as p9 from "@clack/prompts";
|
|
3569
3592
|
import chalk9 from "chalk";
|
|
3570
3593
|
async function whoami(options = {}) {
|
|
3571
3594
|
const stored = readAuthData();
|
|
3572
3595
|
if (!stored) {
|
|
3573
|
-
|
|
3596
|
+
p9.log.info("Not logged in. Run `vocoder init` to authenticate.");
|
|
3574
3597
|
return 1;
|
|
3575
3598
|
}
|
|
3576
3599
|
const apiUrl = options.apiUrl ?? stored.apiUrl ?? "https://vocoder.app";
|
|
3577
3600
|
const api = new VocoderAPI({ apiUrl, apiKey: "" });
|
|
3578
3601
|
try {
|
|
3579
3602
|
const info = await api.getCliUserInfo(stored.token);
|
|
3580
|
-
|
|
3603
|
+
p9.log.info(`Logged in as ${chalk9.bold(info.email)}`);
|
|
3581
3604
|
if (info.name) {
|
|
3582
|
-
|
|
3605
|
+
p9.log.info(`Name: ${info.name}`);
|
|
3583
3606
|
}
|
|
3584
|
-
|
|
3607
|
+
p9.log.info(`API: ${apiUrl}`);
|
|
3585
3608
|
return 0;
|
|
3586
3609
|
} catch {
|
|
3587
|
-
|
|
3610
|
+
p9.log.error(
|
|
3588
3611
|
"Stored credentials are invalid or expired. Run `vocoder init` to re-authenticate."
|
|
3589
3612
|
);
|
|
3590
3613
|
return 1;
|