agileflow 2.89.1 → 2.89.3

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.
@@ -664,7 +664,7 @@ function handleLoop(rootDir) {
664
664
  }
665
665
 
666
666
  // Evaluate discretion conditions
667
- let discretionResults = [];
667
+ const discretionResults = [];
668
668
  if (hasDiscretionConditions) {
669
669
  console.log('');
670
670
  console.log(`${c.blue}Evaluating discretion conditions...${c.reset}`);
@@ -7,7 +7,7 @@
7
7
  const chalk = require('chalk');
8
8
  const path = require('node:path');
9
9
  const fs = require('fs-extra');
10
- const yaml = require('js-yaml');
10
+ const { safeLoad, safeDump } = require('../../../lib/yaml-utils');
11
11
  const { Installer } = require('../installers/core/installer');
12
12
  const { IdeManager } = require('../installers/ide/manager');
13
13
  const { displayLogo, displaySection, success, warning, error, info } = require('../lib/ui');
@@ -194,7 +194,7 @@ async function handleSet(directory, status, manifestPath, key, value) {
194
194
 
195
195
  // Read current manifest
196
196
  const manifestContent = await fs.readFile(manifestPath, 'utf8');
197
- const manifest = yaml.load(manifestContent);
197
+ const manifest = safeLoad(manifestContent);
198
198
 
199
199
  // Track if we need to update IDE configs
200
200
  let needsIdeUpdate = false;
@@ -247,7 +247,7 @@ async function handleSet(directory, status, manifestPath, key, value) {
247
247
  manifest.updated_at = new Date().toISOString();
248
248
 
249
249
  // Write manifest
250
- await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
250
+ await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
251
251
  success('Configuration updated');
252
252
 
253
253
  // Update IDE configs if needed
@@ -21,6 +21,13 @@ const {
21
21
  const { IdeManager } = require('../installers/ide/manager');
22
22
  const { getCurrentVersion } = require('../lib/version-checker');
23
23
  const { ErrorHandler } = require('../lib/error-handler');
24
+ const {
25
+ ErrorCodes,
26
+ getErrorCodeFromError,
27
+ getSuggestedFix,
28
+ isRecoverable,
29
+ } = require('../../../lib/error-codes');
30
+ const { safeDump } = require('../../../lib/yaml-utils');
24
31
 
25
32
  const installer = new Installer();
26
33
 
@@ -88,6 +95,7 @@ module.exports = {
88
95
  issues++;
89
96
  repairs.push({
90
97
  type: 'missing-manifest',
98
+ errorCode: 'ENOENT',
91
99
  message: 'Recreate missing manifest.yaml',
92
100
  fix: async () => {
93
101
  info('Recreating manifest.yaml...');
@@ -95,7 +103,6 @@ module.exports = {
95
103
  const cfgDir = path.join(status.path, '_cfg');
96
104
  await fs.ensureDir(cfgDir);
97
105
 
98
- const yaml = require('js-yaml');
99
106
  const manifest = {
100
107
  version: packageJson.version,
101
108
  installed_at: new Date().toISOString(),
@@ -107,7 +114,7 @@ module.exports = {
107
114
  docs_folder: 'docs',
108
115
  };
109
116
 
110
- await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
117
+ await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
111
118
  success('Created manifest.yaml');
112
119
  },
113
120
  });
@@ -142,6 +149,7 @@ module.exports = {
142
149
  warnings++;
143
150
  repairs.push({
144
151
  type: 'invalid-file-index',
152
+ errorCode: 'EPARSE',
145
153
  message: 'Recreate files.json safe-update index',
146
154
  fix: async () => {
147
155
  await createProtectedFileIndex(status.path, fileIndexPath);
@@ -154,6 +162,7 @@ module.exports = {
154
162
  warnings++;
155
163
  repairs.push({
156
164
  type: 'missing-file-index',
165
+ errorCode: 'ENOENT',
157
166
  message: 'Create files.json safe-update index',
158
167
  fix: async () => {
159
168
  await createProtectedFileIndex(status.path, fileIndexPath);
@@ -196,6 +205,7 @@ module.exports = {
196
205
  if (missingCore) {
197
206
  repairs.push({
198
207
  type: 'missing-core',
208
+ errorCode: 'EEMPTYDIR',
199
209
  message: 'Reinstall missing core content',
200
210
  fix: async () => {
201
211
  info('Reinstalling core content...');
@@ -241,6 +251,7 @@ module.exports = {
241
251
  warnings++;
242
252
  repairs.push({
243
253
  type: 'missing-ide-config',
254
+ errorCode: 'ENODIR',
244
255
  message: `Reinstall ${ideName} configuration`,
245
256
  fix: async () => {
246
257
  info(`Reinstalling ${ideName} configuration...`);
@@ -267,6 +278,7 @@ module.exports = {
267
278
  warnings++;
268
279
  repairs.push({
269
280
  type: 'orphaned-config',
281
+ errorCode: 'ECONFLICT',
270
282
  message: `Remove orphaned ${ideName} configuration`,
271
283
  fix: async () => {
272
284
  info(`Removing orphaned ${ideName} configuration...`);
@@ -291,7 +303,13 @@ module.exports = {
291
303
  try {
292
304
  await repair.fix();
293
305
  } catch (err) {
306
+ // Use error codes for better diagnosis
307
+ const codeData = getErrorCodeFromError(err);
294
308
  error(`Failed to ${repair.message.toLowerCase()}: ${err.message}`);
309
+ if (codeData.code !== 'EUNKNOWN') {
310
+ console.log(chalk.dim(` Error code: ${codeData.code}`));
311
+ console.log(chalk.dim(` Suggestion: ${codeData.suggestedFix}`));
312
+ }
295
313
  }
296
314
  }
297
315
 
@@ -300,6 +318,16 @@ module.exports = {
300
318
  } else if (repairs.length > 0 && !options.fix) {
301
319
  console.log();
302
320
  info(`Found ${repairs.length} fixable issue(s). Run with --fix to auto-repair.`);
321
+
322
+ // Show summary of fixable issues with error codes
323
+ console.log(chalk.bold('\nFixable Issues:'));
324
+ for (const repair of repairs) {
325
+ const errorCode = repair.errorCode || 'ECONFIG';
326
+ const codeData = ErrorCodes[errorCode] || ErrorCodes.ECONFIG;
327
+ console.log(
328
+ ` ${chalk.yellow('!')} ${repair.message} ${chalk.dim(`[${codeData.code}]`)}`
329
+ );
330
+ }
303
331
  }
304
332
 
305
333
  // Print summary
@@ -7,7 +7,7 @@
7
7
  const chalk = require('chalk');
8
8
  const path = require('node:path');
9
9
  const fs = require('fs-extra');
10
- const yaml = require('js-yaml');
10
+ const { safeLoad } = require('../../../lib/yaml-utils');
11
11
  const { Installer } = require('../installers/core/installer');
12
12
  const { displayLogo, displaySection, success, warning, info } = require('../lib/ui');
13
13
  const {
@@ -233,7 +233,7 @@ async function listExperts(agileflowPath) {
233
233
 
234
234
  try {
235
235
  const content = await fs.readFile(expertiseFile, 'utf8');
236
- const parsed = yaml.load(content);
236
+ const parsed = safeLoad(content);
237
237
 
238
238
  experts.push({
239
239
  name: entry.name,
@@ -78,12 +78,12 @@ module.exports = {
78
78
  // Update the manifest to remove this IDE
79
79
  const manifestPath = path.join(status.path, '_cfg', 'manifest.yaml');
80
80
  if (await fs.pathExists(manifestPath)) {
81
- const yaml = require('js-yaml');
81
+ const { safeLoad, safeDump } = require('../../../lib/yaml-utils');
82
82
  const manifestContent = await fs.readFile(manifestPath, 'utf8');
83
- const manifest = yaml.load(manifestContent);
83
+ const manifest = safeLoad(manifestContent);
84
84
  manifest.ides = (manifest.ides || []).filter(ide => ide !== ideName);
85
85
  manifest.updated_at = new Date().toISOString();
86
- await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
86
+ await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
87
87
  success('Updated manifest');
88
88
  }
89
89
 
@@ -8,10 +8,15 @@ const path = require('node:path');
8
8
  const fs = require('fs-extra');
9
9
  const chalk = require('chalk');
10
10
  const ora = require('ora');
11
- const yaml = require('js-yaml');
11
+ const { safeLoad, safeDump } = require('../../../../lib/yaml-utils');
12
12
  const { injectContent } = require('../../lib/content-injector');
13
13
  const { sha256Hex, toPosixPath, safeTimestampForPath } = require('../../lib/utils');
14
14
  const { validatePath, PathValidationError } = require('../../../../lib/validate');
15
+ const {
16
+ createTypedError,
17
+ getErrorCodeFromError,
18
+ attachErrorCode,
19
+ } = require('../../../../lib/error-codes');
15
20
 
16
21
  const TEXT_EXTENSIONS = new Set(['.md', '.yaml', '.yml', '.txt', '.json']);
17
22
 
@@ -188,6 +193,14 @@ class Installer {
188
193
  };
189
194
  } catch (error) {
190
195
  spinner.fail('Installation failed');
196
+
197
+ // Convert to typed error if not already
198
+ if (!error.errorCode) {
199
+ const errorCode = getErrorCodeFromError(error);
200
+ attachErrorCode(error, errorCode.code);
201
+ error.context = { directory, agileflowFolder };
202
+ }
203
+
191
204
  throw error;
192
205
  }
193
206
  }
@@ -490,13 +503,22 @@ class Installer {
490
503
  created_at: new Date().toISOString(),
491
504
  };
492
505
 
493
- await fs.writeFile(configPath, yaml.dump(config), 'utf8');
506
+ await fs.writeFile(configPath, safeDump(config), 'utf8');
494
507
  return;
495
508
  }
496
509
 
497
510
  try {
498
511
  const existingContent = await fs.readFile(configPath, 'utf8');
499
- const loaded = yaml.load(existingContent);
512
+ let loaded;
513
+ try {
514
+ loaded = safeLoad(existingContent);
515
+ } catch (parseErr) {
516
+ // Attach error code for YAML parse errors
517
+ throw createTypedError(`Failed to parse config.yaml: ${parseErr.message}`, 'EPARSE', {
518
+ cause: parseErr,
519
+ context: { configPath },
520
+ });
521
+ }
500
522
  const existing = loaded && typeof loaded === 'object' && !Array.isArray(loaded) ? loaded : {};
501
523
 
502
524
  const next = {
@@ -508,8 +530,13 @@ class Installer {
508
530
  updated_at: new Date().toISOString(),
509
531
  };
510
532
 
511
- await fs.writeFile(configPath, yaml.dump(next), 'utf8');
512
- } catch {
533
+ await fs.writeFile(configPath, safeDump(next), 'utf8');
534
+ } catch (err) {
535
+ // If it's a typed parse error and not forcing, re-throw
536
+ if (err.errorCode === 'EPARSE' && !options.force) {
537
+ throw err;
538
+ }
539
+
513
540
  if (options.force) {
514
541
  const config = {
515
542
  version: packageJson.version,
@@ -519,7 +546,7 @@ class Installer {
519
546
  created_at: new Date().toISOString(),
520
547
  };
521
548
 
522
- await fs.writeFile(configPath, yaml.dump(config), 'utf8');
549
+ await fs.writeFile(configPath, safeDump(config), 'utf8');
523
550
  }
524
551
  }
525
552
  }
@@ -550,13 +577,22 @@ class Installer {
550
577
  docs_folder: docsFolder || 'docs',
551
578
  };
552
579
 
553
- await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
580
+ await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
554
581
  return;
555
582
  }
556
583
 
557
584
  try {
558
585
  const existingContent = await fs.readFile(manifestPath, 'utf8');
559
- const loaded = yaml.load(existingContent);
586
+ let loaded;
587
+ try {
588
+ loaded = safeLoad(existingContent);
589
+ } catch (parseErr) {
590
+ // Attach error code for YAML parse errors
591
+ throw createTypedError(`Failed to parse manifest.yaml: ${parseErr.message}`, 'EPARSE', {
592
+ cause: parseErr,
593
+ context: { manifestPath },
594
+ });
595
+ }
560
596
  const existing = loaded && typeof loaded === 'object' && !Array.isArray(loaded) ? loaded : {};
561
597
 
562
598
  const manifest = {
@@ -571,8 +607,13 @@ class Installer {
571
607
  docs_folder: docsFolder || existing.docs_folder || 'docs',
572
608
  };
573
609
 
574
- await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
575
- } catch {
610
+ await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
611
+ } catch (err) {
612
+ // If it's a typed parse error and not forcing, re-throw
613
+ if (err.errorCode === 'EPARSE' && !options.force) {
614
+ throw err;
615
+ }
616
+
576
617
  if (options.force) {
577
618
  const manifest = {
578
619
  version: packageJson.version,
@@ -585,7 +626,7 @@ class Installer {
585
626
  docs_folder: docsFolder || 'docs',
586
627
  };
587
628
 
588
- await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
629
+ await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
589
630
  }
590
631
  }
591
632
  }
@@ -790,7 +831,16 @@ class Installer {
790
831
  status.path = agileflowDir;
791
832
 
792
833
  const manifestContent = await fs.readFile(manifestPath, 'utf8');
793
- const manifest = yaml.load(manifestContent);
834
+ let manifest;
835
+ try {
836
+ manifest = safeLoad(manifestContent);
837
+ } catch (parseErr) {
838
+ // Attach error code for YAML parse errors
839
+ throw createTypedError(`Failed to parse manifest.yaml: ${parseErr.message}`, 'EPARSE', {
840
+ cause: parseErr,
841
+ context: { manifestPath },
842
+ });
843
+ }
794
844
 
795
845
  status.version = manifest.version;
796
846
  status.ides = manifest.ides || [];
@@ -0,0 +1,238 @@
1
+ /**
2
+ * _interface.js - IDE Handler Interface
3
+ *
4
+ * Defines the formal contract that all IDE handlers must implement.
5
+ * This interface ensures consistency across IDE installers and
6
+ * enables validation at registration time.
7
+ *
8
+ * Usage:
9
+ * const { IdeHandlerInterface, validateHandler } = require('./_interface');
10
+ *
11
+ * class MyIdeSetup extends BaseIdeSetup {
12
+ * // Implement required methods
13
+ * }
14
+ *
15
+ * // In IdeManager.loadHandlers():
16
+ * const validationResult = validateHandler(handler);
17
+ * if (!validationResult.valid) {
18
+ * throw new Error(`Invalid handler: ${validationResult.errors.join(', ')}`);
19
+ * }
20
+ */
21
+
22
+ /**
23
+ * Required methods that every IDE handler must implement
24
+ * @type {Object.<string, {required: boolean, description: string, signature: string}>}
25
+ */
26
+ const REQUIRED_METHODS = {
27
+ setup: {
28
+ required: true,
29
+ description: 'Main setup method to configure the IDE',
30
+ signature:
31
+ 'async setup(projectDir: string, agileflowDir: string, options?: object): Promise<object>',
32
+ },
33
+ cleanup: {
34
+ required: true,
35
+ description: 'Cleanup old IDE configuration',
36
+ signature: 'async cleanup(projectDir: string): Promise<void>',
37
+ },
38
+ detect: {
39
+ required: true,
40
+ description: 'Detect if this IDE is configured in the project',
41
+ signature: 'async detect(projectDir: string): Promise<boolean>',
42
+ },
43
+ };
44
+
45
+ /**
46
+ * Required properties that every IDE handler must have
47
+ * @type {Object.<string, {required: boolean, description: string, type: string}>}
48
+ */
49
+ const REQUIRED_PROPERTIES = {
50
+ name: {
51
+ required: true,
52
+ description: 'Unique identifier for the IDE (lowercase)',
53
+ type: 'string',
54
+ },
55
+ displayName: {
56
+ required: true,
57
+ description: 'Human-readable name for display',
58
+ type: 'string',
59
+ },
60
+ configDir: {
61
+ required: true,
62
+ description: 'Configuration directory name (e.g., ".cursor", ".claude")',
63
+ type: 'string',
64
+ },
65
+ };
66
+
67
+ /**
68
+ * Optional methods that handlers may implement
69
+ * @type {Object.<string, {description: string, signature: string}>}
70
+ */
71
+ const OPTIONAL_METHODS = {
72
+ setAgileflowFolder: {
73
+ description: 'Set the AgileFlow folder name',
74
+ signature: 'setAgileflowFolder(folderName: string): void',
75
+ },
76
+ setDocsFolder: {
77
+ description: 'Set the docs folder name',
78
+ signature: 'setDocsFolder(folderName: string): void',
79
+ },
80
+ };
81
+
82
+ /**
83
+ * Validate that a handler implements all required methods and properties
84
+ * @param {object} handler - IDE handler instance to validate
85
+ * @returns {{valid: boolean, errors: string[], warnings: string[]}}
86
+ */
87
+ function validateHandler(handler) {
88
+ const errors = [];
89
+ const warnings = [];
90
+
91
+ if (!handler) {
92
+ return { valid: false, errors: ['Handler is null or undefined'], warnings: [] };
93
+ }
94
+
95
+ // Check required properties
96
+ for (const [propName, propDef] of Object.entries(REQUIRED_PROPERTIES)) {
97
+ if (propDef.required) {
98
+ const value = handler[propName];
99
+
100
+ if (value === undefined || value === null) {
101
+ errors.push(`Missing required property: ${propName} (${propDef.description})`);
102
+ } else if (propDef.type && typeof value !== propDef.type) {
103
+ errors.push(`Property ${propName} must be of type ${propDef.type}, got ${typeof value}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ // Check required methods
109
+ for (const [methodName, methodDef] of Object.entries(REQUIRED_METHODS)) {
110
+ if (methodDef.required) {
111
+ const method = handler[methodName];
112
+
113
+ if (typeof method !== 'function') {
114
+ errors.push(`Missing required method: ${methodName}() - ${methodDef.description}`);
115
+ }
116
+ }
117
+ }
118
+
119
+ // Check for optional methods (just warnings)
120
+ for (const [methodName, methodDef] of Object.entries(OPTIONAL_METHODS)) {
121
+ if (typeof handler[methodName] !== 'function') {
122
+ warnings.push(`Optional method not implemented: ${methodName}() - ${methodDef.description}`);
123
+ }
124
+ }
125
+
126
+ return {
127
+ valid: errors.length === 0,
128
+ errors,
129
+ warnings,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Get a summary of the interface requirements
135
+ * @returns {string} Human-readable summary
136
+ */
137
+ function getInterfaceSummary() {
138
+ const lines = ['IDE Handler Interface Requirements:', ''];
139
+
140
+ lines.push('Required Properties:');
141
+ for (const [name, def] of Object.entries(REQUIRED_PROPERTIES)) {
142
+ lines.push(` - ${name}: ${def.type} - ${def.description}`);
143
+ }
144
+
145
+ lines.push('');
146
+ lines.push('Required Methods:');
147
+ for (const [name, def] of Object.entries(REQUIRED_METHODS)) {
148
+ lines.push(` - ${name}()`);
149
+ lines.push(` Signature: ${def.signature}`);
150
+ lines.push(` Purpose: ${def.description}`);
151
+ lines.push('');
152
+ }
153
+
154
+ lines.push('Optional Methods:');
155
+ for (const [name, def] of Object.entries(OPTIONAL_METHODS)) {
156
+ lines.push(` - ${name}(): ${def.description}`);
157
+ }
158
+
159
+ return lines.join('\n');
160
+ }
161
+
162
+ /**
163
+ * IDE Handler Interface - Abstract base that defines the contract
164
+ * This is for documentation purposes; actual handlers extend BaseIdeSetup
165
+ */
166
+ class IdeHandlerInterface {
167
+ /**
168
+ * @param {string} name - Unique identifier (lowercase)
169
+ * @param {string} displayName - Human-readable name
170
+ * @param {string} configDir - Configuration directory name
171
+ */
172
+ constructor(name, displayName, configDir) {
173
+ if (this.constructor === IdeHandlerInterface) {
174
+ throw new Error('IdeHandlerInterface is abstract and cannot be instantiated directly');
175
+ }
176
+
177
+ this.name = name;
178
+ this.displayName = displayName;
179
+ this.configDir = configDir;
180
+ }
181
+
182
+ /**
183
+ * Main setup method - MUST be implemented
184
+ * @param {string} projectDir - Project directory
185
+ * @param {string} agileflowDir - AgileFlow installation directory
186
+ * @param {Object} [options] - Setup options
187
+ * @returns {Promise<{success: boolean, commands?: number, agents?: number}>}
188
+ * @abstract
189
+ */
190
+ async setup(projectDir, agileflowDir, options = {}) {
191
+ throw new Error('setup() must be implemented by subclass');
192
+ }
193
+
194
+ /**
195
+ * Cleanup IDE configuration - MUST be implemented
196
+ * @param {string} projectDir - Project directory
197
+ * @returns {Promise<void>}
198
+ * @abstract
199
+ */
200
+ async cleanup(projectDir) {
201
+ throw new Error('cleanup() must be implemented by subclass');
202
+ }
203
+
204
+ /**
205
+ * Detect if IDE is configured - MUST be implemented
206
+ * @param {string} projectDir - Project directory
207
+ * @returns {Promise<boolean>}
208
+ * @abstract
209
+ */
210
+ async detect(projectDir) {
211
+ throw new Error('detect() must be implemented by subclass');
212
+ }
213
+
214
+ /**
215
+ * Set AgileFlow folder name - OPTIONAL
216
+ * @param {string} folderName - Folder name
217
+ */
218
+ setAgileflowFolder(folderName) {
219
+ // Optional - implement if needed
220
+ }
221
+
222
+ /**
223
+ * Set docs folder name - OPTIONAL
224
+ * @param {string} folderName - Folder name
225
+ */
226
+ setDocsFolder(folderName) {
227
+ // Optional - implement if needed
228
+ }
229
+ }
230
+
231
+ module.exports = {
232
+ IdeHandlerInterface,
233
+ validateHandler,
234
+ getInterfaceSummary,
235
+ REQUIRED_METHODS,
236
+ REQUIRED_PROPERTIES,
237
+ OPTIONAL_METHODS,
238
+ };
@@ -14,7 +14,7 @@ const path = require('node:path');
14
14
  const os = require('node:os');
15
15
  const fs = require('fs-extra');
16
16
  const chalk = require('chalk');
17
- const yaml = require('js-yaml');
17
+ const { safeLoad, yaml } = require('../../../../lib/yaml-utils');
18
18
  const { BaseIdeSetup } = require('./_base-ide');
19
19
  const { parseFrontmatter } = require('../../../../scripts/lib/frontmatter-parser');
20
20
 
@@ -120,7 +120,7 @@ ${codexHeader}${bodyContent}`;
120
120
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
121
121
  if (frontmatterMatch) {
122
122
  try {
123
- const frontmatter = yaml.load(frontmatterMatch[1]);
123
+ const frontmatter = safeLoad(frontmatterMatch[1]);
124
124
  if (frontmatter.description) {
125
125
  description = frontmatter.description;
126
126
  }
@@ -2,11 +2,13 @@
2
2
  * AgileFlow CLI - IDE Manager
3
3
  *
4
4
  * Manages IDE-specific installers and configuration.
5
+ * Validates handlers implement the required interface before registration.
5
6
  */
6
7
 
7
8
  const fs = require('fs-extra');
8
9
  const path = require('node:path');
9
10
  const chalk = require('chalk');
11
+ const { validateHandler } = require('./_interface');
10
12
 
11
13
  /**
12
14
  * IDE Manager - handles IDE-specific setup
@@ -68,6 +70,19 @@ class IdeManager {
68
70
 
69
71
  if (HandlerClass && typeof HandlerClass === 'function') {
70
72
  const instance = new HandlerClass();
73
+
74
+ // Validate handler implements required interface
75
+ const validation = validateHandler(instance);
76
+
77
+ if (!validation.valid) {
78
+ console.log(
79
+ chalk.yellow(
80
+ ` Warning: IDE handler ${file} failed validation: ${validation.errors.join(', ')}`
81
+ )
82
+ );
83
+ continue; // Skip invalid handlers
84
+ }
85
+
71
86
  if (instance.name) {
72
87
  this.handlers.set(instance.name, instance);
73
88
  }