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.
- package/CHANGELOG.md +10 -0
- 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/session-registry.js +461 -0
- package/lib/smart-json-file.js +449 -0
- package/lib/validate.js +165 -11
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- 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/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- package/tools/cli/commands/config.js +3 -3
- package/tools/cli/commands/doctor.js +30 -2
- package/tools/cli/commands/list.js +2 -2
- package/tools/cli/commands/uninstall.js +3 -3
- package/tools/cli/installers/core/installer.js +62 -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/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
package/scripts/ralph-loop.js
CHANGED
|
@@ -664,7 +664,7 @@ function handleLoop(rootDir) {
|
|
|
664
664
|
}
|
|
665
665
|
|
|
666
666
|
// Evaluate discretion conditions
|
|
667
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
|
81
|
+
const { safeLoad, safeDump } = require('../../../lib/yaml-utils');
|
|
82
82
|
const manifestContent = await fs.readFile(manifestPath, 'utf8');
|
|
83
|
-
const manifest =
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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('
|
|
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
|
}
|