tsoft-cli 3.10.2 → 3.12.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 CHANGED
@@ -5,6 +5,44 @@ 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.12.1] - 2026-08-08
9
+
10
+ ### Fixed
11
+ - Theme lists no longer call two different things "active". Standing inside a theme directory and running `tsoft theme use` named that theme on one line and then marked a different one as active in the list below it. The mark was reading which theme is published on the storefront, while the line above was reading the theme you are working on — both were correct, and both were called active. The theme you are working on is now marked `(Selected)` and the one your customers see is marked `(Published)`, so a list can show both without the two contradicting each other. `theme list`, `theme pull` and `theme dev` use the same two marks.
12
+
13
+ ## [3.12.0] - 2026-08-05
14
+
15
+ ### Added
16
+ - Commands now take the store and theme from the directory you are standing in. Working on two stores at once meant one terminal quietly changing the other's store, because there was a single shared record of where you were. Inside `<store>/<theme>` that record is no longer consulted: each terminal follows its own directory, so several stores and themes can be open side by side. Nothing is written while doing this — the store you picked with `store use` stays as it was.
17
+ - Store directories are matched against the stores you are signed in to, not against a name pattern, so an unrelated folder cannot be mistaken for one. Outside such a directory everything behaves as before and falls back to the store you last chose. `--store` still wins over the directory.
18
+
19
+ ### Fixed
20
+ - `theme create` and `theme pull` no longer nest a second store directory inside the one you are already in. Theme directories are placed relative to the workspace containing the store directory rather than blindly under the current one, so running them from inside a theme puts the files where you would expect.
21
+
22
+ ## [3.11.0] - 2026-08-05
23
+
24
+ ### Added
25
+ - `tsoft store use` sets the active store, and `tsoft store list` shows the ones you are signed in to. Sessions are kept per store and stay valid for a month, but until now the only way to move between two stores was to sign in again, because nothing else set the active store. Both commands show how long each session has left and say plainly which stores need a fresh sign-in.
26
+ - `--store <domain>` runs a single command against another store without changing the one you are on: `tsoft --store shop.example.com theme push`.
27
+ - Store and theme pickers become searchable once there are more than seven to choose from. Scrolling a list of twenty-five stores to find one was the slow part; typing a few letters of the domain now narrows it. Stores also match on the folder name and the signed-in email, themes on their folder and alias.
28
+
29
+ ### Fixed
30
+ - Refreshing an expired session no longer changes which store you are on. Every command renews the access token when it has expired, and that renewal was also writing the active store — so running a command against one store, whether through `--store` or from another terminal, quietly moved everyone else to it. The active store now changes only when you sign in or run `store use`.
31
+ - Signing out of one store no longer clears the active store when you were on a different one.
32
+
33
+ ### Changed
34
+ - The active theme is now remembered per store, next to that store's credentials, instead of in one shared file. Switching stores no longer loses your place — each store comes back to the theme you were last working on. Your current theme is carried over the first time you use this version, so there is nothing to set up again.
35
+ - `tsoft theme push` prints the store and theme it is about to push to, and `tsoft theme use` names the store whose themes it is listing. Neither asks anything; the line is there so working on the wrong store is visible before it matters rather than after. The theme picker gave no clue which store the list came from.
36
+
37
+ ## [3.10.3] - 2026-08-05
38
+
39
+ ### Fixed
40
+ - `tsoft theme push` returned "theme not found" on some older themes while working normally on newer ones. The active theme was stored as a slug derived from the theme name, and that slug was sent to the server as the theme identifier. A theme can be identified by its folder or by its alias, and the alias is optional — themes installed through certain paths never received one, so there was nothing for the slug to match and the push was rejected. The active theme is now stored as the theme folder, which every theme always has.
41
+ - Existing installations are migrated on first use: a stored slug is looked up once and the stored value is replaced with the folder, so there is no need to re-select a theme. Local theme directories keep their readable names.
42
+ - `tsoft theme section` commands accept a stored theme folder as well as a slug, so they keep working through and after the migration.
43
+ - Confirmation boxes drew a ragged right border. The "T-Soft" mark was positioned with a fixed number of spaces rather than measured against the box, so it drifted out of alignment whenever the content was wider or narrower than that fixed guess. It is now aligned to the actual content width.
44
+ - Warning, input and stop symbols across all messages are replaced with plain geometric ones. The previous symbols carried a variation selector, which many terminals render one cell narrower than its measured width; any line containing one drifted, and inside a box it dragged the border with it. The clock in the default theme update prompt became an hourglass for the same reason. Spacing after the affected symbols was adjusted to match their real width.
45
+
8
46
  ## [3.10.2] - 2026-08-03
9
47
 
10
48
  ### Fixed
package/locales/en.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "ui.label.confirm": "❓ Confirm",
3
3
  "ui.label.select": "📋 Select",
4
- "ui.label.input": "✏️ Input",
5
- "ui.label.warning": "⚠️ Warning",
4
+ "ui.label.input": "Input",
5
+ "ui.label.warning": "Warning",
6
6
  "ui.label.error": "❌ Error",
7
7
  "ui.label.success": "✅ Success",
8
8
  "ui.label.info": "Info",
@@ -57,19 +57,19 @@
57
57
  "login.error_log": "Check log files for details.",
58
58
  "login.security_error": "State parameter mismatch! This may indicate a security issue.",
59
59
  "logout.starting": "🚪 Logging out...",
60
- "logout.not_logged_in": "⚠️ You are not logged in.",
60
+ "logout.not_logged_in": "You are not logged in.",
61
61
  "logout.revoking": "🔄 Revoking token...",
62
62
  "logout.revoked": "✓ Token revoked",
63
- "logout.revoke_failed": "⚠️ Could not revoke token (may already be invalid)",
63
+ "logout.revoke_failed": "Could not revoke token (may already be invalid)",
64
64
  "logout.credentials_cleared": "✓ Local credentials cleared",
65
65
  "logout.success": "👋 Successfully logged out!",
66
66
  "logout.relogin_tip": "To log in again, use the \"tsoft login\" command.",
67
- "whoami.not_logged_in": "⚠️ You are not logged in.",
67
+ "whoami.not_logged_in": "You are not logged in.",
68
68
  "whoami.not_logged_in_tip": "Use \"tsoft login\" to log in.",
69
69
  "whoami.not_logged_in.json.message": "Not logged in.",
70
70
  "whoami.not_logged_in.json.hint": "To log in: tsoft login --org",
71
71
  "whoami.token_refreshed": "🔄 Token automatically refreshed",
72
- "whoami.session_expired": "⚠️ Session expired, logging in again...",
72
+ "whoami.session_expired": "Session expired, logging in again...",
73
73
  "whoami.title": "👤 Identity Information",
74
74
  "whoami.user_label": "User:",
75
75
  "whoami.store_label": "Store:",
@@ -82,7 +82,7 @@
82
82
  "whoami.refresh_valid": "✓ Valid ({{days}} days remaining)",
83
83
  "whoami.relogin_tip": "Tip: Use tsoft login --org to log in again",
84
84
  "theme.listing": "📋 Listing themes...",
85
- "theme.no_themes": "⚠️ No themes found yet.",
85
+ "theme.no_themes": "No themes found yet.",
86
86
  "theme.count": "Total {{count}} themes found:",
87
87
  "theme.folder_label": "Folder:",
88
88
  "theme.login_required": "You must log in first. Run: tsoft login",
@@ -93,7 +93,7 @@
93
93
  "theme.author_prompt": "Developer:",
94
94
  "theme.minimal_prompt": "Start with a starter theme?\n\nStarter theme includes only basic blocks, suitable for building from scratch.",
95
95
  "theme.minimal_title": "🎨 Theme Type",
96
- "theme.dir_exists_title": "⚠️ Directory Exists",
96
+ "theme.dir_exists_title": "Directory Exists",
97
97
  "theme.dir_exists_prompt": "Directory already exists!\n\nLocation: {{path}}\n\nOverwrite?",
98
98
  "theme.dir_overwrite_prompt": "Directory already exists!\n\nLocation: {{path}}\n\nOverwrite?",
99
99
  "theme.cancelled": "Operation cancelled.",
@@ -114,7 +114,7 @@
114
114
  "theme.connection_check": "Check your internet connection and try again.",
115
115
  "theme.updating": "🔄 Creating theme...",
116
116
  "theme.update_failed_contact": "Tip: Please contact your system administrator.",
117
- "theme.force_update_confirm": "The default theme will be automatically updated and your new theme will be created.\n\n🔸 This operation:\n • Upgrades the default theme to the latest version\n • Updates all layouts and sections\n • Creates your new theme based on the updated default theme\n\n⏱️ This operation may take a few minutes.\n\nProceed?",
117
+ "theme.force_update_confirm": "The default theme will be automatically updated and your new theme will be created.\n\n🔸 This operation:\n • Upgrades the default theme to the latest version\n • Updates all layouts and sections\n • Creates your new theme based on the updated default theme\n\nThis operation may take a few minutes.\n\nProceed?",
118
118
  "theme.force_update_title": "🔄 Default Theme Update",
