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/CHANGELOG.md +166 -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 +216 -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
|
@@ -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
|
+
}
|