@rglabs/butterfly 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +201 -0
- package/README.md +371 -0
- package/dist/commands/add.d.ts +23 -0
- package/dist/commands/add.js +303 -0
- package/dist/commands/code.d.ts +11 -0
- package/dist/commands/code.js +72 -0
- package/dist/commands/create-object.d.ts +6 -0
- package/dist/commands/create-object.js +293 -0
- package/dist/commands/create-report.d.ts +6 -0
- package/dist/commands/create-report.js +154 -0
- package/dist/commands/diff.d.ts +4 -0
- package/dist/commands/diff.js +238 -0
- package/dist/commands/download.d.ts +4 -0
- package/dist/commands/download.js +374 -0
- package/dist/commands/layout.d.ts +12 -0
- package/dist/commands/layout.js +83 -0
- package/dist/commands/record.d.ts +21 -0
- package/dist/commands/record.js +483 -0
- package/dist/commands/run-poc.d.ts +3 -0
- package/dist/commands/run-poc.js +18 -0
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.js +66 -0
- package/dist/commands/start-poc.d.ts +3 -0
- package/dist/commands/start-poc.js +55 -0
- package/dist/commands/sync-docs.d.ts +3 -0
- package/dist/commands/sync-docs.js +27 -0
- package/dist/commands/translate.d.ts +13 -0
- package/dist/commands/translate.js +401 -0
- package/dist/commands/upload.d.ts +3 -0
- package/dist/commands/upload.js +150 -0
- package/dist/commands/workflow-info.d.ts +13 -0
- package/dist/commands/workflow-info.js +161 -0
- package/dist/components/ConflictResolver.d.ts +12 -0
- package/dist/components/ConflictResolver.js +77 -0
- package/dist/components/DiffView.d.ts +11 -0
- package/dist/components/DiffView.js +101 -0
- package/dist/components/DownloadProgress.d.ts +11 -0
- package/dist/components/DownloadProgress.js +29 -0
- package/dist/components/RecordPreview.d.ts +11 -0
- package/dist/components/RecordPreview.js +91 -0
- package/dist/components/SetupForm.d.ts +8 -0
- package/dist/components/SetupForm.js +56 -0
- package/dist/components/UploadProgress.d.ts +13 -0
- package/dist/components/UploadProgress.js +42 -0
- package/dist/diff/adapters/index.d.ts +8 -0
- package/dist/diff/adapters/index.js +18 -0
- package/dist/diff/adapters/objectsAdapter.d.ts +13 -0
- package/dist/diff/adapters/objectsAdapter.js +177 -0
- package/dist/diff/adapters/reportsAdapter.d.ts +14 -0
- package/dist/diff/adapters/reportsAdapter.js +212 -0
- package/dist/diff/adapters/types.d.ts +19 -0
- package/dist/diff/adapters/types.js +2 -0
- package/dist/diff/engine.d.ts +19 -0
- package/dist/diff/engine.js +57 -0
- package/dist/diff/types.d.ts +34 -0
- package/dist/diff/types.js +110 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +117 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/api.d.ts +85 -0
- package/dist/utils/api.js +1031 -0
- package/dist/utils/auth.d.ts +4 -0
- package/dist/utils/auth.js +22 -0
- package/dist/utils/bfySplitter.d.ts +12 -0
- package/dist/utils/bfySplitter.js +151 -0
- package/dist/utils/docs.d.ts +16 -0
- package/dist/utils/docs.js +186 -0
- package/dist/utils/errorLogger.d.ts +6 -0
- package/dist/utils/errorLogger.js +29 -0
- package/dist/utils/files.d.ts +14 -0
- package/dist/utils/files.js +772 -0
- package/dist/utils/lockManager.d.ts +15 -0
- package/dist/utils/lockManager.js +126 -0
- package/dist/utils/resourceHandlers.d.ts +50 -0
- package/dist/utils/resourceHandlers.js +684 -0
- package/dist/utils/resourceMapping.d.ts +32 -0
- package/dist/utils/resourceMapping.js +210 -0
- package/dist/utils/singleResourceDownload.d.ts +14 -0
- package/dist/utils/singleResourceDownload.js +261 -0
- package/dist/utils/summaryGenerator.d.ts +2 -0
- package/dist/utils/summaryGenerator.js +183 -0
- package/dist/utils/uploadHandler.d.ts +31 -0
- package/dist/utils/uploadHandler.js +263 -0
- package/docs/AI_API.md +93 -0
- package/docs/CLAUDE.md +216 -0
- package/docs/PROJECT_SPECIFIC.md +1 -0
- package/docs/RECORD_COMMAND.md +262 -0
- package/docs/WORKFLOW_API.md +480 -0
- package/docs/bfy-splitting.md +126 -0
- package/docs/cli-commands.md +333 -0
- package/docs/examples/README.md +95 -0
- package/docs/examples/order-system.md +147 -0
- package/docs/examples/product-catalog.md +195 -0
- package/docs/examples/reports.md +187 -0
- package/docs/excel-export.md +216 -0
- package/docs/field-types/README.md +29 -0
- package/docs/field-types/calculated.md +147 -0
- package/docs/field-types/code-mappings.md +84 -0
- package/docs/field-types/custom.md +340 -0
- package/docs/object-specs/README.md +136 -0
- package/docs/object-specs/code-parameters.md +151 -0
- package/docs/object-specs/creating.md +203 -0
- package/docs/object-specs/js-code-examples.md +208 -0
- package/docs/object-specs/js-field-updates.md +168 -0
- package/docs/objects/README.md +89 -0
- package/docs/objects/creating.md +127 -0
- package/docs/page-layout.md +361 -0
- package/docs/permissions.md +260 -0
- package/docs/reports.md +197 -0
- package/docs/state-machines.md +544 -0
- package/docs/tasks/create-object.md +81 -0
- package/docs/translations.md +346 -0
- package/docs/twig-helpers.md +283 -0
- package/docs/webservices.md +159 -0
- package/docs/workspaces.md +176 -0
- package/package.json +59 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, Box, Text } from 'ink';
|
|
3
|
+
import { SetupForm } from '../components/SetupForm.js';
|
|
4
|
+
import { saveAuthConfig } from '../utils/auth.js';
|
|
5
|
+
import { ButterflyAPI } from '../utils/api.js';
|
|
6
|
+
import { copyDocs } from '../utils/docs.js';
|
|
7
|
+
const shared = { api: null, ink: null };
|
|
8
|
+
const SetupCommand = ({ onExit }) => {
|
|
9
|
+
const [status, setStatus] = React.useState('input');
|
|
10
|
+
const [errorMessage, setErrorMessage] = React.useState('');
|
|
11
|
+
const handleSubmit = async (config) => {
|
|
12
|
+
setStatus('testing');
|
|
13
|
+
try {
|
|
14
|
+
await saveAuthConfig(config);
|
|
15
|
+
const api = new ButterflyAPI(config);
|
|
16
|
+
await api.authenticate();
|
|
17
|
+
if (api.needs2FA) {
|
|
18
|
+
shared.api = api;
|
|
19
|
+
if (shared.ink) {
|
|
20
|
+
shared.ink.clear();
|
|
21
|
+
shared.ink.unmount();
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await copyDocs();
|
|
26
|
+
setStatus('success');
|
|
27
|
+
setTimeout(() => onExit(0), 2000);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
setErrorMessage(error instanceof Error ? error.message : 'Unknown error');
|
|
31
|
+
setStatus('error');
|
|
32
|
+
setTimeout(() => onExit(1), 3000);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
36
|
+
status === 'input' && React.createElement(SetupForm, { onSubmit: handleSubmit }),
|
|
37
|
+
status === 'testing' && React.createElement(Text, { color: "yellow" }, "Testing connection..."),
|
|
38
|
+
status === 'success' && React.createElement(Text, { color: "green" }, "\u2713 Configuration saved successfully in .butterfly/config.json, docs copied to docs/ and commands to .claude/commands/"),
|
|
39
|
+
status === 'error' && React.createElement(Text, { color: "red" },
|
|
40
|
+
"\u2717 Error: ",
|
|
41
|
+
errorMessage)));
|
|
42
|
+
};
|
|
43
|
+
async function handlePending2FA() {
|
|
44
|
+
if (!shared.api)
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
await shared.api.complete2FA();
|
|
48
|
+
await copyDocs();
|
|
49
|
+
console.log('\n✓ Configuration saved and authenticated successfully!');
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error('\nAuthentication failed:', error instanceof Error ? error.message : error);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export default async () => {
|
|
57
|
+
shared.api = null;
|
|
58
|
+
shared.ink = render(React.createElement(SetupCommand, { onExit: (code) => {
|
|
59
|
+
if (shared.ink)
|
|
60
|
+
shared.ink.unmount();
|
|
61
|
+
process.exit(code);
|
|
62
|
+
} }));
|
|
63
|
+
await shared.ink.waitUntilExit();
|
|
64
|
+
await handlePending2FA();
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, Box, Text } from 'ink';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
const POC_REPO_URL = 'git@github.com:butterfly-team/POC-Generator.git';
|
|
7
|
+
const REPO_FOLDER = 'POC-Generator';
|
|
8
|
+
const StartPocCommand = () => {
|
|
9
|
+
const [status, setStatus] = React.useState('checking');
|
|
10
|
+
const [errorMessage, setErrorMessage] = React.useState('');
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
const cloneRepo = async () => {
|
|
13
|
+
const targetPath = path.join(process.cwd(), REPO_FOLDER);
|
|
14
|
+
if (existsSync(targetPath)) {
|
|
15
|
+
setStatus('exists');
|
|
16
|
+
setTimeout(() => process.exit(0), 1500);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
setStatus('cloning');
|
|
20
|
+
try {
|
|
21
|
+
execSync(`git clone ${POC_REPO_URL}`, {
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
cwd: process.cwd()
|
|
24
|
+
});
|
|
25
|
+
setStatus('success');
|
|
26
|
+
setTimeout(() => process.exit(0), 1000);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
let message = 'Failed to clone repository';
|
|
30
|
+
if (error && typeof error === 'object' && 'stderr' in error) {
|
|
31
|
+
message = String(error.stderr);
|
|
32
|
+
}
|
|
33
|
+
else if (error instanceof Error) {
|
|
34
|
+
message = error.message;
|
|
35
|
+
}
|
|
36
|
+
setErrorMessage(message);
|
|
37
|
+
setStatus('error');
|
|
38
|
+
setTimeout(() => process.exit(1), 2000);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
cloneRepo();
|
|
42
|
+
}, []);
|
|
43
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
44
|
+
status === 'checking' && React.createElement(Text, { color: "yellow" }, "Checking..."),
|
|
45
|
+
status === 'cloning' && React.createElement(Text, { color: "yellow" }, "Cloning POC-Generator repository..."),
|
|
46
|
+
status === 'success' && React.createElement(Text, { color: "green" }, "POC-Generator cloned successfully!"),
|
|
47
|
+
status === 'exists' && React.createElement(Text, { color: "cyan" }, "POC-Generator folder already exists. Skipping clone."),
|
|
48
|
+
status === 'error' && React.createElement(Text, { color: "red" },
|
|
49
|
+
"Error: ",
|
|
50
|
+
errorMessage)));
|
|
51
|
+
};
|
|
52
|
+
export default () => {
|
|
53
|
+
render(React.createElement(StartPocCommand, null));
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=start-poc.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { syncDocs } from '../utils/docs.js';
|
|
2
|
+
export default async () => {
|
|
3
|
+
console.log('Syncing documentation and commands...');
|
|
4
|
+
const result = await syncDocs();
|
|
5
|
+
if (result.success) {
|
|
6
|
+
const docFiles = result.syncedFiles.filter(f => f.startsWith('docs/') || f === 'CLAUDE.md');
|
|
7
|
+
const commandFiles = result.syncedFiles.filter(f => f.startsWith('.claude/commands/'));
|
|
8
|
+
console.log(`\x1b[32m✓ Synced ${result.syncedFiles.length} file(s)\x1b[0m`);
|
|
9
|
+
if (docFiles.length > 0) {
|
|
10
|
+
console.log(`\n\x1b[36mDocumentation (${docFiles.length}):\x1b[0m`);
|
|
11
|
+
for (const file of docFiles) {
|
|
12
|
+
console.log(` - ${file}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (commandFiles.length > 0) {
|
|
16
|
+
console.log(`\n\x1b[36mSlash Commands (${commandFiles.length}):\x1b[0m`);
|
|
17
|
+
for (const file of commandFiles) {
|
|
18
|
+
console.log(` - ${file}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log(`\x1b[31m✗ Failed to sync: ${result.error}\x1b[0m`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=sync-docs.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface TranslateCommandOptions {
|
|
2
|
+
lang?: string;
|
|
3
|
+
langId?: string;
|
|
4
|
+
source?: string;
|
|
5
|
+
sourceId?: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
file?: string;
|
|
8
|
+
format?: 'json' | 'table';
|
|
9
|
+
limit?: string;
|
|
10
|
+
}
|
|
11
|
+
export default function translateCommand(operation: string, options: TranslateCommandOptions): Promise<void>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=translate.d.ts.map
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { loadAuthConfig } from '../utils/auth.js';
|
|
3
|
+
import { ButterflyAPI } from '../utils/api.js';
|
|
4
|
+
async function getUntranslated(api, options) {
|
|
5
|
+
const languageId = await resolveLanguageId(api, options);
|
|
6
|
+
if (!languageId) {
|
|
7
|
+
console.error('ERROR: Language not found. Use --lang (iso_code) or --lang-id');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const limit = options.limit ? parseInt(options.limit, 10) : undefined;
|
|
11
|
+
const code = `
|
|
12
|
+
{% set existing = db().from('bfy_translations').where('bfy_language_id', ${languageId}).column('bfy_translation_text_id') %}
|
|
13
|
+
{% set untranslated = db().from('bfy_translation_texts').whereNotIn('id', existing)${limit ? `.limit(${limit})` : ''}.get() %}
|
|
14
|
+
{{ untranslated|json_encode }}
|
|
15
|
+
`.trim();
|
|
16
|
+
const result = await api.executeCode(code);
|
|
17
|
+
if (!result.success) {
|
|
18
|
+
console.error(`ERROR: ${result.error || 'Failed to fetch untranslated texts'}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
let untranslated = [];
|
|
22
|
+
try {
|
|
23
|
+
untranslated = JSON.parse(result.output || '[]');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
console.error('ERROR: Failed to parse response');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (untranslated.length === 0) {
|
|
30
|
+
console.log('All texts are translated for this language.');
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
if (options.format === 'table') {
|
|
34
|
+
console.log('\nUntranslated texts:\n');
|
|
35
|
+
console.log('ID\t| Source');
|
|
36
|
+
console.log('-'.repeat(60));
|
|
37
|
+
for (const text of untranslated) {
|
|
38
|
+
const truncated = text.source.length > 50 ? text.source.substring(0, 47) + '...' : text.source;
|
|
39
|
+
console.log(`${text.id}\t| ${truncated}`);
|
|
40
|
+
}
|
|
41
|
+
console.log(`\nTotal: ${untranslated.length} untranslated texts`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log(JSON.stringify(untranslated, null, 2));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function addTranslation(api, options) {
|
|
48
|
+
if (!options.text) {
|
|
49
|
+
console.error('ERROR: --text is required for add operation');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const languageId = await resolveLanguageId(api, options);
|
|
53
|
+
if (!languageId) {
|
|
54
|
+
console.error('ERROR: Language not found. Use --lang (iso_code) or --lang-id');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const sourceTextId = await resolveSourceTextId(api, options);
|
|
58
|
+
if (!sourceTextId) {
|
|
59
|
+
console.error('ERROR: Source text not found. Use --source (text) or --source-id');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const checkCode = `
|
|
63
|
+
{% set t = db().from('bfy_translations')
|
|
64
|
+
.where('bfy_language_id', ${languageId})
|
|
65
|
+
.where('bfy_translation_text_id', ${sourceTextId})
|
|
66
|
+
.first() %}
|
|
67
|
+
{{ t ? t.id : 'null' }}
|
|
68
|
+
`.trim();
|
|
69
|
+
const checkResult = await api.executeCode(checkCode);
|
|
70
|
+
const existingId = checkResult.output && checkResult.output !== 'null'
|
|
71
|
+
? parseInt(checkResult.output, 10)
|
|
72
|
+
: null;
|
|
73
|
+
if (existingId) {
|
|
74
|
+
const result = await api.performOperation('bfy_translations', 'edit', {
|
|
75
|
+
id: existingId,
|
|
76
|
+
text: options.text
|
|
77
|
+
});
|
|
78
|
+
if (result.success) {
|
|
79
|
+
console.log(`SUCCESS: Translation updated (ID: ${existingId})`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.error(`ERROR: ${result.error || result.message || 'Failed to update translation'}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const result = await api.performOperation('bfy_translations', 'add', {
|
|
88
|
+
bfy_language_id: languageId,
|
|
89
|
+
bfy_translation_text_id: sourceTextId,
|
|
90
|
+
text: options.text
|
|
91
|
+
});
|
|
92
|
+
if (result.success) {
|
|
93
|
+
console.log(`SUCCESS: Translation added (ID: ${result.id})`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.error(`ERROR: ${result.error || result.message || 'Failed to add translation'}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function parseCSVLine(line) {
|
|
102
|
+
const fields = [];
|
|
103
|
+
let current = '';
|
|
104
|
+
let inQuotes = false;
|
|
105
|
+
for (let i = 0; i < line.length; i++) {
|
|
106
|
+
const char = line[i];
|
|
107
|
+
const nextChar = line[i + 1];
|
|
108
|
+
if (char === '"' && !inQuotes) {
|
|
109
|
+
inQuotes = true;
|
|
110
|
+
}
|
|
111
|
+
else if (char === '"' && inQuotes) {
|
|
112
|
+
if (nextChar === '"') {
|
|
113
|
+
current += '"';
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
inQuotes = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (char === '\t' && !inQuotes) {
|
|
121
|
+
fields.push(current);
|
|
122
|
+
current = '';
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
current += char;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
fields.push(current);
|
|
129
|
+
return fields;
|
|
130
|
+
}
|
|
131
|
+
async function bulkTranslate(api, options) {
|
|
132
|
+
if (!options.file) {
|
|
133
|
+
console.error('ERROR: --file is required for bulk operation');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
const languageId = await resolveLanguageId(api, options);
|
|
137
|
+
if (!languageId) {
|
|
138
|
+
console.error('ERROR: Language not found. Use --lang (iso_code) or --lang-id');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
let translations;
|
|
142
|
+
try {
|
|
143
|
+
const fileContent = await fs.readFile(options.file, 'utf-8');
|
|
144
|
+
const lines = fileContent.split('\n').filter(line => line.trim() !== '');
|
|
145
|
+
if (lines.length === 0) {
|
|
146
|
+
console.error('ERROR: File is empty');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
const firstLine = lines[0].toLowerCase();
|
|
150
|
+
const startIndex = (firstLine.includes('source') && firstLine.includes('text')) ? 1 : 0;
|
|
151
|
+
translations = [];
|
|
152
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
153
|
+
const fields = parseCSVLine(lines[i]);
|
|
154
|
+
if (fields.length < 2) {
|
|
155
|
+
console.error(`WARNING: Skipping line ${i + 1} - expected 2 columns (source, text), got ${fields.length}`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const source = fields[0].trim();
|
|
159
|
+
const text = fields[1].trim();
|
|
160
|
+
if (source && text) {
|
|
161
|
+
translations.push({ source, text });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error(`ERROR: Failed to read/parse file: ${error.message}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
if (translations.length === 0) {
|
|
170
|
+
console.error('ERROR: No valid translations found in file');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
console.log(`Processing ${translations.length} translations...`);
|
|
174
|
+
const sourcesCode = `
|
|
175
|
+
{% set texts = db().from('bfy_translation_texts').get() %}
|
|
176
|
+
{% set map = {} %}
|
|
177
|
+
{% for t in texts %}
|
|
178
|
+
{% set map = map|merge({('s_' ~ t.id): t.source}) %}
|
|
179
|
+
{% endfor %}
|
|
180
|
+
{{ map|json_encode }}
|
|
181
|
+
`.trim();
|
|
182
|
+
const sourcesResult = await api.executeCode(sourcesCode);
|
|
183
|
+
let sourceTextMap = {};
|
|
184
|
+
try {
|
|
185
|
+
sourceTextMap = JSON.parse(sourcesResult.output || '{}');
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
console.error('ERROR: Failed to fetch source texts');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
const sourceToId = {};
|
|
192
|
+
for (const [key, source] of Object.entries(sourceTextMap)) {
|
|
193
|
+
const id = parseInt(key.replace('s_', ''), 10);
|
|
194
|
+
sourceToId[source] = id;
|
|
195
|
+
}
|
|
196
|
+
const existingCode = `
|
|
197
|
+
{% set t = db().from('bfy_translations').where('bfy_language_id', ${languageId}).get() %}
|
|
198
|
+
{% set map = {} %}
|
|
199
|
+
{% for tr in t %}
|
|
200
|
+
{% set map = map|merge({('t_' ~ tr.bfy_translation_text_id): tr.id}) %}
|
|
201
|
+
{% endfor %}
|
|
202
|
+
{{ map|json_encode }}
|
|
203
|
+
`.trim();
|
|
204
|
+
const existingResult = await api.executeCode(existingCode);
|
|
205
|
+
let existingMap = {};
|
|
206
|
+
try {
|
|
207
|
+
existingMap = JSON.parse(existingResult.output || '{}');
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
console.error('ERROR: Failed to fetch existing translations');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
let added = 0;
|
|
214
|
+
let updated = 0;
|
|
215
|
+
let skipped = 0;
|
|
216
|
+
let errors = 0;
|
|
217
|
+
const missingSourceTexts = [];
|
|
218
|
+
for (const entry of translations) {
|
|
219
|
+
let sourceTextId;
|
|
220
|
+
if (entry.sourceId) {
|
|
221
|
+
sourceTextId = entry.sourceId;
|
|
222
|
+
}
|
|
223
|
+
else if (entry.source) {
|
|
224
|
+
sourceTextId = sourceToId[entry.source];
|
|
225
|
+
}
|
|
226
|
+
if (!sourceTextId) {
|
|
227
|
+
if (entry.source) {
|
|
228
|
+
const createResult = await api.performOperation('bfy_translation_texts', 'add', {
|
|
229
|
+
source: entry.source
|
|
230
|
+
});
|
|
231
|
+
if (createResult.success && createResult.id) {
|
|
232
|
+
sourceTextId = createResult.id;
|
|
233
|
+
sourceToId[entry.source] = sourceTextId;
|
|
234
|
+
console.log(` Created source text: "${entry.source.substring(0, 30)}..." (ID: ${sourceTextId})`);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
missingSourceTexts.push(entry.source);
|
|
238
|
+
errors++;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
skipped++;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const existingTranslationId = existingMap[`t_${sourceTextId}`];
|
|
248
|
+
if (existingTranslationId) {
|
|
249
|
+
const result = await api.performOperation('bfy_translations', 'edit', {
|
|
250
|
+
id: existingTranslationId,
|
|
251
|
+
text: entry.text
|
|
252
|
+
});
|
|
253
|
+
if (result.success) {
|
|
254
|
+
updated++;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
errors++;
|
|
258
|
+
console.error(` ERROR updating translation for source ID ${sourceTextId}: ${result.error || result.message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
const result = await api.performOperation('bfy_translations', 'add', {
|
|
263
|
+
bfy_language_id: languageId,
|
|
264
|
+
bfy_translation_text_id: sourceTextId,
|
|
265
|
+
text: entry.text
|
|
266
|
+
});
|
|
267
|
+
if (result.success) {
|
|
268
|
+
added++;
|
|
269
|
+
existingMap[`t_${sourceTextId}`] = result.id;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
errors++;
|
|
273
|
+
console.error(` ERROR adding translation for source ID ${sourceTextId}: ${result.error || result.message}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
console.log('\n--- Summary ---');
|
|
278
|
+
console.log(`Added: ${added}`);
|
|
279
|
+
console.log(`Updated: ${updated}`);
|
|
280
|
+
console.log(`Skipped: ${skipped}`);
|
|
281
|
+
console.log(`Errors: ${errors}`);
|
|
282
|
+
if (missingSourceTexts.length > 0) {
|
|
283
|
+
console.log(`\nMissing source texts that couldn't be created:`);
|
|
284
|
+
for (const text of missingSourceTexts.slice(0, 10)) {
|
|
285
|
+
console.log(` - ${text.substring(0, 50)}...`);
|
|
286
|
+
}
|
|
287
|
+
if (missingSourceTexts.length > 10) {
|
|
288
|
+
console.log(` ... and ${missingSourceTexts.length - 10} more`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
process.exit(errors > 0 ? 1 : 0);
|
|
292
|
+
}
|
|
293
|
+
async function listLanguages(api, options) {
|
|
294
|
+
const code = `
|
|
295
|
+
{% set langs = db().from('bfy_languages').get() %}
|
|
296
|
+
{% set result = [] %}
|
|
297
|
+
{% for l in langs %}
|
|
298
|
+
{% set total = db().from('bfy_translation_texts').count() %}
|
|
299
|
+
{% set translated = db().from('bfy_translations').where('bfy_language_id', l.id).count() %}
|
|
300
|
+
{% set result = result|merge([{
|
|
301
|
+
"id": l.id,
|
|
302
|
+
"name": l.name,
|
|
303
|
+
"iso_code": l.iso_code,
|
|
304
|
+
"total": total,
|
|
305
|
+
"translated": translated,
|
|
306
|
+
"untranslated": total - translated
|
|
307
|
+
}]) %}
|
|
308
|
+
{% endfor %}
|
|
309
|
+
{{ result|json_encode }}
|
|
310
|
+
`.trim();
|
|
311
|
+
const result = await api.executeCode(code);
|
|
312
|
+
if (!result.success) {
|
|
313
|
+
console.error(`ERROR: ${result.error || 'Failed to fetch languages'}`);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
let languages = [];
|
|
317
|
+
try {
|
|
318
|
+
languages = JSON.parse(result.output || '[]');
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
console.error('ERROR: Failed to parse response');
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
if (options.format === 'table' || !options.format) {
|
|
325
|
+
console.log('\nAvailable languages:\n');
|
|
326
|
+
console.log('ID\t| ISO\t| Name\t\t| Translated\t| Untranslated');
|
|
327
|
+
console.log('-'.repeat(70));
|
|
328
|
+
for (const lang of languages) {
|
|
329
|
+
const name = lang.name.padEnd(12);
|
|
330
|
+
console.log(`${lang.id}\t| ${lang.iso_code}\t| ${name}\t| ${lang.translated}/${lang.total}\t\t| ${lang.untranslated}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
console.log(JSON.stringify(languages, null, 2));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async function resolveLanguageId(api, options) {
|
|
338
|
+
if (options.langId) {
|
|
339
|
+
return parseInt(options.langId, 10);
|
|
340
|
+
}
|
|
341
|
+
if (options.lang) {
|
|
342
|
+
const code = `{% set l = db().from('bfy_languages').where('iso_code', '${options.lang}').first() %}{{ l ? l.id : 'null' }}`;
|
|
343
|
+
const result = await api.executeCode(code);
|
|
344
|
+
if (result.success && result.output && result.output !== 'null') {
|
|
345
|
+
return parseInt(result.output, 10);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
async function resolveSourceTextId(api, options) {
|
|
351
|
+
if (options.sourceId) {
|
|
352
|
+
return parseInt(options.sourceId, 10);
|
|
353
|
+
}
|
|
354
|
+
if (options.source) {
|
|
355
|
+
const escapedSource = options.source.replace(/'/g, "\\'");
|
|
356
|
+
const code = `{% set t = db().from('bfy_translation_texts').where('source', '${escapedSource}').first() %}{{ t ? t.id : 'null' }}`;
|
|
357
|
+
const result = await api.executeCode(code);
|
|
358
|
+
if (result.success && result.output && result.output !== 'null') {
|
|
359
|
+
return parseInt(result.output, 10);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
export default async function translateCommand(operation, options) {
|
|
365
|
+
try {
|
|
366
|
+
const validOperations = ['get-untranslated', 'add', 'bulk', 'languages'];
|
|
367
|
+
if (!validOperations.includes(operation)) {
|
|
368
|
+
console.error(`ERROR: Invalid operation: ${operation}`);
|
|
369
|
+
console.error(`Valid operations: ${validOperations.join(', ')}`);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
const config = await loadAuthConfig();
|
|
373
|
+
if (!config) {
|
|
374
|
+
console.error('ERROR: No authentication configured. Run "butterfly-cli setup" first.');
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
const api = new ButterflyAPI(config);
|
|
378
|
+
await api.authenticate();
|
|
379
|
+
if (api.needs2FA)
|
|
380
|
+
await api.complete2FA();
|
|
381
|
+
switch (operation) {
|
|
382
|
+
case 'get-untranslated':
|
|
383
|
+
await getUntranslated(api, options);
|
|
384
|
+
break;
|
|
385
|
+
case 'add':
|
|
386
|
+
await addTranslation(api, options);
|
|
387
|
+
break;
|
|
388
|
+
case 'bulk':
|
|
389
|
+
await bulkTranslate(api, options);
|
|
390
|
+
break;
|
|
391
|
+
case 'languages':
|
|
392
|
+
await listLanguages(api, options);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
console.error(`ERROR: ${error.message}`);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
//# sourceMappingURL=translate.js.map
|