119
119
  "theme.force_updating": "⏳ Updating default theme and creating new theme...",
120
120
  "theme.force_update_success_1": "✓ Default theme successfully updated",
@@ -133,6 +133,8 @@
133
133
  "theme.use.select_title": "🎯 Theme Selection",
134
134
  "theme.use.set": "✓ Active theme set: {{name}}",
135
135
  "theme.use.folder": "Folder: {{folder}}",
136
+ "theme.mark.selected": "(Selected)",
137
+ "theme.mark.published": "(Published)",
136
138
  "push.no_active_theme": "No active theme found.",
137
139
  "push.usage": "Usage: tsoft theme push <theme-name>",
138
140
  "push.use_tip": "Or set an active theme with \"tsoft theme use <theme-name>\".",
@@ -174,11 +176,11 @@
174
176
  "dev.lock_acquired": "✓ Lock successfully acquired",
175
177
  "dev.already_locked": "✓ You already have the lock",
176
178
  "dev.already_synced": "✓ Theme is synchronized",
177
- "dev.not_synced": "⚠️ Lock exists but theme is not synchronized",
179
+ "dev.not_synced": "Lock exists but theme is not synchronized",
178
180
  "dev.lock_by_other": "Theme is locked by another developer!\n\nDeveloper: {{dev}}\nLast access: {{last_access}}\n\nAre you sure you want to take the lock?",
179
181
  "dev.lock_warning_title": "🔒 Lock Warning",
180
182
  "dev.lock_override": "🔐 Acquiring lock (override)...",
181
- "dev.stopping": "⏸️ Shutting down development environment...",
183
+ "dev.stopping": "Shutting down development environment...",
182
184
  "dev.watch_stopped": "✓ File watching stopped",
183
185
  "dev.keepalive_stopped": "✓ Keep-alive stopped",
184
186
  "dev.lock_kept": "(Lock kept active - you can continue next time)",
@@ -191,12 +193,12 @@
191
193
  "dev.lock_stolen_owner": "New lock owner: {{owner}}",
192
194
  "dev.lock_stolen_stopping": "Stopping development environment...",
193
195
  "dev.lock_lost": "❌ Theme was locked by another developer!",
194
- "dev.not_synced_pulling": "⚠️ Theme is not synced! Pulling...",
196
+ "dev.not_synced_pulling": "Theme is not synced! Pulling...",
195
197
  "dev.file_deleted": "🗑️ {{path}} → Deleted",
196
198
  "dev.file_delete_failed": "{{path}} → Could not delete: {{error}}",
197
199
  "dev.file_saved": "✓ {{path}} → Saved ({{duration}}ms)",
198
200
  "dev.file_error": "{{path}} → Error: {{error}}",
199
- "dev.keepalive_error": "⚠️ Keep-alive error (will retry)",
201
+ "dev.keepalive_error": "Keep-alive error (will retry)",
200
202
  "init.no_themes": "No themes found.",
201
203
  "init.select_prompt": "Which theme would you like to fork?",
202
204
  "init.select_title": "Theme Selection",
@@ -219,14 +221,14 @@
219
221
  "section.list.no_active_theme": "No active theme found. Please run \"tsoft theme use <theme-name>\" first.",
220
222
  "section.list.starting": "Listing sections...",
221
223
  "section.list.active_theme": "Active Theme: {{theme}}",
222
- "section.list.no_blocks": "⚠️ No blocks found yet.",
224
+ "section.list.no_blocks": "No blocks found yet.",
223
225
  "section.list.count": "Total {{count}} blocks found:",
224
226
  "section.list.id_label": "ID:",
225
227
  "section.list.folder_label": "Folder:",
226
228
  "section.list.sections_label": "Available sections ({{count}}):",
227
229
  "section.list.tags_label": "Tags:",
228
230
  "section.list.file_label": "File:",
229
- "section.list.no_sections": "⚠️ No sections found for this block",
231
+ "section.list.no_sections": "No sections found for this block",
230
232
  "section.add.no_active_theme": "No active theme found. Please run \"tsoft theme use <theme-name>\" first.",
231
233
  "section.add.starting": "🔧 Starting section add operation...",
232
234
  "section.add.active_theme": "Active Theme: {{theme}}",
@@ -247,7 +249,7 @@
247
249
  "section.add.cancelled": "Operation cancelled.",
248
250
  "section.add.adding": "🔄 Adding sections...",
249
251
  "section.add.added": "✓ {{name}} added",
250
- "section.add.already_exists": "⚠️ {{name}} already exists.",
252
+ "section.add.already_exists": "{{name}} already exists.",
251
253
  "section.add.overwrite_prompt": "What would you like to do?",
252
254
  "section.add.overwrite": "Overwrite",
253
255
  "section.add.overwrite_all": "Overwrite all",
@@ -266,7 +268,7 @@
266
268
  "section.add.files_updated": "✓ Theme files updated",
267
269
  "section.add.complete": "🎉 Operation completed!",
268
270
  "section.add.location": "Location: {{path}}",
269
- "section.add.download_error": "⚠️ Error while updating theme files:",
271
+ "section.add.download_error": "Error while updating theme files:",
270
272
  "section.add.manual_tip": "💡 Try downloading the theme manually:",
271
273
  "section.add.manual_command": "tsoft theme pull",
272
274
  "section.menu.title": "🧩 Section Operations",
@@ -337,5 +339,23 @@
337
339
  "dev.section_synced": "🧩 section updated: {{folder}}",
338
340
  "dev.status_section": "section",
339
341
  "dev.section_menu_opening": "Opening section menu...",
340
- "dev.section_menu_resumed": "Resuming watch."
342
+ "dev.section_menu_resumed": "Resuming watch.",
343
+ "store.none": "No stores are saved yet.",
344
+ "store.login_hint": "Sign in first: tsoft login",
345
+ "store.count": "{{count}} saved stores",
346
+ "store.session_expired": "— session expired",
347
+ "store.session_days": "({{days}} days)",
348
+ "store.select_prompt": "Which store will you work on?",
349
+ "store.select_title": "Store Selection",
350
+ "store.selected": "Active store: {{store}}",
351
+ "store.selected_theme": "Theme: {{theme}}",
352
+ "store.expired_warning": "This store's session has expired. To continue: tsoft login",
353
+ "store.not_found": "No saved store matches: {{store}}",
354
+ "store.json_needs_argument": "A store name is required in JSON mode.",
355
+ "store.json_hint": "Usage: tsoft --json store use <domain>",
356
+ "context.line": "Store: {{store}} · Theme: {{theme}}",
357
+ "store.search_prompt": "Search or pick a store",
358
+ "theme.use.on_store": "Store: {{store}}",
359
+ "theme.use.search_prompt": "Search or pick a theme",
360
+ "context.workspace": "From this directory: {{store}} · {{theme}}"
341
361
  }
package/locales/tr.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "ui.label.confirm": "❓ Onay",
3
3
  "ui.label.select": "📋 Seçim",
4
- "ui.label.input": "✏️ Giriş",
5
- "ui.label.warning": "⚠️ Uyarı",
4
+ "ui.label.input": "Giriş",
5
+ "ui.label.warning": "Uyarı",
6
6
  "ui.label.error": "❌ Hata",
7
7
  "ui.label.success": "✅ Başarılı",
8
8
  "ui.label.info": "Bilgi",
@@ -57,19 +57,19 @@
57
57
  "login.error_log": "Detaylar için log dosyalarını kontrol edin.",
58
58
  "login.security_error": "State parametresi eşleşmiyor! Güvenlik hatası olabilir.",
59
59
  "logout.starting": "🚪 Çıkış yapılıyor...",
60
- "logout.not_logged_in": "⚠️ Zaten giriş yapmamışsınız.",
60
+ "logout.not_logged_in": "Zaten giriş yapmamışsınız.",
61
61
  "logout.revoking": "🔄 Token iptal ediliyor...",
62
62
  "logout.revoked": "✓ Token iptal edildi",
63
- "logout.revoke_failed": "⚠️ Token iptal edilemedi (zaten geçersiz olabilir)",
63
+ "logout.revoke_failed": "Token iptal edilemedi (zaten geçersiz olabilir)",
64
64
  "logout.credentials_cleared": "✓ Yerel kimlik bilgileri temizlendi",
65
65
  "logout.success": "👋 Başarıyla çıkış yapıldı!",
66
66
  "logout.relogin_tip": "Tekrar giriş yapmak için \"tsoft login\" komutunu kullanın.",
67
- "whoami.not_logged_in": "⚠️ Giriş yapmamışsınız.",
67
+ "whoami.not_logged_in": "Giriş yapmamışsınız.",
68
68
  "whoami.not_logged_in_tip": "Giriş yapmak için \"tsoft login\" komutunu kullanın.",
69
69
  "whoami.not_logged_in.json.message": "Giriş yapılmamış.",
70
70
  "whoami.not_logged_in.json.hint": "Giriş yapmak için: tsoft login --org",
