i18ntk 1.10.2 → 2.0.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/LICENSE +1 -1
- package/README.md +141 -1191
- package/main/i18ntk-analyze.js +65 -84
- package/main/i18ntk-backup-class.js +420 -0
- package/main/i18ntk-backup.js +3 -3
- package/main/i18ntk-complete.js +90 -65
- package/main/i18ntk-doctor.js +123 -103
- package/main/i18ntk-fixer.js +61 -725
- package/main/i18ntk-go.js +14 -15
- package/main/i18ntk-init.js +77 -26
- package/main/i18ntk-java.js +27 -32
- package/main/i18ntk-js.js +70 -68
- package/main/i18ntk-manage.js +129 -30
- package/main/i18ntk-php.js +75 -75
- package/main/i18ntk-py.js +55 -56
- package/main/i18ntk-scanner.js +59 -57
- package/main/i18ntk-setup.js +9 -404
- package/main/i18ntk-sizing.js +6 -6
- package/main/i18ntk-summary.js +21 -18
- package/main/i18ntk-ui.js +11 -10
- package/main/i18ntk-usage.js +54 -18
- package/main/i18ntk-validate.js +13 -13
- package/main/manage/commands/AnalyzeCommand.js +1124 -0
- package/main/manage/commands/BackupCommand.js +62 -0
- package/main/manage/commands/CommandRouter.js +295 -0
- package/main/manage/commands/CompleteCommand.js +61 -0
- package/main/manage/commands/DoctorCommand.js +60 -0
- package/main/manage/commands/FixerCommand.js +624 -0
- package/main/manage/commands/InitCommand.js +62 -0
- package/main/manage/commands/ScannerCommand.js +654 -0
- package/main/manage/commands/SizingCommand.js +60 -0
- package/main/manage/commands/SummaryCommand.js +61 -0
- package/main/manage/commands/UsageCommand.js +60 -0
- package/main/manage/commands/ValidateCommand.js +978 -0
- package/main/manage/index-fixed.js +1447 -0
- package/main/manage/index.js +1462 -0
- package/main/manage/managers/DebugMenu.js +140 -0
- package/main/manage/managers/InteractiveMenu.js +177 -0
- package/main/manage/managers/LanguageMenu.js +62 -0
- package/main/manage/managers/SettingsMenu.js +53 -0
- package/main/manage/services/AuthenticationService.js +263 -0
- package/main/manage/services/ConfigurationService-fixed.js +449 -0
- package/main/manage/services/ConfigurationService.js +449 -0
- package/main/manage/services/FileManagementService.js +368 -0
- package/main/manage/services/FrameworkDetectionService.js +458 -0
- package/main/manage/services/InitService.js +1051 -0
- package/main/manage/services/SetupService.js +462 -0
- package/main/manage/services/SummaryService.js +450 -0
- package/main/manage/services/UsageService.js +1502 -0
- package/package.json +32 -29
- package/runtime/enhanced.d.ts +221 -221
- package/runtime/index.d.ts +29 -29
- package/runtime/index.full.d.ts +331 -331
- package/runtime/index.js +7 -6
- package/scripts/build-lite.js +17 -17
- package/scripts/deprecate-versions.js +23 -6
- package/scripts/export-translations.js +5 -5
- package/scripts/fix-all-i18n.js +3 -3
- package/scripts/fix-and-purify-i18n.js +3 -2
- package/scripts/fix-locale-control-chars.js +110 -0
- package/scripts/lint-locales.js +80 -0
- package/scripts/locale-optimizer.js +8 -8
- package/scripts/prepublish.js +21 -21
- package/scripts/security-check.js +117 -117
- package/scripts/sync-translations.js +4 -4
- package/scripts/sync-ui-locales.js +9 -8
- package/scripts/validate-all-translations.js +8 -7
- package/scripts/verify-deprecations.js +157 -161
- package/scripts/verify-translations.js +6 -5
- package/settings/i18ntk-config.json +282 -282
- package/settings/language-config.json +5 -5
- package/settings/settings-cli.js +9 -9
- package/settings/settings-manager.js +18 -18
- package/ui-locales/de.json +2417 -2348
- package/ui-locales/en.json +2415 -2352
- package/ui-locales/es.json +2425 -2353
- package/ui-locales/fr.json +2418 -2348
- package/ui-locales/ja.json +2463 -2361
- package/ui-locales/ru.json +2463 -2359
- package/ui-locales/zh.json +2418 -2351
- package/utils/admin-auth.js +2 -2
- package/utils/admin-cli.js +297 -297
- package/utils/admin-pin.js +9 -9
- package/utils/cli-helper.js +9 -9
- package/utils/config-helper.js +73 -104
- package/utils/config-manager.js +204 -171
- package/utils/config.js +5 -4
- package/utils/env-manager.js +249 -263
- package/utils/framework-detector.js +27 -24
- package/utils/i18n-helper.js +85 -41
- package/utils/init-helper.js +152 -94
- package/utils/json-output.js +98 -98
- package/utils/mini-commander.js +179 -0
- package/utils/missing-key-validator.js +5 -5
- package/utils/plugin-loader.js +40 -29
- package/utils/prompt.js +14 -44
- package/utils/safe-json.js +40 -0
- package/utils/secure-errors.js +3 -3
- package/utils/security-check-improved.js +390 -0
- package/utils/security-config.js +5 -5
- package/utils/security-fixed.js +607 -0
- package/utils/security.js +652 -602
- package/utils/setup-enforcer.js +136 -44
- package/utils/setup-validator.js +33 -32
- package/utils/ultra-performance-optimizer.js +11 -9
- package/utils/watch-locales.js +2 -1
- package/utils/prompt-fixed.js +0 -55
- package/utils/security-check.js +0 -454
package/utils/json-output.js
CHANGED
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JSON Output Utility for i18ntk commands
|
|
3
|
-
* Provides consistent machine-readable output format for CI/CD integration
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class JsonOutput {
|
|
7
|
-
constructor(command) {
|
|
8
|
-
this.command = command;
|
|
9
|
-
this.version = this.getPackageVersion();
|
|
10
|
-
this.startTime = Date.now();
|
|
11
|
-
this.data = {
|
|
12
|
-
command: this.command,
|
|
13
|
-
version: this.version,
|
|
14
|
-
status: 'ok',
|
|
15
|
-
stats: {},
|
|
16
|
-
issues: [],
|
|
17
|
-
metadata: {
|
|
18
|
-
timestamp: new Date().toISOString(),
|
|
19
|
-
duration: 0
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
getPackageVersion() {
|
|
25
|
-
try {
|
|
26
|
-
const packageJson = require('../package.json');
|
|
27
|
-
return packageJson.version;
|
|
28
|
-
} catch (error) {
|
|
29
|
-
return '1.8.3';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Set the overall status
|
|
35
|
-
* @param {'ok'|'warn'|'error'} status
|
|
36
|
-
*/
|
|
37
|
-
setStatus(status) {
|
|
38
|
-
this.data.status = status;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Add statistics to the output
|
|
43
|
-
* @param {Object} stats
|
|
44
|
-
*/
|
|
45
|
-
setStats(stats) {
|
|
46
|
-
this.data.stats = { ...this.data.stats, ...stats };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Add an issue to the output
|
|
51
|
-
* @param {Object} issue
|
|
52
|
-
*/
|
|
53
|
-
addIssue(issue) {
|
|
54
|
-
this.data.issues.push({
|
|
55
|
-
file: issue.file || '',
|
|
56
|
-
key: issue.key || '',
|
|
57
|
-
type: issue.type || 'unknown',
|
|
58
|
-
message: issue.message || '',
|
|
59
|
-
severity: issue.severity || 'info'
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Add metadata information
|
|
65
|
-
* @param {Object} metadata
|
|
66
|
-
*/
|
|
67
|
-
addMetadata(metadata) {
|
|
68
|
-
this.data.metadata = { ...this.data.metadata, ...metadata };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Finalize and output the JSON
|
|
73
|
-
*/
|
|
74
|
-
output() {
|
|
75
|
-
this.data.metadata.duration = Date.now() - this.startTime;
|
|
76
|
-
|
|
77
|
-
if (process.env.NODE_ENV !== 'test') {
|
|
78
|
-
console.log(JSON.stringify(this.data, null, 2));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return this.data;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Output error in JSON format
|
|
86
|
-
* @param {Error} error
|
|
87
|
-
*/
|
|
88
|
-
outputError(error) {
|
|
89
|
-
this.setStatus('error');
|
|
90
|
-
this.addIssue({
|
|
91
|
-
type: 'error',
|
|
92
|
-
message: error.message,
|
|
93
|
-
severity: 'error'
|
|
94
|
-
});
|
|
95
|
-
this.output();
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
1
|
+
/**
|
|
2
|
+
* JSON Output Utility for i18ntk commands
|
|
3
|
+
* Provides consistent machine-readable output format for CI/CD integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class JsonOutput {
|
|
7
|
+
constructor(command) {
|
|
8
|
+
this.command = command;
|
|
9
|
+
this.version = this.getPackageVersion();
|
|
10
|
+
this.startTime = Date.now();
|
|
11
|
+
this.data = {
|
|
12
|
+
command: this.command,
|
|
13
|
+
version: this.version,
|
|
14
|
+
status: 'ok',
|
|
15
|
+
stats: {},
|
|
16
|
+
issues: [],
|
|
17
|
+
metadata: {
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
duration: 0
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getPackageVersion() {
|
|
25
|
+
try {
|
|
26
|
+
const packageJson = require('../package.json');
|
|
27
|
+
return packageJson.version;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return '1.8.3';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Set the overall status
|
|
35
|
+
* @param {'ok'|'warn'|'error'} status
|
|
36
|
+
*/
|
|
37
|
+
setStatus(status) {
|
|
38
|
+
this.data.status = status;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add statistics to the output
|
|
43
|
+
* @param {Object} stats
|
|
44
|
+
*/
|
|
45
|
+
setStats(stats) {
|
|
46
|
+
this.data.stats = { ...this.data.stats, ...stats };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Add an issue to the output
|
|
51
|
+
* @param {Object} issue
|
|
52
|
+
*/
|
|
53
|
+
addIssue(issue) {
|
|
54
|
+
this.data.issues.push({
|
|
55
|
+
file: issue.file || '',
|
|
56
|
+
key: issue.key || '',
|
|
57
|
+
type: issue.type || 'unknown',
|
|
58
|
+
message: issue.message || '',
|
|
59
|
+
severity: issue.severity || 'info'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add metadata information
|
|
65
|
+
* @param {Object} metadata
|
|
66
|
+
*/
|
|
67
|
+
addMetadata(metadata) {
|
|
68
|
+
this.data.metadata = { ...this.data.metadata, ...metadata };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Finalize and output the JSON
|
|
73
|
+
*/
|
|
74
|
+
output() {
|
|
75
|
+
this.data.metadata.duration = Date.now() - this.startTime;
|
|
76
|
+
|
|
77
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
78
|
+
console.log(JSON.stringify(this.data, null, 2));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return this.data;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Output error in JSON format
|
|
86
|
+
* @param {Error} error
|
|
87
|
+
*/
|
|
88
|
+
outputError(error) {
|
|
89
|
+
this.setStatus('error');
|
|
90
|
+
this.addIssue({
|
|
91
|
+
type: 'error',
|
|
92
|
+
message: error.message,
|
|
93
|
+
severity: 'error'
|
|
94
|
+
});
|
|
95
|
+
this.output();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
99
|
module.exports = JsonOutput;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal zero-dependency subset of commander used by i18ntk language CLIs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function toCamelCase(input) {
|
|
6
|
+
return String(input || '').replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseOptionDefinition(flags, defaultValue) {
|
|
10
|
+
const longMatch = flags.match(/--([a-zA-Z0-9-]+)/);
|
|
11
|
+
const shortMatch = flags.match(/(^|\s)-([a-zA-Z])(\s|,|$)/);
|
|
12
|
+
const hasValue = /<[^>]+>/.test(flags);
|
|
13
|
+
const longName = longMatch ? longMatch[1] : null;
|
|
14
|
+
const shortName = shortMatch ? shortMatch[2] : null;
|
|
15
|
+
const name = toCamelCase(longName || shortName || '');
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
flags,
|
|
19
|
+
hasValue,
|
|
20
|
+
defaultValue,
|
|
21
|
+
longFlag: longName ? `--${longName}` : null,
|
|
22
|
+
shortFlag: shortName ? `-${shortName}` : null,
|
|
23
|
+
name
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseOptions(args, optionDefs) {
|
|
28
|
+
const options = {};
|
|
29
|
+
|
|
30
|
+
for (const def of optionDefs) {
|
|
31
|
+
if (def.defaultValue !== undefined) {
|
|
32
|
+
options[def.name] = def.defaultValue;
|
|
33
|
+
} else if (!def.hasValue) {
|
|
34
|
+
options[def.name] = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < args.length; i++) {
|
|
39
|
+
const arg = args[i];
|
|
40
|
+
if (!arg || !arg.startsWith('-')) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let matchedDef = null;
|
|
45
|
+
let inlineValue;
|
|
46
|
+
|
|
47
|
+
if (arg.startsWith('--')) {
|
|
48
|
+
const [flag, value] = arg.split('=', 2);
|
|
49
|
+
matchedDef = optionDefs.find(def => def.longFlag === flag);
|
|
50
|
+
inlineValue = value;
|
|
51
|
+
} else {
|
|
52
|
+
matchedDef = optionDefs.find(def => def.shortFlag === arg);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!matchedDef) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!matchedDef.hasValue) {
|
|
60
|
+
options[matchedDef.name] = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let value = inlineValue;
|
|
65
|
+
if (value === undefined) {
|
|
66
|
+
const next = args[i + 1];
|
|
67
|
+
if (next !== undefined && !String(next).startsWith('-')) {
|
|
68
|
+
value = next;
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
options[matchedDef.name] = value !== undefined ? value : '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return options;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class MiniCommand {
|
|
80
|
+
constructor(name) {
|
|
81
|
+
this._name = name;
|
|
82
|
+
this._description = '';
|
|
83
|
+
this._options = [];
|
|
84
|
+
this._action = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
description(text) {
|
|
88
|
+
this._description = text;
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
option(flags, _description, defaultValue) {
|
|
93
|
+
this._options.push(parseOptionDefinition(flags, defaultValue));
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
action(handler) {
|
|
98
|
+
this._action = handler;
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
execute(args) {
|
|
103
|
+
const parsed = parseOptions(args, this._options);
|
|
104
|
+
if (typeof this._action === 'function') {
|
|
105
|
+
const result = this._action(parsed);
|
|
106
|
+
if (result && typeof result.then === 'function') {
|
|
107
|
+
result.catch(error => {
|
|
108
|
+
console.error(error && error.message ? error.message : String(error));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class MiniProgram {
|
|
117
|
+
constructor() {
|
|
118
|
+
this._name = '';
|
|
119
|
+
this._description = '';
|
|
120
|
+
this._version = '';
|
|
121
|
+
this._options = [];
|
|
122
|
+
this._commands = [];
|
|
123
|
+
this._opts = {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
name(value) {
|
|
127
|
+
this._name = value;
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
description(value) {
|
|
132
|
+
this._description = value;
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
version(value) {
|
|
137
|
+
this._version = value;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
option(flags, _description, defaultValue) {
|
|
142
|
+
this._options.push(parseOptionDefinition(flags, defaultValue));
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
command(name) {
|
|
147
|
+
const command = new MiniCommand(name);
|
|
148
|
+
this._commands.push(command);
|
|
149
|
+
return command;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
opts() {
|
|
153
|
+
return { ...this._opts };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
parse(argv = process.argv) {
|
|
157
|
+
const args = argv.slice(2);
|
|
158
|
+
|
|
159
|
+
if (this._commands.length > 0 && args.length > 0 && !args[0].startsWith('-')) {
|
|
160
|
+
const command = this._commands.find(item => item._name === args[0]);
|
|
161
|
+
if (command) {
|
|
162
|
+
command.execute(args.slice(1));
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this._opts = parseOptions(args, this._options);
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createProgram() {
|
|
173
|
+
return new MiniProgram();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
createProgram,
|
|
178
|
+
program: createProgram()
|
|
179
|
+
};
|
|
@@ -775,14 +775,14 @@ class MissingKeyValidator {
|
|
|
775
775
|
}
|
|
776
776
|
|
|
777
777
|
async validateLanguageKeys(language) {
|
|
778
|
-
const localePath = path.join(this.projectRoot, 'ui-locales', `${language}.json`);
|
|
778
|
+
const localePath = path.join(this.projectRoot, 'resources', 'i18n', 'ui-locales', `${language}.json`);
|
|
779
779
|
|
|
780
|
-
if (!
|
|
780
|
+
if (!SecurityUtils.safeExistsSync(localePath)) {
|
|
781
781
|
return this.requiredKeys; // All keys missing if file doesn't exist
|
|
782
782
|
}
|
|
783
783
|
|
|
784
784
|
try {
|
|
785
|
-
const localeData = JSON.parse(
|
|
785
|
+
const localeData = JSON.parse(SecurityUtils.safeReadFileSync(localePath, 'utf8'));
|
|
786
786
|
const existingKeys = Object.keys(localeData);
|
|
787
787
|
|
|
788
788
|
return this.requiredKeys.filter(key => !existingKeys.includes(key));
|
|
@@ -807,7 +807,7 @@ class MissingKeyValidator {
|
|
|
807
807
|
};
|
|
808
808
|
|
|
809
809
|
const reportPath = path.join(__dirname, 'missing-keys-report.json');
|
|
810
|
-
|
|
810
|
+
SecurityUtils.safeWriteFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
811
811
|
|
|
812
812
|
console.log('\n📊 Missing Keys Report:');
|
|
813
813
|
console.log(`Total Languages: ${report.summary.totalLanguages}`);
|
|
@@ -837,7 +837,7 @@ class MissingKeyValidator {
|
|
|
837
837
|
});
|
|
838
838
|
|
|
839
839
|
const templatePath = path.join(__dirname, 'translation-template.json');
|
|
840
|
-
|
|
840
|
+
SecurityUtils.safeWriteFileSync(templatePath, JSON.stringify(template, null, 2));
|
|
841
841
|
|
|
842
842
|
console.log(`Translation template saved to: ${templatePath}`);
|
|
843
843
|
return template;
|
package/utils/plugin-loader.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
function loadOptionalModule(name) {
|
|
2
|
+
// Security hardening: allow only known built-in optional plugins.
|
|
3
|
+
// This avoids dynamic runtime require of arbitrary package names.
|
|
4
|
+
const builtinPlugins = {
|
|
5
|
+
regex: () => require('./extractors/regex'),
|
|
6
|
+
'i18ntk-extractor-regex': () => require('./extractors/regex')
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const direct = builtinPlugins[name];
|
|
10
|
+
if (direct) {
|
|
11
|
+
try {
|
|
12
|
+
return direct();
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
2
17
|
|
|
3
|
-
function loadOptionalModule(name, cwd = process.cwd()) {
|
|
4
18
|
// Sanitize the module name to prevent path traversal
|
|
5
19
|
const sanitizedName = name.replace(/[^a-zA-Z0-9@/_-]/g, '');
|
|
6
20
|
if (sanitizedName !== name) {
|
|
@@ -8,31 +22,28 @@ function loadOptionalModule(name, cwd = process.cwd()) {
|
|
|
8
22
|
return null;
|
|
9
23
|
}
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} catch {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
25
|
+
// Unknown optional modules are disabled by default.
|
|
26
|
+
// Register plugins programmatically through PluginLoader.registerPlugin instead.
|
|
27
|
+
return null;
|
|
17
28
|
}
|
|
18
|
-
class PluginLoader {
|
|
19
|
-
constructor() {
|
|
20
|
-
this.plugins = {};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
registerPlugin(plugin) {
|
|
24
|
-
if (!plugin || !plugin.type) return;
|
|
25
|
-
const type = plugin.type;
|
|
26
|
-
if (!this.plugins[type]) {
|
|
27
|
-
this.plugins[type] = [];
|
|
28
|
-
}
|
|
29
|
-
this.plugins[type].push(plugin);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
getPlugins(type) {
|
|
33
|
-
return this.plugins[type] || [];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
module.exports = PluginLoader;
|
|
38
|
-
module.exports.loadOptionalModule = loadOptionalModule;
|
|
29
|
+
class PluginLoader {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.plugins = {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
registerPlugin(plugin) {
|
|
35
|
+
if (!plugin || !plugin.type) return;
|
|
36
|
+
const type = plugin.type;
|
|
37
|
+
if (!this.plugins[type]) {
|
|
38
|
+
this.plugins[type] = [];
|
|
39
|
+
}
|
|
40
|
+
this.plugins[type].push(plugin);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getPlugins(type) {
|
|
44
|
+
return this.plugins[type] || [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = PluginLoader;
|
|
49
|
+
module.exports.loadOptionalModule = loadOptionalModule;
|
package/utils/prompt.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
const readline = require('
|
|
2
|
-
const {
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const { logger } = require('./logger');
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Simple prompt utility for CLI interactions
|
|
6
|
-
*/
|
|
7
4
|
class Prompt {
|
|
8
5
|
constructor() {
|
|
9
6
|
this.rl = readline.createInterface({
|
|
@@ -26,60 +23,33 @@ class Prompt {
|
|
|
26
23
|
|
|
27
24
|
async confirm(questionText, defaultValue = false) {
|
|
28
25
|
const answer = await this.question(`${questionText} (${defaultValue ? 'Y/n' : 'y/N'}) `);
|
|
29
|
-
const answer = await this.question(`${message} (${defaultValue ? 'Y/n' : 'y/N'}): `);
|
|
30
|
-
return answer ? answer.toLowerCase().startsWith('y') : defaultValue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async select(message, choices, defaultIndex = 0) {
|
|
34
|
-
logger.log(`\n${message}:`);
|
|
35
|
-
async confirm(question, defaultValue = false) {
|
|
36
|
-
const answer = await this.question(`${question} (${defaultValue ? 'Y/n' : 'y/N'}) `);
|
|
37
26
|
if (answer === '') return defaultValue;
|
|
38
27
|
return /^y|yes$/i.test(answer);
|
|
39
28
|
}
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
* @param {string} question - The question to ask
|
|
44
|
-
* @param {Array<string>} choices - Array of choices
|
|
45
|
-
* @returns {Promise<string>} The selected choice
|
|
46
|
-
*/
|
|
47
|
-
async list(question, choices) {
|
|
48
|
-
if (!choices || !choices.length) {
|
|
49
|
-
throw new Error('No choices provided');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
console.log(question);
|
|
30
|
+
async select(questionText, choices, defaultIndex = 0) {
|
|
31
|
+
logger.log(`\n${questionText}:`);
|
|
53
32
|
choices.forEach((choice, index) => {
|
|
54
|
-
|
|
33
|
+
logger.log(` ${index + 1}. ${choice}`);
|
|
55
34
|
});
|
|
56
35
|
|
|
57
36
|
while (true) {
|
|
58
|
-
const answer = await this.question(
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return choices[index];
|
|
37
|
+
const answer = await this.question(`\nSelect an option (1-${choices.length}): `);
|
|
38
|
+
const selected = parseInt(answer, 10) - 1;
|
|
39
|
+
if (!isNaN(selected) && selected >= 0 && selected < choices.length) {
|
|
40
|
+
return selected;
|
|
63
41
|
}
|
|
64
|
-
|
|
65
|
-
console.log('Invalid selection. Please try again.');
|
|
42
|
+
logger.log('Invalid selection. Please try again.');
|
|
66
43
|
}
|
|
67
44
|
}
|
|
68
45
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
close() {
|
|
73
|
-
this.rl.close();
|
|
46
|
+
async input(questionText, defaultValue = '') {
|
|
47
|
+
const answer = await this.question(`${questionText}${defaultValue ? ` [${defaultValue}]` : ''}: `);
|
|
48
|
+
return answer || defaultValue;
|
|
74
49
|
}
|
|
75
50
|
}
|
|
76
51
|
|
|
77
|
-
// Create a singleton instance
|
|
78
52
|
const prompt = new Prompt();
|
|
79
|
-
|
|
80
|
-
// Handle process termination
|
|
81
|
-
process.on('exit', () => {
|
|
82
|
-
prompt.close();
|
|
83
|
-
});
|
|
53
|
+
process.on('exit', () => prompt.close());
|
|
84
54
|
|
|
85
55
|
module.exports = prompt;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// utils/safe-json.js
|
|
2
|
+
const { readFile } = require('fs/promises');
|
|
3
|
+
|
|
4
|
+
function stripBOM(s) {
|
|
5
|
+
if (typeof s === 'string' && s.charCodeAt(0) === 0xFEFF) return s.slice(1);
|
|
6
|
+
return s;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Safe JSON load with guardrails.
|
|
11
|
+
* - Max file size (default 1MB)
|
|
12
|
+
* - BOM stripping
|
|
13
|
+
* - Single, typed error (no loops)
|
|
14
|
+
*/
|
|
15
|
+
async function readJsonSafe(filePath, { maxBytes = 1_000_000 } = {}) {
|
|
16
|
+
const buf = await readFile(filePath);
|
|
17
|
+
if (buf.length === 0) {
|
|
18
|
+
const err = new Error('Empty JSON file');
|
|
19
|
+
err.code = 'EJSONEMPTY';
|
|
20
|
+
err.path = filePath;
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
if (buf.length > maxBytes) {
|
|
24
|
+
const err = new Error(`JSON too large (${buf.length} bytes)`);
|
|
25
|
+
err.code = 'EJSONTOOBIG';
|
|
26
|
+
err.path = filePath;
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(stripBOM(buf.toString('utf8')));
|
|
31
|
+
} catch (e) {
|
|
32
|
+
const err = new Error(`Invalid JSON`);
|
|
33
|
+
err.code = 'EJSONPARSE';
|
|
34
|
+
err.path = filePath;
|
|
35
|
+
err.cause = e;
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { readJsonSafe };
|
package/utils/secure-errors.js
CHANGED
|
@@ -76,7 +76,7 @@ function secureErrorHandler(options = {}) {
|
|
|
76
76
|
const config = { ...defaults, ...options };
|
|
77
77
|
|
|
78
78
|
// Ensure log directory exists
|
|
79
|
-
if (config.logFilePath && !
|
|
79
|
+
if (config.logFilePath && !SecurityUtils.safeExistsSync(path.dirname(config.logFilePath))) {
|
|
80
80
|
try {
|
|
81
81
|
fs.mkdirSync(path.dirname(config.logFilePath), { recursive: true });
|
|
82
82
|
} catch (e) {
|
|
@@ -126,12 +126,12 @@ function secureErrorHandler(options = {}) {
|
|
|
126
126
|
|
|
127
127
|
// Log to console
|
|
128
128
|
if (typeof config.logFunction === 'function') {
|
|
129
|
-
config.
|
|
129
|
+
SecurityUtils.safeWriteFileSync(config.logFilePath, JSON.stringify(logEntry, null, 2));
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Log to file if configured
|
|
133
133
|
if (config.logFilePath) {
|
|
134
|
-
|
|
134
|
+
SecurityUtils.safeWriteFileSync(
|
|
135
135
|
config.logFilePath,
|
|
136
136
|
JSON.stringify(logEntry) + '\n',
|
|
137
137
|
'utf8'
|