tsoft-cli 3.14.1 → 3.14.4
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/CHANGELOG.md +16 -0
- package/locales/en.json +9 -1
- package/locales/tr.json +9 -1
- package/package.json +3 -3
- package/src/commands/login.js +23 -4
- package/src/commands/theme-push.js +15 -1
- package/src/errors/error-mapper.js +70 -1
- package/src/i18n.js +4 -0
- package/src/server.js +24 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to T-Soft CLI will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.14.4] - 2026-10-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `theme push` now tells you when your theme is too big to publish, instead of "Server error occurred." A theme over the store's upload limit was refused with a size error, but the CLI only looked for codes it already knew and reported it as a generic server failure, so there was no way to tell that the fix was on your side or what to remove. When the store reports the package size, the message now gives it next to the limit ("Theme package is too large: 11.4 MB (limit 8 MB).") and lists the largest files in the package, followed by a hint to compress or remove large images. A store that only answers with a bare size refusal gets "Theme package exceeds the server limit." and the same hint. With `--json` the sizes and the file list are returned under `error.details`. Other push errors read exactly as before.
|
|
12
|
+
|
|
13
|
+
## [3.14.3] - 2026-10-15
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
- Theme archives are now opened with `adm-zip` 0.6.1, which closes a set of published advisories in the 0.5 line. A `^0.5.16` range can never reach 0.6, so even a fresh install picked up 0.5.18, and a crafted archive could make it allocate memory without limit or write an empty file over one already in your theme: an entry that declared its uncompressed size as zero was inflated in full, and an archive that named the same file twice was accepted. Both are now refused before anything in your theme directory is touched, the same way a broken download already was. Normal theme downloads behave exactly as before.
|
|
17
|
+
- `axios` now requires 1.20.0 or later. The previous `^1.14.0` range still admitted 1.14–1.19, which carry advisories around proxy handling, header injection and redirects; an older install could keep one of those. Raising the floor rules them out.
|
|
18
|
+
|
|
19
|
+
## [3.14.2] - 2026-10-15
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- `tsoft login` now says plainly when the port it needs for sign-in is taken, and no longer sends you to the browser first. The browser opened and waited for you to sign in before the CLI noticed it could not receive the answer, and what came back was `listen EADDRINUSE: address already in use :::8046` followed by a pointer to log files the CLI does not write — so you signed in for nothing and were left with a line that said nothing about what to do. The local connection is now opened before the browser, and when the port is in use the message names it, suggests stopping a `tsoft login` left running in another terminal, and gives the command to see which application holds the port (`lsof` on macOS and Linux, `netstat` on Windows). Other reasons the connection cannot be opened get their own message, the raw error is still available under `--verbose`, and the misleading log-file line is gone from every login error.
|
|
23
|
+
|
|
8
24
|
## [3.14.1] - 2026-09-22
|
|
9
25
|
|
|
10
26
|
### Fixed
|
package/locales/en.json
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"error.push.backend_reject.hint": "For details: tsoft theme push --verbose",
|
|
23
23
|
"error.push.unexpected.message": "An unexpected error occurred.",
|
|
24
24
|
"error.push.unexpected.hint": "For details: tsoft theme push --verbose",
|
|
25
|
+
"error.push.payload_too_large.message": "Theme package is too large: {{size}} MB (limit {{limit}} MB).",
|
|
26
|
+
"error.push.payload_too_large.message_unknown_size": "Theme package exceeds the server limit.",
|
|
27
|
+
"error.push.payload_too_large.hint": "Compress large images or remove them from the theme.",
|
|
28
|
+
"error.push.payload_too_large.largest_files_title": "Largest files:",
|
|
25
29
|
"error.http.401.message": "Authorization error.",
|
|
26
30
|
"error.http.401.hint": "Login again: tsoft login --org",
|
|
27
31
|
"error.http.403.message": "You do not have permission for this operation.",
|
|
@@ -54,7 +58,11 @@
|
|
|
54
58
|
"login.tip": "💡 Tip: Use \"tsoft whoami\" to check your status.",
|
|
55
59
|
"login.json_error.message": "Login command requires interactive mode.",
|
|
56
60
|
"login.json_error.hint": "For CI/CD, authenticate with tsoft login first to store the token, then use it.",
|
|
57
|
-
"login.
|
|
61
|
+
"login.port_in_use.message": "Port {{port}}, which sign-in needs, is already in use by another application.",
|
|
62
|
+
"login.port_in_use.close_hint": "If \"tsoft login\" is still running in another terminal, stop it there with Ctrl+C and try again.",
|
|
63
|
+
"login.port_in_use.find_hint": "To see which application is using the port, run: {{command}}",
|
|
64
|
+
"login.server_start_failed.message": "Sign-in could not start: the local connection it needs could not be opened on this computer.",
|
|
65
|
+
"login.server_start_failed.hint": "For details: tsoft login --verbose",
|
|
58
66
|
"login.security_error": "State parameter mismatch! This may indicate a security issue.",
|
|
59
67
|
"logout.starting": "🚪 Logging out...",
|
|
60
68
|
"logout.not_logged_in": "▲ You are not logged in.",
|
package/locales/tr.json
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"error.push.backend_reject.hint": "Detaylar için: tsoft theme push --verbose",
|
|
23
23
|
"error.push.unexpected.message": "Beklenmeyen bir hata oluştu.",
|
|
24
24
|
"error.push.unexpected.hint": "Detaylar için: tsoft theme push --verbose",
|
|
25
|
+
"error.push.payload_too_large.message": "Tema paketi çok büyük: {{size}} MB (sınır {{limit}} MB).",
|
|
26
|
+
"error.push.payload_too_large.message_unknown_size": "Tema paketi sunucu sınırını aşıyor.",
|
|
27
|
+
"error.push.payload_too_large.hint": "Büyük görselleri sıkıştırın ya da temadan çıkarın.",
|
|
28
|
+
"error.push.payload_too_large.largest_files_title": "En büyük dosyalar:",
|
|
25
29
|
"error.http.401.message": "Yetkilendirme hatası.",
|
|
26
30
|
"error.http.401.hint": "Tekrar giriş yapın: tsoft login --org",
|
|
27
31
|
"error.http.403.message": "Bu işlem için yetkiniz yok.",
|
|
@@ -54,7 +58,11 @@
|
|
|
54
58
|
"login.tip": "💡 İpucu: Durum kontrolü için \"tsoft whoami\" komutunu kullanın.",
|
|
55
59
|
"login.json_error.message": "Login komutu interaktif mod gerektiriyor.",
|
|
56
60
|
"login.json_error.hint": "CI/CD için önceden tsoft login ile kimlik doğrulayıp token'i sakladıktan sonra kullanın.",
|
|
57
|
-
"login.
|
|
61
|
+
"login.port_in_use.message": "Giriş için gereken {{port}} numaralı port şu anda başka bir uygulama tarafından kullanılıyor.",
|
|
62
|
+
"login.port_in_use.close_hint": "Başka bir terminalde açık kalmış bir \"tsoft login\" varsa o pencerede Ctrl+C ile durdurup tekrar deneyin.",
|
|
63
|
+
"login.port_in_use.find_hint": "Portu hangi uygulamanın kullandığını görmek için şunu çalıştırın: {{command}}",
|
|
64
|
+
"login.server_start_failed.message": "Giriş başlatılamadı: giriş için gereken yerel bağlantı bu bilgisayarda açılamadı.",
|
|
65
|
+
"login.server_start_failed.hint": "Detaylar için: tsoft login --verbose",
|
|
58
66
|
"login.security_error": "State parametresi eşleşmiyor! Güvenlik hatası olabilir.",
|
|
59
67
|
"logout.starting": "🚪 Çıkış yapılıyor...",
|
|
60
68
|
"logout.not_logged_in": "▲ Zaten giriş yapmamışsınız.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tsoft-cli",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.4",
|
|
4
4
|
"description": "Command-line tool for tsoft360 theme development and management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@inquirer/prompts": "^8.0.2",
|
|
47
|
-
"adm-zip": "^0.
|
|
48
|
-
"axios": "^1.
|
|
47
|
+
"adm-zip": "^0.6.1",
|
|
48
|
+
"axios": "^1.20.0",
|
|
49
49
|
"boxen": "^8.0.1",
|
|
50
50
|
"chalk": "^5.3.0",
|
|
51
51
|
"chokidar": "^5.0.0",
|
package/src/commands/login.js
CHANGED
|
@@ -4,7 +4,7 @@ import {generateCodeVerifier, generateCodeChallenge} from '../pkce.js';
|
|
|
4
4
|
import {startCallbackServer} from '../server.js';
|
|
5
5
|
import {exchangeToken, saveTokens, setActiveStore} from '../storage.js';
|
|
6
6
|
import * as config from '../config.js';
|
|
7
|
-
import { isJsonMode, jsonOut } from '../output-mode.js';
|
|
7
|
+
import { isJsonMode, jsonOut, verboseLog } from '../output-mode.js';
|
|
8
8
|
import { t } from '../i18n.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -43,7 +43,8 @@ export async function loginCommand() {
|
|
|
43
43
|
authUrl.searchParams.set('prompt', 'select_org_store');
|
|
44
44
|
|
|
45
45
|
// 3. Callback server'ı başlat
|
|
46
|
-
const
|
|
46
|
+
const callbackServer = await startCallbackServer(config.CALLBACK_PORT)
|
|
47
|
+
.catch((error) => exitOnListenFailure(error, config.CALLBACK_PORT));
|
|
47
48
|
|
|
48
49
|
// 4. Browser'ı aç
|
|
49
50
|
console.log(chalk.cyan(t('login.browser_opening')));
|
|
@@ -56,7 +57,7 @@ export async function loginCommand() {
|
|
|
56
57
|
console.log(chalk.gray(' ' + t('login.waiting_detail') + '\n'));
|
|
57
58
|
|
|
58
59
|
// 5. Callback'i bekle
|
|
59
|
-
const {code, state: returnedState, store} = await
|
|
60
|
+
const {code, state: returnedState, store} = await callbackServer.result;
|
|
60
61
|
|
|
61
62
|
// 6. State doğrula (CSRF koruması)
|
|
62
63
|
if (state !== returnedState) {
|
|
@@ -89,7 +90,25 @@ export async function loginCommand() {
|
|
|
89
90
|
|
|
90
91
|
} catch (error) {
|
|
91
92
|
console.error(chalk.red('\n❌ Hata: ') + error.message);
|
|
92
|
-
console.error(chalk.gray('\n' + t('login.error_log')));
|
|
93
93
|
process.exit(1);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
function exitOnListenFailure(error, port) {
|
|
98
|
+
verboseLog(error.message);
|
|
99
|
+
|
|
100
|
+
if (error.code === 'EADDRINUSE') {
|
|
101
|
+
console.error(chalk.red('\n❌ ' + t('login.port_in_use.message', { port })));
|
|
102
|
+
console.error(chalk.gray(' ' + t('login.port_in_use.close_hint')));
|
|
103
|
+
console.error(chalk.gray(' ' + t('login.port_in_use.find_hint', { command: findPortOwnerCommand(port) })));
|
|
104
|
+
} else {
|
|
105
|
+
console.error(chalk.red('\n❌ ' + t('login.server_start_failed.message')));
|
|
106
|
+
console.error(chalk.gray(' ' + t('login.server_start_failed.hint')));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findPortOwnerCommand(port) {
|
|
113
|
+
return process.platform === 'win32' ? `netstat -ano | findstr :${port}` : `lsof -i :${port}`;
|
|
114
|
+
}
|
|
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { createApiClient } from '../api-client.js';
|
|
4
4
|
import { describeActiveTheme, getActiveStore } from '../storage.js';
|
|
5
|
-
import { mapPushError } from '../errors/error-mapper.js';
|
|
5
|
+
import { formatMegabytes, mapPushError } from '../errors/error-mapper.js';
|
|
6
6
|
import { isJsonMode, jsonOut } from '../output-mode.js';
|
|
7
7
|
import { t } from '../i18n.js';
|
|
8
8
|
|
|
@@ -15,6 +15,8 @@ const CHANGE_CATEGORIES = {
|
|
|
15
15
|
'Meta': ['meta_object_added', 'meta_object_removed', 'meta_field_added', 'meta_field_removed'],
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const LARGEST_FILES_SHOWN = 5;
|
|
19
|
+
|
|
18
20
|
const PARTNERS_BASE = 'https://partners.tsoft360.com';
|
|
19
21
|
const PARTNERS_THEMES_INDEX = `${PARTNERS_BASE}/themes`;
|
|
20
22
|
|
|
@@ -77,6 +79,16 @@ function displayDiffSummary(data, previousVersion) {
|
|
|
77
79
|
console.log(chalk.gray(' ' + t(hintKey)));
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
function displayLargestFiles(details) {
|
|
83
|
+
const files = details?.largest_files ?? [];
|
|
84
|
+
if (files.length === 0) return;
|
|
85
|
+
|
|
86
|
+
console.log(chalk.gray(` ${t('error.push.payload_too_large.largest_files_title')}`));
|
|
87
|
+
for (const file of files.slice(0, LARGEST_FILES_SHOWN)) {
|
|
88
|
+
console.log(chalk.gray(` - ${file.path} ${formatMegabytes(file.bytes)} MB`));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
/**
|
|
81
93
|
* Tema push komutu
|
|
82
94
|
* @param {string|null} themeName - Tema adi/UUID (opsiyonel)
|
|
@@ -187,6 +199,7 @@ export async function themePushCommand(themeName, options = {}) {
|
|
|
187
199
|
error: {
|
|
188
200
|
message: mapped.message,
|
|
189
201
|
hint: mapped.hint || null,
|
|
202
|
+
...(mapped.details ? { details: mapped.details } : {}),
|
|
190
203
|
}
|
|
191
204
|
});
|
|
192
205
|
process.exit(1);
|
|
@@ -196,6 +209,7 @@ export async function themePushCommand(themeName, options = {}) {
|
|
|
196
209
|
if (error.response) {
|
|
197
210
|
const mapped = mapPushError(error, verbose);
|
|
198
211
|
console.error(chalk.red(`X ${mapped.message}`));
|
|
212
|
+
displayLargestFiles(mapped.details);
|
|
199
213
|
if (mapped.hint) {
|
|
200
214
|
console.log(chalk.yellow(` ${mapped.hint}`));
|
|
201
215
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Push error code -> locale key mapper
|
|
3
3
|
*/
|
|
4
|
-
import { t } from '../i18n.js';
|
|
4
|
+
import { t, getLocale } from '../i18n.js';
|
|
5
5
|
import { isReloginRequired } from '../storage.js';
|
|
6
6
|
|
|
7
7
|
const ERROR_MAP = {
|
|
@@ -25,8 +25,73 @@ const ERROR_MAP = {
|
|
|
25
25
|
message: 'error.push.unexpected.message',
|
|
26
26
|
hint: 'error.push.unexpected.hint',
|
|
27
27
|
},
|
|
28
|
+
'PUSH-PAYLOAD-TOO-LARGE': {
|
|
29
|
+
message: 'error.push.payload_too_large.message',
|
|
30
|
+
messageUnknownSize: 'error.push.payload_too_large.message_unknown_size',
|
|
31
|
+
hint: 'error.push.payload_too_large.hint',
|
|
32
|
+
},
|
|
28
33
|
};
|
|
29
34
|
|
|
35
|
+
const PAYLOAD_TOO_LARGE_STATUS = 413;
|
|
36
|
+
const BYTES_PER_MEGABYTE = 1048576;
|
|
37
|
+
|
|
38
|
+
const SMALLEST_ROUNDED_MEGABYTES = 0.1;
|
|
39
|
+
|
|
40
|
+
export function formatMegabytes(bytes) {
|
|
41
|
+
const megabytes = bytes / BYTES_PER_MEGABYTE;
|
|
42
|
+
const precision = megabytes < SMALLEST_ROUNDED_MEGABYTES
|
|
43
|
+
? { maximumSignificantDigits: 1 }
|
|
44
|
+
: { maximumFractionDigits: 1 };
|
|
45
|
+
|
|
46
|
+
return new Intl.NumberFormat(getLocale(), precision).format(megabytes);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toPositiveNumber(value) {
|
|
50
|
+
if (typeof value !== 'number' && typeof value !== 'string') return null;
|
|
51
|
+
if (typeof value === 'string' && value.trim() === '') return null;
|
|
52
|
+
const number = Number(value);
|
|
53
|
+
return Number.isFinite(number) && number > 0 ? number : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isPayloadTooLarge(responseData, errorCode, statusCode) {
|
|
57
|
+
if (errorCode === 'PUSH-PAYLOAD-TOO-LARGE') return true;
|
|
58
|
+
if (!errorCode) return statusCode === PAYLOAD_TOO_LARGE_STATUS;
|
|
59
|
+
if (errorCode !== 'PUSH-BACKEND-ERROR') return false;
|
|
60
|
+
|
|
61
|
+
return statusCode === PAYLOAD_TOO_LARGE_STATUS
|
|
62
|
+
|| Number(responseData?.details?.status) === PAYLOAD_TOO_LARGE_STATUS;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeLargestFiles(files) {
|
|
66
|
+
if (!Array.isArray(files)) return [];
|
|
67
|
+
return files
|
|
68
|
+
.filter((file) => typeof file?.path === 'string' && toPositiveNumber(file?.bytes) !== null)
|
|
69
|
+
.map((file) => ({ path: file.path, bytes: toPositiveNumber(file.bytes) }));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mapPayloadTooLarge(responseData, verbose) {
|
|
73
|
+
const mapped = ERROR_MAP['PUSH-PAYLOAD-TOO-LARGE'];
|
|
74
|
+
const details = responseData?.details;
|
|
75
|
+
const sizeBytes = toPositiveNumber(details?.size_bytes);
|
|
76
|
+
const limitBytes = toPositiveNumber(details?.limit_bytes);
|
|
77
|
+
const raw = verbose ? responseData : null;
|
|
78
|
+
|
|
79
|
+
if (sizeBytes === null || limitBytes === null) {
|
|
80
|
+
return { message: t(mapped.messageUnknownSize), hint: t(mapped.hint), raw };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
message: t(mapped.message, { size: formatMegabytes(sizeBytes), limit: formatMegabytes(limitBytes) }),
|
|
85
|
+
hint: t(mapped.hint),
|
|
86
|
+
raw,
|
|
87
|
+
details: {
|
|
88
|
+
size_bytes: sizeBytes,
|
|
89
|
+
limit_bytes: limitBytes,
|
|
90
|
+
largest_files: normalizeLargestFiles(details.largest_files),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
30
95
|
const HTTP_STATUS_MAP = {
|
|
31
96
|
401: {
|
|
32
97
|
message: 'error.http.401.message',
|
|
@@ -61,6 +126,10 @@ export function mapPushError(error, verbose = false) {
|
|
|
61
126
|
const errorCode = responseData?.error_code;
|
|
62
127
|
const statusCode = error.response?.status;
|
|
63
128
|
|
|
129
|
+
if (isPayloadTooLarge(responseData, errorCode, statusCode)) {
|
|
130
|
+
return mapPayloadTooLarge(responseData, verbose);
|
|
131
|
+
}
|
|
132
|
+
|
|
64
133
|
// 1. error_code oncelikli
|
|
65
134
|
if (errorCode && ERROR_MAP[errorCode]) {
|
|
66
135
|
const mapped = ERROR_MAP[errorCode];
|
package/src/i18n.js
CHANGED
package/src/server.js
CHANGED
|
@@ -7,7 +7,15 @@ import { URL } from 'url';
|
|
|
7
7
|
* @returns {Promise<{code: string, state: string, store: string}>}
|
|
8
8
|
*/
|
|
9
9
|
export function startCallbackServer(port) {
|
|
10
|
-
|
|
10
|
+
let resolve;
|
|
11
|
+
let reject;
|
|
12
|
+
const result = new Promise((resolveResult, rejectResult) => {
|
|
13
|
+
resolve = resolveResult;
|
|
14
|
+
reject = rejectResult;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return new Promise((resolveListening, rejectListening) => {
|
|
18
|
+
let listening = false;
|
|
11
19
|
const server = http.createServer((req, res) => {
|
|
12
20
|
const url = new URL(req.url, `http://localhost:${port}`);
|
|
13
21
|
|
|
@@ -45,15 +53,29 @@ export function startCallbackServer(port) {
|
|
|
45
53
|
});
|
|
46
54
|
|
|
47
55
|
server.on('error', (err) => {
|
|
48
|
-
|
|
56
|
+
if (listening) {
|
|
57
|
+
reject(err);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
server.close();
|
|
61
|
+
rejectListening(listenFailure(err, port));
|
|
49
62
|
});
|
|
50
63
|
|
|
51
64
|
server.listen(port, () => {
|
|
65
|
+
listening = true;
|
|
52
66
|
console.log(`✓ Callback sunucusu başlatıldı (http://localhost:${port}/callback)`);
|
|
67
|
+
resolveListening({ result });
|
|
53
68
|
});
|
|
54
69
|
});
|
|
55
70
|
}
|
|
56
71
|
|
|
72
|
+
function listenFailure(err, port) {
|
|
73
|
+
const failure = new Error(err.message, { cause: err });
|
|
74
|
+
failure.code = err.code;
|
|
75
|
+
failure.port = port;
|
|
76
|
+
return failure;
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
function successHtml() {
|
|
58
80
|
return `
|
|
59
81
|
<!DOCTYPE html>
|