tsoft-cli 2.6.15 → 3.4.0
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 +160 -0
- package/LICENSE +21 -0
- package/README.md +86 -53
- package/locales/en.json +323 -0
- package/locales/tr.json +323 -0
- package/package.json +41 -45
- package/src/api-client.js +217 -0
- package/src/commands/login.js +94 -0
- package/src/commands/logout.js +44 -0
- package/src/commands/org-switch.js +82 -0
- package/src/commands/theme-dev.js +424 -0
- package/src/commands/theme-init.js +162 -0
- package/src/commands/theme-publish.js +12 -0
- package/src/commands/theme-push.js +197 -0
- package/src/commands/theme-section.js +317 -0
- package/src/commands/theme-submit.js +14 -0
- package/src/commands/theme.js +474 -0
- package/src/commands/whoami.js +132 -0
- package/src/config.js +19 -0
- package/src/errors/error-mapper.js +158 -0
- package/src/i18n.js +84 -0
- package/src/index.js +179 -0
- package/src/output-mode.js +45 -0
- package/src/pkce.js +34 -0
- package/src/server.js +209 -0
- package/src/storage.js +488 -0
- package/src/ui/help-renderer.js +219 -0
- package/src/ui/prompt-wrapper.js +183 -0
- package/bin/run.js +0 -5
- package/build/enums.d.ts +0 -1
- package/build/enums.js +0 -7
- package/build/enums.js.map +0 -1
- package/build/index.d.ts +0 -5
- package/build/index.js +0 -10
- package/build/index.js.map +0 -1
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
|
+
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { TSOFT_COLORS, createTSoftGradient } from './prompt-wrapper.js';
|
|
4
|
+
import { humanOut, isJsonMode, jsonOut } from '../output-mode.js';
|
|
5
|
+
import { t } from '../i18n.js';
|
|
6
|
+
import { loadTokens, getActiveStore, getActiveTheme } from '../storage.js';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read version from package.json
|
|
12
|
+
*/
|
|
13
|
+
function getVersion() {
|
|
14
|
+
try {
|
|
15
|
+
const pkg = require('../../package.json');
|
|
16
|
+
return pkg.version || '0.0.0';
|
|
17
|
+
} catch {
|
|
18
|
+
return '0.0.0';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hardcoded ASCII art for "TSOFT" — compact 5-line design
|
|
24
|
+
*/
|
|
25
|
+
const TSOFT_ASCII_ART = `
|
|
26
|
+
████████╗███████╗ ██████╗ ███████╗████████╗
|
|
27
|
+
██╔══╝██╔════╝██╔═══██╗██╔════╝╚══██╔══╝
|
|
28
|
+
██║ ███████╗██║ ██║█████╗ ██║
|
|
29
|
+
██║ ╚════██║██║ ██║██╔══╝ ██║
|
|
30
|
+
██║ ███████║╚██████╔╝██║ ██║
|
|
31
|
+
╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝
|
|
32
|
+
`.trim();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Command groups definition — maps group key to command entries.
|
|
36
|
+
* Each entry has a nameKey (locale) and command path for display.
|
|
37
|
+
*/
|
|
38
|
+
const COMMAND_GROUPS = [
|
|
39
|
+
{
|
|
40
|
+
groupKey: 'help.group.development',
|
|
41
|
+
commands: [
|
|
42
|
+
{ display: 'theme dev', descKey: 'help.cmd.theme_dev' },
|
|
43
|
+
{ display: 'theme push', descKey: 'help.cmd.theme_push' },
|
|
44
|
+
{ display: 'theme section list', descKey: 'help.cmd.theme_section_list' },
|
|
45
|
+
{ display: 'theme section add', descKey: 'help.cmd.theme_section_add' },
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
groupKey: 'help.group.theme_management',
|
|
50
|
+
commands: [
|
|
51
|
+
{ display: 'theme list', descKey: 'help.cmd.theme_list' },
|
|
52
|
+
{ display: 'theme create', descKey: 'help.cmd.theme_create' },
|
|
53
|
+
{ display: 'theme pull', descKey: 'help.cmd.theme_pull' },
|
|
54
|
+
{ display: 'theme use', descKey: 'help.cmd.theme_use' },
|
|
55
|
+
{ display: 'theme init', descKey: 'help.cmd.theme_init' },
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
groupKey: 'help.group.account',
|
|
60
|
+
commands: [
|
|
61
|
+
{ display: 'login', descKey: 'help.cmd.login' },
|
|
62
|
+
{ display: 'logout', descKey: 'help.cmd.logout' },
|
|
63
|
+
{ display: 'whoami', descKey: 'help.cmd.whoami' },
|
|
64
|
+
{ display: 'org switch', descKey: 'help.cmd.org_switch' },
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Compute the max display name length across all command groups
|
|
71
|
+
* for consistent column alignment.
|
|
72
|
+
*/
|
|
73
|
+
function computeMaxNameWidth() {
|
|
74
|
+
let max = 0;
|
|
75
|
+
for (const group of COMMAND_GROUPS) {
|
|
76
|
+
for (const cmd of group.commands) {
|
|
77
|
+
if (cmd.display.length > max) max = cmd.display.length;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return max;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Render a single command group block.
|
|
85
|
+
* @param {object} group
|
|
86
|
+
* @param {number} maxWidth - max command name width for column alignment
|
|
87
|
+
* @returns {string[]} lines
|
|
88
|
+
*/
|
|
89
|
+
function renderGroup(group, maxWidth) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
const groupName = t(group.groupKey);
|
|
92
|
+
lines.push(' ' + chalk.bold.white(groupName));
|
|
93
|
+
lines.push('');
|
|
94
|
+
for (const cmd of group.commands) {
|
|
95
|
+
const padding = ' '.repeat(maxWidth - cmd.display.length + 2);
|
|
96
|
+
const namePart = chalk.hex(TSOFT_COLORS.primary)('tsoft ' + cmd.display);
|
|
97
|
+
const descPart = chalk.dim(t(cmd.descKey));
|
|
98
|
+
lines.push(' ' + namePart + padding + descPart);
|
|
99
|
+
}
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Read contextual status data (logged-in user, store, active theme).
|
|
105
|
+
* Returns all nulls gracefully on any error.
|
|
106
|
+
* @returns {Promise<{store: string|null, tokens: object|null, activeTheme: string|null}>}
|
|
107
|
+
*/
|
|
108
|
+
async function loadStatusData() {
|
|
109
|
+
try {
|
|
110
|
+
const store = await getActiveStore();
|
|
111
|
+
const tokens = store ? await loadTokens(store) : null;
|
|
112
|
+
const activeTheme = await getActiveTheme();
|
|
113
|
+
return { store, tokens, activeTheme };
|
|
114
|
+
} catch {
|
|
115
|
+
return { store: null, tokens: null, activeTheme: null };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build status block lines from context data.
|
|
121
|
+
* @param {{store: string|null, tokens: object|null, activeTheme: string|null}} status
|
|
122
|
+
* @returns {string[]} lines to print
|
|
123
|
+
*/
|
|
124
|
+
function buildStatusLines(status) {
|
|
125
|
+
const { store, tokens, activeTheme } = status;
|
|
126
|
+
const lines = [];
|
|
127
|
+
const orangeBullet = chalk.hex(TSOFT_COLORS.primary)('●');
|
|
128
|
+
|
|
129
|
+
// User line
|
|
130
|
+
if (tokens?.user?.email) {
|
|
131
|
+
const userText = t('help.status.logged_in', { email: tokens.user.email, store: store || '' });
|
|
132
|
+
lines.push(' ' + orangeBullet + ' ' + userText);
|
|
133
|
+
} else {
|
|
134
|
+
lines.push(' ' + chalk.dim('○ ' + t('help.status.not_logged_in')));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Organization line — only shown when available
|
|
138
|
+
const orgName = tokens?.user?.organizationName;
|
|
139
|
+
if (orgName) {
|
|
140
|
+
lines.push(' ' + orangeBullet + ' ' + orgName);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Theme line
|
|
144
|
+
if (activeTheme) {
|
|
145
|
+
const themeText = t('help.status.active_theme', { theme: activeTheme });
|
|
146
|
+
lines.push(' ' + orangeBullet + ' ' + themeText);
|
|
147
|
+
} else {
|
|
148
|
+
lines.push(' ' + chalk.dim('○ ' + t('help.status.no_theme')));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Render the branded help to stdout.
|
|
156
|
+
* Wraps all human-readable output in humanOut().
|
|
157
|
+
* In JSON mode, outputs structured data instead.
|
|
158
|
+
*
|
|
159
|
+
* @param {import('commander').Command} _program — kept for future use / extensibility
|
|
160
|
+
*/
|
|
161
|
+
export async function renderBrandedHelp(_program) {
|
|
162
|
+
const status = await loadStatusData();
|
|
163
|
+
|
|
164
|
+
if (isJsonMode()) {
|
|
165
|
+
const groups = COMMAND_GROUPS.map(g => ({
|
|
166
|
+
group: t(g.groupKey),
|
|
167
|
+
commands: g.commands.map(c => ({
|
|
168
|
+
command: 'tsoft ' + c.display,
|
|
169
|
+
description: t(c.descKey)
|
|
170
|
+
}))
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
const commands = groups.flatMap(g => g.commands);
|
|
174
|
+
jsonOut({
|
|
175
|
+
user: status.tokens?.user || null,
|
|
176
|
+
store: status.store,
|
|
177
|
+
activeTheme: status.activeTheme,
|
|
178
|
+
commands,
|
|
179
|
+
groups,
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
humanOut(() => {
|
|
185
|
+
const version = getVersion();
|
|
186
|
+
const maxWidth = computeMaxNameWidth();
|
|
187
|
+
|
|
188
|
+
// Header — ASCII art with orange gradient
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(createTSoftGradient(TSOFT_ASCII_ART));
|
|
191
|
+
console.log('');
|
|
192
|
+
|
|
193
|
+
// Tagline + version
|
|
194
|
+
const tagline = chalk.dim(t('help.tagline'));
|
|
195
|
+
const versionBadge = chalk.dim('v' + version);
|
|
196
|
+
console.log(' ' + tagline + ' ' + versionBadge);
|
|
197
|
+
|
|
198
|
+
// Contextual status block
|
|
199
|
+
console.log('');
|
|
200
|
+
const statusLines = buildStatusLines(status);
|
|
201
|
+
for (const line of statusLines) {
|
|
202
|
+
console.log(line);
|
|
203
|
+
}
|
|
204
|
+
console.log('');
|
|
205
|
+
|
|
206
|
+
// Command groups
|
|
207
|
+
for (const group of COMMAND_GROUPS) {
|
|
208
|
+
const groupLines = renderGroup(group, maxWidth);
|
|
209
|
+
for (const line of groupLines) {
|
|
210
|
+
console.log(line);
|
|
211
|
+
}
|
|
212
|
+
console.log('');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Footer hint
|
|
216
|
+
console.log(' ' + chalk.dim(t('help.footer')));
|
|
217
|
+
console.log('');
|
|
218
|
+
});
|
|
219
|
+
}
|