@tolgee/cli 2.14.0 → 2.15.0-rc.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/dist/client/ExportClient.js +18 -1
- package/dist/client/TolgeeClient.js +7 -2
- package/dist/client/WebsocketClient.js +120 -0
- package/dist/commands/login.js +4 -2
- package/dist/commands/pull.js +78 -36
- package/dist/config/credentials.js +8 -7
- package/dist/utils/eTagStorage.js +15 -0
- package/dist/utils/isVersionAtLeast.js +29 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/pullWatch/AuthErrorHandler.js +71 -0
- package/dist/utils/pullWatch/watchHandler.js +149 -0
- package/package.json +10 -2
|
@@ -7,14 +7,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
11
|
+
var t = {};
|
|
12
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
13
|
+
t[p] = s[p];
|
|
14
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
15
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
16
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
17
|
+
t[p[i]] = s[p[i]];
|
|
18
|
+
}
|
|
19
|
+
return t;
|
|
20
|
+
};
|
|
10
21
|
export const createExportClient = ({ apiClient }) => {
|
|
11
22
|
return {
|
|
12
23
|
export(req) {
|
|
13
24
|
return __awaiter(this, void 0, void 0, function* () {
|
|
14
|
-
const
|
|
25
|
+
const { ifNoneMatch } = req, exportReq = __rest(req, ["ifNoneMatch"]);
|
|
26
|
+
const body = Object.assign(Object.assign({}, exportReq), { zip: true });
|
|
27
|
+
const headers = {};
|
|
28
|
+
if (ifNoneMatch) {
|
|
29
|
+
headers['If-None-Match'] = ifNoneMatch;
|
|
30
|
+
}
|
|
15
31
|
const loadable = yield apiClient.POST('/v2/projects/{projectId}/export', {
|
|
16
32
|
params: { path: { projectId: apiClient.getProjectId() } },
|
|
17
33
|
body: body,
|
|
34
|
+
headers,
|
|
18
35
|
parseAs: 'blob',
|
|
19
36
|
});
|
|
20
37
|
return Object.assign(Object.assign({}, loadable), { data: loadable.data });
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { exitWithError } from './../utils/logger.js';
|
|
2
1
|
import { createApiClient } from './ApiClient.js';
|
|
3
2
|
import { createExportClient } from './ExportClient.js';
|
|
4
3
|
import { createImportClient } from './ImportClient.js';
|
|
@@ -9,6 +8,12 @@ export function createTolgeeClient(props) {
|
|
|
9
8
|
}
|
|
10
9
|
export const handleLoadableError = (loadable) => {
|
|
11
10
|
if (loadable.error) {
|
|
12
|
-
|
|
11
|
+
throw new LoadableError(loadable);
|
|
13
12
|
}
|
|
14
13
|
};
|
|
14
|
+
export class LoadableError extends Error {
|
|
15
|
+
constructor(loadable) {
|
|
16
|
+
super(errorFromLoadable(loadable));
|
|
17
|
+
this.loadable = loadable;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Stomp } from '@stomp/stompjs';
|
|
2
|
+
import SockJS from 'sockjs-client';
|
|
3
|
+
import { debug, error } from '../utils/logger.js';
|
|
4
|
+
export const WebsocketClient = (options) => {
|
|
5
|
+
let client;
|
|
6
|
+
let deactivated = false;
|
|
7
|
+
let connected = false;
|
|
8
|
+
let connecting = false;
|
|
9
|
+
let subscriptions = [];
|
|
10
|
+
const resubscribe = () => {
|
|
11
|
+
if (deactivated) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (client) {
|
|
15
|
+
subscriptions.forEach((subscription) => {
|
|
16
|
+
subscribeToStompChannel(subscription);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const subscribeToStompChannel = (subscription) => {
|
|
21
|
+
if (connected && client) {
|
|
22
|
+
debug(`Subscribing to ${subscription.channel}`);
|
|
23
|
+
const stompSubscription = client.subscribe(subscription.channel, function (message) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(message.body);
|
|
26
|
+
subscription.callback(parsed);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
error(`Error parsing message: ${e.message}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
subscription.unsubscribe = stompSubscription.unsubscribe;
|
|
33
|
+
subscription.id = stompSubscription.id;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function initClient() {
|
|
37
|
+
client = Stomp.over(() => new SockJS(`${options.serverUrl}/websocket`));
|
|
38
|
+
client.configure({
|
|
39
|
+
reconnectDelay: 3000,
|
|
40
|
+
debug: (msg) => {
|
|
41
|
+
debug(msg);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function connectIfNotAlready() {
|
|
46
|
+
if (deactivated || connected || connecting) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
connecting = true;
|
|
50
|
+
const client = getClient();
|
|
51
|
+
const onConnected = function (message) {
|
|
52
|
+
var _a;
|
|
53
|
+
connected = true;
|
|
54
|
+
connecting = false;
|
|
55
|
+
resubscribe();
|
|
56
|
+
(_a = options.onConnected) === null || _a === void 0 ? void 0 : _a.call(options, message);
|
|
57
|
+
};
|
|
58
|
+
const onDisconnect = function () {
|
|
59
|
+
var _a;
|
|
60
|
+
connected = false;
|
|
61
|
+
connecting = false;
|
|
62
|
+
(_a = options.onConnectionClose) === null || _a === void 0 ? void 0 : _a.call(options);
|
|
63
|
+
};
|
|
64
|
+
const onError = (error) => {
|
|
65
|
+
var _a;
|
|
66
|
+
connecting = false;
|
|
67
|
+
(_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
|
|
68
|
+
};
|
|
69
|
+
client.connect(getAuthentication(options), onConnected, onError, onDisconnect);
|
|
70
|
+
}
|
|
71
|
+
const getClient = () => {
|
|
72
|
+
if (client !== undefined) {
|
|
73
|
+
return client;
|
|
74
|
+
}
|
|
75
|
+
initClient();
|
|
76
|
+
return client;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Subscribes to channel
|
|
80
|
+
* @param channel Channel URI
|
|
81
|
+
* @param callback Callback function to be executed when event is triggered
|
|
82
|
+
* @return Function Function unsubscribing the event listening
|
|
83
|
+
*/
|
|
84
|
+
function subscribe(channel, callback) {
|
|
85
|
+
if (deactivated) {
|
|
86
|
+
return () => { };
|
|
87
|
+
}
|
|
88
|
+
connectIfNotAlready();
|
|
89
|
+
const subscription = { channel, callback };
|
|
90
|
+
subscriptions.push(subscription);
|
|
91
|
+
subscribeToStompChannel(subscription);
|
|
92
|
+
return () => {
|
|
93
|
+
var _a;
|
|
94
|
+
(_a = subscription.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(subscription);
|
|
95
|
+
removeSubscription(subscription);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function disconnect() {
|
|
99
|
+
if (client) {
|
|
100
|
+
client.disconnect();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function deactivate() {
|
|
104
|
+
deactivated = true;
|
|
105
|
+
disconnect();
|
|
106
|
+
}
|
|
107
|
+
function removeSubscription(subscription) {
|
|
108
|
+
subscriptions = subscriptions.filter((it) => it !== subscription);
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze({ subscribe, deactivate, connectIfNotAlready });
|
|
111
|
+
};
|
|
112
|
+
function getAuthentication(options) {
|
|
113
|
+
if (options.authentication.jwtToken) {
|
|
114
|
+
return { jwtToken: options.authentication.jwtToken };
|
|
115
|
+
}
|
|
116
|
+
if (options.authentication.apiKey) {
|
|
117
|
+
return { 'x-api-key': options.authentication.apiKey };
|
|
118
|
+
}
|
|
119
|
+
return {};
|
|
120
|
+
}
|
package/dist/commands/login.js
CHANGED
|
@@ -9,10 +9,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import ansi from 'ansi-colors';
|
|
12
|
-
import {
|
|
13
|
-
import { exitWithError, success } from '../utils/logger.js';
|
|
12
|
+
import { clearAuthStore, removeApiKeys, saveApiKey, } from '../config/credentials.js';
|
|
13
|
+
import { debug, exitWithError, success } from '../utils/logger.js';
|
|
14
14
|
import { createTolgeeClient } from '../client/TolgeeClient.js';
|
|
15
15
|
import { printApiKeyLists } from '../utils/apiKeyList.js';
|
|
16
|
+
import { getStackTrace } from '../utils/getStackTrace.js';
|
|
16
17
|
function loginHandler(key) {
|
|
17
18
|
return __awaiter(this, void 0, void 0, function* () {
|
|
18
19
|
const opts = this.optsWithGlobals();
|
|
@@ -23,6 +24,7 @@ function loginHandler(key) {
|
|
|
23
24
|
else if (!key) {
|
|
24
25
|
exitWithError('Missing argument [API Key]');
|
|
25
26
|
}
|
|
27
|
+
debug(`Logging in with API key ${key === null || key === void 0 ? void 0 : key.slice(0, 5)}...${key === null || key === void 0 ? void 0 : key.slice(-4)}.\n${getStackTrace()}`);
|
|
26
28
|
const keyInfo = yield createTolgeeClient({
|
|
27
29
|
baseUrl: opts.apiUrl.toString(),
|
|
28
30
|
apiKey: key,
|
package/dist/commands/pull.js
CHANGED
|
@@ -10,45 +10,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { Command, Option } from 'commander';
|
|
11
11
|
import { unzipBuffer } from '../utils/zip.js';
|
|
12
12
|
import { prepareDir } from '../utils/prepareDir.js';
|
|
13
|
-
import { exitWithError, loading, success } from '../utils/logger.js';
|
|
13
|
+
import { exitWithError, info, loading, success } from '../utils/logger.js';
|
|
14
14
|
import { checkPathNotAFile } from '../utils/checkPathNotAFile.js';
|
|
15
15
|
import { mapExportFormat } from '../utils/mapExportFormat.js';
|
|
16
16
|
import { handleLoadableError } from '../client/TolgeeClient.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
var _a;
|
|
20
|
-
const exportFormat = mapExportFormat(opts.format);
|
|
21
|
-
const { format, messageFormat } = exportFormat;
|
|
22
|
-
const loadable = yield opts.client.export.export({
|
|
23
|
-
format,
|
|
24
|
-
messageFormat,
|
|
25
|
-
supportArrays: opts.supportArrays,
|
|
26
|
-
languages: opts.languages,
|
|
27
|
-
filterState: opts.states,
|
|
28
|
-
structureDelimiter: (_a = opts.delimiter) !== null && _a !== void 0 ? _a : '',
|
|
29
|
-
filterNamespace: opts.namespaces,
|
|
30
|
-
filterTagIn: opts.tags,
|
|
31
|
-
filterTagNotIn: opts.excludeTags,
|
|
32
|
-
fileStructureTemplate: opts.fileStructureTemplate,
|
|
33
|
-
escapeHtml: false,
|
|
34
|
-
});
|
|
35
|
-
handleLoadableError(loadable);
|
|
36
|
-
return loadable.data;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
const pullHandler = () => function () {
|
|
40
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
-
const opts = this.optsWithGlobals();
|
|
42
|
-
if (!opts.path) {
|
|
43
|
-
exitWithError('Missing option --path <path> or `pull.path` in tolgee config');
|
|
44
|
-
}
|
|
45
|
-
yield checkPathNotAFile(opts.path);
|
|
46
|
-
const zipBlob = yield loading('Fetching strings from Tolgee...', fetchZipBlob(opts));
|
|
47
|
-
yield prepareDir(opts.path, opts.emptyDir);
|
|
48
|
-
yield loading('Extracting strings...', unzipBuffer(zipBlob, opts.path));
|
|
49
|
-
success('Done!');
|
|
50
|
-
});
|
|
51
|
-
};
|
|
17
|
+
import { startWatching } from '../utils/pullWatch/watchHandler.js';
|
|
18
|
+
import { getETag } from '../utils/eTagStorage.js';
|
|
52
19
|
export default (config) => {
|
|
53
20
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
54
21
|
return new Command()
|
|
@@ -67,5 +34,80 @@ export default (config) => {
|
|
|
67
34
|
.addOption(new Option('--support-arrays', 'Export keys with array syntax (e.g. item[0]) as arrays.').default((_j = (_h = config.pull) === null || _h === void 0 ? void 0 : _h.supportArrays) !== null && _j !== void 0 ? _j : false))
|
|
68
35
|
.addOption(new Option('--empty-dir', 'Empty target directory before inserting pulled files.').default((_k = config.pull) === null || _k === void 0 ? void 0 : _k.emptyDir))
|
|
69
36
|
.addOption(new Option('--file-structure-template <template>', 'Defines exported file structure: https://tolgee.io/tolgee-cli/push-pull-strings#file-structure-template-format').default((_l = config.pull) === null || _l === void 0 ? void 0 : _l.fileStructureTemplate))
|
|
37
|
+
.addOption(new Option('--watch', 'Watch for changes and re-pull automatically').default(false))
|
|
70
38
|
.action(pullHandler());
|
|
71
39
|
};
|
|
40
|
+
const pullHandler = () => function () {
|
|
41
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
42
|
+
const opts = this.optsWithGlobals();
|
|
43
|
+
if (!opts.path) {
|
|
44
|
+
exitWithError('Missing option --path <path> or `pull.path` in tolgee config');
|
|
45
|
+
}
|
|
46
|
+
yield checkPathNotAFile(opts.path);
|
|
47
|
+
if (!opts.watch) {
|
|
48
|
+
yield doPull(opts);
|
|
49
|
+
success('Done!');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Start watching for changes
|
|
53
|
+
yield startWatching({
|
|
54
|
+
apiUrl: opts.apiUrl,
|
|
55
|
+
apiKey: opts.apiKey,
|
|
56
|
+
projectId: opts.projectId,
|
|
57
|
+
client: opts.client,
|
|
58
|
+
doPull: () => __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
yield doPull(opts);
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const doPull = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
const result = yield loading('Fetching strings from Tolgee...', fetchZipBlob(opts, getETag(opts.projectId)));
|
|
66
|
+
if (result.notModified) {
|
|
67
|
+
info('Exported data not changed.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
yield prepareDir(opts.path, opts.emptyDir);
|
|
71
|
+
yield loading('Extracting strings...', unzipBuffer(result.data, opts.path));
|
|
72
|
+
// Store ETag after a successful pull
|
|
73
|
+
if (result.etag) {
|
|
74
|
+
const { setETag } = yield import('../utils/eTagStorage.js');
|
|
75
|
+
setETag(opts.projectId, result.etag);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
function fetchZipBlob(opts, ifNoneMatch) {
|
|
79
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
var _a, _b, _c;
|
|
81
|
+
const exportFormat = mapExportFormat(opts.format);
|
|
82
|
+
const { format, messageFormat } = exportFormat;
|
|
83
|
+
const loadable = yield opts.client.export.export({
|
|
84
|
+
format,
|
|
85
|
+
messageFormat,
|
|
86
|
+
supportArrays: opts.supportArrays,
|
|
87
|
+
languages: opts.languages,
|
|
88
|
+
filterState: opts.states,
|
|
89
|
+
structureDelimiter: (_a = opts.delimiter) !== null && _a !== void 0 ? _a : '',
|
|
90
|
+
filterNamespace: opts.namespaces,
|
|
91
|
+
filterTagIn: opts.tags,
|
|
92
|
+
filterTagNotIn: opts.excludeTags,
|
|
93
|
+
fileStructureTemplate: opts.fileStructureTemplate,
|
|
94
|
+
escapeHtml: false,
|
|
95
|
+
ifNoneMatch,
|
|
96
|
+
});
|
|
97
|
+
handleLoadableError(loadable);
|
|
98
|
+
const etag = loadable.response
|
|
99
|
+
? extractETagFromResponse(loadable.response)
|
|
100
|
+
: undefined;
|
|
101
|
+
return {
|
|
102
|
+
data: loadable.data,
|
|
103
|
+
etag,
|
|
104
|
+
// 412 is not modified for POST request
|
|
105
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/412
|
|
106
|
+
// 304 is not modified for GET request
|
|
107
|
+
notModified: [412, 304].includes((_c = (_b = loadable.response) === null || _b === void 0 ? void 0 : _b.status) !== null && _c !== void 0 ? _c : 0),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function extractETagFromResponse(response) {
|
|
112
|
+
return response.headers.get('ETag') || undefined;
|
|
113
|
+
}
|
|
@@ -79,19 +79,20 @@ export function savePak(instance, project, pak) {
|
|
|
79
79
|
return storePak(store, instance, project, pak);
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
|
-
export function getApiKey(
|
|
82
|
+
export function getApiKey(apiUrl, projectId) {
|
|
83
83
|
return __awaiter(this, void 0, void 0, function* () {
|
|
84
84
|
var _a;
|
|
85
85
|
const store = yield loadStore();
|
|
86
|
-
|
|
86
|
+
const apiUrlObj = new URL(apiUrl);
|
|
87
|
+
if (!store[apiUrlObj.hostname]) {
|
|
87
88
|
return null;
|
|
88
89
|
}
|
|
89
|
-
const scopedStore = store[
|
|
90
|
+
const scopedStore = store[apiUrlObj.hostname];
|
|
90
91
|
if (scopedStore.user) {
|
|
91
92
|
if (scopedStore.user.expires !== 0 &&
|
|
92
93
|
Date.now() > scopedStore.user.expires) {
|
|
93
|
-
warn(`Your personal access token for ${
|
|
94
|
-
yield storePat(store,
|
|
94
|
+
warn(`Your personal access token for ${apiUrlObj.hostname} expired.`);
|
|
95
|
+
yield storePat(store, apiUrlObj, undefined);
|
|
95
96
|
return null;
|
|
96
97
|
}
|
|
97
98
|
return scopedStore.user.token;
|
|
@@ -102,8 +103,8 @@ export function getApiKey(instance, projectId) {
|
|
|
102
103
|
const pak = (_a = scopedStore.projects) === null || _a === void 0 ? void 0 : _a[projectId.toString(10)];
|
|
103
104
|
if (pak) {
|
|
104
105
|
if (pak.expires !== 0 && Date.now() > pak.expires) {
|
|
105
|
-
warn(`Your project API key for project #${projectId} on ${
|
|
106
|
-
yield removePak(store,
|
|
106
|
+
warn(`Your project API key for project #${projectId} on ${apiUrlObj.hostname} expired.`);
|
|
107
|
+
yield removePak(store, apiUrlObj, projectId);
|
|
107
108
|
return null;
|
|
108
109
|
}
|
|
109
110
|
return pak.token;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// In-memory storage for ETag data
|
|
2
|
+
// In the future we can use filesystem to store it
|
|
3
|
+
const etagStorage = new Map();
|
|
4
|
+
export function getETag(projectId) {
|
|
5
|
+
const data = etagStorage.get(projectId);
|
|
6
|
+
if (!data) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
return data.etag;
|
|
10
|
+
}
|
|
11
|
+
export function setETag(projectId, etag) {
|
|
12
|
+
etagStorage.set(projectId, {
|
|
13
|
+
etag,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { debug } from './logger.js';
|
|
2
|
+
/**
|
|
3
|
+
Compares two semantic versions (e.g., "3.143.0" vs "3.142.0")
|
|
4
|
+
Returns true if version1 >= version2
|
|
5
|
+
*/
|
|
6
|
+
export function isVersionAtLeast(required, current) {
|
|
7
|
+
debug(`Checking that server version ${current} is at least ${required}`);
|
|
8
|
+
if (current === '??') {
|
|
9
|
+
// local build, lets assume it's supported
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
// Strip optional 'v' prefix from both versions
|
|
13
|
+
const cleanRequired = required.startsWith('v') ? required.slice(1) : required;
|
|
14
|
+
const cleanServerVersion = current.startsWith('v')
|
|
15
|
+
? current.slice(1)
|
|
16
|
+
: current;
|
|
17
|
+
const requiredParts = cleanRequired.split('.').map(Number);
|
|
18
|
+
const currentParts = cleanServerVersion.split('.').map(Number);
|
|
19
|
+
const maxLength = Math.max(requiredParts.length, currentParts.length);
|
|
20
|
+
for (let i = 0; i < maxLength; i++) {
|
|
21
|
+
const requiredPart = requiredParts[i] || 0;
|
|
22
|
+
const currentPart = currentParts[i] || 0;
|
|
23
|
+
if (currentPart > requiredPart)
|
|
24
|
+
return true;
|
|
25
|
+
if (currentPart < requiredPart)
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
package/dist/utils/logger.js
CHANGED
|
@@ -113,7 +113,7 @@ export function exitWithError(err) {
|
|
|
113
113
|
export function loading(comment, promise) {
|
|
114
114
|
if (!process.stdout.isTTY) {
|
|
115
115
|
// Simple stdout without animations
|
|
116
|
-
process.stdout.write(comment);
|
|
116
|
+
process.stdout.write(comment + `\n`);
|
|
117
117
|
promise.then(() => process.stdout.write(` ✓ Success\n`), () => process.stdout.write(` ✗ Failure\n`));
|
|
118
118
|
return promise;
|
|
119
119
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { debug, error } from '../logger.js';
|
|
11
|
+
import { isVersionAtLeast } from '../isVersionAtLeast.js';
|
|
12
|
+
export function AuthErrorHandler(client) {
|
|
13
|
+
function handleAuthErrors(err, shutdown) {
|
|
14
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
15
|
+
var _a, _b;
|
|
16
|
+
if (((_a = err === null || err === void 0 ? void 0 : err.headers) === null || _a === void 0 ? void 0 : _a.message) == 'Unauthenticated') {
|
|
17
|
+
yield printUnauthenticatedError();
|
|
18
|
+
shutdown();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (((_b = err === null || err === void 0 ? void 0 : err.headers) === null || _b === void 0 ? void 0 : _b.message) == 'Forbidden') {
|
|
22
|
+
error("You're not authorized. Insufficient permissions?");
|
|
23
|
+
shutdown();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function printUnauthenticatedError() {
|
|
29
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
30
|
+
const { isSupported, serverVersion } = yield isAppSupportedVersion(client);
|
|
31
|
+
if (isSupported) {
|
|
32
|
+
error("You're not authenticated. Invalid API key?");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
error(`Server version ${serverVersion} does not support CLI watch mode. Please update your Tolgee server to version ${REQUIRED_VERSION} or higher.`);
|
|
36
|
+
return;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function isAppSupportedVersion(client) {
|
|
40
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
const serverVersion = yield getTolgeeServerVersion(client);
|
|
42
|
+
if (!serverVersion) {
|
|
43
|
+
debug('Could not determine server version');
|
|
44
|
+
return {
|
|
45
|
+
isSupported: false,
|
|
46
|
+
serverVersion: serverVersion,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
isSupported: isVersionAtLeast(REQUIRED_VERSION, serverVersion),
|
|
51
|
+
serverVersion: serverVersion,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function getTolgeeServerVersion(client) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
var _a;
|
|
58
|
+
try {
|
|
59
|
+
const config = yield client.GET('/api/public/configuration');
|
|
60
|
+
const version = (_a = config.response) === null || _a === void 0 ? void 0 : _a.headers.get('x-tolgee-version');
|
|
61
|
+
return version || null;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
debug('Failed to get server version: ' + error);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze({ handleAuthErrors });
|
|
70
|
+
}
|
|
71
|
+
const REQUIRED_VERSION = '3.143.0';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { WebsocketClient } from '../../client/WebsocketClient.js';
|
|
11
|
+
import { debug, error, info, success } from '../logger.js';
|
|
12
|
+
import { setETag } from '../eTagStorage.js';
|
|
13
|
+
import { clearInterval } from 'node:timers';
|
|
14
|
+
import { AuthErrorHandler } from './AuthErrorHandler.js';
|
|
15
|
+
// Polling interval as backup when WebSocket is not available (in seconds)
|
|
16
|
+
const POLLING_INTERVAL_SECONDS = 60;
|
|
17
|
+
// Debounce delay for schedulePull in milliseconds
|
|
18
|
+
const SCHEDULE_PULL_DEBOUNCE_MS = 500;
|
|
19
|
+
export function startWatching(options) {
|
|
20
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
21
|
+
const { apiUrl, apiKey, projectId, doPull, client } = options;
|
|
22
|
+
// Watch mode using WebsocketClient on translation-data-modified
|
|
23
|
+
info('Watching for translation changes... Press Ctrl+C to stop.');
|
|
24
|
+
let pulling = false;
|
|
25
|
+
/**
|
|
26
|
+
* Pending handles the situation when changes are detected while the pull is
|
|
27
|
+
* already in progress.
|
|
28
|
+
*/
|
|
29
|
+
let pending = false;
|
|
30
|
+
let pendingEtag;
|
|
31
|
+
let debounceTimer;
|
|
32
|
+
let pollingTimer;
|
|
33
|
+
let lastExecutionTime = 0;
|
|
34
|
+
const executePull = (etag) => __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
if (pulling) {
|
|
36
|
+
pending = true;
|
|
37
|
+
pendingEtag = etag;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
pulling = true;
|
|
41
|
+
lastExecutionTime = Date.now();
|
|
42
|
+
try {
|
|
43
|
+
yield doPull();
|
|
44
|
+
// Store ETag after successful pull
|
|
45
|
+
if (etag) {
|
|
46
|
+
setETag(projectId, etag);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
error('Error during pull: ' + e.message);
|
|
51
|
+
debug(e);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
pulling = false;
|
|
55
|
+
// If there was a pending pull (data changed when pulling), execute it now
|
|
56
|
+
if (pending) {
|
|
57
|
+
pending = false;
|
|
58
|
+
const capturedEtag = pendingEtag;
|
|
59
|
+
pendingEtag = undefined;
|
|
60
|
+
void executePull(capturedEtag);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const schedulePull = (etag) => __awaiter(this, void 0, void 0, function* () {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const timeSinceLastExecution = now - lastExecutionTime;
|
|
67
|
+
// If last execution was more than 500ms ago, execute immediately
|
|
68
|
+
if (timeSinceLastExecution >= SCHEDULE_PULL_DEBOUNCE_MS) {
|
|
69
|
+
yield executePull(etag);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Otherwise, schedule the update with debounce
|
|
73
|
+
if (debounceTimer)
|
|
74
|
+
clearTimeout(debounceTimer);
|
|
75
|
+
debounceTimer = setTimeout(() => executePull(etag), SCHEDULE_PULL_DEBOUNCE_MS);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// Polling mechanism as backup
|
|
79
|
+
const startPolling = () => {
|
|
80
|
+
if (pollingTimer) {
|
|
81
|
+
clearInterval(pollingTimer);
|
|
82
|
+
}
|
|
83
|
+
const poll = () => __awaiter(this, void 0, void 0, function* () {
|
|
84
|
+
if (pulling)
|
|
85
|
+
return;
|
|
86
|
+
debug('Polling for changes...');
|
|
87
|
+
yield schedulePull();
|
|
88
|
+
});
|
|
89
|
+
// Set up periodic polling
|
|
90
|
+
pollingTimer = setInterval(poll, POLLING_INTERVAL_SECONDS * 1000);
|
|
91
|
+
};
|
|
92
|
+
const wsClient = WebsocketClient({
|
|
93
|
+
serverUrl: new URL(apiUrl).origin,
|
|
94
|
+
authentication: { apiKey: apiKey },
|
|
95
|
+
onConnected: () => {
|
|
96
|
+
debug('WebSocket connected and subscriptions active. Performing initial pull...');
|
|
97
|
+
schedulePull();
|
|
98
|
+
},
|
|
99
|
+
onError: (error) => {
|
|
100
|
+
AuthErrorHandler(client)
|
|
101
|
+
.handleAuthErrors(error, shutdown)
|
|
102
|
+
.catch((err) => {
|
|
103
|
+
debug('Error in handleAuthErrors: ' + err);
|
|
104
|
+
});
|
|
105
|
+
// Non-fatal: just inform
|
|
106
|
+
info('Websocket error encountered. Reconnecting...');
|
|
107
|
+
},
|
|
108
|
+
onConnectionClose: () => {
|
|
109
|
+
info('Websocket connection closed. Attempting to reconnect...');
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const channel = `/projects/${projectId}/translation-data-modified`;
|
|
113
|
+
let unsubscribe;
|
|
114
|
+
function subscribe() {
|
|
115
|
+
unsubscribe = wsClient.subscribe(channel, () => {
|
|
116
|
+
debug('Data change detected by websocket. Pulling now... ');
|
|
117
|
+
schedulePull();
|
|
118
|
+
startPolling();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const shutdown = () => {
|
|
122
|
+
try {
|
|
123
|
+
unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
|
|
124
|
+
}
|
|
125
|
+
catch (_a) {
|
|
126
|
+
// Ignore errors during shutdown cleanup
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
wsClient.deactivate();
|
|
130
|
+
}
|
|
131
|
+
catch (_b) {
|
|
132
|
+
// Ignore errors during shutdown cleanup
|
|
133
|
+
}
|
|
134
|
+
if (pollingTimer) {
|
|
135
|
+
clearInterval(pollingTimer);
|
|
136
|
+
}
|
|
137
|
+
if (debounceTimer) {
|
|
138
|
+
clearTimeout(debounceTimer);
|
|
139
|
+
}
|
|
140
|
+
success('Stopped watching. Bye!');
|
|
141
|
+
process.exit(0);
|
|
142
|
+
};
|
|
143
|
+
process.on('SIGINT', shutdown);
|
|
144
|
+
process.on('SIGTERM', shutdown);
|
|
145
|
+
subscribe();
|
|
146
|
+
// Keep process alive
|
|
147
|
+
yield new Promise(() => { });
|
|
148
|
+
});
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tolgee/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0-rc.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A tool to interact with the Tolgee Platform through CLI",
|
|
6
6
|
"repository": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"author": "Jan Cizmar",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@stomp/stompjs": "^7.1.1",
|
|
33
34
|
"ajv": "^8.17.1",
|
|
34
35
|
"ansi-colors": "^4.1.3",
|
|
35
36
|
"base32-decode": "^1.0.0",
|
|
@@ -38,21 +39,29 @@
|
|
|
38
39
|
"json5": "^2.2.3",
|
|
39
40
|
"jsonschema": "^1.5.0",
|
|
40
41
|
"openapi-fetch": "0.13.1",
|
|
42
|
+
"sockjs-client": "^1.6.1",
|
|
41
43
|
"tinyglobby": "^0.2.12",
|
|
42
44
|
"unescape-js": "^1.1.4",
|
|
43
45
|
"vscode-oniguruma": "^2.0.1",
|
|
44
46
|
"vscode-textmate": "^9.1.0",
|
|
47
|
+
"ws": "^8.18.3",
|
|
45
48
|
"yauzl": "^3.2.0"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@eslint/js": "^9.16.0",
|
|
52
|
+
"semantic-release": "^25.0.2",
|
|
53
|
+
"@semantic-release/npm": "^13.1.3",
|
|
49
54
|
"@semantic-release/changelog": "^6.0.3",
|
|
50
55
|
"@semantic-release/git": "^10.0.1",
|
|
56
|
+
"@semantic-release/github": "^12.0.2",
|
|
57
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
58
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
51
59
|
"@tsconfig/node18": "^18.2.4",
|
|
52
60
|
"@tsconfig/recommended": "^1.0.8",
|
|
53
61
|
"@types/eslint__js": "^8.42.3",
|
|
54
62
|
"@types/js-yaml": "^4.0.9",
|
|
55
63
|
"@types/node": "^22.10.1",
|
|
64
|
+
"@types/sockjs-client": "^1.5.4",
|
|
56
65
|
"@types/yauzl": "^2.10.3",
|
|
57
66
|
"cross-env": "^7.0.3",
|
|
58
67
|
"eslint": "^9.16.0",
|
|
@@ -64,7 +73,6 @@
|
|
|
64
73
|
"openapi-typescript": "^7.4.4",
|
|
65
74
|
"premove": "^4.0.0",
|
|
66
75
|
"prettier": "^3.4.1",
|
|
67
|
-
"semantic-release": "^24.2.0",
|
|
68
76
|
"tree-cli": "^0.6.7",
|
|
69
77
|
"tsx": "^4.19.2",
|
|
70
78
|
"typescript": "~5.6.3",
|