tsoft-cli 2.6.15 → 3.4.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/src/server.js ADDED
@@ -0,0 +1,216 @@
1
+ import http from 'http';
2
+ import { URL } from 'url';
3
+
4
+ /**
5
+ * OAuth2 callback'i yakalamak için geçici HTTP server başlatır
6
+ * @param {number} port - Dinlenecek port
7
+ * @returns {Promise<{code: string, state: string, store: string}>}
8
+ */
9
+ export function startCallbackServer(port) {
10
+ return new Promise((resolve, reject) => {
11
+ const server = http.createServer((req, res) => {
12
+ const url = new URL(req.url, `http://localhost:${port}`);
13
+
14
+ if (url.pathname === '/callback') {
15
+ const code = url.searchParams.get('code');
16
+ const state = url.searchParams.get('state');
17
+ const store = url.searchParams.get('store');
18
+ const error = url.searchParams.get('error');
19
+ const errorDescription = url.searchParams.get('error_description');
20
+
21
+ if (error) {
22
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
23
+ res.end(errorHtml(error, errorDescription));
24
+ server.close();
25
+ reject(new Error(`OAuth2 Error: ${error} - ${errorDescription || 'No description'}`));
26
+ return;
27
+ }
28
+
29
+ if (!code || !state || !store) {
30
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
31
+ res.end(errorHtml('invalid_request', 'code, state veya store parametresi eksik'));
32
+ server.close();
33
+ reject(new Error('Eksik parametreler: code, state veya store bulunamadı'));
34
+ return;
35
+ }
36
+
37
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
38
+ res.end(successHtml());
39
+ server.close();
40
+ resolve({ code, state, store });
41
+ } else {
42
+ res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
43
+ res.end('<h1>404 - Sayfa Bulunamadı</h1>');
44
+ }
45
+ });
46
+
47
+ server.on('error', (err) => {
48
+ reject(new Error(`Server başlatılamadı: ${err.message}`));
49
+ });
50
+
51
+ server.listen(port, () => {
52
+ console.log(`✓ Callback sunucusu başlatıldı (http://localhost:${port}/callback)`);
53
+ });
54
+ });
55
+ }
56
+
57
+ function successHtml() {
58
+ return `
59
+ <!DOCTYPE html>
60
+ <html lang="tr">
61
+ <head>
62
+ <meta charset="UTF-8">
63
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
64
+ <title>Giriş Başarılı - tsoft CLI</title>
65
+ <style>
66
+ body {
67
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
68
+ display: flex;
69
+ justify-content: center;
70
+ align-items: center;
71
+ min-height: 100vh;
72
+ margin: 0;
73
+ background: #fff;
74
+ }
75
+ .container {
76
+ background: #fff;
77
+ padding: 3rem;
78
+ border-radius: 1rem;
79
+ border: 1px solid #e5e7eb;
80
+ text-align: center;
81
+ max-width: 400px;
82
+ }
83
+ .success-icon {
84
+ width: 64px;
85
+ height: 64px;
86
+ margin: 0 auto 1.5rem;
87
+ background: #10b981;
88
+ border-radius: 50%;
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ }
93
+ .success-icon svg {
94
+ width: 32px;
95
+ height: 32px;
96
+ }
97
+ h1 {
98
+ color: #1f2937;
99
+ margin: 0 0 0.75rem;
100
+ font-size: 1.5rem;
101
+ font-weight: 600;
102
+ }
103
+ p {
104
+ color: #6b7280;
105
+ margin: 0;
106
+ font-size: 0.938rem;
107
+ line-height: 1.6;
108
+ }
109
+ .note {
110
+ margin-top: 2rem;
111
+ padding: 0.875rem 1.25rem;
112
+ background: #f9fafb;
113
+ border-radius: 0.5rem;
114
+ font-size: 0.875rem;
115
+ color: #4b5563;
116
+ }
117
+ </style>
118
+ </head>
119
+ <body>
120
+ <div class="container">
121
+ <div class="success-icon"><svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></div>
122
+ <h1>Giriş Başarılı!</h1>
123
+ <p>Yetkilendirme işlemi tamamlandı.</p>
124
+ <p>Artık bu pencereyi kapatabilirsiniz.</p>
125
+ <div class="note">
126
+ Terminal'e dönün ve işleme devam edin.
127
+ </div>
128
+ </div>
129
+ <script>
130
+ setTimeout(() => window.close(), 3000);
131
+ </script>
132
+ </body>
133
+ </html>
134
+ `;
135
+ }
136
+
137
+ function errorHtml(error, description) {
138
+ return `
139
+ <!DOCTYPE html>
140
+ <html lang="tr">
141
+ <head>
142
+ <meta charset="UTF-8">
143
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
144
+ <title>Hata - tsoft CLI</title>
145
+ <style>
146
+ body {
147
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
148
+ display: flex;
149
+ justify-content: center;
150
+ align-items: center;
151
+ min-height: 100vh;
152
+ margin: 0;
153
+ background: #fff;
154
+ }
155
+ .container {
156
+ background: #fff;
157
+ padding: 3rem;
158
+ border-radius: 1rem;
159
+ border: 1px solid #e5e7eb;
160
+ text-align: center;
161
+ max-width: 400px;
162
+ }
163
+ .error-icon {
164
+ width: 64px;
165
+ height: 64px;
166
+ margin: 0 auto 1.5rem;
167
+ background: #ef4444;
168
+ border-radius: 50%;
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ }
173
+ .error-icon svg {
174
+ width: 32px;
175
+ height: 32px;
176
+ }
177
+ h1 {
178
+ color: #1f2937;
179
+ margin: 0 0 0.75rem;
180
+ font-size: 1.5rem;
181
+ font-weight: 600;
182
+ }
183
+ p {
184
+ color: #6b7280;
185
+ margin: 0.5rem 0;
186
+ font-size: 0.938rem;
187
+ line-height: 1.6;
188
+ }
189
+ .error-code {
190
+ margin-top: 1.5rem;
191
+ padding: 0.875rem 1.25rem;
192
+ background: #fef2f2;
193
+ border-radius: 0.5rem;
194
+ font-size: 0.875rem;
195
+ color: #991b1b;
196
+ font-family: monospace;
197
+ }
198
+ </style>
199
+ </head>
200
+ <body>
201
+ <div class="container">
202
+ <div class="error-icon"><svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></div>
203
+ <h1>Yetkilendirme Hatası</h1>
204
+ <p>${description || 'Bir hata oluştu'}</p>
205
+ <div class="error-code">
206
+ Hata Kodu: ${error}
207
+ </div>
208
+ <p style="margin-top: 2rem; font-size: 0.875rem;">
209
+ Terminal'e dönün ve tekrar deneyin.
210
+ </p>
211
+ </div>
212
+ </body>
213
+ </html>
214
+ `;
215
+ }
216
+
package/src/storage.js ADDED
@@ -0,0 +1,488 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import axios from 'axios';
4
+ import * as config from './config.js';
5
+
6
+ /**
7
+ * Store domain'i dizin adına uygun formata çevirir
8
+ * @param {string} domain - Store domain (örn: birtan.1isim.com)
9
+ * @returns {string} Normalized slug (örn: birtan-1isim-com)
10
+ */
11
+ export function normalizeStoreDomain(domain) {
12
+ return domain.replace(/\./g, '-').toLowerCase();
13
+ }
14
+
15
+ /**
16
+ * Store'a özel config dizinini döner (CWD bazlı)
17
+ * @param {string} store - Store domain
18
+ * @returns {string} Config dizin path'i
19
+ */
20
+ export function getStoreConfigDir(store) {
21
+ const storeSlug = normalizeStoreDomain(store);
22
+ return path.join(process.cwd(), '.tsoft', storeSlug);
23
+ }
24
+
25
+ /**
26
+ * Store'a özel config dosya path'ini döner
27
+ * @param {string} store - Store domain
28
+ * @returns {string} Config dosya path'i
29
+ */
30
+ export function getStoreConfigFile(store) {
31
+ return path.join(getStoreConfigDir(store), 'config.json');
32
+ }
33
+
34
+ /**
35
+ * Store'a özel .env dosya path'ini döner
36
+ * @param {string} store - Store domain
37
+ * @returns {string} .env dosya path'i
38
+ */
39
+ export function getStoreEnvFile(store) {
40
+ return path.join(getStoreConfigDir(store), '.env');
41
+ }
42
+
43
+ /**
44
+ * Aktif store'u ayarlar
45
+ * @param {string} store - Store domain
46
+ * @returns {Promise<void>}
47
+ */
48
+ export async function setActiveStore(store) {
49
+ const activeStoreFile = path.join(process.cwd(), '.tsoft', 'active-store.txt');
50
+ await fs.mkdir(path.dirname(activeStoreFile), { recursive: true });
51
+ await fs.writeFile(activeStoreFile, store, 'utf-8');
52
+ }
53
+
54
+ /**
55
+ * Aktif store'u okur
56
+ * @returns {Promise<string|null>} Store domain veya null
57
+ */
58
+ export async function getActiveStore() {
59
+ try {
60
+ const activeStoreFile = path.join(process.cwd(), '.tsoft', 'active-store.txt');
61
+ return (await fs.readFile(activeStoreFile, 'utf-8')).trim();
62
+ } catch (error) {
63
+ if (error.code === 'ENOENT') {
64
+ return null;
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Authorization code'u access token'a çevirir
72
+ * @param {string} code - Authorization code
73
+ * @param {string} codeVerifier - PKCE code verifier
74
+ * @param {string} store - Store domain (callback'ten dönen store parametresi)
75
+ * @param {string} state - State (CSRF protection)
76
+ * @returns {Promise<object>} Token bilgileri
77
+ */
78
+ export async function exchangeToken(code, codeVerifier, store, state) {
79
+ const tokenUrl = config.getTokenUrl(store);
80
+
81
+ try {
82
+ // Laravel Passport application/x-www-form-urlencoded formatında bekler
83
+ const params = new URLSearchParams();
84
+ params.append('grant_type', 'authorization_code');
85
+ params.append('client_id', config.CLIENT_ID);
86
+ params.append('redirect_uri', config.REDIRECT_URI);
87
+ params.append('code', code);
88
+ params.append('code_verifier', codeVerifier);
89
+ params.append('state', state);
90
+
91
+ const response = await axios.post(tokenUrl, params, {
92
+ headers: {
93
+ 'Content-Type': 'application/x-www-form-urlencoded',
94
+ 'Accept': 'application/json',
95
+ },
96
+ });
97
+
98
+ // Backend response: { status, message, data: { token, user } }
99
+ const responseData = response.data.data;
100
+
101
+ return {
102
+ accessToken: responseData.token.accessToken,
103
+ accessTokenExpiresAt: responseData.token.accessTokenExpiresAt,
104
+ refreshToken: responseData.token.refreshToken,
105
+ refreshTokenExpiresAt: responseData.token.refreshTokenExpiresAt,
106
+ tokenType: 'Bearer',
107
+ user: responseData.user,
108
+ store: store,
109
+ };
110
+ } catch (error) {
111
+ if (error.response?.data) {
112
+ console.error('Token exchange hatası:', error.response.data);
113
+ }
114
+ if (error.response) {
115
+ throw new Error(
116
+ `Token exchange hatası: ${error.response.status} - ${
117
+ error.response.data.error_description ||
118
+ error.response.data.message ||
119
+ 'Bilinmeyen hata'
120
+ }`
121
+ );
122
+ }
123
+ throw new Error(`Token exchange hatası: ${error.message}`);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Refresh token ile yeni access token alır
129
+ * @param {string} refreshToken - Refresh token
130
+ * @param {string} store - Store domain
131
+ * @returns {Promise<object>} Yeni token bilgileri
132
+ */
133
+ export async function refreshAccessToken(refreshToken, store) {
134
+ const refreshUrl = config.getRefreshUrl(store);
135
+
136
+ try {
137
+ // Laravel Passport application/x-www-form-urlencoded formatında bekler
138
+ const params = new URLSearchParams();
139
+ params.append('grant_type', 'refresh_token');
140
+ params.append('refresh_token', refreshToken);
141
+ params.append('client_id', config.CLIENT_ID);
142
+
143
+ const response = await axios.post(refreshUrl, params, {
144
+ headers: {
145
+ 'Content-Type': 'application/x-www-form-urlencoded',
146
+ 'Accept': 'application/json',
147
+ },
148
+ });
149
+
150
+ // Backend response: { status, message, data: { token, user } }
151
+ const responseData = response.data.data;
152
+
153
+ return {
154
+ accessToken: responseData.token.accessToken,
155
+ accessTokenExpiresAt: responseData.token.accessTokenExpiresAt,
156
+ refreshToken: responseData.token.refreshToken,
157
+ refreshTokenExpiresAt: responseData.token.refreshTokenExpiresAt,
158
+ tokenType: 'Bearer',
159
+ user: responseData.user,
160
+ };
161
+ } catch (error) {
162
+ if (error.response?.data) {
163
+ console.error('Token refresh hatası:', error.response.data);
164
+ }
165
+ if (error.response) {
166
+ throw new Error(
167
+ `Token refresh hatası: ${error.response.status} - ${
168
+ error.response.data.error_description ||
169
+ error.response.data.message ||
170
+ 'Bilinmeyen hata'
171
+ }`
172
+ );
173
+ }
174
+ throw new Error(`Token refresh hatası: ${error.message}`);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Token'ı revoke eder
180
+ * @param {string} token - Access veya refresh token
181
+ * @param {string} store - Store domain
182
+ * @returns {Promise<void>}
183
+ */
184
+ export async function revokeToken(token, store) {
185
+ const revokeUrl = config.getRevokeUrl(store);
186
+
187
+ try {
188
+ // Laravel Passport application/x-www-form-urlencoded formatında bekler
189
+ const params = new URLSearchParams();
190
+ params.append('token', token);
191
+ params.append('client_id', config.CLIENT_ID);
192
+
193
+ await axios.post(revokeUrl, params, {
194
+ headers: {
195
+ 'Content-Type': 'application/x-www-form-urlencoded',
196
+ 'Accept': 'application/json',
197
+ },
198
+ });
199
+ } catch (error) {
200
+ // Revoke hatalarını sessizce yoksay (token zaten geçersiz olabilir)
201
+ console.warn('Token revoke uyarısı:', error.message);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * .env dosyası oluşturur
207
+ * @param {object} tokens - Token ve user bilgileri
208
+ * @param {string} store - Store domain
209
+ * @returns {Promise<void>}
210
+ */
211
+ async function writeEnvFile(tokens, store) {
212
+ const envContent = `# tsoft CLI - Auto-generated credentials
213
+ # Store: ${store}
214
+ # Generated: ${new Date().toISOString()}
215
+
216
+ TSOFT_ACCESS_TOKEN="${tokens.accessToken}"
217
+ TSOFT_REFRESH_TOKEN="${tokens.refreshToken}"
218
+ TSOFT_USER_ID=${tokens.user.id}
219
+ TSOFT_USER_NAME="${tokens.user.name}"
220
+ TSOFT_USER_SURNAME="${tokens.user.surname}"
221
+ TSOFT_USER_EMAIL="${tokens.user.email}"
222
+ TSOFT_STORE_DOMAIN="${store}"
223
+ TSOFT_USER_VERSION="${tokens.user.version}"
224
+ `;
225
+
226
+ const envFile = getStoreEnvFile(store);
227
+ await fs.writeFile(envFile, envContent, 'utf-8');
228
+ await fs.chmod(envFile, 0o600); // rw-------
229
+ }
230
+
231
+ /**
232
+ * Token'ları local config dosyasına kaydeder
233
+ * @param {object} tokens - Token bilgileri
234
+ * @param {string} store - Store domain
235
+ * @returns {Promise<void>}
236
+ */
237
+ export async function saveTokens(tokens, store) {
238
+ const configDir = getStoreConfigDir(store);
239
+ await fs.mkdir(configDir, { recursive: true });
240
+
241
+ const configData = {
242
+ accessToken: tokens.accessToken,
243
+ accessTokenExpiresAt: tokens.accessTokenExpiresAt,
244
+ refreshToken: tokens.refreshToken,
245
+ refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
246
+ tokenType: tokens.tokenType,
247
+ store: store,
248
+ user: {
249
+ id: tokens.user.id,
250
+ name: tokens.user.name,
251
+ surname: tokens.user.surname,
252
+ email: tokens.user.email,
253
+ hasTwoFactor: tokens.user.hasTwoFactor,
254
+ hasRestrictedAccess: tokens.user.hasRestrictedAccess,
255
+ version: tokens.user.version,
256
+ },
257
+ updatedAt: new Date().toISOString(),
258
+ };
259
+
260
+ const configFile = getStoreConfigFile(store);
261
+ await fs.writeFile(configFile, JSON.stringify(configData, null, 2), 'utf-8');
262
+ await fs.chmod(configFile, 0o600);
263
+
264
+ // .env dosyasını da oluştur
265
+ await writeEnvFile(tokens, store);
266
+
267
+ // Aktif store'u ayarla
268
+ await setActiveStore(store);
269
+ }
270
+
271
+ /**
272
+ * Local config dosyasından token'ları yükler
273
+ * @param {string} [store] - Store domain (opsiyonel, belirtilmezse active store kullanılır)
274
+ * @returns {Promise<object|null>} Token bilgileri veya null
275
+ */
276
+ export async function loadTokens(store = null) {
277
+ if (!store) {
278
+ store = await getActiveStore();
279
+ if (!store) {
280
+ return null; // Aktif store yok
281
+ }
282
+ }
283
+
284
+ try {
285
+ const configFile = getStoreConfigFile(store);
286
+ const data = await fs.readFile(configFile, 'utf-8');
287
+ return JSON.parse(data);
288
+ } catch (error) {
289
+ if (error.code === 'ENOENT') {
290
+ return null; // Dosya yok
291
+ }
292
+ throw error;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Token'ların geçerliliğini kontrol eder
298
+ * @param {object} tokens - Token bilgileri
299
+ * @returns {boolean} Token geçerli mi?
300
+ */
301
+ export function isTokenValid(tokens) {
302
+ if (!tokens || !tokens.accessToken || !tokens.accessTokenExpiresAt) {
303
+ return false;
304
+ }
305
+
306
+ const expiresAt = new Date(tokens.accessTokenExpiresAt);
307
+ const now = new Date();
308
+
309
+ // 5 dakika önce expire olacaksa yenile
310
+ const bufferTime = 5 * 60 * 1000; // 5 dakika
311
+ return expiresAt.getTime() - now.getTime() > bufferTime;
312
+ }
313
+
314
+ /**
315
+ * Refresh token'ın geçerliliğini kontrol eder
316
+ * @param {object} tokens - Token bilgileri
317
+ * @returns {boolean} Refresh token geçerli mi?
318
+ */
319
+ export function isRefreshTokenValid(tokens) {
320
+ if (!tokens || !tokens.refreshToken || !tokens.refreshTokenExpiresAt) {
321
+ return false;
322
+ }
323
+
324
+ const expiresAt = new Date(tokens.refreshTokenExpiresAt);
325
+ const now = new Date();
326
+
327
+ return expiresAt.getTime() > now.getTime();
328
+ }
329
+
330
+ /**
331
+ * Access token geçerliliğini kontrol eder, gerekirse refresh eder
332
+ * @param {object} tokens - Token bilgileri
333
+ * @returns {Promise<object>} Güncel token bilgileri
334
+ * @throws {Error} RELOGIN_REQUIRED - Refresh token da expire olduysa
335
+ */
336
+ export async function ensureValidToken(tokens) {
337
+ if (!tokens) {
338
+ throw new Error('RELOGIN_REQUIRED');
339
+ }
340
+
341
+ // Access token geçerliyse direkt dön
342
+ if (isTokenValid(tokens)) {
343
+ return tokens;
344
+ }
345
+
346
+ // Refresh token geçerliyse refresh et
347
+ if (isRefreshTokenValid(tokens)) {
348
+ const newTokens = await refreshAccessToken(tokens.refreshToken, tokens.store);
349
+ await saveTokens(newTokens, tokens.store);
350
+ return await loadTokens(tokens.store);
351
+ }
352
+
353
+ // Her ikisi de geçersizse, otomatik re-login gerekiyor
354
+ throw new Error('RELOGIN_REQUIRED');
355
+ }
356
+
357
+ /**
358
+ * Store'un config dosyalarını siler (logout için)
359
+ * @param {string} [store] - Store domain (opsiyonel, belirtilmezse active store kullanılır)
360
+ * @returns {Promise<void>}
361
+ */
362
+ export async function deleteConfig(store = null) {
363
+ if (!store) {
364
+ store = await getActiveStore();
365
+ if (!store) {
366
+ return; // Aktif store yok, bir şey yapma
367
+ }
368
+ }
369
+
370
+ try {
371
+ const configDir = getStoreConfigDir(store);
372
+ await fs.rm(configDir, { recursive: true, force: true });
373
+
374
+ // Active store dosyasını da sil
375
+ const activeStoreFile = path.join(process.cwd(), '.tsoft', 'active-store.txt');
376
+ await fs.unlink(activeStoreFile).catch(() => {}); // Hata varsa sessizce yoksay
377
+ } catch (error) {
378
+ if (error.code !== 'ENOENT') {
379
+ throw error;
380
+ }
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Token'ın ne kadar süre geçerli olduğunu döner
386
+ * @param {object} tokens - Token bilgileri
387
+ * @returns {string} Kalan süre (human-readable)
388
+ */
389
+ export function getTokenExpiry(tokens) {
390
+ if (!tokens || !tokens.accessTokenExpiresAt) {
391
+ return 'Bilinmiyor';
392
+ }
393
+
394
+ const expiresAt = new Date(tokens.accessTokenExpiresAt);
395
+ const now = new Date();
396
+ const diff = expiresAt.getTime() - now.getTime();
397
+
398
+ if (diff <= 0) {
399
+ return 'Süresi dolmuş';
400
+ }
401
+
402
+ const hours = Math.floor(diff / (1000 * 60 * 60));
403
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
404
+
405
+ if (hours > 0) {
406
+ return `${hours} saat ${minutes} dakika`;
407
+ }
408
+ return `${minutes} dakika`;
409
+ }
410
+
411
+ /**
412
+ * Türkçe karakterleri URL dostu hale getirir
413
+ * @param {string} text - Dönüştürülecek metin
414
+ * @returns {string} Sluglaştırılmış metin
415
+ */
416
+ export function slugify(text) {
417
+ const trMap = {
418
+ 'ç': 'c', 'Ç': 'c',
419
+ 'ğ': 'g', 'Ğ': 'g',
420
+ 'ı': 'i', 'İ': 'i',
421
+ 'ö': 'o', 'Ö': 'o',
422
+ 'ş': 's', 'Ş': 's',
423
+ 'ü': 'u', 'Ü': 'u',
424
+ };
425
+
426
+ return text
427
+ .split('')
428
+ .map(char => trMap[char] || char)
429
+ .join('')
430
+ .toLowerCase()
431
+ .trim()
432
+ .replace(/[^\w\s-]/g, '')
433
+ .replace(/[\s_-]+/g, '-')
434
+ .replace(/^-+|-+$/g, '');
435
+ }
436
+
437
+ /**
438
+ * Aktif temayı ayarlar
439
+ * @param {string} theme - Tema adı
440
+ * @returns {Promise<void>}
441
+ */
442
+ export async function setActiveTheme(theme) {
443
+ const activeThemeFile = path.join(process.cwd(), '.tsoft', 'active-theme.txt');
444
+ await fs.mkdir(path.dirname(activeThemeFile), { recursive: true });
445
+ await fs.writeFile(activeThemeFile, theme, 'utf-8');
446
+ }
447
+
448
+ /**
449
+ * Aktif temayı okur
450
+ * @returns {Promise<string|null>} Tema adı veya null
451
+ */
452
+ export async function getActiveTheme() {
453
+ try {
454
+ const activeThemeFile = path.join(process.cwd(), '.tsoft', 'active-theme.txt');
455
+ return (await fs.readFile(activeThemeFile, 'utf-8')).trim();
456
+ } catch (error) {
457
+ if (error.code === 'ENOENT') {
458
+ return null;
459
+ }
460
+ throw error;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Tema slug'ından UUID/folder adını bulur
466
+ * @param {string} themeSlug - Tema slug (örn: 'my-theme')
467
+ * @returns {Promise<Object>} {uuid, name, version, active}
468
+ */
469
+ export async function getThemeUuidBySlug(themeSlug) {
470
+ const { createApiClient } = await import('./api-client.js');
471
+ const apiClient = await createApiClient();
472
+ const response = await apiClient.get('/theme');
473
+ const themes = response.data || [];
474
+
475
+ const theme = themes.find(t => slugify(t.name) === themeSlug);
476
+
477
+ if (!theme) {
478
+ throw new Error(`Tema bulunamadı: ${themeSlug}`);
479
+ }
480
+
481
+ return {
482
+ uuid: theme.theme_folder,
483
+ name: theme.name,
484
+ version: theme.version,
485
+ active: theme.active
486
+ };
487
+ }
488
+