tsoft-cli 3.7.3 → 3.9.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 +47 -0
- package/locales/en.json +29 -1
- package/locales/tr.json +33 -5
- package/package.json +4 -1
- package/src/api-client.js +11 -9
- package/src/commands/theme-dev.js +109 -22
- package/src/commands/theme-push.js +17 -6
- package/src/commands/theme-section.js +231 -4
- package/src/index.js +19 -2
- package/src/ui/prompt-wrapper.js +35 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,53 @@ All notable changes to T-Soft CLI will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.9.0] - 2026-05-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Interactive `tsoft theme section` menu: running `theme section` with no sub-command now opens a four-way picker (add a ready-made section, create an empty section, fork from an existing section, list sections).
|
|
12
|
+
- `tsoft theme section create` — scaffolds an empty section file inside an existing block folder (`section/{block}/{name}.twig`) in the active theme via the new `POST /theme/{uuid}/sections` endpoint, then refreshes local files.
|
|
13
|
+
- `tsoft theme section fork` — forks an existing section into the active theme under a new name.
|
|
14
|
+
- Searchable pickers: block and section selection prompts (`add`, `create`, `fork`) now filter as you type, matching both the display name and the folder slug.
|
|
15
|
+
- `theme dev` now prints a distinct `section updated` line when a file under `section/` is saved, so section changes are visible during live development.
|
|
16
|
+
- `theme dev` interactive `S` keypress opens the section menu without leaving the watch session; the watcher and keypress listener are paused while the menu is open and the local theme is re-synced afterwards.
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
- Dead `ApiClient.syncTheme()` method — the server now resyncs the section registry automatically on file save, so the explicit sync call is no longer needed.
|
|
20
|
+
|
|
21
|
+
### Backwards compatibility
|
|
22
|
+
- `theme section add` and `theme section list` remain available as direct sub-commands for scripts and CI.
|
|
23
|
+
|
|
24
|
+
## [3.8.0] - 2026-04-28
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Vitest unit tests for `ApiClient` 401 handling: successful refresh-and-retry path, relogin error after a second 401, relogin error when the refresh token is invalid, relogin error when the refresh call itself fails, and POST request retry parity (`tests/api-client.test.js`).
|
|
28
|
+
- Integration tests for the OAuth callback HTTP server (`tests/server.test.js`): success path returns the expected `{ code, state, store }` payload, OAuth `error` query rejects, missing required params reject, unknown paths return 404 without resolving the promise, and an already-bound port surfaces a clear startup error.
|
|
29
|
+
- Tests for `loginCommand` edge cases (`tests/login.test.js`): refusal in JSON mode, state mismatch (CSRF guard), browser open failure, and token exchange failure — all of which exit with code 1.
|
|
30
|
+
- Tests for `themeInitCommand` early-return paths (`tests/theme-init.test.js`): unknown slug under JSON and human modes, empty themes list, and missing-slug + JSON combination.
|
|
31
|
+
- Tests for `themePushCommand` JSON-mode contract (`tests/theme-push.test.js`): no active theme exits 1, success payload shape (`version`, `bump_type`, `action`, `changes`, `partner_panel_url`), partner URL fallback when `/organization/` segment is missing, `no_op` payload on `no_changes`, and error payload on push failure.
|
|
32
|
+
- Tests for `logoutCommand` safety guarantee (`tests/logout.test.js`): quiet exit when no tokens are stored; **local config is cleared even if the revoke call fails**; happy-path revoke + clear ordering.
|
|
33
|
+
- Tests for `orgSwitchCommand` early-return paths (`tests/org-switch.test.js`): JSON mode refusal (exit 1), no active store, no stored tokens, empty organisation list — none of which should call the select-org endpoint.
|
|
34
|
+
- Tests for `whoamiCommand` (`tests/whoami.test.js`): exit 1 in JSON mode when no tokens, quiet return in human mode, and the structured success payload shape (`user`, `store`, `access_token_valid`, `refresh_token_valid`, `updated_at`).
|
|
35
|
+
- Tests for `themeSectionListCommand` and `themeSectionAddCommand` (`tests/theme-section.test.js`): no active theme warning, empty blocks list, blocks with no available default sections, and graceful cancellation when the user declines the confirmation prompt (no `addSection` call).
|
|
36
|
+
- Tests for `themeDevCommand` control-flow guards (`tests/theme-dev.test.js`): empty themes list during the interactive picker, an unresolvable explicit slug exits 1, missing local theme directory shows an error and returns, and a theme locked by another developer respects user's decline of the override prompt (no `acquire-lock` call).
|
|
37
|
+
|
|
38
|
+
### Internal
|
|
39
|
+
- `vitest` added as `devDependency` (was previously referenced by the `test` script but not installed by `npm install`).
|
|
40
|
+
- Test suite grew from 64 specs (7 files) to **102 specs (16 files)**; coverage now spans `api-client`, `server`, and every command in `src/commands/` (`loginCommand`, `logoutCommand`, `whoamiCommand`, `orgSwitchCommand`, `themeInitCommand`, `themeDevCommand`, `themePushCommand`, `themeSection*`) in addition to the existing helper-module suites.
|
|
41
|
+
|
|
42
|
+
## [3.7.4] - 2026-04-20
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- `tsoft theme push` no longer prints a malformed partner panel link on servers or themes where the organization scope cannot be resolved. Previously the CLI would render `https://partners.tsoft360.com/themes/<theme-folder>` — a URL that looks valid but does not exist in the partner panel. The CLI now accepts the backend-provided `partner_panel_url` only when it contains a proper `/organization/<id>/themes/<folder>` segment; otherwise it falls back to the themes index page (`https://partners.tsoft360.com/themes`) and adjusts the hint text accordingly.
|
|
46
|
+
- Restored Turkish diacritics in `push.connecting`, `push.no_changes`, `push.admin_approval`, `push.use_tip` (e.g. `baglaniliyor` → `bağlanılıyor`, `onayindan` → `onayından`).
|
|
47
|
+
- Leading indentation is no longer part of the `chalk.cyan.underline` span, so the partner panel URL no longer shows phantom underscores before `https` on terminals that render whitespace underlines.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- New locale key `push.partner_hint_theme` ("View this theme in the partner panel." / "Temanı partner panelinde görüntüle.") used when a theme-scoped URL is available, keeping the generic `push.partner_hint` for index-page fallbacks.
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
- JSON-mode output (`--json`) emits the same sanitized `partner_panel_url` as the human-readable output, so machine consumers never receive the malformed link either.
|
|
54
|
+
|
|
8
55
|
## [3.7.3] - 2026-04-20
|
|
9
56
|
|
|
10
57
|
### Added
|
package/locales/en.json
CHANGED
|
@@ -143,6 +143,7 @@
|
|
|
143
143
|
"push.draft_created": "New draft created",
|
|
144
144
|
"push.draft_updated": "Existing draft updated",
|
|
145
145
|
"push.partner_hint": "Visit the partner panel for the theme list.",
|
|
146
|
+
"push.partner_hint_theme": "View this theme in the partner panel.",
|
|
146
147
|
"push.changes_added": "+{{count}} new",
|
|
147
148
|
"push.changes_removed": "~{{count}} removed",
|
|
148
149
|
"dev.starting": "🚀 Starting development environment...",
|
|
@@ -265,6 +266,29 @@
|
|
|
265
266
|
"section.add.download_error": "⚠️ Error while updating theme files:",
|
|
266
267
|
"section.add.manual_tip": "💡 Try downloading the theme manually:",
|
|
267
268
|
"section.add.manual_command": "tsoft theme pull",
|
|
269
|
+
"section.menu.title": "🧩 Section Operations",
|
|
270
|
+
"section.menu.prompt": "What would you like to do?",
|
|
271
|
+
"section.menu.add": "Add a ready-made section",
|
|
272
|
+
"section.menu.create": "Create an empty section",
|
|
273
|
+
"section.menu.fork": "Fork from a section",
|
|
274
|
+
"section.menu.list": "List sections",
|
|
275
|
+
"section.create.title": "✨ Create Empty Section",
|
|
276
|
+
"section.create.no_blocks": "No block available to add a section to.",
|
|
277
|
+
"section.create.block_prompt": "Which block should the section be added to?",
|
|
278
|
+
"section.create.name_prompt": "Section name (.twig is added automatically):",
|
|
279
|
+
"section.create.name_required": "Section name cannot be empty.",
|
|
280
|
+
"section.create.name_invalid": "Section name may only contain lowercase letters, digits and hyphens.",
|
|
281
|
+
"section.create.name_no_extension": "Do not type an extension — \".twig\" is added automatically.",
|
|
282
|
+
"section.create.confirm_prompt": "File \"{{file}}\" will be created in theme \"{{theme}}\".\n\nDo you want to continue?",
|
|
283
|
+
"section.create.created": "\"{{file}}\" created.",
|
|
284
|
+
"section.create.failed": "Failed to create section: {{error}}",
|
|
285
|
+
"section.fork.title": "🌱 Fork From Section",
|
|
286
|
+
"section.fork.no_sources": "No source section found to fork from.",
|
|
287
|
+
"section.fork.source_prompt": "Which section would you like to fork from?",
|
|
288
|
+
"section.fork.name_prompt": "New section name (.twig is added automatically):",
|
|
289
|
+
"section.fork.confirm_prompt": "\"{{file}}\" will be forked from \"{{source}}\" and added to theme \"{{theme}}\".\n\nDo you want to continue?",
|
|
290
|
+
"section.fork.created": "\"{{file}}\" created.",
|
|
291
|
+
"section.fork.next_step": "Open \"{{file}}\" to edit its content.",
|
|
268
292
|
"org.switch.json_error.message": "This command requires interactive mode.",
|
|
269
293
|
"org.switch.json_error.hint": "org switch is not supported in JSON mode.",
|
|
270
294
|
"org.switch.no_store": "No active store found. Please run \"tsoft login\" first.",
|
|
@@ -306,5 +330,9 @@
|
|
|
306
330
|
"dev.status_watching": "Watching",
|
|
307
331
|
"dev.status_preview": "preview",
|
|
308
332
|
"dev.status_exit": "exit",
|
|
309
|
-
"dev.preview_open_failed": "Couldn't open browser: {{error}}"
|
|
333
|
+
"dev.preview_open_failed": "Couldn't open browser: {{error}}",
|
|
334
|
+
"dev.section_synced": "🧩 section updated: {{folder}}",
|
|
335
|
+
"dev.status_section": "section",
|
|
336
|
+
"dev.section_menu_opening": "Opening section menu...",
|
|
337
|
+
"dev.section_menu_resumed": "Resuming watch."
|
|
310
338
|
}
|
package/locales/tr.json
CHANGED
|
@@ -132,17 +132,18 @@
|
|
|
132
132
|
"theme.use.folder": "Klasör: {{folder}}",
|
|
133
133
|
"push.no_active_theme": "Aktif tema bulunamadı.",
|
|
134
134
|
"push.usage": "Kullanim: tsoft theme push <theme-name>",
|
|
135
|
-
"push.use_tip": "veya \"tsoft theme use <theme-name>\" ile aktif tema
|
|
136
|
-
"push.connecting": "Sunucuya
|
|
135
|
+
"push.use_tip": "veya \"tsoft theme use <theme-name>\" ile aktif tema ayarlayın.",
|
|
136
|
+
"push.connecting": "Sunucuya bağlanılıyor...",
|
|
137
137
|
"push.checking_status": "Mevcut durum kontrol ediliyor...",
|
|
138
138
|
"push.pushing": "Tema push ediliyor...",
|
|
139
|
-
"push.no_changes": "
|
|
139
|
+
"push.no_changes": "Değişiklik yok — mevcut draft güncel.",
|
|
140
140
|
"push.success": "Push tamamlandı",
|
|
141
141
|
"push.first_push_note": "(ilk push)",
|
|
142
|
-
"push.admin_approval": "Admin
|
|
142
|
+
"push.admin_approval": "Admin onayından sonra 1.0.0 olarak yayınlanacak",
|
|
143
143
|
"push.draft_created": "Yeni draft oluşturuldu",
|
|
144
144
|
"push.draft_updated": "Mevcut draft güncellendi",
|
|
145
145
|
"push.partner_hint": "Tema listesi için partner paneli ziyaret edin.",
|
|
146
|
+
"push.partner_hint_theme": "Temanı partner panelinde görüntüle.",
|
|
146
147
|
"push.changes_added": "+{{count}} yeni",
|
|
147
148
|
"push.changes_removed": "~{{count}} kaldırıldı",
|
|
148
149
|
"dev.starting": "🚀 Geliştirme ortamı başlatılıyor...",
|
|
@@ -265,6 +266,29 @@
|
|
|
265
266
|
"section.add.download_error": "⚠️ Tema dosyaları güncellenirken hata oluştu:",
|
|
266
267
|
"section.add.manual_tip": "💡 Manuel olarak temayı tekrar indirmeyi deneyin:",
|
|
267
268
|
"section.add.manual_command": "tsoft theme pull",
|
|
269
|
+
"section.menu.title": "🧩 Section İşlemleri",
|
|
270
|
+
"section.menu.prompt": "Ne yapmak istiyorsunuz?",
|
|
271
|
+
"section.menu.add": "Hazır section ekle",
|
|
272
|
+
"section.menu.create": "Boş section oluştur",
|
|
273
|
+
"section.menu.fork": "Section'dan türet",
|
|
274
|
+
"section.menu.list": "Section'ları listele",
|
|
275
|
+
"section.create.title": "✨ Boş Section Oluştur",
|
|
276
|
+
"section.create.no_blocks": "Section eklenebilecek block bulunamadı.",
|
|
277
|
+
"section.create.block_prompt": "Hangi block içine eklemek istiyorsunuz?",
|
|
278
|
+
"section.create.name_prompt": "Section adı (.twig otomatik eklenir):",
|
|
279
|
+
"section.create.name_required": "Section adı boş olamaz.",
|
|
280
|
+
"section.create.name_invalid": "Section adı yalnızca küçük harf, rakam ve tire içerebilir.",
|
|
281
|
+
"section.create.name_no_extension": "Uzantı yazmayın — \".twig\" otomatik eklenir.",
|
|
282
|
+
"section.create.confirm_prompt": "\"{{file}}\" dosyası \"{{theme}}\" temasında oluşturulacak.\n\nDevam etmek istiyor musunuz?",
|
|
283
|
+
"section.create.created": "\"{{file}}\" oluşturuldu.",
|
|
284
|
+
"section.create.failed": "Section oluşturulamadı: {{error}}",
|
|
285
|
+
"section.fork.title": "🌱 Section'dan Türet",
|
|
286
|
+
"section.fork.no_sources": "Türetilecek kaynak section bulunamadı.",
|
|
287
|
+
"section.fork.source_prompt": "Hangi section'dan türetmek istiyorsunuz?",
|
|
288
|
+
"section.fork.name_prompt": "Yeni section adı (.twig otomatik eklenir):",
|
|
289
|
+
"section.fork.confirm_prompt": "\"{{source}}\" section'ından \"{{file}}\" türetilecek ve \"{{theme}}\" temasına eklenecek.\n\nDevam etmek istiyor musunuz?",
|
|
290
|
+
"section.fork.created": "\"{{file}}\" oluşturuldu.",
|
|
291
|
+
"section.fork.next_step": "İçeriği düzenlemek için \"{{file}}\" dosyasını açın.",
|
|
268
292
|
"org.switch.json_error.message": "Bu komut interaktif mod gerektiriyor.",
|
|
269
293
|
"org.switch.json_error.hint": "JSON modunda org switch desteklenmiyor.",
|
|
270
294
|
"org.switch.no_store": "Aktif mağaza bulunamadı. Lütfen önce \"tsoft login\" komutunu çalıştırın.",
|
|
@@ -306,5 +330,9 @@
|
|
|
306
330
|
"dev.status_watching": "İzleniyor",
|
|
307
331
|
"dev.status_preview": "önizleme",
|
|
308
332
|
"dev.status_exit": "çıkış",
|
|
309
|
-
"dev.preview_open_failed": "Tarayıcı açılamadı: {{error}}"
|
|
333
|
+
"dev.preview_open_failed": "Tarayıcı açılamadı: {{error}}",
|
|
334
|
+
"dev.section_synced": "🧩 section güncellendi: {{folder}}",
|
|
335
|
+
"dev.status_section": "section",
|
|
336
|
+
"dev.section_menu_opening": "Section menüsü açılıyor...",
|
|
337
|
+
"dev.section_menu_resumed": "İzlemeye devam ediliyor."
|
|
310
338
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tsoft-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "Command-line tool for tsoft360 theme development and management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -52,5 +52,8 @@
|
|
|
52
52
|
"repository": {
|
|
53
53
|
"type": "git",
|
|
54
54
|
"url": "git+https://github.com/tekrom/tsoft-cli.git"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"vitest": "^4.1.5"
|
|
55
58
|
}
|
|
56
59
|
}
|
package/src/api-client.js
CHANGED
|
@@ -131,6 +131,17 @@ export class ApiClient {
|
|
|
131
131
|
return await this.post(`/theme/${themeUuid}/sections/${blockId}`, payload);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Var olan bir block içinde boş section dosyası oluşturur
|
|
136
|
+
* @param {string} themeUuid - Tema UUID/folder
|
|
137
|
+
* @param {string} blockFolder - Hedef block klasörü (ör. slider, banner)
|
|
138
|
+
* @param {string} name - Section dosya adı (slug, .twig'siz)
|
|
139
|
+
* @returns {Promise<Object>} Oluşturulan section bilgisi
|
|
140
|
+
*/
|
|
141
|
+
async createSection(themeUuid, blockFolder, name) {
|
|
142
|
+
return await this.post(`/theme/${themeUuid}/sections`, { block_folder: blockFolder, name });
|
|
143
|
+
}
|
|
144
|
+
|
|
134
145
|
/**
|
|
135
146
|
* Tema dev status'ünü getir
|
|
136
147
|
* @param {string} themeUuid - Tema UUID/folder
|
|
@@ -161,15 +172,6 @@ export class ApiClient {
|
|
|
161
172
|
return await this.post(`/theme/${themeUuid}/publish`, data);
|
|
162
173
|
}
|
|
163
174
|
|
|
164
|
-
/**
|
|
165
|
-
* Temayı sync eder (build işlemi tetiklenir)
|
|
166
|
-
* @param {string} themeUuid - Tema UUID/folder
|
|
167
|
-
* @returns {Promise<Object>} Sync sonucu
|
|
168
|
-
*/
|
|
169
|
-
async syncTheme(themeUuid) {
|
|
170
|
-
return await this.post(`/theme/${themeUuid}/sync`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
175
|
/**
|
|
174
176
|
* Temayı incelemeye gönder (admin onayı için)
|
|
175
177
|
* @param {string} themeUuid - Tema UUID/folder
|
|
@@ -8,8 +8,26 @@ import {createApiClient} from '../api-client.js';
|
|
|
8
8
|
import open from 'open';
|
|
9
9
|
import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActiveStore} from '../storage.js';
|
|
10
10
|
import {brandedConfirm, brandedSelect, showWarning, showError, showInfo} from '../ui/prompt-wrapper.js';
|
|
11
|
+
import {themeSectionMenuCommand} from './theme-section.js';
|
|
11
12
|
import { t } from '../i18n.js';
|
|
12
13
|
|
|
14
|
+
function resolveSectionFolder(relativePath) {
|
|
15
|
+
if (!relativePath) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const segments = relativePath.split(/[\\/]+/).filter(Boolean);
|
|
20
|
+
const sectionIndex = segments.indexOf('section');
|
|
21
|
+
|
|
22
|
+
if (sectionIndex === -1 || !segments[sectionIndex + 1]) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const folder = segments[sectionIndex + 1];
|
|
27
|
+
|
|
28
|
+
return folder.includes('.') ? null : folder;
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
async function fetchAllThemes() {
|
|
14
32
|
const apiClient = await createApiClient();
|
|
15
33
|
const all = [];
|
|
@@ -117,6 +135,11 @@ async function handleFileChange(themeUuid, themePath, filePath) {
|
|
|
117
135
|
const duration = Date.now() - startTime;
|
|
118
136
|
console.log(chalk.green(`[${new Date().toLocaleTimeString()}] ` + t('dev.file_saved', { path: relativePath, duration })));
|
|
119
137
|
|
|
138
|
+
const sectionFolder = resolveSectionFolder(relativePath);
|
|
139
|
+
if (sectionFolder) {
|
|
140
|
+
console.log(chalk.cyan(' ' + t('dev.section_synced', { folder: sectionFolder })));
|
|
141
|
+
}
|
|
142
|
+
|
|
120
143
|
try {
|
|
121
144
|
await apiClient.post(`/theme/${themeUuid}/mark-synced`, {});
|
|
122
145
|
} catch (ackError) {
|
|
@@ -185,6 +208,15 @@ async function handleFileDelete(themeUuid, themePath, filePath) {
|
|
|
185
208
|
}
|
|
186
209
|
}
|
|
187
210
|
|
|
211
|
+
function printWatchStatus(previewUrl) {
|
|
212
|
+
const sep = chalk.dim(' · ');
|
|
213
|
+
const parts = [chalk.cyan('👀 ' + t('dev.status_watching'))];
|
|
214
|
+
if (previewUrl) parts.push(chalk.bold.green('P') + chalk.gray(' ' + t('dev.status_preview')));
|
|
215
|
+
parts.push(chalk.bold.green('S') + chalk.gray(' ' + t('dev.status_section')));
|
|
216
|
+
parts.push(chalk.gray('Ctrl+C ' + t('dev.status_exit')));
|
|
217
|
+
console.log('\n' + parts.join(sep) + '\n');
|
|
218
|
+
}
|
|
219
|
+
|
|
188
220
|
async function startFileWatcher(themeUuid, themePath) {
|
|
189
221
|
// Preview URL — sessizce cek, baglanamazsa status line'da P ipucunu gosterme
|
|
190
222
|
let previewUrl = null;
|
|
@@ -196,15 +228,24 @@ async function startFileWatcher(themeUuid, themePath) {
|
|
|
196
228
|
// sessiz — preview opsiyonel
|
|
197
229
|
}
|
|
198
230
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
console.log('\n' + parts.join(sep) + '\n');
|
|
231
|
+
printWatchStatus(previewUrl);
|
|
232
|
+
|
|
233
|
+
// Menu acikken hem watcher event'leri hem keypress dinleyici askiya alinir
|
|
234
|
+
// (inquirer ile stdin cakismasini onler).
|
|
235
|
+
let paused = false;
|
|
205
236
|
|
|
206
|
-
|
|
207
|
-
|
|
237
|
+
const setRawMode = (enabled) => {
|
|
238
|
+
if (process.stdin.isTTY) {
|
|
239
|
+
try {
|
|
240
|
+
process.stdin.setRawMode(enabled);
|
|
241
|
+
} catch {
|
|
242
|
+
// raw mode bazi terminallerde calismaz — sessizce gec
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Klavye dinleyici — 'P' tarayıcida preview URL'ini sessizce acar, 'S' section menusu, Ctrl+C cikis
|
|
248
|
+
if (process.stdin.isTTY) {
|
|
208
249
|
try {
|
|
209
250
|
process.stdin.setRawMode(true);
|
|
210
251
|
process.stdin.resume();
|
|
@@ -213,37 +254,83 @@ async function startFileWatcher(themeUuid, themePath) {
|
|
|
213
254
|
if (key === '\u0003') { // Ctrl+C
|
|
214
255
|
process.exit(0);
|
|
215
256
|
}
|
|
216
|
-
if (
|
|
257
|
+
if (paused) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (previewUrl && (key === 'p' || key === 'P')) {
|
|
217
261
|
try {
|
|
218
262
|
await open(previewUrl);
|
|
219
263
|
} catch (e) {
|
|
220
264
|
console.log(chalk.red(' ' + t('dev.preview_open_failed', { error: e.message })));
|
|
221
265
|
}
|
|
222
266
|
}
|
|
267
|
+
if (key === 's' || key === 'S') {
|
|
268
|
+
await openSectionMenu();
|
|
269
|
+
}
|
|
223
270
|
});
|
|
224
271
|
} catch {
|
|
225
272
|
// raw mode bazi terminallerde calismaz — sessizce gec
|
|
226
273
|
}
|
|
227
274
|
}
|
|
228
275
|
|
|
229
|
-
const
|
|
276
|
+
const watcherOptions = {
|
|
230
277
|
ignored: /(^|[\/\\])\../, persistent: true, ignoreInitial: true, awaitWriteFinish: {
|
|
231
278
|
stabilityThreshold: 300, pollInterval: 100
|
|
232
279
|
}
|
|
233
|
-
}
|
|
280
|
+
};
|
|
234
281
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
282
|
+
const attachWatcherEvents = (instance) => {
|
|
283
|
+
instance
|
|
284
|
+
.on('change', async (filePath) => {
|
|
285
|
+
if (paused) return;
|
|
286
|
+
await handleFileChange(themeUuid, themePath, filePath);
|
|
287
|
+
})
|
|
288
|
+
.on('add', async (filePath) => {
|
|
289
|
+
if (paused) return;
|
|
290
|
+
await handleFileChange(themeUuid, themePath, filePath);
|
|
291
|
+
})
|
|
292
|
+
.on('unlink', async (filePath) => {
|
|
293
|
+
if (paused) return;
|
|
294
|
+
await handleFileDelete(themeUuid, themePath, filePath);
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
let watcher = chokidar.watch(themePath, watcherOptions);
|
|
299
|
+
attachWatcherEvents(watcher);
|
|
245
300
|
|
|
246
|
-
|
|
301
|
+
async function openSectionMenu() {
|
|
302
|
+
paused = true;
|
|
303
|
+
setRawMode(false);
|
|
304
|
+
|
|
305
|
+
// Menu dallari (create/fork/add) yeni section dosyalarini zip ile
|
|
306
|
+
// indirip extract eder; bu extract eski watcher'da gecikmeli 'change'
|
|
307
|
+
// olaylari uretir. Watcher'i tamamen kapatip menu+extract bittikten
|
|
308
|
+
// sonra yeniden kuruyoruz — ignoreInitial sayesinde yeni watcher
|
|
309
|
+
// extract edilmis dosyalari gormez, sadece sonraki gercek
|
|
310
|
+
// degisiklikleri yakalar.
|
|
311
|
+
await watcher.close();
|
|
312
|
+
|
|
313
|
+
console.log(chalk.cyan('\n' + t('dev.section_menu_opening') + '\n'));
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
await themeSectionMenuCommand();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.log(chalk.red(' ' + error.message));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
watcher = chokidar.watch(themePath, watcherOptions);
|
|
322
|
+
attachWatcherEvents(watcher);
|
|
323
|
+
|
|
324
|
+
setRawMode(true);
|
|
325
|
+
process.stdin.resume();
|
|
326
|
+
console.log(chalk.green('\n' + t('dev.section_menu_resumed')));
|
|
327
|
+
printWatchStatus(previewUrl);
|
|
328
|
+
paused = false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
close: () => watcher.close(),
|
|
333
|
+
};
|
|
247
334
|
}
|
|
248
335
|
|
|
249
336
|
function startKeepAlive(themeUuid) {
|
|
@@ -15,6 +15,16 @@ const CHANGE_CATEGORIES = {
|
|
|
15
15
|
'Meta': ['meta_object_added', 'meta_object_removed', 'meta_field_added', 'meta_field_removed'],
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const PARTNERS_BASE = 'https://partners.tsoft360.com';
|
|
19
|
+
const PARTNERS_THEMES_INDEX = `${PARTNERS_BASE}/themes`;
|
|
20
|
+
|
|
21
|
+
function resolvePanelLink(partnerUrl) {
|
|
22
|
+
if (typeof partnerUrl === 'string' && partnerUrl.includes('/organization/')) {
|
|
23
|
+
return { url: partnerUrl, hintKey: 'push.partner_hint_theme' };
|
|
24
|
+
}
|
|
25
|
+
return { url: PARTNERS_THEMES_INDEX, hintKey: 'push.partner_hint' };
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
/**
|
|
19
29
|
* Diff ozeti goster (gruplu)
|
|
20
30
|
* @param {Object} data - Push response data
|
|
@@ -61,11 +71,10 @@ function displayDiffSummary(data, previousVersion) {
|
|
|
61
71
|
}
|
|
62
72
|
}
|
|
63
73
|
|
|
64
|
-
// Partner panel yonlendirme
|
|
65
74
|
console.log('');
|
|
66
|
-
const panelUrl = partnerUrl
|
|
67
|
-
console.log(chalk.cyan.underline(
|
|
68
|
-
console.log(chalk.gray(' ' + t(
|
|
75
|
+
const { url: panelUrl, hintKey } = resolvePanelLink(partnerUrl);
|
|
76
|
+
console.log(` ${chalk.cyan.underline(panelUrl)}`);
|
|
77
|
+
console.log(chalk.gray(' ' + t(hintKey)));
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/**
|
|
@@ -115,6 +124,8 @@ export async function themePushCommand(themeName, options = {}) {
|
|
|
115
124
|
spinner.text = t('push.pushing');
|
|
116
125
|
const result = await apiClient.pushTheme(themeUuid);
|
|
117
126
|
|
|
127
|
+
const { url: resolvedPanelUrl } = resolvePanelLink(result.data?.partner_panel_url);
|
|
128
|
+
|
|
118
129
|
// 3. No-op kontrolu
|
|
119
130
|
if (result.data?.no_changes === true) {
|
|
120
131
|
// JSON mode
|
|
@@ -125,7 +136,7 @@ export async function themePushCommand(themeName, options = {}) {
|
|
|
125
136
|
command: 'theme push',
|
|
126
137
|
data: {
|
|
127
138
|
reason: 'no_changes',
|
|
128
|
-
partner_panel_url:
|
|
139
|
+
partner_panel_url: resolvedPanelUrl,
|
|
129
140
|
}
|
|
130
141
|
});
|
|
131
142
|
return;
|
|
@@ -148,7 +159,7 @@ export async function themePushCommand(themeName, options = {}) {
|
|
|
148
159
|
bump_type: result.data.bump_type,
|
|
149
160
|
action: result.data.action,
|
|
150
161
|
changes: result.data.changes || [],
|
|
151
|
-
partner_panel_url:
|
|
162
|
+
partner_panel_url: resolvedPanelUrl,
|
|
152
163
|
}
|
|
153
164
|
});
|
|
154
165
|
return;
|
|
@@ -6,9 +6,25 @@ import path from 'path';
|
|
|
6
6
|
import AdmZip from 'adm-zip';
|
|
7
7
|
import {createApiClient} from '../api-client.js';
|
|
8
8
|
import {getActiveTheme, getThemeUuidBySlug, normalizeStoreDomain, getActiveStore} from '../storage.js';
|
|
9
|
-
import {brandedConfirm, brandedSelect, showWarning} from '../ui/prompt-wrapper.js';
|
|
9
|
+
import {brandedConfirm, brandedSelect, brandedSearch, brandedInput, showWarning, showSuccess, showInfo} from '../ui/prompt-wrapper.js';
|
|
10
10
|
import { t } from '../i18n.js';
|
|
11
11
|
|
|
12
|
+
const SECTION_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
13
|
+
|
|
14
|
+
function validateSectionName(value) {
|
|
15
|
+
const trimmed = (value || '').trim();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
return t('section.create.name_required');
|
|
18
|
+
}
|
|
19
|
+
if (/\.[a-z0-9]+$/i.test(trimmed)) {
|
|
20
|
+
return t('section.create.name_no_extension');
|
|
21
|
+
}
|
|
22
|
+
if (!SECTION_NAME_PATTERN.test(trimmed)) {
|
|
23
|
+
return t('section.create.name_invalid');
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
export async function themeSectionListCommand() {
|
|
13
29
|
try {
|
|
14
30
|
// Aktif temayı al
|
|
@@ -109,12 +125,12 @@ export async function themeSectionAddCommand() {
|
|
|
109
125
|
|
|
110
126
|
// Block seçimi
|
|
111
127
|
const blockChoices = availableBlocks.map(block => ({
|
|
112
|
-
name: `${block.name} ${chalk.gray(`(${block.default_sections.length} section)`)}`,
|
|
128
|
+
name: `${block.name} ${chalk.gray(`(${block.folder}, ${block.default_sections.length} section)`)}`,
|
|
113
129
|
value: block,
|
|
114
|
-
|
|
130
|
+
keywords: [block.name, block.folder],
|
|
115
131
|
}));
|
|
116
132
|
|
|
117
|
-
const selectedBlock = await
|
|
133
|
+
const selectedBlock = await brandedSearch({
|
|
118
134
|
message: t('section.add.block_select_prompt'),
|
|
119
135
|
choices: blockChoices,
|
|
120
136
|
}, t('section.add.block_select_title'));
|
|
@@ -315,3 +331,214 @@ export async function themeSectionAddCommand() {
|
|
|
315
331
|
process.exit(1);
|
|
316
332
|
}
|
|
317
333
|
}
|
|
334
|
+
|
|
335
|
+
async function downloadThemeFiles(apiClient, themeUuid, themePath) {
|
|
336
|
+
const downloadResponse = await apiClient.download(`/theme/${themeUuid}/download`, {});
|
|
337
|
+
|
|
338
|
+
const zipPath = path.join(themePath, 'theme-update.zip');
|
|
339
|
+
const writer = createWriteStream(zipPath);
|
|
340
|
+
|
|
341
|
+
downloadResponse.data.pipe(writer);
|
|
342
|
+
|
|
343
|
+
await new Promise((resolve, reject) => {
|
|
344
|
+
writer.on('finish', resolve);
|
|
345
|
+
writer.on('error', reject);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const zip = new AdmZip(zipPath);
|
|
349
|
+
zip.extractAllTo(themePath, true);
|
|
350
|
+
|
|
351
|
+
await fs.unlink(zipPath);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function themeSectionCreateCommand() {
|
|
355
|
+
try {
|
|
356
|
+
const activeTheme = await getActiveTheme();
|
|
357
|
+
if (!activeTheme) {
|
|
358
|
+
showWarning(t('section.add.no_active_theme'));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const themeInfo = await getThemeUuidBySlug(activeTheme);
|
|
363
|
+
|
|
364
|
+
const apiClient = await createApiClient();
|
|
365
|
+
const response = await apiClient.getSections(themeInfo.uuid);
|
|
366
|
+
const blocks = response.data || [];
|
|
367
|
+
|
|
368
|
+
if (blocks.length === 0) {
|
|
369
|
+
showWarning(t('section.create.no_blocks'));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const selectedBlock = await brandedSearch({
|
|
374
|
+
message: t('section.create.block_prompt'),
|
|
375
|
+
choices: blocks.map((block) => ({
|
|
376
|
+
name: `${block.name} ${chalk.gray(`(${block.folder})`)}`,
|
|
377
|
+
value: block,
|
|
378
|
+
keywords: [block.name, block.folder],
|
|
379
|
+
})),
|
|
380
|
+
}, t('section.create.title'));
|
|
381
|
+
|
|
382
|
+
const name = await brandedInput({
|
|
383
|
+
message: t('section.create.name_prompt'),
|
|
384
|
+
validate: validateSectionName,
|
|
385
|
+
}, t('section.create.title'));
|
|
386
|
+
|
|
387
|
+
const sectionName = name.trim();
|
|
388
|
+
const sectionFile = `${sectionName}.twig`;
|
|
389
|
+
const targetPath = `section/${selectedBlock.folder}/${sectionFile}`;
|
|
390
|
+
|
|
391
|
+
const confirm = await brandedConfirm({
|
|
392
|
+
message: t('section.create.confirm_prompt', { file: targetPath, theme: themeInfo.name }),
|
|
393
|
+
default: true,
|
|
394
|
+
}, t('section.add.confirm_title'));
|
|
395
|
+
|
|
396
|
+
if (!confirm) {
|
|
397
|
+
showWarning(t('section.add.cancelled'));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
await apiClient.createSection(themeInfo.uuid, selectedBlock.folder, sectionName);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (error.message.startsWith('VALIDATION_ERROR:')) {
|
|
405
|
+
showWarning(t('section.create.failed', { error: error.message.replace('VALIDATION_ERROR:', '') }));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
showSuccess(t('section.create.created', { file: targetPath }));
|
|
412
|
+
|
|
413
|
+
const store = await getActiveStore();
|
|
414
|
+
const storeSlug = normalizeStoreDomain(store);
|
|
415
|
+
const themePath = path.join(process.cwd(), storeSlug, activeTheme);
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
await downloadThemeFiles(apiClient, themeInfo.uuid, themePath);
|
|
419
|
+
console.log(chalk.green(t('section.add.files_updated')));
|
|
420
|
+
console.log(chalk.gray(' ' + t('section.add.location', { path: `${storeSlug}/${activeTheme}/` }) + '\n'));
|
|
421
|
+
} catch (downloadError) {
|
|
422
|
+
console.error(chalk.red(t('section.add.download_error')), downloadError.message);
|
|
423
|
+
console.log(chalk.yellow('\n' + t('section.add.manual_tip')));
|
|
424
|
+
console.log(chalk.white(` ${t('section.add.manual_command')}\n`));
|
|
425
|
+
}
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error(chalk.red('\n❌ Hata: ') + error.message);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function themeSectionForkCommand() {
|
|
433
|
+
try {
|
|
434
|
+
const activeTheme = await getActiveTheme();
|
|
435
|
+
if (!activeTheme) {
|
|
436
|
+
showWarning(t('section.add.no_active_theme'));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const themeInfo = await getThemeUuidBySlug(activeTheme);
|
|
441
|
+
|
|
442
|
+
const apiClient = await createApiClient();
|
|
443
|
+
const response = await apiClient.getSections(themeInfo.uuid);
|
|
444
|
+
const blocks = response.data || [];
|
|
445
|
+
|
|
446
|
+
const sourceSections = [];
|
|
447
|
+
blocks.forEach((block) => {
|
|
448
|
+
(block.default_sections || []).forEach((section) => {
|
|
449
|
+
sourceSections.push({ block, section });
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (sourceSections.length === 0) {
|
|
454
|
+
showWarning(t('section.fork.no_sources'));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const selected = await brandedSearch({
|
|
459
|
+
message: t('section.fork.source_prompt'),
|
|
460
|
+
choices: sourceSections.map(({ block, section }) => ({
|
|
461
|
+
name: `${section.name} ${chalk.gray(`(${block.name}, v${section.version})`)}`,
|
|
462
|
+
value: { block, section },
|
|
463
|
+
keywords: [section.name, block.name, block.folder],
|
|
464
|
+
})),
|
|
465
|
+
}, t('section.fork.title'));
|
|
466
|
+
|
|
467
|
+
const newName = await brandedInput({
|
|
468
|
+
message: t('section.fork.name_prompt'),
|
|
469
|
+
validate: validateSectionName,
|
|
470
|
+
}, t('section.fork.title'));
|
|
471
|
+
|
|
472
|
+
const sectionName = newName.trim();
|
|
473
|
+
const sectionFile = `${sectionName}.twig`;
|
|
474
|
+
const blockFolder = selected.block.folder;
|
|
475
|
+
const targetPath = `section/${blockFolder}/${sectionFile}`;
|
|
476
|
+
|
|
477
|
+
const confirm = await brandedConfirm({
|
|
478
|
+
message: t('section.fork.confirm_prompt', {
|
|
479
|
+
source: selected.section.name,
|
|
480
|
+
file: targetPath,
|
|
481
|
+
theme: themeInfo.name,
|
|
482
|
+
}),
|
|
483
|
+
default: true,
|
|
484
|
+
}, t('section.add.confirm_title'));
|
|
485
|
+
|
|
486
|
+
if (!confirm) {
|
|
487
|
+
showWarning(t('section.add.cancelled'));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
await apiClient.createSection(themeInfo.uuid, blockFolder, sectionName);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (error.message.startsWith('VALIDATION_ERROR:')) {
|
|
495
|
+
showWarning(t('section.create.failed', { error: error.message.replace('VALIDATION_ERROR:', '') }));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
showSuccess(t('section.fork.created', { file: targetPath }));
|
|
502
|
+
showInfo(t('section.fork.next_step', { file: targetPath }));
|
|
503
|
+
|
|
504
|
+
const store = await getActiveStore();
|
|
505
|
+
const storeSlug = normalizeStoreDomain(store);
|
|
506
|
+
const themePath = path.join(process.cwd(), storeSlug, activeTheme);
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
await downloadThemeFiles(apiClient, themeInfo.uuid, themePath);
|
|
510
|
+
console.log(chalk.green(t('section.add.files_updated')));
|
|
511
|
+
console.log(chalk.gray(' ' + t('section.add.location', { path: `${storeSlug}/${activeTheme}/` }) + '\n'));
|
|
512
|
+
} catch (downloadError) {
|
|
513
|
+
console.error(chalk.red(t('section.add.download_error')), downloadError.message);
|
|
514
|
+
console.log(chalk.yellow('\n' + t('section.add.manual_tip')));
|
|
515
|
+
console.log(chalk.white(` ${t('section.add.manual_command')}\n`));
|
|
516
|
+
}
|
|
517
|
+
} catch (error) {
|
|
518
|
+
console.error(chalk.red('\n❌ Hata: ') + error.message);
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export async function themeSectionMenuCommand() {
|
|
524
|
+
const action = await brandedSelect({
|
|
525
|
+
message: t('section.menu.prompt'),
|
|
526
|
+
choices: [
|
|
527
|
+
{ name: t('section.menu.add'), value: 'add' },
|
|
528
|
+
{ name: t('section.menu.create'), value: 'create' },
|
|
529
|
+
{ name: t('section.menu.fork'), value: 'fork' },
|
|
530
|
+
{ name: t('section.menu.list'), value: 'list' },
|
|
531
|
+
],
|
|
532
|
+
}, t('section.menu.title'));
|
|
533
|
+
|
|
534
|
+
switch (action) {
|
|
535
|
+
case 'add':
|
|
536
|
+
return themeSectionAddCommand();
|
|
537
|
+
case 'create':
|
|
538
|
+
return themeSectionCreateCommand();
|
|
539
|
+
case 'fork':
|
|
540
|
+
return themeSectionForkCommand();
|
|
541
|
+
case 'list':
|
|
542
|
+
return themeSectionListCommand();
|
|
543
|
+
}
|
|
544
|
+
}
|
package/src/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { logoutCommand } from './commands/logout.js';
|
|
|
11
11
|
import { whoamiCommand } from './commands/whoami.js';
|
|
12
12
|
import { themeListCommand, themeCreateCommand, themePullCommand, themeUseCommand } from './commands/theme.js';
|
|
13
13
|
import { themeDevCommand } from './commands/theme-dev.js';
|
|
14
|
-
import { themeSectionListCommand, themeSectionAddCommand } from './commands/theme-section.js';
|
|
14
|
+
import { themeSectionListCommand, themeSectionAddCommand, themeSectionCreateCommand, themeSectionForkCommand, themeSectionMenuCommand } from './commands/theme-section.js';
|
|
15
15
|
import { themePublishCommand } from './commands/theme-publish.js';
|
|
16
16
|
import { themePushCommand } from './commands/theme-push.js';
|
|
17
17
|
import { themeSubmitCommand } from './commands/theme-submit.js';
|
|
@@ -167,7 +167,10 @@ org
|
|
|
167
167
|
// section commands (under theme)
|
|
168
168
|
const section = theme
|
|
169
169
|
.command('section')
|
|
170
|
-
.description('Section management commands')
|
|
170
|
+
.description('Section management commands')
|
|
171
|
+
.action(async () => {
|
|
172
|
+
await themeSectionMenuCommand();
|
|
173
|
+
});
|
|
171
174
|
|
|
172
175
|
section
|
|
173
176
|
.command('list')
|
|
@@ -183,5 +186,19 @@ section
|
|
|
183
186
|
await themeSectionAddCommand();
|
|
184
187
|
});
|
|
185
188
|
|
|
189
|
+
section
|
|
190
|
+
.command('create')
|
|
191
|
+
.description('Create an empty section in the active theme')
|
|
192
|
+
.action(async () => {
|
|
193
|
+
await themeSectionCreateCommand();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
section
|
|
197
|
+
.command('fork')
|
|
198
|
+
.description('Fork an existing section into the active theme')
|
|
199
|
+
.action(async () => {
|
|
200
|
+
await themeSectionForkCommand();
|
|
201
|
+
});
|
|
202
|
+
|
|
186
203
|
// Parse arguments
|
|
187
204
|
program.parse(process.argv);
|
package/src/ui/prompt-wrapper.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import boxen from 'boxen';
|
|
2
2
|
import gradient from 'gradient-string';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import {input as inquirerInput, confirm as inquirerConfirm, select as inquirerSelect} from '@inquirer/prompts';
|
|
4
|
+
import {input as inquirerInput, confirm as inquirerConfirm, select as inquirerSelect, search as inquirerSearch} from '@inquirer/prompts';
|
|
5
5
|
import { t } from '../i18n.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -108,6 +108,40 @@ export async function brandedSelect(options, boxTitle = t('ui.label.select')) {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Aranabilir select prompt — kullanıcı yazdıkça choices filtrelenir.
|
|
113
|
+
* @param {object} options - { message, choices, filter? }
|
|
114
|
+
* choices: [{ name, value, keywords? }] — keywords ek arama terimleri
|
|
115
|
+
* filter: opsiyonel özel filtre fn(choice, term) => boolean
|
|
116
|
+
* @param {string} boxTitle - Box başlığı
|
|
117
|
+
*/
|
|
118
|
+
export async function brandedSearch(options, boxTitle = t('ui.label.select')) {
|
|
119
|
+
const {message, choices, filter} = options;
|
|
120
|
+
|
|
121
|
+
if (message) {
|
|
122
|
+
showBrandedBox(message, boxTitle);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const defaultFilter = (choice, term) => {
|
|
126
|
+
const haystack = [choice.name, choice.value, ...(choice.keywords || [])]
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.join(' ')
|
|
129
|
+
.toLocaleLowerCase('tr');
|
|
130
|
+
return haystack.includes(term.toLocaleLowerCase('tr'));
|
|
131
|
+
};
|
|
132
|
+
const matches = filter || defaultFilter;
|
|
133
|
+
|
|
134
|
+
return await inquirerSearch({
|
|
135
|
+
message: '',
|
|
136
|
+
source: async (term) => {
|
|
137
|
+
if (!term) {
|
|
138
|
+
return choices;
|
|
139
|
+
}
|
|
140
|
+
return choices.filter((choice) => matches(choice, term));
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
111
145
|
/**
|
|
112
146
|
* Özelleştirilmiş input prompt
|
|
113
147
|
* @param {object} options - Prompt seçenekleri
|