71
71
  "whoami.token_refreshed": "🔄 Token otomatik olarak yenilendi",
72
- "whoami.session_expired": "⚠️ Oturum süresi dolmuş, yeniden giriş yapılıyor...",
72
+ "whoami.session_expired": "Oturum süresi dolmuş, yeniden giriş yapılıyor...",
73
73
  "whoami.title": "👤 Kimlik Bilgileri",
74
74
  "whoami.user_label": "Kullanıcı:",
75
75
  "whoami.store_label": "Store:",
@@ -82,7 +82,7 @@
82
82
  "whoami.refresh_valid": "✓ Geçerli ({{days}} gün kaldı)",
83
83
  "whoami.relogin_tip": "İpucu: tsoft login --org ile tekrar giriş yapın",
84
84
  "theme.listing": "📋 Temalar listeleniyor...",
85
- "theme.no_themes": "⚠️ Henüz tema bulunmuyor.",
85
+ "theme.no_themes": "Henüz tema bulunmuyor.",
86
86
  "theme.count": "Toplam {{count}} tema bulundu:",
87
87
  "theme.folder_label": "Klasör:",
88
88
  "theme.login_required": "Önce giriş yapmalısınız. Çalıştırın: tsoft login",
@@ -93,7 +93,7 @@
93
93
  "theme.author_prompt": "Geliştirici:",
94
94
  "theme.minimal_prompt": "Starter tema ile başlansın mı?\n\nStarter tema sadece temel blokları içerir, sıfırdan geliştirmeye uygundur.",
95
95
  "theme.minimal_title": "🎨 Tema Tipi",
96
- "theme.dir_exists_title": "⚠️ Dizin Mevcut",
96
+ "theme.dir_exists_title": "Dizin Mevcut",
97
97
  "theme.dir_exists_prompt": "Dizin zaten mevcut!\n\nKonum: {{path}}\n\nÜzerine yazılsın mı?",
98
98
  "theme.dir_overwrite_prompt": "Dizin zaten mevcut!\n\nKonum: {{path}}\n\nUzerine yazilsin mi?",
99
99
  "theme.cancelled": "İşlem iptal edildi.",
@@ -114,7 +114,7 @@
114
114
  "theme.connection_check": "İnternet bağlantınızı kontrol edip tekrar deneyin.",
115
115
  "theme.updating": "🔄 Tema oluşturuluyor...",
116
116
  "theme.update_failed_contact": "İpucu: Lütfen sistem yöneticinizle iletişime geçin.",
117
- "theme.force_update_confirm": "Default tema otomatik olarak güncellenecek ve yeni temanız oluşturulacak.\n\n🔸 Bu işlem:\n • Default temayı en güncel versiyona yükseltir\n • Tüm layout ve section'ları günceller\n • Yeni temanızı güncel default tema üzerinden oluşturur\n\n⏱️ Bu işlem birkaç dakika sürebilir.\n\nDevam edilsin mi?",
117
+ "theme.force_update_confirm": "Default tema otomatik olarak güncellenecek ve yeni temanız oluşturulacak.\n\n🔸 Bu işlem:\n • Default temayı en güncel versiyona yükseltir\n • Tüm layout ve section'ları günceller\n • Yeni temanızı güncel default tema üzerinden oluşturur\n\nBu işlem birkaç dakika sürebilir.\n\nDevam edilsin mi?",
118
118
  "theme.force_update_title": "🔄 Default Tema Güncellemesi",
119
119
  "theme.force_updating": "⏳ Default tema güncelleniyor ve yeni tema oluşturuluyor...",
120
120
  "theme.force_update_success_1": "✓ Default tema başarıyla güncellendi",
@@ -133,6 +133,8 @@
133
133
  "theme.use.select_title": "🎯 Tema Seçimi",
134
134
  "theme.use.set": "✓ Aktif tema ayarlandı: {{name}}",
135
135
  "theme.use.folder": "Klasör: {{folder}}",
136
+ "theme.mark.selected": "(Seçili)",
137
+ "theme.mark.published": "(Yayında)",
136
138
  "push.no_active_theme": "Aktif tema bulunamadı.",
137
139
  "push.usage": "Kullanim: tsoft theme push <theme-name>",
138
140
  "push.use_tip": "veya \"tsoft theme use <theme-name>\" ile aktif tema ayarlayın.",
@@ -174,11 +176,11 @@
174
176
  "dev.lock_acquired": "✓ Kilit başarıyla alındı",
175
177
  "dev.already_locked": "✓ Zaten sizin kiliniz var",
176
178
  "dev.already_synced": "✓ Tema senkronize",
177
- "dev.not_synced": "⚠️ Kilit var ama tema senkronize değil",
179
+ "dev.not_synced": "Kilit var ama tema senkronize değil",
178
180
  "dev.lock_by_other": "Tema başka bir developer tarafından kilitli!\n\nDeveloper: {{dev}}\nSon erişim: {{last_access}}\n\nKilidi almak istediğinizden emin misiniz?",
179
181
  "dev.lock_warning_title": "🔒 Kilit Uyarısı",
180
182
  "dev.lock_override": "🔐 Kilit alınıyor (override)...",
181
- "dev.stopping": "⏸️ Geliştirme ortamı kapatılıyor...",
183
+ "dev.stopping": "Geliştirme ortamı kapatılıyor...",
182
184
  "dev.watch_stopped": "✓ Dosya izleme durduruldu",
183
185
  "dev.keepalive_stopped": "✓ Keep-alive durduruldu",
184
186
  "dev.lock_kept": "(Lock aktif bırakıldı - sonraki sefer devam edebilirsiniz)",
@@ -191,12 +193,12 @@
191
193
  "dev.lock_stolen_owner": "Yeni kilit sahibi: {{owner}}",
192
194
  "dev.lock_stolen_stopping": "Geliştirme ortamı durduruluyor...",
193
195
  "dev.lock_lost": "❌ Tema başka bir developer tarafından kilitlendi!",
194
- "dev.not_synced_pulling": "⚠️ Tema senkronize değil! Pull yapılıyor...",
196
+ "dev.not_synced_pulling": "Tema senkronize değil! Pull yapılıyor...",
195
197
  "dev.file_deleted": "🗑️ {{path}} → Silindi",
196
198
  "dev.file_delete_failed": "{{path}} → Silinemedi: {{error}}",
197
199
  "dev.file_saved": "✓ {{path}} → Kaydedildi ({{duration}}ms)",
198
200
  "dev.file_error": "{{path}} → Hata: {{error}}",
199
- "dev.keepalive_error": "⚠️ Keep-alive hatası (yeniden denenecek)",
201
+ "dev.keepalive_error": "Keep-alive hatası (yeniden denenecek)",
200
202
  "init.no_themes": "Tema bulunamadı.",
201
203
  "init.select_prompt": "Hangi temayi fork etmek istiyorsunuz?",
202
204
  "init.select_title": "Tema Seçimi",
@@ -219,14 +221,14 @@
219
221
  "section.list.no_active_theme": "Aktif tema bulunamadı. Lütfen önce \"tsoft theme use <tema-adı>\" komutunu çalıştırın.",
220
222
  "section.list.starting": "Section'lar listeleniyor...",
221
223
  "section.list.active_theme": "Aktif Tema: {{theme}}",
222
- "section.list.no_blocks": "⚠️ Henüz block bulunmuyor.",
224
+ "section.list.no_blocks": "Henüz block bulunmuyor.",
223
225
  "section.list.count": "Toplam {{count}} block bulundu:",
224
226
  "section.list.id_label": "ID:",
225
227
  "section.list.folder_label": "Klasör:",
226
228
  "section.list.sections_label": "Mevcut section'lar ({{count}}):",
227
229
  "section.list.tags_label": "Etiketler:",
228
230
  "section.list.file_label": "Dosya:",
229
- "section.list.no_sections": "⚠️ Bu block için section bulunmuyor",
231
+ "section.list.no_sections": "Bu block için section bulunmuyor",
230
232
  "section.add.no_active_theme": "Aktif tema bulunamadı. Lütfen önce \"tsoft theme use <tema-adı>\" komutunu çalıştırın.",
231
233
  "section.add.starting": "🔧 Section ekleme işlemi başlatılıyor...",
232
234
  "section.add.active_theme": "Aktif Tema: {{theme}}",
@@ -247,7 +249,7 @@
247
249
  "section.add.cancelled": "İşlem iptal edildi.",
248
250
  "section.add.adding": "🔄 Section'lar ekleniyor...",
249
251
  "section.add.added": "✓ {{name}} eklendi",
250
- "section.add.already_exists": "⚠️ {{name}} zaten mevcut.",
252
+ "section.add.already_exists": "{{name}} zaten mevcut.",
251
253
  "section.add.overwrite_prompt": "Ne yapmak istersiniz?",
252
254
  "section.add.overwrite": "Üzerine yaz",
253
255
  "section.add.overwrite_all": "Tümünün üzerine yaz",
@@ -266,7 +268,7 @@
266
268
  "section.add.files_updated": "✓ Tema dosyaları güncellendi",
