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.
Files changed (37) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +3 -3
  3. package/lib/placeholder-registry.js +617 -0
  4. package/lib/smart-json-file.js +205 -1
  5. package/lib/table-formatter.js +504 -0
  6. package/lib/transient-status.js +374 -0
  7. package/lib/ui-manager.js +612 -0
  8. package/lib/validate-args.js +213 -0
  9. package/lib/validate-names.js +143 -0
  10. package/lib/validate-paths.js +434 -0
  11. package/lib/validate.js +37 -737
  12. package/package.json +4 -1
  13. package/scripts/check-update.js +16 -3
  14. package/scripts/lib/sessionRegistry.js +682 -0
  15. package/scripts/session-manager.js +77 -10
  16. package/scripts/tui/App.js +176 -0
  17. package/scripts/tui/index.js +75 -0
  18. package/scripts/tui/lib/crashRecovery.js +302 -0
  19. package/scripts/tui/lib/eventStream.js +316 -0
  20. package/scripts/tui/lib/keyboard.js +252 -0
  21. package/scripts/tui/lib/loopControl.js +371 -0
  22. package/scripts/tui/panels/OutputPanel.js +278 -0
  23. package/scripts/tui/panels/SessionPanel.js +178 -0
  24. package/scripts/tui/panels/TracePanel.js +333 -0
  25. package/src/core/commands/tui.md +91 -0
  26. package/tools/cli/commands/config.js +7 -30
  27. package/tools/cli/commands/doctor.js +18 -38
  28. package/tools/cli/commands/list.js +47 -35
  29. package/tools/cli/commands/status.js +13 -37
  30. package/tools/cli/commands/uninstall.js +9 -38
  31. package/tools/cli/installers/core/installer.js +13 -0
  32. package/tools/cli/lib/command-context.js +374 -0
  33. package/tools/cli/lib/config-manager.js +394 -0
  34. package/tools/cli/lib/ide-registry.js +186 -0
  35. package/tools/cli/lib/npm-utils.js +16 -3
  36. package/tools/cli/lib/self-update.js +148 -0
  37. package/tools/cli/lib/validation-middleware.js +491 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.90.0] - 2026-01-16
11
+
12
+ ### Added
13
+ - TUI dashboard with session management and modular CLI architecture
14
+
10
15
  ## [2.89.3] - 2026-01-14
11
16
 
12
17
  ### Added
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/agileflow?color=brightgreen)](https://www.npmjs.com/package/agileflow)
6
- [![Commands](https://img.shields.io/badge/commands-72-blue)](docs/04-architecture/commands.md)
6
+ [![Commands](https://img.shields.io/badge/commands-73-blue)](docs/04-architecture/commands.md)
7
7
  [![Agents/Experts](https://img.shields.io/badge/agents%2Fexperts-29-orange)](docs/04-architecture/subagents.md)
8
8
  [![Skills](https://img.shields.io/badge/skills-dynamic-purple)](docs/04-architecture/skills.md)
9
9
 
@@ -65,7 +65,7 @@ AgileFlow combines three proven methodologies:
65
65
 
66
66
  | Component | Count | Description |
67
67
  |-----------|-------|-------------|
68
- | [Commands](docs/04-architecture/commands.md) | 72 | Slash commands for agile workflows |
68
+ | [Commands](docs/04-architecture/commands.md) | 73 | Slash commands for agile workflows |
69
69
  | [Agents/Experts](docs/04-architecture/subagents.md) | 29 | Specialized agents with self-improving knowledge bases |
70
70
  | [Skills](docs/04-architecture/skills.md) | Dynamic | Generated on-demand with `/agileflow:skill:create` |
71
71
 
@@ -76,7 +76,7 @@ AgileFlow combines three proven methodologies:
76
76
  Full documentation lives in [`docs/04-architecture/`](docs/04-architecture/):
77
77
 
78
78
  ### Reference
79
- - [Commands](docs/04-architecture/commands.md) - All 72 slash commands
79
+ - [Commands](docs/04-architecture/commands.md) - All 73 slash commands
80
80
  - [Agents/Experts](docs/04-architecture/subagents.md) - 29 specialized agents with self-improving knowledge
81
81
  - [Skills](docs/04-architecture/skills.md) - Dynamic skill generator with MCP integration
82
82
 
@@ -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
+ };