tsoft-cli 3.12.3 → 3.12.4

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,11 @@ 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.4] - 2026-08-11
9
+
10
+ ### Fixed
11
+ - `theme pull` no longer fails on Windows with `EBUSY: resource busy or locked`. Putting a finished download in place moved the existing theme directory aside first, and Windows refuses to rename a directory that a terminal is sitting in or that has a file open in an editor — so the command failed for anyone whose editor had the theme open, which is the normal case. The theme directory now stays where it is: fresh files are copied in and files no longer present in the archive are removed afterwards, so nothing has to be renamed. macOS and Linux were unaffected because they allow renaming a directory that is in use.
12
+
8
13
  ## [3.12.3] - 2026-08-11
9
14
 
10
15
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsoft-cli",
3
- "version": "3.12.3",
3
+ "version": "3.12.4",
4
4
  "description": "Command-line tool for tsoft360 theme development and management",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -21,79 +21,85 @@ async function stageArchive(stream, stagingPath) {
21
21
  await fs.rm(zipPath, { force: true });
22
22
  }
23
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 });
24
+ async function withStaging(themePath, apply) {
25
+ const parent = path.dirname(themePath);
26
+ await fs.mkdir(parent, { recursive: true });
36
27
 
37
- const staging = await fs.mkdtemp(path.join(themePath, '.tsoft-update-'));
28
+ const staging = await fs.mkdtemp(path.join(parent, `.${path.basename(themePath)}-`));
38
29
 
39
30
  try {
40
- const zipPath = path.join(staging, 'theme.zip');
31
+ return await apply(staging);
32
+ } finally {
33
+ await fs.rm(staging, { recursive: true, force: true });
34
+ }
35
+ }
41
36
 
42
- await pipeline(stream, createWriteStream(zipPath));
37
+ async function copyInto(sourceDir, targetDir) {
38
+ for (const entry of await fs.readdir(sourceDir)) {
39
+ await fs.cp(path.join(sourceDir, entry), path.join(targetDir, entry), {
40
+ recursive: true,
41
+ force: true,
42
+ });
43
+ }
44
+ }
43
45
 
44
- const { size } = await fs.stat(zipPath);
46
+ /**
47
+ * Arşivde bulunmayan girdileri hedeften siler.
48
+ *
49
+ * Dizini yeniden adlandırmak Windows'ta içine girilmişse veya içindeki bir
50
+ * dosya açıksa EBUSY verir; bu yüzden hedef dizin yerinde bırakılır ve yalnızca
51
+ * fazlalık girdiler kaldırılır. Kalıcı bir kilit tek bir dosyayı geride
52
+ * bırakabilir, ama temanın geri kalanı yine güncellenmiş olur.
53
+ */
54
+ async function removeStaleEntries(themePath, stagingPath) {
55
+ const fresh = new Set(await fs.readdir(stagingPath));
45
56
 
46
- if (size === 0) {
47
- throw new Error('EMPTY_ARCHIVE');
57
+ for (const entry of await fs.readdir(themePath)) {
58
+ if (!fresh.has(entry)) {
59
+ await fs.rm(path.join(themePath, entry), { recursive: true, force: true });
48
60
  }
49
-
50
- const zip = new AdmZip(zipPath);
51
- zip.extractAllTo(themePath, true);
52
- } finally {
53
- await fs.rm(staging, { recursive: true, force: true });
54
61
  }
55
62
  }
56
63
 
57
64
  /**
58
- * Tema arşivini indirir, doğrular ve ancak ondan sonra hedefin üzerine koyar.
65
+ * Tema arşivini indirir, doğrular ve ancak ondan sonra hedefe yerleştirir.
59
66
  *
60
67
  * 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.
68
+ * arşiv bozuksa hedef dizine dokunulmaz. Hedef dizinin kendisi taşınmaz,
69
+ * yalnızca içeriği değiştirilir dizin bir editörde veya kabukta açıkken de
70
+ * çalışır.
63
71
  *
64
72
  * @param {import('stream').Readable} stream - Arşiv veri akışı
65
73
  * @param {string} themePath - Temanın açılacağı hedef dizin
66
74
  * @returns {Promise<void>}
67
75
  */
68
76
  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 {
77
+ await withStaging(themePath, async (staging) => {
76
78
  await stageArchive(stream, staging);
77
- } catch (error) {
78
- await fs.rm(staging, { recursive: true, force: true });
79
- throw error;
80
- }
81
79
 
82
- const hadPrevious = await fs.access(themePath).then(() => true).catch(() => false);
80
+ await fs.mkdir(themePath, { recursive: true });
83
81
 
84
- if (hadPrevious) {
85
- await fs.rename(themePath, backup);
86
- }
82
+ await copyInto(staging, themePath);
83
+ await removeStaleEntries(themePath, staging);
84
+ });
85
+ }
87
86
 
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
- }
87
+ /**
88
+ * Tema arşivini indirir, doğrular ve ancak ondan sonra hedefin üzerine açar.
89
+ *
90
+ * Hedefteki dosyalar silinmez; arşivdekiler üzerine yazılır. Bozuk veya yarım
91
+ * inen bir arşiv hedefe hiç dokunmaz.
92
+ *
93
+ * @param {import('stream').Readable} stream - Arşiv veri akışı
94
+ * @param {string} themePath - Arşivin açılacağı dizin
95
+ * @returns {Promise<void>}
96
+ */
97
+ export async function mergeThemeArchive(stream, themePath) {
98
+ await withStaging(themePath, async (staging) => {
99
+ await stageArchive(stream, staging);
100
+
101
+ await fs.mkdir(themePath, { recursive: true });
97
102
 
98
- await fs.rm(backup, { recursive: true, force: true });
103
+ await copyInto(staging, themePath);
104
+ });
99
105
  }