agileflow 2.89.3 → 2.90.1
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 +10 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/smart-json-file.js +228 -1
- package/lib/table-formatter.js +519 -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 +3 -1
- package/scripts/check-update.js +17 -3
- package/scripts/lib/sessionRegistry.js +678 -0
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +151 -0
- package/scripts/tui/index.js +31 -0
- package/scripts/tui/lib/crashRecovery.js +304 -0
- package/scripts/tui/lib/eventStream.js +309 -0
- package/scripts/tui/lib/keyboard.js +261 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +242 -0
- package/scripts/tui/panels/SessionPanel.js +170 -0
- package/scripts/tui/panels/TracePanel.js +298 -0
- package/scripts/tui/simple-tui.js +390 -0
- package/tools/cli/commands/config.js +7 -31
- package/tools/cli/commands/doctor.js +28 -39
- package/tools/cli/commands/list.js +47 -35
- package/tools/cli/commands/status.js +20 -38
- package/tools/cli/commands/tui.js +59 -0
- package/tools/cli/commands/uninstall.js +12 -39
- package/tools/cli/installers/core/installer.js +13 -0
- package/tools/cli/lib/command-context.js +382 -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 +17 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgileFlow CLI - Argument Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Validation utilities for command-line arguments and options.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
isValidBranchName,
|
|
9
|
+
isValidStoryId,
|
|
10
|
+
isValidEpicId,
|
|
11
|
+
isValidFeatureName,
|
|
12
|
+
isValidProfileName,
|
|
13
|
+
isValidCommandName,
|
|
14
|
+
isValidSessionNickname,
|
|
15
|
+
} = require('./validate-names');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate that a value is a positive integer within bounds.
|
|
19
|
+
* @param {any} val - Value to validate
|
|
20
|
+
* @param {number} min - Minimum allowed value (inclusive)
|
|
21
|
+
* @param {number} max - Maximum allowed value (inclusive)
|
|
22
|
+
* @returns {boolean} True if valid positive integer in range
|
|
23
|
+
*/
|
|
24
|
+
function isPositiveInteger(val, min = 1, max = Number.MAX_SAFE_INTEGER) {
|
|
25
|
+
const num = typeof val === 'string' ? parseInt(val, 10) : val;
|
|
26
|
+
if (!Number.isInteger(num)) return false;
|
|
27
|
+
return num >= min && num <= max;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse and validate an integer with bounds.
|
|
32
|
+
* @param {any} val - Value to parse
|
|
33
|
+
* @param {number} defaultVal - Default if invalid
|
|
34
|
+
* @param {number} min - Minimum allowed value
|
|
35
|
+
* @param {number} max - Maximum allowed value
|
|
36
|
+
* @returns {number} Parsed integer or default
|
|
37
|
+
*/
|
|
38
|
+
function parseIntBounded(val, defaultVal, min = 1, max = Number.MAX_SAFE_INTEGER) {
|
|
39
|
+
const num = typeof val === 'string' ? parseInt(val, 10) : val;
|
|
40
|
+
if (!Number.isInteger(num) || num < min || num > max) {
|
|
41
|
+
return defaultVal;
|
|
42
|
+
}
|
|
43
|
+
return num;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate an option key against an allowed whitelist.
|
|
48
|
+
* @param {string} key - Option key to validate
|
|
49
|
+
* @param {string[]} allowedKeys - Array of allowed keys
|
|
50
|
+
* @returns {boolean} True if key is in whitelist
|
|
51
|
+
*/
|
|
52
|
+
function isValidOption(key, allowedKeys) {
|
|
53
|
+
if (!key || typeof key !== 'string') return false;
|
|
54
|
+
if (!Array.isArray(allowedKeys)) return false;
|
|
55
|
+
return allowedKeys.includes(key);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate and sanitize CLI arguments for known option types.
|
|
60
|
+
* Returns validated args object or null if critical validation fails.
|
|
61
|
+
*
|
|
62
|
+
* @param {string[]} args - Raw process.argv slice
|
|
63
|
+
* @param {Object} schema - Schema defining expected options
|
|
64
|
+
* @returns {{ ok: boolean, data?: Object, error?: string }}
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* const schema = {
|
|
68
|
+
* branch: { type: 'branchName', required: true },
|
|
69
|
+
* max: { type: 'positiveInt', min: 1, max: 100, default: 20 },
|
|
70
|
+
* profile: { type: 'profileName', default: 'default' }
|
|
71
|
+
* };
|
|
72
|
+
* const result = validateArgs(args, schema);
|
|
73
|
+
*/
|
|
74
|
+
function validateArgs(args, schema) {
|
|
75
|
+
const result = {};
|
|
76
|
+
const errors = [];
|
|
77
|
+
|
|
78
|
+
// Parse args into key-value pairs
|
|
79
|
+
const parsed = {};
|
|
80
|
+
for (let i = 0; i < args.length; i++) {
|
|
81
|
+
const arg = args[i];
|
|
82
|
+
if (arg.startsWith('--')) {
|
|
83
|
+
const [key, ...valueParts] = arg.slice(2).split('=');
|
|
84
|
+
const value = valueParts.length > 0 ? valueParts.join('=') : args[++i];
|
|
85
|
+
parsed[key] = value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate each schema field
|
|
90
|
+
for (const [field, config] of Object.entries(schema)) {
|
|
91
|
+
const value = parsed[field];
|
|
92
|
+
|
|
93
|
+
// Check required
|
|
94
|
+
if (config.required && (value === undefined || value === null)) {
|
|
95
|
+
errors.push(`Missing required option: --${field}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Use default if not provided
|
|
100
|
+
if (value === undefined || value === null) {
|
|
101
|
+
if (config.default !== undefined) {
|
|
102
|
+
result[field] = config.default;
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate by type
|
|
108
|
+
switch (config.type) {
|
|
109
|
+
case 'branchName':
|
|
110
|
+
if (!isValidBranchName(value)) {
|
|
111
|
+
errors.push(`Invalid branch name: ${value}`);
|
|
112
|
+
} else {
|
|
113
|
+
result[field] = value;
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'storyId':
|
|
118
|
+
if (!isValidStoryId(value)) {
|
|
119
|
+
errors.push(`Invalid story ID: ${value} (expected US-XXXX)`);
|
|
120
|
+
} else {
|
|
121
|
+
result[field] = value;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'epicId':
|
|
126
|
+
if (!isValidEpicId(value)) {
|
|
127
|
+
errors.push(`Invalid epic ID: ${value} (expected EP-XXXX)`);
|
|
128
|
+
} else {
|
|
129
|
+
result[field] = value;
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'featureName':
|
|
134
|
+
if (!isValidFeatureName(value)) {
|
|
135
|
+
errors.push(`Invalid feature name: ${value}`);
|
|
136
|
+
} else {
|
|
137
|
+
result[field] = value;
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case 'profileName':
|
|
142
|
+
if (!isValidProfileName(value)) {
|
|
143
|
+
errors.push(`Invalid profile name: ${value}`);
|
|
144
|
+
} else {
|
|
145
|
+
result[field] = value;
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
case 'commandName':
|
|
150
|
+
if (!isValidCommandName(value)) {
|
|
151
|
+
errors.push(`Invalid command name: ${value}`);
|
|
152
|
+
} else {
|
|
153
|
+
result[field] = value;
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case 'sessionNickname':
|
|
158
|
+
if (!isValidSessionNickname(value)) {
|
|
159
|
+
errors.push(`Invalid session nickname: ${value}`);
|
|
160
|
+
} else {
|
|
161
|
+
result[field] = value;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case 'positiveInt': {
|
|
166
|
+
const min = config.min || 1;
|
|
167
|
+
const max = config.max || Number.MAX_SAFE_INTEGER;
|
|
168
|
+
if (!isPositiveInteger(value, min, max)) {
|
|
169
|
+
errors.push(`Invalid integer for ${field}: ${value} (expected ${min}-${max})`);
|
|
170
|
+
result[field] = config.default;
|
|
171
|
+
} else {
|
|
172
|
+
result[field] = parseInt(value, 10);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'enum':
|
|
178
|
+
if (!config.values.includes(value)) {
|
|
179
|
+
errors.push(
|
|
180
|
+
`Invalid value for ${field}: ${value} (expected: ${config.values.join(', ')})`
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
result[field] = value;
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case 'boolean':
|
|
188
|
+
result[field] = value === 'true' || value === '1' || value === true;
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case 'string':
|
|
192
|
+
// Basic string - just store it (caller should validate further if needed)
|
|
193
|
+
result[field] = String(value);
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
default:
|
|
197
|
+
result[field] = value;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (errors.length > 0) {
|
|
202
|
+
return { ok: false, error: errors.join('; ') };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { ok: true, data: result };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
isPositiveInteger,
|
|
210
|
+
parseIntBounded,
|
|
211
|
+
isValidOption,
|
|
212
|
+
validateArgs,
|
|
213
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgileFlow CLI - Name Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Validation patterns for names, IDs, and identifiers.
|
|
5
|
+
* Pre-compiled regex patterns for optimal performance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pre-compiled validation patterns for common input types.
|
|
10
|
+
* All patterns use strict whitelisting approach.
|
|
11
|
+
* Compiled once at module load for performance.
|
|
12
|
+
*/
|
|
13
|
+
const PATTERNS = {
|
|
14
|
+
// Git branch: alphanumeric, underscores, hyphens, forward slashes
|
|
15
|
+
// Examples: main, feature/US-0001, session-1, my_branch
|
|
16
|
+
branchName: /^[a-zA-Z0-9][a-zA-Z0-9_/-]*$/,
|
|
17
|
+
|
|
18
|
+
// Story ID: US-0001 to US-99999
|
|
19
|
+
storyId: /^US-\d{4,5}$/,
|
|
20
|
+
|
|
21
|
+
// Epic ID: EP-0001 to EP-99999
|
|
22
|
+
epicId: /^EP-\d{4,5}$/,
|
|
23
|
+
|
|
24
|
+
// Feature name: lowercase with hyphens, starts with letter
|
|
25
|
+
// Examples: damage-control, status-line, archival
|
|
26
|
+
featureName: /^[a-z][a-z0-9-]*$/,
|
|
27
|
+
|
|
28
|
+
// Profile name: alphanumeric with underscores/hyphens, starts with letter
|
|
29
|
+
// Examples: default, my-profile, dev_config
|
|
30
|
+
profileName: /^[a-zA-Z][a-zA-Z0-9_-]*$/,
|
|
31
|
+
|
|
32
|
+
// Command name: alphanumeric with hyphens/colons, starts with letter
|
|
33
|
+
// Examples: babysit, story:list, agileflow:configure
|
|
34
|
+
commandName: /^[a-zA-Z][a-zA-Z0-9:-]*$/,
|
|
35
|
+
|
|
36
|
+
// Session nickname: alphanumeric with hyphens/underscores
|
|
37
|
+
// Examples: auth-work, feature_1, main
|
|
38
|
+
sessionNickname: /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/,
|
|
39
|
+
|
|
40
|
+
// Merge strategy: squash or merge
|
|
41
|
+
// Examples: squash, merge
|
|
42
|
+
mergeStrategy: /^(squash|merge)$/,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate a git branch name.
|
|
47
|
+
* @param {string} name - Branch name to validate
|
|
48
|
+
* @returns {boolean} True if valid
|
|
49
|
+
*/
|
|
50
|
+
function isValidBranchName(name) {
|
|
51
|
+
if (!name || typeof name !== 'string') return false;
|
|
52
|
+
if (name.length > 255) return false; // Git limit
|
|
53
|
+
if (name.startsWith('-')) return false; // Prevent flag injection
|
|
54
|
+
if (name.includes('..')) return false; // Prevent path traversal
|
|
55
|
+
if (name.endsWith('.lock')) return false; // Reserved
|
|
56
|
+
return PATTERNS.branchName.test(name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate a story ID (US-XXXX format).
|
|
61
|
+
* @param {string} id - Story ID to validate
|
|
62
|
+
* @returns {boolean} True if valid
|
|
63
|
+
*/
|
|
64
|
+
function isValidStoryId(id) {
|
|
65
|
+
if (!id || typeof id !== 'string') return false;
|
|
66
|
+
return PATTERNS.storyId.test(id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate an epic ID (EP-XXXX format).
|
|
71
|
+
* @param {string} id - Epic ID to validate
|
|
72
|
+
* @returns {boolean} True if valid
|
|
73
|
+
*/
|
|
74
|
+
function isValidEpicId(id) {
|
|
75
|
+
if (!id || typeof id !== 'string') return false;
|
|
76
|
+
return PATTERNS.epicId.test(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate a feature name.
|
|
81
|
+
* @param {string} name - Feature name to validate
|
|
82
|
+
* @returns {boolean} True if valid
|
|
83
|
+
*/
|
|
84
|
+
function isValidFeatureName(name) {
|
|
85
|
+
if (!name || typeof name !== 'string') return false;
|
|
86
|
+
if (name.length > 50) return false;
|
|
87
|
+
return PATTERNS.featureName.test(name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate a profile name.
|
|
92
|
+
* @param {string} name - Profile name to validate
|
|
93
|
+
* @returns {boolean} True if valid
|
|
94
|
+
*/
|
|
95
|
+
function isValidProfileName(name) {
|
|
96
|
+
if (!name || typeof name !== 'string') return false;
|
|
97
|
+
if (name.length > 50) return false;
|
|
98
|
+
return PATTERNS.profileName.test(name);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validate a command name.
|
|
103
|
+
* @param {string} name - Command name to validate
|
|
104
|
+
* @returns {boolean} True if valid
|
|
105
|
+
*/
|
|
106
|
+
function isValidCommandName(name) {
|
|
107
|
+
if (!name || typeof name !== 'string') return false;
|
|
108
|
+
if (name.length > 100) return false;
|
|
109
|
+
return PATTERNS.commandName.test(name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate a session nickname.
|
|
114
|
+
* @param {string} name - Nickname to validate
|
|
115
|
+
* @returns {boolean} True if valid
|
|
116
|
+
*/
|
|
117
|
+
function isValidSessionNickname(name) {
|
|
118
|
+
if (!name || typeof name !== 'string') return false;
|
|
119
|
+
if (name.length > 50) return false;
|
|
120
|
+
return PATTERNS.sessionNickname.test(name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validate a merge strategy (squash or merge).
|
|
125
|
+
* @param {string} strategy - Strategy to validate
|
|
126
|
+
* @returns {boolean} True if valid
|
|
127
|
+
*/
|
|
128
|
+
function isValidMergeStrategy(strategy) {
|
|
129
|
+
if (!strategy || typeof strategy !== 'string') return false;
|
|
130
|
+
return PATTERNS.mergeStrategy.test(strategy);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
PATTERNS,
|
|
135
|
+
isValidBranchName,
|
|
136
|
+
isValidStoryId,
|
|
137
|
+
isValidEpicId,
|
|
138
|
+
isValidFeatureName,
|
|
139
|
+
isValidProfileName,
|
|
140
|
+
isValidCommandName,
|
|
141
|
+
isValidSessionNickname,
|
|
142
|
+
isValidMergeStrategy,
|
|
143
|
+
};
|