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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsoft-cli",
3
- "version": "3.12.0",
3
+ "version": "3.12.2",
4
4
  "description": "Command-line tool for tsoft360 theme development and management",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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 => slugify(t.name) === themeSlug);
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(`${themeSlug} (${tempThemeInfo.name} v${tempThemeInfo.version})`));
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 response = await apiClient.get('/theme');
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: `${theme.name} ${chalk.gray(`v${theme.version}`)} ${theme.active ? chalk.green('(Aktif)') : ''}`,
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 = slugify(selectedTheme.name);
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(themeSlug);
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 apiClient = await createApiClient();
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(themeSlug);
421
+ await setActiveTheme(selectedTheme.theme_folder);
445
422
  } else if (!themeInfo) {
446
423
  themeInfo = await getThemeUuid(themeSlug);
447
424
  }
448
425
 
449
- console.log(chalk.green(t('dev.active_theme_set', { slug: themeSlug })));
426
+ const themeDir = slugify(themeInfo.name);
450
427
 
451
- let themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
452
- console.log(chalk.green(t('dev.working_dir', { path: `${storeSlug}/${themeSlug}/` }) + '\n'));
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 apiClient.get('/theme');
458
- const currentTheme = (orgResponse.data || []).find(t => t.theme_folder === themeInfo.uuid);
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
- themeSlug = slugify(forkedData.name);
483
- themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
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 response = await apiClient.get('/theme');
18
- const themes = response.data || [];
18
+ const { themes } = await fetchAllThemes(apiClient);
19
19
 
20
20
  let sourceTheme;
21
21
 
@@ -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 apiClient = await createApiClient();
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 activeIndicator = theme.active ? chalk.green('●') : chalk.gray('○');
53
- const name = theme.active ? chalk.bold.green(theme.name) : chalk.white(theme.name);
54
- const version = chalk.gray(`v${theme.version}`);
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(`${activeIndicator} ${name} ${version}${description}`);
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 response = await apiClient.get('/theme');
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: `${theme.name} ${chalk.gray(`v${theme.version}`)} ${theme.active ? chalk.green('(Aktif)') : ''}`,
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 apiClient = await createApiClient();
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 => slugify(t.name) === themeSlug);
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: `${theme.name} ${chalk.gray(`v${theme.version}`)} ${theme.active ? chalk.green('(Şu an aktif)') : ''}`,
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 { createApiClient } = await import('./api-client.js');
739
- const apiClient = await createApiClient();
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
+ }