agileflow 2.94.1 → 2.95.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 +15 -0
- package/lib/colors.generated.js +117 -0
- package/lib/colors.js +59 -109
- package/lib/generator-factory.js +333 -0
- package/lib/path-utils.js +49 -0
- package/lib/session-registry.js +25 -15
- package/lib/smart-json-file.js +40 -32
- package/lib/state-machine.js +286 -0
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +7 -6
- package/scripts/archive-completed-stories.sh +86 -11
- package/scripts/babysit-context-restore.js +89 -0
- package/scripts/claude-tmux.sh +111 -5
- package/scripts/damage-control/bash-tool-damage-control.js +11 -247
- package/scripts/damage-control/edit-tool-damage-control.js +9 -249
- package/scripts/damage-control/write-tool-damage-control.js +9 -244
- package/scripts/generate-colors.js +314 -0
- package/scripts/lib/colors.generated.sh +82 -0
- package/scripts/lib/colors.sh +10 -70
- package/scripts/lib/configure-features.js +401 -0
- package/scripts/lib/context-loader.js +181 -52
- package/scripts/precompact-context.sh +54 -17
- package/scripts/session-coordinator.sh +2 -2
- package/scripts/session-manager.js +653 -10
- package/src/core/commands/audit.md +93 -0
- package/src/core/commands/auto.md +73 -0
- package/src/core/commands/babysit.md +169 -13
- package/src/core/commands/baseline.md +73 -0
- package/src/core/commands/batch.md +64 -0
- package/src/core/commands/blockers.md +60 -0
- package/src/core/commands/board.md +66 -0
- package/src/core/commands/choose.md +77 -0
- package/src/core/commands/ci.md +77 -0
- package/src/core/commands/compress.md +27 -1
- package/src/core/commands/configure.md +126 -10
- package/src/core/commands/council.md +74 -0
- package/src/core/commands/debt.md +72 -0
- package/src/core/commands/deploy.md +73 -0
- package/src/core/commands/deps.md +68 -0
- package/src/core/commands/docs.md +60 -0
- package/src/core/commands/feedback.md +68 -0
- package/src/core/commands/ideate.md +74 -0
- package/src/core/commands/impact.md +74 -0
- package/src/core/commands/install.md +529 -0
- package/src/core/commands/maintain.md +558 -0
- package/src/core/commands/metrics.md +75 -0
- package/src/core/commands/multi-expert.md +74 -0
- package/src/core/commands/packages.md +69 -0
- package/src/core/commands/readme-sync.md +64 -0
- package/src/core/commands/research/analyze.md +285 -121
- package/src/core/commands/research/import.md +281 -109
- package/src/core/commands/retro.md +76 -0
- package/src/core/commands/review.md +72 -0
- package/src/core/commands/rlm.md +83 -0
- package/src/core/commands/rpi.md +90 -0
- package/src/core/commands/session/cleanup.md +214 -12
- package/src/core/commands/session/end.md +155 -17
- package/src/core/commands/sprint.md +72 -0
- package/src/core/commands/story-validate.md +68 -0
- package/src/core/commands/template.md +69 -0
- package/src/core/commands/tests.md +83 -0
- package/src/core/commands/update.md +59 -0
- package/src/core/commands/validate-expertise.md +76 -0
- package/src/core/commands/velocity.md +74 -0
- package/src/core/commands/verify.md +91 -0
- package/src/core/commands/whats-new.md +69 -0
- package/src/core/commands/workflow.md +88 -0
- package/src/core/templates/command-documentation.md +187 -0
- package/tools/cli/commands/session.js +1171 -0
- package/tools/cli/commands/setup.js +2 -81
- package/tools/cli/installers/core/installer.js +0 -5
- package/tools/cli/installers/ide/claude-code.js +6 -0
- package/tools/cli/lib/config-manager.js +42 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.95.0] - 2026-01-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Global session management commands (status, history, cleanup)
|
|
14
|
+
- `/agileflow:install` and `/agileflow:maintain` commands for quick setup
|
|
15
|
+
- Multi-expert Implementation Ideation in `/research:import` and `/research:analyze`
|
|
16
|
+
- Plan file context preservation with experimental profile
|
|
17
|
+
- Tmux freeze recovery keybinds and `--rescue` flag
|
|
18
|
+
- Shellcheck CI job for shell script linting
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Prevent worktree sessions from being marked as main
|
|
22
|
+
- Prevent duplicate agents during install/update
|
|
23
|
+
- Inline uncommitted changes handling for `/session:end`
|
|
24
|
+
|
|
10
25
|
## [2.94.1] - 2026-01-24
|
|
11
26
|
|
|
12
27
|
### Added
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* colors.generated.js - Auto-generated from config/colors.yaml
|
|
3
|
+
*
|
|
4
|
+
* DO NOT EDIT THIS FILE DIRECTLY.
|
|
5
|
+
* Run: node scripts/generate-colors.js
|
|
6
|
+
*
|
|
7
|
+
* Generated: 2026-01-29T08:28:33.726Z
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
// Brand color
|
|
13
|
+
const BRAND_HEX = '#e8683a';
|
|
14
|
+
|
|
15
|
+
// Modifiers
|
|
16
|
+
const modifiers = {
|
|
17
|
+
reset: '\x1b[0m',
|
|
18
|
+
bold: '\x1b[1m',
|
|
19
|
+
dim: '\x1b[2m',
|
|
20
|
+
italic: '\x1b[3m',
|
|
21
|
+
underline: '\x1b[4m',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Standard ANSI colors
|
|
25
|
+
const standard = {
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m',
|
|
29
|
+
blue: '\x1b[34m',
|
|
30
|
+
magenta: '\x1b[35m',
|
|
31
|
+
cyan: '\x1b[36m',
|
|
32
|
+
white: '\x1b[37m',
|
|
33
|
+
black: '\x1b[30m',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Bright ANSI colors
|
|
37
|
+
const bright = {
|
|
38
|
+
brightRed: '\x1b[91m',
|
|
39
|
+
brightGreen: '\x1b[92m',
|
|
40
|
+
brightYellow: '\x1b[93m',
|
|
41
|
+
brightBlue: '\x1b[94m',
|
|
42
|
+
brightMagenta: '\x1b[95m',
|
|
43
|
+
brightCyan: '\x1b[96m',
|
|
44
|
+
brightWhite: '\x1b[97m',
|
|
45
|
+
brightBlack: '\x1b[90m',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// 256-color palette
|
|
49
|
+
const palette256 = {
|
|
50
|
+
mintGreen: '\x1b[38;5;158m', // Healthy/success states
|
|
51
|
+
peach: '\x1b[38;5;215m', // Warning states
|
|
52
|
+
coral: '\x1b[38;5;203m', // Critical/error states
|
|
53
|
+
lightGreen: '\x1b[38;5;194m', // Session healthy
|
|
54
|
+
lightYellow: '\x1b[38;5;228m', // Session warning
|
|
55
|
+
lightPink: '\x1b[38;5;210m', // Session critical
|
|
56
|
+
skyBlue: '\x1b[38;5;117m', // Directories/paths, ready states
|
|
57
|
+
lavender: '\x1b[38;5;147m', // Model info, story IDs
|
|
58
|
+
softGold: '\x1b[38;5;222m', // Cost/money
|
|
59
|
+
teal: '\x1b[38;5;80m', // Pending states
|
|
60
|
+
slate: '\x1b[38;5;103m', // Secondary info
|
|
61
|
+
rose: '\x1b[38;5;211m', // Blocked/critical accent
|
|
62
|
+
amber: '\x1b[38;5;214m', // WIP/in-progress accent
|
|
63
|
+
powder: '\x1b[38;5;153m', // Labels/headers
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Background colors
|
|
67
|
+
const backgrounds = {
|
|
68
|
+
bgRed: '\x1b[41m',
|
|
69
|
+
bgGreen: '\x1b[42m',
|
|
70
|
+
bgYellow: '\x1b[43m',
|
|
71
|
+
bgBlue: '\x1b[44m',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Brand color ANSI
|
|
75
|
+
const brand = '\x1b[38;2;232;104;58m';
|
|
76
|
+
|
|
77
|
+
// High-contrast mode colors (WCAG AAA)
|
|
78
|
+
const highContrast = {
|
|
79
|
+
red: '\x1b[91m',
|
|
80
|
+
green: '\x1b[92m',
|
|
81
|
+
yellow: '\x1b[93m',
|
|
82
|
+
blue: '\x1b[94m',
|
|
83
|
+
magenta: '\x1b[95m',
|
|
84
|
+
cyan: '\x1b[96m',
|
|
85
|
+
white: '\x1b[97m',
|
|
86
|
+
dim: '\x1b[0m',
|
|
87
|
+
brightBlack: '\x1b[37m',
|
|
88
|
+
brand: '\x1b[38;2;255;165;0m',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Combined standard color palette
|
|
92
|
+
const cStandard = {
|
|
93
|
+
...modifiers,
|
|
94
|
+
...standard,
|
|
95
|
+
...bright,
|
|
96
|
+
...palette256,
|
|
97
|
+
...backgrounds,
|
|
98
|
+
brand,
|
|
99
|
+
orange: brand, // Alias
|
|
100
|
+
// Semantic aliases
|
|
101
|
+
success: standard.green,
|
|
102
|
+
error: standard.red,
|
|
103
|
+
warning: standard.yellow,
|
|
104
|
+
info: standard.cyan,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
BRAND_HEX,
|
|
109
|
+
modifiers,
|
|
110
|
+
standard,
|
|
111
|
+
bright,
|
|
112
|
+
palette256,
|
|
113
|
+
backgrounds,
|
|
114
|
+
brand,
|
|
115
|
+
highContrast,
|
|
116
|
+
cStandard,
|
|
117
|
+
};
|
package/lib/colors.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Centralized ANSI color codes and formatting helpers.
|
|
5
5
|
* Uses 256-color palette for modern terminal support.
|
|
6
6
|
*
|
|
7
|
+
* Color definitions are generated from config/colors.yaml.
|
|
8
|
+
* Run: node scripts/generate-colors.js
|
|
9
|
+
*
|
|
7
10
|
* WCAG AA Contrast Ratios (verified against #1a1a1a dark terminal background):
|
|
8
11
|
* - Green (#32CD32): 4.5:1 ✓ (meets AA for normal text)
|
|
9
12
|
* - Red (#FF6B6B): 5.0:1 ✓ (meets AA for normal text)
|
|
@@ -22,6 +25,9 @@
|
|
|
22
25
|
* are for typical dark terminal configurations.
|
|
23
26
|
*/
|
|
24
27
|
|
|
28
|
+
// Import generated color definitions from YAML source of truth
|
|
29
|
+
const generated = require('./colors.generated');
|
|
30
|
+
|
|
25
31
|
// High-contrast mode detection
|
|
26
32
|
let _highContrastMode = null;
|
|
27
33
|
|
|
@@ -57,135 +63,79 @@ function resetHighContrast() {
|
|
|
57
63
|
* Brand color hex value for chalk compatibility.
|
|
58
64
|
* Use with chalk.hex(BRAND_HEX) in files that use chalk.
|
|
59
65
|
*/
|
|
60
|
-
const BRAND_HEX =
|
|
66
|
+
const { BRAND_HEX } = generated;
|
|
61
67
|
|
|
62
68
|
/**
|
|
63
69
|
* WCAG AAA high-contrast color palette (7:1+ contrast ratio).
|
|
64
70
|
* Used when AGILEFLOW_HIGH_CONTRAST=1 or --high-contrast flag.
|
|
71
|
+
* Built from generated highContrast values plus additional mappings.
|
|
65
72
|
*/
|
|
66
73
|
const hc = {
|
|
67
|
-
// Reset and modifiers
|
|
68
|
-
reset:
|
|
69
|
-
bold:
|
|
70
|
-
dim:
|
|
71
|
-
italic:
|
|
72
|
-
underline:
|
|
73
|
-
|
|
74
|
-
// High-contrast standard colors (
|
|
75
|
-
red:
|
|
76
|
-
green:
|
|
77
|
-
yellow:
|
|
78
|
-
blue:
|
|
79
|
-
magenta:
|
|
80
|
-
cyan:
|
|
81
|
-
white:
|
|
74
|
+
// Reset and modifiers from generated
|
|
75
|
+
reset: generated.modifiers.reset,
|
|
76
|
+
bold: generated.modifiers.bold,
|
|
77
|
+
dim: generated.highContrast.dim, // No dimming in high-contrast
|
|
78
|
+
italic: generated.modifiers.italic,
|
|
79
|
+
underline: generated.modifiers.underline,
|
|
80
|
+
|
|
81
|
+
// High-contrast standard colors (from generated)
|
|
82
|
+
red: generated.highContrast.red,
|
|
83
|
+
green: generated.highContrast.green,
|
|
84
|
+
yellow: generated.highContrast.yellow,
|
|
85
|
+
blue: generated.highContrast.blue,
|
|
86
|
+
magenta: generated.highContrast.magenta,
|
|
87
|
+
cyan: generated.highContrast.cyan,
|
|
88
|
+
white: generated.highContrast.white,
|
|
82
89
|
|
|
83
90
|
// Bright variants (same in high-contrast mode)
|
|
84
|
-
brightBlack:
|
|
85
|
-
brightRed:
|
|
86
|
-
brightGreen:
|
|
87
|
-
brightYellow:
|
|
88
|
-
brightBlue:
|
|
89
|
-
brightMagenta:
|
|
90
|
-
brightCyan:
|
|
91
|
-
brightWhite:
|
|
91
|
+
brightBlack: generated.highContrast.brightBlack, // Use white instead of gray
|
|
92
|
+
brightRed: generated.highContrast.red,
|
|
93
|
+
brightGreen: generated.highContrast.green,
|
|
94
|
+
brightYellow: generated.highContrast.yellow,
|
|
95
|
+
brightBlue: generated.highContrast.blue,
|
|
96
|
+
brightMagenta: generated.highContrast.magenta,
|
|
97
|
+
brightCyan: generated.highContrast.cyan,
|
|
98
|
+
brightWhite: generated.highContrast.white,
|
|
92
99
|
|
|
93
100
|
// 256-color high-contrast alternatives (all 7:1+ ratio)
|
|
94
|
-
mintGreen:
|
|
95
|
-
peach:
|
|
96
|
-
coral:
|
|
97
|
-
lightGreen:
|
|
98
|
-
lightYellow:
|
|
99
|
-
lightPink:
|
|
100
|
-
skyBlue:
|
|
101
|
-
lavender:
|
|
102
|
-
softGold:
|
|
103
|
-
teal:
|
|
104
|
-
slate:
|
|
105
|
-
rose:
|
|
106
|
-
amber:
|
|
107
|
-
powder:
|
|
108
|
-
|
|
109
|
-
// Brand color -
|
|
110
|
-
brand:
|
|
111
|
-
orange:
|
|
101
|
+
mintGreen: generated.highContrast.green,
|
|
102
|
+
peach: generated.highContrast.yellow,
|
|
103
|
+
coral: generated.highContrast.red,
|
|
104
|
+
lightGreen: generated.highContrast.green,
|
|
105
|
+
lightYellow: generated.highContrast.yellow,
|
|
106
|
+
lightPink: generated.highContrast.red,
|
|
107
|
+
skyBlue: generated.highContrast.cyan,
|
|
108
|
+
lavender: generated.highContrast.magenta,
|
|
109
|
+
softGold: generated.highContrast.yellow,
|
|
110
|
+
teal: generated.highContrast.cyan,
|
|
111
|
+
slate: generated.highContrast.white, // White instead of gray
|
|
112
|
+
rose: generated.highContrast.red,
|
|
113
|
+
amber: generated.highContrast.yellow,
|
|
114
|
+
powder: generated.highContrast.cyan,
|
|
115
|
+
|
|
116
|
+
// Brand color - from generated high-contrast brand
|
|
117
|
+
brand: generated.highContrast.brand,
|
|
118
|
+
orange: generated.highContrast.brand,
|
|
112
119
|
|
|
113
120
|
// Background colors (same as standard)
|
|
114
|
-
bgRed:
|
|
115
|
-
bgGreen:
|
|
116
|
-
bgYellow:
|
|
117
|
-
bgBlue:
|
|
121
|
+
bgRed: generated.backgrounds.bgRed,
|
|
122
|
+
bgGreen: generated.backgrounds.bgGreen,
|
|
123
|
+
bgYellow: generated.backgrounds.bgYellow,
|
|
124
|
+
bgBlue: generated.backgrounds.bgBlue,
|
|
118
125
|
|
|
119
126
|
// Semantic aliases
|
|
120
|
-
success:
|
|
121
|
-
error:
|
|
122
|
-
warning:
|
|
123
|
-
info:
|
|
127
|
+
success: generated.highContrast.green,
|
|
128
|
+
error: generated.highContrast.red,
|
|
129
|
+
warning: generated.highContrast.yellow,
|
|
130
|
+
info: generated.highContrast.cyan,
|
|
124
131
|
};
|
|
125
132
|
|
|
126
133
|
/**
|
|
127
134
|
* ANSI color codes for terminal output.
|
|
128
135
|
* Includes standard colors, 256-color palette, and brand colors.
|
|
136
|
+
* Values imported from generated colors (config/colors.yaml source of truth).
|
|
129
137
|
*/
|
|
130
|
-
const cStandard =
|
|
131
|
-
// Reset and modifiers
|
|
132
|
-
reset: '\x1b[0m',
|
|
133
|
-
bold: '\x1b[1m',
|
|
134
|
-
dim: '\x1b[2m',
|
|
135
|
-
italic: '\x1b[3m',
|
|
136
|
-
underline: '\x1b[4m',
|
|
137
|
-
|
|
138
|
-
// Standard ANSI colors (8 colors)
|
|
139
|
-
red: '\x1b[31m',
|
|
140
|
-
green: '\x1b[32m',
|
|
141
|
-
yellow: '\x1b[33m',
|
|
142
|
-
blue: '\x1b[34m',
|
|
143
|
-
magenta: '\x1b[35m',
|
|
144
|
-
cyan: '\x1b[36m',
|
|
145
|
-
white: '\x1b[37m',
|
|
146
|
-
|
|
147
|
-
// Bright variants
|
|
148
|
-
brightBlack: '\x1b[90m',
|
|
149
|
-
brightRed: '\x1b[91m',
|
|
150
|
-
brightGreen: '\x1b[92m',
|
|
151
|
-
brightYellow: '\x1b[93m',
|
|
152
|
-
brightBlue: '\x1b[94m',
|
|
153
|
-
brightMagenta: '\x1b[95m',
|
|
154
|
-
brightCyan: '\x1b[96m',
|
|
155
|
-
brightWhite: '\x1b[97m',
|
|
156
|
-
|
|
157
|
-
// 256-color palette (vibrant, modern look)
|
|
158
|
-
mintGreen: '\x1b[38;5;158m', // Healthy/success states
|
|
159
|
-
peach: '\x1b[38;5;215m', // Warning states
|
|
160
|
-
coral: '\x1b[38;5;203m', // Critical/error states
|
|
161
|
-
lightGreen: '\x1b[38;5;194m', // Session healthy
|
|
162
|
-
lightYellow: '\x1b[38;5;228m', // Session warning
|
|
163
|
-
lightPink: '\x1b[38;5;210m', // Session critical
|
|
164
|
-
skyBlue: '\x1b[38;5;117m', // Directories/paths, ready states
|
|
165
|
-
lavender: '\x1b[38;5;147m', // Model info, story IDs
|
|
166
|
-
softGold: '\x1b[38;5;222m', // Cost/money
|
|
167
|
-
teal: '\x1b[38;5;80m', // Pending states
|
|
168
|
-
slate: '\x1b[38;5;103m', // Secondary info
|
|
169
|
-
rose: '\x1b[38;5;211m', // Blocked/critical accent
|
|
170
|
-
amber: '\x1b[38;5;214m', // WIP/in-progress accent
|
|
171
|
-
powder: '\x1b[38;5;153m', // Labels/headers
|
|
172
|
-
|
|
173
|
-
// Brand color (#e8683a - burnt orange/terracotta)
|
|
174
|
-
brand: '\x1b[38;2;232;104;58m',
|
|
175
|
-
orange: '\x1b[38;2;232;104;58m', // Alias for brand color
|
|
176
|
-
|
|
177
|
-
// Background colors
|
|
178
|
-
bgRed: '\x1b[41m',
|
|
179
|
-
bgGreen: '\x1b[42m',
|
|
180
|
-
bgYellow: '\x1b[43m',
|
|
181
|
-
bgBlue: '\x1b[44m',
|
|
182
|
-
|
|
183
|
-
// Semantic aliases (for consistent meaning across codebase)
|
|
184
|
-
success: '\x1b[32m', // Same as green
|
|
185
|
-
error: '\x1b[31m', // Same as red
|
|
186
|
-
warning: '\x1b[33m', // Same as yellow
|
|
187
|
-
info: '\x1b[36m', // Same as cyan
|
|
188
|
-
};
|
|
138
|
+
const cStandard = generated.cStandard;
|
|
189
139
|
|
|
190
140
|
/**
|
|
191
141
|
* Get the active color palette based on high-contrast mode.
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeneratorFactory - Dependency Injection for Content Generators
|
|
3
|
+
*
|
|
4
|
+
* Provides a centralized factory for creating content generators with:
|
|
5
|
+
* - Shared PlaceholderRegistry built once
|
|
6
|
+
* - Dependency injection for testability
|
|
7
|
+
* - Generator registration pattern
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const factory = new GeneratorFactory();
|
|
11
|
+
* factory.registerGenerator('help', HelpGenerator);
|
|
12
|
+
* factory.registerGenerator('readme', ReadmeGenerator);
|
|
13
|
+
*
|
|
14
|
+
* // Build registry once, pass to all generators
|
|
15
|
+
* const context = await factory.buildContext();
|
|
16
|
+
* await factory.runAll(context);
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const { PlaceholderRegistry, createDefaultRegistry } = require('./placeholder-registry');
|
|
22
|
+
const { createContainer, createScannerFactory } = require('./registry-di');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generator interface
|
|
26
|
+
* @typedef {Object} IGenerator
|
|
27
|
+
* @property {string} name - Generator name
|
|
28
|
+
* @property {Function} register - Register placeholders: (registry) => void
|
|
29
|
+
* @property {Function} generate - Generate content: (context) => Promise<void>
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GeneratorFactory - Factory for creating and running generators
|
|
34
|
+
*/
|
|
35
|
+
class GeneratorFactory {
|
|
36
|
+
/**
|
|
37
|
+
* Create a new GeneratorFactory
|
|
38
|
+
* @param {Object} options - Factory options
|
|
39
|
+
* @param {Object} [options.container] - DI container (from registry-di.js)
|
|
40
|
+
* @param {PlaceholderRegistry} [options.registry] - Pre-built registry
|
|
41
|
+
* @param {Object} [options.paths] - Path configuration
|
|
42
|
+
*/
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this._generators = new Map();
|
|
45
|
+
this._container = options.container || createContainer();
|
|
46
|
+
this._registry = options.registry || null;
|
|
47
|
+
this._paths = options.paths || {};
|
|
48
|
+
this._built = false;
|
|
49
|
+
this._context = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Register a generator with the factory
|
|
54
|
+
* @param {string} name - Generator name
|
|
55
|
+
* @param {IGenerator|Function} generator - Generator instance or class
|
|
56
|
+
* @returns {GeneratorFactory} this for chaining
|
|
57
|
+
*/
|
|
58
|
+
registerGenerator(name, generator) {
|
|
59
|
+
if (!name || typeof name !== 'string') {
|
|
60
|
+
throw new Error('Generator name must be a non-empty string');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Support both instances and classes
|
|
64
|
+
const instance = typeof generator === 'function' ? new generator() : generator;
|
|
65
|
+
|
|
66
|
+
if (!instance.register || typeof instance.register !== 'function') {
|
|
67
|
+
throw new Error(`Generator "${name}" must have a register(registry) method`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!instance.generate || typeof instance.generate !== 'function') {
|
|
71
|
+
throw new Error(`Generator "${name}" must have a generate(context) method`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this._generators.set(name, instance);
|
|
75
|
+
|
|
76
|
+
// Reset built state when new generator is added
|
|
77
|
+
this._built = false;
|
|
78
|
+
this._context = null;
|
|
79
|
+
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Unregister a generator
|
|
85
|
+
* @param {string} name - Generator name
|
|
86
|
+
* @returns {boolean} True if removed
|
|
87
|
+
*/
|
|
88
|
+
unregisterGenerator(name) {
|
|
89
|
+
const result = this._generators.delete(name);
|
|
90
|
+
if (result) {
|
|
91
|
+
this._built = false;
|
|
92
|
+
this._context = null;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get list of registered generator names
|
|
99
|
+
* @returns {string[]}
|
|
100
|
+
*/
|
|
101
|
+
getGeneratorNames() {
|
|
102
|
+
return Array.from(this._generators.keys());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if a generator is registered
|
|
107
|
+
* @param {string} name - Generator name
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
hasGenerator(name) {
|
|
111
|
+
return this._generators.has(name);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get the shared registry instance
|
|
116
|
+
* @returns {PlaceholderRegistry}
|
|
117
|
+
*/
|
|
118
|
+
getRegistry() {
|
|
119
|
+
if (!this._registry) {
|
|
120
|
+
this._registry = createDefaultRegistry();
|
|
121
|
+
}
|
|
122
|
+
return this._registry;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build context by registering all generator placeholders
|
|
127
|
+
* Registry is built once and shared across all generators
|
|
128
|
+
* @param {Object} [baseContext={}] - Base context to merge
|
|
129
|
+
* @returns {Object} Built context
|
|
130
|
+
*/
|
|
131
|
+
buildContext(baseContext = {}) {
|
|
132
|
+
if (this._built && this._context) {
|
|
133
|
+
return { ...this._context, ...baseContext };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const registry = this.getRegistry();
|
|
137
|
+
|
|
138
|
+
// Let each generator register its placeholders
|
|
139
|
+
for (const [name, generator] of this._generators) {
|
|
140
|
+
try {
|
|
141
|
+
generator.register(registry);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(`Failed to register placeholders for "${name}":`, error.message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Build context with resolved values
|
|
148
|
+
this._context = {
|
|
149
|
+
registry,
|
|
150
|
+
container: this._container,
|
|
151
|
+
paths: this._paths,
|
|
152
|
+
scanner: createScannerFactory(this._container),
|
|
153
|
+
...baseContext,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
this._built = true;
|
|
157
|
+
|
|
158
|
+
return this._context;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Run a specific generator
|
|
163
|
+
* @param {string} name - Generator name
|
|
164
|
+
* @param {Object} [context] - Context override
|
|
165
|
+
* @returns {Promise<{success: boolean, error?: Error}>}
|
|
166
|
+
*/
|
|
167
|
+
async runGenerator(name, context) {
|
|
168
|
+
const generator = this._generators.get(name);
|
|
169
|
+
|
|
170
|
+
if (!generator) {
|
|
171
|
+
return { success: false, error: new Error(`Generator not found: ${name}`) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const ctx = context || this.buildContext();
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await generator.generate(ctx);
|
|
178
|
+
return { success: true };
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return { success: false, error };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Run all registered generators
|
|
186
|
+
* @param {Object} [context] - Context override
|
|
187
|
+
* @returns {Promise<Map<string, {success: boolean, error?: Error}>>}
|
|
188
|
+
*/
|
|
189
|
+
async runAll(context) {
|
|
190
|
+
const ctx = context || this.buildContext();
|
|
191
|
+
const results = new Map();
|
|
192
|
+
|
|
193
|
+
for (const name of this._generators.keys()) {
|
|
194
|
+
results.set(name, await this.runGenerator(name, ctx));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Run generators in parallel
|
|
202
|
+
* @param {Object} [context] - Context override
|
|
203
|
+
* @returns {Promise<Map<string, {success: boolean, error?: Error}>>}
|
|
204
|
+
*/
|
|
205
|
+
async runParallel(context) {
|
|
206
|
+
const ctx = context || this.buildContext();
|
|
207
|
+
const results = new Map();
|
|
208
|
+
|
|
209
|
+
const promises = Array.from(this._generators.keys()).map(async name => {
|
|
210
|
+
const result = await this.runGenerator(name, ctx);
|
|
211
|
+
return { name, result };
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const settled = await Promise.allSettled(promises);
|
|
215
|
+
|
|
216
|
+
for (const item of settled) {
|
|
217
|
+
if (item.status === 'fulfilled') {
|
|
218
|
+
results.set(item.value.name, item.value.result);
|
|
219
|
+
} else {
|
|
220
|
+
// Shouldn't happen since runGenerator catches errors
|
|
221
|
+
const name = 'unknown';
|
|
222
|
+
results.set(name, { success: false, error: item.reason });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return results;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Reset the factory state
|
|
231
|
+
*/
|
|
232
|
+
reset() {
|
|
233
|
+
this._built = false;
|
|
234
|
+
this._context = null;
|
|
235
|
+
this._registry = null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get container (for testing)
|
|
240
|
+
* @returns {Object}
|
|
241
|
+
*/
|
|
242
|
+
getContainer() {
|
|
243
|
+
return this._container;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Base generator class with common utilities
|
|
249
|
+
*/
|
|
250
|
+
class BaseGenerator {
|
|
251
|
+
constructor(options = {}) {
|
|
252
|
+
this.name = options.name || this.constructor.name;
|
|
253
|
+
this.placeholders = [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Register placeholders - override in subclass
|
|
258
|
+
* @param {PlaceholderRegistry} registry
|
|
259
|
+
*/
|
|
260
|
+
register(registry) {
|
|
261
|
+
// Subclasses should override this method
|
|
262
|
+
for (const placeholder of this.placeholders) {
|
|
263
|
+
registry.register(placeholder.name, placeholder.resolver, placeholder.config);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Generate content - override in subclass
|
|
269
|
+
* @param {Object} context
|
|
270
|
+
* @returns {Promise<void>}
|
|
271
|
+
*/
|
|
272
|
+
async generate(context) {
|
|
273
|
+
throw new Error('Subclass must implement generate(context)');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Helper to add a placeholder definition
|
|
278
|
+
* @param {string} name - Placeholder name
|
|
279
|
+
* @param {Function} resolver - Resolver function
|
|
280
|
+
* @param {Object} [config] - Configuration
|
|
281
|
+
*/
|
|
282
|
+
addPlaceholder(name, resolver, config = {}) {
|
|
283
|
+
this.placeholders.push({ name, resolver, config });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Create a factory with standard generators
|
|
289
|
+
* @param {Object} options - Factory options
|
|
290
|
+
* @returns {GeneratorFactory}
|
|
291
|
+
*/
|
|
292
|
+
function createGeneratorFactory(options = {}) {
|
|
293
|
+
const factory = new GeneratorFactory(options);
|
|
294
|
+
|
|
295
|
+
// Register standard generators if provided
|
|
296
|
+
if (options.generators) {
|
|
297
|
+
for (const [name, generator] of Object.entries(options.generators)) {
|
|
298
|
+
factory.registerGenerator(name, generator);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return factory;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Singleton instance
|
|
306
|
+
let _factoryInstance = null;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get singleton factory instance
|
|
310
|
+
* @param {Object} [options] - Factory options
|
|
311
|
+
* @returns {GeneratorFactory}
|
|
312
|
+
*/
|
|
313
|
+
function getGeneratorFactory(options = {}) {
|
|
314
|
+
if (!_factoryInstance || options.forceNew) {
|
|
315
|
+
_factoryInstance = createGeneratorFactory(options);
|
|
316
|
+
}
|
|
317
|
+
return _factoryInstance;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Reset singleton (for testing)
|
|
322
|
+
*/
|
|
323
|
+
function resetGeneratorFactory() {
|
|
324
|
+
_factoryInstance = null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = {
|
|
328
|
+
GeneratorFactory,
|
|
329
|
+
BaseGenerator,
|
|
330
|
+
createGeneratorFactory,
|
|
331
|
+
getGeneratorFactory,
|
|
332
|
+
resetGeneratorFactory,
|
|
333
|
+
};
|