agileflow 2.89.2 → 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 +10 -0
- package/README.md +3 -3
- package/lib/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +653 -0
- 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 +38 -584
- package/package.json +4 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- 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 +10 -33
- package/tools/cli/commands/doctor.js +48 -40
- package/tools/cli/commands/list.js +49 -37
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +12 -41
- package/tools/cli/installers/core/installer.js +75 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
- 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,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgileFlow CLI - Configuration Manager
|
|
3
|
+
*
|
|
4
|
+
* Centralized configuration management with schema validation,
|
|
5
|
+
* migration support, and consistent access patterns.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { ConfigManager } = require('./lib/config-manager');
|
|
9
|
+
* const config = await ConfigManager.load(projectDir);
|
|
10
|
+
* const userName = config.get('userName');
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs-extra');
|
|
15
|
+
const { safeLoad, safeDump } = require('../../../lib/yaml-utils');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration schema definition
|
|
19
|
+
* @typedef {Object} ConfigSchema
|
|
20
|
+
* @property {string} type - Value type (string, array, boolean, number)
|
|
21
|
+
* @property {*} default - Default value
|
|
22
|
+
* @property {boolean} required - Whether the field is required
|
|
23
|
+
* @property {Function} [validate] - Custom validation function
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration schema for AgileFlow manifest
|
|
28
|
+
*/
|
|
29
|
+
const CONFIG_SCHEMA = {
|
|
30
|
+
version: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
default: '0.0.0',
|
|
33
|
+
required: true,
|
|
34
|
+
validate: v => /^\d+\.\d+\.\d+/.test(v),
|
|
35
|
+
},
|
|
36
|
+
userName: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: 'Developer',
|
|
39
|
+
required: false,
|
|
40
|
+
},
|
|
41
|
+
ides: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
default: ['claude-code'],
|
|
44
|
+
required: true,
|
|
45
|
+
validate: arr =>
|
|
46
|
+
Array.isArray(arr) &&
|
|
47
|
+
arr.every(ide => ['claude-code', 'cursor', 'windsurf', 'codex'].includes(ide)),
|
|
48
|
+
},
|
|
49
|
+
agileflowFolder: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
default: '.agileflow',
|
|
52
|
+
required: true,
|
|
53
|
+
validate: v => typeof v === 'string' && v.length > 0 && !v.includes('..'),
|
|
54
|
+
},
|
|
55
|
+
docsFolder: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
default: 'docs',
|
|
58
|
+
required: true,
|
|
59
|
+
validate: v => typeof v === 'string' && v.length > 0 && !v.includes('..'),
|
|
60
|
+
},
|
|
61
|
+
installedAt: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
default: null,
|
|
64
|
+
required: false,
|
|
65
|
+
},
|
|
66
|
+
updatedAt: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
default: null,
|
|
69
|
+
required: false,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Valid configuration keys (for external reference)
|
|
75
|
+
*/
|
|
76
|
+
const VALID_CONFIG_KEYS = Object.keys(CONFIG_SCHEMA);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* User-editable configuration keys
|
|
80
|
+
*/
|
|
81
|
+
const EDITABLE_CONFIG_KEYS = ['userName', 'ides', 'agileflowFolder', 'docsFolder'];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Configuration Manager class
|
|
85
|
+
*/
|
|
86
|
+
class ConfigManager {
|
|
87
|
+
/**
|
|
88
|
+
* Create a new ConfigManager instance
|
|
89
|
+
* @param {Object} data - Configuration data
|
|
90
|
+
* @param {string} manifestPath - Path to manifest file
|
|
91
|
+
*/
|
|
92
|
+
constructor(data = {}, manifestPath = null) {
|
|
93
|
+
this._data = { ...data };
|
|
94
|
+
this._manifestPath = manifestPath;
|
|
95
|
+
this._dirty = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load configuration from a project directory
|
|
100
|
+
* @param {string} projectDir - Project directory
|
|
101
|
+
* @param {Object} options - Load options
|
|
102
|
+
* @param {string} [options.agileflowFolder='.agileflow'] - AgileFlow folder name
|
|
103
|
+
* @returns {Promise<ConfigManager>} ConfigManager instance
|
|
104
|
+
*/
|
|
105
|
+
static async load(projectDir, options = {}) {
|
|
106
|
+
const agileflowFolder = options.agileflowFolder || '.agileflow';
|
|
107
|
+
const manifestPath = path.join(projectDir, agileflowFolder, '_cfg', 'manifest.yaml');
|
|
108
|
+
|
|
109
|
+
let data = {};
|
|
110
|
+
|
|
111
|
+
if (await fs.pathExists(manifestPath)) {
|
|
112
|
+
try {
|
|
113
|
+
const content = await fs.readFile(manifestPath, 'utf8');
|
|
114
|
+
const parsed = safeLoad(content);
|
|
115
|
+
// Normalize keys from snake_case to camelCase
|
|
116
|
+
data = ConfigManager._normalizeKeys(parsed);
|
|
117
|
+
} catch {
|
|
118
|
+
// If manifest is corrupted, use defaults
|
|
119
|
+
data = {};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Apply defaults for missing fields
|
|
124
|
+
for (const [key, schema] of Object.entries(CONFIG_SCHEMA)) {
|
|
125
|
+
if (data[key] === undefined && schema.default !== null) {
|
|
126
|
+
data[key] = schema.default;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return new ConfigManager(data, manifestPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Normalize keys from snake_case to camelCase
|
|
135
|
+
* @param {Object} obj - Object with snake_case keys
|
|
136
|
+
* @returns {Object} Object with camelCase keys
|
|
137
|
+
*/
|
|
138
|
+
static _normalizeKeys(obj) {
|
|
139
|
+
const keyMap = {
|
|
140
|
+
user_name: 'userName',
|
|
141
|
+
agileflow_folder: 'agileflowFolder',
|
|
142
|
+
docs_folder: 'docsFolder',
|
|
143
|
+
installed_at: 'installedAt',
|
|
144
|
+
updated_at: 'updatedAt',
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = {};
|
|
148
|
+
for (const [key, value] of Object.entries(obj || {})) {
|
|
149
|
+
const normalizedKey = keyMap[key] || key;
|
|
150
|
+
result[normalizedKey] = value;
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Denormalize keys from camelCase to snake_case for storage
|
|
157
|
+
* @param {Object} obj - Object with camelCase keys
|
|
158
|
+
* @returns {Object} Object with snake_case keys
|
|
159
|
+
*/
|
|
160
|
+
static _denormalizeKeys(obj) {
|
|
161
|
+
const keyMap = {
|
|
162
|
+
userName: 'user_name',
|
|
163
|
+
agileflowFolder: 'agileflow_folder',
|
|
164
|
+
docsFolder: 'docs_folder',
|
|
165
|
+
installedAt: 'installed_at',
|
|
166
|
+
updatedAt: 'updated_at',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const result = {};
|
|
170
|
+
for (const [key, value] of Object.entries(obj || {})) {
|
|
171
|
+
const denormalizedKey = keyMap[key] || key;
|
|
172
|
+
result[denormalizedKey] = value;
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get a configuration value
|
|
179
|
+
* @param {string} key - Configuration key
|
|
180
|
+
* @returns {*} Configuration value
|
|
181
|
+
*/
|
|
182
|
+
get(key) {
|
|
183
|
+
const schema = CONFIG_SCHEMA[key];
|
|
184
|
+
if (!schema) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
return this._data[key] !== undefined ? this._data[key] : schema.default;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Set a configuration value
|
|
192
|
+
* @param {string} key - Configuration key
|
|
193
|
+
* @param {*} value - Configuration value
|
|
194
|
+
* @returns {{ok: boolean, error?: string}} Result
|
|
195
|
+
*/
|
|
196
|
+
set(key, value) {
|
|
197
|
+
const schema = CONFIG_SCHEMA[key];
|
|
198
|
+
|
|
199
|
+
if (!schema) {
|
|
200
|
+
return { ok: false, error: `Unknown configuration key: ${key}` };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!EDITABLE_CONFIG_KEYS.includes(key)) {
|
|
204
|
+
return { ok: false, error: `Configuration key '${key}' is read-only` };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Type validation
|
|
208
|
+
const typeError = this._validateType(key, value, schema);
|
|
209
|
+
if (typeError) {
|
|
210
|
+
return { ok: false, error: typeError };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Custom validation
|
|
214
|
+
if (schema.validate && !schema.validate(value)) {
|
|
215
|
+
return { ok: false, error: `Invalid value for '${key}'` };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this._data[key] = value;
|
|
219
|
+
this._dirty = true;
|
|
220
|
+
return { ok: true };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Validate type of a value
|
|
225
|
+
* @param {string} key - Configuration key
|
|
226
|
+
* @param {*} value - Value to validate
|
|
227
|
+
* @param {ConfigSchema} schema - Schema definition
|
|
228
|
+
* @returns {string|null} Error message or null if valid
|
|
229
|
+
*/
|
|
230
|
+
_validateType(key, value, schema) {
|
|
231
|
+
switch (schema.type) {
|
|
232
|
+
case 'string':
|
|
233
|
+
if (typeof value !== 'string') {
|
|
234
|
+
return `'${key}' must be a string`;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case 'array':
|
|
238
|
+
if (!Array.isArray(value)) {
|
|
239
|
+
return `'${key}' must be an array`;
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
case 'boolean':
|
|
243
|
+
if (typeof value !== 'boolean') {
|
|
244
|
+
return `'${key}' must be a boolean`;
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case 'number':
|
|
248
|
+
if (typeof value !== 'number') {
|
|
249
|
+
return `'${key}' must be a number`;
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Validate all configuration values
|
|
258
|
+
* @returns {{ok: boolean, errors: string[]}} Validation result
|
|
259
|
+
*/
|
|
260
|
+
validate() {
|
|
261
|
+
const errors = [];
|
|
262
|
+
|
|
263
|
+
for (const [key, schema] of Object.entries(CONFIG_SCHEMA)) {
|
|
264
|
+
const value = this._data[key];
|
|
265
|
+
|
|
266
|
+
// Check required fields
|
|
267
|
+
if (schema.required && (value === undefined || value === null)) {
|
|
268
|
+
errors.push(`Missing required field: ${key}`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Skip validation for undefined optional fields
|
|
273
|
+
if (value === undefined || value === null) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Type validation
|
|
278
|
+
const typeError = this._validateType(key, value, schema);
|
|
279
|
+
if (typeError) {
|
|
280
|
+
errors.push(typeError);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Custom validation
|
|
285
|
+
if (schema.validate && !schema.validate(value)) {
|
|
286
|
+
errors.push(`Invalid value for '${key}'`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { ok: errors.length === 0, errors };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Save configuration to manifest file
|
|
295
|
+
* @returns {Promise<{ok: boolean, error?: string}>} Save result
|
|
296
|
+
*/
|
|
297
|
+
async save() {
|
|
298
|
+
if (!this._manifestPath) {
|
|
299
|
+
return { ok: false, error: 'No manifest path set' };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Update timestamp
|
|
304
|
+
this._data.updatedAt = new Date().toISOString();
|
|
305
|
+
|
|
306
|
+
// Ensure directory exists
|
|
307
|
+
await fs.ensureDir(path.dirname(this._manifestPath));
|
|
308
|
+
|
|
309
|
+
// Denormalize keys for storage
|
|
310
|
+
const storageData = ConfigManager._denormalizeKeys(this._data);
|
|
311
|
+
|
|
312
|
+
// Write to file
|
|
313
|
+
await fs.writeFile(this._manifestPath, safeDump(storageData), 'utf8');
|
|
314
|
+
|
|
315
|
+
this._dirty = false;
|
|
316
|
+
return { ok: true };
|
|
317
|
+
} catch (err) {
|
|
318
|
+
return { ok: false, error: err.message };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get all configuration data
|
|
324
|
+
* @returns {Object} All configuration data
|
|
325
|
+
*/
|
|
326
|
+
getAll() {
|
|
327
|
+
const result = {};
|
|
328
|
+
for (const key of VALID_CONFIG_KEYS) {
|
|
329
|
+
result[key] = this.get(key);
|
|
330
|
+
}
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check if configuration has unsaved changes
|
|
336
|
+
* @returns {boolean}
|
|
337
|
+
*/
|
|
338
|
+
isDirty() {
|
|
339
|
+
return this._dirty;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get the manifest file path
|
|
344
|
+
* @returns {string|null}
|
|
345
|
+
*/
|
|
346
|
+
getManifestPath() {
|
|
347
|
+
return this._manifestPath;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Migrate configuration from an older format
|
|
352
|
+
* @param {Object} oldData - Old configuration data
|
|
353
|
+
* @returns {{ok: boolean, migrated: string[]}} Migration result
|
|
354
|
+
*/
|
|
355
|
+
migrate(oldData) {
|
|
356
|
+
const migrated = [];
|
|
357
|
+
|
|
358
|
+
// Migration: rename 'name' to 'userName'
|
|
359
|
+
if (oldData.name && !this._data.userName) {
|
|
360
|
+
this._data.userName = oldData.name;
|
|
361
|
+
migrated.push('name → userName');
|
|
362
|
+
this._dirty = true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Migration: normalize IDE names
|
|
366
|
+
if (this._data.ides) {
|
|
367
|
+
const normalizedIdes = this._data.ides.map(ide => ide.toLowerCase());
|
|
368
|
+
if (JSON.stringify(normalizedIdes) !== JSON.stringify(this._data.ides)) {
|
|
369
|
+
this._data.ides = normalizedIdes;
|
|
370
|
+
migrated.push('ides normalized to lowercase');
|
|
371
|
+
this._dirty = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Migration: ensure agileflowFolder starts with dot
|
|
376
|
+
if (
|
|
377
|
+
this._data.agileflowFolder &&
|
|
378
|
+
!this._data.agileflowFolder.startsWith('.') &&
|
|
379
|
+
this._data.agileflowFolder !== 'agileflow'
|
|
380
|
+
) {
|
|
381
|
+
// Only migrate if it looks like it should have a dot
|
|
382
|
+
// Don't migrate 'agileflow' to '.agileflow' automatically
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { ok: true, migrated };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = {
|
|
390
|
+
ConfigManager,
|
|
391
|
+
CONFIG_SCHEMA,
|
|
392
|
+
VALID_CONFIG_KEYS,
|
|
393
|
+
EDITABLE_CONFIG_KEYS,
|
|
394
|
+
};
|
|
@@ -33,6 +33,13 @@ const {
|
|
|
33
33
|
countSkills,
|
|
34
34
|
getCounts,
|
|
35
35
|
} = require('../../../scripts/lib/counter');
|
|
36
|
+
const {
|
|
37
|
+
sanitize,
|
|
38
|
+
sanitizeAgentData,
|
|
39
|
+
sanitizeCommandData,
|
|
40
|
+
validatePlaceholderValue,
|
|
41
|
+
detectInjectionAttempt,
|
|
42
|
+
} = require('../../../lib/content-sanitizer');
|
|
36
43
|
|
|
37
44
|
// =============================================================================
|
|
38
45
|
// List Generation Functions
|
|
@@ -41,12 +48,14 @@ const {
|
|
|
41
48
|
/**
|
|
42
49
|
* Validate that a file path is within the expected directory.
|
|
43
50
|
* Prevents reading files outside the expected scope.
|
|
51
|
+
* Security: Symlinks are NOT allowed to prevent escape attacks.
|
|
44
52
|
* @param {string} filePath - File path to validate
|
|
45
53
|
* @param {string} baseDir - Expected base directory
|
|
46
54
|
* @returns {boolean} True if path is safe
|
|
47
55
|
*/
|
|
48
56
|
function isPathSafe(filePath, baseDir) {
|
|
49
|
-
|
|
57
|
+
// Security hardening (US-0104): Symlinks disabled to prevent escape attacks
|
|
58
|
+
const result = validatePath(filePath, baseDir, { allowSymlinks: false });
|
|
50
59
|
return result.ok;
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -76,19 +85,32 @@ function generateAgentList(agentsDir) {
|
|
|
76
85
|
continue;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
// Sanitize agent data to prevent injection attacks
|
|
89
|
+
const rawAgent = {
|
|
80
90
|
name: frontmatter.name || path.basename(file, '.md'),
|
|
81
91
|
description: frontmatter.description || '',
|
|
82
92
|
tools: normalizeTools(frontmatter.tools),
|
|
83
93
|
model: frontmatter.model || 'haiku',
|
|
84
|
-
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const sanitizedAgent = sanitizeAgentData(rawAgent);
|
|
97
|
+
|
|
98
|
+
// Skip if sanitization produced invalid data
|
|
99
|
+
if (!sanitizedAgent.name || sanitizedAgent.name === 'unknown') {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
agents.push(sanitizedAgent);
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
agents.sort((a, b) => a.name.localeCompare(b.name));
|
|
88
107
|
|
|
89
|
-
|
|
108
|
+
// Sanitize the count value
|
|
109
|
+
const safeCount = sanitize.count(agents.length);
|
|
110
|
+
let output = `**AVAILABLE AGENTS (${safeCount} total)**:\n\n`;
|
|
90
111
|
|
|
91
112
|
agents.forEach((agent, index) => {
|
|
113
|
+
// All values are already sanitized by sanitizeAgentData
|
|
92
114
|
output += `${index + 1}. **${agent.name}** (model: ${agent.model})\n`;
|
|
93
115
|
output += ` - **Purpose**: ${agent.description}\n`;
|
|
94
116
|
output += ` - **Tools**: ${agent.tools.join(', ')}\n`;
|
|
@@ -127,11 +149,19 @@ function generateCommandList(commandsDir) {
|
|
|
127
149
|
continue;
|
|
128
150
|
}
|
|
129
151
|
|
|
130
|
-
|
|
152
|
+
// Sanitize command data to prevent injection attacks
|
|
153
|
+
const rawCommand = {
|
|
131
154
|
name: cmdName,
|
|
132
155
|
description: frontmatter.description || '',
|
|
133
156
|
argumentHint: frontmatter['argument-hint'] || '',
|
|
134
|
-
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const sanitizedCommand = sanitizeCommandData(rawCommand);
|
|
160
|
+
if (!sanitizedCommand.name || sanitizedCommand.name === 'unknown') {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
commands.push(sanitizedCommand);
|
|
135
165
|
}
|
|
136
166
|
|
|
137
167
|
// Scan subdirectories (e.g., session/)
|
|
@@ -163,20 +193,31 @@ function generateCommandList(commandsDir) {
|
|
|
163
193
|
continue;
|
|
164
194
|
}
|
|
165
195
|
|
|
166
|
-
|
|
196
|
+
// Sanitize command data
|
|
197
|
+
const rawCommand = {
|
|
167
198
|
name: cmdName,
|
|
168
199
|
description: frontmatter.description || '',
|
|
169
200
|
argumentHint: frontmatter['argument-hint'] || '',
|
|
170
|
-
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const sanitizedCommand = sanitizeCommandData(rawCommand);
|
|
204
|
+
if (!sanitizedCommand.name || sanitizedCommand.name === 'unknown') {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
commands.push(sanitizedCommand);
|
|
171
209
|
}
|
|
172
210
|
}
|
|
173
211
|
}
|
|
174
212
|
|
|
175
213
|
commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
176
214
|
|
|
177
|
-
|
|
215
|
+
// Sanitize the count value
|
|
216
|
+
const safeCount = sanitize.count(commands.length);
|
|
217
|
+
let output = `Available commands (${safeCount} total):\n`;
|
|
178
218
|
|
|
179
219
|
commands.forEach(cmd => {
|
|
220
|
+
// All values are already sanitized by sanitizeCommandData
|
|
180
221
|
const argHint = cmd.argumentHint ? ` ${cmd.argumentHint}` : '';
|
|
181
222
|
output += `- \`/agileflow:${cmd.name}${argHint}\` - ${cmd.description}\n`;
|
|
182
223
|
});
|
|
@@ -208,16 +249,28 @@ function injectContent(content, context = {}) {
|
|
|
208
249
|
counts = getCounts(coreDir);
|
|
209
250
|
}
|
|
210
251
|
|
|
252
|
+
// Validate and sanitize all placeholder values before injection
|
|
253
|
+
const safeCommandCount = validatePlaceholderValue('COMMAND_COUNT', counts.commands).sanitized;
|
|
254
|
+
const safeAgentCount = validatePlaceholderValue('AGENT_COUNT', counts.agents).sanitized;
|
|
255
|
+
const safeSkillCount = validatePlaceholderValue('SKILL_COUNT', counts.skills).sanitized;
|
|
256
|
+
const safeVersion = validatePlaceholderValue('VERSION', version).sanitized;
|
|
257
|
+
const safeDate = validatePlaceholderValue('INSTALL_DATE', new Date()).sanitized;
|
|
258
|
+
const safeAgileflowFolder = validatePlaceholderValue(
|
|
259
|
+
'agileflow_folder',
|
|
260
|
+
agileflowFolder
|
|
261
|
+
).sanitized;
|
|
262
|
+
|
|
211
263
|
// Replace count placeholders (both formats: {{X}} and <!-- {{X}} -->)
|
|
212
|
-
result = result.replace(/\{\{COMMAND_COUNT\}\}/g, String(
|
|
213
|
-
result = result.replace(/\{\{AGENT_COUNT\}\}/g, String(
|
|
214
|
-
result = result.replace(/\{\{SKILL_COUNT\}\}/g, String(
|
|
264
|
+
result = result.replace(/\{\{COMMAND_COUNT\}\}/g, String(safeCommandCount));
|
|
265
|
+
result = result.replace(/\{\{AGENT_COUNT\}\}/g, String(safeAgentCount));
|
|
266
|
+
result = result.replace(/\{\{SKILL_COUNT\}\}/g, String(safeSkillCount));
|
|
215
267
|
|
|
216
268
|
// Replace metadata placeholders
|
|
217
|
-
result = result.replace(/\{\{VERSION\}\}/g,
|
|
218
|
-
result = result.replace(/\{\{INSTALL_DATE\}\}/g,
|
|
269
|
+
result = result.replace(/\{\{VERSION\}\}/g, safeVersion);
|
|
270
|
+
result = result.replace(/\{\{INSTALL_DATE\}\}/g, safeDate);
|
|
219
271
|
|
|
220
272
|
// Replace list placeholders (only if core directory available)
|
|
273
|
+
// List generation already includes sanitization via sanitizeAgentData/sanitizeCommandData
|
|
221
274
|
if (coreDir && fs.existsSync(coreDir)) {
|
|
222
275
|
if (result.includes('{{AGENT_LIST}}')) {
|
|
223
276
|
const agentList = generateAgentList(path.join(coreDir, 'agents'));
|
|
@@ -232,8 +285,8 @@ function injectContent(content, context = {}) {
|
|
|
232
285
|
}
|
|
233
286
|
}
|
|
234
287
|
|
|
235
|
-
// Replace folder placeholders
|
|
236
|
-
result = result.replace(/\{agileflow_folder\}/g,
|
|
288
|
+
// Replace folder placeholders with sanitized values
|
|
289
|
+
result = result.replace(/\{agileflow_folder\}/g, safeAgileflowFolder);
|
|
237
290
|
result = result.replace(/\{project-root\}/g, '{project-root}'); // Keep as-is for runtime
|
|
238
291
|
|
|
239
292
|
return result;
|