agileflow 2.79.0 → 2.81.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.
@@ -12,40 +12,13 @@
12
12
  * Usage: Configured as PreToolUse hook in .claude/settings.json
13
13
  */
14
14
 
15
- const fs = require('fs');
16
- const path = require('path');
17
- const os = require('os');
18
-
19
- // Color codes for output
20
- const c = {
21
- red: '\x1b[38;5;203m',
22
- reset: '\x1b[0m',
23
- dim: '\x1b[2m'
24
- };
25
-
26
- /**
27
- * Find project root by looking for .agileflow directory
28
- */
29
- function findProjectRoot() {
30
- let dir = process.cwd();
31
- while (dir !== '/') {
32
- if (fs.existsSync(path.join(dir, '.agileflow'))) {
33
- return dir;
34
- }
35
- dir = path.dirname(dir);
36
- }
37
- return process.cwd();
38
- }
39
-
40
- /**
41
- * Expand ~ to home directory
42
- */
43
- function expandPath(p) {
44
- if (p.startsWith('~/')) {
45
- return path.join(os.homedir(), p.slice(2));
46
- }
47
- return p;
48
- }
15
+ const {
16
+ findProjectRoot,
17
+ loadPatterns,
18
+ pathMatches,
19
+ outputBlocked,
20
+ runDamageControlHook,
21
+ } = require('./lib/damage-control-utils');
49
22
 
50
23
  /**
51
24
  * Parse simplified YAML for path patterns
@@ -54,7 +27,7 @@ function parseSimpleYAML(content) {
54
27
  const config = {
55
28
  zeroAccessPaths: [],
56
29
  readOnlyPaths: [],
57
- noDeletePaths: []
30
+ noDeletePaths: [],
58
31
  };
59
32
 
60
33
  let currentSection = null;
@@ -85,79 +58,6 @@ function parseSimpleYAML(content) {
85
58
  return config;
86
59
  }
87
60
 
88
- /**
89
- * Load patterns configuration from YAML file
90
- */
91
- function loadPatterns(projectRoot) {
92
- const configPaths = [
93
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
94
- path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
95
- path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml')
96
- ];
97
-
98
- for (const configPath of configPaths) {
99
- if (fs.existsSync(configPath)) {
100
- try {
101
- const content = fs.readFileSync(configPath, 'utf8');
102
- return parseSimpleYAML(content);
103
- } catch (e) {
104
- // Continue to next path
105
- }
106
- }
107
- }
108
-
109
- // Return empty config if no file found (fail-open)
110
- return { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
111
- }
112
-
113
- /**
114
- * Check if a file path matches any of the protected patterns
115
- */
116
- function pathMatches(filePath, patterns) {
117
- if (!filePath) return null;
118
-
119
- const normalizedPath = path.resolve(filePath);
120
- const relativePath = path.relative(process.cwd(), normalizedPath);
121
-
122
- for (const pattern of patterns) {
123
- const expandedPattern = expandPath(pattern);
124
-
125
- // Check if pattern is a directory prefix
126
- if (pattern.endsWith('/')) {
127
- const patternDir = expandedPattern.slice(0, -1);
128
- if (normalizedPath.startsWith(patternDir)) {
129
- return pattern;
130
- }
131
- }
132
-
133
- // Check exact match
134
- if (normalizedPath === expandedPattern) {
135
- return pattern;
136
- }
137
-
138
- // Check if normalized path ends with pattern (for filenames like "id_rsa")
139
- if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
140
- return pattern;
141
- }
142
-
143
- // Check if pattern appears in path (for patterns like "*.pem")
144
- if (pattern.startsWith('*')) {
145
- const ext = pattern.slice(1);
146
- if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
147
- return pattern;
148
- }
149
- }
150
-
151
- // Check if path contains pattern (for things like ".env.production")
152
- const patternBase = path.basename(pattern);
153
- if (path.basename(normalizedPath) === patternBase) {
154
- return pattern;
155
- }
156
- }
157
-
158
- return null;
159
- }
160
-
161
61
  /**
162
62
  * Validate file path for write operation
163
63
  */
