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.
@@ -0,0 +1,94 @@
1
+ import open from 'open';
2
+ import chalk from 'chalk';
3
+ import {generateCodeVerifier, generateCodeChallenge} from '../pkce.js';
4
+ import {startCallbackServer} from '../server.js';
5
+ import {exchangeToken, saveTokens} from '../storage.js';
6
+ import * as config from '../config.js';
7
+ import { isJsonMode, jsonOut } from '../output-mode.js';
8
+ import { t } from '../i18n.js';
9
+
10
+ /**
11
+ * Login komutu - OAuth2 Authorization Code Flow with PKCE
12
+ */
13
+ export async function loginCommand() {
14
+ if (isJsonMode()) {
15
+ jsonOut({
16
+ status: 'error',
17
+ command: 'login',
18
+ error: {
19
+ message: t('login.json_error.message'),
20
+ hint: t('login.json_error.hint')
21
+ }
22
+ });
23
+ process.exit(1);
24
+ }
25
+
26
+ try {
27
+ console.log(chalk.cyan(t('login.starting') + '\n'));
28
+
29
+ // 1. PKCE parametrelerini oluştur
30
+ const codeVerifier = generateCodeVerifier();
31
+ const codeChallenge = generateCodeChallenge(codeVerifier);
32
+ const state = generateCodeVerifier(); // Random state for CSRF protection
33
+
34
+ // 2. Authorization URL'i oluştur
35
+ const authUrl = new URL(config.getAuthorizationUrl());
36
+ authUrl.searchParams.set('client_id', config.CLIENT_ID);
37
+ authUrl.searchParams.set('redirect_uri', config.REDIRECT_URI);
38
+ authUrl.searchParams.set('response_type', 'code');
39
+ authUrl.searchParams.set('scope', config.SCOPE);
40
+ authUrl.searchParams.set('state', state);
41
+ authUrl.searchParams.set('code_challenge', codeChallenge);
42
+ authUrl.searchParams.set('code_challenge_method', 'S256');
43
+ authUrl.searchParams.set('prompt', 'select_store');
44
+
45
+ // 3. Callback server'ı başlat
46
+ const serverPromise = startCallbackServer(config.CALLBACK_PORT);
47
+
48
+ // 4. Browser'ı aç
49
+ console.log(chalk.cyan(t('login.browser_opening')));
50
+ console.log(chalk.gray(` ${authUrl.toString().substring(0, 80)}...`));
51
+ console.log();
52
+
53
+ await open(authUrl.toString());
54
+
55
+ console.log(chalk.yellow(t('login.waiting')));
56
+ console.log(chalk.gray(' ' + t('login.waiting_detail') + '\n'));
57
+
58
+ // 5. Callback'i bekle
59
+ const {code, state: returnedState, store} = await serverPromise;
60
+
61
+ // 6. State doğrula (CSRF koruması)
62
+ if (state !== returnedState) {
63
+ throw new Error(t('login.security_error'));
64
+ }
65
+
66
+ console.log(chalk.green(t('login.code_received')));
67
+ console.log(chalk.cyan(`✓ Store: ${store}\n`));
68
+
69
+ // 7. Token exchange
70
+ console.log(chalk.yellow(t('login.token_fetching')));
71
+ const tokens = await exchangeToken(code, codeVerifier, store, returnedState);
72
+
73
+ console.log(chalk.green(t('login.token_received')));
74
+
75
+ // 8. Token'ları kaydet
76
+ await saveTokens(tokens, store);
77
+
78
+ console.log(chalk.green(t('login.credentials_saved') + '\n'));
79
+
80
+ // Başarı mesajı
81
+ console.log(chalk.bold.green(t('login.success')));
82
+ console.log(chalk.gray(` Store: ${store}`));
83
+ console.log(chalk.gray(` Kullanıcı: ${tokens.user.name} ${tokens.user.surname}`));
84
+ console.log(chalk.gray(` Email: ${tokens.user.email}`));
85
+ console.log(chalk.gray(` Config: .tsoft/${store.replace(/\./g, '-').toLowerCase()}/\n`));
86
+
87
+ console.log(chalk.gray('💡 İpucu: ') + chalk.white(t('login.tip').replace('💡 ', '').replace('Tip: ', '') + '\n'));
88
+
89
+ } catch (error) {
90
+ console.error(chalk.red('\n❌ Hata: ') + error.message);
91
+ console.error(chalk.gray('\n' + t('login.error_log')));
92
+ process.exit(1);
93
+ }
94
+ }
@@ -0,0 +1,44 @@
1
+ import chalk from 'chalk';
2
+ import { loadTokens, revokeToken, deleteConfig } from '../storage.js';
3
+ import { t } from '../i18n.js';
4
+
5
+ /**
6
+ * Logout komutu - Token'ı revoke eder ve local config'i siler
7
+ */
8
+ export async function logoutCommand() {
9
+ try {
10
+ console.log(chalk.cyan(t('logout.starting') + '\n'));
11
+
12
+ // Mevcut token'ları yükle
13
+ const tokens = await loadTokens();
14
+
15
+ if (!tokens) {
16
+ console.log(chalk.yellow(t('logout.not_logged_in')));
17
+ return;
18
+ }
19
+
20
+ console.log(chalk.cyan(`Store: ${tokens.store}\n`));
21
+
22
+ // Token'ı revoke et
23
+ if (tokens.accessToken && tokens.store) {
24
+ console.log(chalk.yellow(t('logout.revoking')));
25
+ try {
26
+ await revokeToken(tokens.accessToken, tokens.store);
27
+ console.log(chalk.green(t('logout.revoked')));
28
+ } catch (error) {
29
+ console.log(chalk.yellow(t('logout.revoke_failed')));
30
+ }
31
+ }
32
+
33
+ // Local config'i sil
34
+ await deleteConfig(tokens.store);
35
+ console.log(chalk.green(t('logout.credentials_cleared') + '\n'));
36
+
37
+ console.log(chalk.bold.green(t('logout.success')));
38
+ console.log(chalk.gray(' ' + t('logout.relogin_tip') + '\n'));
39
+
40
+ } catch (error) {
41
+ console.error(chalk.red('\n❌ Hata: ') + error.message);
42
+ process.exit(1);
43
+ }
44
+ }
@@ -0,0 +1,82 @@
1
+ import chalk from 'chalk';
2
+ import axios from 'axios';
3
+ import { loadTokens, ensureValidToken, getActiveStore } from '../storage.js';
4
+ import { brandedSelect, showSuccess, showError, showInfo } from '../ui/prompt-wrapper.js';
5
+ import { isJsonMode, jsonOut } from '../output-mode.js';
6
+ import { mapError } from '../errors/error-mapper.js';
7
+ import { t } from '../i18n.js';
8
+
9
+ /**
10
+ * Org switch komutu - Organizasyon seçimi
11
+ * GET /api/v3/public/organization/developer/orgs -> org listesi
12
+ * POST /api/v3/public/organization/developer/select-org -> org seçimi
13
+ */
14
+ export async function orgSwitchCommand() {
15
+ if (isJsonMode()) {
16
+ jsonOut({ status: 'error', command: 'org switch', error: { message: t('org.switch.json_error.message'), hint: t('org.switch.json_error.hint') } });
17
+ process.exit(1);
18
+ }
19
+
20
+ try {
21
+ const store = await getActiveStore();
22
+ if (!store) {
23
+ showError(t('org.switch.no_store'));
24
+ return;
25
+ }
26
+
27
+ let tokens = await loadTokens(store);
28
+ if (!tokens) {
29
+ showError(t('org.switch.no_token'));
30
+ return;
31
+ }
32
+
33
+ tokens = await ensureValidToken(tokens);
34
+
35
+ const baseURL = `https://${store}/api/v3/public/organization/developer`;
36
+ const headers = {
37
+ 'Authorization': `Bearer ${tokens.accessToken}`,
38
+ 'Accept': 'application/json',
39
+ };
40
+
41
+ // 1. Org listesini getir
42
+ console.log(chalk.yellow(t('org.switch.loading')));
43
+
44
+ const orgsResponse = await axios.get(`${baseURL}/orgs`, { headers });
45
+ const organizations = orgsResponse.data?.data || orgsResponse.data || [];
46
+
47
+ if (!Array.isArray(organizations) || organizations.length === 0) {
48
+ showInfo(t('org.switch.no_orgs'));
49
+ return;
50
+ }
51
+
52
+ // 2. Org seçimi
53
+ const choices = organizations.map(org => ({
54
+ name: `${org.name}${org.is_system ? chalk.cyan(' [System]') : ''} ${chalk.gray('#' + org.id)}`,
55
+ value: org,
56
+ }));
57
+
58
+ const selectedOrg = await brandedSelect({
59
+ message: t('org.switch.select_prompt'),
60
+ choices,
61
+ }, t('org.switch.select_title'));
62
+
63
+ // 3. Seçimi kaydet
64
+ console.log(chalk.yellow('\n' + t('org.switch.selecting', { name: selectedOrg.name })));
65
+
66
+ await axios.post(`${baseURL}/select-org`, {
67
+ organization_id: selectedOrg.id,
68
+ }, { headers });
69
+
70
+ showSuccess(t('org.switch.success', { name: selectedOrg.name, id: selectedOrg.id }));
71
+
72
+ } catch (error) {
73
+ if (isJsonMode()) {
74
+ const mapped = mapError(error, { command: 'org switch' });
75
+ jsonOut({ status: 'error', command: 'org switch', error: { message: mapped.message, hint: mapped.hint } });
76
+ process.exit(1);
77
+ }
78
+ const message = error.response?.data?.message || error.message;
79
+ showError(t('org.switch.error', { error: message }));
80
+ process.exit(1);
81
+ }
82
+ }
@@ -0,0 +1,424 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import {createWriteStream} from 'fs';
4
+ import path from 'path';
5
+ import AdmZip from 'adm-zip';
6
+ import chokidar from 'chokidar';
7
+ import {createApiClient} from '../api-client.js';
8
+ import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActiveStore} from '../storage.js';
9
+ import {brandedConfirm, brandedSelect, showWarning, showError, showInfo} from '../ui/prompt-wrapper.js';
10
+ import { t } from '../i18n.js';
11
+
12
+ async function getThemeUuid(themeSlug) {
13
+ const apiClient = await createApiClient();
14
+ const response = await apiClient.get('/theme');
15
+ const themes = response.data || [];
16
+
17
+ const theme = themes.find(t => slugify(t.name) === themeSlug);
18
+
19
+ if (!theme) {
20
+ throw new Error(`Tema bulunamadı: ${themeSlug}`);
21
+ }
22
+
23
+ return {
24
+ uuid: theme.theme_folder, name: theme.name, version: theme.version
25
+ };
26
+ }
27
+
28
+ async function checkLockStatus(themeUuid) {
29
+ const apiClient = await createApiClient();
30
+ return apiClient.post(`/theme/${themeUuid}/check-lock`, {});
31
+ }
32
+
33
+ async function acquireLock(themeUuid) {
34
+ const apiClient = await createApiClient();
35
+ return await apiClient.post(`/theme/${themeUuid}/acquire-lock`, {});
36
+ }
37
+
38
+ async function pullAndSync(themeUuid, themePath) {
39
+ const apiClient = await createApiClient();
40
+
41
+ console.log(chalk.yellow(t('dev.sync_pulling')));
42
+ const response = await apiClient.download(`/theme/${themeUuid}/download`, {});
43
+
44
+ await fs.mkdir(themePath, {recursive: true});
45
+ const zipPath = path.join(themePath, 'theme.zip');
46
+ const writer = createWriteStream(zipPath);
47
+
48
+ response.data.pipe(writer);
49
+
50
+ await new Promise((resolve, reject) => {
51
+ writer.on('finish', resolve);
52
+ writer.on('error', reject);
53
+ });
54
+
55
+ const zip = new AdmZip(zipPath);
56
+ zip.extractAllTo(themePath, true);
57
+ await fs.unlink(zipPath);
58
+
59
+ console.log(chalk.green(t('dev.sync_updated')));
60
+
61
+ console.log(chalk.yellow(t('dev.sync_marking')));
62
+
63
+ try {
64
+ await apiClient.post(`/theme/${themeUuid}/mark-synced`, {});
65
+ console.log(chalk.green(t('dev.sync_done') + '\n'));
66
+ } catch (error) {
67
+ if (error.message.startsWith('LOCK_STOLEN:')) {
68
+ throw error;
69
+ } else if (error.message.includes('lock owner') || error.message.includes('locked')) {
70
+ throw new Error('LOCK_LOST');
71
+ }
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ async function handleFileChange(themeUuid, themePath, filePath) {
77
+ try {
78
+ const relativePath = path.relative(themePath, filePath);
79
+ const extension = path.extname(filePath).slice(1);
80
+
81
+ const allowedExtensions = ['theme', 'twig', 'css', 'js', 'md', 'json', 'svg', 'jpeg', 'jpg', 'png', 'gif', 'webp'];
82
+
83
+ if (!allowedExtensions.includes(extension)) {
84
+ return;
85
+ }
86
+
87
+ const content = await fs.readFile(filePath, 'utf-8');
88
+ const apiClient = await createApiClient();
89
+
90
+ const startTime = Date.now();
91
+
92
+ await apiClient.post(`/theme/${themeUuid}/file-manager?q=save`, {
93
+ path: relativePath, content: content
94
+ });
95
+
96
+ const duration = Date.now() - startTime;
97
+ console.log(chalk.green(`[${new Date().toLocaleTimeString()}] ` + t('dev.file_saved', { path: relativePath, duration })));
98
+
99
+ try {
100
+ await apiClient.post(`/theme/${themeUuid}/mark-synced`, {});
101
+ } catch (ackError) {
102
+ }
103
+
104
+ } catch (error) {
105
+ const relativePath = path.relative(themePath, filePath);
106
+
107
+ if (error.message.startsWith('LOCK_STOLEN:')) {
108
+ const lockOwner = error.message.split(':')[1];
109
+ console.log(chalk.red('\n' + t('dev.lock_stolen')));
110
+ console.log(chalk.yellow(' ' + t('dev.lock_stolen_owner', { owner: lockOwner })));
111
+ console.log(chalk.gray(' ' + t('dev.lock_stolen_stopping') + '\n'));
112
+ process.exit(1);
113
+ } else if (error.message === 'LOCK_LOST' || error.message.includes('lock owner') || error.message.includes('locked by another')) {
114
+ console.log(chalk.red('\n' + t('dev.lock_lost')));
115
+ console.log(chalk.yellow(' ' + t('dev.lock_stolen_stopping') + '\n'));
116
+ process.exit(1);
117
+ } else if (error.message.includes('not synced') || error.message.includes('developerSynced')) {
118
+ console.log(chalk.red('\n' + t('dev.not_synced_pulling') + '\n'));
119
+
120
+ try {
121
+ await pullAndSync(themeUuid, themePath);
122
+ await handleFileChange(themeUuid, themePath, filePath);
123
+ } catch (pullError) {
124
+ if (pullError.message.startsWith('LOCK_STOLEN:')) {
125
+ const lockOwner = pullError.message.split(':')[1];
126
+ console.log(chalk.red('\n' + t('dev.lock_stolen')));
127
+ console.log(chalk.yellow(' ' + t('dev.lock_stolen_owner', { owner: lockOwner })));
128
+ console.log(chalk.gray(' ' + t('dev.lock_stolen_stopping') + '\n'));
129
+ process.exit(1);
130
+ } else if (pullError.message === 'LOCK_LOST') {
131
+ console.log(chalk.red('\n' + t('dev.lock_lost')));
132
+ console.log(chalk.yellow(' ' + t('dev.lock_stolen_stopping') + '\n'));
133
+ process.exit(1);
134
+ }
135
+ throw pullError;
136
+ }
137
+ } else {
138
+ console.log(chalk.red(`[${new Date().toLocaleTimeString()}] ` + t('dev.file_error', { path: relativePath, error: error.message })));
139
+ }
140
+ }
141
+ }
142
+
143
+ async function handleFileDelete(themeUuid, themePath, filePath) {
144
+ try {
145
+ const relativePath = path.relative(themePath, filePath);
146
+ const apiClient = await createApiClient();
147
+
148
+ await apiClient.post(`/theme/${themeUuid}/file-manager?q=delete`, {
149
+ items: [{
150
+ path: relativePath, type: 'file'
151
+ }]
152
+ });
153
+
154
+ console.log(chalk.yellow(`[${new Date().toLocaleTimeString()}] ` + t('dev.file_deleted', { path: relativePath })));
155
+
156
+ try {
157
+ await apiClient.post(`/theme/${themeUuid}/mark-synced`, {});
158
+ } catch (ackError) {
159
+ }
160
+
161
+ } catch (error) {
162
+ const relativePath = path.relative(themePath, filePath);
163
+ console.log(chalk.red(`[${new Date().toLocaleTimeString()}] ` + t('dev.file_delete_failed', { path: relativePath, error: error.message })));
164
+ }
165
+ }
166
+
167
+ async function startFileWatcher(themeUuid, themePath) {
168
+ console.log(chalk.cyan(t('dev.watching') + '\n'));
169
+ console.log(chalk.gray(t('dev.env_running')));
170
+ console.log(chalk.gray(' ' + t('dev.exit_tip') + '\n'));
171
+
172
+ const watcher = chokidar.watch(themePath, {
173
+ ignored: /(^|[\/\\])\../, persistent: true, ignoreInitial: true, awaitWriteFinish: {
174
+ stabilityThreshold: 300, pollInterval: 100
175
+ }
176
+ });
177
+
178
+ watcher
179
+ .on('change', async (filePath) => {
180
+ await handleFileChange(themeUuid, themePath, filePath);
181
+ })
182
+ .on('add', async (filePath) => {
183
+ await handleFileChange(themeUuid, themePath, filePath);
184
+ })
185
+ .on('unlink', async (filePath) => {
186
+ await handleFileDelete(themeUuid, themePath, filePath);
187
+ });
188
+
189
+ return watcher;
190
+ }
191
+
192
+ function startKeepAlive(themeUuid) {
193
+ return setInterval(async () => {
194
+ try {
195
+ const apiClient = await createApiClient();
196
+ await apiClient.post(`/theme/${themeUuid}/mark-synced`, {});
197
+ console.log(chalk.gray(`[${new Date().toLocaleTimeString()}] 🔄 Keep-alive (mark-synced)`));
198
+ } catch (error) {
199
+ console.log(chalk.gray(`[${new Date().toLocaleTimeString()}] ` + t('dev.keepalive_error')));
200
+ }
201
+ }, 30000);
202
+ }
203
+
204
+ function setupGracefulShutdown(watcher, keepAliveInterval) {
205
+ process.on('SIGINT', async () => {
206
+ console.log(chalk.yellow('\n\n' + t('dev.stopping') + '\n'));
207
+
208
+ if (watcher) {
209
+ await watcher.close();
210
+ console.log(chalk.green(t('dev.watch_stopped')));
211
+ }
212
+
213
+ if (keepAliveInterval) {
214
+ clearInterval(keepAliveInterval);
215
+ console.log(chalk.green(t('dev.keepalive_stopped')));
216
+ }
217
+
218
+ console.log(chalk.gray(' ' + t('dev.lock_kept')));
219
+
220
+ console.log(chalk.green('\n' + t('dev.exit_success') + '\n'));
221
+ process.exit(0);
222
+ });
223
+ }
224
+
225
+ export async function themeDevCommand(themeName) {
226
+ try {
227
+ console.log(chalk.cyan(t('dev.starting') + '\n'));
228
+
229
+ const store = await getActiveStore();
230
+ const storeSlug = normalizeStoreDomain(store);
231
+
232
+ let themeSlug = themeName;
233
+ let themeInfo;
234
+
235
+ if (!themeSlug) {
236
+ themeSlug = await getActiveTheme();
237
+ }
238
+
239
+ if (themeSlug && !themeName) {
240
+ console.log(chalk.cyan(t('dev.active_site')) + ' ' + chalk.white(store));
241
+
242
+ const apiClient = await createApiClient();
243
+ const tempThemeInfo = await getThemeUuid(themeSlug);
244
+ console.log(chalk.cyan(t('dev.active_theme')) + ' ' + chalk.white(`${themeSlug} (${tempThemeInfo.name} v${tempThemeInfo.version})`));
245
+ console.log();
246
+
247
+ const continueWithActive = await brandedConfirm({
248
+ message: t('dev.continue_prompt'),
249
+ default: true
250
+ }, t('dev.continue_title'));
251
+
252
+ if (!continueWithActive) {
253
+ const response = await apiClient.get('/theme');
254
+ const themes = response.data || [];
255
+
256
+ if (themes.length === 0) {
257
+ showWarning(t('dev.no_themes'));
258
+ return;
259
+ }
260
+
261
+ console.log();
262
+ const themeChoices = themes.map(theme => ({
263
+ name: `${theme.name} ${chalk.gray(`v${theme.version}`)} ${theme.active ? chalk.green('(Aktif)') : ''}`,
264
+ value: theme,
265
+ description: theme.description || `Klasör: ${theme.theme_folder}`
266
+ }));
267
+
268
+ const selectedTheme = await brandedSelect({
269
+ message: t('dev.select_theme_prompt'), choices: themeChoices
270
+ }, t('dev.select_theme_title'));
271
+
272
+ themeSlug = slugify(selectedTheme.name);
273
+ themeInfo = {
274
+ uuid: selectedTheme.theme_folder, name: selectedTheme.name, version: selectedTheme.version
275
+ };
276
+
277
+ await setActiveTheme(themeSlug);
278
+ console.log();
279
+ } else {
280
+ themeInfo = tempThemeInfo;
281
+ }
282
+ }
283
+
284
+ if (!themeSlug) {
285
+ const apiClient = await createApiClient();
286
+ const response = await apiClient.get('/theme');
287
+ const themes = response.data || [];
288
+
289
+ if (themes.length === 0) {
290
+ showWarning(t('dev.no_themes'));
291
+ return;
292
+ }
293
+
294
+ const themeChoices = themes.map(theme => ({
295
+ name: `${theme.name} ${chalk.gray(`v${theme.version}`)}`, value: theme
296
+ }));
297
+
298
+ const selectedTheme = await brandedSelect({
299
+ message: t('dev.select_theme_prompt'), choices: themeChoices
300
+ }, t('dev.select_theme_title'));
301
+
302
+ themeSlug = slugify(selectedTheme.name);
303
+ themeInfo = {
304
+ uuid: selectedTheme.theme_folder, name: selectedTheme.name, version: selectedTheme.version
305
+ };
306
+
307
+ await setActiveTheme(themeSlug);
308
+ } else if (!themeInfo) {
309
+ themeInfo = await getThemeUuid(themeSlug);
310
+ }
311
+
312
+ console.log(chalk.green(t('dev.active_theme_set', { slug: themeSlug })));
313
+
314
+ let themePath = path.join(process.cwd(), storeSlug, themeSlug);
315
+ console.log(chalk.green(t('dev.working_dir', { path: `${storeSlug}/${themeSlug}/` }) + '\n'));
316
+
317
+ // Fork check: foreign-org themes need fork before development
318
+ {
319
+ const apiClient = await createApiClient();
320
+ const orgResponse = await apiClient.get('/theme');
321
+ const currentTheme = (orgResponse.data || []).find(t => t.theme_folder === themeInfo.uuid);
322
+ const userOrg = orgResponse.user_organization;
323
+
324
+ if (currentTheme && currentTheme.organization_id != null && userOrg && !userOrg.is_system && currentTheme.organization_id !== userOrg.id) {
325
+ const forkConfirm = await brandedConfirm({
326
+ message: t('dev.fork_warning', {
327
+ name: currentTheme.name,
328
+ version: currentTheme.version,
329
+ org_id: currentTheme.organization_id,
330
+ user_org: userOrg.name
331
+ }),
332
+ default: true
333
+ }, t('dev.fork_warning_title'));
334
+
335
+ if (!forkConfirm) {
336
+ showInfo(t('dev.fork_cancelled'));
337
+ return;
338
+ }
339
+
340
+ console.log(chalk.yellow(t('dev.forking')));
341
+ const forkResult = await apiClient.forkTheme(themeInfo.uuid);
342
+ const forkedData = forkResult.data;
343
+
344
+ themeInfo = { uuid: forkedData.uuid, name: forkedData.name, version: forkedData.version };
345
+ themeSlug = slugify(forkedData.name);
346
+ themePath = path.join(process.cwd(), storeSlug, themeSlug);
347
+
348
+ console.log(chalk.green(t('dev.fork_success', { name: forkedData.name })));
349
+ console.log(chalk.gray(' ' + t('dev.fork_source', { source: forkedData.source.name, version: forkedData.source.version })));
350
+ console.log();
351
+
352
+ // Check if forked theme directory already exists
353
+ const forkedExists = await fs.access(themePath).then(() => true).catch(() => false);
354
+ if (forkedExists) {
355
+ const overwrite = await brandedConfirm({
356
+ message: t('dev.dir_exists_prompt', { path: themePath }),
357
+ default: false
358
+ }, t('dev.dir_exists_title'));
359
+
360
+ if (!overwrite) {
361
+ showWarning(t('dev.fork_cancelled'));
362
+ return;
363
+ }
364
+ await fs.rm(themePath, { recursive: true, force: true });
365
+ }
366
+ }
367
+ }
368
+
369
+ const exists = await fs.access(themePath).then(() => true).catch(() => false);
370
+ if (!exists) {
371
+ showError(t('dev.theme_not_found', { path: themePath }));
372
+ return;
373
+ }
374
+
375
+ console.log(chalk.yellow(t('dev.checking_lock')));
376
+ const lockStatus = await checkLockStatus(themeInfo.uuid);
377
+
378
+ if (!lockStatus.locked) {
379
+ console.log(chalk.green(t('dev.not_locked') + '\n'));
380
+ console.log(chalk.yellow(t('dev.acquiring_lock')));
381
+ await acquireLock(themeInfo.uuid);
382
+ console.log(chalk.green(t('dev.lock_acquired') + '\n'));
383
+
384
+ await pullAndSync(themeInfo.uuid, themePath);
385
+
386
+ } else if (lockStatus.isCurrentDeveloper) {
387
+ if (lockStatus.developerSynced) {
388
+ console.log(chalk.green(t('dev.already_locked')));
389
+ console.log(chalk.green(t('dev.already_synced') + '\n'));
390
+ } else {
391
+ console.log(chalk.yellow(t('dev.not_synced') + '\n'));
392
+ await pullAndSync(themeInfo.uuid, themePath);
393
+ }
394
+ } else {
395
+ const lockMessage = t('dev.lock_by_other', {
396
+ dev: lockStatus.developerUsername,
397
+ last_access: lockStatus.developerLastAccessedAt
398
+ });
399
+
400
+ const override = await brandedConfirm({
401
+ message: lockMessage, default: false
402
+ }, t('dev.lock_warning_title'));
403
+
404
+ if (!override) {
405
+ showInfo(t('dev.fork_cancelled'));
406
+ return;
407
+ }
408
+
409
+ console.log(chalk.yellow('\n' + t('dev.lock_override')));
410
+ await acquireLock(themeInfo.uuid);
411
+ console.log(chalk.green(t('dev.lock_acquired') + '\n'));
412
+
413
+ await pullAndSync(themeInfo.uuid, themePath);
414
+ }
415
+
416
+ const watcher = await startFileWatcher(themeInfo.uuid, themePath);
417
+ const keepAliveInterval = startKeepAlive(themeInfo.uuid);
418
+ setupGracefulShutdown(watcher, keepAliveInterval);
419
+
420
+ } catch (error) {
421
+ console.error(chalk.red('\n❌ Hata: ') + error.message);
422
+ process.exit(1);
423
+ }
424
+ }