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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.90.1] - 2026-01-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- TUI as CLI command with session fixes
|
|
14
|
+
|
|
15
|
+
## [2.90.0] - 2026-01-16
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- TUI dashboard with session management and modular CLI architecture
|
|
19
|
+
|
|
10
20
|
## [2.89.3] - 2026-01-14
|
|
11
21
|
|
|
12
22
|
### Added
|
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PlaceholderRegistry - Extensible Content Injection System
|
|
5
|
+
*
|
|
6
|
+
* Provides a registry-based approach to placeholder resolution with:
|
|
7
|
+
* - Modular resolver functions for each placeholder type
|
|
8
|
+
* - Plugin system for external extensions
|
|
9
|
+
* - Security validation layer for all values
|
|
10
|
+
* - Clear separation between built-in and custom placeholders
|
|
11
|
+
*
|
|
12
|
+
* Security Model:
|
|
13
|
+
* - All values pass through sanitization before injection
|
|
14
|
+
* - Resolvers can define their own validation rules
|
|
15
|
+
* - External plugins are sandboxed to prevent security bypass
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const EventEmitter = require('events');
|
|
19
|
+
|
|
20
|
+
// Import security utilities
|
|
21
|
+
const {
|
|
22
|
+
sanitize,
|
|
23
|
+
validatePlaceholderValue,
|
|
24
|
+
detectInjectionAttempt,
|
|
25
|
+
escapeMarkdown,
|
|
26
|
+
} = require('./content-sanitizer');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Placeholder configuration
|
|
30
|
+
* @typedef {Object} PlaceholderConfig
|
|
31
|
+
* @property {string} name - Placeholder name (e.g., 'COMMAND_COUNT')
|
|
32
|
+
* @property {string} description - Human-readable description
|
|
33
|
+
* @property {string} type - Value type: 'count', 'string', 'list', 'date', 'version'
|
|
34
|
+
* @property {Function} resolver - Function that returns the value
|
|
35
|
+
* @property {Function} [validator] - Optional custom validator
|
|
36
|
+
* @property {Object} [context] - Context passed to resolver
|
|
37
|
+
* @property {boolean} [secure=true] - Whether to apply security sanitization
|
|
38
|
+
* @property {boolean} [cacheable=true] - Whether results can be cached
|
|
39
|
+
* @property {string} [source='builtin'] - Source: 'builtin' | 'plugin'
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default resolver types with their sanitization rules
|
|
44
|
+
*/
|
|
45
|
+
const RESOLVER_TYPES = {
|
|
46
|
+
count: {
|
|
47
|
+
sanitizer: sanitize.count,
|
|
48
|
+
defaultValue: 0,
|
|
49
|
+
},
|
|
50
|
+
string: {
|
|
51
|
+
sanitizer: value => sanitize.description(value, { escapeMarkdown: false }),
|
|
52
|
+
defaultValue: '',
|
|
53
|
+
},
|
|
54
|
+
list: {
|
|
55
|
+
sanitizer: value => (Array.isArray(value) ? value : []),
|
|
56
|
+
defaultValue: [],
|
|
57
|
+
},
|
|
58
|
+
date: {
|
|
59
|
+
sanitizer: sanitize.date,
|
|
60
|
+
defaultValue: () => new Date().toISOString().split('T')[0],
|
|
61
|
+
},
|
|
62
|
+
version: {
|
|
63
|
+
sanitizer: sanitize.version,
|
|
64
|
+
defaultValue: 'unknown',
|
|
65
|
+
},
|
|
66
|
+
markdown: {
|
|
67
|
+
sanitizer: value => sanitize.description(value, { escapeMarkdown: true }),
|
|
68
|
+
defaultValue: '',
|
|
69
|
+
},
|
|
70
|
+
folderName: {
|
|
71
|
+
sanitizer: sanitize.folderName,
|
|
72
|
+
defaultValue: '.agileflow',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* PlaceholderRegistry - Central registry for placeholder resolvers
|
|
78
|
+
*/
|
|
79
|
+
class PlaceholderRegistry extends EventEmitter {
|
|
80
|
+
constructor(options = {}) {
|
|
81
|
+
super();
|
|
82
|
+
|
|
83
|
+
this._resolvers = new Map();
|
|
84
|
+
this._cache = new Map();
|
|
85
|
+
this._plugins = new Map();
|
|
86
|
+
this._context = options.context || {};
|
|
87
|
+
|
|
88
|
+
// Options
|
|
89
|
+
this.cacheEnabled = options.cache !== false;
|
|
90
|
+
this.strictMode = options.strict !== false;
|
|
91
|
+
this.secureByDefault = options.secure !== false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Register a placeholder resolver
|
|
96
|
+
* @param {string} name - Placeholder name
|
|
97
|
+
* @param {Function} resolver - Resolver function
|
|
98
|
+
* @param {Object} config - Configuration
|
|
99
|
+
* @returns {PlaceholderRegistry} this for chaining
|
|
100
|
+
*/
|
|
101
|
+
register(name, resolver, config = {}) {
|
|
102
|
+
if (!name || typeof name !== 'string') {
|
|
103
|
+
throw new Error('Placeholder name must be a non-empty string');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof resolver !== 'function') {
|
|
107
|
+
throw new Error('Resolver must be a function');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Validate name format - allow:
|
|
111
|
+
// - UPPER_SNAKE_CASE (e.g., COMMAND_COUNT)
|
|
112
|
+
// - lower_snake_case (e.g., plugin_name)
|
|
113
|
+
// - lower-kebab-case (e.g., project-root)
|
|
114
|
+
// - Mixed for plugins (e.g., myplugin_COMMAND_COUNT)
|
|
115
|
+
if (
|
|
116
|
+
!/^[A-Z][A-Z0-9_]*$/.test(name) && // UPPER_SNAKE_CASE
|
|
117
|
+
!/^[a-z][a-z0-9_]*$/.test(name) && // lower_snake_case
|
|
118
|
+
!/^[a-z][a-z0-9-]*$/.test(name) && // lower-kebab-case
|
|
119
|
+
!/^[a-z][a-z0-9]*_[A-Z][A-Z0-9_]*$/.test(name) // plugin_PLACEHOLDER
|
|
120
|
+
) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Invalid placeholder name: ${name}. Use UPPER_SNAKE_CASE, lower_snake_case, or lower-kebab-case`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const resolverConfig = {
|
|
127
|
+
name,
|
|
128
|
+
description: config.description || `Placeholder: ${name}`,
|
|
129
|
+
type: config.type || 'string',
|
|
130
|
+
resolver,
|
|
131
|
+
validator: config.validator || null,
|
|
132
|
+
context: config.context || {},
|
|
133
|
+
secure: config.secure !== false,
|
|
134
|
+
cacheable: config.cacheable !== false,
|
|
135
|
+
source: config.source || 'builtin',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Validate type
|
|
139
|
+
if (!RESOLVER_TYPES[resolverConfig.type]) {
|
|
140
|
+
throw new Error(`Invalid resolver type: ${resolverConfig.type}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this._resolvers.set(name, resolverConfig);
|
|
144
|
+
|
|
145
|
+
// Clear cache for this placeholder
|
|
146
|
+
this._cache.delete(name);
|
|
147
|
+
|
|
148
|
+
this.emit('registered', { name, config: resolverConfig });
|
|
149
|
+
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Unregister a placeholder resolver
|
|
155
|
+
* @param {string} name - Placeholder name
|
|
156
|
+
* @returns {boolean} True if removed
|
|
157
|
+
*/
|
|
158
|
+
unregister(name) {
|
|
159
|
+
if (!this._resolvers.has(name)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const config = this._resolvers.get(name);
|
|
164
|
+
|
|
165
|
+
// Prevent removing built-in resolvers unless in non-strict mode
|
|
166
|
+
if (this.strictMode && config.source === 'builtin') {
|
|
167
|
+
throw new Error(`Cannot unregister built-in placeholder: ${name}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this._resolvers.delete(name);
|
|
171
|
+
this._cache.delete(name);
|
|
172
|
+
|
|
173
|
+
this.emit('unregistered', { name });
|
|
174
|
+
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if a placeholder is registered
|
|
180
|
+
* @param {string} name - Placeholder name
|
|
181
|
+
* @returns {boolean} True if registered
|
|
182
|
+
*/
|
|
183
|
+
has(name) {
|
|
184
|
+
return this._resolvers.has(name);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get resolver configuration
|
|
189
|
+
* @param {string} name - Placeholder name
|
|
190
|
+
* @returns {PlaceholderConfig|null} Configuration or null
|
|
191
|
+
*/
|
|
192
|
+
getConfig(name) {
|
|
193
|
+
return this._resolvers.get(name) || null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolve a placeholder value
|
|
198
|
+
* @param {string} name - Placeholder name
|
|
199
|
+
* @param {Object} context - Additional context
|
|
200
|
+
* @returns {any} Resolved and sanitized value
|
|
201
|
+
*/
|
|
202
|
+
resolve(name, context = {}) {
|
|
203
|
+
const config = this._resolvers.get(name);
|
|
204
|
+
|
|
205
|
+
if (!config) {
|
|
206
|
+
if (this.strictMode) {
|
|
207
|
+
throw new Error(`Unknown placeholder: ${name}`);
|
|
208
|
+
}
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check cache
|
|
213
|
+
const cacheKey = this._getCacheKey(name, context);
|
|
214
|
+
if (this.cacheEnabled && config.cacheable && this._cache.has(cacheKey)) {
|
|
215
|
+
return this._cache.get(cacheKey);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Merge contexts
|
|
219
|
+
const mergedContext = { ...this._context, ...config.context, ...context };
|
|
220
|
+
|
|
221
|
+
// Call resolver
|
|
222
|
+
let value;
|
|
223
|
+
try {
|
|
224
|
+
value = config.resolver(mergedContext);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
// Emit error event for logging but don't throw
|
|
227
|
+
if (this.listenerCount('error') > 0) {
|
|
228
|
+
this.emit('error', { name, error });
|
|
229
|
+
}
|
|
230
|
+
value = this._getDefaultValue(config.type);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Custom validation if provided
|
|
234
|
+
if (config.validator) {
|
|
235
|
+
const validation = config.validator(value);
|
|
236
|
+
if (!validation.valid) {
|
|
237
|
+
this.emit('validationFailed', { name, value, error: validation.error });
|
|
238
|
+
value = this._getDefaultValue(config.type);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Security sanitization
|
|
243
|
+
if (config.secure && this.secureByDefault) {
|
|
244
|
+
value = this._sanitize(config.type, value);
|
|
245
|
+
|
|
246
|
+
// Detect injection attempts
|
|
247
|
+
if (typeof value === 'string') {
|
|
248
|
+
const detection = detectInjectionAttempt(value);
|
|
249
|
+
if (!detection.safe) {
|
|
250
|
+
this.emit('injectionAttempt', { name, value, reason: detection.reason });
|
|
251
|
+
value = this._getDefaultValue(config.type);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Cache result
|
|
257
|
+
if (this.cacheEnabled && config.cacheable) {
|
|
258
|
+
this._cache.set(cacheKey, value);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.emit('resolved', { name, value });
|
|
262
|
+
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolve all placeholders in content
|
|
268
|
+
* @param {string} content - Content with placeholders
|
|
269
|
+
* @param {Object} context - Context for resolution
|
|
270
|
+
* @returns {string} Content with placeholders replaced
|
|
271
|
+
*/
|
|
272
|
+
inject(content, context = {}) {
|
|
273
|
+
if (!content || typeof content !== 'string') {
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let result = content;
|
|
278
|
+
|
|
279
|
+
// Replace <!-- {{PLACEHOLDER}} --> format FIRST (more specific pattern)
|
|
280
|
+
result = result.replace(/<!--\s*\{\{([A-Z][A-Z0-9_]*)\}\}\s*-->/g, (match, name) => {
|
|
281
|
+
if (this.has(name)) {
|
|
282
|
+
const value = this.resolve(name, context);
|
|
283
|
+
return this._formatValue(value);
|
|
284
|
+
}
|
|
285
|
+
return match;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Replace {{PLACEHOLDER}} format
|
|
289
|
+
result = result.replace(/\{\{([A-Z][A-Z0-9_]*)\}\}/g, (match, name) => {
|
|
290
|
+
if (this.has(name)) {
|
|
291
|
+
const value = this.resolve(name, context);
|
|
292
|
+
return this._formatValue(value);
|
|
293
|
+
}
|
|
294
|
+
return match; // Keep unresolved
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Replace {placeholder} format (lowercase)
|
|
298
|
+
result = result.replace(/\{([a-z][a-z0-9_-]*)\}/g, (match, name) => {
|
|
299
|
+
if (this.has(name)) {
|
|
300
|
+
const value = this.resolve(name, context);
|
|
301
|
+
return this._formatValue(value);
|
|
302
|
+
}
|
|
303
|
+
return match;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get all registered placeholder names
|
|
311
|
+
* @returns {string[]} Placeholder names
|
|
312
|
+
*/
|
|
313
|
+
getNames() {
|
|
314
|
+
return Array.from(this._resolvers.keys());
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get documentation for all placeholders
|
|
319
|
+
* @returns {Object} Documentation object
|
|
320
|
+
*/
|
|
321
|
+
getDocs() {
|
|
322
|
+
const docs = {};
|
|
323
|
+
|
|
324
|
+
for (const [name, config] of this._resolvers) {
|
|
325
|
+
docs[name] = {
|
|
326
|
+
description: config.description,
|
|
327
|
+
type: config.type,
|
|
328
|
+
source: config.source,
|
|
329
|
+
secure: config.secure,
|
|
330
|
+
cacheable: config.cacheable,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return docs;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Clear resolution cache
|
|
339
|
+
* @param {string} [name] - Specific placeholder to clear (or all if not specified)
|
|
340
|
+
*/
|
|
341
|
+
clearCache(name) {
|
|
342
|
+
if (name) {
|
|
343
|
+
this._cache.delete(name);
|
|
344
|
+
} else {
|
|
345
|
+
this._cache.clear();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Extend registry with a plugin
|
|
351
|
+
* @param {string} pluginName - Plugin identifier
|
|
352
|
+
* @param {Object} plugin - Plugin object with register method
|
|
353
|
+
* @returns {PlaceholderRegistry} this for chaining
|
|
354
|
+
*/
|
|
355
|
+
extend(pluginName, plugin) {
|
|
356
|
+
if (!pluginName || typeof pluginName !== 'string') {
|
|
357
|
+
throw new Error('Plugin name must be a non-empty string');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (this._plugins.has(pluginName)) {
|
|
361
|
+
throw new Error(`Plugin already registered: ${pluginName}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!plugin || typeof plugin.register !== 'function') {
|
|
365
|
+
throw new Error('Plugin must have a register method');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Create sandboxed registry for plugin
|
|
369
|
+
const sandboxedRegister = (name, resolver, config = {}) => {
|
|
370
|
+
// Prefix plugin placeholders to avoid conflicts
|
|
371
|
+
const prefixedName = `${pluginName}_${name}`;
|
|
372
|
+
|
|
373
|
+
// Force plugin source
|
|
374
|
+
const pluginConfig = {
|
|
375
|
+
...config,
|
|
376
|
+
source: 'plugin',
|
|
377
|
+
pluginName,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
return this.register(prefixedName, resolver, pluginConfig);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// Call plugin register method
|
|
384
|
+
plugin.register({
|
|
385
|
+
register: sandboxedRegister,
|
|
386
|
+
context: this._context,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
this._plugins.set(pluginName, plugin);
|
|
390
|
+
|
|
391
|
+
this.emit('pluginLoaded', { pluginName });
|
|
392
|
+
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Remove a plugin and its placeholders
|
|
398
|
+
* @param {string} pluginName - Plugin identifier
|
|
399
|
+
* @returns {boolean} True if removed
|
|
400
|
+
*/
|
|
401
|
+
removePlugin(pluginName) {
|
|
402
|
+
if (!this._plugins.has(pluginName)) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Remove all placeholders from this plugin
|
|
407
|
+
const prefix = `${pluginName}_`;
|
|
408
|
+
for (const name of this._resolvers.keys()) {
|
|
409
|
+
if (name.startsWith(prefix)) {
|
|
410
|
+
this._resolvers.delete(name);
|
|
411
|
+
this._cache.delete(name);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this._plugins.delete(pluginName);
|
|
416
|
+
|
|
417
|
+
this.emit('pluginRemoved', { pluginName });
|
|
418
|
+
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Set global context for all resolvers
|
|
424
|
+
* @param {Object} context - Context object
|
|
425
|
+
*/
|
|
426
|
+
setContext(context) {
|
|
427
|
+
this._context = { ...this._context, ...context };
|
|
428
|
+
this.clearCache(); // Clear cache when context changes
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get default value for type
|
|
433
|
+
*/
|
|
434
|
+
_getDefaultValue(type) {
|
|
435
|
+
const typeConfig = RESOLVER_TYPES[type];
|
|
436
|
+
if (!typeConfig) return '';
|
|
437
|
+
|
|
438
|
+
return typeof typeConfig.defaultValue === 'function'
|
|
439
|
+
? typeConfig.defaultValue()
|
|
440
|
+
: typeConfig.defaultValue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Sanitize value based on type
|
|
445
|
+
*/
|
|
446
|
+
_sanitize(type, value) {
|
|
447
|
+
const typeConfig = RESOLVER_TYPES[type];
|
|
448
|
+
if (!typeConfig || !typeConfig.sanitizer) {
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return typeConfig.sanitizer(value);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Format value for injection
|
|
457
|
+
*/
|
|
458
|
+
_formatValue(value) {
|
|
459
|
+
if (Array.isArray(value)) {
|
|
460
|
+
return value.join('\n');
|
|
461
|
+
}
|
|
462
|
+
return String(value);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get cache key for a placeholder with context
|
|
467
|
+
*/
|
|
468
|
+
_getCacheKey(name, context) {
|
|
469
|
+
// Simple cache key - just placeholder name for now
|
|
470
|
+
// Could be extended to include context hash
|
|
471
|
+
return name;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// =============================================================================
|
|
476
|
+
// Built-in Resolver Modules
|
|
477
|
+
// =============================================================================
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Count resolver - creates resolver for numeric counts
|
|
481
|
+
* @param {Function} countFn - Function that returns count
|
|
482
|
+
* @returns {Function} Resolver function
|
|
483
|
+
*/
|
|
484
|
+
function createCountResolver(countFn) {
|
|
485
|
+
return context => {
|
|
486
|
+
try {
|
|
487
|
+
return countFn(context);
|
|
488
|
+
} catch (e) {
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* List resolver - creates resolver for list generation
|
|
496
|
+
* @param {Function} listFn - Function that returns list content
|
|
497
|
+
* @returns {Function} Resolver function
|
|
498
|
+
*/
|
|
499
|
+
function createListResolver(listFn) {
|
|
500
|
+
return context => {
|
|
501
|
+
try {
|
|
502
|
+
return listFn(context);
|
|
503
|
+
} catch (e) {
|
|
504
|
+
return '';
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Static resolver - creates resolver for static values
|
|
511
|
+
* @param {any} value - Static value
|
|
512
|
+
* @returns {Function} Resolver function
|
|
513
|
+
*/
|
|
514
|
+
function createStaticResolver(value) {
|
|
515
|
+
return () => value;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// =============================================================================
|
|
519
|
+
// Default Registry Factory
|
|
520
|
+
// =============================================================================
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Create a registry with built-in AgileFlow placeholders
|
|
524
|
+
* @param {Object} options - Options
|
|
525
|
+
* @returns {PlaceholderRegistry} Configured registry
|
|
526
|
+
*/
|
|
527
|
+
function createDefaultRegistry(options = {}) {
|
|
528
|
+
const registry = new PlaceholderRegistry(options);
|
|
529
|
+
|
|
530
|
+
// Count placeholders (resolvers provided by caller via context)
|
|
531
|
+
registry.register('COMMAND_COUNT', ctx => ctx.commandCount || 0, {
|
|
532
|
+
type: 'count',
|
|
533
|
+
description: 'Total number of slash commands',
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
registry.register('AGENT_COUNT', ctx => ctx.agentCount || 0, {
|
|
537
|
+
type: 'count',
|
|
538
|
+
description: 'Total number of specialized agents',
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
registry.register('SKILL_COUNT', ctx => ctx.skillCount || 0, {
|
|
542
|
+
type: 'count',
|
|
543
|
+
description: 'Total number of skills',
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Metadata placeholders
|
|
547
|
+
registry.register('VERSION', ctx => ctx.version || 'unknown', {
|
|
548
|
+
type: 'version',
|
|
549
|
+
description: 'AgileFlow version from package.json',
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
registry.register('INSTALL_DATE', () => new Date(), {
|
|
553
|
+
type: 'date',
|
|
554
|
+
description: 'Installation date (YYYY-MM-DD)',
|
|
555
|
+
cacheable: false,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// List placeholders (resolvers provided by caller)
|
|
559
|
+
registry.register('AGENT_LIST', ctx => ctx.agentList || '', {
|
|
560
|
+
type: 'string',
|
|
561
|
+
description: 'Full formatted agent list with details',
|
|
562
|
+
secure: false, // List generation already sanitizes
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
registry.register('COMMAND_LIST', ctx => ctx.commandList || '', {
|
|
566
|
+
type: 'string',
|
|
567
|
+
description: 'Full formatted command list',
|
|
568
|
+
secure: false,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Folder placeholders (lowercase format)
|
|
572
|
+
registry.register('agileflow_folder', ctx => ctx.agileflowFolder || '.agileflow', {
|
|
573
|
+
type: 'folderName',
|
|
574
|
+
description: 'Name of the AgileFlow folder',
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
registry.register('project-root', () => '{project-root}', {
|
|
578
|
+
type: 'string',
|
|
579
|
+
description: 'Project root reference (kept as-is for runtime)',
|
|
580
|
+
secure: false,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
return registry;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Singleton instance
|
|
587
|
+
let _instance = null;
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Get singleton registry instance
|
|
591
|
+
* @param {Object} options - Options
|
|
592
|
+
* @returns {PlaceholderRegistry} Registry instance
|
|
593
|
+
*/
|
|
594
|
+
function getRegistry(options = {}) {
|
|
595
|
+
if (!_instance || options.forceNew) {
|
|
596
|
+
_instance = createDefaultRegistry(options);
|
|
597
|
+
}
|
|
598
|
+
return _instance;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Reset singleton (for testing)
|
|
603
|
+
*/
|
|
604
|
+
function resetRegistry() {
|
|
605
|
+
_instance = null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
module.exports = {
|
|
609
|
+
PlaceholderRegistry,
|
|
610
|
+
RESOLVER_TYPES,
|
|
611
|
+
createCountResolver,
|
|
612
|
+
createListResolver,
|
|
613
|
+
createStaticResolver,
|
|
614
|
+
createDefaultRegistry,
|
|
615
|
+
getRegistry,
|
|
616
|
+
resetRegistry,
|
|
617
|
+
};
|