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 +38 -0
- package/locales/en.json +38 -18
- package/locales/tr.json +38 -18
- package/package.json +2 -1
- package/src/commands/login.js +2 -1
- package/src/commands/store.js +144 -0
- package/src/commands/theme-dev.js +5 -4
- package/src/commands/theme-init.js +2 -2
- package/src/commands/theme-push.js +8 -2
- package/src/commands/theme-section.js +4 -4
- package/src/commands/theme.js +45 -21
- package/src/index.js +27 -1
- package/src/storage.js +283 -17
- package/src/ui/prompt-wrapper.js +9 -3
- package/src/ui/theme-choice.js +33 -0
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": "
|
|
5
|
-
"ui.label.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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
|
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?",
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
5
|
-
"ui.label.warning": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
|
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?",
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
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",
|
package/src/commands/login.js
CHANGED
|
@@ -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:
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
512
|
+
const themePath = path.join(getWorkspaceRoot(), storeSlug, activeTheme);
|
|
513
513
|
|
|
514
514
|
try {
|
|
515
515
|
await downloadThemeFiles(apiClient, themeInfo.uuid, themePath);
|
package/src/commands/theme.js
CHANGED
|
@@ -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
|
|
51
|
-
const
|
|
52
|
-
|
|
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(`${
|
|
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(
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
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:
|
|
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 =
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
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
|
-
|
|
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 =>
|
|
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}`);
|
package/src/ui/prompt-wrapper.js
CHANGED
|
@@ -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
|
|
33
|
-
const
|
|
34
|
-
|
|
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
|
+
}
|