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.
Files changed (57) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/lib/content-sanitizer.js +463 -0
  4. package/lib/error-codes.js +544 -0
  5. package/lib/errors.js +336 -5
  6. package/lib/feedback.js +561 -0
  7. package/lib/path-resolver.js +396 -0
  8. package/lib/placeholder-registry.js +617 -0
  9. package/lib/session-registry.js +461 -0
  10. package/lib/smart-json-file.js +653 -0
  11. package/lib/table-formatter.js +504 -0
  12. package/lib/transient-status.js +374 -0
  13. package/lib/ui-manager.js +612 -0
  14. package/lib/validate-args.js +213 -0
  15. package/lib/validate-names.js +143 -0
  16. package/lib/validate-paths.js +434 -0
  17. package/lib/validate.js +38 -584
  18. package/package.json +4 -1
  19. package/scripts/agileflow-configure.js +40 -1440
  20. package/scripts/agileflow-welcome.js +2 -1
  21. package/scripts/check-update.js +16 -3
  22. package/scripts/lib/configure-detect.js +383 -0
  23. package/scripts/lib/configure-features.js +811 -0
  24. package/scripts/lib/configure-repair.js +314 -0
  25. package/scripts/lib/configure-utils.js +115 -0
  26. package/scripts/lib/frontmatter-parser.js +3 -3
  27. package/scripts/lib/sessionRegistry.js +682 -0
  28. package/scripts/obtain-context.js +417 -113
  29. package/scripts/ralph-loop.js +1 -1
  30. package/scripts/session-manager.js +77 -10
  31. package/scripts/tui/App.js +176 -0
  32. package/scripts/tui/index.js +75 -0
  33. package/scripts/tui/lib/crashRecovery.js +302 -0
  34. package/scripts/tui/lib/eventStream.js +316 -0
  35. package/scripts/tui/lib/keyboard.js +252 -0
  36. package/scripts/tui/lib/loopControl.js +371 -0
  37. package/scripts/tui/panels/OutputPanel.js +278 -0
  38. package/scripts/tui/panels/SessionPanel.js +178 -0
  39. package/scripts/tui/panels/TracePanel.js +333 -0
  40. package/src/core/commands/tui.md +91 -0
  41. package/tools/cli/commands/config.js +10 -33
  42. package/tools/cli/commands/doctor.js +48 -40
  43. package/tools/cli/commands/list.js +49 -37
  44. package/tools/cli/commands/status.js +13 -37
  45. package/tools/cli/commands/uninstall.js +12 -41
  46. package/tools/cli/installers/core/installer.js +75 -12
  47. package/tools/cli/installers/ide/_interface.js +238 -0
  48. package/tools/cli/installers/ide/codex.js +2 -2
  49. package/tools/cli/installers/ide/manager.js +15 -0
  50. package/tools/cli/lib/command-context.js +374 -0
  51. package/tools/cli/lib/config-manager.js +394 -0
  52. package/tools/cli/lib/content-injector.js +69 -16
  53. package/tools/cli/lib/ide-errors.js +163 -29
  54. package/tools/cli/lib/ide-registry.js +186 -0
  55. package/tools/cli/lib/npm-utils.js +16 -3
  56. package/tools/cli/lib/self-update.js +148 -0
  57. 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 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');
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, yaml.dump(config), 'utf8');
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
- const loaded = yaml.load(existingContent);
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, yaml.dump(next), 'utf8');
512
- } catch {
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, yaml.dump(config), 'utf8');
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, yaml.dump(manifest), 'utf8');
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
- const loaded = yaml.load(existingContent);
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, yaml.dump(manifest), 'utf8');
575
- } catch {
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, yaml.dump(manifest), 'utf8');
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
- const manifest = yaml.load(manifestContent);
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('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
  }