tsoft-cli 3.10.2 → 3.12.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 CHANGED
@@ -5,6 +5,39 @@ 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.0] - 2026-08-05
9
+
10
+ ### Added
11
+ - 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.
12
+ - 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.
13
+
14
+ ### Fixed
15
+ - `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.
16
+
17
+ ## [3.11.0] - 2026-08-05
18
+
19
+ ### Added
20
+ - `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.
21
+ - `--store <domain>` runs a single command against another store without changing the one you are on: `tsoft --store shop.example.com theme push`.
22
+ - 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.
23
+
24
+ ### Fixed
25
+ - 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`.
26
+ - Signing out of one store no longer clears the active store when you were on a different one.
27
+
28
+ ### Changed
29
+ - 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.
30
+ - `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.
31
+
32
+ ## [3.10.3] - 2026-08-05
33
+
34
+ ### Fixed
35
+ - `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.
36
+ - 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.
37
+ - `tsoft theme section` commands accept a stored theme folder as well as a slug, so they keep working through and after the migration.
38
+ - 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.
39
+ - 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.
40
+
8
41
  ## [3.10.2] - 2026-08-03
9
42
 
10
43
  ### 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",
@@ -174,11 +174,11 @@
174
174
  "dev.lock_acquired": "✓ Lock successfully acquired",
175
175
  "dev.already_locked": "✓ You already have the lock",
176
176
  "dev.already_synced": "✓ Theme is synchronized",
177
- "dev.not_synced": "⚠️ Lock exists but theme is not synchronized",
177
+ "dev.not_synced": "Lock exists but theme is not synchronized",
178
178
  "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
179
  "dev.lock_warning_title": "🔒 Lock Warning",
180
180
  "dev.lock_override": "🔐 Acquiring lock (override)...",
181
- "dev.stopping": "⏸️ Shutting down development environment...",
181
+ "dev.stopping": "Shutting down development environment...",
182
182
  "dev.watch_stopped": "✓ File watching stopped",
183
183
  "dev.keepalive_stopped": "✓ Keep-alive stopped",
184
184
  "dev.lock_kept": "(Lock kept active - you can continue next time)",
@@ -191,12 +191,12 @@
191
191
  "dev.lock_stolen_owner": "New lock owner: {{owner}}",
192
192
  "dev.lock_stolen_stopping": "Stopping development environment...",
193
193
  "dev.lock_lost": "❌ Theme was locked by another developer!",
194
- "dev.not_synced_pulling": "⚠️ Theme is not synced! Pulling...",
194
+ "dev.not_synced_pulling": "Theme is not synced! Pulling...",
195
195
  "dev.file_deleted": "🗑️ {{path}} → Deleted",
196
196
  "dev.file_delete_failed": "{{path}} → Could not delete: {{error}}",
197
197
  "dev.file_saved": "✓ {{path}} → Saved ({{duration}}ms)",
198
198
  "dev.file_error": "{{path}} → Error: {{error}}",
199
- "dev.keepalive_error": "⚠️ Keep-alive error (will retry)",
199
+ "dev.keepalive_error": "Keep-alive error (will retry)",
200
200
  "init.no_themes": "No themes found.",
201
201
  "init.select_prompt": "Which theme would you like to fork?",
202
202
  "init.select_title": "Theme Selection",
@@ -219,14 +219,14 @@
219
219
  "section.list.no_active_theme": "No active theme found. Please run \"tsoft theme use <theme-name>\" first.",
220
220
  "section.list.starting": "Listing sections...",
221
221
  "section.list.active_theme": "Active Theme: {{theme}}",
222
- "section.list.no_blocks": "⚠️ No blocks found yet.",
222
+ "section.list.no_blocks": "No blocks found yet.",
223
223
  "section.list.count": "Total {{count}} blocks found:",
224
224
  "section.list.id_label": "ID:",
225
225
  "section.list.folder_label": "Folder:",
226
226
  "section.list.sections_label": "Available sections ({{count}}):",
227
227
  "section.list.tags_label": "Tags:",
228
228
  "section.list.file_label": "File:",
229
- "section.list.no_sections": "⚠️ No sections found for this block",
229
+ "section.list.no_sections": "No sections found for this block",
230
230
  "section.add.no_active_theme": "No active theme found. Please run \"tsoft theme use <theme-name>\" first.",
231
231
  "section.add.starting": "🔧 Starting section add operation...",
232
232
  "section.add.active_theme": "Active Theme: {{theme}}",
@@ -247,7 +247,7 @@
247
247
  "section.add.cancelled": "Operation cancelled.",
248
248
  "section.add.adding": "🔄 Adding sections...",
249
249
  "section.add.added": "✓ {{name}} added",
250
- "section.add.already_exists": "⚠️ {{name}} already exists.",
250
+ "section.add.already_exists": "{{name}} already exists.",
251
251
  "section.add.overwrite_prompt": "What would you like to do?",
252
252
  "section.add.overwrite": "Overwrite",
253
253
  "section.add.overwrite_all": "Overwrite all",
@@ -266,7 +266,7 @@
266
266
  "section.add.files_updated": "✓ Theme files updated",
267
267
  "section.add.complete": "🎉 Operation completed!",
268
268
  "section.add.location": "Location: {{path}}",
269
- "section.add.download_error": "⚠️ Error while updating theme files:",
269
+ "section.add.download_error": "Error while updating theme files:",
270
270
  "section.add.manual_tip": "💡 Try downloading the theme manually:",
271
271
  "section.add.manual_command": "tsoft theme pull",
272
272
  "section.menu.title": "🧩 Section Operations",
@@ -337,5 +337,23 @@
337
337
  "dev.section_synced": "🧩 section updated: {{folder}}",
338
338
  "dev.status_section": "section",
339
339
  "dev.section_menu_opening": "Opening section menu...",
340
- "dev.section_menu_resumed": "Resuming watch."
340
+ "dev.section_menu_resumed": "Resuming watch.",
341
+ "store.none": "No stores are saved yet.",
342
+ "store.login_hint": "Sign in first: tsoft login",
343
+ "store.count": "{{count}} saved stores",
344
+ "store.session_expired": "— session expired",
345
+ "store.session_days": "({{days}} days)",
346
+ "store.select_prompt": "Which store will you work on?",
347
+ "store.select_title": "Store Selection",
348
+ "store.selected": "Active store: {{store}}",
349
+ "store.selected_theme": "Theme: {{theme}}",
350
+ "store.expired_warning": "This store's session has expired. To continue: tsoft login",
351
+ "store.not_found": "No saved store matches: {{store}}",
352
+ "store.json_needs_argument": "A store name is required in JSON mode.",
353
+ "store.json_hint": "Usage: tsoft --json store use <domain>",
354
+ "context.line": "Store: {{store}} · Theme: {{theme}}",
355
+ "store.search_prompt": "Search or pick a store",
356
+ "theme.use.on_store": "Store: {{store}}",
357
+ "theme.use.search_prompt": "Search or pick a theme",
358
+ "context.workspace": "From this directory: {{store}} · {{theme}}"
341
359
  }
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",
@@ -174,11 +174,11 @@
174
174
  "dev.lock_acquired": "✓ Kilit başarıyla alındı",
175
175
  "dev.already_locked": "✓ Zaten sizin kiliniz var",
176
176
  "dev.already_synced": "✓ Tema senkronize",
177
- "dev.not_synced": "⚠️ Kilit var ama tema senkronize değil",
177
+ "dev.not_synced": "Kilit var ama tema senkronize değil",
178
178
  "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
179
  "dev.lock_warning_title": "🔒 Kilit Uyarısı",
180
180
  "dev.lock_override": "🔐 Kilit alınıyor (override)...",
181
- "dev.stopping": "⏸️ Geliştirme ortamı kapatılıyor...",
181
+ "dev.stopping": "Geliştirme ortamı kapatılıyor...",
182
182
  "dev.watch_stopped": "✓ Dosya izleme durduruldu",
183
183
  "dev.keepalive_stopped": "✓ Keep-alive durduruldu",
184
184
  "dev.lock_kept": "(Lock aktif bırakıldı - sonraki sefer devam edebilirsiniz)",
@@ -191,12 +191,12 @@
191
191
  "dev.lock_stolen_owner": "Yeni kilit sahibi: {{owner}}",
192
192
  "dev.lock_stolen_stopping": "Geliştirme ortamı durduruluyor...",
193
193
  "dev.lock_lost": "❌ Tema başka bir developer tarafından kilitlendi!",
194
- "dev.not_synced_pulling": "⚠️ Tema senkronize değil! Pull yapılıyor...",
194
+ "dev.not_synced_pulling": "Tema senkronize değil! Pull yapılıyor...",
195
195
  "dev.file_deleted": "🗑️ {{path}} → Silindi",
196
196
  "dev.file_delete_failed": "{{path}} → Silinemedi: {{error}}",
197
197
  "dev.file_saved": "✓ {{path}} → Kaydedildi ({{duration}}ms)",
198
198
  "dev.file_error": "{{path}} → Hata: {{error}}",
199
- "dev.keepalive_error": "⚠️ Keep-alive hatası (yeniden denenecek)",
199
+ "dev.keepalive_error": "Keep-alive hatası (yeniden denenecek)",
200
200
  "init.no_themes": "Tema bulunamadı.",
201
201
  "init.select_prompt": "Hangi temayi fork etmek istiyorsunuz?",
202
202
  "init.select_title": "Tema Seçimi",
@@ -219,14 +219,14 @@
219
219
  "section.list.no_active_theme": "Aktif tema bulunamadı. Lütfen önce \"tsoft theme use <tema-adı>\" komutunu çalıştırın.",
220
220
  "section.list.starting": "Section'lar listeleniyor...",
221
221
  "section.list.active_theme": "Aktif Tema: {{theme}}",
222
- "section.list.no_blocks": "⚠️ Henüz block bulunmuyor.",
222
+ "section.list.no_blocks": "Henüz block bulunmuyor.",
223
223
  "section.list.count": "Toplam {{count}} block bulundu:",
224
224
  "section.list.id_label": "ID:",
225
225
  "section.list.folder_label": "Klasör:",
226
226
  "section.list.sections_label": "Mevcut section'lar ({{count}}):",
227
227
  "section.list.tags_label": "Etiketler:",
228
228
  "section.list.file_label": "Dosya:",
229
- "section.list.no_sections": "⚠️ Bu block için section bulunmuyor",
229
+ "section.list.no_sections": "Bu block için section bulunmuyor",
230
230
  "section.add.no_active_theme": "Aktif tema bulunamadı. Lütfen önce \"tsoft theme use <tema-adı>\" komutunu çalıştırın.",
231
231
  "section.add.starting": "🔧 Section ekleme işlemi başlatılıyor...",
232
232
  "section.add.active_theme": "Aktif Tema: {{theme}}",
@@ -247,7 +247,7 @@
247
247
  "section.add.cancelled": "İşlem iptal edildi.",
248
248
  "section.add.adding": "🔄 Section'lar ekleniyor...",
249
249
  "section.add.added": "✓ {{name}} eklendi",
250
- "section.add.already_exists": "⚠️ {{name}} zaten mevcut.",
250
+ "section.add.already_exists": "{{name}} zaten mevcut.",
251
251
  "section.add.overwrite_prompt": "Ne yapmak istersiniz?",
252
252
  "section.add.overwrite": "Üzerine yaz",
253
253
  "section.add.overwrite_all": "Tümünün üzerine yaz",
@@ -266,7 +266,7 @@
266
266
  "section.add.files_updated": "✓ Tema dosyaları güncellendi",
267
267
  "section.add.complete": "🎉 İşlem tamamlandı!",
268
268
  "section.add.location": "Konum: {{path}}",
269
- "section.add.download_error": "⚠️ Tema dosyaları güncellenirken hata oluştu:",
269
+ "section.add.download_error": "Tema dosyaları güncellenirken hata oluştu:",
270
270
  "section.add.manual_tip": "💡 Manuel olarak temayı tekrar indirmeyi deneyin:",
271
271
  "section.add.manual_command": "tsoft theme pull",
272
272
  "section.menu.title": "🧩 Section İşlemleri",
@@ -337,5 +337,23 @@
337
337
  "dev.section_synced": "🧩 section güncellendi: {{folder}}",
338
338
  "dev.status_section": "section",
339
339
  "dev.section_menu_opening": "Section menüsü açılıyor...",
340
- "dev.section_menu_resumed": "İzlemeye devam ediliyor."
340
+ "dev.section_menu_resumed": "İzlemeye devam ediliyor.",
341
+ "store.none": "Kayıtlı mağaza yok.",
342
+ "store.login_hint": "Önce giriş yapın: tsoft login",
343
+ "store.count": "{{count}} kayıtlı mağaza",
344
+ "store.session_expired": "— oturum bitti",
345
+ "store.session_days": "({{days}} gün)",
346
+ "store.select_prompt": "Hangi mağazada çalışacaksınız?",
347
+ "store.select_title": "Mağaza Seçimi",
348
+ "store.selected": "Aktif mağaza: {{store}}",
349
+ "store.selected_theme": "Tema: {{theme}}",
350
+ "store.expired_warning": "Bu mağazanın oturumu bitmiş. Devam etmek için: tsoft login",
351
+ "store.not_found": "Kayıtlı mağaza bulunamadı: {{store}}",
352
+ "store.json_needs_argument": "JSON modunda mağaza adı gerekir.",
353
+ "store.json_hint": "Kullanım: tsoft --json store use <domain>",
354
+ "context.line": "Mağaza: {{store}} · Tema: {{theme}}",
355
+ "store.search_prompt": "Mağaza arayın veya seçin",
356
+ "theme.use.on_store": "Mağaza: {{store}}",
357
+ "theme.use.search_prompt": "Tema arayın veya seçin",
358
+ "context.workspace": "Bulunduğunuz dizin: {{store}} · {{theme}}"
341
359
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsoft-cli",
3
- "version": "3.10.2",
3
+ "version": "3.12.0",
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,7 @@ 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
10
  import {brandedConfirm, brandedSelect, showWarning, showError, showInfo} from '../ui/prompt-wrapper.js';
11
11
  import {themeSectionMenuCommand} from './theme-section.js';
12
12
  import { t } from '../i18n.js';
@@ -448,7 +448,7 @@ export async function themeDevCommand(themeName) {
448
448
 
449
449
  console.log(chalk.green(t('dev.active_theme_set', { slug: themeSlug })));
450
450
 
451
- let themePath = path.join(process.cwd(), storeSlug, themeSlug);
451
+ let themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
452
452
  console.log(chalk.green(t('dev.working_dir', { path: `${storeSlug}/${themeSlug}/` }) + '\n'));
453
453
 
454
454
  // Fork check: foreign-org themes need fork before development
@@ -480,7 +480,7 @@ export async function themeDevCommand(themeName) {
480
480
 
481
481
  themeInfo = { uuid: forkedData.uuid, name: forkedData.name, version: forkedData.version };
482
482
  themeSlug = slugify(forkedData.name);
483
- themePath = path.join(process.cwd(), storeSlug, themeSlug);
483
+ themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
484
484
 
485
485
  console.log(chalk.green(t('dev.fork_success', { name: forkedData.name })));
486
486
  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,14 @@ 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, getActiveStore, getWorkspaceRoot, getWorkspaceContext} from '../storage.js';
9
+ import {brandedConfirm, brandedSelect, brandedSearch, showWarning} from '../ui/prompt-wrapper.js';
10
10
  import { isJsonMode, jsonOut } from '../output-mode.js';
11
11
  import { mapError } from '../errors/error-mapper.js';
12
12
  import { t } from '../i18n.js';
13
13
 
14
+ const THEME_SEARCH_THRESHOLD = 7;
15
+
14
16
  export async function themeListCommand() {
15
17
  try {
16
18
  if (!isJsonMode()) {
@@ -103,7 +105,7 @@ export async function themeCreateCommand(themeName) {
103
105
 
104
106
  const themeSlug = slugify(name);
105
107
  const storeSlug = normalizeStoreDomain(store);
106
- const themePath = path.join(process.cwd(), storeSlug, themeSlug);
108
+ const themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
107
109
 
108
110
  const exists = await fs.access(themePath).then(() => true).catch(() => false);
109
111
  if (exists) {
@@ -174,7 +176,7 @@ export async function themeCreateCommand(themeName) {
174
176
  impact: 'Yeni tema güvenli bir şekilde oluşturulamaz.'
175
177
  },
176
178
  'default_theme_not_installed': {
177
- title: '⚠️ Default Tema Yüklenmemiş',
179
+ title: 'Default Tema Yüklenmemiş',
178
180
  description: 'Sisteminizde default tema bulunamadı.',
179
181
  impact: 'Default tema olmadan yeni tema geliştirilemez.'
180
182
  },
@@ -184,7 +186,7 @@ export async function themeCreateCommand(themeName) {
184
186
  impact: 'Güncel özellikler ve düzeltmeler yeni temanızda eksik olacak.'
185
187
  },
186
188
  'check_failed': {
187
- title: '⚠️ Default Tema Kontrolü Başarısız',
189
+ title: 'Default Tema Kontrolü Başarısız',
188
190
  description: 'Default tema güncellik kontrolü yapılamadı.',
189
191
  impact: 'Güvenlik nedeniyle default tema güncellenmelidir.'
190
192
  },
@@ -196,7 +198,7 @@ export async function themeCreateCommand(themeName) {
196
198
  };
197
199
 
198
200
  const reasonInfo = reasonDetails[errorData.reason] || {
199
- title: '⚠️ Default Tema Güncelleme Gerekli',
201
+ title: 'Default Tema Güncelleme Gerekli',
200
202
  description: errorData.reason || 'Bilinmeyen bir durum tespit edildi.',
201
203
  impact: 'Devam etmek için default tema güncellenmelidir.'
202
204
  };
@@ -236,7 +238,7 @@ export async function themeCreateCommand(themeName) {
236
238
  console.log(chalk.gray('🔹 Layout ve section\'lar import ediliyor...'));
237
239
  console.log(chalk.gray('🔹 Yeni tema bootstrap ediliyor...'));
238
240
  console.log(chalk.gray('━'.repeat(60)));
239
- console.log(chalk.yellow('\n⏱️ Lütfen bekleyin, bu işlem birkaç dakika sürebilir...\n'));
241
+ console.log(chalk.yellow('\n Lütfen bekleyin, bu işlem birkaç dakika sürebilir...\n'));
240
242
 
241
243
  // forceUpdate: true ile tekrar dene
242
244
  themeData.forceUpdate = true;
@@ -369,7 +371,7 @@ export async function themePullCommand() {
369
371
  const themeUuid = selectedTheme.theme_folder;
370
372
  const store = await getActiveStore();
371
373
  const storeSlug = normalizeStoreDomain(store);
372
- const themePath = path.join(process.cwd(), storeSlug, themeSlug);
374
+ const themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
373
375
 
374
376
  const exists = await fs.access(themePath).then(() => true).catch(() => false);
375
377
  if (exists) {
@@ -411,7 +413,7 @@ export async function themePullCommand() {
411
413
 
412
414
  console.log(chalk.green(t('theme.extracted', { path: `${storeSlug}/${themeSlug}/` })));
413
415
 
414
- await setActiveTheme(themeSlug);
416
+ await setActiveTheme(themeUuid);
415
417
  console.log(chalk.green(t('theme.active_set') + '\n'));
416
418
 
417
419
  console.log(chalk.bold.green(t('theme.pull.success')));
@@ -429,6 +431,14 @@ export async function themeUseCommand(themeName = null) {
429
431
  try {
430
432
  console.log(chalk.cyan(t('theme.use.starting') + '\n'));
431
433
 
434
+ const store = await getActiveStore();
435
+ const context = getWorkspaceContext();
436
+ if (context?.store === store && context.theme) {
437
+ console.log(chalk.gray(t('context.workspace', { store, theme: context.theme })) + '\n');
438
+ } else if (store) {
439
+ console.log(chalk.gray(t('theme.use.on_store', { store })) + '\n');
440
+ }
441
+
432
442
  const apiClient = await createApiClient();
433
443
  const response = await apiClient.get('/theme');
434
444
  const themes = response.data || [];
@@ -456,18 +466,24 @@ export async function themeUseCommand(themeName = null) {
456
466
  const themeChoices = themes.map(theme => ({
457
467
  name: `${theme.name} ${chalk.gray(`v${theme.version}`)} ${theme.active ? chalk.green('(Şu an aktif)') : ''}`,
458
468
  value: theme,
459
- description: theme.description || `${t('theme.folder_label')} ${theme.theme_folder}`
469
+ description: theme.description || `${t('theme.folder_label')} ${theme.theme_folder}`,
470
+ keywords: [theme.theme_folder, theme.alias].filter(Boolean),
460
471
  }));
461
472
 
462
- selectedTheme = await brandedSelect({
463
- message: t('theme.use.select_prompt'),
464
- choices: themeChoices,
465
- }, t('theme.use.select_title'));
473
+ selectedTheme = themes.length > THEME_SEARCH_THRESHOLD
474
+ ? await brandedSearch({
475
+ message: t('theme.use.search_prompt'),
476
+ choices: themeChoices,
477
+ }, t('theme.use.select_title'))
478
+ : await brandedSelect({
479
+ message: t('theme.use.select_prompt'),
480
+ choices: themeChoices,
481
+ }, t('theme.use.select_title'));
466
482
  }
467
483
 
468
484
  const themeSlug = slugify(selectedTheme.name);
469
485
 
470
- await setActiveTheme(themeSlug);
486
+ await setActiveTheme(selectedTheme.theme_folder);
471
487
 
472
488
  console.log(chalk.green('\n' + t('theme.use.set', { name: chalk.bold(selectedTheme.name) })));
473
489
  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,