267
269
  "section.add.complete": "🎉 İşlem tamamlandı!",
268
270
  "section.add.location": "Konum: {{path}}",
269
- "section.add.download_error": "⚠️ Tema dosyaları güncellenirken hata oluştu:",
271
+ "section.add.download_error": "Tema dosyaları güncellenirken hata oluştu:",
270
272
  "section.add.manual_tip": "💡 Manuel olarak temayı tekrar indirmeyi deneyin:",
271
273
  "section.add.manual_command": "tsoft theme pull",
272
274
  "section.menu.title": "🧩 Section İşlemleri",
@@ -337,5 +339,23 @@
337
339
  "dev.section_synced": "🧩 section güncellendi: {{folder}}",
338
340
  "dev.status_section": "section",
339
341
  "dev.section_menu_opening": "Section menüsü açılıyor...",
340
- "dev.section_menu_resumed": "İzlemeye devam ediliyor."
342
+ "dev.section_menu_resumed": "İzlemeye devam ediliyor.",
343
+ "store.none": "Kayıtlı mağaza yok.",
344
+ "store.login_hint": "Önce giriş yapın: tsoft login",
345
+ "store.count": "{{count}} kayıtlı mağaza",
346
+ "store.session_expired": "— oturum bitti",
347
+ "store.session_days": "({{days}} gün)",
348
+ "store.select_prompt": "Hangi mağazada çalışacaksınız?",
349
+ "store.select_title": "Mağaza Seçimi",
350
+ "store.selected": "Aktif mağaza: {{store}}",
351
+ "store.selected_theme": "Tema: {{theme}}",
352
+ "store.expired_warning": "Bu mağazanın oturumu bitmiş. Devam etmek için: tsoft login",
353
+ "store.not_found": "Kayıtlı mağaza bulunamadı: {{store}}",
354
+ "store.json_needs_argument": "JSON modunda mağaza adı gerekir.",
355
+ "store.json_hint": "Kullanım: tsoft --json store use <domain>",
356
+ "context.line": "Mağaza: {{store}} · Tema: {{theme}}",
357
+ "store.search_prompt": "Mağaza arayın veya seçin",
358
+ "theme.use.on_store": "Mağaza: {{store}}",
359
+ "theme.use.search_prompt": "Tema arayın veya seçin",
360
+ "context.workspace": "Bulunduğunuz dizin: {{store}} · {{theme}}"
341
361
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsoft-cli",
3
- "version": "3.10.2",
3
+ "version": "3.12.1",
4
4
  "description": "Command-line tool for tsoft360 theme development and management",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -47,6 +47,7 @@
47
47
  "gradient-string": "^3.0.0",
48
48
  "open": "^10.1.0",
49
49
  "ora": "^9.3.0",
50
+ "string-width": "^7.2.0",
50
51
  "strip-ansi": "^7.1.2"
51
52
  },
52
53
  "homepage": "https://github.com/tekrom/tsoft-cli",
@@ -2,7 +2,7 @@ import open from 'open';
2
2
  import chalk from 'chalk';
3
3
  import {generateCodeVerifier, generateCodeChallenge} from '../pkce.js';
4
4
  import {startCallbackServer} from '../server.js';
5
- import {exchangeToken, saveTokens} from '../storage.js';
5
+ import {exchangeToken, saveTokens, setActiveStore} from '../storage.js';
6
6
  import * as config from '../config.js';
7
7
  import { isJsonMode, jsonOut } from '../output-mode.js';
8
8
  import { t } from '../i18n.js';
@@ -74,6 +74,7 @@ export async function loginCommand() {
74
74
 
75
75
  // 8. Token'ları kaydet
76
76
  await saveTokens(tokens, store);
77
+ await setActiveStore(store);
77
78
 
78
79
  console.log(chalk.green(t('login.credentials_saved') + '\n'));
79
80
 
@@ -0,0 +1,144 @@
1
+ import chalk from 'chalk';
2
+ import { listKnownStores, setActiveStore, getActiveTheme } from '../storage.js';
3
+ import { brandedSelect, brandedSearch, showSuccess, showError, showInfo } from '../ui/prompt-wrapper.js';
4
+ import { isJsonMode, jsonOut } from '../output-mode.js';
5
+ import { t } from '../i18n.js';
6
+
7
+ const SEARCH_THRESHOLD = 7;
8
+
9
+ function sessionLabel(entry) {
10
+ if (!entry.usable) {
11
+ return chalk.gray(t('store.session_expired'));
12
+ }
13
+
14
+ return chalk.gray(t('store.session_days', { days: entry.daysLeft }));
15
+ }
16
+
17
+ function toJsonEntry(entry) {
18
+ return {
19
+ store: entry.store,
20
+ active: entry.active,
21
+ session_valid: entry.usable,
22
+ session_days_left: entry.daysLeft,
23
+ user: entry.user?.email ?? null,
24
+ };
25
+ }
26
+
27
+ export async function storeListCommand() {
28
+ const stores = await listKnownStores();
29
+
30
+ if (isJsonMode()) {
31
+ jsonOut({
32
+ status: 'success',
33
+ command: 'store list',
34
+ data: { stores: stores.map(toJsonEntry) },
35
+ });
36
+ return;
37
+ }
38
+
39
+ if (stores.length === 0) {
40
+ showInfo(t('store.none'));
41
+ return;
42
+ }
43
+
44
+ console.log(chalk.bold.cyan(t('store.count', { count: stores.length }) + '\n'));
45
+
46
+ for (const entry of stores) {
47
+ const marker = entry.active ? chalk.green('●') : chalk.gray('○');
48
+ const name = entry.active ? chalk.bold.green(entry.store) : chalk.white(entry.store);
49
+ console.log(`${marker} ${name} ${sessionLabel(entry)}`);
50
+ }
51
+
52
+ console.log();
53
+ }
54
+
55
+ export async function storeUseCommand(storeName) {
56
+ const stores = await listKnownStores();
57
+
58
+ if (stores.length === 0) {
59
+ if (isJsonMode()) {
60
+ jsonOut({
61
+ status: 'error',
62
+ command: 'store use',
63
+ error: { message: t('store.none'), hint: t('store.login_hint') },
64
+ });
65
+ process.exit(1);
66
+ }
67
+ showInfo(t('store.none'));
68
+ console.log(chalk.gray(' ' + t('store.login_hint') + '\n'));
69
+ return;
70
+ }
71
+
72
+ let selected;
73
+
74
+ if (storeName) {
75
+ selected = stores.find(entry => entry.store === storeName);
76
+
77
+ if (!selected) {
78
+ const message = t('store.not_found', { store: storeName });
79
+
80
+ if (isJsonMode()) {
81
+ jsonOut({
82
+ status: 'error',
83
+ command: 'store use',
84
+ error: { message, hint: t('store.login_hint') },
85
+ });
86
+ process.exit(1);
87
+ }
88
+
89
+ showError(message);
90
+ return;
91
+ }
92
+ } else {
93
+ if (isJsonMode()) {
94
+ jsonOut({
95
+ status: 'error',
96
+ command: 'store use',
97
+ error: { message: t('store.json_needs_argument'), hint: t('store.json_hint') },
98
+ });
99
+ process.exit(1);
100
+ }
101
+
102
+ const choices = stores.map(entry => ({
103
+ name: `${entry.store} ${sessionLabel(entry)}`,
104
+ value: entry,
105
+ keywords: [entry.slug, entry.user?.email].filter(Boolean),
106
+ }));
107
+
108
+ selected = stores.length > SEARCH_THRESHOLD
109
+ ? await brandedSearch({
110
+ message: t('store.search_prompt'),
111
+ choices,
112
+ }, t('store.select_title'))
113
+ : await brandedSelect({
114
+ message: t('store.select_prompt'),
115
+ choices,
116
+ default: stores.find(entry => entry.active) ?? stores[0],
117
+ }, t('store.select_title'));
118
+ }
119
+
120
+ await setActiveStore(selected.store);
121
+
122
+ const theme = await getActiveTheme(selected.store);
123
+
124
+ if (isJsonMode()) {
125
+ jsonOut({
126
+ status: 'success',
127
+ command: 'store use',
128
+ data: { ...toJsonEntry(selected), active: true, theme },
129
+ });
130
+ return;
131
+ }
132
+
133
+ showSuccess(t('store.selected', { store: selected.store }));
134
+
135
+ if (theme) {
136
+ console.log(chalk.gray(' ' + t('store.selected_theme', { theme })));
137
+ }
138
+
139
+ if (!selected.usable) {
140
+ console.log(chalk.yellow(' ' + t('store.expired_warning')));
141
+ }
142
+
143
+ console.log();
144
+ }
@@ -6,7 +6,8 @@ import AdmZip from 'adm-zip';
6
6
  import chokidar from 'chokidar';
7
7
  import {createApiClient} from '../api-client.js';
8
8
  import open from 'open';
9
- import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActiveStore} from '../storage.js';
9
+ import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActiveStore, getWorkspaceRoot} from '../storage.js';
10
+ import {decorateThemeName} from '../ui/theme-choice.js';
10
11
  import {brandedConfirm, brandedSelect, showWarning, showError, showInfo} from '../ui/prompt-wrapper.js';
