tsoft-cli 3.12.0 → 3.12.2
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 +11 -0
- package/locales/en.json +2 -0
- package/locales/tr.json +2 -0
- package/package.json +1 -1
- package/src/commands/theme-dev.js +22 -42
- package/src/commands/theme-init.js +2 -2
- package/src/commands/theme.js +21 -18
- package/src/storage.js +2 -4
- package/src/theme-list.js +25 -0
- package/src/ui/theme-choice.js +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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.2] - 2026-08-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `theme dev` no longer reports the theme you are working on as missing. Downloading a theme records which one is active, and `theme dev` then looked that record up by the theme's name — but what gets recorded is the theme's folder, so the lookup could never match and every run ended in "theme not found". Re-downloading the theme did not help, because the failure was in the lookup rather than in the files on disk. Both spellings are now accepted, so the record written by `theme pull`, `theme use` and `theme dev` resolves the same way. `theme use` also accepts a theme folder as its argument.
|
|
12
|
+
- Theme lists no longer stop at the first page. `theme list`, `theme pull`, `theme use`, `theme init` and `theme section` asked the store for its themes and read only the first page of the answer, so on a store with many themes the rest were invisible: pickers showed a fraction of what was there and looking a theme up by name failed for anything further down. Every page is now read before the list is used.
|
|
13
|
+
|
|
14
|
+
## [3.12.1] - 2026-08-08
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
8
19
|
## [3.12.0] - 2026-08-05
|
|
9
20
|
|
|
10
21
|
### Added
|
package/locales/en.json
CHANGED
|
@@ -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>\".",
|
package/locales/tr.json
CHANGED
|
@@ -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.",
|
package/package.json
CHANGED
|
@@ -7,8 +7,10 @@ import chokidar from 'chokidar';
|
|
|
7
7
|
import {createApiClient} from '../api-client.js';
|
|
8
8
|
import open from 'open';
|
|
9
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';
|
|
13
|
+
import {fetchAllThemes} from '../theme-list.js';
|
|
12
14
|
import { t } from '../i18n.js';
|
|
13
15
|
|
|
14
16
|
function resolveSectionFolder(relativePath) {
|
|
@@ -28,32 +30,11 @@ function resolveSectionFolder(relativePath) {
|
|
|
28
30
|
return folder.includes('.') ? null : folder;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
async function fetchAllThemes() {
|
|
32
|
-
const apiClient = await createApiClient();
|
|
33
|
-
const all = [];
|
|
34
|
-
let page = 1;
|
|
35
|
-
|
|
36
|
-
// Server ignores per_page; we walk every page until last_page is reached.
|
|
37
|
-
// Hard cap at 20 pages to avoid accidental infinite loops.
|
|
38
|
-
for (let i = 0; i < 20; i++) {
|
|
39
|
-
const response = await apiClient.get(`/theme?per_page=500&page=${page}`);
|
|
40
|
-
const batch = response.data || [];
|
|
41
|
-
all.push(...batch);
|
|
42
|
-
|
|
43
|
-
// Laravel paginator puts last_page at the top level (not under meta).
|
|
44
|
-
// Some envelopes wrap it under meta — support both.
|
|
45
|
-
const lastPage = response.last_page ?? response.meta?.last_page ?? 1;
|
|
46
|
-
if (page >= lastPage || batch.length === 0) break;
|
|
47
|
-
page++;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return all;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
33
|
async function getThemeUuid(themeSlug) {
|
|
54
|
-
const themes = await fetchAllThemes();
|
|
34
|
+
const { themes } = await fetchAllThemes();
|
|
55
35
|
|
|
56
|
-
const theme = themes.find(t =>
|
|
36
|
+
const theme = themes.find(t => t.theme_folder === themeSlug)
|
|
37
|
+
?? themes.find(t => slugify(t.name) === themeSlug);
|
|
57
38
|
|
|
58
39
|
if (!theme) {
|
|
59
40
|
throw new Error(`Tema bulunamadı: ${themeSlug}`);
|
|
@@ -376,9 +357,8 @@ export async function themeDevCommand(themeName) {
|
|
|
376
357
|
if (themeSlug && !themeName) {
|
|
377
358
|
console.log(chalk.cyan(t('dev.active_site')) + ' ' + chalk.white(store));
|
|
378
359
|
|
|
379
|
-
const apiClient = await createApiClient();
|
|
380
360
|
const tempThemeInfo = await getThemeUuid(themeSlug);
|
|
381
|
-
console.log(chalk.cyan(t('dev.active_theme')) + ' ' + chalk.white(`${
|
|
361
|
+
console.log(chalk.cyan(t('dev.active_theme')) + ' ' + chalk.white(`${slugify(tempThemeInfo.name)} (${tempThemeInfo.name} v${tempThemeInfo.version})`));
|
|
382
362
|
console.log();
|
|
383
363
|
|
|
384
364
|
const continueWithActive = await brandedConfirm({
|
|
@@ -387,8 +367,7 @@ export async function themeDevCommand(themeName) {
|
|
|
387
367
|
}, t('dev.continue_title'));
|
|
388
368
|
|
|
389
369
|
if (!continueWithActive) {
|
|
390
|
-
const
|
|
391
|
-
const themes = response.data || [];
|
|
370
|
+
const { themes } = await fetchAllThemes();
|
|
392
371
|
|
|
393
372
|
if (themes.length === 0) {
|
|
394
373
|
showWarning(t('dev.no_themes'));
|
|
@@ -397,7 +376,7 @@ export async function themeDevCommand(themeName) {
|
|
|
397
376
|
|
|
398
377
|
console.log();
|
|
399
378
|
const themeChoices = themes.map(theme => ({
|
|
400
|
-
name:
|
|
379
|
+
name: decorateThemeName(theme, themeSlug),
|
|
401
380
|
value: theme,
|
|
402
381
|
description: theme.description || `Klasör: ${theme.theme_folder}`
|
|
403
382
|
}));
|
|
@@ -406,12 +385,12 @@ export async function themeDevCommand(themeName) {
|
|
|
406
385
|
message: t('dev.select_theme_prompt'), choices: themeChoices
|
|
407
386
|
}, t('dev.select_theme_title'));
|
|
408
387
|
|
|
409
|
-
themeSlug =
|
|
388
|
+
themeSlug = selectedTheme.theme_folder;
|
|
410
389
|
themeInfo = {
|
|
411
390
|
uuid: selectedTheme.theme_folder, name: selectedTheme.name, version: selectedTheme.version
|
|
412
391
|
};
|
|
413
392
|
|
|
414
|
-
await setActiveTheme(
|
|
393
|
+
await setActiveTheme(selectedTheme.theme_folder);
|
|
415
394
|
console.log();
|
|
416
395
|
} else {
|
|
417
396
|
themeInfo = tempThemeInfo;
|
|
@@ -419,9 +398,7 @@ export async function themeDevCommand(themeName) {
|
|
|
419
398
|
}
|
|
420
399
|
|
|
421
400
|
if (!themeSlug) {
|
|
422
|
-
const
|
|
423
|
-
const response = await apiClient.get('/theme');
|
|
424
|
-
const themes = response.data || [];
|
|
401
|
+
const { themes } = await fetchAllThemes();
|
|
425
402
|
|
|
426
403
|
if (themes.length === 0) {
|
|
427
404
|
showWarning(t('dev.no_themes'));
|
|
@@ -441,21 +418,23 @@ export async function themeDevCommand(themeName) {
|
|
|
441
418
|
uuid: selectedTheme.theme_folder, name: selectedTheme.name, version: selectedTheme.version
|
|
442
419
|
};
|
|
443
420
|
|
|
444
|
-
await setActiveTheme(
|
|
421
|
+
await setActiveTheme(selectedTheme.theme_folder);
|
|
445
422
|
} else if (!themeInfo) {
|
|
446
423
|
themeInfo = await getThemeUuid(themeSlug);
|
|
447
424
|
}
|
|
448
425
|
|
|
449
|
-
|
|
426
|
+
const themeDir = slugify(themeInfo.name);
|
|
450
427
|
|
|
451
|
-
|
|
452
|
-
|
|
428
|
+
console.log(chalk.green(t('dev.active_theme_set', { slug: themeDir })));
|
|
429
|
+
|
|
430
|
+
let themePath = path.join(getWorkspaceRoot(), storeSlug, themeDir);
|
|
431
|
+
console.log(chalk.green(t('dev.working_dir', { path: `${storeSlug}/${themeDir}/` }) + '\n'));
|
|
453
432
|
|
|
454
433
|
// Fork check: foreign-org themes need fork before development
|
|
455
434
|
{
|
|
456
435
|
const apiClient = await createApiClient();
|
|
457
|
-
const orgResponse = await
|
|
458
|
-
const currentTheme =
|
|
436
|
+
const { themes: orgThemes, envelope: orgResponse } = await fetchAllThemes();
|
|
437
|
+
const currentTheme = orgThemes.find(t => t.theme_folder === themeInfo.uuid);
|
|
459
438
|
const userOrg = orgResponse.user_organization;
|
|
460
439
|
|
|
461
440
|
if (currentTheme && currentTheme.organization_id != null && userOrg && !userOrg.is_system && currentTheme.organization_id !== userOrg.id) {
|
|
@@ -479,8 +458,9 @@ export async function themeDevCommand(themeName) {
|
|
|
479
458
|
const forkedData = forkResult.data;
|
|
480
459
|
|
|
481
460
|
themeInfo = { uuid: forkedData.uuid, name: forkedData.name, version: forkedData.version };
|
|
482
|
-
|
|
483
|
-
|
|
461
|
+
themePath = path.join(getWorkspaceRoot(), storeSlug, slugify(forkedData.name));
|
|
462
|
+
|
|
463
|
+
await setActiveTheme(forkedData.uuid);
|
|
484
464
|
|
|
485
465
|
console.log(chalk.green(t('dev.fork_success', { name: forkedData.name })));
|
|
486
466
|
console.log(chalk.gray(' ' + t('dev.fork_source', { source: forkedData.source.name, version: forkedData.source.version })));
|
|
@@ -6,6 +6,7 @@ import AdmZip from 'adm-zip';
|
|
|
6
6
|
import {createApiClient} from '../api-client.js';
|
|
7
7
|
import {slugify, getActiveStore, getWorkspaceRoot} from '../storage.js';
|
|
8
8
|
import {brandedConfirm, brandedSelect, showWarning, showInfo, showError} from '../ui/prompt-wrapper.js';
|
|
9
|
+
import {fetchAllThemes} from '../theme-list.js';
|
|
9
10
|
import {normalizeStoreDomain} from '../storage.js';
|
|
10
11
|
import {isJsonMode, jsonOut} from '../output-mode.js';
|
|
11
12
|
import {mapError} from '../errors/error-mapper.js';
|
|
@@ -14,8 +15,7 @@ import { t } from '../i18n.js';
|
|
|
14
15
|
export async function themeInitCommand(fromSlug) {
|
|
15
16
|
try {
|
|
16
17
|
const apiClient = await createApiClient();
|
|
17
|
-
const
|
|
18
|
-
const themes = response.data || [];
|
|
18
|
+
const { themes } = await fetchAllThemes(apiClient);
|
|
19
19
|
|
|
20
20
|
let sourceTheme;
|
|
21
21
|
|
package/src/commands/theme.js
CHANGED
|
@@ -5,8 +5,10 @@ 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, getWorkspaceRoot, getWorkspaceContext} from '../storage.js';
|
|
8
|
+
import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActiveStore, getWorkspaceRoot, getWorkspaceContext} from '../storage.js';
|
|
9
9
|
import {brandedConfirm, brandedSelect, brandedSearch, showWarning} from '../ui/prompt-wrapper.js';
|
|
10
|
+
import {fetchAllThemes} from '../theme-list.js';
|
|
11
|
+
import {themeLabels, decorateThemeName} from '../ui/theme-choice.js';
|
|
10
12
|
import { isJsonMode, jsonOut } from '../output-mode.js';
|
|
11
13
|
import { mapError } from '../errors/error-mapper.js';
|
|
12
14
|
import { t } from '../i18n.js';
|
|
@@ -19,10 +21,7 @@ export async function themeListCommand() {
|
|
|
19
21
|
console.log(chalk.cyan(t('theme.listing') + '\n'));
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
const
|
|
23
|
-
const response = await apiClient.get('/theme');
|
|
24
|
-
|
|
25
|
-
const themes = response.data || [];
|
|
24
|
+
const { themes } = await fetchAllThemes();
|
|
26
25
|
|
|
27
26
|
if (isJsonMode()) {
|
|
28
27
|
jsonOut({
|
|
@@ -48,13 +47,16 @@ export async function themeListCommand() {
|
|
|
48
47
|
|
|
49
48
|
console.log(chalk.bold.cyan(t('theme.count', { count: themes.length }) + '\n'));
|
|
50
49
|
|
|
50
|
+
const localTheme = await getActiveTheme();
|
|
51
|
+
|
|
51
52
|
themes.forEach((theme) => {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
53
|
+
const labels = themeLabels(theme, localTheme);
|
|
54
|
+
const indicator = labels.includes('selected')
|
|
55
|
+
? chalk.cyan('●')
|
|
56
|
+
: (labels.includes('published') ? chalk.green('●') : chalk.gray('○'));
|
|
55
57
|
const description = theme.description ? chalk.gray(` - ${theme.description}`) : '';
|
|
56
58
|
|
|
57
|
-
console.log(`${
|
|
59
|
+
console.log(`${indicator} ${decorateThemeName(theme, localTheme)}${description}`);
|
|
58
60
|
console.log(chalk.gray(` ${t('theme.folder_label')} ${theme.theme_folder || theme.alias}`));
|
|
59
61
|
console.log();
|
|
60
62
|
});
|
|
@@ -340,17 +342,17 @@ export async function themePullCommand() {
|
|
|
340
342
|
console.log(chalk.cyan(t('theme.pull.starting') + '\n'));
|
|
341
343
|
|
|
342
344
|
const apiClient = await createApiClient();
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
const themes = response.data || [];
|
|
345
|
+
const { themes } = await fetchAllThemes(apiClient);
|
|
346
346
|
|
|
347
347
|
if (themes.length === 0) {
|
|
348
348
|
showWarning(t('theme.pull.no_themes'));
|
|
349
349
|
return;
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
const localTheme = await getActiveTheme();
|
|
353
|
+
|
|
352
354
|
const themeChoices = themes.map(theme => ({
|
|
353
|
-
name:
|
|
355
|
+
name: decorateThemeName(theme, localTheme),
|
|
354
356
|
value: theme,
|
|
355
357
|
description: theme.description || `${t('theme.folder_label')} ${theme.theme_folder}`
|
|
356
358
|
}));
|
|
@@ -439,9 +441,7 @@ export async function themeUseCommand(themeName = null) {
|
|
|
439
441
|
console.log(chalk.gray(t('theme.use.on_store', { store })) + '\n');
|
|
440
442
|
}
|
|
441
443
|
|
|
442
|
-
const
|
|
443
|
-
const response = await apiClient.get('/theme');
|
|
444
|
-
const themes = response.data || [];
|
|
444
|
+
const { themes } = await fetchAllThemes();
|
|
445
445
|
|
|
446
446
|
if (themes.length === 0) {
|
|
447
447
|
showWarning(t('theme.use.no_themes'));
|
|
@@ -452,7 +452,8 @@ export async function themeUseCommand(themeName = null) {
|
|
|
452
452
|
|
|
453
453
|
if (themeName) {
|
|
454
454
|
const themeSlug = slugify(themeName);
|
|
455
|
-
selectedTheme = themes.find(t =>
|
|
455
|
+
selectedTheme = themes.find(t => t.theme_folder === themeName)
|
|
456
|
+
?? themes.find(t => slugify(t.name) === themeSlug);
|
|
456
457
|
|
|
457
458
|
if (!selectedTheme) {
|
|
458
459
|
console.error(chalk.red('\n' + t('theme.use.not_found', { name: themeName })));
|
|
@@ -463,8 +464,10 @@ export async function themeUseCommand(themeName = null) {
|
|
|
463
464
|
process.exit(1);
|
|
464
465
|
}
|
|
465
466
|
} else {
|
|
467
|
+
const localTheme = await getActiveTheme();
|
|
468
|
+
|
|
466
469
|
const themeChoices = themes.map(theme => ({
|
|
467
|
-
name:
|
|
470
|
+
name: decorateThemeName(theme, localTheme),
|
|
468
471
|
value: theme,
|
|
469
472
|
description: theme.description || `${t('theme.folder_label')} ${theme.theme_folder}`,
|
|
470
473
|
keywords: [theme.theme_folder, theme.alias].filter(Boolean),
|
package/src/storage.js
CHANGED
|
@@ -735,10 +735,8 @@ export async function resolveActiveThemeUuid() {
|
|
|
735
735
|
* @returns {Promise<Object>} {uuid, name, version, active}
|
|
736
736
|
*/
|
|
737
737
|
export async function getThemeUuidBySlug(themeSlug) {
|
|
738
|
-
const {
|
|
739
|
-
const
|
|
740
|
-
const response = await apiClient.get('/theme');
|
|
741
|
-
const themes = response.data || [];
|
|
738
|
+
const { fetchAllThemes } = await import('./theme-list.js');
|
|
739
|
+
const { themes } = await fetchAllThemes();
|
|
742
740
|
|
|
743
741
|
const theme = themes.find(t => t.theme_folder === themeSlug)
|
|
744
742
|
?? themes.find(t => slugify(t.name) === themeSlug);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createApiClient } from './api-client.js';
|
|
2
|
+
|
|
3
|
+
export async function fetchAllThemes(client = null) {
|
|
4
|
+
const apiClient = client ?? await createApiClient();
|
|
5
|
+
const all = [];
|
|
6
|
+
let envelope = {};
|
|
7
|
+
let page = 1;
|
|
8
|
+
|
|
9
|
+
// Server ignores per_page; we walk every page until last_page is reached.
|
|
10
|
+
// Hard cap at 20 pages to avoid accidental infinite loops.
|
|
11
|
+
for (let i = 0; i < 20; i++) {
|
|
12
|
+
const response = await apiClient.get(`/theme?per_page=500&page=${page}`);
|
|
13
|
+
const batch = response.data || [];
|
|
14
|
+
all.push(...batch);
|
|
15
|
+
envelope = response;
|
|
16
|
+
|
|
17
|
+
// Laravel paginator puts last_page at the top level (not under meta).
|
|
18
|
+
// Some envelopes wrap it under meta — support both.
|
|
19
|
+
const lastPage = response.last_page ?? response.meta?.last_page ?? 1;
|
|
20
|
+
if (page >= lastPage || batch.length === 0) break;
|
|
21
|
+
page++;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { themes: all, envelope };
|
|
25
|
+
}
|
|
@@ -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
|
+
}
|