tsoft-cli 3.12.1 → 3.12.3

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,19 @@ 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.3] - 2026-08-11
9
+
10
+ ### Fixed
11
+ - A failed download no longer destroys the theme you already had on disk. `theme pull`, `theme create` and `theme init` deleted the target directory *before* fetching anything, so a download that arrived truncated or died mid-transfer left nothing behind but a `theme.zip` that could not be opened — the files you had been working on were already gone. Downloads now land in a temporary directory next to the target and are only put in place once the archive has been received and read successfully; if anything goes wrong the existing files stay exactly as they were.
12
+ - A download cut off mid-transfer is now reported instead of hanging. Only the file being written was watched for errors, not the incoming data, so a dropped connection could leave the command waiting indefinitely or crash it outside its own error handling.
13
+ - `theme section` no longer risks the theme it is updating: a corrupt archive is detected before any file in the theme is touched.
14
+
15
+ ## [3.12.2] - 2026-08-11
16
+
17
+ ### Fixed
18
+ - `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.
19
+ - 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.
20
+
8
21
  ## [3.12.1] - 2026-08-08
9
22
 
10
23
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsoft-cli",
3
- "version": "3.12.1",
3
+ "version": "3.12.3",
4
4
  "description": "Command-line tool for tsoft360 theme development and management",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,8 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import fs from 'fs/promises';
3
- import {createWriteStream} from 'fs';
4
3
  import path from 'path';
5
- import AdmZip from 'adm-zip';
6
4
  import chokidar from 'chokidar';
7
5
  import {createApiClient} from '../api-client.js';
8
6
  import open from 'open';
@@ -10,7 +8,9 @@ import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActive
10
8
  import {decorateThemeName} from '../ui/theme-choice.js';
11
9
  import {brandedConfirm, brandedSelect, showWarning, showError, showInfo} from '../ui/prompt-wrapper.js';
12
10
  import {themeSectionMenuCommand} from './theme-section.js';
11
+ import {fetchAllThemes} from '../theme-list.js';
13
12
  import { t } from '../i18n.js';
13
+ import {extractThemeArchive} from '../theme-download.js';
14
14
 
15
15
  function resolveSectionFolder(relativePath) {
16
16
  if (!relativePath) {
@@ -29,32 +29,11 @@ function resolveSectionFolder(relativePath) {
29
29
  return folder.includes('.') ? null : folder;
30
30
  }
31
31
 
32
- async function fetchAllThemes() {
33
- const apiClient = await createApiClient();
34
- const all = [];
35
- let page = 1;
36
-
37
- // Server ignores per_page; we walk every page until last_page is reached.
38
- // Hard cap at 20 pages to avoid accidental infinite loops.
39
- for (let i = 0; i < 20; i++) {
40
- const response = await apiClient.get(`/theme?per_page=500&page=${page}`);
41
- const batch = response.data || [];
42
- all.push(...batch);
43
-
44
- // Laravel paginator puts last_page at the top level (not under meta).
45
- // Some envelopes wrap it under meta — support both.
46
- const lastPage = response.last_page ?? response.meta?.last_page ?? 1;
47
- if (page >= lastPage || batch.length === 0) break;
48
- page++;
49
- }
50
-
51
- return all;
52
- }
53
-
54
32
  async function getThemeUuid(themeSlug) {
55
- const themes = await fetchAllThemes();
33
+ const { themes } = await fetchAllThemes();
56
34
 
57
- const theme = themes.find(t => slugify(t.name) === themeSlug);
35
+ const theme = themes.find(t => t.theme_folder === themeSlug)
36
+ ?? themes.find(t => slugify(t.name) === themeSlug);
58
37
 
59
38
  if (!theme) {
60
39
  throw new Error(`Tema bulunamadı: ${themeSlug}`);
@@ -81,20 +60,7 @@ async function pullAndSync(themeUuid, themePath) {
81
60
  console.log(chalk.yellow(t('dev.sync_pulling')));
82
61
  const response = await apiClient.download(`/theme/${themeUuid}/download`, {});
83
62
 
84
- await fs.mkdir(themePath, {recursive: true});
85
- const zipPath = path.join(themePath, 'theme.zip');
86
- const writer = createWriteStream(zipPath);
87
-
88
- response.data.pipe(writer);
89
-
90
- await new Promise((resolve, reject) => {
91
- writer.on('finish', resolve);
92
- writer.on('error', reject);
93
- });
94
-
95
- const zip = new AdmZip(zipPath);
96
- zip.extractAllTo(themePath, true);
97
- await fs.unlink(zipPath);
63
+ await extractThemeArchive(response.data, themePath);
98
64
 
99
65
  console.log(chalk.green(t('dev.sync_updated')));
100
66
 
@@ -377,9 +343,8 @@ export async function themeDevCommand(themeName) {
377
343
  if (themeSlug && !themeName) {
378
344
  console.log(chalk.cyan(t('dev.active_site')) + ' ' + chalk.white(store));
379
345
 
380
- const apiClient = await createApiClient();
381
346
  const tempThemeInfo = await getThemeUuid(themeSlug);
382
- console.log(chalk.cyan(t('dev.active_theme')) + ' ' + chalk.white(`${themeSlug} (${tempThemeInfo.name} v${tempThemeInfo.version})`));
347
+ console.log(chalk.cyan(t('dev.active_theme')) + ' ' + chalk.white(`${slugify(tempThemeInfo.name)} (${tempThemeInfo.name} v${tempThemeInfo.version})`));
383
348
  console.log();
384
349
 
385
350
  const continueWithActive = await brandedConfirm({
@@ -388,8 +353,7 @@ export async function themeDevCommand(themeName) {
388
353
  }, t('dev.continue_title'));
389
354
 
390
355
  if (!continueWithActive) {
391
- const response = await apiClient.get('/theme');
392
- const themes = response.data || [];
356
+ const { themes } = await fetchAllThemes();
393
357
 
394
358
  if (themes.length === 0) {
395
359
  showWarning(t('dev.no_themes'));
@@ -407,12 +371,12 @@ export async function themeDevCommand(themeName) {
407
371
  message: t('dev.select_theme_prompt'), choices: themeChoices
408
372
  }, t('dev.select_theme_title'));
409
373
 
410
- themeSlug = slugify(selectedTheme.name);
374
+ themeSlug = selectedTheme.theme_folder;
411
375
  themeInfo = {
412
376
  uuid: selectedTheme.theme_folder, name: selectedTheme.name, version: selectedTheme.version
413
377
  };
414
378
 
415
- await setActiveTheme(themeSlug);
379
+ await setActiveTheme(selectedTheme.theme_folder);
416
380
  console.log();
417
381
  } else {
418
382
  themeInfo = tempThemeInfo;
@@ -420,9 +384,7 @@ export async function themeDevCommand(themeName) {
420
384
  }
421
385
 
422
386
  if (!themeSlug) {
423
- const apiClient = await createApiClient();
424
- const response = await apiClient.get('/theme');
425
- const themes = response.data || [];
387
+ const { themes } = await fetchAllThemes();
426
388
 
427
389
  if (themes.length === 0) {
428
390
  showWarning(t('dev.no_themes'));
@@ -442,21 +404,23 @@ export async function themeDevCommand(themeName) {
442
404
  uuid: selectedTheme.theme_folder, name: selectedTheme.name, version: selectedTheme.version
443
405
  };
444
406
 
445
- await setActiveTheme(themeSlug);
407
+ await setActiveTheme(selectedTheme.theme_folder);
446
408
  } else if (!themeInfo) {
447
409
  themeInfo = await getThemeUuid(themeSlug);
448
410
  }
449
411
 
450
- console.log(chalk.green(t('dev.active_theme_set', { slug: themeSlug })));
412
+ const themeDir = slugify(themeInfo.name);
413
+
414
+ console.log(chalk.green(t('dev.active_theme_set', { slug: themeDir })));
451
415
 
452
- let themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
453
- console.log(chalk.green(t('dev.working_dir', { path: `${storeSlug}/${themeSlug}/` }) + '\n'));
416
+ let themePath = path.join(getWorkspaceRoot(), storeSlug, themeDir);
417
+ console.log(chalk.green(t('dev.working_dir', { path: `${storeSlug}/${themeDir}/` }) + '\n'));
454
418
 
455
419
  // Fork check: foreign-org themes need fork before development
456
420
  {
457
421
  const apiClient = await createApiClient();
458
- const orgResponse = await apiClient.get('/theme');
459
- const currentTheme = (orgResponse.data || []).find(t => t.theme_folder === themeInfo.uuid);
422
+ const { themes: orgThemes, envelope: orgResponse } = await fetchAllThemes();
423
+ const currentTheme = orgThemes.find(t => t.theme_folder === themeInfo.uuid);
460
424
  const userOrg = orgResponse.user_organization;
461
425
 
462
426
  if (currentTheme && currentTheme.organization_id != null && userOrg && !userOrg.is_system && currentTheme.organization_id !== userOrg.id) {
@@ -480,8 +444,9 @@ export async function themeDevCommand(themeName) {
480
444
  const forkedData = forkResult.data;
481
445
 
482
446
  themeInfo = { uuid: forkedData.uuid, name: forkedData.name, version: forkedData.version };
483
- themeSlug = slugify(forkedData.name);
484
- themePath = path.join(getWorkspaceRoot(), storeSlug, themeSlug);
447
+ themePath = path.join(getWorkspaceRoot(), storeSlug, slugify(forkedData.name));
448
+
449
+ await setActiveTheme(forkedData.uuid);
485
450
 
486
451
  console.log(chalk.green(t('dev.fork_success', { name: forkedData.name })));
487
452
  console.log(chalk.gray(' ' + t('dev.fork_source', { source: forkedData.source.name, version: forkedData.source.version })));
@@ -1,21 +1,20 @@
1
1
  import chalk from 'chalk';
2
2
  import fs from 'fs/promises';
3
- import {createWriteStream} from 'fs';
4
3
  import path from 'path';
5
- import AdmZip from 'adm-zip';
6
4
  import {createApiClient} from '../api-client.js';
7
5
  import {slugify, getActiveStore, getWorkspaceRoot} from '../storage.js';
8
6
  import {brandedConfirm, brandedSelect, showWarning, showInfo, showError} from '../ui/prompt-wrapper.js';
7
+ import {fetchAllThemes} from '../theme-list.js';
9
8
  import {normalizeStoreDomain} from '../storage.js';
10
9
  import {isJsonMode, jsonOut} from '../output-mode.js';
11
10
  import {mapError} from '../errors/error-mapper.js';
12
11
  import { t } from '../i18n.js';
12
+ import {extractThemeArchive} from '../theme-download.js';
13
13
 
14
14
  export async function themeInitCommand(fromSlug) {
15
15
  try {
16
16
  const apiClient = await createApiClient();
17
- const response = await apiClient.get('/theme');
18
- const themes = response.data || [];
17
+ const { themes } = await fetchAllThemes(apiClient);
19
18
 
20
19
  let sourceTheme;
21
20
 
@@ -83,21 +82,15 @@ export async function themeInitCommand(fromSlug) {
83
82
 
84
83
  // Check if directory exists
85
84
  const exists = await fs.access(themePath).then(() => true).catch(() => false);
86
- if (exists) {
87
- if (isJsonMode()) {
88
- // JSON modda otomatik overwrite
89
- await fs.rm(themePath, { recursive: true, force: true });
90
- } else {
91
- const overwrite = await brandedConfirm({
92
- message: t('init.dir_exists_prompt', { path: themePath }),
93
- default: false,
94
- }, t('init.dir_exists_title'));
95
-
96
- if (!overwrite) {
97
- showWarning(t('init.dir_cancelled'));
98
- return;
99
- }
100
- await fs.rm(themePath, { recursive: true, force: true });
85
+ if (exists && !isJsonMode()) {
86
+ const overwrite = await brandedConfirm({
87
+ message: t('init.dir_exists_prompt', { path: themePath }),
88
+ default: false,
89
+ }, t('init.dir_exists_title'));
90
+
91
+ if (!overwrite) {
92
+ showWarning(t('init.dir_cancelled'));
93
+ return;
101
94
  }
102
95
  }
103
96
 
@@ -105,19 +98,9 @@ export async function themeInitCommand(fromSlug) {
105
98
  if (!isJsonMode()) {
106
99
  console.log(chalk.yellow(t('init.downloading')));
107
100
  }
108
- await fs.mkdir(themePath, { recursive: true });
109
101
  const downloadResponse = await apiClient.download(`/theme/${forkedData.uuid}/download`, {});
110
- const zipPath = path.join(themePath, 'theme.zip');
111
- const writer = createWriteStream(zipPath);
112
- downloadResponse.data.pipe(writer);
113
- await new Promise((resolve, reject) => {
114
- writer.on('finish', resolve);
115
- writer.on('error', reject);
116
- });
117
-
118
- const zip = new AdmZip(zipPath);
119
- zip.extractAllTo(themePath, true);
120
- await fs.unlink(zipPath);
102
+
103
+ await extractThemeArchive(downloadResponse.data, themePath);
121
104
 
122
105
  // JSON mode success output
123
106
  if (isJsonMode()) {
@@ -1,13 +1,12 @@
1
1
  import chalk from 'chalk';
2
2
  import {checkbox} from '@inquirer/prompts';
3
3
  import fs from 'fs/promises';
4
- import {createWriteStream} from 'fs';
5
4
  import path from 'path';
6
- import AdmZip from 'adm-zip';
7
5
  import {createApiClient} from '../api-client.js';
8
6
  import {getActiveTheme, getThemeUuidBySlug, normalizeStoreDomain, getActiveStore, getWorkspaceRoot} from '../storage.js';
9
7
  import {brandedConfirm, brandedSelect, brandedSearch, brandedInput, showWarning, showSuccess, showInfo} from '../ui/prompt-wrapper.js';
10
8
  import { t } from '../i18n.js';
9
+ import {mergeThemeArchive} from '../theme-download.js';
11
10
 
12
11
  const SECTION_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
13
12
 
@@ -294,31 +293,13 @@ export async function themeSectionAddCommand() {
294
293
  const themePath = path.join(getWorkspaceRoot(), storeSlug, activeTheme);
295
294
 
296
295
  try {
297
- // Tema klasörü henüz lokalde yoksa (theme dev/pull yapılmamışsa)
298
- // createWriteStream ENOENT verir — önce dizini garantiye al.
299
- await fs.mkdir(themePath, { recursive: true });
300
-
301
296
  // Temayı indir
302
297
  const downloadResponse = await apiClient.download(`/theme/${themeInfo.uuid}/download`, {});
303
298
 
304
- const zipPath = path.join(themePath, 'theme-update.zip');
305
- const writer = createWriteStream(zipPath);
306
-
307
- downloadResponse.data.pipe(writer);
308
-
309
- await new Promise((resolve, reject) => {
310
- writer.on('finish', resolve);
311
- writer.on('error', reject);
312
- });
313
-
314
299
  console.log(chalk.green(t('section.add.files_downloaded')));
315
300
  console.log(chalk.yellow(t('section.add.files_updating')));
316
301
 
317
- // Extract
318
- const zip = new AdmZip(zipPath);
319
- zip.extractAllTo(themePath, true);
320
-
321
- await fs.unlink(zipPath);
302
+ await mergeThemeArchive(downloadResponse.data, themePath);
322
303
 
323
304
  console.log(chalk.green(t('section.add.files_updated') + '\n'));
324
305
  console.log(chalk.bold.green(t('section.add.complete')));
@@ -337,24 +318,9 @@ export async function themeSectionAddCommand() {
337
318
  }
338
319
 
339
320
  async function downloadThemeFiles(apiClient, themeUuid, themePath) {
340
- await fs.mkdir(themePath, { recursive: true });
341
-
342
321
  const downloadResponse = await apiClient.download(`/theme/${themeUuid}/download`, {});
343
322
 
344
- const zipPath = path.join(themePath, 'theme-update.zip');
345
- const writer = createWriteStream(zipPath);
346
-
347
- downloadResponse.data.pipe(writer);
348
-
349
- await new Promise((resolve, reject) => {
350
- writer.on('finish', resolve);
351
- writer.on('error', reject);
352
- });
353
-
354
- const zip = new AdmZip(zipPath);
355
- zip.extractAllTo(themePath, true);
356
-
357
- await fs.unlink(zipPath);
323
+ await mergeThemeArchive(downloadResponse.data, themePath);
358
324
  }
359
325
 
360
326
  export async function themeSectionCreateCommand() {
@@ -1,16 +1,16 @@
1
1
  import chalk from 'chalk';
2
2
  import {input} from '@inquirer/prompts';
3
3
  import fs from 'fs/promises';
4
- import {createWriteStream} from 'fs';
5
4
  import path from 'path';
6
- import AdmZip from 'adm-zip';
7
5
  import {createApiClient} from '../api-client.js';
8
6
  import {normalizeStoreDomain, slugify, setActiveTheme, getActiveTheme, getActiveStore, getWorkspaceRoot, getWorkspaceContext} from '../storage.js';
9
7
  import {brandedConfirm, brandedSelect, brandedSearch, showWarning} from '../ui/prompt-wrapper.js';
8
+ import {fetchAllThemes} from '../theme-list.js';
10
9
  import {themeLabels, decorateThemeName} from '../ui/theme-choice.js';
11
10
  import { isJsonMode, jsonOut } from '../output-mode.js';
12
11
  import { mapError } from '../errors/error-mapper.js';
13
12
  import { t } from '../i18n.js';
13
+ import {extractThemeArchive} from '../theme-download.js';
14
14
 
15
15
  const THEME_SEARCH_THRESHOLD = 7;
16
16
 
@@ -20,10 +20,7 @@ export async function themeListCommand() {
20
20
  console.log(chalk.cyan(t('theme.listing') + '\n'));
21
21
  }
22
22
 
23
- const apiClient = await createApiClient();
24
- const response = await apiClient.get('/theme');
25
-
26
- const themes = response.data || [];
23
+ const { themes } = await fetchAllThemes();
27
24
 
28
25
  if (isJsonMode()) {
29
26
  jsonOut({
@@ -291,37 +288,20 @@ export async function themeCreateCommand(themeName) {
291
288
 
292
289
  const response = await apiClient.download('/theme/default/download', downloadParams);
293
290
 
294
- await fs.mkdir(themePath, {recursive: true});
295
-
296
- const zipPath = path.join(themePath, 'theme.zip');
297
- const writer = createWriteStream(zipPath);
298
-
299
- response.data.pipe(writer);
300
-
301
- writer.on('finish', async () => {
302
- console.log(chalk.green(t('theme.files_downloaded')));
303
- console.log(chalk.yellow(t('theme.extracting')));
304
-
305
- const zip = new AdmZip(zipPath);
306
- zip.extractAllTo(themePath, true);
307
-
308
- await fs.unlink(zipPath);
291
+ console.log(chalk.green(t('theme.files_downloaded')));
292
+ console.log(chalk.yellow(t('theme.extracting')));
309
293
 
310
- console.log(chalk.green(t('theme.extracted', { path: `${storeSlug}/${themeSlug}/` })));
294
+ await extractThemeArchive(response.data, themePath);
311
295
 
312
- await setActiveTheme(themeSlug);
313
- console.log(chalk.green(t('theme.active_set') + '\n'));
296
+ console.log(chalk.green(t('theme.extracted', { path: `${storeSlug}/${themeSlug}/` })));
314
297
 
315
- console.log(chalk.bold.green(t('theme.create_success')));
316
- console.log(chalk.gray(' ' + t('theme.create_location', { path: `${storeSlug}/${themeSlug}/` }) + '\n'));
317
- console.log(chalk.cyan(t('theme.dev_start_tip')));
318
- console.log(chalk.white(' tsoft theme dev\n'));
319
- });
298
+ await setActiveTheme(themeSlug);
299
+ console.log(chalk.green(t('theme.active_set') + '\n'));
320
300
 
321
- writer.on('error', (error) => {
322
- console.error(chalk.red('❌ Hata: ' + t('theme.file_write_error')), error.message);
323
- process.exit(1);
324
- });
301
+ console.log(chalk.bold.green(t('theme.create_success')));
302
+ console.log(chalk.gray(' ' + t('theme.create_location', { path: `${storeSlug}/${themeSlug}/` }) + '\n'));
303
+ console.log(chalk.cyan(t('theme.dev_start_tip')));
304
+ console.log(chalk.white(' tsoft theme dev\n'));
325
305
 
326
306
  } catch (error) {
327
307
  console.error(chalk.red('\n❌ Hata: ') + error.message);
@@ -344,9 +324,7 @@ export async function themePullCommand() {
344
324
  console.log(chalk.cyan(t('theme.pull.starting') + '\n'));
345
325
 
346
326
  const apiClient = await createApiClient();
347
- const response = await apiClient.get('/theme');
348
-
349
- const themes = response.data || [];
327
+ const { themes } = await fetchAllThemes(apiClient);
350
328
 
351
329
  if (themes.length === 0) {
352
330
  showWarning(t('theme.pull.no_themes'));
@@ -389,33 +367,16 @@ export async function themePullCommand() {
389
367
  showWarning(t('theme.cancelled'));
390
368
  return;
391
369
  }
392
-
393
- await fs.rm(themePath, {recursive: true, force: true});
394
370
  }
395
371
 
396
372
  console.log(chalk.yellow(t('theme.downloading_files')));
397
373
 
398
374
  const downloadResponse = await apiClient.download(`/theme/${themeUuid}/download`, {});
399
375
 
400
- await fs.mkdir(themePath, {recursive: true});
401
-
402
- const zipPath = path.join(themePath, 'theme.zip');
403
- const writer = createWriteStream(zipPath);
404
-
405
- downloadResponse.data.pipe(writer);
406
-
407
- await new Promise((resolve, reject) => {
408
- writer.on('finish', resolve);
409
- writer.on('error', reject);
410
- });
411
-
412
376
  console.log(chalk.green(t('theme.files_downloaded')));
413
377
  console.log(chalk.yellow(t('theme.extracting')));
414
378
 
415
- const zip = new AdmZip(zipPath);
416
- zip.extractAllTo(themePath, true);
417
-
418
- await fs.unlink(zipPath);
379
+ await extractThemeArchive(downloadResponse.data, themePath);
419
380
 
420
381
  console.log(chalk.green(t('theme.extracted', { path: `${storeSlug}/${themeSlug}/` })));
421
382
 
@@ -445,9 +406,7 @@ export async function themeUseCommand(themeName = null) {
445
406
  console.log(chalk.gray(t('theme.use.on_store', { store })) + '\n');
446
407
  }
447
408
 
448
- const apiClient = await createApiClient();
449
- const response = await apiClient.get('/theme');
450
- const themes = response.data || [];
409
+ const { themes } = await fetchAllThemes();
451
410
 
452
411
  if (themes.length === 0) {
453
412
  showWarning(t('theme.use.no_themes'));
@@ -458,7 +417,8 @@ export async function themeUseCommand(themeName = null) {
458
417
 
459
418
  if (themeName) {
460
419
  const themeSlug = slugify(themeName);
461
- selectedTheme = themes.find(t => slugify(t.name) === themeSlug);
420
+ selectedTheme = themes.find(t => t.theme_folder === themeName)
421
+ ?? themes.find(t => slugify(t.name) === themeSlug);
462
422
 
463
423
  if (!selectedTheme) {
464
424
  console.error(chalk.red('\n' + t('theme.use.not_found', { name: themeName })));
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,99 @@
1
+ import fs from 'fs/promises';
2
+ import { createWriteStream } from 'fs';
3
+ import path from 'path';
4
+ import { pipeline } from 'stream/promises';
5
+ import AdmZip from 'adm-zip';
6
+
7
+ async function stageArchive(stream, stagingPath) {
8
+ const zipPath = path.join(stagingPath, 'theme.zip');
9
+
10
+ await pipeline(stream, createWriteStream(zipPath));
11
+
12
+ const { size } = await fs.stat(zipPath);
13
+
14
+ if (size === 0) {
15
+ throw new Error('EMPTY_ARCHIVE');
16
+ }
17
+
18
+ const zip = new AdmZip(zipPath);
19
+ zip.extractAllTo(stagingPath, true);
20
+
21
+ await fs.rm(zipPath, { force: true });
22
+ }
23
+
24
+ /**
25
+ * Tema arşivini indirir, doğrular ve ancak ondan sonra hedefin üzerine açar.
26
+ *
27
+ * Hedefteki dosyalar silinmez; arşivdekiler üzerine yazılır. Bozuk veya yarım
28
+ * inen bir arşiv hedefe hiç dokunmaz.
29
+ *
30
+ * @param {import('stream').Readable} stream - Arşiv veri akışı
31
+ * @param {string} themePath - Arşivin açılacağı dizin
32
+ * @returns {Promise<void>}
33
+ */
34
+ export async function mergeThemeArchive(stream, themePath) {
35
+ await fs.mkdir(themePath, { recursive: true });
36
+
37
+ const staging = await fs.mkdtemp(path.join(themePath, '.tsoft-update-'));
38
+
39
+ try {
40
+ const zipPath = path.join(staging, 'theme.zip');
41
+
42
+ await pipeline(stream, createWriteStream(zipPath));
43
+
44
+ const { size } = await fs.stat(zipPath);
45
+
46
+ if (size === 0) {
47
+ throw new Error('EMPTY_ARCHIVE');
48
+ }
49
+
50
+ const zip = new AdmZip(zipPath);
51
+ zip.extractAllTo(themePath, true);
52
+ } finally {
53
+ await fs.rm(staging, { recursive: true, force: true });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Tema arşivini indirir, doğrular ve ancak ondan sonra hedefin üzerine koyar.
59
+ *
60
+ * Arşiv hedefin yanındaki geçici dizine açılır; indirme yarıda koparsa veya
61
+ * arşiv bozuksa hedef dizine dokunulmaz. Böylece başarısız bir indirme
62
+ * kullanıcının mevcut tema dosyalarını silmez.
63
+ *
64
+ * @param {import('stream').Readable} stream - Arşiv veri akışı
65
+ * @param {string} themePath - Temanın açılacağı hedef dizin
66
+ * @returns {Promise<void>}
67
+ */
68
+ export async function extractThemeArchive(stream, themePath) {
69
+ const parent = path.dirname(themePath);
70
+ await fs.mkdir(parent, { recursive: true });
71
+
72
+ const staging = await fs.mkdtemp(path.join(parent, `.${path.basename(themePath)}-`));
73
+ const backup = `${staging}-previous`;
74
+
75
+ try {
76
+ await stageArchive(stream, staging);
77
+ } catch (error) {
78
+ await fs.rm(staging, { recursive: true, force: true });
79
+ throw error;
80
+ }
81
+
82
+ const hadPrevious = await fs.access(themePath).then(() => true).catch(() => false);
83
+
84
+ if (hadPrevious) {
85
+ await fs.rename(themePath, backup);
86
+ }
87
+
88
+ try {
89
+ await fs.rename(staging, themePath);
90
+ } catch (error) {
91
+ if (hadPrevious) {
92
+ await fs.rename(backup, themePath);
93
+ }
94
+ await fs.rm(staging, { recursive: true, force: true });
95
+ throw error;
96
+ }
97
+
98
+ await fs.rm(backup, { recursive: true, force: true });
99
+ }
@@ -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
+ }