i18ntk 2.3.8 → 2.4.0

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.8
1
+ # i18ntk v2.4.0
2
2
 
3
3
  Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, and translation completion.
4
4
 
@@ -9,12 +9,12 @@ 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.8)](https://socket.dev/npm/package/i18ntk/overview/2.3.8)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.4.0)](https://socket.dev/npm/package/i18ntk/overview/2.4.0)
13
13
 
14
14
  ## Upgrade Notice
15
15
 
16
- Versions earlier than `2.3.8` may contain known stability and security issues.
17
- They are considered unsupported for production use. Upgrade to `2.3.8` or newer.
16
+ Versions earlier than `2.4.0` may contain known stability and security issues.
17
+ They are considered unsupported for production use. Upgrade to `2.4.0` or newer.
18
18
 
19
19
  ## What i18ntk Does
20
20
 
@@ -104,6 +104,9 @@ i18ntk-fixer
104
104
  i18ntk-backup
105
105
  ```
106
106
 
107
+ Note: `i18ntk --command=backup` in the manager flow is disabled in current builds.
108
+ Use the standalone `i18ntk-backup` executable when backup operations are required.
109
+
107
110
  ## Common Flags
108
111
 
109
112
  - `--source-dir <path>`
@@ -151,7 +154,7 @@ Example `.i18ntk-config`:
151
154
 
152
155
  ```json
153
156
  {
154
- "version": "2.3.8",
157
+ "version": "2.4.0",
155
158
  "sourceDir": "./locales",
156
159
  "i18nDir": "./locales",
157
160
  "outputDir": "./i18ntk-reports",
@@ -174,7 +177,7 @@ See [docs/api/CONFIGURATION.md](docs/api/CONFIGURATION.md) for the full configur
174
177
  - [Runtime API Guide](docs/runtime.md)
175
178
  - [Scanner Guide](docs/scanner-guide.md)
176
179
  - [Environment Variables](docs/environment-variables.md)
177
- - [Migration Guide v2.3.8](docs/migration-guide-v2.3.8.md)
180
+ - [Migration Guide v2.4.0](docs/migration-guide-v2.4.0.md)
178
181
  - [Optimization Prompt](docs/development/package-optimization-prompt.md)
179
182
 
180
183
  ## Code of Conduct
@@ -1,430 +1,42 @@
1
- #!/usr/bin/env node
2
-
3
- 'use strict';
4
-
5
- const fs = require('fs');
6
- const fsp = fs.promises;
7
- const path = require('path');
8
- const configManager = require('../utils/config-manager');
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
9
5
  const { logger } = require('../utils/logger');
10
- const { colors } = require('../utils/logger');
11
- const SecurityUtils = require('../utils/security');
12
-
13
- /**
14
- * I18NTK BACKUP CLASS
15
- *
16
- * Class-based implementation of backup functionality for use with CommandRouter
17
- */
6
+
7
+ /**
8
+ * I18NTK BACKUP CLASS
9
+ *
10
+ * Backup operations are intentionally disabled in the manager command path
11
+ * to reduce filesystem scanner surface.
12
+ */
18
13
  class I18nBackup {
19
- constructor(config = {}) {
20
- this.config = config;
21
- this.backupDir = path.join(process.cwd(), 'i18ntk-backups');
22
- this.maxBackups = Math.min(Math.max(parseInt(config.backup?.maxBackups, 10) || 1, 1), 3);
23
- }
14
+ constructor(config = {}) {
15
+ this.config = config;
16
+ }
24
17
 
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
- }
32
-
33
- /**
34
- * Main run method for the backup command
35
- */
36
- async run(options = {}) {
37
- const command = options.command || options._ && options._[0];
38
-
39
- // Show help if no command provided
40
- if (!command) {
41
- this.showHelp();
42
- return { success: true, command: 'help' };
43
- }
44
-
45
- try {
46
- switch (command) {
47
- case 'create':
48
- return await this.handleCreate(options);
49
- case 'restore':
50
- return await this.handleRestore(options);
51
- case 'list':
52
- return await this.handleList();
53
- case 'verify':
54
- return await this.handleVerify(options);
55
- case 'cleanup':
56
- return await this.handleCleanup(options);
57
- default:
58
- logger.error(`Unknown command: ${command}`);
59
- this.showHelp();
60
- return { success: false, error: `Unknown command: ${command}` };
61
- }
62
- } catch (error) {
63
- this.handleError(error);
64
- return { success: false, error: error.message };
65
- }
66
- }
67
-
68
- showHelp() {
69
- console.log(`
70
- i18ntk-backup - Secure backup and restore for i18n translation files
71
-
72
- Usage:
73
- i18ntk-backup <command> [options]
74
-
75
- Commands:
76
- create <dir> Create a backup of translation files
77
- restore <file> Restore from a backup
78
- list List available backups
79
- verify <file> Verify the integrity of a backup file
80
- cleanup Remove old backups
81
-
82
- Options:
83
- --output <path> Output directory for backup/restore
84
- --force Overwrite existing files without prompting
85
- --keep <number> Number of backups to keep (default: 10)
86
- `);
87
- }
88
-
89
- // Command handlers
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');
99
-
100
- // Log the paths for debugging
101
- logger.debug(`Source directory: ${dir}`);
102
- logger.debug(`Backup will be saved to: ${backupPath}`);
103
-
104
- // Create backup directory if it doesn't exist
105
- try {
106
- await fsp.mkdir(outputDir, { recursive: true });
107
- logger.debug(`Created backup directory: ${outputDir}`);
108
- } catch (err) {
109
- if (err.code !== 'EEXIST') {
110
- logger.error(`Failed to create backup directory: ${err.message}`);
111
- throw err;
112
- }
113
- logger.debug(`Using existing backup directory: ${outputDir}`);
114
- }
115
-
116
- // Validate directory
117
- const sourceDir = dir;
118
- try {
119
- const stats = await fsp.stat(sourceDir);
120
- if (!stats.isDirectory()) {
121
- throw new Error(`Path exists but is not a directory: ${sourceDir}`);
122
- }
123
- logger.debug(`Source directory exists: ${sourceDir}`);
124
- } catch (err) {
125
- if (err.code === 'ENOENT') {
126
- throw new Error(`Directory not found: ${sourceDir}. Please specify a valid directory.`);
127
- }
128
- throw new Error(`Error accessing directory ${sourceDir}: ${err.message}`);
129
- }
130
-
131
- logger.info('\nCreating backup...');
132
-
133
- // Read all files in the directory
134
- const files = (await fsp.readdir(sourceDir, { withFileTypes: true }))
135
- .filter(dirent => dirent.isFile() && dirent.name.endsWith('.json'))
136
- .map(dirent => dirent.name);
137
-
138
- if (files.length === 0) {
139
- logger.warn('No JSON files found in the specified directory');
140
- return { success: true, message: 'No files to backup' };
141
- }
142
-
143
- // Read all translation files
144
- const translations = {};
145
- for (const file of files) {
146
- const filePath = path.join(sourceDir, file);
147
- try {
148
- const content = JSON.parse(await fsp.readFile(filePath, 'utf8'));
149
- translations[file] = content;
150
- } catch (error) {
151
- logger.error(`Could not read file ${file}: ${error.message}`);
152
- }
153
- }
154
-
155
- // Create the backup
156
- await fsp.writeFile(backupPath, JSON.stringify(translations, null, 2));
157
- const stats = await fsp.stat(backupPath);
158
-
159
- logger.success('Backup created successfully');
160
- logger.info(` Location: ${backupPath}`);
161
- logger.info(` Size: ${(stats.size / 1024).toFixed(2)} KB`);
162
- logger.info(` Timestamp: ${new Date().toLocaleString()}`);
163
-
164
- // Clean up old backups
165
- await this.cleanupOldBackups(outputDir);
166
-
167
- return {
168
- success: true,
169
- command: 'create',
170
- backupPath,
171
- size: stats.size,
172
- fileCount: files.length
173
- };
174
- }
175
-
176
- async handleRestore(options = {}) {
177
- const backupFile = options._ && options._[1];
178
- if (!backupFile) {
179
- throw new Error('Backup file path is required');
180
- }
18
+ showHelp() {
19
+ logger.warn('Backup command is disabled in this build.');
20
+ logger.info('Use project-level source control or external backup tooling instead.');
21
+ }
181
22
 
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
- );
187
-
188
- // Validate backup file
189
- if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
190
- throw new Error(`Backup file not found: ${backupPath}`);
191
- }
192
-
193
- logger.info('\nRestoring backup...');
194
-
195
- try {
196
- // Read the backup file
197
- const backupData = await fsp.readFile(backupPath, 'utf8');
198
- const translations = JSON.parse(backupData);
199
-
200
- // Create output directory if it doesn't exist
201
- try {
202
- await fsp.mkdir(outputDir, { recursive: true });
203
- } catch (err) {
204
- if (err.code !== 'EEXIST') throw err;
205
- }
206
-
207
- // Write the restored files
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
- }
212
-
213
- logger.success('Backup restored successfully');
214
- logger.info(` Restored ${Object.keys(translations).length} files to: ${outputDir}`);
215
-
216
- return {
217
- success: true,
218
- command: 'restore',
219
- outputDir,
220
- fileCount: Object.keys(translations).length
221
- };
222
- } catch (error) {
223
- this.handleError(error);
224
- throw error;
225
- }
226
- }
227
-
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) {
235
- if (err.code === 'ENOENT') {
236
- logger.warn('No backups found. The backup directory does not exist yet.');
237
- } else {
238
- logger.error(`Error accessing backup directory: ${err.message}`);
239
- }
240
- return { success: true, backups: [] };
241
- }
242
-
243
- const files = await fsp.readdir(backupDir);
244
- const backups = [];
245
-
246
- for (const file of files) {
247
- if (file.startsWith('backup-') && file.endsWith('.json')) {
248
- try {
249
- const filePath = this.validateInProject(path.join(backupDir, file), 'backup file');
250
- const stats = await fsp.stat(filePath);
251
- backups.push({
252
- name: file,
253
- path: filePath,
254
- size: stats.size,
255
- createdAt: stats.mtime
256
- });
257
- } catch (err) {
258
- logger.warn(`Skipping invalid backup file ${file}: ${err.message}`);
259
- }
260
- }
261
- }
262
-
263
- if (backups.length === 0) {
264
- logger.warn('No valid backup files found in the backup directory.');
265
- return { success: true, backups: [] };
266
- }
267
-
268
- // Sort by creation time (newest first)
269
- backups.sort((a, b) => b.createdAt - a.createdAt);
270
-
271
- logger.info('\n📋 Available Backups');
272
- logger.info('='.repeat(50));
273
-
274
- backups.forEach((backup, index) => {
275
- const sizeKB = (backup.size / 1024).toFixed(2);
276
- const formattedDate = backup.createdAt.toLocaleString();
277
-
278
- logger.info(`🔹 ${index + 1}. ${backup.name}`);
279
- logger.info(` 📏 Size: ${sizeKB} KB`);
280
- logger.info(` 📅 Created: ${formattedDate}`);
281
- logger.info(` 📂 Path: ${backup.path}\n`);
282
- });
283
-
284
- logger.info(`Total backups: ${backups.length}`);
285
-
286
- return { success: true, backups, count: backups.length };
287
- } catch (err) {
288
- if (err.code === 'ENOENT') {
289
- logger.warn('No backups found.');
290
- return { success: true, backups: [] };
291
- } else {
292
- throw err;
293
- }
294
- }
295
- }
296
-
297
- async handleVerify(options = {}) {
298
- const backupFile = options._ && options._[1];
299
- if (!backupFile) {
300
- throw new Error('Backup file path is required');
301
- }
302
-
303
- const backupPath = this.validateInProject(path.resolve(process.cwd(), backupFile), 'backup file');
304
-
305
- // Validate backup file
306
- if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
307
- throw new Error(`Backup file not found: ${backupPath}`);
308
- }
309
-
310
- logger.info('\nVerifying backup...');
311
-
312
- try {
313
- const data = await fsp.readFile(backupPath, 'utf8');
314
- const content = JSON.parse(data);
315
-
316
- if (typeof content === 'object' && content !== null) {
317
- const fileCount = Object.keys(content).length;
318
- logger.success('Backup is valid');
319
- logger.info(` Contains ${fileCount} translation files`);
320
- logger.info(` Last modified: ${(await fsp.stat(backupPath)).mtime.toLocaleString()}`);
321
-
322
- return {
323
- success: true,
324
- command: 'verify',
325
- valid: true,
326
- fileCount
327
- };
328
- } else {
329
- throw new Error('Invalid backup format');
330
- }
331
- } catch (error) {
332
- logger.error('Backup verification failed!');
333
- logger.error(` Error: ${error.message}`);
334
- return {
335
- success: false,
336
- command: 'verify',
337
- valid: false,
338
- error: error.message
339
- };
340
- }
341
- }
342
-
343
- async handleCleanup(options = {}) {
344
- const keep = options.keep ? parseInt(options.keep, 10) : this.maxBackups;
345
- const backupDir = this.validateInProject(this.backupDir, 'backup directory');
346
-
347
- logger.info('\nCleaning up old backups...');
348
-
349
- try {
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()
357
- }))
358
- .sort((a, b) => b.time - a.time);
359
-
360
- // Keep only the most recent 'keep' files
361
- const toDelete = backupFiles.slice(keep);
362
-
363
- if (toDelete.length === 0) {
364
- logger.info('No old backups to delete.');
365
- return { success: true, deleted: 0 };
366
- }
367
-
368
- // Delete old backups
369
- for (const file of toDelete) {
370
- try {
371
- await fsp.unlink(file.path);
372
- logger.info(` - Deleted: ${file.name}`);
373
- } catch (err) {
374
- logger.error(` - Failed to delete ${file.name}: ${err.message}`);
375
- }
376
- }
377
-
378
- logger.info(`\nRemoved ${toDelete.length} old backups`);
379
- logger.info(`Total backups kept: ${keep}`);
380
-
381
- return {
382
- success: true,
383
- command: 'cleanup',
384
- deleted: toDelete.length,
385
- kept: keep
386
- };
387
- } catch (error) {
388
- logger.error('Error cleaning up backups:');
389
- logger.error(` ${error.message}`);
390
- logger.debug(error.stack || error.message);
391
- throw error;
392
- }
393
- }
394
-
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);
407
-
408
- // Keep only the most recent files
409
- const toDelete = backupFiles.slice(this.maxBackups);
410
-
411
- for (const file of toDelete) {
412
- try {
413
- await fsp.unlink(file.path);
414
- } catch (err) {
415
- // Ignore cleanup errors
416
- }
417
- }
418
- } catch (error) {
419
- // Ignore cleanup errors
420
- }
421
- }
422
-
423
- handleError(error) {
424
- logger.error('Backup operation failed:');
425
- logger.error(` ${error.message}`);
426
- logger.debug(error.stack || error.message);
23
+ async run(options = {}) {
24
+ const command = options.command || (options._ && options._[0]);
25
+ if (!command || command === 'help') {
26
+ this.showHelp();
27
+ return { success: false, command: 'backup', disabled: true };
427
28
  }
29
+
30
+ logger.warn(`Backup command '${command}' is disabled in this build.`);
31
+ logger.info('Use project-level source control or external backup tooling instead.');
32
+
33
+ return {
34
+ success: false,
35
+ command,
36
+ disabled: true,
37
+ error: 'Backup command is disabled'
38
+ };
39
+ }
428
40
  }