11
12
  import {themeSectionMenuCommand} from './theme-section.js';
12
13
  import { t } from '../i18n.js';
@@ -397,7 +398,7 @@ export async function themeDevCommand(themeName) {
397
398
 
398
399
  console.log();
399
400
  const themeChoices = themes.map(theme => ({
400
- name: `${theme.name} ${chalk.gray(`v${theme.version}`)} ${theme.active ? chalk.green('(Aktif)') : ''}`,
401
+ name: decorateThemeName(theme, themeSlug),
401
402
  value: theme,
402
403
  description: theme.description || `Klasör: ${theme.theme_folder}`
403
404
  }));
@@ -448,7 +449,7 @@ export async function themeDevCommand(themeName) {
448
449
 
449
450
  console.log(chalk.green(t('dev.active_theme_set', { slug: themeSlug })));
450
451
 
451
- let themePath = path.join(process.cwd(), storeSlug, themeSlug);
452
+ let themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
452
453
  console.log(chalk.green(t('dev.working_dir', { path: `${storeSlug}/${themeSlug}/` }) + '\n'));
453
454
 
454
455
  // Fork check: foreign-org themes need fork before development
@@ -480,7 +481,7 @@ export async function themeDevCommand(themeName) {
480
481
 
481
482
  themeInfo = { uuid: forkedData.uuid, name: forkedData.name, version: forkedData.version };
482
483
  themeSlug = slugify(forkedData.name);
483
- themePath = path.join(process.cwd(), storeSlug, themeSlug);
484
+ themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
484
485
 
485
486
  console.log(chalk.green(t('dev.fork_success', { name: forkedData.name })));
486
487
  console.log(chalk.gray(' ' + t('dev.fork_source', { source: forkedData.source.name, version: forkedData.source.version })));
@@ -4,7 +4,7 @@ import {createWriteStream} from 'fs';
4
4
  import path from 'path';
5
5
  import AdmZip from 'adm-zip';
6
6
  import {createApiClient} from '../api-client.js';
7
- import {slugify, getActiveStore} from '../storage.js';
7
+ import {slugify, getActiveStore, getWorkspaceRoot} from '../storage.js';
8
8
  import {brandedConfirm, brandedSelect, showWarning, showInfo, showError} from '../ui/prompt-wrapper.js';
9
9
  import {normalizeStoreDomain} from '../storage.js';
10
10
  import {isJsonMode, jsonOut} from '../output-mode.js';
@@ -79,7 +79,7 @@ export async function themeInitCommand(fromSlug) {
79
79
  const store = await getActiveStore();
80
80
  const storeSlug = normalizeStoreDomain(store);
81
81
  const forkedSlug = slugify(forkedData.name);
82
- const themePath = path.join(process.cwd(), storeSlug, forkedSlug);
82
+ const themePath = path.join(getWorkspaceRoot(), storeSlug, forkedSlug);
83
83
 
84
84
  // Check if directory exists
85
85
  const exists = await fs.access(themePath).then(() => true).catch(() => false);
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { createApiClient } from '../api-client.js';
4
- import { getActiveTheme } from '../storage.js';
4
+ import { resolveActiveThemeUuid, getActiveStore } from '../storage.js';
5
5
  import { mapPushError } from '../errors/error-mapper.js';
6
6
  import { isJsonMode, jsonOut } from '../output-mode.js';
7
7
  import { t } from '../i18n.js';
@@ -91,7 +91,13 @@ export async function themePushCommand(themeName, options = {}) {
91
91
  if (themeName) {
92
92
  themeUuid = themeName;
93
93
  } else {
94
- themeUuid = await getActiveTheme();
94
+ themeUuid = await resolveActiveThemeUuid();
95
+ if (themeUuid && !isJsonMode()) {
96
+ const store = await getActiveStore();
97
+ if (store) {
98
+ console.log(chalk.gray(t('context.line', { store, theme: themeUuid })));
99
+ }
100
+ }
95
101
  if (!themeUuid) {
96
102
  console.error(chalk.red(t('push.no_active_theme')));
97
103
  console.log(chalk.gray(t('push.usage')));
@@ -5,7 +5,7 @@ import {createWriteStream} from 'fs';
5
5
  import path from 'path';
6
6
  import AdmZip from 'adm-zip';
7
7
  import {createApiClient} from '../api-client.js';
8
- import {getActiveTheme, getThemeUuidBySlug, normalizeStoreDomain, getActiveStore} from '../storage.js';
8
+ import {getActiveTheme, getThemeUuidBySlug, normalizeStoreDomain, getActiveStore, getWorkspaceRoot} from '../storage.js';
9
9
  import {brandedConfirm, brandedSelect, brandedSearch, brandedInput, showWarning, showSuccess, showInfo} from '../ui/prompt-wrapper.js';
10
10
  import { t } from '../i18n.js';
11
11
 
@@ -291,7 +291,7 @@ export async function themeSectionAddCommand() {
291
291
 
292
292
  const store = await getActiveStore();
293
293
  const storeSlug = normalizeStoreDomain(store);
294
- const themePath = path.join(process.cwd(), storeSlug, activeTheme);
294
+ const themePath = path.join(getWorkspaceRoot(), storeSlug, activeTheme);
295
295
 
296
296
  try {
297
297
  // Tema klasörü henüz lokalde yoksa (theme dev/pull yapılmamışsa)
@@ -418,7 +418,7 @@ export async function themeSectionCreateCommand() {
418
418
 
419
419
  const store = await getActiveStore();
420
420
  const storeSlug = normalizeStoreDomain(store);
421
- const themePath = path.join(process.cwd(), storeSlug, activeTheme);
421
+ const themePath = path.join(getWorkspaceRoot(), storeSlug, activeTheme);
422
422
 
423
423
  try {
424
424
  await downloadThemeFiles(apiClient, themeInfo.uuid, themePath);
@@ -509,7 +509,7 @@ export async function themeSectionForkCommand() {
509
509
 
510
510
  const store = await getActiveStore();
511
511
  const storeSlug = normalizeStoreDomain(store);
512
- const themePath = path.join(process.cwd(), storeSlug, activeTheme);
512
+ const themePath = path.join(getWorkspaceRoot(), storeSlug, activeTheme);
513
513
 
514
514
  try {
515
515
  await downloadThemeFiles(apiClient, themeInfo.uuid, themePath);
@@ -5,12 +5,15 @@ import {createWriteStream} from 'fs';
5
5
  import path from 'path';
6
6
  import AdmZip from 'adm-zip';
7
7
  import {createApiClient} from '../api-client.js';
8
- import {normalizeStoreDomain, slugify, setActiveTheme, getActiveStore} from '../storage.js';
9
- import {brandedConfirm, brandedSelect, showWarning} from '../ui/prompt-wrapper.js';
8
+ import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActiveStore, getWorkspaceRoot, getWorkspaceContext} from '../storage.js';
9
+ import {brandedConfirm, brandedSelect, brandedSearch, showWarning} from '../ui/prompt-wrapper.js';
10
+ import {themeLabels, decorateThemeName} from '../ui/theme-choice.js';
10
11
  import { isJsonMode, jsonOut } from '../output-mode.js';
11
12
  import { mapError } from '../errors/error-mapper.js';
12
13
  import { t } from '../i18n.js';
13
14
 
15
+ const THEME_SEARCH_THRESHOLD = 7;
16
+
14
17
  export async function themeListCommand() {
15
18
  try {
16
19
  if (!isJsonMode()) {
@@ -46,13 +49,16 @@ export async function themeListCommand() {
46
49
 
47
50
  console.log(chalk.bold.cyan(t('theme.count', { count: themes.length }) + '\n'));
48
51
 
52
+ const localTheme = await getActiveTheme();
53
+
49
54
  themes.forEach((theme) => {
50
- const activeIndicator = theme.active ? chalk.green('●') : chalk.gray('○');
51
- const name = theme.active ? chalk.bold.green(theme.name) : chalk.white(theme.name);
52
- const version = chalk.gray(`v${theme.version}`);
55
+ const labels = themeLabels(theme, localTheme);
56
+ const indicator = labels.includes('selected')
57
+ ? chalk.cyan('●')
58
+ : (labels.includes('published') ? chalk.green('●') : chalk.gray('○'));
53
59
  const description = theme.description ? chalk.gray(` - ${theme.description}`) : '';
54
60
 
55
- console.log(`${activeIndicator} ${name} ${version}${description}`);
61
+ console.log(`${indicator} ${decorateThemeName(theme, localTheme)}${description}`);
56
62
  console.log(chalk.gray(` ${t('theme.folder_label')} ${theme.theme_folder || theme.alias}`));
57
63
  console.log();
58
64
  });
@@ -103,7 +109,7 @@ export async function themeCreateCommand(themeName) {
103
109
 
104
110
  const themeSlug = slugify(name);
105
111
  const storeSlug = normalizeStoreDomain(store);
106
- const themePath = path.join(process.cwd(), storeSlug, themeSlug);
112
+ const themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
107
113
 
108
114
  const exists = await fs.access(themePath).then(() => true).catch(() => false);
109
115
  if (exists) {
@@ -174,7 +180,7 @@ export async function themeCreateCommand(themeName) {
174
180
  impact: 'Yeni tema güvenli bir şekilde oluşturulamaz.'
175
181
  },
176
182
  'default_theme_not_installed': {
177
- title: '⚠️ Default Tema Yüklenmemiş',
183
+ title: 'Default Tema Yüklenmemiş',
178
184
  description: 'Sisteminizde default tema bulunamadı.',
179
185
  impact: 'Default tema olmadan yeni tema geliştirilemez.'
180
186
  },
@@ -184,7 +190,7 @@ export async function themeCreateCommand(themeName) {
184
190
  impact: 'Güncel özellikler ve düzeltmeler yeni temanızda eksik olacak.'
185
191
  },
186
192
  'check_failed': {
187
- title: '⚠️ Default Tema Kontrolü Başarısız',
193
+ title: 'Default Tema Kontrolü Başarısız',
188
194
  description: 'Default tema güncellik kontrolü yapılamadı.',
189
195
  impact: 'Güvenlik nedeniyle default tema güncellenmelidir.'
190
196
  },
@@ -196,7 +202,7 @@ export async function themeCreateCommand(themeName) {
196
202
  };
197
203
 
198
204
  const reasonInfo = reasonDetails[errorData.reason] || {
199
- title: '⚠️ Default Tema Güncelleme Gerekli',
205
+ title: 'Default Tema Güncelleme Gerekli',
200
206
  description: errorData.reason || 'Bilinmeyen bir durum tespit edildi.',
201
207
  impact: 'Devam etmek için default tema güncellenmelidir.'
202
208
  };
@@ -236,7 +242,7 @@ export async function themeCreateCommand(themeName) {
236
242
  console.log(chalk.gray('🔹 Layout ve section\'lar import ediliyor...'));
237
243
  console.log(chalk.gray('🔹 Yeni tema bootstrap ediliyor...'));
238
244
  console.log(chalk.gray('━'.repeat(60)));
239
- console.log(chalk.yellow('\n⏱️ Lütfen bekleyin, bu işlem birkaç dakika sürebilir...\n'));
245
+ console.log(chalk.yellow('\n Lütfen bekleyin, bu işlem birkaç dakika sürebilir...\n'));
240
246
 
241
247
  // forceUpdate: true ile tekrar dene
242
248
  themeData.forceUpdate = true;
@@ -347,8 +353,10 @@ export async function themePullCommand() {
347
353
  return;
348
354
  }
349
355
 
356
+ const localTheme = await getActiveTheme();
357
+
350
358
  const themeChoices = themes.map(theme => ({
351
- name: `${theme.name} ${chalk.gray(`v${theme.version}`)} ${theme.active ? chalk.green('(Aktif)') : ''}`,
359
+ name: decorateThemeName(theme, localTheme),
352
360
  value: theme,
353
361
  description: theme.description || `${t('theme.folder_label')} ${theme.theme_folder}`
354
362
  }));
@@ -369,7 +377,7 @@ export async function themePullCommand() {
369
377
  const themeUuid = selectedTheme.theme_folder;
370
378
  const store = await getActiveStore();
371
379
  const storeSlug = normalizeStoreDomain(store);
372
- const themePath = path.join(process.cwd(), storeSlug, themeSlug);
380
+ const themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
373
381
 
374
382
  const exists = await fs.access(themePath).then(() => true).catch(() => false);
375
383
  if (exists) {
@@ -411,7 +419,7 @@ export async function themePullCommand() {
411
419
 
412
420
  console.log(chalk.green(t('theme.extracted', { path: `${storeSlug}/${themeSlug}/` })));
413
421
 
414
- await setActiveTheme(themeSlug);
422
+ await setActiveTheme(themeUuid);
415
423
  console.log(chalk.green(t('theme.active_set') + '\n'));
416
424
 
417
425
  console.log(chalk.bold.green(t('theme.pull.success')));
@@ -429,6 +437,14 @@ export async function themeUseCommand(themeName = null) {
429
437
  try {
430
438
  console.log(chalk.cyan(t('theme.use.starting') + '\n'));
431
439
 
440
+ const store = await getActiveStore();
441
+ const context = getWorkspaceContext();
442
+ if (context?.store === store && context.theme) {
443
+ console.log(chalk.gray(t('context.workspace', { store, theme: context.theme })) + '\n');
444
+ } else if (store) {
445
+ console.log(chalk.gray(t('theme.use.on_store', { store })) + '\n');
446
+ }
447
+
432
448
  const apiClient = await createApiClient();
433
449
  const response = await apiClient.get('/theme');
434
450
  const themes = response.data || [];
@@ -453,21 +469,29 @@ export async function themeUseCommand(themeName = null) {
453
469
  process.exit(1);
454
470
  }
455
471
  } else {
472
+ const localTheme = await getActiveTheme();
473
+
456
474
  const themeChoices = themes.map(theme => ({
457
- name: `${theme.name} ${chalk.gray(`v${theme.version}`)} ${theme.active ? chalk.green('(Şu an aktif)') : ''}`,
475
+ name: decorateThemeName(theme, localTheme),
458
476
  value: theme,
459
- description: theme.description || `${t('theme.folder_label')} ${theme.theme_folder}`
477
+ description: theme.description || `${t('theme.folder_label')} ${theme.theme_folder}`,
478
+ keywords: [theme.theme_folder, theme.alias].filter(Boolean),
460
479
  }));
461
480
 
462
- selectedTheme = await brandedSelect({
463
- message: t('theme.use.select_prompt'),
464
- choices: themeChoices,
465
- }, t('theme.use.select_title'));
481
+ selectedTheme = themes.length > THEME_SEARCH_THRESHOLD
482
+ ? await brandedSearch({
483
+ message: t('theme.use.search_prompt'),
484
+ choices: themeChoices,
485
+ }, t('theme.use.select_title'))
486
+ : await brandedSelect({
487
+ message: t('theme.use.select_prompt'),
488
+ choices: themeChoices,
489
+ }, t('theme.use.select_title'));
466
490
  }
467
491
 
468
492
  const themeSlug = slugify(selectedTheme.name);
469
493
 
470
- await setActiveTheme(themeSlug);
494
+ await setActiveTheme(selectedTheme.theme_folder);
471
495
 
472
496
  console.log(chalk.green('\n' + t('theme.use.set', { name: chalk.bold(selectedTheme.name) })));
473
497
  console.log(chalk.gray(' ' + t('theme.use.folder', { folder: themeSlug }) + '\n'));
package/src/index.js CHANGED
@@ -17,6 +17,8 @@ import { themePushCommand } from './commands/theme-push.js';
17
17
  import { themeSubmitCommand } from './commands/theme-submit.js';
18
18
  import { themeInitCommand } from './commands/theme-init.js';
19
19
  import { orgSwitchCommand } from './commands/org-switch.js';
20
+ import { storeListCommand, storeUseCommand } from './commands/store.js';
21
+ import { setStoreOverride, applyWorkspaceContext } from './storage.js';
20
22
 
21
23
  const require = createRequire(import.meta.url);
22
24
  const { version } = require('../package.json');
@@ -33,9 +35,10 @@ program
33
35
  // Global flags
34
36
  program.option('--json', 'Machine-readable JSON output for CI/CD');
35
37
  program.option('--verbose', 'Verbose debug output');
38
+ program.option('--store <domain>', 'Run this command against a specific store');
36
39
 
37
40
  // preAction hook: runs before every command
38
- program.hook('preAction', (thisCommand) => {
41
+ program.hook('preAction', async (thisCommand) => {
39
42
  const opts = thisCommand.opts();
40
43
  if (opts.json) {
41
44
  setJsonMode(true);
@@ -44,6 +47,10 @@ program.hook('preAction', (thisCommand) => {
44
47
  if (opts.verbose) {
45
48
  setVerboseMode(true);
46
49
  }
50
+ if (opts.store) {
51
+ setStoreOverride(opts.store);
52
+ }
53
+ await applyWorkspaceContext();
47
54
  });
48
55
 
49
56
  // Show branded help when no arguments provided
@@ -153,6 +160,25 @@ theme
153
160
  await themeInitCommand(options.from);
154
161
  });
155
162
 
163
+ // store commands
164
+ const store = program
165
+ .command('store')
166
+ .description('Store selection commands');
167
+
168
+ store
169
+ .command('list')
170
+ .description('List the stores you are signed in to')
171
+ .action(async () => {
172
+ await storeListCommand();
173
+ });
174
+
175
+ store
176
+ .command('use [store-domain]')
177
+ .description('Set the active store')
178
+ .action(async (storeDomain) => {
179
+ await storeUseCommand(storeDomain);
180
+ });
181
+
156
182
  // org commands
157
183
  const org = program
158
184
  .command('org')
package/src/storage.js CHANGED
@@ -10,6 +10,12 @@ import * as config from './config.js';
10
10
  */
11
11
  const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.tsoft-cli');
12
12
 
13
+ const DEFAULT_THEME_FOLDER = 'default';
14
+
15
+ const ACTIVE_THEME_FILE = 'active-theme';
16
+
17
+ const LEGACY_ACTIVE_THEME_FILE = 'active-theme.txt';
18
+
13
19
  /**
14
20
  * Store domain'i dizin adına uygun formata çevirir
15
21
  * @param {string} domain - Store domain (örn: birtan.1isim.com)
@@ -55,23 +61,201 @@ export function getStoreEnvFile(store) {
55
61
  export async function setActiveStore(store) {
56
62
  const activeStoreFile = path.join(GLOBAL_CONFIG_DIR, 'active-store.txt');
57
63
  await fs.mkdir(GLOBAL_CONFIG_DIR, { recursive: true });
64
+
65
+ const previous = await readTrimmed(activeStoreFile);
66
+
67
+ if (previous && previous !== store) {
68
+ await claimLegacyThemeFor(previous);
69
+ }
70
+
58
71
  await fs.writeFile(activeStoreFile, store, 'utf-8');
59
72
  }
60
73
 
74
+ /**
75
+ * Global tema dosyasını sahibi olan mağazanın dizinine taşır.
76
+ *
77
+ * Aktif mağaza değişmeden önce çağrılır; global dosya yalnızca o ana kadar
78
+ * aktif olan mağazaya aittir ve yeni mağaza tarafından devralınamaz.
79
+ */
80
+ async function claimLegacyThemeFor(store) {
81
+ const legacyFile = path.join(GLOBAL_CONFIG_DIR, LEGACY_ACTIVE_THEME_FILE);
82
+ const legacy = await readTrimmed(legacyFile);
83
+
84
+ if (!legacy) {
85
+ return;
86
+ }
87
+
88
+ const storeThemeFile = path.join(getStoreConfigDir(store), ACTIVE_THEME_FILE);
89
+
90
+ if (await readTrimmed(storeThemeFile) === null) {
91
+ await fs.mkdir(getStoreConfigDir(store), { recursive: true });
92
+ await fs.writeFile(storeThemeFile, legacy, 'utf-8');
93
+ }
94
+
95
+ await fs.rm(legacyFile, { force: true });
96
+ }
97
+
61
98
  /**
62
99
  * Aktif store'u okur
63
100
  * @returns {Promise<string|null>} Store domain veya null
64
101
  */
65
102
  export async function getActiveStore() {
103
+ if (storeOverride) {
104
+ return storeOverride;
105
+ }
106
+
107
+ if (workspaceContext) {
108
+ return workspaceContext.store;
109
+ }
110
+
111
+ return await readTrimmed(path.join(GLOBAL_CONFIG_DIR, 'active-store.txt'));
112
+ }
113
+
114
+ let storeOverride = null;
115
+
116
+ let workspaceContext = null;
117
+
118
+ /**
119
+ * Tek komutluk mağaza geçişi. Kayıtlı aktif mağazayı değiştirmez.
120
+ * @param {string|null} store - Mağaza domain'i
121
+ */
122
+ export function setStoreOverride(store) {
123
+ storeOverride = store || null;
124
+ }
125
+
126
+ /**
127
+ * Çalışma dizininden mağaza ve tema bağlamını çözer.
128
+ *
129
+ * Yukarı yürürken dizin adı rastgele bir desene değil, kayıtlı mağaza
130
+ * envanterine eşlenir. Eşleşen en dıştaki atadizin mağaza köküdür; gerçek
131
+ * domain o mağazanın config dosyasından okunur.
132
+ *
133
+ * @param {string} [fromDir] - Başlangıç dizini (verilmezse çalışma dizini)
134
+ * @returns {Promise<{store: string, theme: string|null, root: string, storeDir: string}|null>}
135
+ */
136
+ export async function detectWorkspaceContext(fromDir = process.cwd()) {
137
+ const known = await listKnownStores();
138
+
139
+ if (known.length === 0) {
140
+ return null;
141
+ }
142
+
143
+ const bySlug = new Map(known.map(entry => [entry.slug.toLowerCase(), entry.store]));
144
+ const segments = path.resolve(fromDir).split(path.sep);
145
+
146
+ let matchIndex = -1;
147
+
148
+ for (let i = 1; i < segments.length; i++) {
149
+ if (bySlug.has(segments[i].toLowerCase())) {
150
+ matchIndex = i;
151
+ break;
152
+ }
153
+ }
154
+
155
+ if (matchIndex === -1) {
156
+ return null;
157
+ }
158
+
159
+ return {
160
+ store: bySlug.get(segments[matchIndex].toLowerCase()),
161
+ theme: segments[matchIndex + 1] ?? null,
162
+ root: segments.slice(0, matchIndex).join(path.sep) || path.sep,
163
+ storeDir: segments.slice(0, matchIndex + 1).join(path.sep) || path.sep,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Çalışma dizini bağlamını komut süresince etkin kılar. Kayıtlı duruma yazmaz.
169
+ *
170
+ * @param {string} [fromDir] - Başlangıç dizini
171
+ * @returns {Promise<object|null>} Bulunan bağlam veya null
172
+ */
173
+ export async function applyWorkspaceContext(fromDir = process.cwd()) {
174
+ workspaceContext = await detectWorkspaceContext(fromDir);
175
+
176
+ return workspaceContext;
177
+ }
178
+
179
+ /**
180
+ * Etkin çalışma dizini bağlamını döner
181
+ * @returns {object|null}
182
+ */
183
+ export function getWorkspaceContext() {
184
+ return workspaceContext;
185
+ }
186
+
187
+ /**
188
+ * Tema dizinlerinin altına açılacağı kök.
189
+ *
190
+ * Bağlam varsa mağaza dizininin üstü, yoksa çalışma dizini. Kullanıcı bir tema
191
+ * dizininin içindeyken komut çalıştırdığında dizinlerin iç içe açılmasını önler.
192
+ *
193
+ * @param {string} [fallbackDir] - Bağlam yoksa kullanılacak dizin
194
+ * @returns {string}
195
+ */
196
+ export function getWorkspaceRoot(fallbackDir = process.cwd()) {
197
+ return workspaceContext?.root ?? fallbackDir;
198
+ }
199
+
200
+ /**
201
+ * Kayıtlı mağazaları oturum durumuyla birlikte listeler
202
+ * @returns {Promise<Array<{store: string, slug: string, usable: boolean, daysLeft: number, active: boolean, user: object|null}>>}
203
+ */
204
+ export async function listKnownStores() {
205
+ let entries;
66
206
  try {
67
- const activeStoreFile = path.join(GLOBAL_CONFIG_DIR, 'active-store.txt');
68
- return (await fs.readFile(activeStoreFile, 'utf-8')).trim();
207
+ entries = await fs.readdir(GLOBAL_CONFIG_DIR, { withFileTypes: true });
69
208
  } catch (error) {
70
209
  if (error.code === 'ENOENT') {
71
- return null;
210
+ return [];
72
211
  }
73
212
  throw error;
74
213
  }
214
+
215
+ const activeStore = await getActiveStore();
216
+ const now = Date.now();
217
+ const stores = [];
218
+
219
+ for (const entry of entries) {
220
+ if (!entry.isDirectory()) {
221
+ continue;
222
+ }
223
+
224
+ let tokens;
225
+ try {
226
+ const raw = await fs.readFile(
227
+ path.join(GLOBAL_CONFIG_DIR, entry.name, 'config.json'),
228
+ 'utf-8'
229
+ );
230
+ tokens = JSON.parse(raw);
231
+ } catch {
232
+ continue;
233
+ }
234
+
235
+ const store = tokens.store ?? entry.name;
236
+ const expiresAt = tokens.refreshTokenExpiresAt
237
+ ? new Date(tokens.refreshTokenExpiresAt).getTime()
238
+ : NaN;
239
+ const daysLeft = Number.isNaN(expiresAt)
240
+ ? 0
241
+ : Math.round((expiresAt - now) / 86400000);
242
+
243
+ stores.push({
244
+ store,
245
+ slug: entry.name,
246
+ usable: !Number.isNaN(expiresAt) && expiresAt > now,
247
+ daysLeft,
248
+ active: store === activeStore,
249
+ user: tokens.user ?? null,
250
+ });
251
+ }
252
+
253
+ return stores.sort((a, b) => {
254
+ if (a.usable !== b.usable) {
255
+ return a.usable ? -1 : 1;
256
+ }
257
+ return a.store.localeCompare(b.store);
258
+ });
75
259
  }
76
260
 
77
261
  /**
@@ -266,9 +450,6 @@ export async function saveTokens(tokens, store) {
266
450
 
267
451
  // .env dosyasını da oluştur
268
452
  await writeEnvFile(tokens, store);
269
-
270
- // Aktif store'u ayarla
271
- await setActiveStore(store);
272
453
  }
273
454
 
274
455
  /**
@@ -374,9 +555,11 @@ export async function deleteConfig(store = null) {
374
555
  const configDir = getStoreConfigDir(store);
375
556
  await fs.rm(configDir, { recursive: true, force: true });
376
557
 
377
- // Active store dosyasını da sil
378
558
  const activeStoreFile = path.join(GLOBAL_CONFIG_DIR, 'active-store.txt');
379
- await fs.unlink(activeStoreFile).catch(() => {}); // Hata varsa sessizce yoksay
559
+
560
+ if (await readTrimmed(activeStoreFile) === store) {
561
+ await fs.unlink(activeStoreFile).catch(() => {});
562
+ }
380
563
  } catch (error) {
381
564
  if (error.code !== 'ENOENT') {
382
565
  throw error;
@@ -442,20 +625,64 @@ export function slugify(text) {
442
625
  * @param {string} theme - Tema adı
443
626
  * @returns {Promise<void>}
444
627
  */
445
- export async function setActiveTheme(theme) {
446
- const activeThemeFile = path.join(GLOBAL_CONFIG_DIR, 'active-theme.txt');
447
- await fs.mkdir(GLOBAL_CONFIG_DIR, { recursive: true });
448
- await fs.writeFile(activeThemeFile, theme, 'utf-8');
628
+ export async function setActiveTheme(theme, store = null) {
629
+ const target = store ?? await getActiveStore();
630
+
631
+ if (!target) {
632
+ const legacyFile = path.join(GLOBAL_CONFIG_DIR, LEGACY_ACTIVE_THEME_FILE);
633
+ await fs.mkdir(GLOBAL_CONFIG_DIR, { recursive: true });
634
+ await fs.writeFile(legacyFile, theme, 'utf-8');
635
+ return;
636
+ }
637
+
638
+ const configDir = getStoreConfigDir(target);
639
+ await fs.mkdir(configDir, { recursive: true });
640
+ await fs.writeFile(path.join(configDir, ACTIVE_THEME_FILE), theme, 'utf-8');
449
641
  }
450
642
 
451
643
  /**
452
- * Aktif temayı okur
644
+ * Aktif mağazanın temasını okur.
645
+ *
646
+ * Tema bilgisi mağaza dizininde tutulur. Eski kurulumlarda global dosyada
647
+ * duruyor olabilir; o değer yalnızca global aktif mağaza ile eşleşen mağaza
648
+ * için devralınır ve mağaza dizinine yazılır.
649
+ *
650
+ * @param {string} [store] - Mağaza domain'i (verilmezse aktif mağaza)
453
651
  * @returns {Promise<string|null>} Tema adı veya null
454
652
  */
455
- export async function getActiveTheme() {
653
+ export async function getActiveTheme(store = null) {
654
+ const target = store ?? await getActiveStore();
655
+
656
+ if (!store && !storeOverride && workspaceContext?.theme && workspaceContext.store === target) {
657
+ return workspaceContext.theme;
658
+ }
659
+
660
+ if (target) {
661
+ const stored = await readTrimmed(
662
+ path.join(getStoreConfigDir(target), ACTIVE_THEME_FILE)
663
+ );
664
+
665
+ if (stored) {
666
+ return stored;
667
+ }
668
+ }
669
+
670
+ const legacy = await readTrimmed(
671
+ path.join(GLOBAL_CONFIG_DIR, LEGACY_ACTIVE_THEME_FILE)
672
+ );
673
+
674
+ if (!legacy || !target) {
675
+ return legacy;
676
+ }
677
+
678
+ await claimLegacyThemeFor(target);
679
+
680
+ return legacy;
681
+ }
682
+
683
+ async function readTrimmed(file) {
456
684
  try {
457
- const activeThemeFile = path.join(GLOBAL_CONFIG_DIR, 'active-theme.txt');
458
- return (await fs.readFile(activeThemeFile, 'utf-8')).trim();
685
+ return (await fs.readFile(file, 'utf-8')).trim() || null;
459
686
  } catch (error) {
460
687
  if (error.code === 'ENOENT') {
461
688
  return null;
@@ -464,6 +691,44 @@ export async function getActiveTheme() {
464
691
  }
465
692
  }
466
693
 
694
+ const THEME_UUID_PATTERN =
695
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
696
+
697
+ /**
698
+ * Değerin tema folder UUID'si olup olmadığını söyler
699
+ * @param {string|null|undefined} value
700
+ * @returns {boolean}
701
+ */
702
+ export function isThemeUuid(value) {
703
+ return typeof value === 'string' && THEME_UUID_PATTERN.test(value);
704
+ }
705
+
706
+ /**
707
+ * Aktif temanın theme_folder değerini döner.
708
+ *
709
+ * Dosyada UUID varsa doğrudan kullanılır. Eski kurulumlarda slug yazılı
710
+ * olabilir; slug bir kez API üzerinden çözülür ve dosya UUID'ye güncellenir.
711
+ *
712
+ * @returns {Promise<string|null>} theme_folder veya aktif tema yoksa null
713
+ */
714
+ export async function resolveActiveThemeUuid() {
715
+ const stored = await getActiveTheme();
716
+
717
+ if (!stored) {
718
+ return null;
719
+ }
720
+
721
+ if (isThemeUuid(stored) || stored === DEFAULT_THEME_FOLDER) {
722
+ return stored;
723
+ }
724
+
725
+ const { uuid } = await getThemeUuidBySlug(stored);
726
+
727
+ await setActiveTheme(uuid);
728
+
729
+ return uuid;
730
+ }
731
+
467
732
  /**
468
733
  * Tema slug'ından UUID/folder adını bulur
469
734
  * @param {string} themeSlug - Tema slug (örn: 'my-theme')
@@ -475,7 +740,8 @@ export async function getThemeUuidBySlug(themeSlug) {
475
740
  const response = await apiClient.get('/theme');
476
741
  const themes = response.data || [];
477
742
 
478
- const theme = themes.find(t => slugify(t.name) === themeSlug);
743
+ const theme = themes.find(t => t.theme_folder === themeSlug)
744
+ ?? themes.find(t => slugify(t.name) === themeSlug);
479
745
 
480
746
  if (!theme) {
481
747
  throw new Error(`Tema bulunamadı: ${themeSlug}`);
@@ -1,5 +1,6 @@
1
1
  import boxen from 'boxen';
2
2
  import gradient from 'gradient-string';
3
+ import stringWidth from 'string-width';
3
4
  import chalk from 'chalk';
4
5
  import {input as inquirerInput, confirm as inquirerConfirm, select as inquirerSelect, search as inquirerSearch} from '@inquirer/prompts';
5
6
  import { t } from '../i18n.js';
@@ -29,9 +30,14 @@ export function createTSoftGradient(text) {
29
30
  */
30
31
  export function showBrandedBox(message, title = '', options = {}) {
31
32
  // T-Soft branding'ini mesajın sonuna sağa hizalanmış olarak ekle
32
- const tsoftBranding = createTSoftGradient('T-Soft');
33
- const brandingPadding = ' '.repeat(50); // Sağa hizalama için boşluk
34
- const messageWithBranding = `${message}\n\n${brandingPadding}${tsoftBranding}`;
33
+ const brandingText = 'T-Soft';
34
+ const contentWidth = Math.max(
35
+ ...message.split('\n').map(stringWidth),
36
+ stringWidth(title) + 2,
37
+ stringWidth(brandingText)
38
+ );
39
+ const brandingPadding = ' '.repeat(Math.max(0, contentWidth - stringWidth(brandingText)));
40
+ const messageWithBranding = `${message}\n\n${brandingPadding}${createTSoftGradient(brandingText)}`;
35
41
 
36
42
  const defaultOptions = {
37
43
  padding: 1,
@@ -0,0 +1,33 @@
1
+ import chalk from 'chalk';
2
+ import { slugify } from '../storage.js';
3
+ import { t } from '../i18n.js';
4
+
5
+ export function themeLabels(theme, selectedTheme) {
6
+ const labels = [];
7
+
8
+ const folder = theme.theme_folder || theme.alias;
9
+
10
+ if (selectedTheme && (folder === selectedTheme || slugify(theme.name) === selectedTheme)) {
11
+ labels.push('selected');
12
+ }
13
+
14
+ if (theme.active) {
15
+ labels.push('published');
16
+ }
17
+
18
+ return labels;
19
+ }
20
+
21
+ export function decorateThemeName(theme, selectedTheme) {
22
+ const labels = themeLabels(theme, selectedTheme);
23
+
24
+ const name = labels.includes('selected')
25
+ ? chalk.bold.cyan(theme.name)
26
+ : chalk.white(theme.name);
27
+
28
+ const marks = labels.map(label => label === 'selected'
29
+ ? chalk.cyan(t('theme.mark.selected'))
30
+ : chalk.green(t('theme.mark.published')));
31
+
32
+ return [name, chalk.gray(`v${theme.version}`), ...marks].join(' ');
33
+ }