@@ -168,7 +68,7 @@ function validatePath(filePath, config) {
168
68
  return {
169
69
  action: 'block',
170
70
  reason: `Zero-access path: ${zeroMatch}`,
171
- detail: 'This file is protected and cannot be accessed'
71
+ detail: 'This file is protected and cannot be accessed',
172
72
  };
173
73
  }
174
74
 
@@ -178,7 +78,7 @@ function validatePath(filePath, config) {
178
78
  return {
179
79
  action: 'block',
180
80
  reason: `Read-only path: ${readOnlyMatch}`,
181
- detail: 'This file is read-only and cannot be written to'
81
+ detail: 'This file is read-only and cannot be written to',
182
82
  };
183
83
  }
184
84
 
@@ -186,58 +86,15 @@ function validatePath(filePath, config) {
186
86
  return { action: 'allow' };
187
87
  }
188
88
 
189
- /**
190
- * Main function - read input and validate
191
- */
192
- function main() {
193
- const projectRoot = findProjectRoot();
194
- let inputData = '';
195
-
196
- process.stdin.setEncoding('utf8');
197
-
198
- process.stdin.on('data', chunk => {
199
- inputData += chunk;
200
- });
201
-
202
- process.stdin.on('end', () => {
203
- try {
204
- // Parse tool input from Claude Code
205
- const input = JSON.parse(inputData);
206
- const filePath = input.file_path || input.tool_input?.file_path || '';
207
-
208
- if (!filePath) {
209
- // No path to validate - allow
210
- process.exit(0);
211
- }
212
-
213
- // Load patterns and validate
214
- const config = loadPatterns(projectRoot);
215
- const result = validatePath(filePath, config);
216
-
217
- if (result.action === 'block') {
218
- console.error(`${c.red}[BLOCKED]${c.reset} ${result.reason}`);
219
- console.error(`${c.dim}${result.detail}${c.reset}`);
220
- console.error(`${c.dim}File: ${filePath}${c.reset}`);
221
- process.exit(2);
222
- }
223
-
224
- // Allow
225
- process.exit(0);
226
- } catch (e) {
227
- // Parse error or other issue - fail open
228
- process.exit(0);
229
- }
230
- });
231
-
232
- // Handle no stdin
233
- process.stdin.on('error', () => {
234
- process.exit(0);
235
- });
236
-
237
- // Set timeout to prevent hanging
238
- setTimeout(() => {
239
- process.exit(0);
240
- }, 4000);
241
- }
242
-
243
- main();
89
+ // Run the hook
90
+ const projectRoot = findProjectRoot();
91
+ const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
92
+
93
+ runDamageControlHook({
94
+ getInputValue: input => input.file_path || input.tool_input?.file_path,
95
+ loadConfig: () => loadPatterns(projectRoot, parseSimpleYAML, defaultConfig),
96
+ validate: validatePath,
97
+ onBlock: (result, filePath) => {
98
+ outputBlocked(result.reason, result.detail, `File: ${filePath}`);
99
+ },
100
+ });
@@ -156,12 +156,12 @@ function formatOutput(info, asJson = false, compact = false) {
156
156
  brand: '\x1b[38;2;232;104;58m', // #e8683a - AgileFlow brand orange
157
157
 
158
158
  // Vibrant 256-color palette (modern, sleek look)
159
- mintGreen: '\x1b[38;5;158m', // Healthy/success states
160
- peach: '\x1b[38;5;215m', // Warning states
161
- coral: '\x1b[38;5;203m', // Critical/error states
162
- lightGreen: '\x1b[38;5;194m', // Session healthy
163
- skyBlue: '\x1b[38;5;117m', // Directories/paths
164
- lavender: '\x1b[38;5;147m', // Model info
159
+ mintGreen: '\x1b[38;5;158m', // Healthy/success states
160
+ peach: '\x1b[38;5;215m', // Warning states
161
+ coral: '\x1b[38;5;203m', // Critical/error states
162
+ lightGreen: '\x1b[38;5;194m', // Session healthy
163
+ skyBlue: '\x1b[38;5;117m', // Directories/paths
164
+ lavender: '\x1b[38;5;147m', // Model info
165
165
  };
166
166
 
167
167
  // Beautiful compact colorful format (using vibrant 256-color palette)
