i18ntk 2.3.4 → 2.3.5

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i18ntk v2.3.4
1
+ # i18ntk v2.3.5
2
2
 
3
3
  Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, and translation completion.
4
4
 
@@ -9,14 +9,15 @@ Zero-dependency internationalization toolkit for setup, scanning, analysis, vali
9
9
  [![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)
10
10
  [![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)
11
11
  [![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
12
- [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.3.4)](https://socket.dev/npm/package/i18ntk/overview/2.3.4)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.3.5)](https://socket.dev/npm/package/i18ntk/overview/2.3.5)
13
13
 
14
14
  ## Upgrade Notice
15
15
 
16
- Versions earlier than `2.3.4` may contain known stability and security issues.
17
- They are considered unsupported for production use. Upgrade to `2.3.4` or newer.
18
- The CLI now checks npm registry metadata at startup and warns when your installed version is out of date.
19
- Set `I18NTK_DISABLE_UPDATE_CHECK=true` to disable this warning in restricted/offline environments.
16
+ Versions earlier than `2.3.5` may contain known stability and security issues.
17
+ They are considered unsupported for production use. Upgrade to `2.3.5` or newer.
18
+ The CLI can check npm registry metadata and warn when your installed version is out of date.
19
+ Set `I18NTK_ENABLE_UPDATE_CHECK=true` to enable this behavior.
20
+ Set `I18NTK_DISABLE_UPDATE_CHECK=true` to force-disable it in restricted/offline environments.
20
21
  Set `I18NTK_DISABLE_AUTOSAVE=1` in server/runtime environments to keep config in memory and skip disk writes.
21
22
 
22
23
  ## What i18ntk Does
@@ -154,7 +155,7 @@ Example `.i18ntk-config`:
154
155
 
155
156
  ```json
156
157
  {
157
- "version": "2.3.4",
158
+ "version": "2.3.5",
158
159
  "sourceDir": "./locales",
159
160
  "i18nDir": "./locales",
160
161
  "outputDir": "./i18ntk-reports",
@@ -177,7 +178,7 @@ See [docs/api/CONFIGURATION.md](docs/api/CONFIGURATION.md) for the full configur
177
178
  - [Runtime API Guide](docs/runtime.md)
178
179
  - [Scanner Guide](docs/scanner-guide.md)
179
180
  - [Environment Variables](docs/environment-variables.md)
180
- - [Migration Guide v2.3.4](docs/migration-guide-v2.3.4.md)
181
+ - [Migration Guide v2.3.5](docs/migration-guide-v2.3.5.md)
181
182
  - [Optimization Prompt](docs/development/package-optimization-prompt.md)
182
183
 
183
184
  ## License
@@ -15,12 +15,20 @@ const SecurityUtils = require('../utils/security');
15
15
  *
16
16
  * Class-based implementation of backup functionality for use with CommandRouter
17
17
  */
18
- class I18nBackup {
18
+ class I18nBackup {
19
19
  constructor(config = {}) {
20
20
  this.config = config;
21
21
  this.backupDir = path.join(process.cwd(), 'i18ntk-backups');
22
22
  this.maxBackups = Math.min(Math.max(parseInt(config.backup?.maxBackups, 10) || 1, 1), 3);
23
- }
23
+ }
24
+
25
+ validateInProject(targetPath, label = 'path') {
26
+ const validated = SecurityUtils.validatePath(targetPath, process.cwd());
27
+ if (!validated) {
28
+ throw new Error(`Invalid ${label}: ${targetPath}`);
29
+ }
30
+ return validated;
31
+ }
24
32
 
25
33
  /**
26
34
  * Main run method for the backup command
@@ -79,13 +87,15 @@ Options:
79
87
  }
80
88
 
81
89
  // Command handlers
82
- async handleCreate(options = {}) {
83
- // Use absolute path for the locales directory
84
- const dir = (options._ && options._[1]) || options.dir || path.join(__dirname, '..', 'locales');
85
- const outputDir = options.output || this.backupDir;
86
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
87
- const backupName = `backup-${timestamp}.json`;
88
- const backupPath = path.join(outputDir, backupName);
90
+ async handleCreate(options = {}) {
91
+ // Use absolute path for the locales directory
92
+ const requestedDir = (options._ && options._[1]) || options.dir || path.join(process.cwd(), 'locales');
93
+ const requestedOutputDir = options.output || this.backupDir;
94
+ const dir = this.validateInProject(path.resolve(process.cwd(), requestedDir), 'source directory');
95
+ const outputDir = this.validateInProject(path.resolve(process.cwd(), requestedOutputDir), 'backup directory');
96
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
97
+ const backupName = `backup-${timestamp}.json`;
98
+ const backupPath = this.validateInProject(path.join(outputDir, backupName), 'backup file');
89
99
 
90
100
  // Log the paths for debugging
91
101
  logger.debug(`Source directory: ${dir}`);
@@ -104,7 +114,7 @@ Options:
104
114
  }
105
115
 
106
116
  // Validate directory
107
- const sourceDir = path.resolve(dir);
117
+ const sourceDir = dir;
108
118
  try {
109
119
  const stats = await fsp.stat(sourceDir);
110
120
  if (!stats.isDirectory()) {
@@ -163,16 +173,17 @@ Options:
163
173
  };
164
174
  }
165
175
 
166
- async handleRestore(options = {}) {
167
- const backupFile = options._ && options._[1];
168
- if (!backupFile) {
169
- throw new Error('Backup file path is required');
170
- }
171
-
172
- const backupPath = path.resolve(process.cwd(), backupFile);
173
- const outputDir = options.output
174
- ? path.resolve(process.cwd(), options.output)
175
- : path.join(process.cwd(), 'restored');
176
+ async handleRestore(options = {}) {
177
+ const backupFile = options._ && options._[1];
178
+ if (!backupFile) {
179
+ throw new Error('Backup file path is required');
180
+ }
181
+
182
+ const backupPath = this.validateInProject(path.resolve(process.cwd(), backupFile), 'backup file');
183
+ const outputDir = this.validateInProject(
184
+ options.output ? path.resolve(process.cwd(), options.output) : path.join(process.cwd(), 'restored'),
185
+ 'restore output directory'
186
+ );
176
187
 
177
188
  // Validate backup file
178
189
  if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
@@ -183,21 +194,21 @@ Options:
183
194
 
184
195
  try {
185
196
  // Read the backup file
186
- const backupData = await fs.readFile(backupPath, 'utf8');
187
- const translations = JSON.parse(backupData);
197
+ const backupData = await fsp.readFile(backupPath, 'utf8');
198
+ const translations = JSON.parse(backupData);
188
199
 
189
200
  // Create output directory if it doesn't exist
190
201
  try {
191
- await fs.mkdir(outputDir, { recursive: true });
192
- } catch (err) {
193
- if (err.code !== 'EEXIST') throw err;
194
- }
202
+ await fsp.mkdir(outputDir, { recursive: true });
203
+ } catch (err) {
204
+ if (err.code !== 'EEXIST') throw err;
205
+ }
195
206
 
196
207
  // Write the restored files
197
- for (const [file, content] of Object.entries(translations)) {
198
- const filePath = path.join(outputDir, file);
199
- await fs.writeFile(filePath, JSON.stringify(content, null, 2));
200
- }
208
+ for (const [file, content] of Object.entries(translations)) {
209
+ const filePath = this.validateInProject(path.join(outputDir, file), 'restore file');
210
+ await fsp.writeFile(filePath, JSON.stringify(content, null, 2), 'utf8');
211
+ }
201
212
 
202
213
  logger.success('Backup restored successfully');
203
214
  logger.info(` Restored ${Object.keys(translations).length} files to: ${outputDir}`);
@@ -214,12 +225,13 @@ Options:
214
225
  }
215
226
  }
216
227
 
217
- async handleList() {
218
- try {
219
- // Ensure backup directory exists
220
- try {
221
- await fsp.access(this.backupDir);
222
- } catch (err) {
228
+ async handleList() {
229
+ const backupDir = this.validateInProject(this.backupDir, 'backup directory');
230
+ try {
231
+ // Ensure backup directory exists
232
+ try {
233
+ await fsp.access(backupDir);
234
+ } catch (err) {
223
235
  if (err.code === 'ENOENT') {
224
236
  logger.warn('No backups found. The backup directory does not exist yet.');
225
237
  } else {
@@ -228,13 +240,13 @@ Options:
228
240
  return { success: true, backups: [] };
229
241
  }
230
242
 
231
- const files = await fsp.readdir(this.backupDir);
243
+ const files = await fsp.readdir(backupDir);
232
244
  const backups = [];
233
245
 
234
246
  for (const file of files) {
235
247
  if (file.startsWith('backup-') && file.endsWith('.json')) {
236
248
  try {
237
- const filePath = path.join(this.backupDir, file);
249
+ const filePath = this.validateInProject(path.join(backupDir, file), 'backup file');
238
250
  const stats = await fsp.stat(filePath);
239
251
  backups.push({
240
252
  name: file,
@@ -288,7 +300,7 @@ Options:
288
300
  throw new Error('Backup file path is required');
289
301
  }
290
302
 
291
- const backupPath = path.resolve(process.cwd(), backupFile);
303
+ const backupPath = this.validateInProject(path.resolve(process.cwd(), backupFile), 'backup file');
292
304
 
293
305
  // Validate backup file
294
306
  if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
@@ -328,21 +340,22 @@ Options:
328
340
  }
329
341
  }
330
342
 
331
- async handleCleanup(options = {}) {
332
- const keep = options.keep ? parseInt(options.keep, 10) : this.maxBackups;
343
+ async handleCleanup(options = {}) {
344
+ const keep = options.keep ? parseInt(options.keep, 10) : this.maxBackups;
345
+ const backupDir = this.validateInProject(this.backupDir, 'backup directory');
333
346
 
334
347
  logger.info('\nCleaning up old backups...');
335
348
 
336
349
  try {
337
- const files = await fsp.readdir(this.backupDir);
338
- const backupFiles = files
339
- .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
340
- .map(file => ({
341
- name: file,
342
- path: path.join(this.backupDir, file),
343
- time: fs.statSync(path.join(this.backupDir, file)).mtime.getTime()
350
+ const files = await fsp.readdir(backupDir);
351
+ const backupFiles = files
352
+ .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
353
+ .map(file => ({
354
+ name: file,
355
+ path: this.validateInProject(path.join(backupDir, file), 'backup file'),
356
+ time: (SecurityUtils.safeStatSync(path.join(backupDir, file), process.cwd()) || { mtime: new Date(0) }).mtime.getTime()
344
357
  }))
345
- .sort((a, b) => b.time - a.time);
358
+ .sort((a, b) => b.time - a.time);
346
359
 
347
360
  // Keep only the most recent 'keep' files
348
361
  const toDelete = backupFiles.slice(keep);
@@ -379,27 +392,28 @@ Options:
379
392
  }
380
393
  }
381
394
 
382
- async cleanupOldBackups(outputDir) {
383
- try {
384
- const files = await fs.readdir(outputDir);
385
- const backupFiles = files
386
- .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
387
- .map(file => ({
388
- name: file,
389
- path: path.join(outputDir, file),
390
- time: fs.statSync(path.join(outputDir, file)).mtime.getTime()
391
- }))
392
- .sort((a, b) => b.time - a.time);
395
+ async cleanupOldBackups(outputDir) {
396
+ try {
397
+ const safeOutputDir = this.validateInProject(outputDir, 'backup output directory');
398
+ const files = await fsp.readdir(safeOutputDir);
399
+ const backupFiles = files
400
+ .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
401
+ .map(file => ({
402
+ name: file,
403
+ path: this.validateInProject(path.join(safeOutputDir, file), 'backup file'),
404
+ time: (SecurityUtils.safeStatSync(path.join(safeOutputDir, file), process.cwd()) || { mtime: new Date(0) }).mtime.getTime()
405
+ }))
406
+ .sort((a, b) => b.time - a.time);
393
407
 
394
408
  // Keep only the most recent files
395
409
  const toDelete = backupFiles.slice(this.maxBackups);
396
410
 
397
- for (const file of toDelete) {
398
- try {
399
- await fs.unlink(file.path);
400
- } catch (err) {
401
- // Ignore cleanup errors
402
- }
411
+ for (const file of toDelete) {
412
+ try {
413
+ await fsp.unlink(file.path);
414
+ } catch (err) {
415
+ // Ignore cleanup errors
416
+ }
403
417
  }
404
418
  } catch (error) {
405
419
  // Ignore cleanup errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "2.3.4",
3
+ "version": "2.3.5",
4
4
  "description": "🚀 The fastest internationalization toolkit with 97% performance boost! Zero-dependency, enterprise-grade internationalization for React, Vue, Angular, Python, Java, PHP & more. Features PIN protection, auto framework detection, 7+ UI languages, and comprehensive translation management. Perfect for startups to enterprises.",
5
5
  "keywords": [
6
6
  "i18n",
@@ -227,7 +227,7 @@
227
227
  },
228
228
  "preferGlobal": true,
229
229
  "versionInfo": {
230
- "version": "2.3.4",
230
+ "version": "2.3.5",
231
231
  "releaseDate": "12/04/2026",
232
232
  "lastUpdated": "12/04/2026",
233
233
  "maintainer": "Vlad Noskov",
@@ -238,9 +238,10 @@
238
238
  "HOTFIX: Removed deprecated package-path fallback that caused production build warnings for non-exported subpaths.",
239
239
  "CRITICAL FIX: Resolved sizing and usage-analysis regressions in v2 command flow.",
240
240
  "PACKAGING: Reduced publish footprint by removing internal development scripts and legacy fixed-file artifacts.",
241
- "SECURITY: Hardened release checks and added explicit support guidance to update from pre-2.3.4 versions.",
241
+ "SECURITY: Hardened release checks and added explicit support guidance to update from pre-2.3.5 versions.",
242
242
  "CONFIG: Added cross-process file locking for .i18ntk-config writes to prevent production rename races.",
243
243
  "CONFIG: Made autosave runtime-safe with non-throwing save failures and I18NTK_DISABLE_AUTOSAVE support.",
244
+ "SECURITY: Hardened reset/backup path handling and made npm update checks explicit opt-in.",
244
245
  "CLI: Added npm registry version check with upgrade warning for out-of-date installs.",
245
246
  "I18N: Completed internal UI locale parity and actionable untranslated-key cleanup across supported languages."
246
247
  ],
@@ -267,7 +268,7 @@
267
268
  "spring-boot": ">=2.5.0",
268
269
  "laravel": ">=8.0.0"
269
270
  },
270
- "supportPolicy": "Versions earlier than 2.3.4 may be unstable or insecure. Upgrade to 2.3.4 or newer."
271
+ "supportPolicy": "Versions earlier than 2.3.5 may be unstable or insecure. Upgrade to 2.3.5 or newer."
271
272
  },
272
273
  "_comment": "This package is zero-dependency and uses only native Node.js modules"
273
274
  }
@@ -312,6 +312,18 @@ class SettingsManager {
312
312
  this.settings = this.loadSettings();
313
313
  }
314
314
 
315
+ resolveSafePath(targetPath, basePath = process.cwd()) {
316
+ return SecurityUtils.validatePath(path.resolve(targetPath), basePath);
317
+ }
318
+
319
+ safeDeleteFile(targetPath, basePath = process.cwd()) {
320
+ const validated = this.resolveSafePath(targetPath, basePath);
321
+ if (!validated) return false;
322
+ if (!SecurityUtils.safeExistsSync(validated, basePath)) return false;
323
+ fs.unlinkSync(validated);
324
+ return true;
325
+ }
326
+
315
327
  /**
316
328
  * Load settings from file or return default settings
317
329
  * @returns {object} Settings object
@@ -432,12 +444,18 @@ class SettingsManager {
432
444
  */
433
445
  _saveImmediately() {
434
446
  try {
435
- if (!SecurityUtils.safeExistsSync(this.configDir)) {
436
- fs.mkdirSync(this.configDir, { recursive: true });
447
+ const configDir = path.dirname(this.configFile);
448
+ const validatedConfigDir = this.resolveSafePath(configDir, process.cwd());
449
+ if (!validatedConfigDir) {
450
+ throw new Error('Invalid configuration directory');
451
+ }
452
+
453
+ if (!SecurityUtils.safeExistsSync(validatedConfigDir, process.cwd())) {
454
+ SecurityUtils.safeMkdirSync(validatedConfigDir, process.cwd(), { recursive: true });
437
455
  }
438
456
 
439
457
  const content = JSON.stringify(this.settings, null, 4);
440
- SecurityUtils.safeWriteFileSync(this.configFile, content, path.dirname(this.configFile), 'utf8');
458
+ SecurityUtils.safeWriteFileSync(this.configFile, content, validatedConfigDir, 'utf8');
441
459
 
442
460
  // Create backup if enabled
443
461
  if (this.settings.backup?.enabled) {
@@ -466,13 +484,19 @@ class SettingsManager {
466
484
  }
467
485
 
468
486
  if (!SecurityUtils.safeExistsSync(validatedBackupDir, process.cwd())) {
469
- fs.mkdirSync(validatedBackupDir, { recursive: true });
487
+ SecurityUtils.safeMkdirSync(validatedBackupDir, process.cwd(), { recursive: true });
470
488
  }
471
489
 
472
490
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
473
491
  const backupFile = path.join(validatedBackupDir, `config-${timestamp}.json`);
492
+ const validatedConfigFile = this.resolveSafePath(this.configFile, process.cwd());
493
+ const validatedBackupFile = this.resolveSafePath(backupFile, process.cwd());
494
+ if (!validatedConfigFile || !validatedBackupFile) {
495
+ console.error('Error creating backup: Invalid source or destination path');
496
+ return;
497
+ }
474
498
 
475
- fs.copyFileSync(this.configFile, backupFile);
499
+ fs.copyFileSync(validatedConfigFile, validatedBackupFile);
476
500
 
477
501
  // Clean old backups
478
502
  this.cleanupOldBackups(validatedBackupDir);
@@ -486,17 +510,20 @@ class SettingsManager {
486
510
  */
487
511
  cleanupOldBackups(backupDirectory = null) {
488
512
  try {
489
- const activeBackupDir = backupDirectory || this.backupDir;
513
+ const activeBackupDir = this.resolveSafePath(backupDirectory || this.backupDir, process.cwd());
514
+ if (!activeBackupDir) {
515
+ return;
516
+ }
490
517
  if (!SecurityUtils.safeExistsSync(activeBackupDir, process.cwd())) {
491
518
  return;
492
519
  }
493
520
 
494
- const files = fs.readdirSync(activeBackupDir)
521
+ const files = (SecurityUtils.safeReaddirSync(activeBackupDir, process.cwd()) || [])
495
522
  .filter(file => file.startsWith('config-') && file.endsWith('.json'))
496
523
  .map(file => ({
497
524
  name: file,
498
525
  path: path.join(activeBackupDir, file),
499
- mtime: fs.statSync(path.join(activeBackupDir, file)).mtime
526
+ mtime: (SecurityUtils.safeStatSync(path.join(activeBackupDir, file), process.cwd()) || { mtime: new Date(0) }).mtime
500
527
  }))
501
528
  .sort((a, b) => b.mtime - a.mtime);
502
529
 
@@ -506,7 +533,7 @@ class SettingsManager {
506
533
  : 1;
507
534
  if (files.length > maxBackups) {
508
535
  files.slice(maxBackups).forEach(file => {
509
- fs.unlinkSync(file.path);
536
+ this.safeDeleteFile(file.path, process.cwd());
510
537
  });
511
538
  }
512
539
  } catch (error) {
@@ -546,52 +573,49 @@ class SettingsManager {
546
573
 
547
574
  // 2. Remove actual configuration files used by the system
548
575
  const packageDir = path.resolve(__dirname, '..');
576
+ const projectRoot = process.cwd();
549
577
  const settingsDir = path.join(packageDir, 'settings');
550
578
 
551
579
  // Main configuration file
552
580
  const mainConfigPath = path.join(settingsDir, 'i18ntk-config.json');
553
- if (SecurityUtils.safeExistsSync(mainConfigPath)) {
554
- fs.unlinkSync(mainConfigPath);
581
+ if (this.safeDeleteFile(mainConfigPath, packageDir)) {
555
582
  console.log('✅ Main configuration file removed');
556
583
  }
557
584
 
558
585
  // Project configuration file
559
586
  const projectConfigPath = path.join(settingsDir, 'project-config.json');
560
- if (SecurityUtils.safeExistsSync(projectConfigPath)) {
561
- fs.unlinkSync(projectConfigPath);
587
+ if (this.safeDeleteFile(projectConfigPath, packageDir)) {
562
588
  console.log('✅ Project configuration removed');
563
589
  }
564
590
 
565
591
  // Setup tracking file
566
592
  const setupFile = path.join(settingsDir, 'setup.json');
567
- if (SecurityUtils.safeExistsSync(setupFile)) {
568
- fs.unlinkSync(setupFile);
593
+ if (this.safeDeleteFile(setupFile, packageDir)) {
569
594
  console.log('✅ Setup tracking cleared');
570
595
  }
571
596
 
572
597
  // 3. Clear all backup files from backups directory
573
598
  const backupsDir = path.join(packageDir, 'backups');
574
- if (SecurityUtils.safeExistsSync(backupsDir)) {
575
- const backupFiles = fs.readdirSync(backupsDir);
599
+ if (SecurityUtils.safeExistsSync(backupsDir, packageDir)) {
600
+ const backupFiles = SecurityUtils.safeReaddirSync(backupsDir, packageDir) || [];
576
601
  for (const file of backupFiles) {
577
602
  if (file.endsWith('.json') || file.endsWith('.bak')) {
578
- fs.unlinkSync(path.join(backupsDir, file));
603
+ this.safeDeleteFile(path.join(backupsDir, file), packageDir);
579
604
  }
580
605
  }
581
606
  console.log('✅ All backup files cleared');
582
607
  }
583
608
 
584
- // 4. Clear admin PIN configuration (multiple possible locations including root)
609
+ // 4. Clear admin PIN configuration (package/project scope only)
585
610
  const adminConfigPaths = [
586
611
  path.join(packageDir, '.i18n-admin-config.json'),
587
612
  path.join(settingsDir, '.i18n-admin-config.json'),
588
613
  path.join(settingsDir, 'admin-config.json'),
589
- path.join(packageDir, '..', '.i18n-admin-config.json') // Root level
614
+ path.join(projectRoot, '.i18n-admin-config.json')
590
615
  ];
591
616
 
592
617
  for (const adminConfigPath of adminConfigPaths) {
593
- if (SecurityUtils.safeExistsSync(adminConfigPath)) {
594
- fs.unlinkSync(adminConfigPath);
618
+ if (this.safeDeleteFile(adminConfigPath, projectRoot)) {
595
619
  console.log('✅ Admin PIN configuration cleared');
596
620
  }
597
621
  }
@@ -603,8 +627,7 @@ class SettingsManager {
603
627
  ];
604
628
 
605
629
  for (const initFile of initFiles) {
606
- if (SecurityUtils.safeExistsSync(initFile)) {
607
- fs.unlinkSync(initFile);
630
+ if (this.safeDeleteFile(initFile, packageDir)) {
608
631
  console.log('✅ Initialization tracking cleared');
609
632
  }
610
633
  }
@@ -616,10 +639,10 @@ class SettingsManager {
616
639
  ];
617
640
 
618
641
  for (const cacheDir of cacheDirs) {
619
- if (SecurityUtils.safeExistsSync(cacheDir)) {
620
- const cacheFiles = fs.readdirSync(cacheDir);
642
+ if (SecurityUtils.safeExistsSync(cacheDir, packageDir)) {
643
+ const cacheFiles = SecurityUtils.safeReaddirSync(cacheDir, packageDir) || [];
621
644
  for (const file of cacheFiles) {
622
- fs.unlinkSync(path.join(cacheDir, file));
645
+ this.safeDeleteFile(path.join(cacheDir, file), packageDir);
623
646
  }
624
647
  console.log('✅ Cache cleared');
625
648
  }
@@ -639,9 +662,7 @@ class SettingsManager {
639
662
  ];
640
663
 
641
664
  for (const tempFile of tempFiles) {
642
- if (SecurityUtils.safeExistsSync(tempFile)) {
643
- fs.unlinkSync(tempFile);
644
- }
665
+ this.safeDeleteFile(tempFile, packageDir);
645
666
  }
646
667
  console.log('✅ Temporary files cleared');
647
668
 
@@ -656,8 +677,7 @@ class SettingsManager {
656
677
  ];
657
678
 
658
679
  for (const file of additionalFiles) {
659
- if (SecurityUtils.safeExistsSync(file)) {
660
- fs.unlinkSync(file);
680
+ if (this.safeDeleteFile(file, packageDir)) {
661
681
  console.log(`✅ Removed ${path.basename(file)}`);
662
682
  }
663
683
  }
@@ -108,6 +108,11 @@ async function printUpgradeWarningIfOutdated({
108
108
  currentVersion,
109
109
  timeoutMs = DEFAULT_TIMEOUT_MS
110
110
  }) {
111
+ const enabled = String(process.env.I18NTK_ENABLE_UPDATE_CHECK || '').toLowerCase();
112
+ if (!(enabled === '1' || enabled === 'true' || enabled === 'yes')) {
113
+ return;
114
+ }
115
+
111
116
  if (process.env.I18NTK_DISABLE_UPDATE_CHECK === 'true') {
112
117
  return;
113
118
  }