@kopynator/cli 1.3.0 → 1.4.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 +2 -0
- package/dist/index.js +118 -36
- package/package.json +1 -1
- package/src/commands/sync.ts +41 -22
- package/src/commands/upload.ts +97 -22
- package/src/index.ts +9 -1
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ npx -y @kopynator/cli sync
|
|
|
43
43
|
### 4. Upload
|
|
44
44
|
Uploads a JSON translation file to the Kopynator Cloud. Keys are merged/updated for the given language. Use the same API key (token) as in your app or `kopynator.config.json`.
|
|
45
45
|
|
|
46
|
+
**Project:** The target project is determined by the token. Each token is linked to one project when created in [Dashboard → Settings → Tokens](https://www.kopynator.com/dashboard/settings/tokens). To upload to a different project, use that project’s token (e.g. another `apiKey` in `kopynator.config.json` or a separate config).
|
|
47
|
+
|
|
46
48
|
```bash
|
|
47
49
|
# Language inferred from filename (es.json → es)
|
|
48
50
|
npx -y @kopynator/cli upload --file=src/assets/i18n/es.json
|
package/dist/index.js
CHANGED
|
@@ -341,10 +341,32 @@ function getTranslationDir(framework) {
|
|
|
341
341
|
return import_path3.default.join(process.cwd(), "locales");
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
|
+
function loadJsonConfig(jsonPath) {
|
|
345
|
+
if (!import_fs3.default.existsSync(jsonPath)) return null;
|
|
346
|
+
try {
|
|
347
|
+
const config = JSON.parse(import_fs3.default.readFileSync(jsonPath, "utf-8"));
|
|
348
|
+
if (config.apiKey) return { apiKey: config.apiKey, baseUrl: config.baseUrl };
|
|
349
|
+
} catch (e) {
|
|
350
|
+
if (jsonPath.includes("kopynator.config.json")) {
|
|
351
|
+
console.log(import_chalk3.default.red("\u274C Error parsing kopynator.config.json"));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
344
356
|
function extractApiKey() {
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
357
|
+
const cwd = process.cwd();
|
|
358
|
+
const configFromRoot = loadJsonConfig(import_path3.default.join(cwd, "kopynator.config.json"));
|
|
359
|
+
if (configFromRoot) {
|
|
360
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Found API key in kopynator.config.json"));
|
|
361
|
+
return configFromRoot;
|
|
362
|
+
}
|
|
363
|
+
const configFromSrc = loadJsonConfig(import_path3.default.join(cwd, "src/kopynator.config.json"));
|
|
364
|
+
if (configFromSrc) {
|
|
365
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Found API key in src/kopynator.config.json"));
|
|
366
|
+
return configFromSrc;
|
|
367
|
+
}
|
|
368
|
+
const appConfigPath = import_path3.default.join(cwd, "src/app/app.config.ts");
|
|
369
|
+
const appModulePath = import_path3.default.join(cwd, "src/app/app.module.ts");
|
|
348
370
|
if (import_fs3.default.existsSync(appConfigPath)) {
|
|
349
371
|
const content = import_fs3.default.readFileSync(appConfigPath, "utf-8");
|
|
350
372
|
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
@@ -361,16 +383,13 @@ function extractApiKey() {
|
|
|
361
383
|
return { apiKey: apiKeyMatch[1] };
|
|
362
384
|
}
|
|
363
385
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
} catch (e) {
|
|
372
|
-
console.log(import_chalk3.default.red("\u274C Error parsing kopynator.config.json"));
|
|
373
|
-
}
|
|
386
|
+
const envKey = process.env.KOPYNATOR_API_KEY?.trim();
|
|
387
|
+
if (envKey) {
|
|
388
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Using API key from KOPYNATOR_API_KEY"));
|
|
389
|
+
return {
|
|
390
|
+
apiKey: envKey,
|
|
391
|
+
baseUrl: process.env.KOPYNATOR_BASE_URL?.trim() || void 0
|
|
392
|
+
};
|
|
374
393
|
}
|
|
375
394
|
return null;
|
|
376
395
|
}
|
|
@@ -446,7 +465,7 @@ async function syncCommand() {
|
|
|
446
465
|
const languagesUrl = `${baseUrl}/languages?token=${token}`;
|
|
447
466
|
const languagesResponse = await fetch(languagesUrl, {
|
|
448
467
|
headers: {
|
|
449
|
-
"x-kopynator-version": "1.
|
|
468
|
+
"x-kopynator-version": "1.4.0"
|
|
450
469
|
}
|
|
451
470
|
});
|
|
452
471
|
if (!languagesResponse.ok) {
|
|
@@ -466,7 +485,7 @@ async function syncCommand() {
|
|
|
466
485
|
try {
|
|
467
486
|
const translationResponse = await fetch(fetchUrl, {
|
|
468
487
|
headers: {
|
|
469
|
-
"x-kopynator-version": "1.
|
|
488
|
+
"x-kopynator-version": "1.4.0"
|
|
470
489
|
}
|
|
471
490
|
});
|
|
472
491
|
if (!translationResponse.ok) {
|
|
@@ -513,10 +532,24 @@ function flatten(data, prefix = "") {
|
|
|
513
532
|
}
|
|
514
533
|
return result;
|
|
515
534
|
}
|
|
535
|
+
function loadJsonConfig2(jsonPath) {
|
|
536
|
+
if (!import_fs4.default.existsSync(jsonPath)) return null;
|
|
537
|
+
try {
|
|
538
|
+
const config = JSON.parse(import_fs4.default.readFileSync(jsonPath, "utf-8"));
|
|
539
|
+
const key = config.apiKey ?? config.api_key;
|
|
540
|
+
if (key && typeof key === "string") return { apiKey: key, baseUrl: config.baseUrl };
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
516
545
|
function extractApiKey2() {
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
const
|
|
546
|
+
const cwd = process.cwd();
|
|
547
|
+
const appConfigPath = import_path4.default.join(cwd, "src/app/app.config.ts");
|
|
548
|
+
const appModulePath = import_path4.default.join(cwd, "src/app/app.module.ts");
|
|
549
|
+
const configFromRoot = loadJsonConfig2(import_path4.default.join(cwd, "kopynator.config.json"));
|
|
550
|
+
if (configFromRoot) return configFromRoot;
|
|
551
|
+
const configFromSrc = loadJsonConfig2(import_path4.default.join(cwd, "src/kopynator.config.json"));
|
|
552
|
+
if (configFromSrc) return configFromSrc;
|
|
520
553
|
if (import_fs4.default.existsSync(appConfigPath)) {
|
|
521
554
|
const content = import_fs4.default.readFileSync(appConfigPath, "utf-8");
|
|
522
555
|
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
@@ -527,12 +560,12 @@ function extractApiKey2() {
|
|
|
527
560
|
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
528
561
|
if (apiKeyMatch) return { apiKey: apiKeyMatch[1] };
|
|
529
562
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
563
|
+
const envKey = process.env.KOPYNATOR_API_KEY?.trim();
|
|
564
|
+
if (envKey) {
|
|
565
|
+
return {
|
|
566
|
+
apiKey: envKey,
|
|
567
|
+
baseUrl: process.env.KOPYNATOR_BASE_URL?.trim() || void 0
|
|
568
|
+
};
|
|
536
569
|
}
|
|
537
570
|
return null;
|
|
538
571
|
}
|
|
@@ -544,7 +577,18 @@ async function uploadCommand(options) {
|
|
|
544
577
|
console.log(import_chalk4.default.bold.blue("\n\u{1F4E4} Upload translations to Kopynator Cloud...\n"));
|
|
545
578
|
const config = extractApiKey2();
|
|
546
579
|
if (!config) {
|
|
547
|
-
|
|
580
|
+
const cwd = process.cwd();
|
|
581
|
+
const rootConfig = import_path4.default.join(cwd, "kopynator.config.json");
|
|
582
|
+
const srcConfig = import_path4.default.join(cwd, "src/kopynator.config.json");
|
|
583
|
+
console.log(import_chalk4.default.red("\u274C Could not find API key. Set it in kopynator.config.json, app.config.ts, or KOPYNATOR_API_KEY. Run `npx kopynator init` to create config."));
|
|
584
|
+
console.log(import_chalk4.default.gray(` Directorio actual: ${cwd}`));
|
|
585
|
+
console.log(import_chalk4.default.gray(` Comprobado: ${rootConfig} (${import_fs4.default.existsSync(rootConfig) ? "existe" : "no existe"})`));
|
|
586
|
+
console.log(import_chalk4.default.gray(` Comprobado: ${srcConfig} (${import_fs4.default.existsSync(srcConfig) ? "existe" : "no existe"})`));
|
|
587
|
+
if (import_fs4.default.existsSync(rootConfig) || import_fs4.default.existsSync(srcConfig)) {
|
|
588
|
+
console.log(import_chalk4.default.yellow('\u{1F4A1} Si el archivo existe, comprueba que tenga la propiedad "apiKey" (o "api_key") con un valor no vac\xEDo.'));
|
|
589
|
+
} else {
|
|
590
|
+
console.log(import_chalk4.default.yellow("\u{1F4A1} Ejecuta el comando desde la ra\xEDz del proyecto (donde est\xE1 package.json)."));
|
|
591
|
+
}
|
|
548
592
|
return;
|
|
549
593
|
}
|
|
550
594
|
const fileOption = options.file;
|
|
@@ -558,7 +602,7 @@ async function uploadCommand(options) {
|
|
|
558
602
|
return;
|
|
559
603
|
}
|
|
560
604
|
const lang = options.lang || inferLangFromFile(resolvedPath);
|
|
561
|
-
const baseUrl = config.baseUrl || "https://api.kopynator.com/
|
|
605
|
+
const baseUrl = config.baseUrl || "https://api.kopynator.com/tokens";
|
|
562
606
|
const token = config.apiKey;
|
|
563
607
|
let raw;
|
|
564
608
|
try {
|
|
@@ -583,41 +627,79 @@ async function uploadCommand(options) {
|
|
|
583
627
|
batches.push(Object.fromEntries(entries.slice(i, i + BATCH_SIZE)));
|
|
584
628
|
}
|
|
585
629
|
const spinner = (0, import_ora2.default)(`Uploading ${total} keys (${lang})...`).start();
|
|
586
|
-
|
|
630
|
+
const DEFAULT_API_BASE = "https://api.kopynator.com/tokens";
|
|
631
|
+
const FALLBACK_API_BASE = "https://api.kopynator.com/api/tokens";
|
|
632
|
+
const doUpload = async (urlBase) => {
|
|
587
633
|
let imported = 0;
|
|
588
634
|
for (let i = 0; i < batches.length; i++) {
|
|
589
635
|
spinner.text = `Uploading batch ${i + 1}/${batches.length}...`;
|
|
590
|
-
const res = await fetch(`${
|
|
636
|
+
const res = await fetch(`${urlBase}/import`, {
|
|
591
637
|
method: "POST",
|
|
592
638
|
headers: {
|
|
593
639
|
"Content-Type": "application/json",
|
|
594
640
|
"x-api-token": token,
|
|
595
|
-
"x-kopynator-version": "1.
|
|
641
|
+
"x-kopynator-version": "1.4.0"
|
|
596
642
|
},
|
|
597
643
|
body: JSON.stringify({ lang, data: batches[i] })
|
|
598
644
|
});
|
|
599
645
|
if (!res.ok) {
|
|
600
646
|
const errText = await res.text();
|
|
601
|
-
|
|
602
|
-
|
|
647
|
+
let errMsg;
|
|
648
|
+
try {
|
|
649
|
+
const errJson = JSON.parse(errText);
|
|
650
|
+
errMsg = errJson.message || errJson.error || errText;
|
|
651
|
+
} catch {
|
|
652
|
+
errMsg = errText || res.statusText;
|
|
653
|
+
}
|
|
654
|
+
return { ok: false, status: res.status, errMsg };
|
|
603
655
|
}
|
|
604
656
|
const result = await res.json();
|
|
605
|
-
imported += result.importedCount ?? batches[i].length;
|
|
657
|
+
imported += Number(result.importedCount ?? Object.keys(batches[i]).length);
|
|
606
658
|
}
|
|
607
|
-
|
|
608
|
-
|
|
659
|
+
return { ok: true, imported };
|
|
660
|
+
};
|
|
661
|
+
try {
|
|
662
|
+
let result = await doUpload(baseUrl);
|
|
663
|
+
let triedFallback = false;
|
|
664
|
+
if (!result.ok && result.status === 404 && baseUrl === DEFAULT_API_BASE) {
|
|
665
|
+
spinner.text = "Reintentando con URL alternativa...";
|
|
666
|
+
result = await doUpload(FALLBACK_API_BASE);
|
|
667
|
+
triedFallback = true;
|
|
668
|
+
}
|
|
669
|
+
if (result.ok) {
|
|
670
|
+
spinner.succeed(import_chalk4.default.green(`Uploaded ${result.imported} keys for language "${lang}".`));
|
|
671
|
+
console.log(import_chalk4.default.bold.green("\n\u{1F389} Upload completed. Run `npx kopynator sync` to pull latest from cloud.\n"));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
spinner.fail(`Upload failed: ${result.status} ${result.errMsg}`);
|
|
675
|
+
if (result.status === 401) {
|
|
676
|
+
console.log(import_chalk4.default.yellow("\u{1F4A1} Comprueba que el API key sea correcto (Dashboard \u2192 Settings \u2192 Tokens)."));
|
|
677
|
+
}
|
|
678
|
+
if (result.status === 404) {
|
|
679
|
+
console.log(import_chalk4.default.yellow("\u{1F4A1} Comprueba que la API est\xE9 desplegada y que la URL sea correcta (KOPYNATOR_BASE_URL si usas entorno propio)."));
|
|
680
|
+
}
|
|
681
|
+
console.log(import_chalk4.default.gray(` URL usada: ${baseUrl}/import`));
|
|
682
|
+
if (triedFallback) console.log(import_chalk4.default.gray(` (tambi\xE9n probada: ${FALLBACK_API_BASE}/import)`));
|
|
609
683
|
} catch (error) {
|
|
610
|
-
|
|
684
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
685
|
+
spinner.fail(`Upload failed: ${msg}`);
|
|
686
|
+
if (msg.includes("fetch") || msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND")) {
|
|
687
|
+
console.log(import_chalk4.default.yellow("\u{1F4A1} Comprueba conexi\xF3n y que la API est\xE9 disponible. Para entorno local: KOPYNATOR_BASE_URL=http://localhost:7300/api/tokens"));
|
|
688
|
+
}
|
|
611
689
|
}
|
|
612
690
|
}
|
|
613
691
|
|
|
614
692
|
// src/index.ts
|
|
615
693
|
var program = new import_commander.Command();
|
|
616
|
-
program.name("kopynator").description("Kopynator CLI - Manage your i18n workflow").version("1.
|
|
694
|
+
program.name("kopynator").description("Kopynator CLI - Manage your i18n workflow").version("1.4.0");
|
|
617
695
|
program.command("init").description("Initialize Kopynator in your project").action(initCommand);
|
|
618
696
|
program.command("check").description("Validate your local JSON translation files").action(checkCommand);
|
|
619
697
|
program.command("sync").description("Sync your translations with the Kopynator Cloud").action(syncCommand);
|
|
620
698
|
program.command("upload").description("Upload a JSON translation file to Kopynator Cloud").option("-f, --file <path>", "Path to the JSON file (e.g. es.json)").option("-l, --lang <code>", "Language code (default: inferred from filename)").action((opts) => uploadCommand({ file: opts.file, lang: opts.lang }));
|
|
699
|
+
program.command("help").description("Show help for all commands").action(() => {
|
|
700
|
+
console.log(import_chalk5.default.blue("\u{1F44B} Kopynator CLI - Comandos disponibles:\n"));
|
|
701
|
+
program.outputHelp();
|
|
702
|
+
});
|
|
621
703
|
program.parse(process.argv);
|
|
622
704
|
if (!process.argv.slice(2).length) {
|
|
623
705
|
console.log(import_chalk5.default.blue("\u{1F44B} Welcome to Kopynator CLI!"));
|
package/package.json
CHANGED
package/src/commands/sync.ts
CHANGED
|
@@ -65,15 +65,40 @@ function getTranslationDir(framework: string): string {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
68
|
+
* API key del proyecto primero (no hace falta en el comando), luego env para CI.
|
|
69
|
+
* Order: app.config.ts -> app.module.ts -> kopynator.config.json -> KOPYNATOR_API_KEY
|
|
70
70
|
*/
|
|
71
|
+
function loadJsonConfig(jsonPath: string): KopyConfig | null {
|
|
72
|
+
if (!fs.existsSync(jsonPath)) return null;
|
|
73
|
+
try {
|
|
74
|
+
const config = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
75
|
+
if (config.apiKey) return { apiKey: config.apiKey, baseUrl: config.baseUrl };
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (jsonPath.includes('kopynator.config.json')) {
|
|
78
|
+
console.log(chalk.red('❌ Error parsing kopynator.config.json'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
function extractApiKey(): KopyConfig | null {
|
|
72
|
-
const
|
|
73
|
-
const appModulePath = path.join(process.cwd(), 'src/app/app.module.ts');
|
|
74
|
-
const jsonConfigPath = path.join(process.cwd(), 'kopynator.config.json');
|
|
85
|
+
const cwd = process.cwd();
|
|
75
86
|
|
|
76
|
-
//
|
|
87
|
+
// 1) kopynator.config.json (raíz o src/) — funciona aunque en app.config uses apiKey: kopynatorConfig.apiKey
|
|
88
|
+
const configFromRoot = loadJsonConfig(path.join(cwd, 'kopynator.config.json'));
|
|
89
|
+
if (configFromRoot) {
|
|
90
|
+
console.log(chalk.blue('ℹ️ Found API key in kopynator.config.json'));
|
|
91
|
+
return configFromRoot;
|
|
92
|
+
}
|
|
93
|
+
const configFromSrc = loadJsonConfig(path.join(cwd, 'src/kopynator.config.json'));
|
|
94
|
+
if (configFromSrc) {
|
|
95
|
+
console.log(chalk.blue('ℹ️ Found API key in src/kopynator.config.json'));
|
|
96
|
+
return configFromSrc;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 2) Literales en app.config.ts / app.module.ts
|
|
100
|
+
const appConfigPath = path.join(cwd, 'src/app/app.config.ts');
|
|
101
|
+
const appModulePath = path.join(cwd, 'src/app/app.module.ts');
|
|
77
102
|
if (fs.existsSync(appConfigPath)) {
|
|
78
103
|
const content = fs.readFileSync(appConfigPath, 'utf-8');
|
|
79
104
|
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
@@ -82,8 +107,6 @@ function extractApiKey(): KopyConfig | null {
|
|
|
82
107
|
return { apiKey: apiKeyMatch[1] };
|
|
83
108
|
}
|
|
84
109
|
}
|
|
85
|
-
|
|
86
|
-
// Try app.module.ts
|
|
87
110
|
if (fs.existsSync(appModulePath)) {
|
|
88
111
|
const content = fs.readFileSync(appModulePath, 'utf-8');
|
|
89
112
|
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
@@ -93,19 +116,15 @@ function extractApiKey(): KopyConfig | null {
|
|
|
93
116
|
}
|
|
94
117
|
}
|
|
95
118
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
} catch (e) {
|
|
105
|
-
console.log(chalk.red('❌ Error parsing kopynator.config.json'));
|
|
106
|
-
}
|
|
119
|
+
// Env (CI)
|
|
120
|
+
const envKey = process.env.KOPYNATOR_API_KEY?.trim();
|
|
121
|
+
if (envKey) {
|
|
122
|
+
console.log(chalk.blue('ℹ️ Using API key from KOPYNATOR_API_KEY'));
|
|
123
|
+
return {
|
|
124
|
+
apiKey: envKey,
|
|
125
|
+
baseUrl: process.env.KOPYNATOR_BASE_URL?.trim() || undefined,
|
|
126
|
+
};
|
|
107
127
|
}
|
|
108
|
-
|
|
109
128
|
return null;
|
|
110
129
|
}
|
|
111
130
|
|
|
@@ -204,7 +223,7 @@ export async function syncCommand() {
|
|
|
204
223
|
const languagesUrl = `${baseUrl}/languages?token=${token}`;
|
|
205
224
|
const languagesResponse = await fetch(languagesUrl, {
|
|
206
225
|
headers: {
|
|
207
|
-
'x-kopynator-version': '1.
|
|
226
|
+
'x-kopynator-version': '1.4.0'
|
|
208
227
|
}
|
|
209
228
|
});
|
|
210
229
|
if (!languagesResponse.ok) {
|
|
@@ -230,7 +249,7 @@ export async function syncCommand() {
|
|
|
230
249
|
try {
|
|
231
250
|
const translationResponse = await fetch(fetchUrl, {
|
|
232
251
|
headers: {
|
|
233
|
-
'x-kopynator-version': '1.
|
|
252
|
+
'x-kopynator-version': '1.4.0'
|
|
234
253
|
}
|
|
235
254
|
});
|
|
236
255
|
|
package/src/commands/upload.ts
CHANGED
|
@@ -27,11 +27,35 @@ function flatten(data: Record<string, unknown>, prefix = ''): Record<string, str
|
|
|
27
27
|
return result;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Obtiene el API key y baseUrl del proyecto para enviarlos en la petición.
|
|
32
|
+
* Prioridad: archivos del proyecto (no hace falta poner el key en el comando) → env para CI.
|
|
33
|
+
* El backend identifica el proyecto por el token en el header x-api-token.
|
|
34
|
+
*/
|
|
35
|
+
function loadJsonConfig(jsonPath: string): KopyConfig | null {
|
|
36
|
+
if (!fs.existsSync(jsonPath)) return null;
|
|
37
|
+
try {
|
|
38
|
+
const config = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
39
|
+
const key = config.apiKey ?? config.api_key;
|
|
40
|
+
if (key && typeof key === 'string') return { apiKey: key, baseUrl: config.baseUrl };
|
|
41
|
+
} catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
function extractApiKey(): KopyConfig | null {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const appConfigPath = path.join(cwd, 'src/app/app.config.ts');
|
|
50
|
+
const appModulePath = path.join(cwd, 'src/app/app.module.ts');
|
|
51
|
+
|
|
52
|
+
// 1) kopynator.config.json (raíz o src/) — así funciona aunque en app.config uses apiKey: kopynatorConfig.apiKey
|
|
53
|
+
const configFromRoot = loadJsonConfig(path.join(cwd, 'kopynator.config.json'));
|
|
54
|
+
if (configFromRoot) return configFromRoot;
|
|
55
|
+
const configFromSrc = loadJsonConfig(path.join(cwd, 'src/kopynator.config.json'));
|
|
56
|
+
if (configFromSrc) return configFromSrc;
|
|
34
57
|
|
|
58
|
+
// 2) Literales en app.config.ts / app.module.ts
|
|
35
59
|
if (fs.existsSync(appConfigPath)) {
|
|
36
60
|
const content = fs.readFileSync(appConfigPath, 'utf-8');
|
|
37
61
|
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
@@ -42,13 +66,14 @@ function extractApiKey(): KopyConfig | null {
|
|
|
42
66
|
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
43
67
|
if (apiKeyMatch) return { apiKey: apiKeyMatch[1] };
|
|
44
68
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
|
|
70
|
+
// 2) Env (útil en CI donde no hay app.config/kopynator.config.json)
|
|
71
|
+
const envKey = process.env.KOPYNATOR_API_KEY?.trim();
|
|
72
|
+
if (envKey) {
|
|
73
|
+
return {
|
|
74
|
+
apiKey: envKey,
|
|
75
|
+
baseUrl: process.env.KOPYNATOR_BASE_URL?.trim() || undefined,
|
|
76
|
+
};
|
|
52
77
|
}
|
|
53
78
|
return null;
|
|
54
79
|
}
|
|
@@ -66,7 +91,18 @@ export async function uploadCommand(options: { file?: string; lang?: string }) {
|
|
|
66
91
|
|
|
67
92
|
const config = extractApiKey();
|
|
68
93
|
if (!config) {
|
|
69
|
-
|
|
94
|
+
const cwd = process.cwd();
|
|
95
|
+
const rootConfig = path.join(cwd, 'kopynator.config.json');
|
|
96
|
+
const srcConfig = path.join(cwd, 'src/kopynator.config.json');
|
|
97
|
+
console.log(chalk.red('❌ Could not find API key. Set it in kopynator.config.json, app.config.ts, or KOPYNATOR_API_KEY. Run `npx kopynator init` to create config.'));
|
|
98
|
+
console.log(chalk.gray(` Directorio actual: ${cwd}`));
|
|
99
|
+
console.log(chalk.gray(` Comprobado: ${rootConfig} (${fs.existsSync(rootConfig) ? 'existe' : 'no existe'})`));
|
|
100
|
+
console.log(chalk.gray(` Comprobado: ${srcConfig} (${fs.existsSync(srcConfig) ? 'existe' : 'no existe'})`));
|
|
101
|
+
if (fs.existsSync(rootConfig) || fs.existsSync(srcConfig)) {
|
|
102
|
+
console.log(chalk.yellow('💡 Si el archivo existe, comprueba que tenga la propiedad "apiKey" (o "api_key") con un valor no vacío.'));
|
|
103
|
+
} else {
|
|
104
|
+
console.log(chalk.yellow('💡 Ejecuta el comando desde la raíz del proyecto (donde está package.json).'));
|
|
105
|
+
}
|
|
70
106
|
return;
|
|
71
107
|
}
|
|
72
108
|
|
|
@@ -83,7 +119,9 @@ export async function uploadCommand(options: { file?: string; lang?: string }) {
|
|
|
83
119
|
}
|
|
84
120
|
|
|
85
121
|
const lang = options.lang || inferLangFromFile(resolvedPath);
|
|
86
|
-
|
|
122
|
+
// Backend expone /tokens/import sin prefijo /api (exclude en setGlobalPrefix)
|
|
123
|
+
const baseUrl = config.baseUrl || 'https://api.kopynator.com/tokens';
|
|
124
|
+
// Token enviado en la petición: el backend lo usa para saber a qué proyecto importar (como en la web)
|
|
87
125
|
const token = config.apiKey;
|
|
88
126
|
|
|
89
127
|
let raw: unknown;
|
|
@@ -114,32 +152,69 @@ export async function uploadCommand(options: { file?: string; lang?: string }) {
|
|
|
114
152
|
|
|
115
153
|
const spinner = ora(`Uploading ${total} keys (${lang})...`).start();
|
|
116
154
|
|
|
117
|
-
|
|
155
|
+
const DEFAULT_API_BASE = 'https://api.kopynator.com/tokens';
|
|
156
|
+
const FALLBACK_API_BASE = 'https://api.kopynator.com/api/tokens';
|
|
157
|
+
|
|
158
|
+
const doUpload = async (urlBase: string): Promise<{ ok: true; imported: number } | { ok: false; status: number; errMsg: string }> => {
|
|
118
159
|
let imported = 0;
|
|
119
160
|
for (let i = 0; i < batches.length; i++) {
|
|
120
161
|
spinner.text = `Uploading batch ${i + 1}/${batches.length}...`;
|
|
121
|
-
const res = await fetch(`${
|
|
162
|
+
const res = await fetch(`${urlBase}/import`, {
|
|
122
163
|
method: 'POST',
|
|
123
164
|
headers: {
|
|
124
165
|
'Content-Type': 'application/json',
|
|
125
166
|
'x-api-token': token,
|
|
126
|
-
'x-kopynator-version': '1.
|
|
167
|
+
'x-kopynator-version': '1.4.0',
|
|
127
168
|
},
|
|
128
169
|
body: JSON.stringify({ lang, data: batches[i] }),
|
|
129
170
|
});
|
|
130
|
-
|
|
131
171
|
if (!res.ok) {
|
|
132
172
|
const errText = await res.text();
|
|
133
|
-
|
|
134
|
-
|
|
173
|
+
let errMsg: string;
|
|
174
|
+
try {
|
|
175
|
+
const errJson = JSON.parse(errText) as { message?: string; error?: string };
|
|
176
|
+
errMsg = errJson.message || errJson.error || errText;
|
|
177
|
+
} catch {
|
|
178
|
+
errMsg = errText || res.statusText;
|
|
179
|
+
}
|
|
180
|
+
return { ok: false, status: res.status, errMsg };
|
|
135
181
|
}
|
|
136
182
|
const result = (await res.json()) as { importedCount?: number };
|
|
137
|
-
imported += result.importedCount ?? batches[i].length;
|
|
183
|
+
imported += Number(result.importedCount ?? Object.keys(batches[i]).length);
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, imported };
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
let result = await doUpload(baseUrl);
|
|
190
|
+
let triedFallback = false;
|
|
191
|
+
|
|
192
|
+
if (!result.ok && result.status === 404 && baseUrl === DEFAULT_API_BASE) {
|
|
193
|
+
spinner.text = 'Reintentando con URL alternativa...';
|
|
194
|
+
result = await doUpload(FALLBACK_API_BASE);
|
|
195
|
+
triedFallback = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (result.ok) {
|
|
199
|
+
spinner.succeed(chalk.green(`Uploaded ${result.imported} keys for language "${lang}".`));
|
|
200
|
+
console.log(chalk.bold.green('\n🎉 Upload completed. Run `npx kopynator sync` to pull latest from cloud.\n'));
|
|
201
|
+
return;
|
|
138
202
|
}
|
|
139
203
|
|
|
140
|
-
spinner.
|
|
141
|
-
|
|
204
|
+
spinner.fail(`Upload failed: ${result.status} ${result.errMsg}`);
|
|
205
|
+
if (result.status === 401) {
|
|
206
|
+
console.log(chalk.yellow('💡 Comprueba que el API key sea correcto (Dashboard → Settings → Tokens).'));
|
|
207
|
+
}
|
|
208
|
+
if (result.status === 404) {
|
|
209
|
+
console.log(chalk.yellow('💡 Comprueba que la API esté desplegada y que la URL sea correcta (KOPYNATOR_BASE_URL si usas entorno propio).'));
|
|
210
|
+
}
|
|
211
|
+
console.log(chalk.gray(` URL usada: ${baseUrl}/import`));
|
|
212
|
+
if (triedFallback) console.log(chalk.gray(` (también probada: ${FALLBACK_API_BASE}/import)`));
|
|
142
213
|
} catch (error) {
|
|
143
|
-
|
|
214
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
215
|
+
spinner.fail(`Upload failed: ${msg}`);
|
|
216
|
+
if (msg.includes('fetch') || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
217
|
+
console.log(chalk.yellow('💡 Comprueba conexión y que la API esté disponible. Para entorno local: KOPYNATOR_BASE_URL=http://localhost:7300/api/tokens'));
|
|
218
|
+
}
|
|
144
219
|
}
|
|
145
220
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ const program = new Command();
|
|
|
8
8
|
program
|
|
9
9
|
.name('kopynator')
|
|
10
10
|
.description('Kopynator CLI - Manage your i18n workflow')
|
|
11
|
-
.version('1.
|
|
11
|
+
.version('1.4.0');
|
|
12
12
|
|
|
13
13
|
program
|
|
14
14
|
.command('init')
|
|
@@ -32,6 +32,14 @@ program
|
|
|
32
32
|
.option('-l, --lang <code>', 'Language code (default: inferred from filename)')
|
|
33
33
|
.action((opts) => uploadCommand({ file: opts.file, lang: opts.lang }));
|
|
34
34
|
|
|
35
|
+
program
|
|
36
|
+
.command('help')
|
|
37
|
+
.description('Show help for all commands')
|
|
38
|
+
.action(() => {
|
|
39
|
+
console.log(chalk.blue('👋 Kopynator CLI - Comandos disponibles:\n'));
|
|
40
|
+
program.outputHelp();
|
|
41
|
+
});
|
|
42
|
+
|
|
35
43
|
program.parse(process.argv);
|
|
36
44
|
|
|
37
45
|
if (!process.argv.slice(2).length) {
|