tsoft-cli 3.10.1 → 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 +39 -0
- package/README.md +20 -15
- package/locales/en.json +36 -18
- package/locales/tr.json +36 -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 +3 -3
- 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 +31 -15
- package/src/index.js +27 -1
- package/src/storage.js +283 -17
- package/src/ui/prompt-wrapper.js +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,45 @@ 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
|
+
|
|
41
|
+
## [3.10.2] - 2026-08-03
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- The README documented `tsoft theme publish` as the way to publish a theme. The canonical command is `tsoft theme push`; `publish` is a deprecated alias that prints a warning and forwards to it. `push` did not appear in the README at all, so anyone starting from the npm package page was sent down the deprecated path and hit a warning on their first command. The command table and examples now use `push`, and the deprecated `publish` and `submit` aliases are covered by a single backwards-compatibility note rather than being advertised as commands.
|
|
45
|
+
- Two other rows in the command table had drifted from the code: `tsoft theme create` accepts an optional theme name argument, and `tsoft theme push` supports `--verbose`. Neither was documented.
|
|
46
|
+
|
|
8
47
|
## [3.10.1] - 2026-07-30
|
|
9
48
|
|
|
10
49
|
### Fixed
|
package/README.md
CHANGED
|
@@ -40,28 +40,33 @@ tsoft theme dev
|
|
|
40
40
|
|
|
41
41
|
### Theme Management
|
|
42
42
|
|
|
43
|
-
| Command
|
|
44
|
-
|
|
|
45
|
-
| `tsoft theme list`
|
|
46
|
-
| `tsoft theme create`
|
|
47
|
-
| `tsoft theme pull`
|
|
48
|
-
| `tsoft theme use [name]`
|
|
49
|
-
| `tsoft theme dev [name]`
|
|
50
|
-
| `tsoft theme
|
|
51
|
-
| `tsoft theme section list`
|
|
52
|
-
| `tsoft theme section add`
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
| Command | Description |
|
|
44
|
+
| -------------------------- | --------------------------------------- |
|
|
45
|
+
| `tsoft theme list` | List all themes |
|
|
46
|
+
| `tsoft theme create [name]` | Create a new theme |
|
|
47
|
+
| `tsoft theme pull` | Pull an existing theme |
|
|
48
|
+
| `tsoft theme use [name]` | Set the active theme |
|
|
49
|
+
| `tsoft theme dev [name]` | Start dev mode (hot-reload) |
|
|
50
|
+
| `tsoft theme push [name]` | Push theme files to your store |
|
|
51
|
+
| `tsoft theme section list` | List blocks and sections |
|
|
52
|
+
| `tsoft theme section add` | Add a section (interactive) |
|
|
53
|
+
|
|
54
|
+
`tsoft theme publish` and `tsoft theme submit` are deprecated aliases kept for backwards compatibility. `publish` forwards to `push`; submission is now done from the partner panel.
|
|
55
|
+
|
|
56
|
+
## Pushing a Theme
|
|
55
57
|
|
|
56
58
|
```bash
|
|
57
59
|
# Using the active theme
|
|
58
|
-
tsoft theme
|
|
60
|
+
tsoft theme push
|
|
59
61
|
|
|
60
62
|
# Using a specific theme
|
|
61
|
-
tsoft theme
|
|
63
|
+
tsoft theme push my-theme
|
|
64
|
+
|
|
65
|
+
# With verbose error output
|
|
66
|
+
tsoft theme push --verbose
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
On the first
|
|
69
|
+
On the first push you enter the initial version number. Subsequent updates prompt for a semantic-versioning bump type (patch / minor / major).
|
|
65
70
|
|
|
66
71
|
## Security
|
|
67
72
|
|
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",
|
|
@@ -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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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",
|
|
@@ -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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
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",
|
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,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(
|
|
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(
|
|
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(
|
|
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,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(
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|