agileflow 2.89.3 → 2.90.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/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/lib/placeholder-registry.js +617 -0
- package/lib/smart-json-file.js +205 -1
- package/lib/table-formatter.js +504 -0
- package/lib/transient-status.js +374 -0
- package/lib/ui-manager.js +612 -0
- package/lib/validate-args.js +213 -0
- package/lib/validate-names.js +143 -0
- package/lib/validate-paths.js +434 -0
- package/lib/validate.js +37 -737
- package/package.json +4 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +176 -0
- package/scripts/tui/index.js +75 -0
- package/scripts/tui/lib/crashRecovery.js +302 -0
- package/scripts/tui/lib/eventStream.js +316 -0
- package/scripts/tui/lib/keyboard.js +252 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +278 -0
- package/scripts/tui/panels/SessionPanel.js +178 -0
- package/scripts/tui/panels/TracePanel.js +333 -0
- package/src/core/commands/tui.md +91 -0
- package/tools/cli/commands/config.js +7 -30
- package/tools/cli/commands/doctor.js +18 -38
- package/tools/cli/commands/list.js +47 -35
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +9 -38
- package/tools/cli/installers/core/installer.js +13 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +16 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgileFlow CLI - Self-Update Module
|
|
3
|
+
*
|
|
4
|
+
* Provides self-update capability for the CLI to ensure users
|
|
5
|
+
* always run the latest version.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { checkSelfUpdate, performSelfUpdate } = require('./lib/self-update');
|
|
9
|
+
* await checkSelfUpdate(options, 'update');
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { spawnSync } = require('node:child_process');
|
|
14
|
+
const semver = require('semver');
|
|
15
|
+
const chalk = require('chalk');
|
|
16
|
+
const { getLatestVersion } = require('./npm-utils');
|
|
17
|
+
const { info } = require('./ui');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the local CLI version from package.json
|
|
21
|
+
* @returns {string} Local CLI version
|
|
22
|
+
*/
|
|
23
|
+
function getLocalVersion() {
|
|
24
|
+
const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
|
|
25
|
+
return packageJson.version;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a self-update is needed
|
|
30
|
+
* @param {Object} options - Command options
|
|
31
|
+
* @param {boolean} [options.selfUpdate=true] - Whether self-update is enabled
|
|
32
|
+
* @param {boolean} [options.selfUpdated=false] - Whether already self-updated
|
|
33
|
+
* @returns {Promise<{needed: boolean, localVersion: string, latestVersion: string|null}>}
|
|
34
|
+
*/
|
|
35
|
+
async function checkSelfUpdate(options = {}) {
|
|
36
|
+
const shouldCheck = options.selfUpdate !== false && !options.selfUpdated;
|
|
37
|
+
|
|
38
|
+
if (!shouldCheck) {
|
|
39
|
+
return {
|
|
40
|
+
needed: false,
|
|
41
|
+
localVersion: getLocalVersion(),
|
|
42
|
+
latestVersion: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const localVersion = getLocalVersion();
|
|
47
|
+
const latestVersion = await getLatestVersion('agileflow');
|
|
48
|
+
|
|
49
|
+
if (!latestVersion) {
|
|
50
|
+
return {
|
|
51
|
+
needed: false,
|
|
52
|
+
localVersion,
|
|
53
|
+
latestVersion: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const needed = semver.lt(localVersion, latestVersion);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
needed,
|
|
61
|
+
localVersion,
|
|
62
|
+
latestVersion,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Perform self-update by re-running with latest CLI version
|
|
68
|
+
* @param {string} command - Command name to re-run
|
|
69
|
+
* @param {Object} options - Command options
|
|
70
|
+
* @param {Object} versionInfo - Version info from checkSelfUpdate
|
|
71
|
+
* @returns {number} Exit code from spawned process
|
|
72
|
+
*/
|
|
73
|
+
function performSelfUpdate(command, options, versionInfo) {
|
|
74
|
+
const { localVersion, latestVersion } = versionInfo;
|
|
75
|
+
|
|
76
|
+
// Display update notice
|
|
77
|
+
console.log(chalk.hex('#e8683a').bold('\n AgileFlow CLI Update\n'));
|
|
78
|
+
info(`Updating CLI from v${localVersion} to v${latestVersion}...`);
|
|
79
|
+
console.log(chalk.dim(' Fetching latest version from npm...\n'));
|
|
80
|
+
|
|
81
|
+
// Build the command with all current options forwarded
|
|
82
|
+
const args = ['agileflow@latest', command, '--self-updated'];
|
|
83
|
+
|
|
84
|
+
// Forward common options
|
|
85
|
+
if (options.directory) args.push('-d', options.directory);
|
|
86
|
+
if (options.force) args.push('--force');
|
|
87
|
+
|
|
88
|
+
// Forward command-specific options
|
|
89
|
+
if (command === 'update' && options.ides) {
|
|
90
|
+
args.push('--ides', options.ides);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = spawnSync('npx', args, {
|
|
94
|
+
stdio: 'inherit',
|
|
95
|
+
cwd: process.cwd(),
|
|
96
|
+
shell: process.platform === 'win32',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return result.status ?? 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check and perform self-update if needed
|
|
104
|
+
* Returns true if the caller should exit (because update was performed)
|
|
105
|
+
* @param {string} command - Command name
|
|
106
|
+
* @param {Object} options - Command options
|
|
107
|
+
* @returns {Promise<boolean>} True if caller should exit
|
|
108
|
+
*/
|
|
109
|
+
async function handleSelfUpdate(command, options) {
|
|
110
|
+
const versionInfo = await checkSelfUpdate(options);
|
|
111
|
+
|
|
112
|
+
if (versionInfo.needed) {
|
|
113
|
+
const exitCode = performSelfUpdate(command, options, versionInfo);
|
|
114
|
+
process.exit(exitCode);
|
|
115
|
+
return true; // Never reached, but for type safety
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Middleware for self-update check
|
|
123
|
+
* @param {string} command - Command name
|
|
124
|
+
* @returns {Function} Middleware function
|
|
125
|
+
*/
|
|
126
|
+
function selfUpdateMiddleware(command) {
|
|
127
|
+
return async (ctx, next) => {
|
|
128
|
+
const versionInfo = await checkSelfUpdate(ctx.options);
|
|
129
|
+
|
|
130
|
+
if (versionInfo.needed) {
|
|
131
|
+
const exitCode = performSelfUpdate(command, ctx.options, versionInfo);
|
|
132
|
+
process.exit(exitCode);
|
|
133
|
+
return; // Never reached
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Store version info in context for later use
|
|
137
|
+
ctx.versionInfo = versionInfo;
|
|
138
|
+
await next();
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
getLocalVersion,
|
|
144
|
+
checkSelfUpdate,
|
|
145
|
+
performSelfUpdate,
|
|
146
|
+
handleSelfUpdate,
|
|
147
|
+
selfUpdateMiddleware,
|
|
148
|
+
};
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgileFlow CLI - Validation Middleware
|
|
3
|
+
*
|
|
4
|
+
* Provides middleware layer for input validation that runs
|
|
5
|
+
* before command action handlers.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { withValidation, schemas } = require('./lib/validation-middleware');
|
|
9
|
+
* module.exports = {
|
|
10
|
+
* name: 'mycommand',
|
|
11
|
+
* validate: {
|
|
12
|
+
* directory: schemas.path,
|
|
13
|
+
* ide: schemas.ide,
|
|
14
|
+
* },
|
|
15
|
+
* action: withValidation(async (options) => { ... })
|
|
16
|
+
* };
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { validatePath, isValidStoryId, isValidEpicId } = require('../../../lib/validate');
|
|
21
|
+
const { IdeRegistry } = require('./ide-registry');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validation schema definition
|
|
25
|
+
* @typedef {Object} ValidationSchema
|
|
26
|
+
* @property {string} type - Schema type (path, ide, storyId, epicId, string, number)
|
|
27
|
+
* @property {boolean} [required=false] - Whether the field is required
|
|
28
|
+
* @property {*} [default] - Default value if not provided
|
|
29
|
+
* @property {Function} [validate] - Custom validation function
|
|
30
|
+
* @property {string} [errorMessage] - Custom error message
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validation result
|
|
35
|
+
* @typedef {Object} ValidationResult
|
|
36
|
+
* @property {boolean} ok - Whether validation passed
|
|
37
|
+
* @property {Object} [data] - Validated and transformed data
|
|
38
|
+
* @property {string[]} [errors] - List of validation errors
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pre-defined validation schemas for common types
|
|
43
|
+
*/
|
|
44
|
+
const schemas = {
|
|
45
|
+
/**
|
|
46
|
+
* Path schema - validates directory/file paths
|
|
47
|
+
* Automatically resolves to absolute path and validates against traversal
|
|
48
|
+
*/
|
|
49
|
+
path: {
|
|
50
|
+
type: 'path',
|
|
51
|
+
required: false,
|
|
52
|
+
default: '.',
|
|
53
|
+
errorMessage: 'Invalid path',
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Required path schema
|
|
58
|
+
*/
|
|
59
|
+
pathRequired: {
|
|
60
|
+
type: 'path',
|
|
61
|
+
required: true,
|
|
62
|
+
errorMessage: 'Path is required',
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* IDE schema - validates IDE name against registry
|
|
67
|
+
*/
|
|
68
|
+
ide: {
|
|
69
|
+
type: 'ide',
|
|
70
|
+
required: false,
|
|
71
|
+
errorMessage: 'Invalid IDE name',
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Required IDE schema
|
|
76
|
+
*/
|
|
77
|
+
ideRequired: {
|
|
78
|
+
type: 'ide',
|
|
79
|
+
required: true,
|
|
80
|
+
errorMessage: 'IDE name is required',
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Story ID schema - validates US-XXXX format
|
|
85
|
+
*/
|
|
86
|
+
storyId: {
|
|
87
|
+
type: 'storyId',
|
|
88
|
+
required: false,
|
|
89
|
+
errorMessage: 'Invalid story ID (expected US-XXXX)',
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Required story ID schema
|
|
94
|
+
*/
|
|
95
|
+
storyIdRequired: {
|
|
96
|
+
type: 'storyId',
|
|
97
|
+
required: true,
|
|
98
|
+
errorMessage: 'Story ID is required (expected US-XXXX)',
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Epic ID schema - validates EP-XXXX format
|
|
103
|
+
*/
|
|
104
|
+
epicId: {
|
|
105
|
+
type: 'epicId',
|
|
106
|
+
required: false,
|
|
107
|
+
errorMessage: 'Invalid epic ID (expected EP-XXXX)',
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Required epic ID schema
|
|
112
|
+
*/
|
|
113
|
+
epicIdRequired: {
|
|
114
|
+
type: 'epicId',
|
|
115
|
+
required: true,
|
|
116
|
+
errorMessage: 'Epic ID is required (expected EP-XXXX)',
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* String schema with optional length constraints
|
|
121
|
+
* @param {Object} options - Schema options
|
|
122
|
+
* @returns {ValidationSchema}
|
|
123
|
+
*/
|
|
124
|
+
string: (options = {}) => ({
|
|
125
|
+
type: 'string',
|
|
126
|
+
required: options.required || false,
|
|
127
|
+
minLength: options.minLength,
|
|
128
|
+
maxLength: options.maxLength,
|
|
129
|
+
pattern: options.pattern,
|
|
130
|
+
errorMessage: options.errorMessage || 'Invalid string value',
|
|
131
|
+
}),
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Number schema with optional range constraints
|
|
135
|
+
* @param {Object} options - Schema options
|
|
136
|
+
* @returns {ValidationSchema}
|
|
137
|
+
*/
|
|
138
|
+
number: (options = {}) => ({
|
|
139
|
+
type: 'number',
|
|
140
|
+
required: options.required || false,
|
|
141
|
+
min: options.min,
|
|
142
|
+
max: options.max,
|
|
143
|
+
integer: options.integer || false,
|
|
144
|
+
errorMessage: options.errorMessage || 'Invalid number value',
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Boolean schema
|
|
149
|
+
*/
|
|
150
|
+
boolean: {
|
|
151
|
+
type: 'boolean',
|
|
152
|
+
required: false,
|
|
153
|
+
errorMessage: 'Invalid boolean value',
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Enum schema
|
|
158
|
+
* @param {string[]} values - Allowed values
|
|
159
|
+
* @param {Object} options - Schema options
|
|
160
|
+
* @returns {ValidationSchema}
|
|
161
|
+
*/
|
|
162
|
+
enum: (values, options = {}) => ({
|
|
163
|
+
type: 'enum',
|
|
164
|
+
values,
|
|
165
|
+
required: options.required || false,
|
|
166
|
+
errorMessage: options.errorMessage || `Invalid value (expected: ${values.join(', ')})`,
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Validate a single field against its schema
|
|
172
|
+
* @param {string} fieldName - Name of the field
|
|
173
|
+
* @param {*} value - Value to validate
|
|
174
|
+
* @param {ValidationSchema} schema - Schema to validate against
|
|
175
|
+
* @param {string} baseDir - Base directory for path validation
|
|
176
|
+
* @returns {ValidationResult}
|
|
177
|
+
*/
|
|
178
|
+
function validateField(fieldName, value, schema, baseDir) {
|
|
179
|
+
const errors = [];
|
|
180
|
+
|
|
181
|
+
// Check required
|
|
182
|
+
if (schema.required && (value === undefined || value === null || value === '')) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
errors: [schema.errorMessage || `${fieldName} is required`],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Return default if not provided
|
|
190
|
+
if (value === undefined || value === null || value === '') {
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
data: schema.default,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Validate by type
|
|
198
|
+
switch (schema.type) {
|
|
199
|
+
case 'path': {
|
|
200
|
+
// Convert to string if not already
|
|
201
|
+
const pathValue = String(value);
|
|
202
|
+
|
|
203
|
+
// Resolve path relative to current directory
|
|
204
|
+
const resolvedPath = path.resolve(pathValue);
|
|
205
|
+
|
|
206
|
+
// Validate path is within safe bounds
|
|
207
|
+
const pathResult = validatePath(pathValue, baseDir, {
|
|
208
|
+
allowSymlinks: false,
|
|
209
|
+
mustExist: false,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (!pathResult.ok) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
errors: [pathResult.error?.message || schema.errorMessage || 'Invalid path'],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
data: resolvedPath,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case 'ide': {
|
|
226
|
+
// Normalize IDE name to lowercase before validation
|
|
227
|
+
const normalizedIde = String(value).toLowerCase();
|
|
228
|
+
const validation = IdeRegistry.validate(normalizedIde);
|
|
229
|
+
if (!validation.ok) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
errors: [validation.error || schema.errorMessage],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
ok: true,
|
|
237
|
+
data: normalizedIde,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'storyId': {
|
|
242
|
+
// Normalize story ID to uppercase before validation
|
|
243
|
+
const normalizedStoryId = String(value).toUpperCase();
|
|
244
|
+
if (!isValidStoryId(normalizedStoryId)) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
errors: [schema.errorMessage || 'Invalid story ID (expected US-XXXX)'],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
data: normalizedStoryId,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
case 'epicId': {
|
|
257
|
+
// Normalize epic ID to uppercase before validation
|
|
258
|
+
const normalizedEpicId = String(value).toUpperCase();
|
|
259
|
+
if (!isValidEpicId(normalizedEpicId)) {
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
errors: [schema.errorMessage || 'Invalid epic ID (expected EP-XXXX)'],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
data: normalizedEpicId,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
case 'string': {
|
|
272
|
+
if (typeof value !== 'string') {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
errors: [schema.errorMessage || `${fieldName} must be a string`],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (schema.minLength && value.length < schema.minLength) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
errors: [`${fieldName} must be at least ${schema.minLength} characters`],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (schema.maxLength && value.length > schema.maxLength) {
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
errors: [`${fieldName} must be at most ${schema.maxLength} characters`],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (schema.pattern && !schema.pattern.test(value)) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
errors: [schema.errorMessage || `${fieldName} has invalid format`],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
ok: true,
|
|
302
|
+
data: value,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case 'number': {
|
|
307
|
+
const num = parseFloat(value);
|
|
308
|
+
|
|
309
|
+
if (isNaN(num)) {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
errors: [schema.errorMessage || `${fieldName} must be a number`],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (schema.integer && !Number.isInteger(num)) {
|
|
317
|
+
return {
|
|
318
|
+
ok: false,
|
|
319
|
+
errors: [`${fieldName} must be an integer`],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (schema.min !== undefined && num < schema.min) {
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
errors: [`${fieldName} must be at least ${schema.min}`],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (schema.max !== undefined && num > schema.max) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
errors: [`${fieldName} must be at most ${schema.max}`],
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
data: num,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case 'boolean': {
|
|
344
|
+
const boolValue = value === true || value === 'true' || value === '1';
|
|
345
|
+
return {
|
|
346
|
+
ok: true,
|
|
347
|
+
data: boolValue,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
case 'enum': {
|
|
352
|
+
if (!schema.values.includes(value)) {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
errors: [schema.errorMessage || `Invalid value for ${fieldName}`],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
ok: true,
|
|
360
|
+
data: value,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
default:
|
|
365
|
+
// Unknown type - pass through
|
|
366
|
+
return {
|
|
367
|
+
ok: true,
|
|
368
|
+
data: value,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Validate all fields in options against their schemas
|
|
375
|
+
* @param {Object} options - Command options
|
|
376
|
+
* @param {Object} validationSchemas - Schema definitions
|
|
377
|
+
* @param {string} [baseDir=process.cwd()] - Base directory for path validation
|
|
378
|
+
* @returns {ValidationResult}
|
|
379
|
+
*/
|
|
380
|
+
function validateOptions(options, validationSchemas, baseDir = process.cwd()) {
|
|
381
|
+
const errors = [];
|
|
382
|
+
const validatedData = { ...options };
|
|
383
|
+
|
|
384
|
+
for (const [fieldName, schema] of Object.entries(validationSchemas)) {
|
|
385
|
+
const value = options[fieldName];
|
|
386
|
+
const result = validateField(fieldName, value, schema, baseDir);
|
|
387
|
+
|
|
388
|
+
if (!result.ok) {
|
|
389
|
+
errors.push(...result.errors);
|
|
390
|
+
} else {
|
|
391
|
+
validatedData[fieldName] = result.data;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (errors.length > 0) {
|
|
396
|
+
return { ok: false, errors };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return { ok: true, data: validatedData };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Create a validation wrapper for command actions
|
|
404
|
+
* @param {Function} action - Original command action
|
|
405
|
+
* @param {Object} validationSchemas - Schema definitions
|
|
406
|
+
* @returns {Function} Wrapped action with validation
|
|
407
|
+
*/
|
|
408
|
+
function createValidationWrapper(action, validationSchemas) {
|
|
409
|
+
return async function validatedAction(...args) {
|
|
410
|
+
// Get options from the last argument (Commander.js pattern)
|
|
411
|
+
const options = args[args.length - 1];
|
|
412
|
+
|
|
413
|
+
// Validate options
|
|
414
|
+
const result = validateOptions(options, validationSchemas);
|
|
415
|
+
|
|
416
|
+
if (!result.ok) {
|
|
417
|
+
const { ErrorHandler } = require('./error-handler');
|
|
418
|
+
const handler = new ErrorHandler('validation');
|
|
419
|
+
|
|
420
|
+
// Format errors nicely
|
|
421
|
+
const errorList = result.errors.map(e => ` • ${e}`).join('\n');
|
|
422
|
+
handler.warning(
|
|
423
|
+
'Input validation failed',
|
|
424
|
+
`Please fix the following:\n${errorList}`,
|
|
425
|
+
'Check command help with --help'
|
|
426
|
+
);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Replace options with validated data
|
|
431
|
+
args[args.length - 1] = { ...options, ...result.data };
|
|
432
|
+
|
|
433
|
+
// Call original action
|
|
434
|
+
return action.apply(this, args);
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Decorator to add validation to a command action
|
|
440
|
+
* @param {Object} validationSchemas - Schema definitions
|
|
441
|
+
* @returns {Function} Decorator function
|
|
442
|
+
*/
|
|
443
|
+
function withValidation(validationSchemas) {
|
|
444
|
+
return function (action) {
|
|
445
|
+
return createValidationWrapper(action, validationSchemas);
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Shorthand to wrap an action directly with schemas
|
|
451
|
+
* @param {Function} action - Action function
|
|
452
|
+
* @param {Object} validationSchemas - Schema definitions
|
|
453
|
+
* @returns {Function} Wrapped action
|
|
454
|
+
*/
|
|
455
|
+
function validated(action, validationSchemas) {
|
|
456
|
+
return createValidationWrapper(action, validationSchemas);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Validate path argument automatically
|
|
461
|
+
* Use as middleware before any path-based command
|
|
462
|
+
* @param {string} pathValue - Path value from options
|
|
463
|
+
* @param {string} fieldName - Name of the path field
|
|
464
|
+
* @returns {ValidationResult}
|
|
465
|
+
*/
|
|
466
|
+
function validatePathArgument(pathValue, fieldName = 'path') {
|
|
467
|
+
return validateField(fieldName, pathValue, schemas.path, process.cwd());
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Format validation errors for display
|
|
472
|
+
* @param {string[]} errors - List of errors
|
|
473
|
+
* @returns {string} Formatted error message
|
|
474
|
+
*/
|
|
475
|
+
function formatValidationErrors(errors) {
|
|
476
|
+
if (errors.length === 1) {
|
|
477
|
+
return errors[0];
|
|
478
|
+
}
|
|
479
|
+
return errors.map((e, i) => `${i + 1}. ${e}`).join('\n');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
module.exports = {
|
|
483
|
+
schemas,
|
|
484
|
+
validateField,
|
|
485
|
+
validateOptions,
|
|
486
|
+
createValidationWrapper,
|
|
487
|
+
withValidation,
|
|
488
|
+
validated,
|
|
489
|
+
validatePathArgument,
|
|
490
|
+
formatValidationErrors,
|
|
491
|
+
};
|