agileflow 2.91.0 → 2.92.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 +5 -0
- package/README.md +3 -3
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +31 -23
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate.js +116 -52
- package/package.json +1 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +435 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +43 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +80 -1248
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +23 -10
- package/scripts/query-codebase.js +122 -14
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +220 -42
- package/scripts/spawn-parallel.js +651 -0
- package/scripts/tui/blessed/data/watcher.js +20 -15
- package/scripts/tui/blessed/index.js +2 -2
- package/scripts/tui/blessed/panels/output.js +14 -8
- package/scripts/tui/blessed/panels/sessions.js +22 -15
- package/scripts/tui/blessed/panels/trace.js +14 -8
- package/scripts/tui/blessed/ui/help.js +3 -3
- package/scripts/tui/blessed/ui/screen.js +4 -4
- package/scripts/tui/blessed/ui/statusbar.js +5 -9
- package/scripts/tui/blessed/ui/tabbar.js +11 -11
- package/scripts/validators/component-validator.js +41 -14
- package/scripts/validators/json-schema-validator.js +11 -4
- package/scripts/validators/markdown-validator.js +1 -2
- package/scripts/validators/migration-validator.js +17 -5
- package/scripts/validators/security-validator.js +137 -33
- package/scripts/validators/story-format-validator.js +31 -10
- package/scripts/validators/test-result-validator.js +19 -4
- package/scripts/validators/workflow-validator.js +12 -5
- package/src/core/agents/codebase-query.md +24 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +113 -0
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +75 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +113 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +74 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +86 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +114 -1
- package/tools/cli/lib/ui.js +14 -25
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry Dependency Injection Module
|
|
3
|
+
*
|
|
4
|
+
* Provides dependency injection patterns for registry classes to enable:
|
|
5
|
+
* - Easier testing with mock dependencies
|
|
6
|
+
* - Configuration without modifying code
|
|
7
|
+
* - Clear separation of concerns
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const { createContainer, createPlaceholderRegistry } = require('./registry-di');
|
|
11
|
+
*
|
|
12
|
+
* // Create a container with custom dependencies
|
|
13
|
+
* const container = createContainer({
|
|
14
|
+
* sanitizer: mockSanitizer,
|
|
15
|
+
* fs: mockFs,
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // Create registry using container
|
|
19
|
+
* const registry = createPlaceholderRegistry(container);
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
// Default dependencies
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} IDependencyContainer
|
|
30
|
+
* @property {Object} fs - File system module
|
|
31
|
+
* @property {Object} path - Path module
|
|
32
|
+
* @property {Object} sanitizer - Content sanitizer module
|
|
33
|
+
* @property {Object} [logger] - Optional logger interface
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} ISanitizer
|
|
38
|
+
* @property {Function} sanitize.count - Sanitize count values
|
|
39
|
+
* @property {Function} sanitize.description - Sanitize description strings
|
|
40
|
+
* @property {Function} sanitize.date - Sanitize date values
|
|
41
|
+
* @property {Function} sanitize.version - Sanitize version strings
|
|
42
|
+
* @property {Function} sanitize.folderName - Sanitize folder names
|
|
43
|
+
* @property {Function} validatePlaceholderValue - Validate placeholder values
|
|
44
|
+
* @property {Function} detectInjectionAttempt - Detect injection attempts
|
|
45
|
+
* @property {Function} escapeMarkdown - Escape markdown characters
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} ILogger
|
|
50
|
+
* @property {Function} debug - Debug level logging
|
|
51
|
+
* @property {Function} info - Info level logging
|
|
52
|
+
* @property {Function} warn - Warning level logging
|
|
53
|
+
* @property {Function} error - Error level logging
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Default no-op logger
|
|
58
|
+
*/
|
|
59
|
+
const noopLogger = {
|
|
60
|
+
debug: () => {},
|
|
61
|
+
info: () => {},
|
|
62
|
+
warn: () => {},
|
|
63
|
+
error: () => {},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Console logger adapter
|
|
68
|
+
*/
|
|
69
|
+
const consoleLogger = {
|
|
70
|
+
debug: (...args) => console.debug('[Registry]', ...args),
|
|
71
|
+
info: (...args) => console.log('[Registry]', ...args),
|
|
72
|
+
warn: (...args) => console.warn('[Registry]', ...args),
|
|
73
|
+
error: (...args) => console.error('[Registry]', ...args),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Load default sanitizer module
|
|
78
|
+
* @returns {ISanitizer}
|
|
79
|
+
*/
|
|
80
|
+
function loadDefaultSanitizer() {
|
|
81
|
+
try {
|
|
82
|
+
return require('./content-sanitizer');
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// Return stub sanitizer if module not found
|
|
85
|
+
return {
|
|
86
|
+
sanitize: {
|
|
87
|
+
count: v => (typeof v === 'number' ? Math.max(0, Math.floor(v)) : 0),
|
|
88
|
+
description: v => String(v || ''),
|
|
89
|
+
date: v => (v instanceof Date ? v.toISOString().split('T')[0] : String(v || '')),
|
|
90
|
+
version: v => String(v || 'unknown'),
|
|
91
|
+
folderName: v => String(v || '.agileflow'),
|
|
92
|
+
},
|
|
93
|
+
validatePlaceholderValue: () => ({ valid: true }),
|
|
94
|
+
detectInjectionAttempt: () => ({ safe: true }),
|
|
95
|
+
escapeMarkdown: v => String(v || ''),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a dependency container with defaults
|
|
102
|
+
* @param {Partial<IDependencyContainer>} [overrides={}] - Override specific dependencies
|
|
103
|
+
* @returns {IDependencyContainer}
|
|
104
|
+
*/
|
|
105
|
+
function createContainer(overrides = {}) {
|
|
106
|
+
const defaultSanitizer = loadDefaultSanitizer();
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
fs: overrides.fs || fs,
|
|
110
|
+
path: overrides.path || path,
|
|
111
|
+
sanitizer: overrides.sanitizer || defaultSanitizer,
|
|
112
|
+
logger: overrides.logger || noopLogger,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a test container with mock-friendly defaults
|
|
118
|
+
* @param {Partial<IDependencyContainer>} [overrides={}] - Override specific dependencies
|
|
119
|
+
* @returns {IDependencyContainer}
|
|
120
|
+
*/
|
|
121
|
+
function createTestContainer(overrides = {}) {
|
|
122
|
+
const mockFs = {
|
|
123
|
+
readdirSync: () => [],
|
|
124
|
+
readFileSync: () => '',
|
|
125
|
+
existsSync: () => false,
|
|
126
|
+
writeFileSync: () => {},
|
|
127
|
+
mkdirSync: () => {},
|
|
128
|
+
...overrides.fs,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const mockSanitizer = {
|
|
132
|
+
sanitize: {
|
|
133
|
+
count: v => v,
|
|
134
|
+
description: v => v,
|
|
135
|
+
date: v => v,
|
|
136
|
+
version: v => v,
|
|
137
|
+
folderName: v => v,
|
|
138
|
+
},
|
|
139
|
+
validatePlaceholderValue: () => ({ valid: true }),
|
|
140
|
+
detectInjectionAttempt: () => ({ safe: true }),
|
|
141
|
+
escapeMarkdown: v => v,
|
|
142
|
+
...overrides.sanitizer,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return createContainer({
|
|
146
|
+
fs: mockFs,
|
|
147
|
+
sanitizer: mockSanitizer,
|
|
148
|
+
logger: overrides.logger || noopLogger,
|
|
149
|
+
...overrides,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Registry factory configuration
|
|
155
|
+
* @typedef {Object} RegistryFactoryConfig
|
|
156
|
+
* @property {Object} [options] - Registry options
|
|
157
|
+
* @property {IDependencyContainer} [container] - Dependency container
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create PlaceholderRegistry with injected dependencies
|
|
162
|
+
* @param {IDependencyContainer} container - Dependency container
|
|
163
|
+
* @param {Object} [options={}] - Registry options
|
|
164
|
+
* @returns {PlaceholderRegistry}
|
|
165
|
+
*/
|
|
166
|
+
function createPlaceholderRegistry(container, options = {}) {
|
|
167
|
+
const { PlaceholderRegistry } = require('./placeholder-registry');
|
|
168
|
+
|
|
169
|
+
// Create registry with injected container
|
|
170
|
+
const registry = new PlaceholderRegistry({
|
|
171
|
+
...options,
|
|
172
|
+
// Inject dependencies via context
|
|
173
|
+
_container: container,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return registry;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Scan registry factory - creates scanner functions with injected dependencies
|
|
181
|
+
* @param {IDependencyContainer} container - Dependency container
|
|
182
|
+
* @returns {Object} Scanner functions
|
|
183
|
+
*/
|
|
184
|
+
function createScannerFactory(container) {
|
|
185
|
+
const { fs: fsModule, path: pathModule, logger } = container;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generic file scanner
|
|
189
|
+
* @param {string} directory - Directory to scan
|
|
190
|
+
* @param {string} extension - File extension (e.g., '.md')
|
|
191
|
+
* @param {Function} parser - Frontmatter parser function
|
|
192
|
+
* @returns {Array} Parsed items
|
|
193
|
+
*/
|
|
194
|
+
function scanDirectory(directory, extension, parser) {
|
|
195
|
+
logger.debug(`Scanning directory: ${directory}`);
|
|
196
|
+
|
|
197
|
+
if (!fsModule.existsSync(directory)) {
|
|
198
|
+
logger.warn(`Directory not found: ${directory}`);
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let files;
|
|
203
|
+
try {
|
|
204
|
+
files = fsModule.readdirSync(directory);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
logger.error(`Failed to read directory: ${err.message}`);
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const items = [];
|
|
211
|
+
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
if (!file.endsWith(extension)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const filePath = pathModule.join(directory, file);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const content = fsModule.readFileSync(filePath, 'utf8');
|
|
221
|
+
const parsed = parser(content, file, filePath);
|
|
222
|
+
|
|
223
|
+
if (parsed) {
|
|
224
|
+
items.push(parsed);
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
logger.warn(`Failed to parse ${file}: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
logger.debug(`Found ${items.length} items`);
|
|
232
|
+
return items;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
scanDirectory,
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Scan for command files
|
|
240
|
+
* @param {string} commandsDir - Commands directory path
|
|
241
|
+
* @param {Function} extractFrontmatter - Frontmatter extractor
|
|
242
|
+
* @returns {Array} Command metadata
|
|
243
|
+
*/
|
|
244
|
+
scanCommands(commandsDir, extractFrontmatter) {
|
|
245
|
+
return scanDirectory(commandsDir, '.md', (content, file, filePath) => {
|
|
246
|
+
const frontmatter = extractFrontmatter(filePath);
|
|
247
|
+
if (Object.keys(frontmatter).length === 0) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const name = file.replace('.md', '');
|
|
252
|
+
return {
|
|
253
|
+
name,
|
|
254
|
+
file,
|
|
255
|
+
path: filePath,
|
|
256
|
+
description: frontmatter.description || '',
|
|
257
|
+
argumentHint: frontmatter['argument-hint'] || '',
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Scan for agent files
|
|
264
|
+
* @param {string} agentsDir - Agents directory path
|
|
265
|
+
* @param {Function} extractFrontmatter - Frontmatter extractor
|
|
266
|
+
* @param {Function} normalizeTools - Tools normalizer
|
|
267
|
+
* @returns {Array} Agent metadata
|
|
268
|
+
*/
|
|
269
|
+
scanAgents(agentsDir, extractFrontmatter, normalizeTools) {
|
|
270
|
+
return scanDirectory(agentsDir, '.md', (content, file, filePath) => {
|
|
271
|
+
const frontmatter = extractFrontmatter(filePath);
|
|
272
|
+
if (Object.keys(frontmatter).length === 0) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const name = file.replace('.md', '');
|
|
277
|
+
const tools = normalizeTools ? normalizeTools(frontmatter.tools) : frontmatter.tools || [];
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
name,
|
|
281
|
+
file,
|
|
282
|
+
path: filePath,
|
|
283
|
+
displayName: frontmatter.name || name,
|
|
284
|
+
description: frontmatter.description || '',
|
|
285
|
+
tools,
|
|
286
|
+
model: frontmatter.model || 'haiku',
|
|
287
|
+
color: frontmatter.color || 'blue',
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Resolve paths factory - creates path resolver with injected dependencies
|
|
296
|
+
* @param {IDependencyContainer} container - Dependency container
|
|
297
|
+
* @param {string} [baseDir] - Base directory for relative paths
|
|
298
|
+
* @returns {Object} Path resolver functions
|
|
299
|
+
*/
|
|
300
|
+
function createPathResolver(container, baseDir) {
|
|
301
|
+
const { path: pathModule, fs: fsModule } = container;
|
|
302
|
+
const resolvedBaseDir = baseDir || process.cwd();
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
/**
|
|
306
|
+
* Resolve path relative to base directory
|
|
307
|
+
* @param {...string} segments - Path segments
|
|
308
|
+
* @returns {string} Resolved path
|
|
309
|
+
*/
|
|
310
|
+
resolve(...segments) {
|
|
311
|
+
return pathModule.resolve(resolvedBaseDir, ...segments);
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Join path segments
|
|
316
|
+
* @param {...string} segments - Path segments
|
|
317
|
+
* @returns {string} Joined path
|
|
318
|
+
*/
|
|
319
|
+
join(...segments) {
|
|
320
|
+
return pathModule.join(...segments);
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Check if path exists
|
|
325
|
+
* @param {string} targetPath - Path to check
|
|
326
|
+
* @returns {boolean}
|
|
327
|
+
*/
|
|
328
|
+
exists(targetPath) {
|
|
329
|
+
return fsModule.existsSync(targetPath);
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get base directory
|
|
334
|
+
* @returns {string}
|
|
335
|
+
*/
|
|
336
|
+
getBaseDir() {
|
|
337
|
+
return resolvedBaseDir;
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
// Container factories
|
|
344
|
+
createContainer,
|
|
345
|
+
createTestContainer,
|
|
346
|
+
|
|
347
|
+
// Registry factories
|
|
348
|
+
createPlaceholderRegistry,
|
|
349
|
+
createScannerFactory,
|
|
350
|
+
createPathResolver,
|
|
351
|
+
|
|
352
|
+
// Loggers
|
|
353
|
+
noopLogger,
|
|
354
|
+
consoleLogger,
|
|
355
|
+
|
|
356
|
+
// Utilities
|
|
357
|
+
loadDefaultSanitizer,
|
|
358
|
+
};
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* result-schema.js - Unified Result<T> type for consistent return values
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized Result type with metadata for:
|
|
5
|
+
* - Consistent success/failure handling
|
|
6
|
+
* - Error code integration
|
|
7
|
+
* - Severity and category tracking
|
|
8
|
+
* - Automatic recovery suggestions
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const { success, failure, createResult, isSuccess } = require('./result-schema');
|
|
12
|
+
*
|
|
13
|
+
* // Success case
|
|
14
|
+
* return success(data);
|
|
15
|
+
*
|
|
16
|
+
* // Failure case
|
|
17
|
+
* return failure('ENOENT', 'File not found');
|
|
18
|
+
*
|
|
19
|
+
* // Check result
|
|
20
|
+
* if (isSuccess(result)) {
|
|
21
|
+
* console.log(result.data);
|
|
22
|
+
* } else {
|
|
23
|
+
* console.error(result.error);
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { ErrorCodes, Severity, Category, getErrorCode } = require('./error-codes');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} ResultSuccess<T>
|
|
31
|
+
* @property {true} ok - Indicates success
|
|
32
|
+
* @property {T} data - The success data
|
|
33
|
+
* @property {undefined} error - No error on success
|
|
34
|
+
* @property {undefined} errorCode - No error code on success
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} ResultFailure
|
|
39
|
+
* @property {false} ok - Indicates failure
|
|
40
|
+
* @property {undefined} data - No data on failure
|
|
41
|
+
* @property {string} error - Human-readable error message
|
|
42
|
+
* @property {string} errorCode - Machine-readable error code
|
|
43
|
+
* @property {string} severity - Error severity (critical, high, medium, low)
|
|
44
|
+
* @property {string} category - Error category (filesystem, permission, etc.)
|
|
45
|
+
* @property {boolean} recoverable - Whether the error can be recovered from
|
|
46
|
+
* @property {string} [suggestedFix] - Suggested fix for the error
|
|
47
|
+
* @property {string} [autoFix] - Auto-fix action name if available
|
|
48
|
+
* @property {Object} [context] - Additional context about the error
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {ResultSuccess<T> | ResultFailure} Result<T>
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a success result
|
|
57
|
+
* @template T
|
|
58
|
+
* @param {T} data - The success data
|
|
59
|
+
* @param {Object} [meta] - Optional metadata
|
|
60
|
+
* @returns {ResultSuccess<T>}
|
|
61
|
+
*/
|
|
62
|
+
function success(data, meta = {}) {
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
data,
|
|
66
|
+
...meta,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a failure result with error code metadata
|
|
72
|
+
* @param {string} errorCode - Error code (e.g., 'ENOENT', 'ECONFIG')
|
|
73
|
+
* @param {string} [message] - Optional custom error message
|
|
74
|
+
* @param {Object} [options] - Additional options
|
|
75
|
+
* @param {Object} [options.context] - Additional context about the error
|
|
76
|
+
* @param {Error} [options.cause] - Original error that caused this failure
|
|
77
|
+
* @returns {ResultFailure}
|
|
78
|
+
*/
|
|
79
|
+
function failure(errorCode, message, options = {}) {
|
|
80
|
+
const codeData = getErrorCode(errorCode) || ErrorCodes.EUNKNOWN;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: message || codeData.message,
|
|
85
|
+
errorCode: codeData.code,
|
|
86
|
+
severity: codeData.severity,
|
|
87
|
+
category: codeData.category,
|
|
88
|
+
recoverable: codeData.recoverable,
|
|
89
|
+
suggestedFix: codeData.suggestedFix,
|
|
90
|
+
autoFix: codeData.autoFix || null,
|
|
91
|
+
context: options.context,
|
|
92
|
+
cause: options.cause,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a failure result from an existing Error object
|
|
98
|
+
* @param {Error} error - The error object
|
|
99
|
+
* @param {string} [defaultCode='EUNKNOWN'] - Default error code if none detected
|
|
100
|
+
* @param {Object} [options] - Additional options
|
|
101
|
+
* @param {Object} [options.context] - Additional context about the error
|
|
102
|
+
* @returns {ResultFailure}
|
|
103
|
+
*/
|
|
104
|
+
function failureFromError(error, defaultCode = 'EUNKNOWN', options = {}) {
|
|
105
|
+
const { getErrorCodeFromError } = require('./error-codes');
|
|
106
|
+
const codeData = getErrorCodeFromError(error);
|
|
107
|
+
|
|
108
|
+
// Use detected code or default
|
|
109
|
+
const code = codeData.code !== 'EUNKNOWN' ? codeData.code : defaultCode;
|
|
110
|
+
const finalCodeData = ErrorCodes[code] || ErrorCodes.EUNKNOWN;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: error.message || finalCodeData.message,
|
|
115
|
+
errorCode: finalCodeData.code,
|
|
116
|
+
severity: finalCodeData.severity,
|
|
117
|
+
category: finalCodeData.category,
|
|
118
|
+
recoverable: finalCodeData.recoverable,
|
|
119
|
+
suggestedFix: finalCodeData.suggestedFix,
|
|
120
|
+
autoFix: finalCodeData.autoFix || null,
|
|
121
|
+
context: options.context,
|
|
122
|
+
cause: error,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a result from a boolean condition
|
|
128
|
+
* @template T
|
|
129
|
+
* @param {boolean} condition - Condition to check
|
|
130
|
+
* @param {T} data - Data to return on success
|
|
131
|
+
* @param {string} errorCode - Error code on failure
|
|
132
|
+
* @param {string} [errorMessage] - Error message on failure
|
|
133
|
+
* @returns {Result<T>}
|
|
134
|
+
*/
|
|
135
|
+
function fromCondition(condition, data, errorCode, errorMessage) {
|
|
136
|
+
if (condition) {
|
|
137
|
+
return success(data);
|
|
138
|
+
}
|
|
139
|
+
return failure(errorCode, errorMessage);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a result from a Promise
|
|
144
|
+
* @template T
|
|
145
|
+
* @param {Promise<T>} promise - Promise to wrap
|
|
146
|
+
* @param {string} [defaultCode='EUNKNOWN'] - Default error code on rejection
|
|
147
|
+
* @returns {Promise<Result<T>>}
|
|
148
|
+
*/
|
|
149
|
+
async function fromPromise(promise, defaultCode = 'EUNKNOWN') {
|
|
150
|
+
try {
|
|
151
|
+
const data = await promise;
|
|
152
|
+
return success(data);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return failureFromError(error, defaultCode);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create a result from a sync function call
|
|
160
|
+
* @template T
|
|
161
|
+
* @param {() => T} fn - Function to execute
|
|
162
|
+
* @param {string} [defaultCode='EUNKNOWN'] - Default error code on throw
|
|
163
|
+
* @returns {Result<T>}
|
|
164
|
+
*/
|
|
165
|
+
function fromTry(fn, defaultCode = 'EUNKNOWN') {
|
|
166
|
+
try {
|
|
167
|
+
const data = fn();
|
|
168
|
+
return success(data);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return failureFromError(error, defaultCode);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if a result is successful
|
|
176
|
+
* @template T
|
|
177
|
+
* @param {Result<T>} result - Result to check
|
|
178
|
+
* @returns {boolean}
|
|
179
|
+
*/
|
|
180
|
+
function isSuccess(result) {
|
|
181
|
+
return result != null && result.ok === true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a result is a failure
|
|
186
|
+
* @template T
|
|
187
|
+
* @param {Result<T>} result - Result to check
|
|
188
|
+
* @returns {boolean}
|
|
189
|
+
*/
|
|
190
|
+
function isFailure(result) {
|
|
191
|
+
return result != null && result.ok === false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get data from result or throw if failure
|
|
196
|
+
* @template T
|
|
197
|
+
* @param {Result<T>} result - Result to unwrap
|
|
198
|
+
* @param {string} [context] - Context for error message
|
|
199
|
+
* @returns {T}
|
|
200
|
+
* @throws {Error} If result is a failure
|
|
201
|
+
*/
|
|
202
|
+
function unwrap(result, context) {
|
|
203
|
+
if (isSuccess(result)) {
|
|
204
|
+
return result.data;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const prefix = context ? `${context}: ` : '';
|
|
208
|
+
const error = new Error(`${prefix}${result.error}`);
|
|
209
|
+
error.errorCode = result.errorCode;
|
|
210
|
+
error.severity = result.severity;
|
|
211
|
+
error.category = result.category;
|
|
212
|
+
error.recoverable = result.recoverable;
|
|
213
|
+
error.suggestedFix = result.suggestedFix;
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get data from result or return default value on failure
|
|
219
|
+
* @template T
|
|
220
|
+
* @param {Result<T>} result - Result to unwrap
|
|
221
|
+
* @param {T} defaultValue - Default value on failure
|
|
222
|
+
* @returns {T}
|
|
223
|
+
*/
|
|
224
|
+
function unwrapOr(result, defaultValue) {
|
|
225
|
+
if (isSuccess(result)) {
|
|
226
|
+
return result.data;
|
|
227
|
+
}
|
|
228
|
+
return defaultValue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Transform successful result data
|
|
233
|
+
* @template T, U
|
|
234
|
+
* @param {Result<T>} result - Result to transform
|
|
235
|
+
* @param {(data: T) => U} fn - Transform function
|
|
236
|
+
* @returns {Result<U>}
|
|
237
|
+
*/
|
|
238
|
+
function map(result, fn) {
|
|
239
|
+
if (isSuccess(result)) {
|
|
240
|
+
return success(fn(result.data));
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Chain result-returning operations
|
|
247
|
+
* @template T, U
|
|
248
|
+
* @param {Result<T>} result - Result to chain from
|
|
249
|
+
* @param {(data: T) => Result<U>} fn - Function returning Result
|
|
250
|
+
* @returns {Result<U>}
|
|
251
|
+
*/
|
|
252
|
+
function flatMap(result, fn) {
|
|
253
|
+
if (isSuccess(result)) {
|
|
254
|
+
return fn(result.data);
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Combine multiple results into one
|
|
261
|
+
* @template T
|
|
262
|
+
* @param {Result<T>[]} results - Results to combine
|
|
263
|
+
* @returns {Result<T[]>} Combined result with array of data or first failure
|
|
264
|
+
*/
|
|
265
|
+
function all(results) {
|
|
266
|
+
const data = [];
|
|
267
|
+
|
|
268
|
+
for (const result of results) {
|
|
269
|
+
if (isFailure(result)) {
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
data.push(result.data);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return success(data);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get first successful result or last failure
|
|
280
|
+
* @template T
|
|
281
|
+
* @param {Result<T>[]} results - Results to check
|
|
282
|
+
* @returns {Result<T>} First success or last failure
|
|
283
|
+
*/
|
|
284
|
+
function any(results) {
|
|
285
|
+
let lastFailure = null;
|
|
286
|
+
|
|
287
|
+
for (const result of results) {
|
|
288
|
+
if (isSuccess(result)) {
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
lastFailure = result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return lastFailure || failure('EUNKNOWN', 'No results provided');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Format a result for logging/display
|
|
299
|
+
* @template T
|
|
300
|
+
* @param {Result<T>} result - Result to format
|
|
301
|
+
* @param {Object} [options] - Format options
|
|
302
|
+
* @param {boolean} [options.includeData=false] - Include data in output
|
|
303
|
+
* @param {boolean} [options.includeSuggestion=true] - Include suggested fix
|
|
304
|
+
* @returns {string}
|
|
305
|
+
*/
|
|
306
|
+
function format(result, options = {}) {
|
|
307
|
+
const { includeData = false, includeSuggestion = true } = options;
|
|
308
|
+
|
|
309
|
+
if (isSuccess(result)) {
|
|
310
|
+
if (includeData) {
|
|
311
|
+
return `[OK] ${JSON.stringify(result.data)}`;
|
|
312
|
+
}
|
|
313
|
+
return '[OK]';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const lines = [`[${result.errorCode}] ${result.error}`];
|
|
317
|
+
lines.push(` Severity: ${result.severity} | Category: ${result.category}`);
|
|
318
|
+
|
|
319
|
+
if (includeSuggestion && result.suggestedFix) {
|
|
320
|
+
lines.push(` Fix: ${result.suggestedFix}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (result.autoFix) {
|
|
324
|
+
lines.push(` Auto-fix available: npx agileflow doctor --fix`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return lines.join('\n');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = {
|
|
331
|
+
// Constructors
|
|
332
|
+
success,
|
|
333
|
+
failure,
|
|
334
|
+
failureFromError,
|
|
335
|
+
|
|
336
|
+
// From helpers
|
|
337
|
+
fromCondition,
|
|
338
|
+
fromPromise,
|
|
339
|
+
fromTry,
|
|
340
|
+
|
|
341
|
+
// Type guards
|
|
342
|
+
isSuccess,
|
|
343
|
+
isFailure,
|
|
344
|
+
|
|
345
|
+
// Extractors
|
|
346
|
+
unwrap,
|
|
347
|
+
unwrapOr,
|
|
348
|
+
|
|
349
|
+
// Transformers
|
|
350
|
+
map,
|
|
351
|
+
flatMap,
|
|
352
|
+
|
|
353
|
+
// Combinators
|
|
354
|
+
all,
|
|
355
|
+
any,
|
|
356
|
+
|
|
357
|
+
// Utilities
|
|
358
|
+
format,
|
|
359
|
+
|
|
360
|
+
// Re-export enums for convenience
|
|
361
|
+
Severity,
|
|
362
|
+
Category,
|
|
363
|
+
};
|