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 +13 -0
- package/package.json +1 -1
- package/src/commands/theme-dev.js +22 -57
- package/src/commands/theme-init.js +14 -31
- package/src/commands/theme-section.js +3 -37
- package/src/commands/theme.js +18 -58
- package/src/storage.js +2 -4
- package/src/theme-download.js +99 -0
- package/src/theme-list.js +25 -0
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,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 =>
|
|
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
|
|
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(`${
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
407
|
+
await setActiveTheme(selectedTheme.theme_folder);
|
|
446
408
|
} else if (!themeInfo) {
|
|
447
409
|
themeInfo = await getThemeUuid(themeSlug);
|
|
448
410
|
}
|
|
449
411
|
|
|
450
|
-
|
|
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,
|
|
453
|
-
console.log(chalk.green(t('dev.working_dir', { path: `${storeSlug}/${
|
|
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
|
|
459
|
-
const currentTheme =
|
|
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
|
-
|
|
484
|
-
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|
package/src/commands/theme.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
294
|
+
await extractThemeArchive(response.data, themePath);
|
|
311
295
|
|
|
312
|
-
|
|
313
|
-
console.log(chalk.green(t('theme.active_set') + '\n'));
|
|
296
|
+
console.log(chalk.green(t('theme.extracted', { path: `${storeSlug}/${themeSlug}/` })));
|
|
314
297
|
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =>
|
|
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 {
|
|
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,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
|
+
}
|