@@ -0,0 +1,251 @@
1
+ /**
2
+ * damage-control-utils.js - Shared utilities for damage-control hooks
3
+ *
4
+ * IMPORTANT: These scripts must FAIL OPEN (exit 0 on error)
5
+ * to avoid blocking users when config is broken.
6
+ *
7
+ * This module is copied to .agileflow/scripts/lib/ at install time
8
+ * and used by damage-control-bash.js, damage-control-edit.js, damage-control-write.js
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ // Inline colors (no external dependency - keeps scripts standalone)
16
+ const c = {
17
+ coral: '\x1b[38;5;203m',
18
+ dim: '\x1b[2m',
19
+ reset: '\x1b[0m',
20
+ };
21
+
22
+ // Shared constants
23
+ const CONFIG_PATHS = [
24
+ '.agileflow/config/damage-control-patterns.yaml',
25
+ '.agileflow/config/damage-control-patterns.yml',
26
+ '.agileflow/templates/damage-control-patterns.yaml',
27
+ ];
28
+
29
+ const STDIN_TIMEOUT_MS = 4000;
30
+
31
+ /**
32
+ * Find project root by looking for .agileflow directory
33
+ * @returns {string} Project root path or current working directory
34
+ */
35
+ function findProjectRoot() {
36
+ let dir = process.cwd();
37
+ while (dir !== '/') {
38
+ if (fs.existsSync(path.join(dir, '.agileflow'))) {
39
+ return dir;
40
+ }
41
+ dir = path.dirname(dir);
42
+ }
43
+ return process.cwd();
44
+ }
45
+
46
+ /**
47
+ * Expand ~ to home directory
48
+ * @param {string} p - Path that may start with ~/
49
+ * @returns {string} Expanded path
50
+ */
51
+ function expandPath(p) {
52
+ if (p.startsWith('~/')) {
53
+ return path.join(os.homedir(), p.slice(2));
54
+ }
55
+ return p;
56
+ }
57
+
58
+ /**
59
+ * Load patterns configuration from YAML file
60
+ * Returns empty config if not found (fail-open)
61
+ *
62
+ * @param {string} projectRoot - Project root directory
63
+ * @param {function} parseYAML - Function to parse YAML content
64
+ * @param {object} defaultConfig - Default config if no file found
65
+ * @returns {object} Parsed configuration
66
+ */
67
+ function loadPatterns(projectRoot, parseYAML, defaultConfig = {}) {
68
+ for (const configPath of CONFIG_PATHS) {
69
+ const fullPath = path.join(projectRoot, configPath);
70
+ if (fs.existsSync(fullPath)) {
71
+ try {
72
+ const content = fs.readFileSync(fullPath, 'utf8');
73
+ return parseYAML(content);
74
+ } catch (e) {
75
+ // Continue to next path
76
+ }
77
+ }
78
+ }
79
+
80
+ // Return empty config if no file found (fail-open)
81
+ return defaultConfig;
82
+ }
83
+
84
+ /**
85
+ * Check if a file path matches any of the protected patterns
86
+ *
87
+ * @param {string} filePath - File path to check
88
+ * @param {string[]} patterns - Array of patterns to match against
89
+ * @returns {string|null} Matched pattern or null
90
+ */
91
+ function pathMatches(filePath, patterns) {
92
+ if (!filePath) return null;
93
+
94
+ const normalizedPath = path.resolve(filePath);
95
+ const relativePath = path.relative(process.cwd(), normalizedPath);
96
+
97
+ for (const pattern of patterns) {
98
+ const expandedPattern = expandPath(pattern);
99
+
100
+ // Check if pattern is a directory prefix
101
+ if (pattern.endsWith('/')) {
102
+ const patternDir = expandedPattern.slice(0, -1);
103
+ if (normalizedPath.startsWith(patternDir)) {
104
+ return pattern;
105
+ }
106
+ }
107
+
108
+ // Check exact match
109
+ if (normalizedPath === expandedPattern) {
110
+ return pattern;
111
+ }
112
+
113
+ // Check if normalized path ends with pattern (for filenames like "id_rsa")
114
+ if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
115
+ return pattern;
116
+ }
117
+
118
+ // Check if pattern appears in path (for patterns like "*.pem")
119
+ if (pattern.startsWith('*')) {
120
+ const ext = pattern.slice(1);
121
+ if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
122
+ return pattern;
123
+ }
124
+ }
125
+
126
+ // Check if path contains pattern (for things like ".env.production")
127
+ const patternBase = path.basename(pattern);
128
+ if (path.basename(normalizedPath) === patternBase) {
129
+ return pattern;
130
+ }
131
+ }
132
+
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Output blocked message to stderr
138
+ *
139
+ * @param {string} reason - Main reason for blocking
140
+ * @param {string} [detail] - Additional detail
141
+ * @param {string} [context] - Context info (file path or command)
142
+ */
143
+ function outputBlocked(reason, detail, context) {
144
+ console.error(`${c.coral}[BLOCKED]${c.reset} ${reason}`);
145
+ if (detail) {
146
+ console.error(`${c.dim}${detail}${c.reset}`);
147
+ }
148
+ if (context) {
149
+ console.error(`${c.dim}${context}${c.reset}`);
150
+ }
151
+ // Help message for AI and user
152
+ console.error('');
153
+ console.error(
154
+ `${c.dim}This is intentional - AgileFlow Damage Control blocked a potentially dangerous operation.${c.reset}`
155
+ );
156
+ console.error(
157
+ `${c.dim}DO NOT retry this command. Ask the user if they want to proceed manually.${c.reset}`
158
+ );
159
+ console.error(`${c.dim}To disable: run /configure → Infrastructure → Damage Control${c.reset}`);
160
+ }
161
+
162
+ /**
163
+ * Run damage control hook with stdin parsing
164
+ * Handles common error cases and timeout
165
+ *
166
+ * @param {object} options - Hook options
167
+ * @param {function} options.getInputValue - Extract value from parsed input (input) => value
168
+ * @param {function} options.loadConfig - Load configuration () => config
169
+ * @param {function} options.validate - Validate input (value, config) => { action, reason, detail? }
170
+ * @param {function} options.onBlock - Handle blocked result (result, value) => void
171
+ * @param {function} [options.onAsk] - Handle ask result (result, value) => void (optional)
172
+ */
173
+ function runDamageControlHook(options) {
174
+ const { getInputValue, loadConfig, validate, onBlock, onAsk } = options;
175
+
176
+ let inputData = '';
177
+
178
+ process.stdin.setEncoding('utf8');
179
+
180
+ process.stdin.on('data', chunk => {
181
+ inputData += chunk;
182
+ });
183
+
184
+ process.stdin.on('end', () => {
185
+ try {
186
+ // Parse tool input from Claude Code
187
+ const input = JSON.parse(inputData);
188
+ const value = getInputValue(input);
189
+
190
+ if (!value) {
191
+ // No value to validate - allow
192
+ process.exit(0);
193
+ }
194
+
195
+ // Load patterns and validate
196
+ const config = loadConfig();
197
+ const result = validate(value, config);
198
+
199
+ switch (result.action) {
200
+ case 'block':
201
+ onBlock(result, value);
202
+ process.exit(2);
203
+ break;
204
+
205
+ case 'ask':
206
+ if (onAsk) {
207
+ onAsk(result, value);
208
+ } else {
209
+ // Default ask behavior - output JSON
210
+ console.log(
211
+ JSON.stringify({
212
+ result: 'ask',
213
+ message: result.reason,
214
+ })
215
+ );
216
+ }
217
+ process.exit(0);
218
+ break;
219
+
220
+ case 'allow':
221
+ default:
222
+ process.exit(0);
223
+ }
224
+ } catch (e) {
225
+ // Parse error or other issue - fail open
226
+ process.exit(0);
227
+ }
228
+ });
229
+
230
+ // Handle no stdin (direct invocation)
231
+ process.stdin.on('error', () => {
232
+ process.exit(0);
233
+ });
234
+
235
+ // Set timeout to prevent hanging
236
+ setTimeout(() => {
237
+ process.exit(0);
238
+ }, STDIN_TIMEOUT_MS);
239
+ }
240
+
241
+ module.exports = {
242
+ c,
243
+ findProjectRoot,
244
+ expandPath,
245
+ loadPatterns,
246
+ pathMatches,
247
+ outputBlocked,
248
+ runDamageControlHook,
249
+ CONFIG_PATHS,
250
+ STDIN_TIMEOUT_MS,
251
+ };