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
|
@@ -8,10 +8,16 @@ 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
|
|
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');
|
|
20
|
+
const { setSecurePermissions, SECURE_FILE_MODE } = require('../../../../lib/smart-json-file');
|
|
15
21
|
|
|
16
22
|
const TEXT_EXTENSIONS = new Set(['.md', '.yaml', '.yml', '.txt', '.json']);
|
|
17
23
|
|
|
@@ -188,6 +194,14 @@ class Installer {
|
|
|
188
194
|
};
|
|
189
195
|
} catch (error) {
|
|
190
196
|
spinner.fail('Installation failed');
|
|
197
|
+
|
|
198
|
+
// Convert to typed error if not already
|
|
199
|
+
if (!error.errorCode) {
|
|
200
|
+
const errorCode = getErrorCodeFromError(error);
|
|
201
|
+
attachErrorCode(error, errorCode.code);
|
|
202
|
+
error.context = { directory, agileflowFolder };
|
|
203
|
+
}
|
|
204
|
+
|
|
191
205
|
throw error;
|
|
192
206
|
}
|
|
193
207
|
}
|
|
@@ -490,13 +504,24 @@ class Installer {
|
|
|
490
504
|
created_at: new Date().toISOString(),
|
|
491
505
|
};
|
|
492
506
|
|
|
493
|
-
await fs.writeFile(configPath,
|
|
507
|
+
await fs.writeFile(configPath, safeDump(config), 'utf8');
|
|
508
|
+
// Security: Set secure permissions (0o600) on config file
|
|
509
|
+
setSecurePermissions(configPath);
|
|
494
510
|
return;
|
|
495
511
|
}
|
|
496
512
|
|
|
497
513
|
try {
|
|
498
514
|
const existingContent = await fs.readFile(configPath, 'utf8');
|
|
499
|
-
|
|
515
|
+
let loaded;
|
|
516
|
+
try {
|
|
517
|
+
loaded = safeLoad(existingContent);
|
|
518
|
+
} catch (parseErr) {
|
|
519
|
+
// Attach error code for YAML parse errors
|
|
520
|
+
throw createTypedError(`Failed to parse config.yaml: ${parseErr.message}`, 'EPARSE', {
|
|
521
|
+
cause: parseErr,
|
|
522
|
+
context: { configPath },
|
|
523
|
+
});
|
|
524
|
+
}
|
|
500
525
|
const existing = loaded && typeof loaded === 'object' && !Array.isArray(loaded) ? loaded : {};
|
|
501
526
|
|
|
502
527
|
const next = {
|
|
@@ -508,8 +533,15 @@ class Installer {
|
|
|
508
533
|
updated_at: new Date().toISOString(),
|
|
509
534
|
};
|
|
510
535
|
|
|
511
|
-
await fs.writeFile(configPath,
|
|
512
|
-
|
|
536
|
+
await fs.writeFile(configPath, safeDump(next), 'utf8');
|
|
537
|
+
// Security: Set secure permissions (0o600) on config file
|
|
538
|
+
setSecurePermissions(configPath);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
// If it's a typed parse error and not forcing, re-throw
|
|
541
|
+
if (err.errorCode === 'EPARSE' && !options.force) {
|
|
542
|
+
throw err;
|
|
543
|
+
}
|
|
544
|
+
|
|
513
545
|
if (options.force) {
|
|
514
546
|
const config = {
|
|
515
547
|
version: packageJson.version,
|
|
@@ -519,7 +551,9 @@ class Installer {
|
|
|
519
551
|
created_at: new Date().toISOString(),
|
|
520
552
|
};
|
|
521
553
|
|
|
522
|
-
await fs.writeFile(configPath,
|
|
554
|
+
await fs.writeFile(configPath, safeDump(config), 'utf8');
|
|
555
|
+
// Security: Set secure permissions (0o600) on config file
|
|
556
|
+
setSecurePermissions(configPath);
|
|
523
557
|
}
|
|
524
558
|
}
|
|
525
559
|
}
|
|
@@ -550,13 +584,24 @@ class Installer {
|
|
|
550
584
|
docs_folder: docsFolder || 'docs',
|
|
551
585
|
};
|
|
552
586
|
|
|
553
|
-
await fs.writeFile(manifestPath,
|
|
587
|
+
await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
|
|
588
|
+
// Security: Set secure permissions (0o600) on manifest file
|
|
589
|
+
setSecurePermissions(manifestPath);
|
|
554
590
|
return;
|
|
555
591
|
}
|
|
556
592
|
|
|
557
593
|
try {
|
|
558
594
|
const existingContent = await fs.readFile(manifestPath, 'utf8');
|
|
559
|
-
|
|
595
|
+
let loaded;
|
|
596
|
+
try {
|
|
597
|
+
loaded = safeLoad(existingContent);
|
|
598
|
+
} catch (parseErr) {
|
|
599
|
+
// Attach error code for YAML parse errors
|
|
600
|
+
throw createTypedError(`Failed to parse manifest.yaml: ${parseErr.message}`, 'EPARSE', {
|
|
601
|
+
cause: parseErr,
|
|
602
|
+
context: { manifestPath },
|
|
603
|
+
});
|
|
604
|
+
}
|
|
560
605
|
const existing = loaded && typeof loaded === 'object' && !Array.isArray(loaded) ? loaded : {};
|
|
561
606
|
|
|
562
607
|
const manifest = {
|
|
@@ -571,8 +616,15 @@ class Installer {
|
|
|
571
616
|
docs_folder: docsFolder || existing.docs_folder || 'docs',
|
|
572
617
|
};
|
|
573
618
|
|
|
574
|
-
await fs.writeFile(manifestPath,
|
|
575
|
-
|
|
619
|
+
await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
|
|
620
|
+
// Security: Set secure permissions (0o600) on manifest file
|
|
621
|
+
setSecurePermissions(manifestPath);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
// If it's a typed parse error and not forcing, re-throw
|
|
624
|
+
if (err.errorCode === 'EPARSE' && !options.force) {
|
|
625
|
+
throw err;
|
|
626
|
+
}
|
|
627
|
+
|
|
576
628
|
if (options.force) {
|
|
577
629
|
const manifest = {
|
|
578
630
|
version: packageJson.version,
|
|
@@ -585,7 +637,9 @@ class Installer {
|
|
|
585
637
|
docs_folder: docsFolder || 'docs',
|
|
586
638
|
};
|
|
587
639
|
|
|
588
|
-
await fs.writeFile(manifestPath,
|
|
640
|
+
await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
|
|
641
|
+
// Security: Set secure permissions (0o600) on manifest file
|
|
642
|
+
setSecurePermissions(manifestPath);
|
|
589
643
|
}
|
|
590
644
|
}
|
|
591
645
|
}
|
|
@@ -790,7 +844,16 @@ class Installer {
|
|
|
790
844
|
status.path = agileflowDir;
|
|
791
845
|
|
|
792
846
|
const manifestContent = await fs.readFile(manifestPath, 'utf8');
|
|
793
|
-
|
|
847
|
+
let manifest;
|
|
848
|
+
try {
|
|
849
|
+
manifest = safeLoad(manifestContent);
|
|
850
|
+
} catch (parseErr) {
|
|
851
|
+
// Attach error code for YAML parse errors
|
|
852
|
+
throw createTypedError(`Failed to parse manifest.yaml: ${parseErr.message}`, 'EPARSE', {
|
|
853
|
+
cause: parseErr,
|
|
854
|
+
context: { manifestPath },
|
|
855
|
+
});
|
|
856
|
+
}
|
|
794
857
|
|
|
795
858
|
status.version = manifest.version;
|
|
796
859
|
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('
|
|
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 =
|
|
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
|
}
|