tsoft-cli 3.13.3 → 3.14.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/CHANGELOG.md +10 -0
- package/locales/en.json +2 -0
- package/locales/tr.json +2 -0
- package/package.json +1 -1
- package/src/commands/theme-dev.js +34 -10
- package/src/commands/whoami.js +2 -2
- package/src/errors/error-mapper.js +9 -0
- 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.1] - 2026-09-22
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
13
|
+
## [3.14.0] - 2026-09-15
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `theme dev` can open the panel's theme editor with `E`, and now prints both addresses as text. The status line only ever offered `P` for the preview and named no address at all, so reaching the editor meant leaving the terminal and finding the theme in the panel by hand, and the preview address could not be copied into another browser or pasted to a colleague. The editor address is asked of the store when the watcher starts; if the store does not answer or has no address for the theme, the `E` hint and the editor line are left out and `theme dev` carries on as before.
|
|
17
|
+
|
|
8
18
|
## [3.13.3] - 2026-09-10
|
|
9
19
|
|
|
10
20
|
### Fixed
|
package/locales/en.json
CHANGED
|
@@ -330,8 +330,10 @@
|
|
|
330
330
|
"submit.step3": "3. Submit from the partner panel",
|
|
331
331
|
"dev.status_watching": "Watching",
|
|
332
332
|
"dev.status_preview": "preview",
|
|
333
|
+
"dev.status_editor": "editor",
|
|
333
334
|
"dev.status_exit": "exit",
|
|
334
335
|
"dev.preview_open_failed": "Couldn't open browser: {{error}}",
|
|
336
|
+
"dev.editor_open_failed": "Couldn't open browser: {{error}}",
|
|
335
337
|
"dev.section_synced": "🧩 section updated: {{folder}}",
|
|
336
338
|
"dev.status_section": "section",
|
|
337
339
|
"dev.section_menu_opening": "Opening section menu...",
|
package/locales/tr.json
CHANGED
|
@@ -330,8 +330,10 @@
|
|
|
330
330
|
"submit.step3": "3. Partner panelden submit edin",
|
|
331
331
|
"dev.status_watching": "İzleniyor",
|
|
332
332
|
"dev.status_preview": "önizleme",
|
|
333
|
+
"dev.status_editor": "editör",
|
|
333
334
|
"dev.status_exit": "çıkış",
|
|
334
335
|
"dev.preview_open_failed": "Tarayıcı açılamadı: {{error}}",
|
|
336
|
+
"dev.editor_open_failed": "Tarayıcı açılamadı: {{error}}",
|
|
335
337
|
"dev.section_synced": "🧩 section güncellendi: {{folder}}",
|
|
336
338
|
"dev.status_section": "section",
|
|
337
339
|
"dev.section_menu_opening": "Section menüsü açılıyor...",
|
package/package.json
CHANGED
|
@@ -181,13 +181,35 @@ async function handleFileDelete(themeUuid, themePath, filePath) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
function printWatchStatus(previewUrl) {
|
|
184
|
+
export function printWatchStatus(previewUrl, editorUrl) {
|
|
185
185
|
const sep = chalk.dim(' · ');
|
|
186
186
|
const parts = [chalk.cyan('👀 ' + t('dev.status_watching'))];
|
|
187
187
|
if (previewUrl) parts.push(chalk.bold.green('P') + chalk.gray(' ' + t('dev.status_preview')));
|
|
188
|
+
if (editorUrl) parts.push(chalk.bold.green('E') + chalk.gray(' ' + t('dev.status_editor')));
|
|
188
189
|
parts.push(chalk.bold.green('S') + chalk.gray(' ' + t('dev.status_section')));
|
|
189
190
|
parts.push(chalk.gray('Ctrl+C ' + t('dev.status_exit')));
|
|
190
191
|
console.log('\n' + parts.join(sep) + '\n');
|
|
192
|
+
|
|
193
|
+
if (previewUrl) console.log(` ${chalk.cyan.underline(previewUrl)}`);
|
|
194
|
+
if (editorUrl) console.log(` ${chalk.cyan.underline(editorUrl)}`);
|
|
195
|
+
if (previewUrl || editorUrl) console.log('');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function handleWatchKey(key, { previewUrl, editorUrl }) {
|
|
199
|
+
if (previewUrl && (key === 'p' || key === 'P')) {
|
|
200
|
+
try {
|
|
201
|
+
await open(previewUrl);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.log(chalk.red(' ' + t('dev.preview_open_failed', { error: e.message })));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (editorUrl && (key === 'e' || key === 'E')) {
|
|
207
|
+
try {
|
|
208
|
+
await open(editorUrl);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.log(chalk.red(' ' + t('dev.editor_open_failed', { error: e.message })));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
191
213
|
}
|
|
192
214
|
|
|
193
215
|
async function startFileWatcher(themeUuid, themePath) {
|
|
@@ -201,7 +223,15 @@ async function startFileWatcher(themeUuid, themePath) {
|
|
|
201
223
|
// sessiz — preview opsiyonel
|
|
202
224
|
}
|
|
203
225
|
|
|
204
|
-
|
|
226
|
+
let editorUrl = null;
|
|
227
|
+
try {
|
|
228
|
+
const apiClient = await createApiClient();
|
|
229
|
+
const editorRes = await apiClient.get(`/theme/${themeUuid}/editor-url`);
|
|
230
|
+
editorUrl = editorRes?.data?.editor_url || null;
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
printWatchStatus(previewUrl, editorUrl);
|
|
205
235
|
|
|
206
236
|
// Menu acikken hem watcher event'leri hem keypress dinleyici askiya alinir
|
|
207
237
|
// (inquirer ile stdin cakismasini onler).
|
|
@@ -230,13 +260,7 @@ async function startFileWatcher(themeUuid, themePath) {
|
|
|
230
260
|
if (paused) {
|
|
231
261
|
return;
|
|
232
262
|
}
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
await open(previewUrl);
|
|
236
|
-
} catch (e) {
|
|
237
|
-
console.log(chalk.red(' ' + t('dev.preview_open_failed', { error: e.message })));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
263
|
+
await handleWatchKey(key, { previewUrl, editorUrl });
|
|
240
264
|
if (key === 's' || key === 'S') {
|
|
241
265
|
await openSectionMenu();
|
|
242
266
|
}
|
|
@@ -297,7 +321,7 @@ async function startFileWatcher(themeUuid, themePath) {
|
|
|
297
321
|
setRawMode(true);
|
|
298
322
|
process.stdin.resume();
|
|
299
323
|
console.log(chalk.green('\n' + t('dev.section_menu_resumed')));
|
|
300
|
-
printWatchStatus(previewUrl);
|
|
324
|
+
printWatchStatus(previewUrl, editorUrl);
|
|
301
325
|
paused = false;
|
|
302
326
|
}
|
|
303
327
|
|
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/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
|
/**
|