tsoft-cli 3.14.0 → 3.14.2
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 +10 -0
- package/locales/en.json +5 -1
- package/locales/tr.json +5 -1
- package/package.json +1 -1
- package/src/commands/login.js +23 -4
- package/src/commands/whoami.js +2 -2
- package/src/errors/error-mapper.js +9 -0
- package/src/server.js +24 -2
- package/src/storage.js +31 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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.2] - 2026-10-15
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `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.
|
|
12
|
+
|
|
13
|
+
## [3.14.1] - 2026-09-22
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- An expired session now says so and points at `tsoft login --org`, instead of printing a raw status line. When the store refused a session renewal the CLI wrote out `Token refresh hatası: 401 - Token refresh failed` and stopped there — a line that names an internal step, is not in the language the rest of the output uses, and says nothing about what to do next; the session was simply over and signing in again was all that was needed, but nothing on screen said that. The refusal is now recognised where it happens and reported as an expired session with the sign-in command to run. A store that is genuinely failing is still reported as a failure, so a broken store is not mistaken for an expired session and nobody is sent to sign in again over an outage. The renewal details are still available under `--verbose` for anyone diagnosing one.
|
|
17
|
+
|
|
8
18
|
## [3.14.0] - 2026-09-15
|
|
9
19
|
|
|
10
20
|
### Added
|
package/locales/en.json
CHANGED
|
@@ -54,7 +54,11 @@
|
|
|
54
54
|
"login.tip": "💡 Tip: Use \"tsoft whoami\" to check your status.",
|
|
55
55
|
"login.json_error.message": "Login command requires interactive mode.",
|
|
56
56
|
"login.json_error.hint": "For CI/CD, authenticate with tsoft login first to store the token, then use it.",
|
|
57
|
-
"login.
|
|
57
|
+
"login.port_in_use.message": "Port {{port}}, which sign-in needs, is already in use by another application.",
|
|
58
|
+
"login.port_in_use.close_hint": "If \"tsoft login\" is still running in another terminal, stop it there with Ctrl+C and try again.",
|
|
59
|
+
"login.port_in_use.find_hint": "To see which application is using the port, run: {{command}}",
|
|
60
|
+
"login.server_start_failed.message": "Sign-in could not start: the local connection it needs could not be opened on this computer.",
|
|
61
|
+
"login.server_start_failed.hint": "For details: tsoft login --verbose",
|
|
58
62
|
"login.security_error": "State parameter mismatch! This may indicate a security issue.",
|
|
59
63
|
"logout.starting": "🚪 Logging out...",
|
|
60
64
|
"logout.not_logged_in": "▲ You are not logged in.",
|
package/locales/tr.json
CHANGED
|
@@ -54,7 +54,11 @@
|
|
|
54
54
|
"login.tip": "💡 İpucu: Durum kontrolü için \"tsoft whoami\" komutunu kullanın.",
|
|
55
55
|
"login.json_error.message": "Login komutu interaktif mod gerektiriyor.",
|
|
56
56
|
"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.
|
|
57
|
+
"login.port_in_use.message": "Giriş için gereken {{port}} numaralı port şu anda başka bir uygulama tarafından kullanılıyor.",
|
|
58
|
+
"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.",
|
|
59
|
+
"login.port_in_use.find_hint": "Portu hangi uygulamanın kullandığını görmek için şunu çalıştırın: {{command}}",
|
|
60
|
+
"login.server_start_failed.message": "Giriş başlatılamadı: giriş için gereken yerel bağlantı bu bilgisayarda açılamadı.",
|
|
61
|
+
"login.server_start_failed.hint": "Detaylar için: tsoft login --verbose",
|
|
58
62
|
"login.security_error": "State parametresi eşleşmiyor! Güvenlik hatası olabilir.",
|
|
59
63
|
"logout.starting": "🚪 Çıkış yapılıyor...",
|
|
60
64
|
"logout.not_logged_in": "▲ Zaten giriş yapmamışsınız.",
|
package/package.json
CHANGED
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
|
+
}
|
package/src/commands/whoami.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { loadTokens, ensureValidToken, getTokenExpiry, isTokenValid, isRefreshTokenValid } from '../storage.js';
|
|
2
|
+
import { loadTokens, ensureValidToken, getTokenExpiry, isTokenValid, isRefreshTokenValid, isReloginRequired } from '../storage.js';
|
|
3
3
|
import { loginCommand } from './login.js';
|
|
4
4
|
import { isJsonMode, jsonOut } from '../output-mode.js';
|
|
5
5
|
import { mapError } from '../errors/error-mapper.js';
|
|
@@ -30,7 +30,7 @@ export async function whoamiCommand() {
|
|
|
30
30
|
console.log(chalk.yellow(t('whoami.token_refreshed') + '\n'));
|
|
31
31
|
}
|
|
32
32
|
} catch (error) {
|
|
33
|
-
if (error
|
|
33
|
+
if (isReloginRequired(error)) {
|
|
34
34
|
console.log(chalk.yellow(t('whoami.session_expired') + '\n'));
|
|
35
35
|
await loginCommand();
|
|
36
36
|
tokens = await loadTokens();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Push error code -> locale key mapper
|
|
3
3
|
*/
|
|
4
4
|
import { t } from '../i18n.js';
|
|
5
|
+
import { isReloginRequired } from '../storage.js';
|
|
5
6
|
|
|
6
7
|
const ERROR_MAP = {
|
|
7
8
|
'PUSH-PENDING-REVIEW': {
|
|
@@ -120,6 +121,14 @@ export function mapError(error, { command = 'unknown', verbose = false } = {}) {
|
|
|
120
121
|
const responseData = error.response?.data;
|
|
121
122
|
const statusCode = error.response?.status;
|
|
122
123
|
|
|
124
|
+
if (isReloginRequired(error)) {
|
|
125
|
+
return {
|
|
126
|
+
message: t('error.auth_required.message'),
|
|
127
|
+
hint: t('error.auth_required.hint'),
|
|
128
|
+
raw: verbose ? responseData : null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
// 1. HTTP status code — mevcut HTTP_STATUS_MAP kullan
|
|
124
133
|
if (statusCode && HTTP_STATUS_MAP[statusCode]) {
|
|
125
134
|
const mapped = HTTP_STATUS_MAP[statusCode];
|
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>
|
package/src/storage.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import axios from 'axios';
|
|
5
5
|
import * as config from './config.js';
|
|
6
|
+
import { t } from './i18n.js';
|
|
7
|
+
import { verboseLog } from './output-mode.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Global config dizini (~/.tsoft-cli/)
|
|
@@ -16,6 +18,23 @@ const ACTIVE_THEME_FILE = 'active-theme';
|
|
|
16
18
|
|
|
17
19
|
const LEGACY_ACTIVE_THEME_FILE = 'active-theme.txt';
|
|
18
20
|
|
|
21
|
+
export const RELOGIN_REQUIRED = 'RELOGIN_REQUIRED';
|
|
22
|
+
|
|
23
|
+
export function createReloginRequiredError(response = null) {
|
|
24
|
+
const error = new Error(t('error.auth_required.message'));
|
|
25
|
+
error.code = RELOGIN_REQUIRED;
|
|
26
|
+
|
|
27
|
+
if (response) {
|
|
28
|
+
error.response = response;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isReloginRequired(error) {
|
|
35
|
+
return error?.code === RELOGIN_REQUIRED || error?.message === RELOGIN_REQUIRED;
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
/**
|
|
20
39
|
* Store domain'i dizin adına uygun formata çevirir
|
|
21
40
|
* @param {string} domain - Store domain (örn: birtan.1isim.com)
|
|
@@ -347,16 +366,24 @@ export async function refreshAccessToken(refreshToken, store) {
|
|
|
347
366
|
};
|
|
348
367
|
} catch (error) {
|
|
349
368
|
if (error.response?.data) {
|
|
350
|
-
|
|
369
|
+
verboseLog('Token refresh hatası:', error.response.data);
|
|
351
370
|
}
|
|
352
371
|
if (error.response) {
|
|
353
|
-
|
|
372
|
+
if (error.response.status === 401) {
|
|
373
|
+
throw createReloginRequiredError(error.response);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const refreshError = new Error(
|
|
354
377
|
`Token refresh hatası: ${error.response.status} - ${
|
|
355
378
|
error.response.data.error_description ||
|
|
356
379
|
error.response.data.message ||
|
|
357
380
|
'Bilinmeyen hata'
|
|
358
381
|
}`
|
|
359
382
|
);
|
|
383
|
+
|
|
384
|
+
refreshError.response = error.response;
|
|
385
|
+
|
|
386
|
+
throw refreshError;
|
|
360
387
|
}
|
|
361
388
|
throw new Error(`Token refresh hatası: ${error.message}`);
|
|
362
389
|
}
|
|
@@ -519,7 +546,7 @@ export function isRefreshTokenValid(tokens) {
|
|
|
519
546
|
*/
|
|
520
547
|
export async function ensureValidToken(tokens) {
|
|
521
548
|
if (!tokens) {
|
|
522
|
-
throw
|
|
549
|
+
throw createReloginRequiredError();
|
|
523
550
|
}
|
|
524
551
|
|
|
525
552
|
// Access token geçerliyse direkt dön
|
|
@@ -535,7 +562,7 @@ export async function ensureValidToken(tokens) {
|
|
|
535
562
|
}
|
|
536
563
|
|
|
537
564
|
// Her ikisi de geçersizse, otomatik re-login gerekiyor
|
|
538
|
-
throw
|
|
565
|
+
throw createReloginRequiredError();
|
|
539
566
|
}
|
|
540
567
|
|
|
541
568
|
/**
|