429
-
41
+
430
42
  module.exports = I18nBackup;
@@ -1,62 +1,62 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * I18NTK BACKUP COMMAND
5
- *
6
- * Handles backup operations for translation files and settings.
7
- */
8
-
9
- const I18nBackup = require('../../i18ntk-backup-class');
10
-
11
- class BackupCommand {
12
- constructor(config = {}, ui = null) {
13
- this.config = config;
14
- this.ui = ui;
15
- this.prompt = null;
16
- this.isNonInteractiveMode = false;
17
- this.safeClose = null;
18
- }
19
-
20
- /**
21
- * Set runtime dependencies for interactive operations
22
- */
23
- setRuntimeDependencies(prompt, isNonInteractiveMode, safeClose) {
24
- this.prompt = prompt;
25
- this.isNonInteractiveMode = isNonInteractiveMode;
26
- this.safeClose = safeClose;
27
- }
28
-
29
- /**
30
- * Execute the backup command
31
- */
32
- async execute(options = {}) {
33
- try {
34
- const backup = new I18nBackup();
35
- await backup.run(options);
36
- return { success: true, command: 'backup' };
37
- } catch (error) {
38
- console.error(`Backup command failed: ${error.message}`);
39
- throw error;
40
- }
41
- }
42
-
43
- /**
44
- * Get command metadata
45
- */
46
- getMetadata() {
47
- return {
48
- name: 'backup',
49
- description: 'Create backups of translation files and settings',
50
- category: 'maintenance',
51
- aliases: [],
52
- usage: 'backup [options]',
53
- examples: [
54
- 'backup',
55
- 'backup --output-dir=./backups',
56
- 'backup --include-settings'
57
- ]
58
- };
59
- }
60
- }
61
-
62
- module.exports = BackupCommand;
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * I18NTK BACKUP COMMAND
5
+ *
6
+ * Handles backup operations for translation files and settings.
7
+ */
8
+
9
+ const I18nBackup = require('../../i18ntk-backup-class');
10
+
11
+ class BackupCommand {
12
+ constructor(config = {}, ui = null) {
13
+ this.config = config;
14
+ this.ui = ui;
15
+ this.prompt = null;
16
+ this.isNonInteractiveMode = false;
17
+ this.safeClose = null;
18
+ }
19
+
20
+ /**
21
+ * Set runtime dependencies for interactive operations
22
+ */
23
+ setRuntimeDependencies(prompt, isNonInteractiveMode, safeClose) {
24
+ this.prompt = prompt;
25
+ this.isNonInteractiveMode = isNonInteractiveMode;
26
+ this.safeClose = safeClose;
27
+ }
28
+
29
+ /**
30
+ * Execute the backup command
31
+ */
32
+ async execute(options = {}) {
33
+ try {
34
+ const backup = new I18nBackup();
35
+ const result = await backup.run(options);
36
+ return result || { success: false, command: 'backup', error: 'Backup command failed' };
37
+ } catch (error) {
38
+ console.error(`Backup command failed: ${error.message}`);
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Get command metadata
45
+ */
46
+ getMetadata() {
47
+ return {
48
+ name: 'backup',
49
+ description: 'Create backups of translation files and settings',
50
+ category: 'maintenance',
51
+ aliases: [],
52
+ usage: 'backup [options]',
53
+ examples: [
54
+ 'backup',
55
+ 'backup --output-dir=./backups',
56
+ 'backup --include-settings'
57
+ ]
58
+ };
59
+ }
60
+ }
61
+
62
+ module.exports = BackupCommand;