@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.
@@ -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 body = Object.assign(Object.assign({}, req), { zip: true });
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
- exitWithError(errorFromLoadable(loadable));
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
+ }
@@ -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 { saveApiKey, removeApiKeys, clearAuthStore, } from '../config/credentials.js';
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,
@@ -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
- function fetchZipBlob(opts) {
18
- return __awaiter(this, void 0, void 0, function* () {
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(instance, projectId) {
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
- if (!store[instance.hostname]) {
86
+ const apiUrlObj = new URL(apiUrl);
87
+ if (!store[apiUrlObj.hostname]) {
87
88
  return null;
88
89
  }
89
- const scopedStore = store[instance.hostname];
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 ${instance.hostname} expired.`);
94
- yield storePat(store, instance, undefined);
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 ${instance.hostname} expired.`);
106
- yield removePak(store, instance, projectId);
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
+ }
@@ -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.14.0",
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",